OpenFeature: Add context attributes to frontend settings (#112105)

* Update targeting key to namespace

* Pass eval ctx attributes to frontend

* Add context config to the frontend

* Fix OF namespace not defined in e2e tests

* Set default namespace and targeting key

* remove e2e server config now that namespace is default

---------

Co-authored-by: joshhunt <josh.hunt@grafana.com>
This commit is contained in:
Tania
2025-10-10 13:43:24 +02:00
committed by GitHub
co-authored by joshhunt
parent fa9d6be255
commit 21e26aefdb
10 changed files with 35 additions and 12 deletions
+1
View File
@@ -312,6 +312,7 @@
"@locker/near-membrane-shared-dom": "0.14.0",
"@msagl/core": "^1.1.19",
"@msagl/parser": "^1.1.19",
"@openfeature/ofrep-web-provider": "^0.3.3",
"@openfeature/web-sdk": "^1.6.1",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/exporter-collector": "0.25.0",
@@ -316,6 +316,7 @@ export interface GrafanaConfig {
quickRanges?: TimeOption[];
pluginRestrictedAPIsAllowList?: Record<string, string[]>;
pluginRestrictedAPIsBlockList?: Record<string, string[]>;
openFeatureContext: Record<string, unknown>;
// The namespace to use for kubernetes apiserver requests
namespace: string;
+2
View File
@@ -259,6 +259,8 @@ export class GrafanaBootConfig {
listDashboardScopesEndpoint = '';
listScopesEndpoint = '';
openFeatureContext: Record<string, unknown> = {};
constructor(
options: BootData['settings'] & {
bootData: BootData;
@@ -24,7 +24,7 @@ export async function initOpenFeature() {
await OpenFeature.setProviderAndWait(ofProvider, {
targetingKey: config.namespace,
namespace: config.namespace,
...config.openFeatureContext,
});
}
+3 -2
View File
@@ -305,6 +305,7 @@ type FrontendSettingsDTO struct {
LocalFileSystemAvailable bool `json:"localFileSystemAvailable"`
// Experimental Scope settings
ListScopesEndpoint string `json:"listScopesEndpoint"`
ListDashboardScopesEndpoint string `json:"listDashboardScopesEndpoint"`
ListScopesEndpoint string `json:"listScopesEndpoint"`
ListDashboardScopesEndpoint string `json:"listDashboardScopesEndpoint"`
OpenFeatureContext map[string]string `json:"openFeatureContext"`
}
+1
View File
@@ -350,6 +350,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
MaxIdleConns: hs.Cfg.SqlDatasourceMaxIdleConnsDefault,
ConnMaxLifetime: hs.Cfg.SqlDatasourceMaxConnLifetimeDefault,
},
OpenFeatureContext: hs.Cfg.OpenFeature.ContextAttrs,
}
if hs.Cfg.UnifiedAlerting.StateHistory.Enabled {
+5 -1
View File
@@ -40,7 +40,11 @@ func InitOpenFeatureWithCfg(cfg *setting.Cfg) error {
if err != nil {
return fmt.Errorf("failed to initialize OpenFeature: %w", err)
}
openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(cfg.OpenFeature.TargetingKey, cfg.OpenFeature.ContextAttrs))
contextAttrs := make(map[string]any)
for k, v := range cfg.OpenFeature.ContextAttrs {
contextAttrs[k] = v
}
openfeature.SetEvaluationContext(openfeature.NewEvaluationContext(cfg.OpenFeature.TargetingKey, contextAttrs))
return nil
}
+13 -4
View File
@@ -15,7 +15,7 @@ type OpenFeatureSettings struct {
ProviderType string
URL *url.URL
TargetingKey string
ContextAttrs map[string]any
ContextAttrs map[string]string
}
func (cfg *Cfg) readOpenFeatureSettings() error {
@@ -24,10 +24,15 @@ func (cfg *Cfg) readOpenFeatureSettings() error {
config := cfg.Raw.Section("feature_toggles.openfeature")
cfg.OpenFeature.APIEnabled = config.Key("enable_api").MustBool(true)
cfg.OpenFeature.ProviderType = config.Key("provider").MustString(StaticProviderType)
cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(cfg.AppURL)
strURL := config.Key("url").MustString("")
defaultTargetingKey := "default"
if cfg.StackID != "" {
defaultTargetingKey = fmt.Sprintf("stacks-%s", cfg.StackID)
}
cfg.OpenFeature.TargetingKey = config.Key("targetingKey").MustString(defaultTargetingKey)
if strURL != "" && cfg.OpenFeature.ProviderType == GOFFProviderType {
u, err := url.Parse(strURL)
if err != nil {
@@ -38,7 +43,7 @@ func (cfg *Cfg) readOpenFeatureSettings() error {
// build the eval context attributes using [feature_toggles.openfeature.context] section
ctxConf := cfg.Raw.Section("feature_toggles.openfeature.context")
attrs := map[string]any{}
attrs := map[string]string{}
for _, key := range ctxConf.KeyStrings() {
attrs[key] = ctxConf.Key(key).String()
}
@@ -48,6 +53,10 @@ func (cfg *Cfg) readOpenFeatureSettings() error {
attrs["grafana_version"] = BuildVersion
}
if _, ok := attrs["namespace"]; !ok {
attrs["namespace"] = defaultTargetingKey
}
cfg.OpenFeature.ContextAttrs = attrs
return nil
}
+7 -4
View File
@@ -11,12 +11,13 @@ func Test_CtxAttrs(t *testing.T) {
testCases := []struct {
name string
conf string
expected map[string]any
expected map[string]string
}{
{
name: "empty config - only default attributes should be present",
expected: map[string]any{
expected: map[string]string{
"grafana_version": "",
"namespace": "default",
},
},
{
@@ -26,11 +27,12 @@ func Test_CtxAttrs(t *testing.T) {
foo = bar
baz = qux
quux = corge`,
expected: map[string]any{
expected: map[string]string{
"foo": "bar",
"baz": "qux",
"quux": "corge",
"grafana_version": "",
"namespace": "default",
},
},
{
@@ -39,9 +41,10 @@ quux = corge`,
[feature_toggles.openfeature.context]
grafana_version = 10.0.0
foo = bar`,
expected: map[string]any{
expected: map[string]string{
"grafana_version": "10.0.0",
"foo": "bar",
"namespace": "default",
},
},
}
+1
View File
@@ -18308,6 +18308,7 @@ __metadata:
"@msagl/core": "npm:^1.1.19"
"@msagl/parser": "npm:^1.1.19"
"@npmcli/package-json": "npm:^6.0.0"
"@openfeature/ofrep-web-provider": "npm:^0.3.3"
"@openfeature/web-sdk": "npm:^1.6.1"
"@opentelemetry/api": "npm:1.9.0"
"@opentelemetry/exporter-collector": "npm:0.25.0"