mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# 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:
parent
946a98671f
commit
06b5855b1e
5 changed files with 50 additions and 12 deletions
|
@ -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';
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue