mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Feature Flags] Client-side bootstrapping (#224258)
This commit is contained in:
parent
8d1204bb5f
commit
fd7f85149c
24 changed files with 348 additions and 18 deletions
|
@ -249,6 +249,7 @@ describe('FeatureFlagsService Browser', () => {
|
|||
'myPlugin.myOverriddenFlag': true,
|
||||
myDestructuredObjPlugin: { myOverriddenFlag: true },
|
||||
},
|
||||
initialFeatureFlags: {},
|
||||
});
|
||||
featureFlagsService.setup({ injectedMetadata });
|
||||
startContract = await featureFlagsService.start();
|
||||
|
|
|
@ -24,7 +24,7 @@ import { get } from 'lodash';
|
|||
|
||||
/**
|
||||
* setup method dependencies
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
export interface FeatureFlagsSetupDeps {
|
||||
/**
|
||||
|
@ -35,7 +35,7 @@ export interface FeatureFlagsSetupDeps {
|
|||
|
||||
/**
|
||||
* The browser-side Feature Flags Service
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
export class FeatureFlagsService {
|
||||
private readonly featureFlagsClient: Client;
|
||||
|
@ -64,6 +64,7 @@ export class FeatureFlagsService {
|
|||
this.overrides = featureFlagsInjectedMetadata.overrides;
|
||||
}
|
||||
return {
|
||||
getInitialFeatureFlags: () => featureFlagsInjectedMetadata?.initialFeatureFlags ?? {},
|
||||
setProvider: (provider) => {
|
||||
if (this.isProviderReadyPromise) {
|
||||
throw new Error('A provider has already been set. This API cannot be called twice.');
|
||||
|
@ -159,7 +160,7 @@ export class FeatureFlagsService {
|
|||
|
||||
/**
|
||||
* Waits for the provider initialization with a timeout to avoid holding the page load for too long
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
private async waitForProviderInitialization() {
|
||||
// Adding a timeout here to avoid hanging the start for too long if the provider is unresponsive
|
||||
|
@ -185,7 +186,7 @@ export class FeatureFlagsService {
|
|||
* @param evaluationFn The actual evaluation API
|
||||
* @param flagName The name of the flag to evaluate
|
||||
* @param fallbackValue The fallback value
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
private evaluateFlag<T extends string | boolean | number>(
|
||||
evaluationFn: (flagName: string, fallbackValue: T) => T,
|
||||
|
@ -206,7 +207,7 @@ export class FeatureFlagsService {
|
|||
/**
|
||||
* Formats the provided context to fulfill the expected multi-context structure.
|
||||
* @param contextToAppend The {@link EvaluationContext} to append.
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
private async appendContext(contextToAppend: EvaluationContext): Promise<void> {
|
||||
// If no kind provided, default to the project|deployment level.
|
||||
|
|
|
@ -14,6 +14,7 @@ import { of } from 'rxjs';
|
|||
|
||||
const createFeatureFlagsSetup = (): jest.Mocked<FeatureFlagsSetup> => {
|
||||
return {
|
||||
getInitialFeatureFlags: jest.fn().mockImplementation(() => ({})),
|
||||
setProvider: jest.fn(),
|
||||
appendContext: jest.fn().mockImplementation(Promise.resolve),
|
||||
};
|
||||
|
|
|
@ -87,6 +87,12 @@ export type SingleContextEvaluationContext = OpenFeatureEvaluationContext & {
|
|||
* @public
|
||||
*/
|
||||
export interface FeatureFlagsSetup {
|
||||
/**
|
||||
* Used for bootstrapping the browser-side client with a seed of the feature flags for faster load-times.
|
||||
* @remarks It shouldn't be used to evaluate the feature flags because it won't report usage.
|
||||
*/
|
||||
getInitialFeatureFlags: () => Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Registers an OpenFeature provider to talk to the
|
||||
* 3rd-party service that manages the Feature Flags.
|
||||
|
|
|
@ -12,7 +12,7 @@ import { schema } from '@kbn/config-schema';
|
|||
|
||||
/**
|
||||
* The definition of the validation config schema
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
const configSchema = schema.object({
|
||||
overrides: schema.maybe(schema.recordOf(schema.string(), schema.any())),
|
||||
|
@ -20,7 +20,7 @@ const configSchema = schema.object({
|
|||
|
||||
/**
|
||||
* Type definition of the Feature Flags configuration
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
export interface FeatureFlagsConfig {
|
||||
overrides?: Record<string, unknown>;
|
||||
|
@ -28,7 +28,7 @@ export interface FeatureFlagsConfig {
|
|||
|
||||
/**
|
||||
* Config descriptor for the feature flags service
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
export const featureFlagsConfig: ServiceConfigDescriptor<FeatureFlagsConfig> = {
|
||||
/**
|
||||
|
|
|
@ -320,4 +320,19 @@ describe('FeatureFlagsService Server', () => {
|
|||
'myDestructuredObjPlugin.myOverriddenFlag': true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('bootstrapping helpers', () => {
|
||||
test('return empty initial feature flags if no getter registered', async () => {
|
||||
const { getInitialFeatureFlags } = featureFlagsService.setup();
|
||||
await expect(getInitialFeatureFlags()).resolves.toEqual({});
|
||||
});
|
||||
|
||||
test('calls the getter when registered', async () => {
|
||||
const { setInitialFeatureFlagsGetter, getInitialFeatureFlags } = featureFlagsService.setup();
|
||||
const mockGetter = jest.fn().mockResolvedValue({ myFlag: true });
|
||||
setInitialFeatureFlagsGetter(mockGetter);
|
||||
await expect(getInitialFeatureFlags()).resolves.toEqual({ myFlag: true });
|
||||
expect(mockGetter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,24 +26,30 @@ import {
|
|||
import deepMerge from 'deepmerge';
|
||||
import { filter, switchMap, startWith, Subject, BehaviorSubject, pairwise, takeUntil } from 'rxjs';
|
||||
import { get } from 'lodash';
|
||||
import type { InitialFeatureFlagsGetter } from '@kbn/core-feature-flags-server/src/contracts';
|
||||
import { createOpenFeatureLogger } from './create_open_feature_logger';
|
||||
import { setProviderWithRetries } from './set_provider_with_retries';
|
||||
import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
|
||||
|
||||
/**
|
||||
* Core-internal contract for the setup lifecycle step.
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
export interface InternalFeatureFlagsSetup extends FeatureFlagsSetup {
|
||||
/**
|
||||
* Used by the rendering service to share the overrides with the service on the browser side.
|
||||
*/
|
||||
getOverrides: () => Record<string, unknown>;
|
||||
/**
|
||||
* Required to bootstrap the browser-side OpenFeature client with a seed of the feature flags for faster load-times
|
||||
* and to work-around air-gapped environments.
|
||||
*/
|
||||
getInitialFeatureFlags: () => Promise<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server-side Feature Flags Service
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
export class FeatureFlagsService {
|
||||
private readonly featureFlagsClient: Client;
|
||||
|
@ -51,6 +57,7 @@ export class FeatureFlagsService {
|
|||
private readonly stop$ = new Subject<void>();
|
||||
private readonly overrides$ = new BehaviorSubject<Record<string, unknown>>({});
|
||||
private context: MultiContextEvaluationContext = { kind: 'multi' };
|
||||
private initialFeatureFlagsGetter: InitialFeatureFlagsGetter = async () => ({});
|
||||
|
||||
/**
|
||||
* The core service's constructor
|
||||
|
@ -77,6 +84,10 @@ export class FeatureFlagsService {
|
|||
|
||||
return {
|
||||
getOverrides: () => this.overrides$.value,
|
||||
getInitialFeatureFlags: () => this.initialFeatureFlagsGetter(),
|
||||
setInitialFeatureFlagsGetter: (getter: InitialFeatureFlagsGetter) => {
|
||||
this.initialFeatureFlagsGetter = getter;
|
||||
},
|
||||
setProvider: (provider) => {
|
||||
if (OpenFeature.providerMetadata !== NOOP_PROVIDER.metadata) {
|
||||
throw new Error('A provider has already been set. This API cannot be called twice.');
|
||||
|
@ -175,7 +186,7 @@ export class FeatureFlagsService {
|
|||
* @param evaluationFn The actual evaluation API
|
||||
* @param flagName The name of the flag to evaluate
|
||||
* @param fallbackValue The fallback value
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
private async evaluateFlag<T extends string | boolean | number>(
|
||||
evaluationFn: (flagName: string, fallbackValue: T) => Promise<T>,
|
||||
|
@ -196,7 +207,7 @@ export class FeatureFlagsService {
|
|||
/**
|
||||
* Formats the provided context to fulfill the expected multi-context structure.
|
||||
* @param contextToAppend The {@link EvaluationContext} to append.
|
||||
* @private
|
||||
* @internal
|
||||
*/
|
||||
private appendContext(contextToAppend: EvaluationContext): void {
|
||||
// If no kind provided, default to the project|deployment level.
|
||||
|
|
|
@ -23,11 +23,13 @@ const createFeatureFlagsInternalSetup = (): jest.Mocked<InternalFeatureFlagsSetu
|
|||
return {
|
||||
...createFeatureFlagsSetup(),
|
||||
getOverrides: jest.fn().mockReturnValue({}),
|
||||
getInitialFeatureFlags: jest.fn().mockImplementation(async () => ({})),
|
||||
};
|
||||
};
|
||||
|
||||
const createFeatureFlagsSetup = (): jest.Mocked<FeatureFlagsSetup> => {
|
||||
return {
|
||||
setInitialFeatureFlagsGetter: jest.fn(),
|
||||
setProvider: jest.fn(),
|
||||
appendContext: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ export type {
|
|||
SingleContextEvaluationContext,
|
||||
FeatureFlagsSetup,
|
||||
FeatureFlagsStart,
|
||||
InitialFeatureFlagsGetter,
|
||||
} from './src/contracts';
|
||||
export type { FeatureFlagDefinition, FeatureFlagDefinitions } from './src/feature_flag_definition';
|
||||
export type { FeatureFlagsRequestHandlerContext } from './src/request_handler_context';
|
||||
|
|
|
@ -82,6 +82,12 @@ export type SingleContextEvaluationContext = OpenFeatureEvaluationContext & {
|
|||
kind?: 'organization' | 'kibana';
|
||||
};
|
||||
|
||||
/**
|
||||
* Getter function type to retrieve the initial feature flags.
|
||||
* @internal
|
||||
*/
|
||||
export type InitialFeatureFlagsGetter = () => Promise<Record<string, unknown>>;
|
||||
|
||||
/**
|
||||
* Setup contract of the Feature Flags Service
|
||||
* @public
|
||||
|
@ -101,6 +107,15 @@ export interface FeatureFlagsSetup {
|
|||
* @public
|
||||
*/
|
||||
appendContext(contextToAppend: EvaluationContext): void;
|
||||
|
||||
/**
|
||||
* Registers a getter function that will be used to retrieve the initial feature flags to be injected into the
|
||||
* browser for faster bootstrapping.
|
||||
* @param getter A function that returns all the feature flags and their values for this context.
|
||||
* Ideally, using the underlying API shouldn't track them as actual evaluations.
|
||||
* @internal
|
||||
*/
|
||||
setInitialFeatureFlagsGetter(getter: InitialFeatureFlagsGetter): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -170,12 +170,20 @@ describe('setup.getFeatureFlags()', () => {
|
|||
overrides: {
|
||||
'my-overridden-flag': 1234,
|
||||
},
|
||||
initialFeatureFlags: {
|
||||
'my-initial-flag': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as InjectedMetadataParams);
|
||||
|
||||
const contract = injectedMetadata.setup();
|
||||
expect(contract.getFeatureFlags()).toStrictEqual({ overrides: { 'my-overridden-flag': 1234 } });
|
||||
expect(contract.getFeatureFlags()).toStrictEqual({
|
||||
overrides: { 'my-overridden-flag': 1234 },
|
||||
initialFeatureFlags: {
|
||||
'my-initial-flag': true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty injectedMetadata.featureFlags', () => {
|
||||
|
|
|
@ -61,6 +61,7 @@ export interface InternalInjectedMetadataSetup {
|
|||
getFeatureFlags: () =>
|
||||
| {
|
||||
overrides: Record<string, unknown>;
|
||||
initialFeatureFlags: Record<string, unknown>;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ export interface InjectedMetadata {
|
|||
};
|
||||
featureFlags?: {
|
||||
overrides: Record<string, unknown>;
|
||||
initialFeatureFlags: Record<string, unknown>;
|
||||
};
|
||||
anonymousStatusPage: boolean;
|
||||
i18n: {
|
||||
|
|
|
@ -220,6 +220,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>({
|
|||
getAsLabels: deps.executionContext.getAsLabels,
|
||||
},
|
||||
featureFlags: {
|
||||
setInitialFeatureFlagsGetter: deps.featureFlags.setInitialFeatureFlagsGetter,
|
||||
setProvider: deps.featureFlags.setProvider,
|
||||
appendContext: deps.featureFlags.appendContext,
|
||||
},
|
||||
|
|
|
@ -40,6 +40,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -126,6 +127,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -208,6 +210,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -294,6 +297,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -376,6 +380,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -458,6 +463,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -544,6 +550,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -626,6 +633,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -708,6 +716,90 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
|
||||
},
|
||||
"legacyMetadata": Object {
|
||||
"globalUiSettings": Object {
|
||||
"defaults": Object {},
|
||||
"user": Object {},
|
||||
},
|
||||
"uiSettings": Object {
|
||||
"defaults": Object {
|
||||
"registered": Object {
|
||||
"name": "title",
|
||||
},
|
||||
},
|
||||
"user": Object {},
|
||||
},
|
||||
},
|
||||
"logging": Any<Object>,
|
||||
"publicBaseUrl": "http://myhost.com/mock-server-basepath",
|
||||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"name": "borealis",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
"version": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`RenderingService preboot() render() renders initial feature flags 1`] = `
|
||||
Object {
|
||||
"anonymousStatusPage": false,
|
||||
"apmConfig": Object {
|
||||
"stubApmConfig": true,
|
||||
},
|
||||
"assetsHrefBase": "http://foo.bar:1773",
|
||||
"basePath": "/mock-server-basepath",
|
||||
"branch": Any<String>,
|
||||
"buildNumber": Any<Number>,
|
||||
"clusterInfo": Object {},
|
||||
"csp": Object {
|
||||
"warnLegacyBrowsers": true,
|
||||
},
|
||||
"customBranding": Object {},
|
||||
"env": Object {
|
||||
"mode": Object {
|
||||
"dev": Any<Boolean>,
|
||||
"name": Any<String>,
|
||||
"prod": Any<Boolean>,
|
||||
},
|
||||
"packageInfo": Object {
|
||||
"branch": Any<String>,
|
||||
"buildDate": "2023-05-15T23:12:09.000Z",
|
||||
"buildFlavor": Any<String>,
|
||||
"buildNum": Any<Number>,
|
||||
"buildSha": Any<String>,
|
||||
"buildShaShort": "XXXXXX",
|
||||
"dist": Any<Boolean>,
|
||||
"version": Any<String>,
|
||||
},
|
||||
},
|
||||
"externalUrl": Object {
|
||||
"policy": Array [
|
||||
Object {
|
||||
"allow": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -795,6 +887,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -881,6 +974,9 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {
|
||||
"my-initial-flag": 1234,
|
||||
},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -968,6 +1064,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -1059,6 +1156,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -1141,6 +1239,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -1228,6 +1327,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -1319,6 +1419,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -1406,6 +1507,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
|
@ -1493,6 +1595,7 @@ Object {
|
|||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {},
|
||||
"overrides": Object {
|
||||
"my-overridden-flag": 1234,
|
||||
},
|
||||
|
@ -1536,3 +1639,93 @@ Object {
|
|||
"version": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`RenderingService setup() render() renders initial feature flags 1`] = `
|
||||
Object {
|
||||
"anonymousStatusPage": false,
|
||||
"apmConfig": Object {
|
||||
"stubApmConfig": true,
|
||||
},
|
||||
"assetsHrefBase": "/mock-server-basepath",
|
||||
"basePath": "/mock-server-basepath",
|
||||
"branch": Any<String>,
|
||||
"buildNumber": Any<Number>,
|
||||
"clusterInfo": Object {
|
||||
"cluster_build_flavor": "default",
|
||||
"cluster_name": "cluster-name",
|
||||
"cluster_uuid": "cluster-uuid",
|
||||
"cluster_version": "8.0.0",
|
||||
},
|
||||
"csp": Object {
|
||||
"warnLegacyBrowsers": true,
|
||||
},
|
||||
"customBranding": Object {},
|
||||
"env": Object {
|
||||
"mode": Object {
|
||||
"dev": Any<Boolean>,
|
||||
"name": Any<String>,
|
||||
"prod": Any<Boolean>,
|
||||
},
|
||||
"packageInfo": Object {
|
||||
"branch": Any<String>,
|
||||
"buildDate": "2023-05-15T23:12:09.000Z",
|
||||
"buildFlavor": Any<String>,
|
||||
"buildNum": Any<Number>,
|
||||
"buildSha": Any<String>,
|
||||
"buildShaShort": "XXXXXX",
|
||||
"dist": Any<Boolean>,
|
||||
"version": Any<String>,
|
||||
},
|
||||
},
|
||||
"externalUrl": Object {
|
||||
"policy": Array [
|
||||
Object {
|
||||
"allow": true,
|
||||
},
|
||||
],
|
||||
},
|
||||
"featureFlags": Object {
|
||||
"initialFeatureFlags": Object {
|
||||
"my-initial-flag": 1234,
|
||||
},
|
||||
"overrides": Object {},
|
||||
},
|
||||
"i18n": Object {
|
||||
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
|
||||
},
|
||||
"legacyMetadata": Object {
|
||||
"globalUiSettings": Object {
|
||||
"defaults": Object {},
|
||||
"user": Object {},
|
||||
},
|
||||
"uiSettings": Object {
|
||||
"defaults": Object {
|
||||
"registered": Object {
|
||||
"name": "title",
|
||||
},
|
||||
},
|
||||
"user": Object {},
|
||||
},
|
||||
},
|
||||
"logging": Any<Object>,
|
||||
"publicBaseUrl": "http://myhost.com/mock-server-basepath",
|
||||
"serverBasePath": "/mock-server-basepath",
|
||||
"theme": Object {
|
||||
"darkMode": "theme:darkMode",
|
||||
"name": "borealis",
|
||||
"stylesheetPaths": Object {
|
||||
"dark": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
"default": Array [
|
||||
"/style-1.css",
|
||||
"/style-2.css",
|
||||
],
|
||||
},
|
||||
"version": "v8",
|
||||
},
|
||||
"uiPlugins": Array [],
|
||||
"version": Any<String>,
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -265,6 +265,19 @@ function renderTestCases(
|
|||
expect(data).toMatchSnapshot(INJECTED_METADATA);
|
||||
});
|
||||
|
||||
it('renders initial feature flags', async () => {
|
||||
mockRenderingSetupDeps.featureFlags.getInitialFeatureFlags.mockResolvedValueOnce({
|
||||
'my-initial-flag': 1234,
|
||||
});
|
||||
const [render] = await getRender();
|
||||
const content = await render(createKibanaRequest(), uiSettings, {
|
||||
isAnonymousPage: false,
|
||||
});
|
||||
const dom = load(content);
|
||||
const data = JSON.parse(dom('kbn-injected-metadata').attr('data') ?? '""');
|
||||
expect(data).toMatchSnapshot(INJECTED_METADATA);
|
||||
});
|
||||
|
||||
it('renders "core" with logging config injected', async () => {
|
||||
const loggingConfig = {
|
||||
root: {
|
||||
|
|
|
@ -298,6 +298,7 @@ export class RenderingService {
|
|||
env,
|
||||
featureFlags: {
|
||||
overrides: featureFlags?.getOverrides() || {},
|
||||
initialFeatureFlags: (await featureFlags?.getInitialFeatureFlags()) || {},
|
||||
},
|
||||
clusterInfo,
|
||||
apmConfig,
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const LaunchDarklyClientProviderMocked = jest.fn();
|
||||
jest.doMock('@openfeature/launchdarkly-client-provider', () => {
|
||||
return {
|
||||
LaunchDarklyClientProvider: LaunchDarklyClientProviderMocked,
|
||||
};
|
||||
});
|
|
@ -9,6 +9,7 @@ import { duration } from 'moment';
|
|||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { LaunchDarklyClientProviderMocked } from './plugin.test.mocks';
|
||||
import { CloudExperimentsPlugin } from './plugin';
|
||||
import { MetadataService } from '../common/metadata_service';
|
||||
|
||||
|
@ -69,6 +70,32 @@ describe('Cloud Experiments public plugin', () => {
|
|||
})
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('leverages the injected initial feature flags when creating the new client', () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
coreSetup.featureFlags.getInitialFeatureFlags.mockReturnValue({
|
||||
'my-initial-flag': true,
|
||||
});
|
||||
plugin.setup(coreSetup, { cloud: cloudMock.createSetup() });
|
||||
expect(LaunchDarklyClientProviderMocked).toHaveBeenCalledWith(
|
||||
'1234',
|
||||
expect.objectContaining({
|
||||
bootstrap: { 'my-initial-flag': true },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('use undefined bootstrap when creating the new client and no provided initial feature flags', () => {
|
||||
const coreSetup = coreMock.createSetup();
|
||||
coreSetup.featureFlags.getInitialFeatureFlags.mockReturnValue({});
|
||||
plugin.setup(coreSetup, { cloud: cloudMock.createSetup() });
|
||||
expect(LaunchDarklyClientProviderMocked).toHaveBeenCalledWith(
|
||||
'1234',
|
||||
expect.objectContaining({
|
||||
bootstrap: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
|
|
|
@ -73,7 +73,8 @@ export class CloudExperimentsPlugin
|
|||
logger: this.logger,
|
||||
});
|
||||
|
||||
const launchDarklyOpenFeatureProvider = this.createOpenFeatureProvider();
|
||||
const initialFeatureFlags = core.featureFlags.getInitialFeatureFlags();
|
||||
const launchDarklyOpenFeatureProvider = this.createOpenFeatureProvider(initialFeatureFlags);
|
||||
if (launchDarklyOpenFeatureProvider) {
|
||||
core.featureFlags.setProvider(launchDarklyOpenFeatureProvider);
|
||||
}
|
||||
|
@ -101,13 +102,15 @@ export class CloudExperimentsPlugin
|
|||
* Sets up the OpenFeature LaunchDarkly provider
|
||||
* @private
|
||||
*/
|
||||
private createOpenFeatureProvider() {
|
||||
private createOpenFeatureProvider(initialFeatureFlags: Record<string, unknown>) {
|
||||
const { launch_darkly: ldConfig } = this.initializerContext.config.get<{
|
||||
launch_darkly?: LaunchDarklyClientConfig;
|
||||
}>();
|
||||
|
||||
if (!ldConfig) return;
|
||||
|
||||
const bootstrap = Object.keys(initialFeatureFlags).length > 0 ? initialFeatureFlags : undefined;
|
||||
|
||||
return new LaunchDarklyClientProvider(ldConfig.client_id, {
|
||||
// logger: this.logger.get('launch-darkly'),
|
||||
// Using basicLogger for now because we can't limit the level for now if we're using core's logger.
|
||||
|
@ -120,6 +123,7 @@ export class CloudExperimentsPlugin
|
|||
? this.initializerContext.env.packageInfo.buildSha
|
||||
: this.initializerContext.env.packageInfo.version,
|
||||
},
|
||||
bootstrap,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,6 +102,15 @@ describe('Cloud Experiments server plugin', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('registers the initial feature flags getter to enable bootstrapping', async () => {
|
||||
const coreSetupMock = coreMock.createSetup();
|
||||
plugin.setup(coreSetupMock, {
|
||||
cloud: cloudMock.createSetup(),
|
||||
usageCollection: usageCollectionPluginMock.createSetupContract(),
|
||||
});
|
||||
expect(coreSetupMock.featureFlags.setInitialFeatureFlagsGetter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
|||
import type { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server/types';
|
||||
import { initializeMetadata, MetadataService } from '../common/metadata_service';
|
||||
import { registerUsageCollector } from './usage';
|
||||
import { getAllFlags, registerUsageCollector } from './usage';
|
||||
import type { CloudExperimentsConfigType } from './config';
|
||||
|
||||
interface CloudExperimentsPluginSetupDeps {
|
||||
|
@ -79,6 +79,12 @@ export class CloudExperimentsPlugin
|
|||
const launchDarklyOpenFeatureProvider = this.createOpenFeatureProvider();
|
||||
if (launchDarklyOpenFeatureProvider) {
|
||||
core.featureFlags.setProvider(launchDarklyOpenFeatureProvider);
|
||||
core.featureFlags.setInitialFeatureFlagsGetter(async () => {
|
||||
const launchDarklyClient = launchDarklyOpenFeatureProvider.getClient();
|
||||
const context = OpenFeature.getContext();
|
||||
const { flags } = await getAllFlags(launchDarklyClient, context);
|
||||
return flags;
|
||||
});
|
||||
}
|
||||
|
||||
registerUsageCollector(deps.usageCollection, () => ({
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { registerUsageCollector } from './register_usage_collector';
|
||||
export { registerUsageCollector, getAllFlags } from './register_usage_collector';
|
||||
|
|
|
@ -60,7 +60,7 @@ export function registerUsageCollector(
|
|||
);
|
||||
}
|
||||
|
||||
async function getAllFlags(
|
||||
export async function getAllFlags(
|
||||
launchDarklyClient: LDClient,
|
||||
currentContext: EvaluationContext
|
||||
): Promise<Usage> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue