[Feature Flags] Client-side bootstrapping (#224258)

This commit is contained in:
Alejandro Fernández Haro 2025-06-23 12:28:06 +02:00 committed by GitHub
parent 8d1204bb5f
commit fd7f85149c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 348 additions and 18 deletions

View file

@ -249,6 +249,7 @@ describe('FeatureFlagsService Browser', () => {
'myPlugin.myOverriddenFlag': true,
myDestructuredObjPlugin: { myOverriddenFlag: true },
},
initialFeatureFlags: {},
});
featureFlagsService.setup({ injectedMetadata });
startContract = await featureFlagsService.start();

View file

@ -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.

View file

@ -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),
};

View file

@ -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.

View file

@ -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> = {
/**

View file

@ -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);
});
});
});

View file

@ -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.

View file

@ -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(),
};

View file

@ -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';

View file

@ -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;
}
/**

View file

@ -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', () => {

View file

@ -61,6 +61,7 @@ export interface InternalInjectedMetadataSetup {
getFeatureFlags: () =>
| {
overrides: Record<string, unknown>;
initialFeatureFlags: Record<string, unknown>;
}
| undefined;
}

View file

@ -66,6 +66,7 @@ export interface InjectedMetadata {
};
featureFlags?: {
overrides: Record<string, unknown>;
initialFeatureFlags: Record<string, unknown>;
};
anonymousStatusPage: boolean;
i18n: {

View file

@ -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,
},

View file

@ -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>,
}
`;

View file

@ -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: {

View file

@ -298,6 +298,7 @@ export class RenderingService {
env,
featureFlags: {
overrides: featureFlags?.getOverrides() || {},
initialFeatureFlags: (await featureFlags?.getInitialFeatureFlags()) || {},
},
clusterInfo,
apmConfig,

View file

@ -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,
};
});

View file

@ -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', () => {

View file

@ -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,
});
}
}

View file

@ -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', () => {

View file

@ -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, () => ({

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { registerUsageCollector } from './register_usage_collector';
export { registerUsageCollector, getAllFlags } from './register_usage_collector';

View file

@ -60,7 +60,7 @@ export function registerUsageCollector(
);
}
async function getAllFlags(
export async function getAllFlags(
launchDarklyClient: LDClient,
currentContext: EvaluationContext
): Promise<Usage> {