Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature flags example] Apply FF naming conventions #196535

Merged
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