diff --git a/dev_docs/nav-kibana-dev.docnav.json b/dev_docs/nav-kibana-dev.docnav.json
index 2a9893898e8c5..dc8a4373f5450 100644
--- a/dev_docs/nav-kibana-dev.docnav.json
+++ b/dev_docs/nav-kibana-dev.docnav.json
@@ -136,10 +136,6 @@
},
{
"id": "kibDevDocsEmbeddables"
- },
- {
- "id": "kibCloudExperimentsPlugin",
- "label": "A/B testing on Elastic Cloud"
}
]
},
@@ -205,6 +201,10 @@
},
{
"id": "kibDevTutorialCcsSetup"
+ },
+ {
+ "id": "kibFeatureFlagsService",
+ "label": "Feature Flags"
}
]
},
@@ -646,4 +646,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc
index 0010045ab3c92..477ef07db23a5 100644
--- a/docs/developer/plugin-list.asciidoc
+++ b/docs/developer/plugin-list.asciidoc
@@ -499,8 +499,8 @@ The plugin exposes the static DefaultEditorController class to consume.
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_experiments/README.mdx[cloudExperiments]
-|[!WARNING]
-These APIs are deprecated and should not be used as we're working on a replacement Core Feature Flags Service that will arrive soon.
+|[!NOTE]
+This plugin no-longer exposes any evaluation APIs. Refer to for more information about how to interact with feature flags.
|{kib-repo}blob/{branch}/x-pack/plugins/cloud_integrations/cloud_full_story/README.md[cloudFullStory]
diff --git a/examples/feature_flags_example/README.md b/examples/feature_flags_example/README.md
new file mode 100755
index 0000000000000..54ecd4126683d
--- /dev/null
+++ b/examples/feature_flags_example/README.md
@@ -0,0 +1,5 @@
+# featureFlagsExample
+
+This plugin's goal is to demonstrate how to use the core feature flags service.
+
+Refer to [the docs](../../packages/core/feature-flags/README.mdx) to know more.
diff --git a/examples/feature_flags_example/common/feature_flags.ts b/examples/feature_flags_example/common/feature_flags.ts
new file mode 100644
index 0000000000000..fcff25bbd2c42
--- /dev/null
+++ b/examples/feature_flags_example/common/feature_flags.ts
@@ -0,0 +1,12 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * 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';
diff --git a/examples/feature_flags_example/common/index.ts b/examples/feature_flags_example/common/index.ts
new file mode 100644
index 0000000000000..37bde8e9843e1
--- /dev/null
+++ b/examples/feature_flags_example/common/index.ts
@@ -0,0 +1,11 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export const PLUGIN_ID = 'featureFlagsExample';
+export const PLUGIN_NAME = 'Feature Flags Example';
diff --git a/examples/feature_flags_example/kibana.jsonc b/examples/feature_flags_example/kibana.jsonc
new file mode 100644
index 0000000000000..c2a855723bdac
--- /dev/null
+++ b/examples/feature_flags_example/kibana.jsonc
@@ -0,0 +1,13 @@
+{
+ "type": "plugin",
+ "id": "@kbn/feature-flags-example-plugin",
+ "owner": "@elastic/kibana-core",
+ "description": "Plugin that shows how to make use of the feature flags core service.",
+ "plugin": {
+ "id": "featureFlagsExample",
+ "server": true,
+ "browser": true,
+ "requiredPlugins": ["developerExamples"],
+ "optionalPlugins": []
+ }
+}
diff --git a/examples/feature_flags_example/public/application.tsx b/examples/feature_flags_example/public/application.tsx
new file mode 100644
index 0000000000000..eab558d9301bd
--- /dev/null
+++ b/examples/feature_flags_example/public/application.tsx
@@ -0,0 +1,33 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { AppMountParameters, CoreStart } from '@kbn/core/public';
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
+import { FeatureFlagsExampleApp } from './components/app';
+
+export const renderApp = (coreStart: CoreStart, { element }: AppMountParameters) => {
+ const { notifications, http, featureFlags } = coreStart;
+ ReactDOM.render(
+
+
+
+
+ ,
+ element
+ );
+
+ return () => ReactDOM.unmountComponentAtNode(element);
+};
diff --git a/examples/feature_flags_example/public/components/app.tsx b/examples/feature_flags_example/public/components/app.tsx
new file mode 100644
index 0000000000000..432e7dc348abc
--- /dev/null
+++ b/examples/feature_flags_example/public/components/app.tsx
@@ -0,0 +1,91 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import React from 'react';
+import {
+ EuiHorizontalRule,
+ EuiPageTemplate,
+ EuiTitle,
+ EuiText,
+ EuiLink,
+ EuiListGroup,
+ EuiListGroupItem,
+} from '@elastic/eui';
+import type { CoreStart, FeatureFlagsStart } from '@kbn/core/public';
+
+import useObservable from 'react-use/lib/useObservable';
+import {
+ FeatureFlagExampleBoolean,
+ FeatureFlagExampleNumber,
+ FeatureFlagExampleString,
+} from '../../common/feature_flags';
+import { PLUGIN_NAME } from '../../common';
+
+interface FeatureFlagsExampleAppDeps {
+ featureFlags: FeatureFlagsStart;
+ notifications: CoreStart['notifications'];
+ http: CoreStart['http'];
+}
+
+export const FeatureFlagsExampleApp = ({ featureFlags }: FeatureFlagsExampleAppDeps) => {
+ // Fetching the feature flags synchronously
+ const bool = featureFlags.getBooleanValue(FeatureFlagExampleBoolean, false);
+ const str = featureFlags.getStringValue(FeatureFlagExampleString, 'red');
+ const num = featureFlags.getNumberValue(FeatureFlagExampleNumber, 1);
+
+ // Use React Hooks to observe feature flags changes
+ const bool$ = useObservable(featureFlags.getBooleanValue$(FeatureFlagExampleBoolean, false));
+ const str$ = useObservable(featureFlags.getStringValue$(FeatureFlagExampleString, 'red'));
+ const num$ = useObservable(featureFlags.getNumberValue$(FeatureFlagExampleNumber, 1));
+
+ return (
+ <>
+
+
+
+
{PLUGIN_NAME}
+
+
+
+
+
Demo of the feature flags service
+
+
+
+ To learn more, refer to{' '}
+
+ the docs
+
+ .
+
+
+
+
+ The feature flags are:
+
+
+
+
+
+
+
+ The observed feature flags are:
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/examples/feature_flags_example/public/index.ts b/examples/feature_flags_example/public/index.ts
new file mode 100644
index 0000000000000..9324fbb56bc2e
--- /dev/null
+++ b/examples/feature_flags_example/public/index.ts
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { FeatureFlagsExamplePlugin } from './plugin';
+
+export function plugin() {
+ return new FeatureFlagsExamplePlugin();
+}
diff --git a/examples/feature_flags_example/public/plugin.ts b/examples/feature_flags_example/public/plugin.ts
new file mode 100644
index 0000000000000..915c40dcaafe8
--- /dev/null
+++ b/examples/feature_flags_example/public/plugin.ts
@@ -0,0 +1,40 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
+import { AppPluginSetupDependencies } from './types';
+import { PLUGIN_NAME } from '../common';
+
+export class FeatureFlagsExamplePlugin implements Plugin {
+ public setup(core: CoreSetup, deps: AppPluginSetupDependencies) {
+ // Register an application into the side navigation menu
+ core.application.register({
+ id: 'featureFlagsExample',
+ title: PLUGIN_NAME,
+ async mount(params: AppMountParameters) {
+ // Load application bundle
+ const { renderApp } = await import('./application');
+ // Get start services as specified in kibana.json
+ const [coreStart] = await core.getStartServices();
+ // Render the application
+ return renderApp(coreStart, params);
+ },
+ });
+
+ deps.developerExamples.register({
+ appId: 'featureFlagsExample',
+ title: PLUGIN_NAME,
+ description: 'Plugin that shows how to make use of the feature flags core service.',
+ });
+ }
+
+ public start(core: CoreStart) {}
+
+ public stop() {}
+}
diff --git a/examples/feature_flags_example/public/types.ts b/examples/feature_flags_example/public/types.ts
new file mode 100644
index 0000000000000..7f3f7107a1385
--- /dev/null
+++ b/examples/feature_flags_example/public/types.ts
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
+
+export interface AppPluginSetupDependencies {
+ developerExamples: DeveloperExamplesSetup;
+}
diff --git a/examples/feature_flags_example/server/index.ts b/examples/feature_flags_example/server/index.ts
new file mode 100644
index 0000000000000..ad88372a7e11e
--- /dev/null
+++ b/examples/feature_flags_example/server/index.ts
@@ -0,0 +1,77 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { FeatureFlagDefinitions } from '@kbn/core-feature-flags-server';
+import type { PluginInitializerContext } from '@kbn/core-plugins-server';
+import {
+ FeatureFlagExampleBoolean,
+ FeatureFlagExampleNumber,
+ FeatureFlagExampleString,
+} from '../common/feature_flags';
+
+export const featureFlags: FeatureFlagDefinitions = [
+ {
+ key: FeatureFlagExampleBoolean,
+ name: 'Example boolean',
+ description: 'This is a demo of a boolean flag',
+ tags: ['example', 'my-plugin'],
+ variationType: 'boolean',
+ variations: [
+ {
+ name: 'On',
+ description: 'Auto-hides the bar',
+ value: true,
+ },
+ {
+ name: 'Off',
+ description: 'Static always-on',
+ value: false,
+ },
+ ],
+ },
+ {
+ key: FeatureFlagExampleString,
+ name: 'Example string',
+ description: 'This is a demo of a string flag',
+ tags: ['example', 'my-plugin'],
+ variationType: 'string',
+ variations: [
+ {
+ name: 'Pink',
+ value: '#D75489',
+ },
+ {
+ name: 'Turquoise',
+ value: '#65BAAF',
+ },
+ ],
+ },
+ {
+ key: FeatureFlagExampleNumber,
+ name: 'Example Number',
+ description: 'This is a demo of a number flag',
+ tags: ['example', 'my-plugin'],
+ variationType: 'number',
+ variations: [
+ {
+ name: 'Five',
+ value: 5,
+ },
+ {
+ name: 'Ten',
+ value: 10,
+ },
+ ],
+ },
+];
+
+export async function plugin(initializerContext: PluginInitializerContext) {
+ const { FeatureFlagsExamplePlugin } = await import('./plugin');
+ return new FeatureFlagsExamplePlugin(initializerContext);
+}
diff --git a/examples/feature_flags_example/server/plugin.ts b/examples/feature_flags_example/server/plugin.ts
new file mode 100644
index 0000000000000..3abd4554eb335
--- /dev/null
+++ b/examples/feature_flags_example/server/plugin.ts
@@ -0,0 +1,69 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type {
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ Logger,
+} from '@kbn/core/server';
+import { combineLatest } from 'rxjs';
+
+import {
+ FeatureFlagExampleBoolean,
+ FeatureFlagExampleNumber,
+ FeatureFlagExampleString,
+} from '../common/feature_flags';
+import { defineRoutes } from './routes';
+
+export class FeatureFlagsExamplePlugin implements Plugin {
+ private readonly logger: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.logger = initializerContext.logger.get();
+ }
+
+ public setup(core: CoreSetup) {
+ const router = core.http.createRouter();
+
+ // Register server side APIs
+ defineRoutes(router);
+ }
+
+ public start(core: CoreStart) {
+ // Promise form: when we need to fetch it once, like in an HTTP request
+ void Promise.all([
+ core.featureFlags.getBooleanValue(FeatureFlagExampleBoolean, false),
+ core.featureFlags.getStringValue(FeatureFlagExampleString, 'white'),
+ core.featureFlags.getNumberValue(FeatureFlagExampleNumber, 1),
+ ]).then(([bool, str, num]) => {
+ this.logger.info(`The feature flags are:
+ - ${FeatureFlagExampleBoolean}: ${bool}
+ - ${FeatureFlagExampleString}: ${str}
+ - ${FeatureFlagExampleNumber}: ${num}
+ `);
+ });
+
+ // Observable form: when we need to react to the changes
+ combineLatest([
+ core.featureFlags.getBooleanValue$(FeatureFlagExampleBoolean, false),
+ core.featureFlags.getStringValue$(FeatureFlagExampleString, 'red'),
+ core.featureFlags.getNumberValue$(FeatureFlagExampleNumber, 1),
+ ]).subscribe(([bool, str, num]) => {
+ this.logger.info(`The observed feature flags are:
+ - ${FeatureFlagExampleBoolean}: ${bool}
+ - ${FeatureFlagExampleString}: ${str}
+ - ${FeatureFlagExampleNumber}: ${num}
+ `);
+ });
+ }
+
+ public stop() {}
+}
diff --git a/examples/feature_flags_example/server/routes/index.ts b/examples/feature_flags_example/server/routes/index.ts
new file mode 100644
index 0000000000000..97ce19ec9981b
--- /dev/null
+++ b/examples/feature_flags_example/server/routes/index.ts
@@ -0,0 +1,44 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { IRouter } from '@kbn/core/server';
+import { schema } from '@kbn/config-schema';
+import { FeatureFlagExampleNumber } from '../../common/feature_flags';
+
+export function defineRoutes(router: IRouter) {
+ router.versioned
+ .get({
+ path: '/api/feature_flags_example/example',
+ access: 'public',
+ })
+ .addVersion(
+ {
+ version: '2023-10-31',
+ validate: {
+ response: {
+ 200: {
+ body: () =>
+ schema.object({
+ number: schema.number(),
+ }),
+ },
+ },
+ },
+ },
+ async (context, request, response) => {
+ const { featureFlags } = await context.core;
+
+ return response.ok({
+ body: {
+ number: await featureFlags.getNumberValue(FeatureFlagExampleNumber, 1),
+ },
+ });
+ }
+ );
+}
diff --git a/examples/feature_flags_example/tsconfig.json b/examples/feature_flags_example/tsconfig.json
new file mode 100644
index 0000000000000..bbd68332f3d37
--- /dev/null
+++ b/examples/feature_flags_example/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types"
+ },
+ "include": [
+ "index.ts",
+ "common/**/*.ts",
+ "public/**/*.ts",
+ "public/**/*.tsx",
+ "server/**/*.ts",
+ "../../typings/**/*"
+ ],
+ "exclude": ["target/**/*"],
+ "kbn_references": [
+ "@kbn/core",
+ "@kbn/shared-ux-page-kibana-template",
+ "@kbn/react-kibana-context-root",
+ "@kbn/core-feature-flags-server",
+ "@kbn/core-plugins-server",
+ "@kbn/config-schema",
+ "@kbn/developer-examples-plugin",
+ ]
+}
diff --git a/package.json b/package.json
index 431275f8aa694..0ac2beac21188 100644
--- a/package.json
+++ b/package.json
@@ -287,6 +287,12 @@
"@kbn/core-execution-context-server-internal": "link:packages/core/execution-context/core-execution-context-server-internal",
"@kbn/core-fatal-errors-browser": "link:packages/core/fatal-errors/core-fatal-errors-browser",
"@kbn/core-fatal-errors-browser-internal": "link:packages/core/fatal-errors/core-fatal-errors-browser-internal",
+ "@kbn/core-feature-flags-browser": "link:packages/core/feature-flags/core-feature-flags-browser",
+ "@kbn/core-feature-flags-browser-internal": "link:packages/core/feature-flags/core-feature-flags-browser-internal",
+ "@kbn/core-feature-flags-browser-mocks": "link:packages/core/feature-flags/core-feature-flags-browser-mocks",
+ "@kbn/core-feature-flags-server": "link:packages/core/feature-flags/core-feature-flags-server",
+ "@kbn/core-feature-flags-server-internal": "link:packages/core/feature-flags/core-feature-flags-server-internal",
+ "@kbn/core-feature-flags-server-mocks": "link:packages/core/feature-flags/core-feature-flags-server-mocks",
"@kbn/core-history-block-plugin": "link:test/plugin_functional/plugins/core_history_block",
"@kbn/core-http-browser": "link:packages/core/http/core-http-browser",
"@kbn/core-http-browser-internal": "link:packages/core/http/core-http-browser-internal",
@@ -505,6 +511,7 @@
"@kbn/expressions-explorer-plugin": "link:examples/expressions_explorer",
"@kbn/expressions-plugin": "link:src/plugins/expressions",
"@kbn/feature-controls-examples-plugin": "link:examples/feature_control_examples",
+ "@kbn/feature-flags-example-plugin": "link:examples/feature_flags_example",
"@kbn/feature-usage-test-plugin": "link:x-pack/test/plugin_api_integration/plugins/feature_usage_test",
"@kbn/features-plugin": "link:x-pack/plugins/features",
"@kbn/fec-alerts-test-plugin": "link:x-pack/test/functional_execution_context/plugins/alerts",
@@ -988,6 +995,7 @@
"@langchain/openai": "^0.1.3",
"@langtrase/trace-attributes": "^3.0.8",
"@launchdarkly/node-server-sdk": "^9.5.4",
+ "@launchdarkly/openfeature-node-server": "^1.0.0",
"@loaders.gl/core": "^3.4.7",
"@loaders.gl/json": "^3.4.7",
"@loaders.gl/shapefile": "^3.4.7",
@@ -996,6 +1004,10 @@
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mapbox/mapbox-gl-supported": "2.0.1",
"@mapbox/vector-tile": "1.3.1",
+ "@openfeature/core": "^1.3.0",
+ "@openfeature/launchdarkly-client-provider": "^0.3.0",
+ "@openfeature/server-sdk": "^1.15.0",
+ "@openfeature/web-sdk": "^1.2.1",
"@opentelemetry/api": "^1.1.0",
"@opentelemetry/api-metrics": "^0.31.0",
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.34.0",
@@ -1129,7 +1141,6 @@
"langchain": "^0.2.11",
"langsmith": "^0.1.39",
"launchdarkly-js-client-sdk": "^3.4.0",
- "launchdarkly-node-server-sdk": "^7.0.3",
"load-json-file": "^6.2.0",
"lodash": "^4.17.21",
"lru-cache": "^4.1.5",
diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_route_handler_context.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_route_handler_context.ts
index b4be404f137af..ce45e5c11833c 100644
--- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_route_handler_context.ts
+++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/elasticsearch_route_handler_context.ts
@@ -15,7 +15,7 @@ import type {
import type { InternalElasticsearchServiceStart } from './types';
/**
- * The {@link UiSettingsRequestHandlerContext} implementation.
+ * The {@link ElasticsearchRequestHandlerContext} implementation.
* @internal
*/
export class CoreElasticsearchRouteHandlerContext implements ElasticsearchRequestHandlerContext {
diff --git a/packages/core/feature-flags/README.mdx b/packages/core/feature-flags/README.mdx
new file mode 100644
index 0000000000000..d1e3583aaf2b8
--- /dev/null
+++ b/packages/core/feature-flags/README.mdx
@@ -0,0 +1,158 @@
+---
+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
+tags: ['kibana', 'dev', 'contributor', 'api docs', 'a/b testing', 'feature flags', 'flags']
+---
+
+# Feature Flags Service
+
+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.
+
+For a code example, refer to the [Feature Flags Example plugin](../../../examples/feature_flags_example)
+
+## Registering a feature flag
+
+Kibana follows a _gitops_ approach when managing feature flags. To declare a feature flag, add your flags definitions in
+your plugin's `server/index.ts` file:
+
+```typescript
+// /server/index.ts
+import type { FeatureFlagDefinitions } from '@kbn/core-feature-flags-server';
+import type { PluginInitializerContext } from '@kbn/core-plugins-server';
+
+export const featureFlags: FeatureFlagDefinitions = [
+ {
+ key: 'my-cool-feature',
+ name: 'My cool feature',
+ description: 'Enables the cool feature to auto-hide the navigation bar',
+ tags: ['my-plugin', 'my-service', 'ui'],
+ variationType: 'boolean',
+ variations: [
+ {
+ name: 'On',
+ description: 'Auto-hides the bar',
+ value: true,
+ },
+ {
+ name: 'Off',
+ description: 'Static always-on',
+ value: false,
+ },
+ ],
+ },
+ {...},
+];
+
+export async function plugin(initializerContext: PluginInitializerContext) {
+ const { FeatureFlagsExamplePlugin } = await import('./plugin');
+ return new FeatureFlagsExamplePlugin(initializerContext);
+}
+```
+
+After merging your PR, the CI will create/update the flags in our third-party feature flags provider.
+
+### Deprecation/removal strategy
+
+When your code doesn't use the feature flag anymore, it is recommended to clean up the feature flags when possible.
+There are a few considerations to take into account when performing this clean-up:
+
+1. Always deprecate first, remove after
+2. When to remove?
+
+#### Always deprecate first, remove after
+
+Just because the CI syncs the state of `main` to our feature flag provider, there is a high probability that the
+previous version of the code that still relied on the feature flag is still running out there.
+
+For that reason, the recommendation is to always deprecate before removing the flags. This will keep evaluating the flags,
+according to the segmentation rules configured for the flag.
+
+#### When to remove?
+
+After deprecation, we need to consider when it's safe to remove the flag. There are different scenarios that come with
+different recommendations:
+
+* The segmentation rules of my flag are set up to return the fallback value 100% of the time: it should be safe to
+remove the flag at any time.
+* My flag only made it to Serverless (it never made it to Elastic Cloud Hosted): it should be safe to remove the flag
+after 2 releases have been rolled out (roughly 2-3 weeks later). This is to ensure that all Serverless projects have
+been upgraded and that we won't need to rollback to the previous version.
+* My flag made it to Elastic Cloud Hosted: if we want to remove the flag, we should approach the affected customers to
+fix the expected values via [config overrides](#config-overrides).
+
+In general, the recommendation is to check our telemetry to validate the usage of our flags.
+
+## Evaluating feature flags
+
+This service provides 2 ways to evaluate your feature flags, depending on the use case:
+
+1. **Single evaluation**: performs the evaluation once, and doesn't react to updates. These APIs are synchronous in the
+browser, and asynchronous in the server.
+2. **Observed evaluation**: observes the flag for any changes so that the code can adapt. These APIs return an RxJS observable.
+
+Also, the APIs are typed, so you need to use the appropriate API depending on the `variationType` you defined your flag:
+
+| Type | Single evaluation | Observed evaluation |
+|:-------:|:--------------------------------------------------------|:---------------------------------------------------------|
+| Boolean | `core.featureFlags.getBooleanValue(flagName, fallback)` | `core.featureFlags.getBooleanValue$(flagName, fallback)` |
+| String | `core.featureFlags.getStringValue(flagName, fallback)` | `core.featureFlags.getStringValue$(flagName, fallback)` |
+| Number | `core.featureFlags.getNumberValue(flagName, fallback)` | `core.featureFlags.getNumberValue$(flagName, fallback)` |
+
+### Request handler context
+
+Additionally, to make things easier in our HTTP handlers, the _Single evaluation_ APIs are available as part of the core
+context provided to the handlers:
+
+```typescript
+async (context, request, response) => {
+ const { featureFlags } = await context.core;
+ return response.ok({
+ body: {
+ number: await featureFlags.getNumberValue('example-number', 1),
+ },
+ });
+}
+```
+
+## Extending the evaluation context
+
+The should have
+enough information to declare the segmentation rules for your feature flags. However, if your use case requires additional
+context, feel free to call the API `core.featureFlags.setContext()` from your plugin.
+
+At the moment, we use 2 levels of context: `kibana` and `organization` that we can use for segmentation purposes at
+different levels. By default, the API appends the context to the `kibana` scope. If you need to extend the `organization`
+scope, make sure to add `kind: 'organization'` to the object provided to the `setContext` API.
+
+## Config overrides
+
+To help with testing, and to provide an escape hatch in cases where the flag evaluation is not behaving as intended,
+the Feature Flags Service provides a way to force the values of a feature flag without attempting to resolve it via the
+provider. In the `kibana.yml`, the following config sets the overrides:
+
+```yaml
+feature_flags.overrides:
+ my-feature-flag: 'my-forced-value'
+```
+
+> [!WARNING]
+> There is no validation regarding the variations nor the type of the flags. Use these overrides with caution.
+
+### Dynamic config
+
+When running in our test environments, the overrides can be updated without restarting Kibana via the HTTP `PUT /internal/core/_settings`:
+
+```
+PUT /internal/core/_settings
+{
+ "feature_flags.overrides": {
+ "my-feature-flag": "my-forced-value"
+ }
+}
+```
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/README.md b/packages/core/feature-flags/core-feature-flags-browser-internal/README.md
new file mode 100644
index 0000000000000..f5696d4530483
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/README.md
@@ -0,0 +1,5 @@
+# @kbn/core-feature-flags-browser-internal
+
+Internal implementation of the browser-side Feature Flags Service.
+
+It should only be imported by _Core_ packages.
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/index.ts b/packages/core/feature-flags/core-feature-flags-browser-internal/index.ts
new file mode 100644
index 0000000000000..e22aeeecd35fb
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/index.ts
@@ -0,0 +1,10 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export { FeatureFlagsService, type FeatureFlagsSetupDeps } from './src/feature_flags_service';
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/jest.config.js b/packages/core/feature-flags/core-feature-flags-browser-internal/jest.config.js
new file mode 100644
index 0000000000000..be4a9c1b14073
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/jest.config.js
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/packages/core/feature-flags/core-feature-flags-browser-internal'],
+};
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/kibana.jsonc b/packages/core/feature-flags/core-feature-flags-browser-internal/kibana.jsonc
new file mode 100644
index 0000000000000..150509b99f519
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/core-feature-flags-browser-internal",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/package.json b/packages/core/feature-flags/core-feature-flags-browser-internal/package.json
new file mode 100644
index 0000000000000..de82d53b1c964
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-feature-flags-browser-internal",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/src/feature_flags_service.test.ts b/packages/core/feature-flags/core-feature-flags-browser-internal/src/feature_flags_service.test.ts
new file mode 100644
index 0000000000000..596d64c7b77ae
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/src/feature_flags_service.test.ts
@@ -0,0 +1,292 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { firstValueFrom } from 'rxjs';
+import { apm } from '@elastic/apm-rum';
+import { type Client, OpenFeature, type Provider } from '@openfeature/web-sdk';
+import { coreContextMock } from '@kbn/core-base-browser-mocks';
+import type { FeatureFlagsStart } from '@kbn/core-feature-flags-browser';
+import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks';
+import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
+import { FeatureFlagsService } from '..';
+
+async function isSettledPromise(p: Promise) {
+ const immediateValue = {};
+ const result = await Promise.race([p, immediateValue]);
+ return result !== immediateValue;
+}
+
+describe('FeatureFlagsService Browser', () => {
+ let featureFlagsService: FeatureFlagsService;
+ let featureFlagsClient: Client;
+ let injectedMetadata: jest.Mocked;
+
+ beforeEach(() => {
+ const getClientSpy = jest.spyOn(OpenFeature, 'getClient');
+ featureFlagsService = new FeatureFlagsService(coreContextMock.create());
+ featureFlagsClient = getClientSpy.mock.results[0].value;
+ injectedMetadata = injectedMetadataServiceMock.createSetupContract();
+ });
+
+ afterEach(async () => {
+ await featureFlagsService.stop();
+ jest.clearAllMocks();
+ await OpenFeature.clearProviders();
+ await OpenFeature.clearContexts();
+ });
+
+ describe('provider handling', () => {
+ test('appends a provider (without awaiting)', () => {
+ expect.assertions(1);
+ const { setProvider } = featureFlagsService.setup({ injectedMetadata });
+ const spy = jest.spyOn(OpenFeature, 'setProviderAndWait');
+ const fakeProvider = { metadata: { name: 'fake provider' } } as Provider;
+ setProvider(fakeProvider);
+ expect(spy).toHaveBeenCalledWith(fakeProvider);
+ });
+
+ test('throws an error if called twice', () => {
+ const { setProvider } = featureFlagsService.setup({ injectedMetadata });
+ const fakeProvider = { metadata: { name: 'fake provider' } } as Provider;
+ setProvider(fakeProvider);
+ expect(() => setProvider(fakeProvider)).toThrowErrorMatchingInlineSnapshot(
+ `"A provider has already been set. This API cannot be called twice."`
+ );
+ });
+
+ test('awaits initialization in the start context', async () => {
+ const { setProvider } = featureFlagsService.setup({ injectedMetadata });
+ let externalResolve: Function = () => void 0;
+ const spy = jest.spyOn(OpenFeature, 'setProviderAndWait').mockImplementation(async () => {
+ await new Promise((resolve) => {
+ externalResolve = resolve;
+ });
+ });
+ const fakeProvider = {} as Provider;
+ setProvider(fakeProvider);
+ expect(spy).toHaveBeenCalledWith(fakeProvider);
+ const startPromise = featureFlagsService.start();
+ await expect(isSettledPromise(startPromise)).resolves.toBe(false);
+ externalResolve();
+ await new Promise((resolve) => process.nextTick(resolve)); // Wait for the promise resolution to spread
+ await expect(isSettledPromise(startPromise)).resolves.toBe(true);
+ });
+
+ test('do not hold for too long during initialization', async () => {
+ const { setProvider } = featureFlagsService.setup({ injectedMetadata });
+ const spy = jest.spyOn(OpenFeature, 'setProviderAndWait').mockImplementation(async () => {
+ await new Promise(() => {}); // never resolves
+ });
+ const apmCaptureErrorSpy = jest.spyOn(apm, 'captureError');
+ const fakeProvider = {} as Provider;
+ setProvider(fakeProvider);
+ expect(spy).toHaveBeenCalledWith(fakeProvider);
+ const startPromise = featureFlagsService.start();
+ await expect(isSettledPromise(startPromise)).resolves.toBe(false);
+ await new Promise((resolve) => setTimeout(resolve, 2100)); // A bit longer than 2 seconds
+ await expect(isSettledPromise(startPromise)).resolves.toBe(true);
+ expect(apmCaptureErrorSpy).toHaveBeenCalledWith(
+ expect.stringContaining('The feature flags provider took too long to initialize.')
+ );
+ });
+ });
+
+ describe('context handling', () => {
+ let setContextSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ setContextSpy = jest.spyOn(OpenFeature, 'setContext');
+ });
+
+ test('appends context to the provider', async () => {
+ const { appendContext } = featureFlagsService.setup({ injectedMetadata });
+ await appendContext({ kind: 'multi' });
+ expect(setContextSpy).toHaveBeenCalledWith({ kind: 'multi' });
+ });
+
+ test('appends context to the provider (start method)', async () => {
+ featureFlagsService.setup({ injectedMetadata });
+ const { appendContext } = await featureFlagsService.start();
+ await appendContext({ kind: 'multi' });
+ expect(setContextSpy).toHaveBeenCalledWith({ kind: 'multi' });
+ });
+
+ test('full multi context pass-through', async () => {
+ const { appendContext } = featureFlagsService.setup({ injectedMetadata });
+ const context = {
+ kind: 'multi' as const,
+ kibana: {
+ key: 'kibana-1',
+ },
+ organization: {
+ key: 'organization-1',
+ },
+ };
+ await appendContext(context);
+ expect(setContextSpy).toHaveBeenCalledWith(context);
+ });
+
+ test('appends to the existing context', async () => {
+ const { appendContext } = featureFlagsService.setup({ injectedMetadata });
+ const initialContext = {
+ kind: 'multi' as const,
+ kibana: {
+ key: 'kibana-1',
+ },
+ organization: {
+ key: 'organization-1',
+ },
+ };
+ await appendContext(initialContext);
+ expect(setContextSpy).toHaveBeenCalledWith(initialContext);
+
+ await appendContext({ kind: 'multi', kibana: { has_data: true } });
+ expect(setContextSpy).toHaveBeenCalledWith({
+ ...initialContext,
+ kibana: {
+ ...initialContext.kibana,
+ has_data: true,
+ },
+ });
+ });
+
+ test('converts single-contexts to multi-context', async () => {
+ const { appendContext } = featureFlagsService.setup({ injectedMetadata });
+ await appendContext({ kind: 'organization', key: 'organization-1' });
+ expect(setContextSpy).toHaveBeenCalledWith({
+ kind: 'multi',
+ organization: {
+ key: 'organization-1',
+ },
+ });
+ });
+
+ test('if no `kind` provided, it defaults to the kibana context', async () => {
+ const { appendContext } = featureFlagsService.setup({ injectedMetadata });
+ await appendContext({ key: 'key-1', has_data: false });
+ expect(setContextSpy).toHaveBeenCalledWith({
+ kind: 'multi',
+ kibana: {
+ key: 'key-1',
+ has_data: false,
+ },
+ });
+ });
+ });
+
+ describe('flag evaluation', () => {
+ let startContract: FeatureFlagsStart;
+ let apmSpy: jest.SpyInstance;
+ let addHandlerSpy: jest.SpyInstance;
+
+ beforeEach(async () => {
+ addHandlerSpy = jest.spyOn(featureFlagsClient, 'addHandler');
+ injectedMetadata.getFeatureFlags.mockReturnValue({
+ overrides: { 'my-overridden-flag': true },
+ });
+ featureFlagsService.setup({ injectedMetadata });
+ startContract = await featureFlagsService.start();
+ apmSpy = jest.spyOn(apm, 'addLabels');
+ });
+
+ // We don't need to test the client, just our APIs, so testing that it returns the fallback value should be enough.
+ test('get boolean flag', () => {
+ const value = false;
+ expect(startContract.getBooleanValue('my-flag', value)).toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ });
+
+ test('get string flag', () => {
+ const value = 'my-default';
+ expect(startContract.getStringValue('my-flag', value)).toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ });
+
+ test('get number flag', () => {
+ const value = 42;
+ expect(startContract.getNumberValue('my-flag', value)).toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ });
+
+ test('observe a boolean flag', async () => {
+ const value = false;
+ const flag$ = startContract.getBooleanValue$('my-flag', value);
+ const observedValues: boolean[] = [];
+ flag$.subscribe((v) => observedValues.push(v));
+ // Initial emission
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ expect(observedValues).toHaveLength(1);
+
+ // Does not reevaluate and emit if the other flags are changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['another-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(1); // still 1
+
+ // Reevaluates and emits when the observed flag is changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(2);
+ });
+
+ test('observe a string flag', async () => {
+ const value = 'my-value';
+ const flag$ = startContract.getStringValue$('my-flag', value);
+ const observedValues: string[] = [];
+ flag$.subscribe((v) => observedValues.push(v));
+ // Initial emission
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ expect(observedValues).toHaveLength(1);
+
+ // Does not reevaluate and emit if the other flags are changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['another-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(1); // still 1
+
+ // Reevaluates and emits when the observed flag is changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(2);
+ });
+
+ test('observe a number flag', async () => {
+ const value = 42;
+ const flag$ = startContract.getNumberValue$('my-flag', value);
+ const observedValues: number[] = [];
+ flag$.subscribe((v) => observedValues.push(v));
+ // Initial emission
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ expect(observedValues).toHaveLength(1);
+
+ // Does not reevaluate and emit if the other flags are changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['another-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(1); // still 1
+
+ // Reevaluates and emits when the observed flag is changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(2);
+ });
+
+ test('with overrides', async () => {
+ const getBooleanValueSpy = jest.spyOn(featureFlagsClient, 'getBooleanValue');
+ expect(startContract.getBooleanValue('my-overridden-flag', false)).toEqual(true);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-overridden-flag': true });
+ expect(getBooleanValueSpy).not.toHaveBeenCalled();
+
+ // Only to prove the spy works
+ expect(startContract.getBooleanValue('another-flag', false)).toEqual(false);
+ expect(getBooleanValueSpy).toHaveBeenCalledTimes(1);
+ expect(getBooleanValueSpy).toHaveBeenCalledWith('another-flag', false);
+ });
+ });
+});
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/src/feature_flags_service.ts b/packages/core/feature-flags/core-feature-flags-browser-internal/src/feature_flags_service.ts
new file mode 100644
index 0000000000000..0f7e572ef5ce0
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/src/feature_flags_service.ts
@@ -0,0 +1,203 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { CoreContext } from '@kbn/core-base-browser-internal';
+import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
+import type { Logger } from '@kbn/logging';
+import type {
+ EvaluationContext,
+ FeatureFlagsSetup,
+ FeatureFlagsStart,
+ MultiContextEvaluationContext,
+} from '@kbn/core-feature-flags-browser';
+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';
+
+/**
+ * setup method dependencies
+ * @private
+ */
+export interface FeatureFlagsSetupDeps {
+ /**
+ * Used to read the flag overrides set up in the configuration file.
+ */
+ injectedMetadata: InternalInjectedMetadataSetup;
+}
+
+/**
+ * The browser-side Feature Flags Service
+ * @private
+ */
+export class FeatureFlagsService {
+ private readonly featureFlagsClient: Client;
+ private readonly logger: Logger;
+ private isProviderReadyPromise?: Promise;
+ private context: MultiContextEvaluationContext = { kind: 'multi' };
+ private overrides: Record = {};
+
+ /**
+ * The core service's constructor
+ * @param core {@link CoreContext}
+ */
+ constructor(core: CoreContext) {
+ this.logger = core.logger.get('feature-flags-service');
+ this.featureFlagsClient = OpenFeature.getClient();
+ OpenFeature.setLogger(this.logger.get('open-feature'));
+ }
+
+ /**
+ * Setup lifecycle method
+ * @param deps {@link FeatureFlagsSetup} including the {@link InternalInjectedMetadataSetup} used to retrieve the feature flags.
+ */
+ public setup(deps: FeatureFlagsSetupDeps): FeatureFlagsSetup {
+ const featureFlagsInjectedMetadata = deps.injectedMetadata.getFeatureFlags();
+ if (featureFlagsInjectedMetadata) {
+ this.overrides = featureFlagsInjectedMetadata.overrides;
+ }
+ return {
+ setProvider: (provider) => {
+ if (this.isProviderReadyPromise) {
+ throw new Error('A provider has already been set. This API cannot be called twice.');
+ }
+ this.isProviderReadyPromise = OpenFeature.setProviderAndWait(provider);
+ },
+ appendContext: (contextToAppend) => this.appendContext(contextToAppend),
+ };
+ }
+
+ /**
+ * Start lifecycle method
+ */
+ public async start(): Promise {
+ const featureFlagsChanged$ = new Subject();
+ this.featureFlagsClient.addHandler(ClientProviderEvents.ConfigurationChanged, (event) => {
+ if (event?.flagsChanged) {
+ featureFlagsChanged$.next(event.flagsChanged);
+ }
+ });
+ const observeFeatureFlag$ = (flagName: string) =>
+ featureFlagsChanged$.pipe(
+ filter((flagNames) => flagNames.includes(flagName)),
+ startWith([flagName]) // only to emit on the first call
+ );
+
+ await this.waitForProviderInitialization();
+
+ return {
+ appendContext: (contextToAppend) => this.appendContext(contextToAppend),
+ getBooleanValue: (flagName: string, fallbackValue: boolean) =>
+ this.evaluateFlag(this.featureFlagsClient.getBooleanValue, flagName, fallbackValue),
+ getStringValue: (flagName: string, fallbackValue: Value) =>
+ this.evaluateFlag(this.featureFlagsClient.getStringValue, flagName, fallbackValue),
+ getNumberValue: (flagName: string, fallbackValue: Value) =>
+ this.evaluateFlag(this.featureFlagsClient.getNumberValue, flagName, fallbackValue),
+ getBooleanValue$: (flagName, fallbackValue) => {
+ return observeFeatureFlag$(flagName).pipe(
+ map(() =>
+ this.evaluateFlag(this.featureFlagsClient.getBooleanValue, flagName, fallbackValue)
+ )
+ );
+ },
+ getStringValue$: (flagName: string, fallbackValue: Value) => {
+ return observeFeatureFlag$(flagName).pipe(
+ map(() =>
+ this.evaluateFlag(
+ this.featureFlagsClient.getStringValue,
+ flagName,
+ fallbackValue
+ )
+ )
+ );
+ },
+ getNumberValue$: (flagName: string, fallbackValue: Value) => {
+ return observeFeatureFlag$(flagName).pipe(
+ map(() =>
+ this.evaluateFlag(
+ this.featureFlagsClient.getNumberValue,
+ flagName,
+ fallbackValue
+ )
+ )
+ );
+ },
+ };
+ }
+
+ /**
+ * Stop lifecycle method
+ */
+ public async stop() {
+ await OpenFeature.close();
+ }
+
+ /**
+ * Waits for the provider initialization with a timeout to avoid holding the page load for too long
+ * @private
+ */
+ private async waitForProviderInitialization() {
+ // Adding a timeout here to avoid hanging the start for too long if the provider is unresponsive
+ let timeoutId: NodeJS.Timeout | undefined;
+ await Promise.race([
+ this.isProviderReadyPromise,
+ new Promise((resolve) => {
+ timeoutId = setTimeout(resolve, 2 * 1000);
+ }).then(() => {
+ const msg = `The feature flags provider took too long to initialize.
+ Won't hold the page load any longer.
+ Feature flags will return the provided fallbacks until the provider is eventually initialized.`;
+ this.logger.warn(msg);
+ apm.captureError(msg);
+ }),
+ ]);
+ clearTimeout(timeoutId);
+ }
+
+ /**
+ * Wrapper to evaluate flags with the common config overrides interceptions + APM and counters reporting
+ * @param evaluationFn The actual evaluation API
+ * @param flagName The name of the flag to evaluate
+ * @param fallbackValue The fallback value
+ * @private
+ */
+ private evaluateFlag(
+ evaluationFn: (flagName: string, fallbackValue: T) => T,
+ flagName: string,
+ fallbackValue: T
+ ): T {
+ const value =
+ typeof this.overrides[flagName] !== 'undefined'
+ ? (this.overrides[flagName] 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 });
+ // TODO: increment usage counter
+ return value;
+ }
+
+ /**
+ * Formats the provided context to fulfill the expected multi-context structure.
+ * @param contextToAppend The {@link EvaluationContext} to append.
+ * @private
+ */
+ private async appendContext(contextToAppend: EvaluationContext): Promise {
+ // If no kind provided, default to the project|deployment level.
+ const { kind = 'kibana', ...rest } = contextToAppend;
+ // Format the context to fulfill the expected multi-context structure
+ const formattedContextToAppend: MultiContextEvaluationContext =
+ kind === 'multi'
+ ? (contextToAppend as MultiContextEvaluationContext)
+ : { kind: 'multi', [kind]: rest };
+
+ // Merge the formatted context to append to the global context, and set it in the OpenFeature client.
+ this.context = deepMerge(this.context, formattedContextToAppend);
+ await OpenFeature.setContext(this.context);
+ }
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser-internal/tsconfig.json b/packages/core/feature-flags/core-feature-flags-browser-internal/tsconfig.json
new file mode 100644
index 0000000000000..3ed73d73e75a3
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-internal/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/core-base-browser-internal",
+ "@kbn/core-feature-flags-browser",
+ "@kbn/logging",
+ "@kbn/core-base-browser-mocks",
+ "@kbn/core-injected-metadata-browser-internal",
+ "@kbn/core-injected-metadata-browser-mocks",
+ ]
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser-mocks/README.md b/packages/core/feature-flags/core-feature-flags-browser-mocks/README.md
new file mode 100644
index 0000000000000..db756eddf2f1a
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-mocks/README.md
@@ -0,0 +1,3 @@
+# @kbn/core-feature-flags-browser-mocks
+
+Browser-side Jest mocks for the Feature Flags Service.
diff --git a/packages/core/feature-flags/core-feature-flags-browser-mocks/index.ts b/packages/core/feature-flags/core-feature-flags-browser-mocks/index.ts
new file mode 100644
index 0000000000000..ad8cdae6a5ef1
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-mocks/index.ts
@@ -0,0 +1,58 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { FeatureFlagsSetup, FeatureFlagsStart } from '@kbn/core-feature-flags-browser';
+import type { FeatureFlagsService } from '@kbn/core-feature-flags-browser-internal';
+import type { PublicMethodsOf } from '@kbn/utility-types';
+import { of } from 'rxjs';
+
+const createFeatureFlagsSetup = (): jest.Mocked => {
+ return {
+ setProvider: jest.fn(),
+ appendContext: jest.fn().mockImplementation(Promise.resolve),
+ };
+};
+
+const createFeatureFlagsStart = (): jest.Mocked => {
+ return {
+ appendContext: jest.fn().mockImplementation(Promise.resolve),
+ getBooleanValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getNumberValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getStringValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getBooleanValue$: jest.fn().mockImplementation((_, fallback) => of(fallback)),
+ getStringValue$: jest.fn().mockImplementation((_, fallback) => of(fallback)),
+ getNumberValue$: jest.fn().mockImplementation((_, fallback) => of(fallback)),
+ };
+};
+
+const createFeatureFlagsServiceMock = (): jest.Mocked> => {
+ return {
+ setup: jest.fn().mockImplementation(createFeatureFlagsSetup),
+ start: jest.fn().mockImplementation(async () => createFeatureFlagsStart()),
+ stop: jest.fn().mockImplementation(Promise.resolve),
+ };
+};
+
+/**
+ * Mocks for the Feature Flags service (browser-side)
+ */
+export const coreFeatureFlagsMock = {
+ /**
+ * Mocks the entire feature flags service
+ */
+ create: createFeatureFlagsServiceMock,
+ /**
+ * Mocks the setup contract
+ */
+ createSetup: createFeatureFlagsSetup,
+ /**
+ * Mocks the start contract
+ */
+ createStart: createFeatureFlagsStart,
+};
diff --git a/packages/core/feature-flags/core-feature-flags-browser-mocks/jest.config.js b/packages/core/feature-flags/core-feature-flags-browser-mocks/jest.config.js
new file mode 100644
index 0000000000000..f259faecb6046
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-mocks/jest.config.js
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../..',
+ roots: ['/packages/core/feature-flags/core-feature-flags-browser-mocks'],
+};
diff --git a/packages/core/feature-flags/core-feature-flags-browser-mocks/kibana.jsonc b/packages/core/feature-flags/core-feature-flags-browser-mocks/kibana.jsonc
new file mode 100644
index 0000000000000..0917a098841c4
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-mocks/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/core-feature-flags-browser-mocks",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser-mocks/package.json b/packages/core/feature-flags/core-feature-flags-browser-mocks/package.json
new file mode 100644
index 0000000000000..77e9150ce7834
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-mocks/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-feature-flags-browser-mocks",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-browser-mocks/tsconfig.json b/packages/core/feature-flags/core-feature-flags-browser-mocks/tsconfig.json
new file mode 100644
index 0000000000000..b7d1b3ca28cbb
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser-mocks/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/core-feature-flags-browser",
+ "@kbn/core-feature-flags-browser-internal",
+ "@kbn/utility-types",
+ ]
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser/README.md b/packages/core/feature-flags/core-feature-flags-browser/README.md
new file mode 100644
index 0000000000000..5a6743adc5a09
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser/README.md
@@ -0,0 +1,3 @@
+# @kbn/core-feature-flags-browser
+
+Browser-side type definitions for the Feature Flags Service.
diff --git a/packages/core/feature-flags/core-feature-flags-browser/index.ts b/packages/core/feature-flags/core-feature-flags-browser/index.ts
new file mode 100644
index 0000000000000..6c79c96f01878
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export type {
+ EvaluationContext,
+ MultiContextEvaluationContext,
+ SingleContextEvaluationContext,
+ FeatureFlagsSetup,
+ FeatureFlagsStart,
+} from './src/types';
diff --git a/packages/core/feature-flags/core-feature-flags-browser/kibana.jsonc b/packages/core/feature-flags/core-feature-flags-browser/kibana.jsonc
new file mode 100644
index 0000000000000..56187119509b9
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-browser",
+ "id": "@kbn/core-feature-flags-browser",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser/package.json b/packages/core/feature-flags/core-feature-flags-browser/package.json
new file mode 100644
index 0000000000000..235f52c0521f1
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-feature-flags-browser",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-browser/src/types.ts b/packages/core/feature-flags/core-feature-flags-browser/src/types.ts
new file mode 100644
index 0000000000000..844675aab4603
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser/src/types.ts
@@ -0,0 +1,165 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { Provider } from '@openfeature/web-sdk';
+import { type EvaluationContext as OpenFeatureEvaluationContext } from '@openfeature/core';
+import type { Observable } from 'rxjs';
+
+/**
+ * The evaluation context to use when retrieving the flags.
+ *
+ * We use multi-context so that we can apply segmentation rules at different levels (`organization`/`kibana`).
+ * * `organization` includes any information that is common to all the projects/deployments in an organization. An example is the in_trial status.
+ * * The `kibana` context includes all the information that identifies a project/deployment. Examples are version, offering, and has_data.
+ * Kind helps us specify which sub-context should receive the new attributes.
+ * If no `kind` is provided, it defaults to `kibana`.
+ *
+ * @example Providing properties for both contexts
+ * {
+ * kind: 'multi',
+ * organization: {
+ * key: 1234,
+ * in_trial: true,
+ * },
+ * kibana: {
+ * key: 12345567890,
+ * version: 8.15.0,
+ * buildHash: 'ffffffffaaaaaaaa',
+ * },
+ * }
+ *
+ * @example Appending context to the organization sub-context
+ * {
+ * kind: 'organization',
+ * key: 1234,
+ * in_trial: true,
+ * }
+ *
+ * @example Appending context to the `kibana` sub-context
+ * {
+ * key: 12345567890,
+ * version: 8.15.0,
+ * buildHash: 'ffffffffaaaaaaaa',
+ * }
+ * }
+ *
+ * @public
+ */
+export type EvaluationContext = MultiContextEvaluationContext | SingleContextEvaluationContext;
+
+/**
+ * Multi-context format. The sub-contexts are provided in their nested properties.
+ * @public
+ */
+export type MultiContextEvaluationContext = OpenFeatureEvaluationContext & {
+ /**
+ * Static `multi` string
+ */
+ kind: 'multi';
+ /**
+ * The Elastic Cloud organization-specific context.
+ */
+ organization?: OpenFeatureEvaluationContext;
+ /**
+ * The deployment/project-specific context.
+ */
+ kibana?: OpenFeatureEvaluationContext;
+};
+
+/**
+ * Single Context format. If `kind` is not specified, it applies to the `kibana` sub-context.
+ */
+export type SingleContextEvaluationContext = OpenFeatureEvaluationContext & {
+ /**
+ * The sub-context that it's updated. Defaults to `kibana`.
+ */
+ kind?: 'organization' | 'kibana';
+};
+
+/**
+ * Setup contract of the Feature Flags Service
+ * @public
+ */
+export interface FeatureFlagsSetup {
+ /**
+ * Registers an OpenFeature provider to talk to the
+ * 3rd-party service that manages the Feature Flags.
+ * @param provider The {@link Provider | OpenFeature Provider} to handle the communication with the feature flags management system.
+ * @public
+ */
+ setProvider(provider: Provider): void;
+
+ /**
+ * Appends new keys to the evaluation context.
+ * @param contextToAppend The additional keys that should be appended/modified in the evaluation context.
+ * @public
+ */
+ appendContext(contextToAppend: EvaluationContext): Promise;
+}
+
+/**
+ * Setup contract of the Feature Flags Service
+ * @public
+ */
+export interface FeatureFlagsStart {
+ /**
+ * Appends new keys to the evaluation context.
+ * @param contextToAppend The additional keys that should be appended/modified in the evaluation context.
+ * @public
+ */
+ appendContext(contextToAppend: EvaluationContext): Promise;
+
+ /**
+ * Evaluates a boolean flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getBooleanValue(flagName: string, fallbackValue: boolean): boolean;
+
+ /**
+ * Evaluates a string flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getStringValue(flagName: string, fallbackValue: Value): Value;
+
+ /**
+ * Evaluates a number flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getNumberValue(flagName: string, fallbackValue: Value): Value;
+
+ /**
+ * Returns an observable of a boolean flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getBooleanValue$(flagName: string, fallbackValue: boolean): Observable;
+
+ /**
+ * Returns an observable of a string flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getStringValue$(flagName: string, fallbackValue: Value): Observable;
+
+ /**
+ * Returns an observable of a number flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getNumberValue$(flagName: string, fallbackValue: Value): Observable;
+}
diff --git a/packages/core/feature-flags/core-feature-flags-browser/tsconfig.json b/packages/core/feature-flags/core-feature-flags-browser/tsconfig.json
new file mode 100644
index 0000000000000..9fa73d55be770
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-browser/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "node",
+ "react"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ "**/*.tsx",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": []
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/README.md b/packages/core/feature-flags/core-feature-flags-server-internal/README.md
new file mode 100644
index 0000000000000..288d47fdc95eb
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/README.md
@@ -0,0 +1,5 @@
+# @kbn/core-feature-flags-server-internal
+
+Internal implementation of the server-side Feature Flags Service.
+
+It should only be imported by _Core_ packages.
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/index.ts b/packages/core/feature-flags/core-feature-flags-server-internal/index.ts
new file mode 100644
index 0000000000000..97083327e609d
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export { featureFlagsConfig } from './src/feature_flags_config';
+export { FeatureFlagsService, type InternalFeatureFlagsSetup } from './src/feature_flags_service';
+export { CoreFeatureFlagsRouteHandlerContext } from './src/feature_flags_request_handler_context';
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/jest.config.js b/packages/core/feature-flags/core-feature-flags-server-internal/jest.config.js
new file mode 100644
index 0000000000000..67b65d2040c54
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/jest.config.js
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test/jest_node',
+ rootDir: '../../../..',
+ roots: ['/packages/core/feature-flags/core-feature-flags-server-internal'],
+};
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/kibana.jsonc b/packages/core/feature-flags/core-feature-flags-server-internal/kibana.jsonc
new file mode 100644
index 0000000000000..60a01597c0454
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-server",
+ "id": "@kbn/core-feature-flags-server-internal",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/package.json b/packages/core/feature-flags/core-feature-flags-server-internal/package.json
new file mode 100644
index 0000000000000..33383b043fa5c
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-feature-flags-server-internal",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_config.ts b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_config.ts
new file mode 100644
index 0000000000000..fe6725456806b
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_config.ts
@@ -0,0 +1,42 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
+import { schema } from '@kbn/config-schema';
+
+/**
+ * The definition of the validation config schema
+ * @private
+ */
+const configSchema = schema.object({
+ overrides: schema.maybe(schema.recordOf(schema.string(), schema.any())),
+});
+
+/**
+ * Type definition of the Feature Flags configuration
+ * @private
+ */
+export interface FeatureFlagsConfig {
+ overrides?: Record;
+}
+
+/**
+ * Config descriptor for the feature flags service
+ * @private
+ */
+export const featureFlagsConfig: ServiceConfigDescriptor = {
+ /**
+ * All config is prefixed by `feature_flags`
+ */
+ path: 'feature_flags',
+ /**
+ * The definition of the validation config schema
+ */
+ schema: configSchema,
+};
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_request_handler_context.ts b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_request_handler_context.ts
new file mode 100644
index 0000000000000..f0ac4da69b1a9
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_request_handler_context.ts
@@ -0,0 +1,33 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type {
+ FeatureFlagsRequestHandlerContext,
+ FeatureFlagsStart,
+} from '@kbn/core-feature-flags-server';
+
+/**
+ * The {@link FeatureFlagsRequestHandlerContext} implementation.
+ * @internal
+ */
+export class CoreFeatureFlagsRouteHandlerContext implements FeatureFlagsRequestHandlerContext {
+ constructor(private readonly featureFlags: FeatureFlagsStart) {}
+
+ public getBooleanValue(flagName: string, fallback: boolean): Promise {
+ return this.featureFlags.getBooleanValue(flagName, fallback);
+ }
+
+ public getStringValue(flagName: string, fallback: Value): Promise {
+ return this.featureFlags.getStringValue(flagName, fallback);
+ }
+
+ public getNumberValue(flagName: string, fallback: Value): Promise {
+ return this.featureFlags.getNumberValue(flagName, fallback);
+ }
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_service.test.ts b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_service.test.ts
new file mode 100644
index 0000000000000..7bad676b9528b
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_service.test.ts
@@ -0,0 +1,260 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { firstValueFrom } from 'rxjs';
+import apm from 'elastic-apm-node';
+import { type Client, OpenFeature, type Provider } from '@openfeature/server-sdk';
+import { mockCoreContext } from '@kbn/core-base-server-mocks';
+import { configServiceMock } from '@kbn/config-mocks';
+import type { FeatureFlagsStart } from '@kbn/core-feature-flags-server';
+import { FeatureFlagsService } from '..';
+
+describe('FeatureFlagsService Server', () => {
+ let featureFlagsService: FeatureFlagsService;
+ let featureFlagsClient: Client;
+
+ beforeEach(() => {
+ const getClientSpy = jest.spyOn(OpenFeature, 'getClient');
+ featureFlagsService = new FeatureFlagsService(
+ mockCoreContext.create({
+ configService: configServiceMock.create({
+ atPath: {
+ overrides: {
+ 'my-overridden-flag': true,
+ },
+ },
+ }),
+ })
+ );
+ featureFlagsClient = getClientSpy.mock.results[0].value;
+ });
+
+ afterEach(async () => {
+ await featureFlagsService.stop();
+ jest.clearAllMocks();
+ await OpenFeature.clearProviders();
+ });
+
+ describe('provider handling', () => {
+ test('appends a provider (no async operation)', () => {
+ expect.assertions(1);
+ const { setProvider } = featureFlagsService.setup();
+ const spy = jest.spyOn(OpenFeature, 'setProvider');
+ const fakeProvider = { metadata: { name: 'fake provider' } } as Provider;
+ setProvider(fakeProvider);
+ expect(spy).toHaveBeenCalledWith(fakeProvider);
+ });
+
+ test('throws an error if called twice', () => {
+ const { setProvider } = featureFlagsService.setup();
+ const fakeProvider = { metadata: { name: 'fake provider' } } as Provider;
+ setProvider(fakeProvider);
+ expect(() => setProvider(fakeProvider)).toThrowErrorMatchingInlineSnapshot(
+ `"A provider has already been set. This API cannot be called twice."`
+ );
+ });
+ });
+
+ describe('context handling', () => {
+ let setContextSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ setContextSpy = jest.spyOn(OpenFeature, 'setContext');
+ });
+
+ test('appends context to the provider', () => {
+ const { appendContext } = featureFlagsService.setup();
+ appendContext({ kind: 'multi' });
+ expect(setContextSpy).toHaveBeenCalledWith({ kind: 'multi' });
+ });
+
+ test('appends context to the provider (start method)', () => {
+ featureFlagsService.setup();
+ const { appendContext } = featureFlagsService.start();
+ appendContext({ kind: 'multi' });
+ expect(setContextSpy).toHaveBeenCalledWith({ kind: 'multi' });
+ });
+
+ test('full multi context pass-through', () => {
+ const { appendContext } = featureFlagsService.setup();
+ const context = {
+ kind: 'multi' as const,
+ kibana: {
+ key: 'kibana-1',
+ },
+ organization: {
+ key: 'organization-1',
+ },
+ };
+ appendContext(context);
+ expect(setContextSpy).toHaveBeenCalledWith(context);
+ });
+
+ test('appends to the existing context', () => {
+ const { appendContext } = featureFlagsService.setup();
+ const initialContext = {
+ kind: 'multi' as const,
+ kibana: {
+ key: 'kibana-1',
+ },
+ organization: {
+ key: 'organization-1',
+ },
+ };
+ appendContext(initialContext);
+ expect(setContextSpy).toHaveBeenCalledWith(initialContext);
+
+ appendContext({ kind: 'multi', kibana: { has_data: true } });
+ expect(setContextSpy).toHaveBeenCalledWith({
+ ...initialContext,
+ kibana: {
+ ...initialContext.kibana,
+ has_data: true,
+ },
+ });
+ });
+
+ test('converts single-contexts to multi-context', () => {
+ const { appendContext } = featureFlagsService.setup();
+ appendContext({ kind: 'organization', key: 'organization-1' });
+ expect(setContextSpy).toHaveBeenCalledWith({
+ kind: 'multi',
+ organization: {
+ key: 'organization-1',
+ },
+ });
+ });
+
+ test('if no `kind` provided, it defaults to the kibana context', () => {
+ const { appendContext } = featureFlagsService.setup();
+ appendContext({ key: 'key-1', has_data: false });
+ expect(setContextSpy).toHaveBeenCalledWith({
+ kind: 'multi',
+ kibana: {
+ key: 'key-1',
+ has_data: false,
+ },
+ });
+ });
+ });
+
+ describe('flag evaluation', () => {
+ let startContract: FeatureFlagsStart;
+ let apmSpy: jest.SpyInstance;
+ let addHandlerSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ addHandlerSpy = jest.spyOn(featureFlagsClient, 'addHandler');
+ featureFlagsService.setup();
+ startContract = featureFlagsService.start();
+ apmSpy = jest.spyOn(apm, 'addLabels');
+ });
+
+ // We don't need to test the client, just our APIs, so testing that it returns the fallback value should be enough.
+ test('get boolean flag', async () => {
+ const value = false;
+ await expect(startContract.getBooleanValue('my-flag', value)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ });
+
+ test('get string flag', async () => {
+ const value = 'my-default';
+ await expect(startContract.getStringValue('my-flag', value)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ });
+
+ test('get number flag', async () => {
+ const value = 42;
+ await expect(startContract.getNumberValue('my-flag', value)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ });
+
+ test('observe a boolean flag', async () => {
+ const value = false;
+ const flag$ = startContract.getBooleanValue$('my-flag', value);
+ const observedValues: boolean[] = [];
+ flag$.subscribe((v) => observedValues.push(v));
+ // Initial emission
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ expect(observedValues).toHaveLength(1);
+
+ // Does not reevaluate and emit if the other flags are changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['another-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(1); // still 1
+
+ // Reevaluates and emits when the observed flag is changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(2);
+ });
+
+ test('observe a string flag', async () => {
+ const value = 'my-value';
+ const flag$ = startContract.getStringValue$('my-flag', value);
+ const observedValues: string[] = [];
+ flag$.subscribe((v) => observedValues.push(v));
+ // Initial emission
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ expect(observedValues).toHaveLength(1);
+
+ // Does not reevaluate and emit if the other flags are changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['another-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(1); // still 1
+
+ // Reevaluates and emits when the observed flag is changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(2);
+ });
+
+ test('observe a number flag', async () => {
+ const value = 42;
+ const flag$ = startContract.getNumberValue$('my-flag', value);
+ const observedValues: number[] = [];
+ flag$.subscribe((v) => observedValues.push(v));
+ // Initial emission
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-flag': value });
+ expect(observedValues).toHaveLength(1);
+
+ // Does not reevaluate and emit if the other flags are changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['another-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(1); // still 1
+
+ // Reevaluates and emits when the observed flag is changed
+ addHandlerSpy.mock.calls[0][1]({ flagsChanged: ['my-flag'] });
+ await expect(firstValueFrom(flag$)).resolves.toEqual(value);
+ expect(observedValues).toHaveLength(2);
+ });
+
+ test('with overrides', async () => {
+ const getBooleanValueSpy = jest.spyOn(featureFlagsClient, 'getBooleanValue');
+ await expect(startContract.getBooleanValue('my-overridden-flag', false)).resolves.toEqual(
+ true
+ );
+ expect(apmSpy).toHaveBeenCalledWith({ 'flag_my-overridden-flag': true });
+ expect(getBooleanValueSpy).not.toHaveBeenCalled();
+
+ // Only to prove the spy works
+ await expect(startContract.getBooleanValue('another-flag', false)).resolves.toEqual(false);
+ expect(getBooleanValueSpy).toHaveBeenCalledTimes(1);
+ expect(getBooleanValueSpy).toHaveBeenCalledWith('another-flag', false);
+ });
+ });
+
+ test('returns overrides', () => {
+ const { getOverrides } = featureFlagsService.setup();
+ expect(getOverrides()).toStrictEqual({ 'my-overridden-flag': true });
+ });
+});
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_service.ts b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_service.ts
new file mode 100644
index 0000000000000..7b01ebde731fe
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/src/feature_flags_service.ts
@@ -0,0 +1,196 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { CoreContext } from '@kbn/core-base-server-internal';
+import type {
+ EvaluationContext,
+ FeatureFlagsSetup,
+ FeatureFlagsStart,
+ MultiContextEvaluationContext,
+} from '@kbn/core-feature-flags-server';
+import type { Logger } from '@kbn/logging';
+import apm from 'elastic-apm-node';
+import {
+ type Client,
+ OpenFeature,
+ ServerProviderEvents,
+ NOOP_PROVIDER,
+} from '@openfeature/server-sdk';
+import deepMerge from 'deepmerge';
+import { filter, switchMap, startWith, Subject } from 'rxjs';
+import { type FeatureFlagsConfig, featureFlagsConfig } from './feature_flags_config';
+
+/**
+ * Core-internal contract for the setup lifecycle step.
+ * @private
+ */
+export interface InternalFeatureFlagsSetup extends FeatureFlagsSetup {
+ /**
+ * Used by the rendering service to share the overrides with the service on the browser side.
+ */
+ getOverrides: () => Record;
+}
+
+/**
+ * The server-side Feature Flags Service
+ * @private
+ */
+export class FeatureFlagsService {
+ private readonly featureFlagsClient: Client;
+ private readonly logger: Logger;
+ private overrides: Record = {};
+ private context: MultiContextEvaluationContext = { kind: 'multi' };
+
+ /**
+ * The core service's constructor
+ * @param core {@link CoreContext}
+ */
+ constructor(private readonly core: CoreContext) {
+ this.logger = core.logger.get('feature-flags-service');
+ this.featureFlagsClient = OpenFeature.getClient();
+ OpenFeature.setLogger(this.logger.get('open-feature'));
+ }
+
+ /**
+ * Setup lifecycle method
+ */
+ public setup(): InternalFeatureFlagsSetup {
+ // Register "overrides" to be changed via the dynamic config endpoint (enabled in test environments only)
+ this.core.configService.addDynamicConfigPaths(featureFlagsConfig.path, ['overrides']);
+
+ this.core.configService
+ .atPath(featureFlagsConfig.path)
+ .subscribe(({ overrides = {} }) => {
+ this.overrides = overrides;
+ });
+
+ return {
+ getOverrides: () => this.overrides,
+ setProvider: (provider) => {
+ if (OpenFeature.providerMetadata !== NOOP_PROVIDER.metadata) {
+ throw new Error('A provider has already been set. This API cannot be called twice.');
+ }
+ OpenFeature.setProvider(provider);
+ },
+ appendContext: (contextToAppend) => this.appendContext(contextToAppend),
+ };
+ }
+
+ /**
+ * Start lifecycle method
+ */
+ public start(): FeatureFlagsStart {
+ const featureFlagsChanged$ = new Subject();
+ this.featureFlagsClient.addHandler(ServerProviderEvents.ConfigurationChanged, (event) => {
+ if (event?.flagsChanged) {
+ featureFlagsChanged$.next(event.flagsChanged);
+ }
+ });
+ const observeFeatureFlag$ = (flagName: string) =>
+ featureFlagsChanged$.pipe(
+ filter((flagNames) => flagNames.includes(flagName)),
+ startWith([flagName]) // only to emit on the first call
+ );
+
+ return {
+ appendContext: (contextToAppend) => this.appendContext(contextToAppend),
+ getBooleanValue: async (flagName, fallbackValue) =>
+ this.evaluateFlag(this.featureFlagsClient.getBooleanValue, flagName, fallbackValue),
+ getStringValue: async (flagName: string, fallbackValue: Value) =>
+ await this.evaluateFlag(
+ this.featureFlagsClient.getStringValue,
+ flagName,
+ fallbackValue
+ ),
+ getNumberValue: async (flagName: string, fallbackValue: Value) =>
+ await this.evaluateFlag(
+ this.featureFlagsClient.getNumberValue,
+ flagName,
+ fallbackValue
+ ),
+ getBooleanValue$: (flagName, fallbackValue) => {
+ return observeFeatureFlag$(flagName).pipe(
+ switchMap(() =>
+ this.evaluateFlag(this.featureFlagsClient.getBooleanValue, flagName, fallbackValue)
+ )
+ );
+ },
+ getStringValue$: (flagName: string, fallbackValue: Value) => {
+ return observeFeatureFlag$(flagName).pipe(
+ switchMap(() =>
+ this.evaluateFlag(
+ this.featureFlagsClient.getStringValue,
+ flagName,
+ fallbackValue
+ )
+ )
+ );
+ },
+ getNumberValue$: (flagName: string, fallbackValue: Value) => {
+ return observeFeatureFlag$(flagName).pipe(
+ switchMap(() =>
+ this.evaluateFlag(
+ this.featureFlagsClient.getNumberValue,
+ flagName,
+ fallbackValue
+ )
+ )
+ );
+ },
+ };
+ }
+
+ /**
+ * Stop lifecycle method
+ */
+ public async stop() {
+ await OpenFeature.close();
+ }
+
+ /**
+ * Wrapper to evaluate flags with the common config overrides interceptions + APM and counters reporting
+ * @param evaluationFn The actual evaluation API
+ * @param flagName The name of the flag to evaluate
+ * @param fallbackValue The fallback value
+ * @private
+ */
+ private async evaluateFlag(
+ evaluationFn: (flagName: string, fallbackValue: T) => Promise,
+ flagName: string,
+ fallbackValue: T
+ ): Promise {
+ const value =
+ typeof this.overrides[flagName] !== 'undefined'
+ ? (this.overrides[flagName] 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 });
+ // TODO: increment usage counter
+ return value;
+ }
+
+ /**
+ * Formats the provided context to fulfill the expected multi-context structure.
+ * @param contextToAppend The {@link EvaluationContext} to append.
+ * @private
+ */
+ private appendContext(contextToAppend: EvaluationContext): void {
+ // If no kind provided, default to the project|deployment level.
+ const { kind = 'kibana', ...rest } = contextToAppend;
+ // Format the context to fulfill the expected multi-context structure
+ const formattedContextToAppend: MultiContextEvaluationContext =
+ kind === 'multi'
+ ? (contextToAppend as MultiContextEvaluationContext)
+ : { kind: 'multi', [kind]: rest };
+
+ // Merge the formatted context to append to the global context, and set it in the OpenFeature client.
+ this.context = deepMerge(this.context, formattedContextToAppend);
+ OpenFeature.setContext(this.context);
+ }
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server-internal/tsconfig.json b/packages/core/feature-flags/core-feature-flags-server-internal/tsconfig.json
new file mode 100644
index 0000000000000..72a97ef56eb4f
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-internal/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/core-base-server-internal",
+ "@kbn/core-feature-flags-server",
+ "@kbn/logging",
+ "@kbn/core-base-server-mocks",
+ "@kbn/config-schema",
+ "@kbn/config-mocks",
+ ]
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server-mocks/README.md b/packages/core/feature-flags/core-feature-flags-server-mocks/README.md
new file mode 100644
index 0000000000000..caf2c4a13f8fb
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-mocks/README.md
@@ -0,0 +1,3 @@
+# @kbn/core-feature-flags-server-mocks
+
+Server-side Jest mocks for the Feature Flags Service.
diff --git a/packages/core/feature-flags/core-feature-flags-server-mocks/index.ts b/packages/core/feature-flags/core-feature-flags-server-mocks/index.ts
new file mode 100644
index 0000000000000..182f6dbc21102
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-mocks/index.ts
@@ -0,0 +1,88 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { PublicMethodsOf } from '@kbn/utility-types';
+import type {
+ FeatureFlagsRequestHandlerContext,
+ FeatureFlagsSetup,
+ FeatureFlagsStart,
+} from '@kbn/core-feature-flags-server';
+import type {
+ FeatureFlagsService,
+ InternalFeatureFlagsSetup,
+} from '@kbn/core-feature-flags-server-internal';
+import { of } from 'rxjs';
+
+const createFeatureFlagsInternalSetup = (): jest.Mocked => {
+ return {
+ ...createFeatureFlagsSetup(),
+ getOverrides: jest.fn().mockReturnValue({}),
+ };
+};
+
+const createFeatureFlagsSetup = (): jest.Mocked => {
+ return {
+ setProvider: jest.fn(),
+ appendContext: jest.fn(),
+ };
+};
+
+const createFeatureFlagsStart = (): jest.Mocked => {
+ return {
+ appendContext: jest.fn(),
+ getBooleanValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getNumberValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getStringValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getBooleanValue$: jest.fn().mockImplementation((_, fallback) => of(fallback)),
+ getStringValue$: jest.fn().mockImplementation((_, fallback) => of(fallback)),
+ getNumberValue$: jest.fn().mockImplementation((_, fallback) => of(fallback)),
+ };
+};
+
+const createRequestHandlerContext = (): jest.Mocked => {
+ return {
+ getBooleanValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getNumberValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ getStringValue: jest.fn().mockImplementation(async (_, fallback) => fallback),
+ };
+};
+
+const createFeatureFlagsServiceMock = (): jest.Mocked> => {
+ return {
+ setup: jest.fn().mockImplementation(createFeatureFlagsInternalSetup),
+ start: jest.fn().mockImplementation(createFeatureFlagsStart),
+ stop: jest.fn().mockImplementation(Promise.resolve),
+ };
+};
+
+/**
+ * Mocks for the Feature Flags service (browser-side)
+ */
+export const coreFeatureFlagsMock = {
+ /**
+ * Mocks the entire feature flags service
+ */
+ create: createFeatureFlagsServiceMock,
+ /**
+ * Mocks the core-internal setup contract
+ */
+ createInternalSetup: createFeatureFlagsInternalSetup,
+ /**
+ * Mocks the setup contract
+ */
+ createSetup: createFeatureFlagsSetup,
+ /**
+ * Mocks the start contract
+ */
+ createStart: createFeatureFlagsStart,
+ /**
+ * Mocks the request handler context contract
+ */
+ createRequestHandlerContext,
+};
diff --git a/packages/core/feature-flags/core-feature-flags-server-mocks/jest.config.js b/packages/core/feature-flags/core-feature-flags-server-mocks/jest.config.js
new file mode 100644
index 0000000000000..bc50c37548c95
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-mocks/jest.config.js
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test/jest_node',
+ rootDir: '../../../..',
+ roots: ['/packages/core/feature-flags/core-feature-flags-server-mocks'],
+};
diff --git a/packages/core/feature-flags/core-feature-flags-server-mocks/kibana.jsonc b/packages/core/feature-flags/core-feature-flags-server-mocks/kibana.jsonc
new file mode 100644
index 0000000000000..69b03f0badbdc
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-mocks/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-server",
+ "id": "@kbn/core-feature-flags-server-mocks",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server-mocks/package.json b/packages/core/feature-flags/core-feature-flags-server-mocks/package.json
new file mode 100644
index 0000000000000..f009e55f76a8e
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-mocks/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-feature-flags-server-mocks",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-server-mocks/tsconfig.json b/packages/core/feature-flags/core-feature-flags-server-mocks/tsconfig.json
new file mode 100644
index 0000000000000..c672eb28c83a9
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server-mocks/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/utility-types",
+ "@kbn/core-feature-flags-server",
+ "@kbn/core-feature-flags-server-internal",
+ ]
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server/README.md b/packages/core/feature-flags/core-feature-flags-server/README.md
new file mode 100644
index 0000000000000..86b6fc210d0d4
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/README.md
@@ -0,0 +1,3 @@
+# @kbn/core-feature-flags-server
+
+Server-side type definitions for the Feature Flags Service.
diff --git a/packages/core/feature-flags/core-feature-flags-server/index.ts b/packages/core/feature-flags/core-feature-flags-server/index.ts
new file mode 100644
index 0000000000000..7538b68686cd9
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/index.ts
@@ -0,0 +1,18 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+export type {
+ EvaluationContext,
+ MultiContextEvaluationContext,
+ SingleContextEvaluationContext,
+ FeatureFlagsSetup,
+ FeatureFlagsStart,
+} from './src/contracts';
+export type { FeatureFlagDefinition, FeatureFlagDefinitions } from './src/feature_flag_definition';
+export type { FeatureFlagsRequestHandlerContext } from './src/request_handler_context';
diff --git a/packages/core/feature-flags/core-feature-flags-server/kibana.jsonc b/packages/core/feature-flags/core-feature-flags-server/kibana.jsonc
new file mode 100644
index 0000000000000..dc896ed83b97b
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-server",
+ "id": "@kbn/core-feature-flags-server",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server/package.json b/packages/core/feature-flags/core-feature-flags-server/package.json
new file mode 100644
index 0000000000000..d1f18a98a3840
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/core-feature-flags-server",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
\ No newline at end of file
diff --git a/packages/core/feature-flags/core-feature-flags-server/src/contracts.ts b/packages/core/feature-flags/core-feature-flags-server/src/contracts.ts
new file mode 100644
index 0000000000000..34fc3a3a73383
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/src/contracts.ts
@@ -0,0 +1,165 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { Provider } from '@openfeature/server-sdk';
+import { type EvaluationContext as OpenFeatureEvaluationContext } from '@openfeature/core';
+import type { Observable } from 'rxjs';
+
+/**
+ * The evaluation context to use when retrieving the flags.
+ *
+ * We use multi-context so that we can apply segmentation rules at different levels (`organization`/`kibana`).
+ * * `organization` includes any information that is common to all the projects/deployments in an organization. An example is the in_trial status.
+ * * The `kibana` context includes all the information that identifies a project/deployment. Examples are version, offering, and has_data.
+ * Kind helps us specify which sub-context should receive the new attributes.
+ * If no `kind` is provided, it defaults to `kibana`.
+ *
+ * @example Providing properties for both contexts
+ * {
+ * kind: 'multi',
+ * organization: {
+ * key: 1234,
+ * in_trial: true,
+ * },
+ * kibana: {
+ * key: 12345567890,
+ * version: 8.15.0,
+ * buildHash: 'ffffffffaaaaaaaa',
+ * },
+ * }
+ *
+ * @example Appending context to the organization sub-context
+ * {
+ * kind: 'organization',
+ * key: 1234,
+ * in_trial: true,
+ * }
+ *
+ * @example Appending context to the `kibana` sub-context
+ * {
+ * key: 12345567890,
+ * version: 8.15.0,
+ * buildHash: 'ffffffffaaaaaaaa',
+ * }
+ * }
+ *
+ * @public
+ */
+export type EvaluationContext = MultiContextEvaluationContext | SingleContextEvaluationContext;
+
+/**
+ * Multi-context format. The sub-contexts are provided in their nested properties.
+ * @public
+ */
+export type MultiContextEvaluationContext = OpenFeatureEvaluationContext & {
+ /**
+ * Static `multi` string
+ */
+ kind: 'multi';
+ /**
+ * The Elastic Cloud organization-specific context.
+ */
+ organization?: OpenFeatureEvaluationContext;
+ /**
+ * The deployment/project-specific context.
+ */
+ kibana?: OpenFeatureEvaluationContext;
+};
+
+/**
+ * Single Context format. If `kind` is not specified, it applies to the `kibana` sub-context.
+ */
+export type SingleContextEvaluationContext = OpenFeatureEvaluationContext & {
+ /**
+ * The sub-context that it's updated. Defaults to `kibana`.
+ */
+ kind?: 'organization' | 'kibana';
+};
+
+/**
+ * Setup contract of the Feature Flags Service
+ * @public
+ */
+export interface FeatureFlagsSetup {
+ /**
+ * Registers an OpenFeature provider to talk to the
+ * 3rd-party service that manages the Feature Flags.
+ * @param provider The {@link Provider | OpenFeature Provider} to handle the communication with the feature flags management system.
+ * @public
+ */
+ setProvider(provider: Provider): void;
+
+ /**
+ * Appends new keys to the evaluation context.
+ * @param contextToAppend The additional keys that should be appended/modified in the evaluation context.
+ * @public
+ */
+ appendContext(contextToAppend: EvaluationContext): void;
+}
+
+/**
+ * Setup contract of the Feature Flags Service
+ * @public
+ */
+export interface FeatureFlagsStart {
+ /**
+ * Appends new keys to the evaluation context.
+ * @param contextToAppend The additional keys that should be appended/modified in the evaluation context.
+ * @public
+ */
+ appendContext(contextToAppend: EvaluationContext): void;
+
+ /**
+ * Evaluates a boolean flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getBooleanValue(flagName: string, fallbackValue: boolean): Promise;
+
+ /**
+ * Evaluates a string flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getStringValue(flagName: string, fallbackValue: Value): Promise;
+
+ /**
+ * Evaluates a number flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getNumberValue(flagName: string, fallbackValue: Value): Promise;
+
+ /**
+ * Returns an observable of a boolean flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getBooleanValue$(flagName: string, fallbackValue: boolean): Observable;
+
+ /**
+ * Returns an observable of a string flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getStringValue$(flagName: string, fallbackValue: Value): Observable;
+
+ /**
+ * Returns an observable of a number flag
+ * @param flagName The flag ID to evaluate
+ * @param fallbackValue If the flag cannot be evaluated for whatever reason, the fallback value is provided.
+ * @public
+ */
+ getNumberValue$(flagName: string, fallbackValue: Value): Observable;
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server/src/feature_flag_definition.ts b/packages/core/feature-flags/core-feature-flags-server/src/feature_flag_definition.ts
new file mode 100644
index 0000000000000..3ea761484fc2a
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/src/feature_flag_definition.ts
@@ -0,0 +1,60 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+/**
+ * List of {@link FeatureFlagDefinition}
+ */
+export type FeatureFlagDefinitions = Array<
+ | FeatureFlagDefinition<'boolean'>
+ | FeatureFlagDefinition<'string'>
+ | FeatureFlagDefinition<'number'>
+>;
+
+/**
+ * Definition of a feature flag
+ */
+export interface FeatureFlagDefinition {
+ /**
+ * The ID of the feature flag. Used to reference it when evaluating the flag.
+ */
+ key: string;
+ /**
+ * Human friendly name.
+ */
+ name: string;
+ /**
+ * Description of the purpose of the feature flag.
+ */
+ description?: string;
+ /**
+ * Tags to apply to the feature flag for easier categorizing. It may include the plugin, the solution, the team.
+ */
+ tags: string[];
+ /**
+ * The type of the values returned by the feature flag ("string", "boolean", or "number").
+ */
+ variationType: ValueType;
+ /**
+ * List of variations of the feature flags.
+ */
+ variations: Array<{
+ /**
+ * Human friendly name of the variation.
+ */
+ name: string;
+ /**
+ * Description of the variation.
+ */
+ description?: string;
+ /**
+ * The value of the variation.
+ */
+ value: ValueType extends 'string' ? string : ValueType extends 'boolean' ? boolean : number;
+ }>;
+}
diff --git a/packages/core/feature-flags/core-feature-flags-server/src/request_handler_context.ts b/packages/core/feature-flags/core-feature-flags-server/src/request_handler_context.ts
new file mode 100644
index 0000000000000..25f521e18f1c9
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/src/request_handler_context.ts
@@ -0,0 +1,18 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { FeatureFlagsStart } from '..';
+
+/**
+ * The HTTP request handler context for evaluating feature flags
+ */
+export type FeatureFlagsRequestHandlerContext = Pick<
+ FeatureFlagsStart,
+ 'getBooleanValue' | 'getStringValue' | 'getNumberValue'
+>;
diff --git a/packages/core/feature-flags/core-feature-flags-server/tsconfig.json b/packages/core/feature-flags/core-feature-flags-server/tsconfig.json
new file mode 100644
index 0000000000000..f5bb1b00512e4
--- /dev/null
+++ b/packages/core/feature-flags/core-feature-flags-server/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": []
+}
diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts
index 2e1e3b790b55a..376eb5a2bd24f 100644
--- a/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts
+++ b/packages/core/http/core-http-request-handler-context-server-internal/src/core_route_handler_context.ts
@@ -33,12 +33,15 @@ import {
CoreUserProfileRouteHandlerContext,
type InternalUserProfileServiceStart,
} from '@kbn/core-user-profile-server-internal';
+import { CoreFeatureFlagsRouteHandlerContext } from '@kbn/core-feature-flags-server-internal';
+import type { FeatureFlagsStart } from '@kbn/core-feature-flags-server';
/**
* Subset of `InternalCoreStart` used by {@link CoreRouteHandlerContext}
* @internal
*/
export interface CoreRouteHandlerContextParams {
+ featureFlags: FeatureFlagsStart;
elasticsearch: InternalElasticsearchServiceStart;
savedObjects: InternalSavedObjectsServiceStart;
uiSettings: InternalUiSettingsServiceStart;
@@ -53,6 +56,7 @@ export interface CoreRouteHandlerContextParams {
* @internal
*/
export class CoreRouteHandlerContext implements CoreRequestHandlerContext {
+ #featureFlags?: CoreFeatureFlagsRouteHandlerContext;
#elasticsearch?: CoreElasticsearchRouteHandlerContext;
#savedObjects?: CoreSavedObjectsRouteHandlerContext;
#uiSettings?: CoreUiSettingsRouteHandlerContext;
@@ -65,6 +69,13 @@ export class CoreRouteHandlerContext implements CoreRequestHandlerContext {
private readonly request: KibanaRequest
) {}
+ public get featureFlags() {
+ if (!this.#featureFlags) {
+ this.#featureFlags = new CoreFeatureFlagsRouteHandlerContext(this.coreStart.featureFlags);
+ }
+ return this.#featureFlags;
+ }
+
public get elasticsearch() {
if (!this.#elasticsearch) {
this.#elasticsearch = new CoreElasticsearchRouteHandlerContext(
diff --git a/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts
index 1839f51a68c62..01b3c7aee4656 100644
--- a/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts
+++ b/packages/core/http/core-http-request-handler-context-server-internal/src/test_helpers/core_route_handler_context_params.mock.ts
@@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks';
import { savedObjectsServiceMock } from '@kbn/core-saved-objects-server-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-server-mocks';
@@ -16,6 +17,7 @@ import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
export const createCoreRouteHandlerContextParamsMock = () => {
return {
+ featureFlags: coreFeatureFlagsMock.createStart(),
elasticsearch: elasticsearchServiceMock.createInternalStart(),
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
uiSettings: uiSettingsServiceMock.createStartContract(),
diff --git a/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json b/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json
index 9e5ab96901e86..99c86608d99de 100644
--- a/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json
+++ b/packages/core/http/core-http-request-handler-context-server-internal/tsconfig.json
@@ -27,6 +27,9 @@
"@kbn/core-security-server-mocks",
"@kbn/core-user-profile-server-internal",
"@kbn/core-user-profile-server-mocks",
+ "@kbn/core-feature-flags-server-internal",
+ "@kbn/core-feature-flags-server",
+ "@kbn/core-feature-flags-server-mocks",
],
"exclude": [
"target/**/*",
diff --git a/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts b/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts
index 8f2755403e1d8..294adf380d8e1 100644
--- a/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts
+++ b/packages/core/http/core-http-request-handler-context-server/src/preboot_request_handler_context.ts
@@ -11,22 +11,34 @@ import type { RequestHandlerContextBase } from '@kbn/core-http-server';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-server';
/**
+ * `uiSettings` http request context provider during the preboot phase.
* @public
*/
export interface PrebootUiSettingsRequestHandlerContext {
+ /**
+ * The {@link IUiSettingsClient | UI Settings client}.
+ */
client: IUiSettingsClient;
}
/**
+ * The `core` context provided to route handler during the preboot phase.
* @public
*/
export interface PrebootCoreRequestHandlerContext {
+ /**
+ * {@link PrebootUiSettingsRequestHandlerContext}
+ */
uiSettings: PrebootUiSettingsRequestHandlerContext;
}
/**
+ * Base context passed to a route handler during the preboot phase, containing the `core` context part.
* @public
*/
export interface PrebootRequestHandlerContext extends RequestHandlerContextBase {
+ /**
+ * Promise that resolves the {@link PrebootCoreRequestHandlerContext}
+ */
core: Promise;
}
diff --git a/packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts b/packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts
index 0dc5655c820fa..1d77e033b8e62 100644
--- a/packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts
+++ b/packages/core/http/core-http-request-handler-context-server/src/request_handler_context.ts
@@ -14,6 +14,7 @@ import type { DeprecationsRequestHandlerContext } from '@kbn/core-deprecations-s
import type { UiSettingsRequestHandlerContext } from '@kbn/core-ui-settings-server';
import type { SecurityRequestHandlerContext } from '@kbn/core-security-server';
import type { UserProfileRequestHandlerContext } from '@kbn/core-user-profile-server';
+import type { FeatureFlagsRequestHandlerContext } from '@kbn/core-feature-flags-server';
/**
* The `core` context provided to route handler.
@@ -30,11 +31,33 @@ import type { UserProfileRequestHandlerContext } from '@kbn/core-user-profile-se
* @public
*/
export interface CoreRequestHandlerContext {
+ /**
+ * {@link SavedObjectsRequestHandlerContext}
+ */
savedObjects: SavedObjectsRequestHandlerContext;
+ /**
+ * {@link ElasticsearchRequestHandlerContext}
+ */
elasticsearch: ElasticsearchRequestHandlerContext;
+ /**
+ * {@link FeatureFlagsRequestHandlerContext}
+ */
+ featureFlags: FeatureFlagsRequestHandlerContext;
+ /**
+ * {@link UiSettingsRequestHandlerContext}
+ */
uiSettings: UiSettingsRequestHandlerContext;
+ /**
+ * {@link DeprecationsRequestHandlerContext}
+ */
deprecations: DeprecationsRequestHandlerContext;
+ /**
+ * {@link SecurityRequestHandlerContext}
+ */
security: SecurityRequestHandlerContext;
+ /**
+ * {@link UserProfileRequestHandlerContext}
+ */
userProfile: UserProfileRequestHandlerContext;
}
@@ -44,6 +67,9 @@ export interface CoreRequestHandlerContext {
* @public
*/
export interface RequestHandlerContext extends RequestHandlerContextBase {
+ /**
+ * Promise that resolves the {@link CoreRequestHandlerContext}
+ */
core: Promise;
}
diff --git a/packages/core/http/core-http-request-handler-context-server/tsconfig.json b/packages/core/http/core-http-request-handler-context-server/tsconfig.json
index 4606770c753d7..905a13801f223 100644
--- a/packages/core/http/core-http-request-handler-context-server/tsconfig.json
+++ b/packages/core/http/core-http-request-handler-context-server/tsconfig.json
@@ -17,7 +17,8 @@
"@kbn/core-deprecations-server",
"@kbn/core-ui-settings-server",
"@kbn/core-security-server",
- "@kbn/core-user-profile-server"
+ "@kbn/core-user-profile-server",
+ "@kbn/core-feature-flags-server"
],
"exclude": [
"target/**/*",
diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts
index dd1e768a64709..dd315b38fe3c2 100644
--- a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts
+++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.test.ts
@@ -9,6 +9,7 @@
import type { DiscoveredPlugin } from '@kbn/core-base-common';
import { InjectedMetadataService } from './injected_metadata_service';
+import type { InjectedMetadataParams } from '..';
describe('setup.getElasticsearchInfo()', () => {
it('returns elasticsearch info from injectedMetadata', () => {
@@ -160,3 +161,29 @@ describe('setup.getLegacyMetadata()', () => {
}).toThrowError();
});
});
+
+describe('setup.getFeatureFlags()', () => {
+ it('returns injectedMetadata.featureFlags', () => {
+ const injectedMetadata = new InjectedMetadataService({
+ injectedMetadata: {
+ featureFlags: {
+ overrides: {
+ 'my-overridden-flag': 1234,
+ },
+ },
+ },
+ } as unknown as InjectedMetadataParams);
+
+ const contract = injectedMetadata.setup();
+ expect(contract.getFeatureFlags()).toStrictEqual({ overrides: { 'my-overridden-flag': 1234 } });
+ });
+
+ it('returns empty injectedMetadata.featureFlags', () => {
+ const injectedMetadata = new InjectedMetadataService({
+ injectedMetadata: {},
+ } as unknown as InjectedMetadataParams);
+
+ const contract = injectedMetadata.setup();
+ expect(contract.getFeatureFlags()).toBeUndefined();
+ });
+});
diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts
index 624c213ce11bc..b9594b9f042e3 100644
--- a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts
+++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/injected_metadata_service.ts
@@ -95,6 +95,10 @@ export class InjectedMetadataService {
getCustomBranding: () => {
return this.state.customBranding;
},
+
+ getFeatureFlags: () => {
+ return this.state.featureFlags;
+ },
};
}
}
diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts
index f8c730414746d..244b99da0c207 100644
--- a/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts
+++ b/packages/core/injected-metadata/core-injected-metadata-browser-internal/src/types.ts
@@ -58,6 +58,11 @@ export interface InternalInjectedMetadataSetup {
};
};
getCustomBranding: () => CustomBranding;
+ getFeatureFlags: () =>
+ | {
+ overrides: Record;
+ }
+ | undefined;
}
/** @internal */
diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts
index 9ee48eda09210..804134cabd4b9 100644
--- a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts
+++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts
@@ -30,6 +30,7 @@ const createSetupContractMock = () => {
getPlugins: jest.fn(),
getKibanaBuildNumber: jest.fn(),
getCustomBranding: jest.fn(),
+ getFeatureFlags: jest.fn(),
};
setupContract.getBasePath.mockReturnValue('/base-path');
setupContract.getServerBasePath.mockReturnValue('/server-base-path');
diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts
index 498e1ff0f15e9..1ee75dbfc0d5d 100644
--- a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts
+++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts
@@ -63,6 +63,9 @@ export interface InjectedMetadata {
mode: EnvironmentMode;
packageInfo: PackageInfo;
};
+ featureFlags?: {
+ overrides: Record;
+ };
anonymousStatusPage: boolean;
i18n: {
translationsUrl: string;
diff --git a/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_setup.ts b/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_setup.ts
index 9730936cedb93..07695d37bea69 100644
--- a/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_setup.ts
+++ b/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_setup.ts
@@ -13,6 +13,7 @@ import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-
import type { InternalHttpSetup } from '@kbn/core-http-browser-internal';
import type { InternalSecurityServiceSetup } from '@kbn/core-security-browser-internal';
import type { InternalUserProfileServiceSetup } from '@kbn/core-user-profile-browser-internal';
+import type { FeatureFlagsSetup } from '@kbn/core-feature-flags-browser';
/** @internal */
export interface InternalCoreSetup
@@ -21,6 +22,7 @@ export interface InternalCoreSetup
'application' | 'plugins' | 'getStartServices' | 'http' | 'security' | 'userProfile'
> {
application: InternalApplicationSetup;
+ featureFlags: FeatureFlagsSetup;
injectedMetadata: InternalInjectedMetadataSetup;
http: InternalHttpSetup;
security: InternalSecurityServiceSetup;
diff --git a/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_start.ts b/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_start.ts
index 0571ca18955b1..f422275f53019 100644
--- a/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_start.ts
+++ b/packages/core/lifecycle/core-lifecycle-browser-internal/src/internal_core_start.ts
@@ -13,11 +13,13 @@ import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-
import type { InternalHttpStart } from '@kbn/core-http-browser-internal';
import type { InternalSecurityServiceStart } from '@kbn/core-security-browser-internal';
import type { InternalUserProfileServiceStart } from '@kbn/core-user-profile-browser-internal';
+import type { FeatureFlagsStart } from '@kbn/core-feature-flags-browser';
/** @internal */
export interface InternalCoreStart
extends Omit {
application: InternalApplicationStart;
+ featureFlags: FeatureFlagsStart;
injectedMetadata: InternalInjectedMetadataStart;
http: InternalHttpStart;
security: InternalSecurityServiceStart;
diff --git a/packages/core/lifecycle/core-lifecycle-browser-internal/tsconfig.json b/packages/core/lifecycle/core-lifecycle-browser-internal/tsconfig.json
index 4fd531018418d..84c55c0e87b90 100644
--- a/packages/core/lifecycle/core-lifecycle-browser-internal/tsconfig.json
+++ b/packages/core/lifecycle/core-lifecycle-browser-internal/tsconfig.json
@@ -17,7 +17,8 @@
"@kbn/core-injected-metadata-browser-internal",
"@kbn/core-http-browser-internal",
"@kbn/core-security-browser-internal",
- "@kbn/core-user-profile-browser-internal"
+ "@kbn/core-user-profile-browser-internal",
+ "@kbn/core-feature-flags-browser"
],
"exclude": [
"target/**/*",
diff --git a/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_setup.mock.ts b/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_setup.mock.ts
index 1a3f6e22ff39a..9937286104efe 100644
--- a/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_setup.mock.ts
+++ b/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_setup.mock.ts
@@ -21,6 +21,7 @@ import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-moc
import { securityServiceMock } from '@kbn/core-security-browser-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
import { createCoreStartMock } from './core_start.mock';
+import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-browser-mocks';
export function createCoreSetupMock({
basePath = '',
@@ -38,6 +39,7 @@ export function createCoreSetupMock({
docLinks: docLinksServiceMock.createSetupContract(),
executionContext: executionContextServiceMock.createSetupContract(),
fatalErrors: fatalErrorsServiceMock.createSetupContract(),
+ featureFlags: coreFeatureFlagsMock.createSetup(),
getStartServices: jest.fn, any, any]>, []>(() =>
Promise.resolve([createCoreStartMock({ basePath }), pluginStartDeps, pluginStartContract])
),
diff --git a/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_start.mock.ts b/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_start.mock.ts
index 71f5dd0db3958..f6f5aa5493f73 100644
--- a/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_start.mock.ts
+++ b/packages/core/lifecycle/core-lifecycle-browser-mocks/src/core_start.mock.ts
@@ -24,6 +24,7 @@ import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
import { securityServiceMock } from '@kbn/core-security-browser-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
+import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-browser-mocks';
export function createCoreStartMock({ basePath = '' } = {}) {
const mock = {
@@ -33,6 +34,7 @@ export function createCoreStartMock({ basePath = '' } = {}) {
customBranding: customBrandingServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
executionContext: executionContextServiceMock.createStartContract(),
+ featureFlags: coreFeatureFlagsMock.createStart(),
http: httpServiceMock.createStartContract({ basePath }),
i18n: i18nServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
diff --git a/packages/core/lifecycle/core-lifecycle-browser-mocks/tsconfig.json b/packages/core/lifecycle/core-lifecycle-browser-mocks/tsconfig.json
index b6df8220f8603..cc1f0ed785dbc 100644
--- a/packages/core/lifecycle/core-lifecycle-browser-mocks/tsconfig.json
+++ b/packages/core/lifecycle/core-lifecycle-browser-mocks/tsconfig.json
@@ -28,7 +28,8 @@
"@kbn/core-chrome-browser-mocks",
"@kbn/core-custom-branding-browser-mocks",
"@kbn/core-security-browser-mocks",
- "@kbn/core-user-profile-browser-mocks"
+ "@kbn/core-user-profile-browser-mocks",
+ "@kbn/core-feature-flags-browser-mocks"
],
"exclude": [
"target/**/*",
diff --git a/packages/core/lifecycle/core-lifecycle-browser/src/core_setup.ts b/packages/core/lifecycle/core-lifecycle-browser/src/core_setup.ts
index a989bdda60426..bef46aa4b84c1 100644
--- a/packages/core/lifecycle/core-lifecycle-browser/src/core_setup.ts
+++ b/packages/core/lifecycle/core-lifecycle-browser/src/core_setup.ts
@@ -10,6 +10,7 @@
import type { ThemeServiceSetup } from '@kbn/core-theme-browser';
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import type { ExecutionContextSetup } from '@kbn/core-execution-context-browser';
+import type { FeatureFlagsSetup } from '@kbn/core-feature-flags-browser';
import type { HttpSetup } from '@kbn/core-http-browser';
import type { FatalErrorsSetup } from '@kbn/core-fatal-errors-browser';
import type { IUiSettingsClient, SettingsStart } from '@kbn/core-ui-settings-browser';
@@ -44,6 +45,8 @@ export interface CoreSetup & {
elasticsearch: ReturnType;
@@ -61,6 +62,7 @@ export function createCoreSetupMock({
userSettings: userSettingsServiceMock.createSetupContract(),
docLinks: docLinksServiceMock.createSetupContract(),
elasticsearch: elasticsearchServiceMock.createSetup(),
+ featureFlags: coreFeatureFlagsMock.createSetup(),
http: httpMock,
i18n: i18nServiceMock.createSetupContract(),
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
diff --git a/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_start.mock.ts b/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_start.mock.ts
index 8bbdb322bb14a..d4b341bcf2f54 100644
--- a/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_start.mock.ts
+++ b/packages/core/lifecycle/core-lifecycle-server-mocks/src/core_start.mock.ts
@@ -22,6 +22,7 @@ import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
import { securityServiceMock } from '@kbn/core-security-server-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
+import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
export function createCoreStartMock() {
const mock: MockedKeys = {
@@ -29,6 +30,7 @@ export function createCoreStartMock() {
capabilities: capabilitiesServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
elasticsearch: elasticsearchServiceMock.createStart(),
+ featureFlags: coreFeatureFlagsMock.createStart(),
http: httpServiceMock.createStartContract(),
metrics: metricsServiceMock.createStartContract(),
savedObjects: savedObjectsServiceMock.createStartContract(),
diff --git a/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_setup.mock.ts b/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_setup.mock.ts
index 4919fa0c65eb5..4e6ca5b75059f 100644
--- a/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_setup.mock.ts
+++ b/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_setup.mock.ts
@@ -29,6 +29,7 @@ import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mock
import { userSettingsServiceMock } from '@kbn/core-user-settings-server-mocks';
import { securityServiceMock } from '@kbn/core-security-server-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
+import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
export function createInternalCoreSetupMock() {
const setupDeps = {
@@ -37,6 +38,7 @@ export function createInternalCoreSetupMock() {
context: contextServiceMock.createSetupContract(),
docLinks: docLinksServiceMock.createSetupContract(),
elasticsearch: elasticsearchServiceMock.createInternalSetup(),
+ featureFlags: coreFeatureFlagsMock.createInternalSetup(),
http: httpServiceMock.createInternalSetupContract(),
savedObjects: savedObjectsServiceMock.createInternalSetupContract(),
status: statusServiceMock.createInternalSetupContract(),
diff --git a/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_start.mock.ts b/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_start.mock.ts
index 4e0d63f654516..248a9712057f4 100644
--- a/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_start.mock.ts
+++ b/packages/core/lifecycle/core-lifecycle-server-mocks/src/internal_core_start.mock.ts
@@ -21,6 +21,7 @@ import { coreUsageDataServiceMock } from '@kbn/core-usage-data-server-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-server-mocks';
import { securityServiceMock } from '@kbn/core-security-server-mocks';
import { userProfileServiceMock } from '@kbn/core-user-profile-server-mocks';
+import { coreFeatureFlagsMock } from '@kbn/core-feature-flags-server-mocks';
export function createInternalCoreStartMock() {
const startDeps = {
@@ -28,6 +29,7 @@ export function createInternalCoreStartMock() {
capabilities: capabilitiesServiceMock.createStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
elasticsearch: elasticsearchServiceMock.createInternalStart(),
+ featureFlags: coreFeatureFlagsMock.createStart(),
http: httpServiceMock.createInternalStartContract(),
metrics: metricsServiceMock.createInternalStartContract(),
savedObjects: savedObjectsServiceMock.createInternalStartContract(),
diff --git a/packages/core/lifecycle/core-lifecycle-server-mocks/tsconfig.json b/packages/core/lifecycle/core-lifecycle-server-mocks/tsconfig.json
index bacda3278557b..89ec5b0e1b7ba 100644
--- a/packages/core/lifecycle/core-lifecycle-server-mocks/tsconfig.json
+++ b/packages/core/lifecycle/core-lifecycle-server-mocks/tsconfig.json
@@ -37,6 +37,7 @@
"@kbn/core-user-settings-server-mocks",
"@kbn/core-security-server-mocks",
"@kbn/core-user-profile-server-mocks",
+ "@kbn/core-feature-flags-server-mocks",
],
"exclude": [
"target/**/*",
diff --git a/packages/core/lifecycle/core-lifecycle-server/src/core_setup.ts b/packages/core/lifecycle/core-lifecycle-server/src/core_setup.ts
index 63f56f22cc145..59616a5d65ecd 100644
--- a/packages/core/lifecycle/core-lifecycle-server/src/core_setup.ts
+++ b/packages/core/lifecycle/core-lifecycle-server/src/core_setup.ts
@@ -13,6 +13,7 @@ import type { DeprecationsServiceSetup } from '@kbn/core-deprecations-server';
import type { DocLinksServiceSetup } from '@kbn/core-doc-links-server';
import type { ElasticsearchServiceSetup } from '@kbn/core-elasticsearch-server';
import type { ExecutionContextSetup } from '@kbn/core-execution-context-server';
+import type { FeatureFlagsSetup } from '@kbn/core-feature-flags-server';
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { HttpResources } from '@kbn/core-http-resources-server';
import type { HttpServiceSetup } from '@kbn/core-http-server';
@@ -52,6 +53,8 @@ export interface CoreSetup & {
/** {@link HttpResources} */
diff --git a/packages/core/lifecycle/core-lifecycle-server/src/core_start.ts b/packages/core/lifecycle/core-lifecycle-server/src/core_start.ts
index ae6135c55800f..13d871df8b759 100644
--- a/packages/core/lifecycle/core-lifecycle-server/src/core_start.ts
+++ b/packages/core/lifecycle/core-lifecycle-server/src/core_start.ts
@@ -12,6 +12,7 @@ import type { CapabilitiesStart } from '@kbn/core-capabilities-server';
import type { DocLinksServiceStart } from '@kbn/core-doc-links-server';
import type { ElasticsearchServiceStart } from '@kbn/core-elasticsearch-server';
import type { ExecutionContextStart } from '@kbn/core-execution-context-server';
+import type { FeatureFlagsStart } from '@kbn/core-feature-flags-server';
import type { HttpServiceStart } from '@kbn/core-http-server';
import type { MetricsServiceStart } from '@kbn/core-metrics-server';
import type { SavedObjectsServiceStart } from '@kbn/core-saved-objects-server';
@@ -40,6 +41,8 @@ export interface CoreStart {
elasticsearch: ElasticsearchServiceStart;
/** {@link ExecutionContextStart} */
executionContext: ExecutionContextStart;
+ /** {@link FeatureFlagsStart} */
+ featureFlags: FeatureFlagsStart;
/** {@link HttpServiceStart} */
http: HttpServiceStart;
/** {@link MetricsServiceStart} */
diff --git a/packages/core/lifecycle/core-lifecycle-server/tsconfig.json b/packages/core/lifecycle/core-lifecycle-server/tsconfig.json
index ed35724914dec..c8b95eed1e6d7 100644
--- a/packages/core/lifecycle/core-lifecycle-server/tsconfig.json
+++ b/packages/core/lifecycle/core-lifecycle-server/tsconfig.json
@@ -32,7 +32,8 @@
"@kbn/core-user-settings-server",
"@kbn/core-plugins-contracts-server",
"@kbn/core-security-server",
- "@kbn/core-user-profile-server"
+ "@kbn/core-user-profile-server",
+ "@kbn/core-feature-flags-server"
],
"exclude": [
"target/**/*",
diff --git a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts
index ad242905f7751..b78e5cec0b276 100644
--- a/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts
+++ b/packages/core/plugins/core-plugins-browser-internal/src/plugin_context.ts
@@ -82,6 +82,7 @@ export function createPluginSetupContext<
},
customBranding: deps.customBranding,
fatalErrors: deps.fatalErrors,
+ featureFlags: deps.featureFlags,
executionContext: deps.executionContext,
http: {
...deps.http,
@@ -147,6 +148,7 @@ export function createPluginStartContext<
customBranding: deps.customBranding,
docLinks: deps.docLinks,
executionContext: deps.executionContext,
+ featureFlags: deps.featureFlags,
http: {
...deps.http,
staticAssets: {
diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts
index 76306751427cf..539b629974982 100644
--- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts
+++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts
@@ -218,6 +218,10 @@ export function createPluginSetupContext({
withContext: deps.executionContext.withContext,
getAsLabels: deps.executionContext.getAsLabels,
},
+ featureFlags: {
+ setProvider: deps.featureFlags.setProvider,
+ appendContext: deps.featureFlags.appendContext,
+ },
http: {
createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,
registerRouteHandlerContext: <
@@ -332,6 +336,15 @@ export function createPluginStartContext({
getCapabilities: deps.elasticsearch.getCapabilities,
},
executionContext: deps.executionContext,
+ featureFlags: {
+ appendContext: deps.featureFlags.appendContext,
+ getBooleanValue: deps.featureFlags.getBooleanValue,
+ getStringValue: deps.featureFlags.getStringValue,
+ getNumberValue: deps.featureFlags.getNumberValue,
+ getBooleanValue$: deps.featureFlags.getBooleanValue$,
+ getStringValue$: deps.featureFlags.getStringValue$,
+ getNumberValue$: deps.featureFlags.getNumberValue$,
+ },
http: {
auth: deps.http.auth,
basePath: deps.http.basePath,
diff --git a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap
index e92e760b400e5..c858b6a8470d2 100644
--- a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap
+++ b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap
@@ -39,6 +39,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -121,6 +124,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -199,6 +205,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -281,6 +290,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -359,6 +371,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -437,6 +452,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -519,6 +537,9 @@ Object {
},
],
},
+ "featureFlags": Object {
+ "overrides": Object {},
+ },
"i18n": Object {
"translationsUrl": "/mock-server-basepath/translations/MOCK_HASH/en.json",
},
@@ -597,6 +618,90 @@ Object {
},
],
},
+ "featureFlags": 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