OpenFeature: Create basic frontend client + example usage (#110587)

* initial basic OpenFeature client for datasource class

* add dep

* update, use a wrapping function to enforce types

* move init OF to grafana-runtime

* docs

* Fix circular dependency causing tests to fail

* codeowners

* use toggle in datasourcewithbackend

* Fix CUJs group-by test

* Comments

* update docs, make default value mandatory

* revert using for queryServiceFromUI toggle
This commit is contained in:
Josh Hunt
2025-10-06 14:22:00 +00:00
committed by GitHub
parent 8ec162afec
commit 98f293e229
9 changed files with 121 additions and 2 deletions
+1
View File
@@ -629,6 +629,7 @@
/packages/grafana-runtime/rollup.config.ts @grafana/grafana-frontend-platform
/packages/grafana-runtime/src/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
/packages/grafana-runtime/src/internal/index.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
/packages/grafana-runtime/src/internal/openFeature @grafana/grafana-frontend-platform
/packages/grafana-runtime/src/unstable.ts @grafana/grafana-frontend-platform @grafana/plugins-platform-frontend
/packages/grafana-runtime/tsconfig.build.json @grafana/grafana-frontend-platform
/packages/grafana-runtime/tsconfig.json @grafana/grafana-frontend-platform
+24 -1
View File
@@ -14,7 +14,30 @@ Once your feature toggle is defined, you can then wrap your feature around a che
Examples:
- [Backend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/pkg/services/authn/clients/session.go#L57): Use the `IsEnabled` function and pass in your feature toggle.
- [Frontend](https://github.com/grafana/grafana/blob/feb2b5878b3e3ec551d64872c35edec2a0187812/public/app/features/search/service/folders.ts#L14): Check the config for your feature toggle.
### Frontend
Use the new OpenFeature-based feature flag client for all new feature flags. There are some differences compared to the legacy `config.featureToggles` system:
- Feature flag initialisation is async, but will be finished by the time the UI is rendered. This means you cannot get the value of a feature flag at the 'top level' of a module/file
- Call `evaluateBooleanFlag("flagName")` from `@grafana/runtime/internal` instead to get the value of a feature flag
- Feature flag values _may_ change over the lifetime of the session. Do not store the value in a variable that is used for longer than a single render - always call `evaluateBooleanFlag` lazily when you use the value.
e.g.
```ts
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
// BAD - Don't do this. The feature toggle will not evaluate correctly
const isEnabled = evaluateBooleanFlag('newPreferences', false);
function makeAPICall() {
// GOOD - The feature toggle should be called after app initialisation
if (evaluateBooleanFlag('newPreferences', false)) {
// do new things
}
}
```
## Enabling toggles in development
@@ -71,9 +71,12 @@ test.describe(
await test.step('3.Edit and restore default groupBy', async () => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UNDER_TEST });
// Wait for the page to load
const groupByVariable = getGroupByInput(dashboardPage, selectors);
await expect(groupByVariable).toBeVisible();
const initialSelectedOptionsCount = await groupByValues.count();
const groupByVariable = getGroupByInput(dashboardPage, selectors);
await groupByVariable.click();
const groupByOption = groupByOptions.nth(1);
+1
View File
@@ -302,6 +302,7 @@
"@locker/near-membrane-shared-dom": "0.14.0",
"@msagl/core": "^1.1.19",
"@msagl/parser": "^1.1.19",
"@openfeature/web-sdk": "^1.6.1",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/exporter-collector": "0.25.0",
"@opentelemetry/semantic-conventions": "1.37.0",
+3
View File
@@ -58,6 +58,9 @@
"@grafana/faro-web-sdk": "^1.13.2",
"@grafana/schema": "12.3.0-pre",
"@grafana/ui": "12.3.0-pre",
"@openfeature/core": "^1.9.0",
"@openfeature/ofrep-web-provider": "^0.3.3",
"@openfeature/web-sdk": "^1.6.1",
"@types/systemjs": "6.15.3",
"history": "4.10.1",
"lodash": "4.17.21",
@@ -27,3 +27,5 @@ export {
} from '../services/pluginExtensions/getObservablePluginLinks';
export { UserStorage } from '../utils/userStorage';
export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
@@ -0,0 +1,33 @@
import { OFREPWebProvider } from '@openfeature/ofrep-web-provider';
import { OpenFeature } from '@openfeature/web-sdk';
import { FeatureToggles } from '@grafana/data';
import { config } from '../../config';
export type FeatureFlagName = keyof FeatureToggles;
export async function initOpenFeature() {
/**
* Note: Currently we don't have a way to override OpenFeature flags for tests or localStorage.
* A few improvements we could make:
* - When running in tests (unit or e2e?), we could use InMemoryProvider instead
* - Use Multi-Provider to combine InMemoryProvider (for localStorage) with OFREPWebProvider
* to allow for overrides https://github.com/open-feature/js-sdk-contrib/tree/main/libs/providers/multi-provider
*/
const ofProvider = new OFREPWebProvider({
baseUrl: '/apis/features.grafana.app/v0alpha1/namespaces/' + config.namespace,
pollInterval: -1, // disable polling
timeoutMs: 5_000,
});
await OpenFeature.setProviderAndWait(ofProvider, {
targetingKey: config.namespace,
namespace: config.namespace,
});
}
export function evaluateBooleanFlag(flagName: FeatureFlagName, defaultValue: boolean): boolean {
return OpenFeature.getClient().getBooleanValue(flagName, defaultValue);
}
+13
View File
@@ -43,6 +43,7 @@ import {
setMegaMenuOpenHook,
} from '@grafana/runtime';
import {
initOpenFeature,
setGetObservablePluginComponents,
setGetObservablePluginLinks,
setPanelDataErrorView,
@@ -129,8 +130,20 @@ export class GrafanaApp {
async init() {
try {
await preInitTasks();
// Let iframe container know grafana has started loading
window.parent.postMessage('GrafanaAppInit', '*');
// Currently the OpenFeature API requires a signed in user. This means feature flags cannot be used
// on the login page.
if (contextSrv.user.isSignedIn) {
try {
await initOpenFeature();
} catch (err) {
console.error('Failed to initialize OpenFeature provider', err);
}
}
const regionalFormat = config.featureToggles.localeFormatPreference
? config.regionalFormat
: contextSrv.user.language;
+40
View File
@@ -3489,6 +3489,9 @@ __metadata:
"@grafana/faro-web-sdk": "npm:^1.13.2"
"@grafana/schema": "npm:12.3.0-pre"
"@grafana/ui": "npm:12.3.0-pre"
"@openfeature/core": "npm:^1.9.0"
"@openfeature/ofrep-web-provider": "npm:^0.3.3"
"@openfeature/web-sdk": "npm:^1.6.1"
"@rollup/plugin-node-resolve": "npm:16.0.1"
"@rollup/plugin-terser": "npm:0.4.4"
"@testing-library/dom": "npm:10.4.1"
@@ -5752,6 +5755,42 @@ __metadata:
languageName: node
linkType: hard
"@openfeature/core@npm:^1.9.0":
version: 1.9.0
resolution: "@openfeature/core@npm:1.9.0"
checksum: 10/c6d20edc09053afd99752fe46d8328158680950bca4b86679f67f79249d7226eea127b31fffdc38e26ecb729f2bab5a4a5a7c1db708ae76b7fbbac68cd56f094
languageName: node
linkType: hard
"@openfeature/ofrep-core@npm:^1.0.0":
version: 1.1.0
resolution: "@openfeature/ofrep-core@npm:1.1.0"
peerDependencies:
"@openfeature/core": ^1.6.0
checksum: 10/4198f2f1abf974822bf14530a7f514292d8235552d6e61465d29ffe42d092f675e7f56a9e9da5aa7d45dfbd98cc36316efbdaadaf83e8f510f35865612f5f24f
languageName: node
linkType: hard
"@openfeature/ofrep-web-provider@npm:^0.3.3":
version: 0.3.3
resolution: "@openfeature/ofrep-web-provider@npm:0.3.3"
dependencies:
"@openfeature/ofrep-core": "npm:^1.0.0"
peerDependencies:
"@openfeature/web-sdk": ^1.4.0
checksum: 10/85f362e3ebaa9d421be91e4d966284e28850649e417d5db81181960b95ac693c9faa4a0bdc84eeb03fa694a8cd1e1f5d9de9bf0fba62f8e2669f9637836f0884
languageName: node
linkType: hard
"@openfeature/web-sdk@npm:^1.6.1":
version: 1.6.1
resolution: "@openfeature/web-sdk@npm:1.6.1"
peerDependencies:
"@openfeature/core": ^1.9.0
checksum: 10/8bd7d1ea386e21cdd7492cab2fd1d2b138b4e6a376a4c0a40244633e5955f6452039bc2633fc5230bd7b494506a4137ba7210d40850634f9618f77a0ee435f9d
languageName: node
linkType: hard
"@opentelemetry/api-logs@npm:0.202.0":
version: 0.202.0
resolution: "@opentelemetry/api-logs@npm:0.202.0"
@@ -18218,6 +18257,7 @@ __metadata:
"@msagl/core": "npm:^1.1.19"
"@msagl/parser": "npm:^1.1.19"
"@npmcli/package-json": "npm:^6.0.0"
"@openfeature/web-sdk": "npm:^1.6.1"
"@opentelemetry/api": "npm:1.9.0"
"@opentelemetry/exporter-collector": "npm:0.25.0"
"@opentelemetry/semantic-conventions": "npm:1.37.0"