From ea9cbe2701036807834863ddc47be0ef833c0195 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 1 Aug 2024 15:22:33 +0200 Subject: [PATCH] feat(relay-proxy): Add the possibility to specify in the evaluation context which flag to evaluate in bulk evaluation flags (#2171) --- cmd/relayproxy/controller/all_flags.go | 11 +- cmd/relayproxy/controller/all_flags_test.go | 11 + cmd/relayproxy/ofrep/evaluate.go | 9 +- cmd/relayproxy/ofrep/evaluate_test.go | 11 + .../valid_request_specify_flags.json | 13 + .../valid_response_specify_flags.json | 25 ++ .../valid_response_specify_flags.json | 20 ++ .../ofrep/valid_request_specify_flags.json | 11 + ffcontext/context.go | 23 +- ffcontext/context_test.go | 49 +++ ffcontext/goff_context_specifics.go | 42 +++ internal/flagstate/flag_state.go | 47 +++ .../js-integration-tests/provider.test.js | 5 + .../config_flag/flag-config-all-flags.yaml | 47 +++ .../marshal_json/all_flags.json | 66 +++++ .../marshal_json/valid_flag1_flag4.json | 23 ++ variation.go | 97 +----- variation_all_flags.go | 78 +++++ variation_all_flags_test.go | 280 ++++++++++++++++++ variation_test.go | 160 ---------- website/docs/configure_flag/rule_format.md | 7 +- .../client_providers/openfeature_android.mdx | 35 +++ .../openfeature_javascript.mdx | 24 ++ .../client_providers/openfeature_swift.mdx | 25 ++ 24 files changed, 839 insertions(+), 280 deletions(-) create mode 100644 cmd/relayproxy/testdata/controller/all_flags/valid_request_specify_flags.json create mode 100644 cmd/relayproxy/testdata/controller/all_flags/valid_response_specify_flags.json create mode 100644 cmd/relayproxy/testdata/ofrep/responses/valid_response_specify_flags.json create mode 100644 cmd/relayproxy/testdata/ofrep/valid_request_specify_flags.json create mode 100644 ffcontext/goff_context_specifics.go create mode 100644 testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml create mode 100644 testdata/ffclient/get_flagstates/marshal_json/all_flags.json create mode 100644 testdata/ffclient/get_flagstates/marshal_json/valid_flag1_flag4.json create mode 100644 variation_all_flags.go create mode 100644 variation_all_flags_test.go diff --git a/cmd/relayproxy/controller/all_flags.go b/cmd/relayproxy/controller/all_flags.go index 87d5c9ec46a..8b6bd9cd782 100644 --- a/cmd/relayproxy/controller/all_flags.go +++ b/cmd/relayproxy/controller/all_flags.go @@ -2,6 +2,7 @@ package controller import ( "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/config" + "github.com/thomaspoignant/go-feature-flag/internal/flagstate" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "net/http" @@ -59,7 +60,15 @@ func (h *allFlags) Handler(c echo.Context) error { tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName) _, span := tracer.Start(c.Request().Context(), "AllFlagsState") defer span.End() - allFlags := h.goFF.AllFlagsState(evaluationCtx) + + var allFlags flagstate.AllFlags + if len(evaluationCtx.ExtractGOFFProtectedFields().FlagList) > 0 { + // if we have a list of flags to evaluate in the evaluation context, we evaluate only those flags. + allFlags = h.goFF.GetFlagStates(evaluationCtx, evaluationCtx.ExtractGOFFProtectedFields().FlagList) + } else { + allFlags = h.goFF.AllFlagsState(evaluationCtx) + } + span.SetAttributes( attribute.Bool("AllFlagsState.valid", allFlags.IsValid()), attribute.Int("AllFlagsState.numberEvaluation", len(allFlags.GetFlags())), diff --git a/cmd/relayproxy/controller/all_flags_test.go b/cmd/relayproxy/controller/all_flags_test.go index 5b9fdf45d16..0ec2ceaafe5 100644 --- a/cmd/relayproxy/controller/all_flags_test.go +++ b/cmd/relayproxy/controller/all_flags_test.go @@ -75,6 +75,17 @@ func Test_all_flag_Handler(t *testing.T) { errorCode: http.StatusBadRequest, }, }, + { + name: "specify flags in evaluation context", + args: args{ + bodyFile: "../testdata/controller/all_flags/valid_request_specify_flags.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/controller/all_flags/valid_response_specify_flags.json", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/relayproxy/ofrep/evaluate.go b/cmd/relayproxy/ofrep/evaluate.go index 47b3f8f9258..677d825b9d1 100644 --- a/cmd/relayproxy/ofrep/evaluate.go +++ b/cmd/relayproxy/ofrep/evaluate.go @@ -9,6 +9,7 @@ import ( "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/internal/flagstate" "github.com/thomaspoignant/go-feature-flag/internal/utils" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" @@ -173,7 +174,13 @@ func (h *EvaluateCtrl) BulkEvaluate(c echo.Context) error { _, span := tracer.Start(c.Request().Context(), "AllFlagsState") defer span.End() - allFlagsResp := h.goFF.AllFlagsState(evalCtx) + var allFlagsResp flagstate.AllFlags + if len(evalCtx.ExtractGOFFProtectedFields().FlagList) > 0 { + // if we have a list of flags to evaluate in the evaluation context, we evaluate only those flags. + allFlagsResp = h.goFF.GetFlagStates(evalCtx, evalCtx.ExtractGOFFProtectedFields().FlagList) + } else { + allFlagsResp = h.goFF.AllFlagsState(evalCtx) + } for key, val := range allFlagsResp.GetFlags() { value := val.Value if val.Reason == flag.ReasonError { diff --git a/cmd/relayproxy/ofrep/evaluate_test.go b/cmd/relayproxy/ofrep/evaluate_test.go index 50aa235c203..62c71d0360f 100644 --- a/cmd/relayproxy/ofrep/evaluate_test.go +++ b/cmd/relayproxy/ofrep/evaluate_test.go @@ -52,6 +52,17 @@ func Test_Bulk_Evaluation(t *testing.T) { bodyFile: "../testdata/ofrep/responses/valid_response.json", }, }, + { + name: "specify flag list in context", + args: args{ + bodyFile: "../testdata/ofrep/valid_request_specify_flags.json", + configFlagsLocation: configFlagsLocation, + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/ofrep/responses/valid_response_specify_flags.json", + }, + }, { name: "Invalid context", args: args{ diff --git a/cmd/relayproxy/testdata/controller/all_flags/valid_request_specify_flags.json b/cmd/relayproxy/testdata/controller/all_flags/valid_request_specify_flags.json new file mode 100644 index 00000000000..77647d3d2d1 --- /dev/null +++ b/cmd/relayproxy/testdata/controller/all_flags/valid_request_specify_flags.json @@ -0,0 +1,13 @@ +{ + "user": { + "key": "a20b1cd5-7165-4e02-a279-c0c8b90a8912", + "anonymous": false, + "custom": { + "custom1": "value1", + "custom2": "value2", + "gofeatureflag":{ + "flagList":["array-flag", "flag-only-for-admin"] + } + } + } +} diff --git a/cmd/relayproxy/testdata/controller/all_flags/valid_response_specify_flags.json b/cmd/relayproxy/testdata/controller/all_flags/valid_response_specify_flags.json new file mode 100644 index 00000000000..e9b0936243e --- /dev/null +++ b/cmd/relayproxy/testdata/controller/all_flags/valid_response_specify_flags.json @@ -0,0 +1,25 @@ +{ + "flags": { + "array-flag": { + "value": [ + "batmanDefault", + "supermanDefault", + "superherosDefault" + ], + "timestamp": 1652273630, + "variationType": "Default", + "trackEvents": true, + "errorCode": "", + "reason": "DEFAULT" + }, + "flag-only-for-admin": { + "value": false, + "timestamp": 1652273630, + "variationType": "Default", + "trackEvents": true, + "errorCode": "", + "reason": "DEFAULT" + } + }, + "valid": true +} diff --git a/cmd/relayproxy/testdata/ofrep/responses/valid_response_specify_flags.json b/cmd/relayproxy/testdata/ofrep/responses/valid_response_specify_flags.json new file mode 100644 index 00000000000..9731018d394 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/responses/valid_response_specify_flags.json @@ -0,0 +1,20 @@ +{ + "flags": [ + { + "key": "array-flag", + "value": [ + "batmanDefault", + "supermanDefault", + "superherosDefault" + ], + "reason": "DEFAULT", + "variant": "Default" + }, + { + "key": "flag-only-for-admin", + "value": false, + "reason": "DEFAULT", + "variant": "Default" + } + ] +} \ No newline at end of file diff --git a/cmd/relayproxy/testdata/ofrep/valid_request_specify_flags.json b/cmd/relayproxy/testdata/ofrep/valid_request_specify_flags.json new file mode 100644 index 00000000000..5c23313cc97 --- /dev/null +++ b/cmd/relayproxy/testdata/ofrep/valid_request_specify_flags.json @@ -0,0 +1,11 @@ +{ + "context": { + "company": "GO Feature Flag", + "firstname": "John", + "lastname": "Doe", + "targetingKey": "4f433951-4c8c-42b3-9f18-8c9a5ed8e9eb", + "gofeatureflag": { + "flagList": ["array-flag", "flag-only-for-admin"] + } + } +} \ No newline at end of file diff --git a/ffcontext/context.go b/ffcontext/context.go index 752bfd96928..65891579b82 100644 --- a/ffcontext/context.go +++ b/ffcontext/context.go @@ -1,9 +1,5 @@ package ffcontext -import ( - "time" -) - type Context interface { // GetKey return the unique key for the context. GetKey() string @@ -17,10 +13,6 @@ type Context interface { ExtractGOFFProtectedFields() GoffContextSpecifics } -type GoffContextSpecifics struct { - CurrentDateTime *time.Time `json:"currentDateTime"` -} - // value is a type to define custom attribute. type value map[string]interface{} @@ -85,20 +77,13 @@ func (u EvaluationContext) AddCustomAttribute(name string, value interface{}) { // ExtractGOFFProtectedFields extract the goff specific attributes from the evaluation context. func (u EvaluationContext) ExtractGOFFProtectedFields() GoffContextSpecifics { goff := GoffContextSpecifics{} - switch v := u.custom["gofeatureflag"].(type) { case map[string]string: - if currentDateTimeStr, ok := v["currentDateTime"]; ok { - if currentDateTime, err := time.ParseInLocation(time.RFC3339, currentDateTimeStr, time.Local); err == nil { - goff.CurrentDateTime = ¤tDateTime - } - } + goff.addCurrentDateTime(v["currentDateTime"]) + goff.addListFlags(v["flagList"]) case map[string]interface{}: - if currentDateTimeStr, ok := v["currentDateTime"].(string); ok { - if currentDateTime, err := time.ParseInLocation(time.RFC3339, currentDateTimeStr, time.Local); err == nil { - goff.CurrentDateTime = ¤tDateTime - } - } + goff.addCurrentDateTime(v["currentDateTime"]) + goff.addListFlags(v["flagList"]) case GoffContextSpecifics: return v } diff --git a/ffcontext/context_test.go b/ffcontext/context_test.go index 7ad2baa0d7b..86c6f2418a6 100644 --- a/ffcontext/context_test.go +++ b/ffcontext/context_test.go @@ -60,6 +60,24 @@ func Test_ExtractGOFFProtectedFields(t *testing.T) { CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), }, }, + { + name: "context goff specifics as map[string]interface and date as time.Time", + ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{ + "currentDateTime": time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC), + }).Build(), + want: ffcontext.GoffContextSpecifics{ + CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), + }, + }, + { + name: "context goff specifics as map[string]interface and date as *time.Time", + ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{ + "currentDateTime": testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), + }).Build(), + want: ffcontext.GoffContextSpecifics{ + CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), + }, + }, { name: "context goff specifics as map[string]interface", ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{ @@ -92,6 +110,37 @@ func Test_ExtractGOFFProtectedFields(t *testing.T) { CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), }, }, + { + name: "context goff specifics as GoffContextSpecifics type contains flagList", + ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", ffcontext.GoffContextSpecifics{ + CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), + FlagList: []string{"flag1", "flag2"}, + }).Build(), + want: ffcontext.GoffContextSpecifics{ + CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), + FlagList: []string{"flag1", "flag2"}, + }, + }, + { + name: "context goff specifics as map[string]interface type contains flagList", + ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{ + "currentDateTime": testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)).Format(time.RFC3339), + "flagList": []string{"flag1", "flag2"}, + }).Build(), + want: ffcontext.GoffContextSpecifics{ + CurrentDateTime: testconvert.Time(time.Date(2022, 8, 1, 0, 0, 10, 0, time.UTC)), + FlagList: []string{"flag1", "flag2"}, + }, + }, + { + name: "context goff specifics only flagList", + ctx: ffcontext.NewEvaluationContextBuilder("my-key").AddCustom("gofeatureflag", map[string]interface{}{ + "flagList": []string{"flag1", "flag2"}, + }).Build(), + want: ffcontext.GoffContextSpecifics{ + FlagList: []string{"flag1", "flag2"}, + }, + }, } for _, tt := range tests { diff --git a/ffcontext/goff_context_specifics.go b/ffcontext/goff_context_specifics.go new file mode 100644 index 00000000000..78178f0f4eb --- /dev/null +++ b/ffcontext/goff_context_specifics.go @@ -0,0 +1,42 @@ +package ffcontext + +import "time" + +type GoffContextSpecifics struct { + // CurrentDateTime is the current date time to use for the evaluation. + CurrentDateTime *time.Time `json:"currentDateTime"` + // FlagList is the list of flags to evaluate in a bulk evaluation. + FlagList []string `json:"flagList"` +} + +// addCurrentDateTime adds the current date time to the context. +// This function formats the current date time to RFC3339 format. +func (g *GoffContextSpecifics) addCurrentDateTime(currentDateTime any) { + switch value := currentDateTime.(type) { + case *time.Time: + g.CurrentDateTime = value + case time.Time: + g.CurrentDateTime = &value + case string: + if currentDateTime, err := time.ParseInLocation(time.RFC3339, value, time.Local); err == nil { + g.CurrentDateTime = ¤tDateTime + } + return + default: + return + } +} + +// addListFlags adds the list of flags to evaluate in a bulk evaluation. +func (g *GoffContextSpecifics) addListFlags(flagList any) { + if value, ok := flagList.([]string); ok { + g.FlagList = value + } + if value, ok := flagList.([]interface{}); ok { + for _, val := range value { + if valAsString, ok := val.(string); ok { + g.FlagList = append(g.FlagList, valAsString) + } + } + } +} diff --git a/internal/flagstate/flag_state.go b/internal/flagstate/flag_state.go index 71236b687ca..1f71d09122f 100644 --- a/internal/flagstate/flag_state.go +++ b/internal/flagstate/flag_state.go @@ -1,7 +1,9 @@ package flagstate import ( + "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/internal/flag" + "time" ) // FlagState represents the state of an individual feature flag, with regard to a specific user, when it was called. @@ -15,3 +17,48 @@ type FlagState struct { Reason flag.ResolutionReason `json:"reason"` Metadata map[string]interface{} `json:"metadata,omitempty"` } + +func FromFlagEvaluation(key string, evaluationCtx ffcontext.Context, + flagCtx flag.Context, currentFlag flag.Flag) FlagState { + flagValue, resolutionDetails := currentFlag.Value(key, evaluationCtx, flagCtx) + + // if the flag is disabled, we are ignoring it. + if resolutionDetails.Reason == flag.ReasonDisabled { + return FlagState{ + Timestamp: time.Now().Unix(), + TrackEvents: currentFlag.IsTrackEvents(), + Failed: resolutionDetails.ErrorCode != "", + ErrorCode: resolutionDetails.ErrorCode, + Reason: resolutionDetails.Reason, + Metadata: resolutionDetails.Metadata, + } + } + + switch v := flagValue; v.(type) { + case int, float64, bool, string, []interface{}, map[string]interface{}: + return FlagState{ + Value: v, + Timestamp: time.Now().Unix(), + VariationType: resolutionDetails.Variant, + TrackEvents: currentFlag.IsTrackEvents(), + Failed: resolutionDetails.ErrorCode != "", + ErrorCode: resolutionDetails.ErrorCode, + Reason: resolutionDetails.Reason, + Metadata: resolutionDetails.Metadata, + } + + default: + defaultVariationName := flag.VariationSDKDefault + defaultVariationValue := currentFlag.GetVariationValue(defaultVariationName) + return FlagState{ + Value: defaultVariationValue, + Timestamp: time.Now().Unix(), + VariationType: defaultVariationName, + TrackEvents: currentFlag.IsTrackEvents(), + Failed: true, + ErrorCode: flag.ErrorCodeTypeMismatch, + Reason: flag.ReasonError, + Metadata: resolutionDetails.Metadata, + } + } +} diff --git a/openfeature/provider_tests/js-integration-tests/provider.test.js b/openfeature/provider_tests/js-integration-tests/provider.test.js index 78e7a083a10..76cbb782ccb 100644 --- a/openfeature/provider_tests/js-integration-tests/provider.test.js +++ b/openfeature/provider_tests/js-integration-tests/provider.test.js @@ -12,6 +12,11 @@ describe('Provider tests', () => { }); goffClient = OpenFeature.getClient('my-app') await OpenFeature.setProviderAndWait('my-app', goFeatureFlagProvider); + OpenFeature.setContext({ + gofeatureflag:{ + flagList: ["flag1", "flag2"] + } + }) userCtx = { targetingKey: 'd45e303a-38c2-11ed-a261-0242ac120002', // user unique identifier (mandatory) diff --git a/testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml b/testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml new file mode 100644 index 00000000000..1c15eb7be57 --- /dev/null +++ b/testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml @@ -0,0 +1,47 @@ +test-flag0: + percentage: 100 + true: true + false: false + default: false +test-flag1: + percentage: 100 + true: "true" + false: "false" + default: "false" +test-flag2: + percentage: 100 + true: 1 + false: 2 + default: 3 +test-flag3: + percentage: 100 + true: + - yo + - ya + false: + - yo + - ya + default: + - yo + - ya +test-flag4: + percentage: 100 + true: + test: yo + false: + test: yo + default: + test: yo +test-flag5: + percentage: 100 + true: 1.1 + false: 1.2 + default: 1.3 + trackEvents: false +test-flag6: + percentage: 100 + true: 1.1 + false: 1.2 + default: 1.3 + trackEvents: false + disable: true diff --git a/testdata/ffclient/get_flagstates/marshal_json/all_flags.json b/testdata/ffclient/get_flagstates/marshal_json/all_flags.json new file mode 100644 index 00000000000..ebb242b2010 --- /dev/null +++ b/testdata/ffclient/get_flagstates/marshal_json/all_flags.json @@ -0,0 +1,66 @@ +{ + "flags": { + "test-flag0": { + "value": true, + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag1": { + "value": "true", + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag2": { + "value": 1, + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag3": { + "value": [ + "yo", + "ya" + ], + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag4": { + "value": { + "test": "yo" + }, + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag5": { + "value": 1.1, + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": false, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag6": { + "value": null, + "timestamp": 1622206239, + "variationType": "", + "trackEvents": false, + "reason":"DISABLED", + "errorCode": "" + } + }, + "valid": true +} diff --git a/testdata/ffclient/get_flagstates/marshal_json/valid_flag1_flag4.json b/testdata/ffclient/get_flagstates/marshal_json/valid_flag1_flag4.json new file mode 100644 index 00000000000..7d45430fbe7 --- /dev/null +++ b/testdata/ffclient/get_flagstates/marshal_json/valid_flag1_flag4.json @@ -0,0 +1,23 @@ +{ + "flags": { + "test-flag1": { + "value": "true", + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + }, + "test-flag4": { + "value": { + "test": "yo" + }, + "timestamp": 1622206239, + "variationType": "True", + "trackEvents": true, + "reason":"STATIC", + "errorCode": "" + } + }, + "valid": true +} diff --git a/variation.go b/variation.go index 4fa8f4818a1..088c46bd616 100644 --- a/variation.go +++ b/variation.go @@ -2,14 +2,11 @@ package ffclient import ( "fmt" - "maps" - "time" - "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/ffcontext" + "maps" "github.com/thomaspoignant/go-feature-flag/internal/flag" - "github.com/thomaspoignant/go-feature-flag/internal/flagstate" "github.com/thomaspoignant/go-feature-flag/model" ) @@ -233,98 +230,6 @@ func (g *GoFeatureFlag) JSONVariationDetails(flagKey string, ctx ffcontext.Conte return res, err } -// AllFlagsState return the values of all the flags for a specific user. -// If valid field is false it means that we had an error when checking the flags. -func AllFlagsState(ctx ffcontext.Context) flagstate.AllFlags { - return ff.AllFlagsState(ctx) -} - -// GetFlagsFromCache returns all the flags present in the cache with their -// current state when calling this method. If cache hasn't been initialized, an -// error reporting this is returned. -func GetFlagsFromCache() (map[string]flag.Flag, error) { - return ff.GetFlagsFromCache() -} - -// AllFlagsState return a flagstate.AllFlags that contains all the flags for a specific user. -func (g *GoFeatureFlag) AllFlagsState(evaluationCtx ffcontext.Context) flagstate.AllFlags { - flags := map[string]flag.Flag{} - if g == nil { - // empty AllFlags will set valid to false - return flagstate.AllFlags{} - } - - if !g.config.Offline { - var err error - flags, err = g.cache.AllFlags() - if err != nil { - // empty AllFlags will set valid to false - return flagstate.AllFlags{} - } - } - - allFlags := flagstate.NewAllFlags() - for key, currentFlag := range flags { - flagCtx := flag.Context{ - EvaluationContextEnrichment: g.config.EvaluationContextEnrichment, - DefaultSdkValue: nil, - } - flagCtx.AddIntoEvaluationContextEnrichment("env", g.config.Environment) - flagValue, resolutionDetails := currentFlag.Value(key, evaluationCtx, flagCtx) - - // if the flag is disabled, we are ignoring it. - if resolutionDetails.Reason == flag.ReasonDisabled { - allFlags.AddFlag(key, flagstate.FlagState{ - Timestamp: time.Now().Unix(), - TrackEvents: currentFlag.IsTrackEvents(), - Failed: resolutionDetails.ErrorCode != "", - ErrorCode: resolutionDetails.ErrorCode, - Reason: resolutionDetails.Reason, - Metadata: resolutionDetails.Metadata, - }) - continue - } - - switch v := flagValue; v.(type) { - case int, float64, bool, string, []interface{}, map[string]interface{}: - allFlags.AddFlag(key, flagstate.FlagState{ - Value: v, - Timestamp: time.Now().Unix(), - VariationType: resolutionDetails.Variant, - TrackEvents: currentFlag.IsTrackEvents(), - Failed: resolutionDetails.ErrorCode != "", - ErrorCode: resolutionDetails.ErrorCode, - Reason: resolutionDetails.Reason, - Metadata: resolutionDetails.Metadata, - }) - - default: - defaultVariationName := flag.VariationSDKDefault - defaultVariationValue := currentFlag.GetVariationValue(defaultVariationName) - allFlags.AddFlag( - key, - flagstate.FlagState{ - Value: defaultVariationValue, - Timestamp: time.Now().Unix(), - VariationType: defaultVariationName, - TrackEvents: currentFlag.IsTrackEvents(), - Failed: true, - ErrorCode: flag.ErrorCodeTypeMismatch, - Reason: flag.ReasonError, - Metadata: resolutionDetails.Metadata, - }) - } - } - return allFlags -} - -// GetFlagsFromCache returns all the flags present in the cache with their -// current state when calling this method. If cache hasn't been initialized, an -// error reporting this is returned. -func (g *GoFeatureFlag) GetFlagsFromCache() (map[string]flag.Flag, error) { - return g.cache.AllFlags() -} - // RawVariation return the raw value of the flag (without any types). // This raw result is mostly used by software built on top of go-feature-flag such as // go-feature-flag relay proxy. diff --git a/variation_all_flags.go b/variation_all_flags.go new file mode 100644 index 00000000000..6fd48d7841d --- /dev/null +++ b/variation_all_flags.go @@ -0,0 +1,78 @@ +package ffclient + +import ( + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/thomaspoignant/go-feature-flag/internal/flagstate" +) + +// AllFlagsState return the values of all the flags for a specific user. +// If a valid field is false, it means that we had an error when checking the flags. +func AllFlagsState(ctx ffcontext.Context) flagstate.AllFlags { + return ff.AllFlagsState(ctx) +} + +// GetFlagsFromCache returns all the flags present in the cache with their +// current state when calling this method. If cache hasn't been initialized, an +// error reporting this is returned. +func GetFlagsFromCache() (map[string]flag.Flag, error) { + return ff.GetFlagsFromCache() +} + +// GetFlagStates is evaluating all the flags in flagsToEvaluate based on the context provided. +// If flagsToEvaluate is nil or empty, it will evaluate all the flags available in GO Feature Flag. +func (g *GoFeatureFlag) GetFlagStates(evaluationCtx ffcontext.Context, flagsToEvaluate []string) flagstate.AllFlags { + if g == nil { + return flagstate.AllFlags{} + } + if g.config.Offline { + return flagstate.NewAllFlags() + } + + // prepare evaluation context enrichment + flagCtx := flag.Context{ + EvaluationContextEnrichment: g.config.EvaluationContextEnrichment, + DefaultSdkValue: nil, + } + flagCtx.AddIntoEvaluationContextEnrichment("env", g.config.Environment) + + // Evaluate only the flags in flagsToEvaluate + if len(flagsToEvaluate) != 0 { + flagStates := flagstate.NewAllFlags() + for _, key := range flagsToEvaluate { + currentFlag, err := g.cache.GetFlag(key) + if err != nil { + // We ignore flags in error + continue + } + flagStates.AddFlag(key, flagstate.FromFlagEvaluation(key, evaluationCtx, flagCtx, currentFlag)) + } + return flagStates + } + + // Evaluate all the flags + flags, err := g.GetFlagsFromCache() + if err != nil { + return flagstate.AllFlags{} + } + allFlags := flagstate.NewAllFlags() + for key, currentFlag := range flags { + allFlags.AddFlag(key, flagstate.FromFlagEvaluation(key, evaluationCtx, flagCtx, currentFlag)) + } + return allFlags +} + +// AllFlagsState return a flagstate.AllFlags that contains all the flags for a specific user. +func (g *GoFeatureFlag) AllFlagsState(evaluationCtx ffcontext.Context) flagstate.AllFlags { + if g == nil { + return flagstate.AllFlags{} + } + return g.GetFlagStates(evaluationCtx, []string{}) +} + +// GetFlagsFromCache returns all the flags present in the cache with their +// current state when calling this method. If cache hasn't been initialized, an +// error reporting this is returned. +func (g *GoFeatureFlag) GetFlagsFromCache() (map[string]flag.Flag, error) { + return g.cache.AllFlags() +} diff --git a/variation_all_flags_test.go b/variation_all_flags_test.go new file mode 100644 index 00000000000..fe0d9553577 --- /dev/null +++ b/variation_all_flags_test.go @@ -0,0 +1,280 @@ +package ffclient + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" + "github.com/thomaspoignant/go-feature-flag/ffcontext" + "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" + "os" + "testing" + "time" +) + +func TestAllFlagsState(t *testing.T) { + tests := []struct { + name string + config Config + valid bool + jsonOutput string + initModule bool + }{ + { + name: "Valid multiple types", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: true, + jsonOutput: "./testdata/ffclient/all_flags/marshal_json/valid_multiple_types.json", + initModule: true, + }, + { + name: "Error in flag-0", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-with-error.yaml", + }, + }, + valid: false, + jsonOutput: "./testdata/ffclient/all_flags/marshal_json/error_in_flag_0.json", + initModule: true, + }, + { + name: "module not init", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: false, + jsonOutput: "./testdata/ffclient/all_flags/marshal_json/module_not_init.json", + initModule: false, + }, + { + name: "offline", + config: Config{ + Offline: true, + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: true, + jsonOutput: "./testdata/ffclient/all_flags/marshal_json/offline.json", + initModule: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // init logger + exportDir, _ := os.MkdirTemp("", "export") + tt.config.DataExporter = DataExporter{ + FlushInterval: 1000, + MaxEventInMemory: 1, + Exporter: &fileexporter.Exporter{OutputDir: exportDir}, + } + + var goff *GoFeatureFlag + var err error + if tt.initModule { + goff, err = New(tt.config) + assert.NoError(t, err) + defer goff.Close() + } else { + // we close directly so we can test with module not init + goff, _ = New(tt.config) + goff.Close() + } + + user := ffcontext.NewEvaluationContext("random-key") + allFlagsState := goff.AllFlagsState(user) + assert.Equal(t, tt.valid, allFlagsState.IsValid()) + + // expected JSON output - we force the timestamp + expected, _ := os.ReadFile(tt.jsonOutput) + var f map[string]interface{} + _ = json.Unmarshal(expected, &f) + if expectedFlags, ok := f["flags"].(map[string]interface{}); ok { + for _, value := range expectedFlags { + if valueObj, ok := value.(map[string]interface{}); ok { + assert.NotNil(t, valueObj["timestamp"]) + assert.NotEqual(t, 0, valueObj["timestamp"]) + valueObj["timestamp"] = time.Now().Unix() + } + } + } + expectedJSON, _ := json.Marshal(f) + marshaled, err := allFlagsState.MarshalJSON() + assert.NoError(t, err) + assert.JSONEq(t, string(expectedJSON), string(marshaled)) + + // no data exported + files, _ := os.ReadDir(exportDir) + assert.Equal(t, 0, len(files)) + }) + } +} + +func TestGetFlagStates(t *testing.T) { + tests := []struct { + name string + config Config + valid bool + jsonOutput string + initModule bool + evaluationContext ffcontext.EvaluationContext + }{ + { + name: "Valid multiple flags", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: true, + jsonOutput: "./testdata/ffclient/get_flagstates/marshal_json/valid_flag1_flag4.json", + initModule: true, + evaluationContext: ffcontext.NewEvaluationContextBuilder("123").AddCustom("gofeatureflag", map[string]interface{}{ + "flagList": []string{"test-flag1", "test-flag4"}, + }).Build(), + }, + { + name: "empty list of flags in context", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: true, + jsonOutput: "./testdata/ffclient/get_flagstates/marshal_json/all_flags.json", + initModule: true, + evaluationContext: ffcontext.NewEvaluationContextBuilder("123").AddCustom("gofeatureflag", map[string]interface{}{ + "flagList": []string{}, + }).Build(), + }, + { + name: "no field in context context", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/get_flagstates/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: true, + jsonOutput: "./testdata/ffclient/get_flagstates/marshal_json/all_flags.json", + initModule: true, + evaluationContext: ffcontext.NewEvaluationContextBuilder("123").Build(), + }, + { + name: "offline", + config: Config{ + Offline: true, + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", + }, + }, + valid: true, + jsonOutput: "./testdata/ffclient/all_flags/marshal_json/offline.json", + initModule: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // init logger + exportDir, _ := os.MkdirTemp("", "export") + tt.config.DataExporter = DataExporter{ + FlushInterval: 1000, + MaxEventInMemory: 1, + Exporter: &fileexporter.Exporter{OutputDir: exportDir}, + } + + var goff *GoFeatureFlag + var err error + if tt.initModule { + goff, err = New(tt.config) + assert.NoError(t, err) + defer goff.Close() + } else { + // we close directly so we can test with module not init + goff, _ = New(tt.config) + goff.Close() + } + + allFlagsState := goff.GetFlagStates(tt.evaluationContext, tt.evaluationContext.ExtractGOFFProtectedFields().FlagList) + assert.Equal(t, tt.valid, allFlagsState.IsValid()) + + // expected JSON output - we force the timestamp + expected, _ := os.ReadFile(tt.jsonOutput) + var f map[string]interface{} + _ = json.Unmarshal(expected, &f) + if expectedFlags, ok := f["flags"].(map[string]interface{}); ok { + for _, value := range expectedFlags { + if valueObj, ok := value.(map[string]interface{}); ok { + assert.NotNil(t, valueObj["timestamp"]) + assert.NotEqual(t, 0, valueObj["timestamp"]) + valueObj["timestamp"] = time.Now().Unix() + } + } + } + expectedJSON, _ := json.Marshal(f) + marshaled, err := allFlagsState.MarshalJSON() + assert.NoError(t, err) + assert.JSONEq(t, string(expectedJSON), string(marshaled)) + + // no data exported + files, _ := os.ReadDir(exportDir) + assert.Equal(t, 0, len(files)) + }) + } +} + +func TestAllFlagsFromCache(t *testing.T) { + tests := []struct { + name string + config Config + initModule bool + }{ + { + name: "Valid multiple types", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", + }, + }, + initModule: true, + }, + { + name: "module not init", + config: Config{ + Retriever: &fileretriever.Retriever{ + Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", + }, + }, + initModule: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var goff *GoFeatureFlag + var err error + if tt.initModule { + goff, err = New(tt.config) + assert.NoError(t, err) + defer goff.Close() + + flags, err := goff.GetFlagsFromCache() + assert.NoError(t, err) + + cf, _ := goff.cache.AllFlags() + assert.Equal(t, flags, cf) + } else { + // we close directly so we can test with module not init + goff, _ = New(tt.config) + goff.Close() + + _, err := goff.GetFlagsFromCache() + assert.Error(t, err) + } + }) + } +} diff --git a/variation_test.go b/variation_test.go index 8882bd21b76..60ede7eb10c 100644 --- a/variation_test.go +++ b/variation_test.go @@ -2,26 +2,22 @@ package ffclient import ( "context" - "encoding/json" "errors" "fmt" "github.com/stretchr/testify/assert" "github.com/thejerf/slogassert" "github.com/thomaspoignant/go-feature-flag/exporter" - "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" "github.com/thomaspoignant/go-feature-flag/exporter/logsexporter" "github.com/thomaspoignant/go-feature-flag/ffcontext" "github.com/thomaspoignant/go-feature-flag/internal/cache" "github.com/thomaspoignant/go-feature-flag/internal/dto" "github.com/thomaspoignant/go-feature-flag/internal/flag" "github.com/thomaspoignant/go-feature-flag/model" - "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" "github.com/thomaspoignant/go-feature-flag/testutils" "github.com/thomaspoignant/go-feature-flag/testutils/flagv1" "github.com/thomaspoignant/go-feature-flag/testutils/testconvert" "github.com/thomaspoignant/go-feature-flag/utils/fflog" "log/slog" - "os" "runtime" "strings" "testing" @@ -3522,162 +3518,6 @@ func TestIntVariationDetails(t *testing.T) { } } -func TestAllFlagsState(t *testing.T) { - tests := []struct { - name string - config Config - valid bool - jsonOutput string - initModule bool - }{ - { - name: "Valid multiple types", - config: Config{ - Retriever: &fileretriever.Retriever{ - Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", - }, - }, - valid: true, - jsonOutput: "./testdata/ffclient/all_flags/marshal_json/valid_multiple_types.json", - initModule: true, - }, - { - name: "Error in flag-0", - config: Config{ - Retriever: &fileretriever.Retriever{ - Path: "./testdata/ffclient/all_flags/config_flag/flag-config-with-error.yaml", - }, - }, - valid: false, - jsonOutput: "./testdata/ffclient/all_flags/marshal_json/error_in_flag_0.json", - initModule: true, - }, - { - name: "module not init", - config: Config{ - Retriever: &fileretriever.Retriever{ - Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", - }, - }, - valid: false, - jsonOutput: "./testdata/ffclient/all_flags/marshal_json/module_not_init.json", - initModule: false, - }, - { - name: "offline", - config: Config{ - Offline: true, - Retriever: &fileretriever.Retriever{ - Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", - }, - }, - valid: true, - jsonOutput: "./testdata/ffclient/all_flags/marshal_json/offline.json", - initModule: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // init logger - exportDir, _ := os.MkdirTemp("", "export") - tt.config.DataExporter = DataExporter{ - FlushInterval: 1000, - MaxEventInMemory: 1, - Exporter: &fileexporter.Exporter{OutputDir: exportDir}, - } - - var goff *GoFeatureFlag - var err error - if tt.initModule { - goff, err = New(tt.config) - assert.NoError(t, err) - defer goff.Close() - } else { - // we close directly so we can test with module not init - goff, _ = New(tt.config) - goff.Close() - } - - user := ffcontext.NewEvaluationContext("random-key") - allFlagsState := goff.AllFlagsState(user) - assert.Equal(t, tt.valid, allFlagsState.IsValid()) - - // expected JSON output - we force the timestamp - expected, _ := os.ReadFile(tt.jsonOutput) - var f map[string]interface{} - _ = json.Unmarshal(expected, &f) - if expectedFlags, ok := f["flags"].(map[string]interface{}); ok { - for _, value := range expectedFlags { - if valueObj, ok := value.(map[string]interface{}); ok { - assert.NotNil(t, valueObj["timestamp"]) - assert.NotEqual(t, 0, valueObj["timestamp"]) - valueObj["timestamp"] = time.Now().Unix() - } - } - } - expectedJSON, _ := json.Marshal(f) - marshaled, err := allFlagsState.MarshalJSON() - assert.NoError(t, err) - assert.JSONEq(t, string(expectedJSON), string(marshaled)) - - // no data exported - files, _ := os.ReadDir(exportDir) - assert.Equal(t, 0, len(files)) - }) - } -} - -func TestAllFlagsFromCache(t *testing.T) { - tests := []struct { - name string - config Config - initModule bool - }{ - { - name: "Valid multiple types", - config: Config{ - Retriever: &fileretriever.Retriever{ - Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", - }, - }, - initModule: true, - }, - { - name: "module not init", - config: Config{ - Retriever: &fileretriever.Retriever{ - Path: "./testdata/ffclient/all_flags/config_flag/flag-config-all-flags.yaml", - }, - }, - initModule: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var goff *GoFeatureFlag - var err error - if tt.initModule { - goff, err = New(tt.config) - assert.NoError(t, err) - defer goff.Close() - - flags, err := goff.GetFlagsFromCache() - assert.NoError(t, err) - - cf, _ := goff.cache.AllFlags() - assert.Equal(t, flags, cf) - } else { - // we close directly so we can test with module not init - goff, _ = New(tt.config) - goff.Close() - - _, err := goff.GetFlagsFromCache() - assert.Error(t, err) - } - }) - } -} - func TestRawVariation(t *testing.T) { type args struct { flagKey string diff --git a/website/docs/configure_flag/rule_format.md b/website/docs/configure_flag/rule_format.md index 9914856bbd5..0126a04fd65 100644 --- a/website/docs/configure_flag/rule_format.md +++ b/website/docs/configure_flag/rule_format.md @@ -25,9 +25,10 @@ The targeting key is a fundamental part of the evaluation context because it dir When you create an evaluation context some fields are reserved for GO Feature Flag. Those fields are used by GO Feature Flag directly, you can use them as will but you should be aware that they are used by GO Feature Flag. -| Field | Description | -|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `gofeatureflag.currentDateTime` | If this property is set, we will use this date as base for all the rollout strategies which implies dates _(experimentation, progressive and scheduled)_.
**Format:** Date following the RF3339 format. | +| Field | Description | +|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `gofeatureflag.currentDateTime` | If this property is set, we will use this date as base for all the rollout strategies which implies dates _(experimentation, progressive and scheduled)_.
**Format:** Date following the RF3339 format. | +| `gofeatureflag.flagList` | If this property is set, in the bulk evaluation mode (for the client SDK) we will only evaluate the flags in this list.
If empty or not set the default behavior is too evaluate all the flags.
**Format:** []string | ## Rule format diff --git a/website/docs/openfeature_sdk/client_providers/openfeature_android.mdx b/website/docs/openfeature_sdk/client_providers/openfeature_android.mdx index 221cc1e5a61..146bbfb61a2 100644 --- a/website/docs/openfeature_sdk/client_providers/openfeature_android.mdx +++ b/website/docs/openfeature_sdk/client_providers/openfeature_android.mdx @@ -109,6 +109,41 @@ OpenFeatureAPI.setEvaluationContext(newEvalCtx) `setEvaluationContext()` is a synchronous function similar to `setProvider()` and will fetch the new version of the feature flags based on this new `EvaluationContext`. +### Limit the flags to evaluate + +By default, the provider will fetch all the flags configured in the GO Feature Flag server to be ready to evaluate them. +If you know in advance, what are the flags you will evaluate in your application, you can specify the list of flags to evaluate in the context. + +You need to add in the evaluation context the restricted key `gofeatureflag.flagList` with the list of flags you want to evaluate. + +```kotlin +val newContext: EvaluationContext = ImmutableContext( + targetingKey = "userId", + attributes = mapOf( + "gofeatureflag" to Value.Structure( + mapOf( + "flagList" to Value.List( + listOf( + // list of flags to evaluate + Value.String("flag1"), + Value.String("flag2"), + Value.String("flag3") + ) + ), + ) + ), + ) + ) + +OpenFeatureAPI.setEvaluationContext(newEvalCtx) +``` + +By setting the `gofeatureflag.flagList` key in the context, the provider will only fetch the flags specified in the list. + +:::warning +When limiting the flags to evaluate, if you try to evaluate a flag not in the list, the provider will return the default value with the error `FLAG_NOT_FOUND`. +::: + ### Evaluate a feature flag The client is used to retrieve values for the current `EvaluationContext`. For example, retrieving a boolean value for the flag **"my-flag"**: diff --git a/website/docs/openfeature_sdk/client_providers/openfeature_javascript.mdx b/website/docs/openfeature_sdk/client_providers/openfeature_javascript.mdx index e4231e50f24..f3d899ce3a1 100644 --- a/website/docs/openfeature_sdk/client_providers/openfeature_javascript.mdx +++ b/website/docs/openfeature_sdk/client_providers/openfeature_javascript.mdx @@ -56,6 +56,30 @@ client.addHandler(ProviderEvents.Stale, () => { //... }); client.addHandler(ProviderEvents.ConfigurationChanged, () => { //... }); ``` +### Limit the flags to evaluate + +By default, the provider will fetch all the flags configured in the GO Feature Flag server to be ready to evaluate them. +If you know in advance, what are the flags you will evaluate in your application, you can specify the list of flags to evaluate in the context. + +You need to add in the evaluation context the restricted key `gofeatureflag.flagList` with the list of flags you want to evaluate. + +```typescript +OpenFeature.setContext({ + // ... + gofeatureflag: { + flagList: ['flag1', 'flag2'] + } +}); + +await OpenFeature.setContext(evaluationCtx); +``` + +By setting the `gofeatureflag.flagList` key in the context, the provider will only fetch the flags specified in the list. + +:::warning +When limiting the flags to evaluate, if you try to evaluate a flag not in the list, the provider will return the default value with the error `FLAG_NOT_FOUND`. +::: + ### Available options | Option name | Type | Default | Description | |-------------------------------|--------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/website/docs/openfeature_sdk/client_providers/openfeature_swift.mdx b/website/docs/openfeature_sdk/client_providers/openfeature_swift.mdx index d9faf8e1423..d9579030396 100644 --- a/website/docs/openfeature_sdk/client_providers/openfeature_swift.mdx +++ b/website/docs/openfeature_sdk/client_providers/openfeature_swift.mdx @@ -89,6 +89,31 @@ OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx) `setEvaluationContext()` is a synchronous function similar to `setProvider()` and will fetch the new version of the feature flags based on this new `EvaluationContext`. +### Limit the flags to evaluate + +By default, the provider will fetch all the flags configured in the GO Feature Flag server to be ready to evaluate them. +If you know in advance, what are the flags you will evaluate in your application, you can specify the list of flags to evaluate in the context. + +You need to add in the evaluation context the restricted key `gofeatureflag.flagList` with the list of flags you want to evaluate. + +```swift +let ctx = MutableContext(targetingKey: "myNewTargetingKey") +ctx.add( + key: "gofeatureflag", + value: Value.list([ + Value.string("flag1"), + Value.string("flag2") + ]) +) +OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: ctx) +``` + +By setting the `gofeatureflag.flagList` key in the context, the provider will only fetch the flags specified in the list. + +:::warning +When limiting the flags to evaluate, if you try to evaluate a flag not in the list, the provider will return the default value with the error `FLAG_NOT_FOUND`. +::: + ### Evaluate a feature flag The client is used to retrieve values for the current `EvaluationContext`. For example, retrieving a boolean value for the flag **"my-flag"**: