[Feature flags example] Apply FF naming conventions (#196535)

This commit is contained in:
Alejandro Fernández Haro 2024-10-17 11:34:43 +02:00 committed by GitHub
parent bad11abb07
commit f25b3be194
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 54 additions and 14 deletions

View file

@ -7,6 +7,6 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export const FeatureFlagExampleBoolean = 'example-boolean';
export const FeatureFlagExampleString = 'example-string';
export const FeatureFlagExampleNumber = 'example-number';
export const FeatureFlagExampleBoolean = 'featureFlagsExample.exampleBoolean';
export const FeatureFlagExampleString = 'featureFlagsExample.exampleString';
export const FeatureFlagExampleNumber = 'featureFlagsExample.exampleNumber';

View file

@ -3,7 +3,7 @@ id: kibFeatureFlagsService
slug: /kibana-dev-docs/tutorials/feature-flags-service
title: Feature Flags service
description: The Feature Flags service provides the necessary APIs to evaluate dynamic feature flags.
date: 2024-07-26
date: 2024-10-16
tags: ['kibana', 'dev', 'contributor', 'api docs', 'a/b testing', 'feature flags', 'flags']
---
@ -12,7 +12,13 @@ tags: ['kibana', 'dev', 'contributor', 'api docs', 'a/b testing', 'feature flags
The Feature Flags service provides the necessary APIs to evaluate dynamic feature flags.
The service is always enabled, however, it will return the fallback value if a feature flags provider hasn't been attached.
Kibana only registers a provider when running on Elastic Cloud Hosted/Serverless.
Kibana only registers a provider when running on Elastic Cloud Hosted/Serverless. And even in those scenarios, we expect that some customers might
have network restrictions that might not allow the flags to evaluate. The fallback value must provide a non-broken experience to users.
:warning: Feature Flags are considered dynamic configuration and cannot be used for settings that require restarting Kibana.
One example of invalid use cases are settings used during the `setup` lifecycle of the plugin, such as settings that define
if an HTTP route is registered or not. Instead, you should always register the route, and return `404 - Not found` in the route
handler if the feature flag returns a _disabled_ state.
For a code example, refer to the [Feature Flags Example plugin](../../../examples/feature_flags_example)
@ -32,7 +38,7 @@ import type { PluginInitializerContext } from '@kbn/core-plugins-server';
export const featureFlags: FeatureFlagDefinitions = [
{
key: 'my-cool-feature',
key: 'myPlugin.myCoolFeature',
name: 'My cool feature',
description: 'Enables the cool feature to auto-hide the navigation bar',
tags: ['my-plugin', 'my-service', 'ui'],
@ -118,7 +124,7 @@ async (context, request, response) => {
const { featureFlags } = await context.core;
return response.ok({
body: {
number: await featureFlags.getNumberValue('example-number', 1),
number: await featureFlags.getNumberValue('myPlugin.exampleNumber', 1),
},
});
}
@ -142,7 +148,7 @@ provider. In the `kibana.yml`, the following config sets the overrides:
```yaml
feature_flags.overrides:
my-feature-flag: 'my-forced-value'
myPlugin.myFeatureFlag: 'my-forced-value'
```
> [!WARNING]

View file

@ -188,7 +188,11 @@ describe('FeatureFlagsService Browser', () => {
beforeEach(async () => {
addHandlerSpy = jest.spyOn(featureFlagsClient, 'addHandler');
injectedMetadata.getFeatureFlags.mockReturnValue({
overrides: { 'my-overridden-flag': true },
overrides: {
'my-overridden-flag': true,
'myPlugin.myOverriddenFlag': true,
myDestructuredObjPlugin: { myOverriddenFlag: true },
},
});
featureFlagsService.setup({ injectedMetadata });
startContract = await featureFlagsService.start();
@ -288,5 +292,14 @@ describe('FeatureFlagsService Browser', () => {
expect(getBooleanValueSpy).toHaveBeenCalledTimes(1);
expect(getBooleanValueSpy).toHaveBeenCalledWith('another-flag', false);
});
test('overrides with dotted names', async () => {
const getBooleanValueSpy = jest.spyOn(featureFlagsClient, 'getBooleanValue');
expect(startContract.getBooleanValue('myPlugin.myOverriddenFlag', false)).toEqual(true);
expect(
startContract.getBooleanValue('myDestructuredObjPlugin.myOverriddenFlag', false)
).toEqual(true);
expect(getBooleanValueSpy).not.toHaveBeenCalled();
});
});
});

View file

@ -20,6 +20,7 @@ import { apm } from '@elastic/apm-rum';
import { type Client, ClientProviderEvents, OpenFeature } from '@openfeature/web-sdk';
import deepMerge from 'deepmerge';
import { filter, map, startWith, Subject } from 'rxjs';
import { get } from 'lodash';
/**
* setup method dependencies
@ -172,9 +173,10 @@ export class FeatureFlagsService {
flagName: string,
fallbackValue: T
): T {
const override = get(this.overrides, flagName); // using lodash get because flagName can come with dots and the config parser might structure it in objects.
const value =
typeof this.overrides[flagName] !== 'undefined'
? (this.overrides[flagName] as T)
typeof override !== 'undefined'
? (override as T)
: // We have to bind the evaluation or the client will lose its internal context
evaluationFn.bind(this.featureFlagsClient)(flagName, fallbackValue);
apm.addLabels({ [`flag_${flagName}`]: value });

View file

@ -27,6 +27,8 @@ describe('FeatureFlagsService Server', () => {
atPath: {
overrides: {
'my-overridden-flag': true,
'myPlugin.myOverriddenFlag': true,
myDestructuredObjPlugin: { myOverriddenFlag: true },
},
},
}),
@ -251,10 +253,25 @@ describe('FeatureFlagsService Server', () => {
expect(getBooleanValueSpy).toHaveBeenCalledTimes(1);
expect(getBooleanValueSpy).toHaveBeenCalledWith('another-flag', false);
});
test('overrides with dotted names', async () => {
const getBooleanValueSpy = jest.spyOn(featureFlagsClient, 'getBooleanValue');
await expect(
startContract.getBooleanValue('myPlugin.myOverriddenFlag', false)
).resolves.toEqual(true);
await expect(
startContract.getBooleanValue('myDestructuredObjPlugin.myOverriddenFlag', false)
).resolves.toEqual(true);
expect(getBooleanValueSpy).not.toHaveBeenCalled();
});
});
test('returns overrides', () => {
const { getOverrides } = featureFlagsService.setup();
expect(getOverrides()).toStrictEqual({ 'my-overridden-flag': true });
expect(getOverrides()).toStrictEqual({
'my-overridden-flag': true,
'myPlugin.myOverriddenFlag': true,
myDestructuredObjPlugin: { myOverriddenFlag: true },
});
});
});

View file

@ -24,6 +24,7 @@ import {
} from '@openfeature/server-sdk';
import deepMerge from 'deepmerge';
import { filter, switchMap, startWith, Subject } from 'rxjs';
import { get } from 'lodash';
import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
/**
@ -165,9 +166,10 @@ export class FeatureFlagsService {
flagName: string,
fallbackValue: T
): Promise<T> {
const override = get(this.overrides, flagName); // using lodash get because flagName can come with dots and the config parser might structure it in objects.
const value =
typeof this.overrides[flagName] !== 'undefined'
? (this.overrides[flagName] as T)
typeof override !== 'undefined'
? (override as T)
: // We have to bind the evaluation or the client will lose its internal context
await evaluationFn.bind(this.featureFlagsClient)(flagName, fallbackValue);
apm.addLabels({ [`flag_${flagName}`]: value });