contribute/feature-toggles.md
This guide helps you to add your feature behind a feature flag, code that lets you enable or disable a feature without redeploying Grafana.
Exhaustive documentation on OpenFeature can be found at OpenFeature.dev
Define the feature flag in registry.go.
grafana.newPreferencesPage. The prefix is the name of the standalone service that owns this feature flag. For flags that are single-tenant Grafana-only, with no plans for a standalone deployment in the future, use grafana. as a prefix.Generate field to control which clients are generated for your flag (see Generation targets below).Run make gen-feature-toggles to regenerate all derived files: the backend constants, the legacy frontend types, the OpenFeature React client, and docs.
The Generate field on a FeatureFlag controls which clients are generated. The available targets are:
| Target | Description |
|---|---|
GenerateLegacyGo | Generates a Go constant in toggles_gen.go, skipping new name requirements (legacy, prefer GenerateGo for new flags) |
GenerateLegacyFrontend | Generates a TypeScript constant in featureToggles.gen.ts (legacy, prefer GenerateReact for new flags) |
GenerateGo | Generates a Go constant in toggles_gen.go |
GenerateReact | Generates a typed React hook in openfeature.gen.ts via the OpenFeature CLI |
e.g.
{
Name: "grafana.newPreferencesPage",
Description: "Whether to use the new SharedPreferences functional component",
Stage: FeatureStageExperimental,
Generate: []GenerateTarget{GenerateGo, GenerateReact},
Owner: grafanaFrontendPlatformSquad,
Expression: "false",
},
Once your feature flag is defined, you can then wrap your feature around a check if the feature flag is enabled on that Grafana instance.
Examples:
Use the OpenFeature client for all new backend feature flags.
commands module before initialization of other modules.import "github.com/open-feature/go-sdk/openfeature"
client := openfeature.NewDefaultClient()
if client.Boolean(ctx, MyTestFlag, false, openfeature.TransactionContext(ctx)) {
...
}
import (
"github.com/open-feature/go-sdk/openfeature"
"github.com/open-feature/go-sdk/openfeature/testing"
)
var (
// Since openfeature relies on global state,
// TestProvider should be a global instance shared between tests
// Under the hood it uses a custom _goroutine local_ storage to manage state on a per-test basis
provider = testing.NewTestProvider()
)
func TestMain(m *testing.M) {
fmt.Println("Setting up test environment...")
if err := openfeature.SetProvider(provider); err != nil {
...
}
exitCode := m.Run()
os.Exit(exitCode)
}
func TestFoo(t *testing.T) {
t.Parallel()
testFlags := map[string]memprovider.InMemoryFlag{
...
}
provider.UsingFlags(t, testFlags)
...
}
Use the OpenFeature React hooks for all new feature flags. The React hooks automatically stay up to date with the latest flag values and integrate seamlessly with React components.
For flags with GenerateReact set, a typed hook is generated into packages/grafana-runtime/src/internal/openFeature/openfeature.gen.ts. Import from @grafana/runtime/internal:
import { useFlagGrafanaNewPreferencesPage } from '@grafana/runtime/internal';
function MyComponent() {
const isEnabled = useFlagGrafanaNewPreferencesPage();
if (isEnabled) {
return <NewPreferencesUI />;
}
return <LegacyPreferencesUI />;
}
A FlagKeys constant object is also exported, useful for passing flag keys to non-hook APIs:
import { FlagKeys } from '@grafana/runtime/internal';
client.getBooleanValue(FlagKeys.GrafanaNewPreferencesPage, false);
Flag values may change over the lifetime of the session, so do not store the result elsewhere in a way it will not react to changes in the flag value.
If the generated hooks don't meet your needs (e.g. you need the flag evaluation details, or a different fallback value), you can use the @openfeature/react-sdk directly:
import { useBooleanFlagDetails } from '@openfeature/react-sdk';
import { FlagKeys } from '@grafana/runtime/internal';
function MyComponent() {
const newPreferencesFlag = useBooleanFlagDetails(FlagKeys.GrafanaNewPreferencesPage, true);
...
}
If using non-boolean flags (a unique feature of the new feature flag system), explore the other exports from @openfeature/react-sdk to see how to use them.
For advanced, non-React contexts (utilities, class methods, callbacks), you can use the OpenFeature client directly.
However, because this is seperate from the React render loop there are important caveats you must be aware of:
getBooleanValue() just at the top-level of a module. You must wait until app.ts has initialised until you call a flag otherwise you will only get the default valueIt is strongly preferred to use the React hooks instead of getting the client.
import { getFeatureFlagClient, FlagKeys } from '@grafana/runtime/internal';
// GOOD - The feature flag should be called after app initialisation
function doThing() {
if (getFeatureFlagClient().getBooleanValue(FlagKeys.GrafanaNewPreferencesPage, false)) {
// do new things
}
}
// BAD - Don't do this. The feature flag must wait until app initialisation
const isEnabled = getFeatureFlagClient().getBooleanValue(FlagKeys.GrafanaNewPreferencesPage, false);
function doThing() {
if (isEnabled) {
// do new things
}
}
// BAD - Don't do this. The feature flag will not change in response to updates
class FooSrv {
constructor() {
this.isEnabled = getFeatureFlagClient().getBooleanValue(FlagKeys.GrafanaNewPreferencesPage, false);
}
doThing() {
if (this.isEnabled) {
// do new things
}
}
}
Add the feature flag to the feature_toggle section in your custom.ini, for example:
[feature_toggles]
localeFormatPreference=true