diff --git a/internal/flag/error_code.go b/internal/flag/error_code.go new file mode 100644 index 00000000000..b177f39924e --- /dev/null +++ b/internal/flag/error_code.go @@ -0,0 +1,12 @@ +package flag + +// ErrorCode is an enum following the open-feature specs about error code. +type ErrorCode = string + +const ( + ErrorCodeProviderNotReady ErrorCode = "PROVIDER_NOT_READY" + ErrorCodeFlagNotFound ErrorCode = "FLAG_NOT_FOUND" + ErrorCodeParseError ErrorCode = "PARSE_ERROR" + ErrorCodeTypeMismatch ErrorCode = "TYPE_MISMATCH" + ErrorCodeGeneral ErrorCode = "GENERAL" +) diff --git a/internal/flag/evaluation_context.go b/internal/flag/evaluation_context.go new file mode 100644 index 00000000000..e453957aaf2 --- /dev/null +++ b/internal/flag/evaluation_context.go @@ -0,0 +1,11 @@ +package flag + +type EvaluationContext struct { + // Environment is the name of your current env + // this value will be added to the custom information of your user and, + // it will allow to create rules based on this environment, + Environment string + + // DefaultSdkValue is the default value of the SDK when calling the variation. + DefaultSdkValue interface{} +} diff --git a/internal/flag/flag.go b/internal/flag/flag.go index 74974d60428..f6369445188 100644 --- a/internal/flag/flag.go +++ b/internal/flag/flag.go @@ -5,9 +5,8 @@ import ( ) type Flag interface { - // Value is returning the Value associate to the flag (True / False / Default ) based - // if the flag apply to the user and environment or not. - Value(flagName string, user ffuser.User, environment string) (interface{}, string) + // Value is returning the Value associate to the flag (True / False / Default ) + Value(flagName string, user ffuser.User, evaluationCtx EvaluationContext) (interface{}, ResolutionDetails) // String display correctly a flag with the right formatting String() string diff --git a/internal/flag/resolution_detail.go b/internal/flag/resolution_detail.go new file mode 100644 index 00000000000..66f50cc8139 --- /dev/null +++ b/internal/flag/resolution_detail.go @@ -0,0 +1,12 @@ +package flag + +type ResolutionDetails struct { + // Variant indicates the name of the variant used when evaluating the flag + Variant string + + // Reason indicates the reason of the decision + Reason ResolutionReason + + // ErrorCode indicates the error code for this evaluation + ErrorCode string +} diff --git a/internal/flag/resolution_reason.go b/internal/flag/resolution_reason.go new file mode 100644 index 00000000000..38ae6a50c5c --- /dev/null +++ b/internal/flag/resolution_reason.go @@ -0,0 +1,36 @@ +package flag + +// ResolutionReason is an enum following the open-feature specs about resolution reasons. +type ResolutionReason = string + +const ( + // ReasonTargetingMatch Indicates that the feature flag is targeting + // 100% of the targeting audience, + // e.g. 100% rollout percentage + ReasonTargetingMatch ResolutionReason = "TARGETING_MATCH" + + // ReasonSplit Indicates that the feature flag is targeting + // a subset of the targeting audience, + // e.g. less than 100% rollout percentage + ReasonSplit ResolutionReason = "SPLIT" + + // ReasonDisabled Indicates that the feature flag is disabled + ReasonDisabled ResolutionReason = "DISABLED" + + // ReasonDefault Indicates that the feature flag evaluated to the default value + ReasonDefault ResolutionReason = "DEFAULT" + + // ReasonStatic Indicates that the feature flag evaluated to a + // static value, for example, the default value for the flag + // + // Note: Typically means that no dynamic evaluation has been + // executed for the feature flag + ReasonStatic ResolutionReason = "STATIC" + + // ReasonUnknown Indicates an unknown issue occurred during evaluation + ReasonUnknown ResolutionReason = "UNKNOWN" + + // ReasonError Indicates that an error occurred during evaluation + // Note: The `errorCode`-field contains the details of this error + ReasonError ResolutionReason = "ERROR" +) diff --git a/internal/flagstate/flag_state.go b/internal/flagstate/flag_state.go index 9d8325fbe1f..b66fe88691f 100644 --- a/internal/flagstate/flag_state.go +++ b/internal/flagstate/flag_state.go @@ -1,30 +1,16 @@ package flagstate import ( - "time" + "github.com/thomaspoignant/go-feature-flag/internal/flag" ) -// NewFlagState is creating a state for a flag. -func NewFlagState( - trackEvents bool, - value interface{}, - variationType string, - failed bool, -) FlagState { - return FlagState{ - Value: value, - Timestamp: time.Now().Unix(), - VariationType: variationType, - Failed: failed, - TrackEvents: trackEvents, - } -} - // FlagState represents the state of an individual feature flag, with regard to a specific user, when it was called. type FlagState struct { - Value interface{} `json:"value"` - Timestamp int64 `json:"timestamp"` - VariationType string `json:"variationType"` - TrackEvents bool `json:"trackEvents"` - Failed bool `json:"-"` + Value interface{} `json:"value"` + Timestamp int64 `json:"timestamp"` + VariationType string `json:"variationType"` + TrackEvents bool `json:"trackEvents"` + Failed bool `json:"-"` + ErrorCode flag.ErrorCode `json:"errorCode"` + Reason flag.ResolutionReason `json:"reason"` } diff --git a/internal/flagv1/flag_data.go b/internal/flagv1/flag_data.go index b72eb2edf27..83f43ec9543 100644 --- a/internal/flagv1/flag_data.go +++ b/internal/flagv1/flag_data.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/thomaspoignant/go-feature-flag/internal/flag" + "github.com/nikunjy/rules/parser" "github.com/thomaspoignant/go-feature-flag/ffuser" "github.com/thomaspoignant/go-feature-flag/internal/utils" @@ -56,24 +58,56 @@ type FlagData struct { // Value is returning the Value associate to the flag (True / False / Default ) based // if the toggle apply to the user or not. -func (f *FlagData) Value(flagName string, user ffuser.User, environment string) (interface{}, string) { +func (f *FlagData) Value( + flagName string, + user ffuser.User, + evaluationCtx flag.EvaluationContext, +) (interface{}, flag.ResolutionDetails) { f.updateFlagStage() if f.isExperimentationOver() { // if we have an experimentation that has not started or that is finished we use the default value. - return f.getDefault(), VariationDefault + return f.getDefault(), flag.ResolutionDetails{ + Variant: VariationDefault, + Reason: flag.ReasonDefault, + } } - if f.evaluateRule(user, environment) { + // Flag disable we cannot apply it. + if f.GetDisable() { + return evaluationCtx.DefaultSdkValue, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonDisabled, + } + } + + // we are targeting all users + if f.getRule() == "" && f.getPercentage() == 100 { + return f.getTrue(), flag.ResolutionDetails{ + Variant: VariationTrue, + Reason: flag.ReasonTargetingMatch, + } + } + + if f.evaluateRule(user, evaluationCtx.Environment) { if f.isInPercentage(flagName, user) { // Rule applied and user in the cohort. - return f.getTrue(), VariationTrue + return f.getTrue(), flag.ResolutionDetails{ + Variant: VariationTrue, + Reason: flag.ReasonSplit, + } } // Rule applied and user not in the cohort. - return f.getFalse(), VariationFalse + return f.getFalse(), flag.ResolutionDetails{ + Variant: VariationFalse, + Reason: flag.ReasonSplit, + } } - // Default value is used if the rule does not applied to the user. - return f.getDefault(), VariationDefault + // Default value is used if the rule does not apply to the user. + return f.getDefault(), flag.ResolutionDetails{ + Variant: VariationDefault, + Reason: flag.ReasonDefault, + } } func (f *FlagData) isExperimentationOver() bool { @@ -103,11 +137,6 @@ func (f *FlagData) isInPercentage(flagName string, user ffuser.User) bool { // evaluateRule is checking if the rule can apply to a specific user. func (f *FlagData) evaluateRule(user ffuser.User, environment string) bool { - // Flag disable we cannot apply it. - if f.GetDisable() { - return false - } - // No rule means that all user can be impacted. if f.getRule() == "" { return true diff --git a/internal/flagv1/flag_priv_test.go b/internal/flagv1/flag_priv_test.go index c9fcf626c11..f1989e9146d 100644 --- a/internal/flagv1/flag_priv_test.go +++ b/internal/flagv1/flag_priv_test.go @@ -28,16 +28,6 @@ func TestFlag_evaluateRule(t *testing.T) { args args want bool }{ - { - name: "Disabled toggle", - fields: fields{ - Disable: true, - }, - args: args{ - user: ffuser.NewAnonymousUser("random-key"), - }, - want: false, - }, { name: "Toggle enabled and no rule", fields: fields{ diff --git a/internal/flagv1/flag_pub_test.go b/internal/flagv1/flag_pub_test.go index ec6cb38a38c..5b92d25130c 100644 --- a/internal/flagv1/flag_pub_test.go +++ b/internal/flagv1/flag_pub_test.go @@ -24,12 +24,13 @@ func TestFlag_value(t *testing.T) { Rollout flagv1.Rollout } type args struct { - flagName string - user ffuser.User + flagName string + user ffuser.User + sdkDefault string } type want struct { - value interface{} - variationType string + value interface{} + resolutionDetails flag.ResolutionDetails } tests := []struct { name string @@ -38,7 +39,7 @@ func TestFlag_value(t *testing.T) { want want }{ { - name: "Rule disable get default value", + name: "Flag disable get default value", fields: fields{ Disable: true, True: "true", @@ -46,12 +47,17 @@ func TestFlag_value(t *testing.T) { Default: "default", }, args: args{ - flagName: "test_689483", - user: ffuser.NewUser("test_689483"), + flagName: "test_689483", + user: ffuser.NewUser("test_689483"), + sdkDefault: "defaultSDK", }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "defaultSDK", + resolutionDetails: flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonDisabled, + ErrorCode: "", + }, }, }, { @@ -68,8 +74,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), // combined hash is 9 }, want: want{ - value: "true", - variationType: flagv1.VariationTrue, + value: "true", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationTrue, + Reason: flag.ReasonSplit, + ErrorCode: "", + }, }, }, { @@ -92,8 +102,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), }, want: want{ - value: "true", - variationType: flagv1.VariationTrue, + value: "true", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationTrue, + Reason: flag.ReasonSplit, + ErrorCode: "", + }, }, }, { @@ -116,8 +130,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("user66").AddCustom("name", "john").Build(), // combined hash is 9 }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "default", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationDefault, + Reason: flag.ReasonDefault, + ErrorCode: "", + }, }, }, { @@ -140,8 +158,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), }, want: want{ - value: "true", - variationType: flagv1.VariationTrue, + value: "true", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationTrue, + Reason: flag.ReasonSplit, + ErrorCode: "", + }, }, }, { @@ -164,8 +186,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("user66").AddCustom("name", "john").Build(), // combined hash is 9 }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "default", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationDefault, + Reason: flag.ReasonDefault, + ErrorCode: "", + }, }, }, { @@ -188,8 +214,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("user66").AddCustom("name", "john").Build(), // combined hash is 9 }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "default", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationDefault, + Reason: flag.ReasonDefault, + ErrorCode: "", + }, }, }, { @@ -212,8 +242,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("user66").AddCustom("name", "john").Build(), // combined hash is 9 }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "default", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationDefault, + Reason: flag.ReasonDefault, + ErrorCode: "", + }, }, }, { @@ -236,8 +270,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), }, want: want{ - value: "true", - variationType: flagv1.VariationTrue, + value: "true", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationTrue, + Reason: flag.ReasonSplit, + ErrorCode: "", + }, }, }, { @@ -260,8 +298,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), }, want: want{ - value: "true", - variationType: flagv1.VariationTrue, + value: "true", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationTrue, + Reason: flag.ReasonSplit, + ErrorCode: "", + }, }, }, { @@ -284,8 +326,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("user66").AddCustom("name", "john").Build(), // combined hash is 9 }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "default", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationDefault, + Reason: flag.ReasonDefault, + ErrorCode: "", + }, }, }, { @@ -302,8 +348,12 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), }, want: want{ - value: "default", - variationType: flagv1.VariationDefault, + value: "default", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationDefault, + Reason: flag.ReasonDefault, + ErrorCode: "", + }, }, }, { @@ -320,8 +370,33 @@ func TestFlag_value(t *testing.T) { user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), }, want: want{ - value: "false", - variationType: flagv1.VariationFalse, + value: "false", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationFalse, + Reason: flag.ReasonSplit, + ErrorCode: "", + }, + }, + }, + { + name: "Flag target everyone", + fields: fields{ + True: "true", + False: "false", + Default: "default", + Percentage: 100, + }, + args: args{ + flagName: "test-flag2", + user: ffuser.NewUserBuilder("7e50ee61-06ad-4bb0-9034-38ad7cdea9f5").AddCustom("name", "john").Build(), + }, + want: want{ + value: "true", + resolutionDetails: flag.ResolutionDetails{ + Variant: flagv1.VariationTrue, + Reason: flag.ReasonTargetingMatch, + ErrorCode: "", + }, }, }, } @@ -337,9 +412,10 @@ func TestFlag_value(t *testing.T) { Rollout: &tt.fields.Rollout, } - got, variationType := f.Value(tt.args.flagName, tt.args.user, "") + got, resolutionDetails := f.Value(tt.args.flagName, tt.args.user, + flag.EvaluationContext{DefaultSdkValue: tt.args.sdkDefault}) assert.Equal(t, tt.want.value, got) - assert.Equal(t, tt.want.variationType, variationType) + assert.Equal(t, tt.want.resolutionDetails, resolutionDetails) }) } } @@ -362,15 +438,15 @@ func TestFlag_ProgressiveRollout(t *testing.T) { flagName := "test-flag" // We evaluate the same flag multiple time overtime. - v, _ := f.Value(flagName, user, "") + v, _ := f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, f.GetVariationValue(flagv1.VariationFalse), v) time.Sleep(1 * time.Second) - v2, _ := f.Value(flagName, user, "") + v2, _ := f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, f.GetVariationValue(flagv1.VariationFalse), v2) time.Sleep(1 * time.Second) - v3, _ := f.Value(flagName, user, "") + v3, _ := f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, f.GetVariationValue(flagv1.VariationTrue), v3) } @@ -447,43 +523,43 @@ func TestFlag_ScheduledRollout(t *testing.T) { flagName := "test-flag" // We evaluate the same flag multiple time overtime. - v, _ := f.Value(flagName, user, "") + v, _ := f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, f.GetVariationValue(flagv1.VariationFalse), v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, "True", v) assert.Equal(t, 1.1, f.GetVersion()) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, "Default2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, "True2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{DefaultSdkValue: "Default2"}) assert.Equal(t, "Default2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, "Default2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, "True2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user, "") + v, _ = f.Value(flagName, user, flag.EvaluationContext{}) assert.Equal(t, "Default2", v) } diff --git a/internal/model/variation_result.go b/internal/model/variation_result.go index 41bc712af8b..43a3c908605 100644 --- a/internal/model/variation_result.go +++ b/internal/model/variation_result.go @@ -1,10 +1,14 @@ package model +import "github.com/thomaspoignant/go-feature-flag/internal/flag" + type VariationResult struct { - TrackEvents bool `json:"trackEvents"` - VariationType string `json:"variationType"` - Failed bool `json:"failed"` - Version float64 `json:"version"` + TrackEvents bool `json:"trackEvents"` + VariationType string `json:"variationType"` + Failed bool `json:"failed"` + Version float64 `json:"version"` + Reason flag.ResolutionReason `json:"reason"` + ErrorCode flag.ErrorCode `json:"errorCode"` } // BoolVarResult is the internal result format of a bool variation. diff --git a/testdata/ffclient/all_flags/marshal_json/error_in_flag_0.json b/testdata/ffclient/all_flags/marshal_json/error_in_flag_0.json index 224ef76ecc3..cd308ff4f65 100644 --- a/testdata/ffclient/all_flags/marshal_json/error_in_flag_0.json +++ b/testdata/ffclient/all_flags/marshal_json/error_in_flag_0.json @@ -4,19 +4,25 @@ "value": false, "timestamp": 1622206239, "variationType": "Default", - "trackEvents": true + "trackEvents": true, + "reason":"ERROR", + "errorCode": "TYPE_MISMATCH" }, "test-flag1": { "value": "true", "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag2": { "value": 1, "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag3": { "value": [ @@ -25,7 +31,9 @@ ], "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag4": { "value": { @@ -33,13 +41,17 @@ }, "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag5": { "value": 1.1, "timestamp": 1622206239, "variationType": "True", - "trackEvents": false + "trackEvents": false, + "reason":"TARGETING_MATCH", + "errorCode": "" } }, "valid": false diff --git a/testdata/ffclient/all_flags/marshal_json/valid_multiple_types.json b/testdata/ffclient/all_flags/marshal_json/valid_multiple_types.json index 87933074513..94ff9bf786e 100644 --- a/testdata/ffclient/all_flags/marshal_json/valid_multiple_types.json +++ b/testdata/ffclient/all_flags/marshal_json/valid_multiple_types.json @@ -4,19 +4,25 @@ "value": true, "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag1": { "value": "true", "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag2": { "value": 1, "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag3": { "value": [ @@ -25,7 +31,9 @@ ], "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag4": { "value": { @@ -33,13 +41,17 @@ }, "timestamp": 1622206239, "variationType": "True", - "trackEvents": true + "trackEvents": true, + "reason":"TARGETING_MATCH", + "errorCode": "" }, "test-flag5": { "value": 1.1, "timestamp": 1622206239, "variationType": "True", - "trackEvents": false + "trackEvents": false, + "reason":"TARGETING_MATCH", + "errorCode": "" } }, "valid": true diff --git a/variation.go b/variation.go index a1c49c8f978..ec28b5fa496 100644 --- a/variation.go +++ b/variation.go @@ -1,7 +1,9 @@ +//nolint:,dupl package ffclient import ( "fmt" + "time" "github.com/thomaspoignant/go-feature-flag/exporter" "github.com/thomaspoignant/go-feature-flag/ffuser" @@ -153,16 +155,36 @@ func (g *GoFeatureFlag) AllFlagsState(user ffuser.User) flagstate.AllFlags { allFlags := flagstate.NewAllFlags() for key, currentFlag := range flags { - flagValue, varType := currentFlag.Value(key, user, g.config.Environment) + flagValue, varType := currentFlag.Value(key, user, flag.EvaluationContext{ + Environment: g.config.Environment, + DefaultSdkValue: nil, + }) switch v := flagValue; v.(type) { case int, float64, bool, string, []interface{}, map[string]interface{}: - allFlags.AddFlag(key, flagstate.NewFlagState(currentFlag.GetTrackEvents(), v, varType, false)) + allFlags.AddFlag(key, flagstate.FlagState{ + Value: v, + Timestamp: time.Now().Unix(), + VariationType: varType.Variant, + TrackEvents: currentFlag.GetTrackEvents(), + Failed: varType.ErrorCode != "", + ErrorCode: varType.ErrorCode, + Reason: varType.Reason, + }) default: defaultVariationName := currentFlag.GetDefaultVariation() defaultVariationValue := currentFlag.GetVariationValue(defaultVariationName) allFlags.AddFlag( - key, flagstate.NewFlagState(currentFlag.GetTrackEvents(), defaultVariationValue, defaultVariationName, true)) + key, + flagstate.FlagState{ + Value: defaultVariationValue, + Timestamp: time.Now().Unix(), + VariationType: defaultVariationName, + TrackEvents: currentFlag.GetTrackEvents(), + Failed: true, + ErrorCode: flag.ErrorCodeTypeMismatch, + Reason: flag.ReasonError, + }) } } return allFlags @@ -186,27 +208,37 @@ func (g *GoFeatureFlag) boolVariation(flagKey string, user ffuser.User, sdkDefau f, err := g.getFlagFromCache(flagKey) if err != nil { return model.BoolVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), }, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res, ok := flagValue.(bool) if !ok { return model.BoolVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, + flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + }), }, fmt.Errorf(errorWrongVariation, flagKey) } return model.BoolVarResult{ Value: res, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } // intVariation is the internal func that handle the logic of a variation with an int value -// the result will always contains a valid model.IntVarResult +// the result will always contain a valid model.IntVarResult func (g *GoFeatureFlag) intVariation(flagKey string, user ffuser.User, sdkDefaultValue int, ) (model.IntVarResult, error) { if g.config.Offline { @@ -216,30 +248,39 @@ func (g *GoFeatureFlag) intVariation(flagKey string, user ffuser.User, sdkDefaul f, err := g.getFlagFromCache(flagKey) if err != nil { return model.IntVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), }, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res, ok := flagValue.(int) if !ok { // if this is a float64 we convert it to int if resFloat, okFloat := flagValue.(float64); okFloat { return model.IntVarResult{ Value: int(resFloat), - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } return model.IntVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + }), }, fmt.Errorf(errorWrongVariation, flagKey) } return model.IntVarResult{ Value: res, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } @@ -254,22 +295,31 @@ func (g *GoFeatureFlag) float64Variation(flagKey string, user ffuser.User, sdkDe f, err := g.getFlagFromCache(flagKey) if err != nil { return model.Float64VarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), }, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res, ok := flagValue.(float64) if !ok { return model.Float64VarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + }), }, fmt.Errorf(errorWrongVariation, flagKey) } return model.Float64VarResult{ Value: res, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } @@ -284,22 +334,31 @@ func (g *GoFeatureFlag) stringVariation(flagKey string, user ffuser.User, sdkDef f, err := g.getFlagFromCache(flagKey) if err != nil { return model.StringVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), }, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res, ok := flagValue.(string) if !ok { return model.StringVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + }), }, fmt.Errorf(errorWrongVariation, flagKey) } return model.StringVarResult{ Value: res, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } @@ -314,22 +373,31 @@ func (g *GoFeatureFlag) jsonArrayVariation(flagKey string, user ffuser.User, sdk f, err := g.getFlagFromCache(flagKey) if err != nil { return model.JSONArrayVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), }, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res, ok := flagValue.([]interface{}) if !ok { return model.JSONArrayVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + }), }, fmt.Errorf(errorWrongVariation, flagKey) } return model.JSONArrayVarResult{ Value: res, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } @@ -344,30 +412,41 @@ func (g *GoFeatureFlag) jsonVariation(flagKey string, user ffuser.User, sdkDefau f, err := g.getFlagFromCache(flagKey) if err != nil { return model.JSONVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), }, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res, ok := flagValue.(map[string]interface{}) if !ok { return model.JSONVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeTypeMismatch, + }), }, fmt.Errorf(errorWrongVariation, flagKey) } return model.JSONVarResult{ Value: res, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), }, nil } // computeVariationResult is creating a model.VariationResult -func computeVariationResult(flag flag.Flag, variationType string, failed bool) model.VariationResult { +func computeVariationResult(flag flag.Flag, resolutionDetails flag.ResolutionDetails) model.VariationResult { varResult := model.VariationResult{ - VariationType: variationType, - Failed: failed, + VariationType: resolutionDetails.Variant, + Failed: resolutionDetails.ErrorCode != "", + Reason: resolutionDetails.Reason, + ErrorCode: resolutionDetails.ErrorCode, } if flag != nil { @@ -392,17 +471,22 @@ func (g *GoFeatureFlag) RawVariation(flagKey string, user ffuser.User, sdkDefaul f, err := g.getFlagFromCache(flagKey) if err != nil { res := model.RawVarResult{ - Value: sdkDefaultValue, - VariationResult: computeVariationResult(f, flag.VariationSDKDefault, true), + Value: sdkDefaultValue, + VariationResult: computeVariationResult(f, flag.ResolutionDetails{ + Variant: flag.VariationSDKDefault, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, + }), } g.notifyVariation(flagKey, user, res.VariationResult, res.Value) return res, err } - flagValue, variationType := f.Value(flagKey, user, g.config.Environment) + flagValue, resolutionDetails := f.Value(flagKey, user, + flag.EvaluationContext{Environment: g.config.Environment, DefaultSdkValue: sdkDefaultValue}) res := model.RawVarResult{ Value: flagValue, - VariationResult: computeVariationResult(f, variationType, false), + VariationResult: computeVariationResult(f, resolutionDetails), } g.notifyVariation(flagKey, user, res.VariationResult, res.Value) return res, nil @@ -429,9 +513,9 @@ func (g *GoFeatureFlag) notifyVariation( // getFlagFromCache try to get the flag from the cache. // It returns an error if the cache is not init or if the flag is not present or disabled. func (g *GoFeatureFlag) getFlagFromCache(flagKey string) (flag.Flag, error) { - flag, err := g.cache.GetFlag(flagKey) - if err != nil || flag.GetDisable() { - return flag, fmt.Errorf(errorFlagNotAvailable, flagKey) + f, err := g.cache.GetFlag(flagKey) + if err != nil { + return f, fmt.Errorf(errorFlagNotAvailable, flagKey) } - return flag, nil + return f, nil } diff --git a/variation_test.go b/variation_test.go index 5ca9fd0f435..4787907919b 100644 --- a/variation_test.go +++ b/variation_test.go @@ -79,7 +79,7 @@ func TestBoolVariation(t *testing.T) { }, nil), }, want: true, - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"true\", variation=\"SdkDefault\"\n", }, { @@ -281,7 +281,7 @@ func TestFloat64Variation(t *testing.T) { }, nil), }, want: 120.12, - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"120.12\", variation=\"SdkDefault\"\n", }, { @@ -482,7 +482,7 @@ func TestJSONArrayVariation(t *testing.T) { }, nil), }, want: []interface{}{"toto"}, - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"\\[toto\\]\"\n", }, { @@ -697,7 +697,7 @@ func TestJSONVariation(t *testing.T) { }, nil), }, want: map[string]interface{}{"default-notkey": true}, - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"map\\[default-notkey:true\\]\", variation=\"SdkDefault\"\n", }, { @@ -880,7 +880,7 @@ func TestStringVariation(t *testing.T) { }, nil), }, want: "default-notkey", - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"default-notkey\", variation=\"SdkDefault\"\n", }, { @@ -1064,7 +1064,7 @@ func TestIntVariation(t *testing.T) { }, nil), }, want: 125, - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"125\", variation=\"SdkDefault\"\n", }, { @@ -1424,11 +1424,12 @@ func TestRawVariation(t *testing.T) { Value: true, VariationResult: model.VariationResult{ VariationType: flag.VariationSDKDefault, - Failed: true, + Failed: false, TrackEvents: true, + Reason: flag.ReasonDisabled, }, }, - wantErr: true, + wantErr: false, expectedLog: "^\\[" + testutils.RFC3339Regex + "\\] user=\"random-key\", flag=\"disable-flag\", value=\"true\", variation=\"SdkDefault\"\n", }, { @@ -1447,6 +1448,8 @@ func TestRawVariation(t *testing.T) { VariationType: flag.VariationSDKDefault, Failed: true, TrackEvents: true, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, }, }, wantErr: true, @@ -1466,6 +1469,8 @@ func TestRawVariation(t *testing.T) { VariationType: flag.VariationSDKDefault, Failed: true, TrackEvents: true, + Reason: flag.ReasonError, + ErrorCode: flag.ErrorCodeFlagNotFound, }, }, wantErr: true, @@ -1491,6 +1496,7 @@ func TestRawVariation(t *testing.T) { VariationType: "Default", Failed: false, TrackEvents: true, + Reason: flag.ReasonDefault, }, }, wantErr: false, @@ -1516,6 +1522,7 @@ func TestRawVariation(t *testing.T) { VariationType: "True", Failed: false, TrackEvents: true, + Reason: flag.ReasonSplit, }, }, wantErr: false, @@ -1541,6 +1548,7 @@ func TestRawVariation(t *testing.T) { VariationType: "False", Failed: false, TrackEvents: true, + Reason: flag.ReasonSplit, }, }, wantErr: false, @@ -1567,6 +1575,7 @@ func TestRawVariation(t *testing.T) { VariationType: "True", Failed: false, TrackEvents: false, + Reason: flag.ReasonSplit, }, }, wantErr: false,