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

# Backport

This will backport the following commits from `main` to `8.18`:
- [[Feature flags example] Apply FF naming conventions
(#196535)](https://github.com/elastic/kibana/pull/196535)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Alejandro Fernández
Haro","email":"alejandro.haro@elastic.co"},"sourceCommit":{"committedDate":"2024-10-17T09:34:43Z","message":"[Feature
flags example] Apply FF naming conventions
(#196535)","sha":"f25b3be1944bb88d000e2d325ca7aeea8511a9ce","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Core","release_note:skip","v9.0.0","backport:prev-major","v9.1.0"],"title":"[Feature
flags example] Apply FF naming
conventions","number":196535,"url":"https://github.com/elastic/kibana/pull/196535","mergeCommit":{"message":"[Feature
flags example] Apply FF naming conventions
(#196535)","sha":"f25b3be1944bb88d000e2d325ca7aeea8511a9ce"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196535","number":196535,"mergeCommit":{"message":"[Feature
flags example] Apply FF naming conventions
(#196535)","sha":"f25b3be1944bb88d000e2d325ca7aeea8511a9ce"}},{"branch":"9.1","label":"v9.1.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Alejandro Fernández Haro 2025-04-21 15:00:33 +02:00 committed by GitHub
parent 946a98671f
commit 06b5855b1e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 50 additions and 12 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)
@ -28,7 +34,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'],
@ -114,7 +120,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),
},
});
}
@ -138,7 +144,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

@ -244,7 +244,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();
@ -344,5 +348,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

@ -27,6 +27,8 @@ describe('FeatureFlagsService Server', () => {
atPath: {
overrides: {
'my-overridden-flag': true,
'myPlugin.myOverriddenFlag': true,
myDestructuredObjPlugin: { myOverriddenFlag: true },
},
},
}),
@ -253,10 +255,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 { createOpenFeatureLogger } from './create_open_feature_logger';
import { setProviderWithRetries } from './set_provider_with_retries';
import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
@ -167,9 +168,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 });