Skip to content

Commit

Permalink
[Feature flags example] Apply FF naming conventions (elastic#196535)
Browse files Browse the repository at this point in the history
  • Loading branch information
afharo authored Oct 17, 2024
1 parent bad11ab commit f25b3be
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 14 deletions.
6 changes: 3 additions & 3 deletions examples/feature_flags_example/common/feature_flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
16 changes: 11 additions & 5 deletions packages/core/feature-flags/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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']
---

Expand All @@ -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)

Expand All @@ -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'],
Expand Down Expand Up @@ -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),
},
});
}
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ describe('FeatureFlagsService Server', () => {
atPath: {
overrides: {
'my-overridden-flag': true,
'myPlugin.myOverriddenFlag': true,
myDestructuredObjPlugin: { myOverriddenFlag: true },
},
},
}),
Expand Down Expand Up @@ -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 },
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 });
Expand Down

0 comments on commit f25b3be

Please sign in to comment.