diff --git a/README.md b/README.md index 705a20e8147..6b5fd6288b3 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ ffclient.Init(ffclient.Config{ }, }, StartWithRetrieverError: false, + Environment: os.Getenv("MYAPP_ENV"), }) ``` ### Configuration fields @@ -126,6 +127,7 @@ ffclient.Init(ffclient.Config{ |---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Retriever` | The configuration retriever you want to use to get your flag file.
*See [Store your flag file](https://thomaspoignant.github.io/go-feature-flag/latest/flag_file/) for the configuration details*. | | `Context` | *(optional)*
The context used by the retriever.
Default: `context.Background()` | +| `Environment` | *(optional)*
The environment the app is running under, can be checked in feature flag rules.
Default: `""` | | `DataExporter` | *(optional)*
DataExporter defines how to export data on how your flags are used.
*see [export data section](https://thomaspoignant.github.io/go-feature-flag/latest/data_collection/) for more details*. | | `FileFormat` | *(optional)*
Format of your configuration file. Available formats are `yaml`, `toml` and `json`, if you omit the field it will try to unmarshal the file as a `yaml` file.
Default: `YAML` | | `Logger` | *(optional)*
Logger used to log what `go-feature-flag` is doing.
If no logger is provided the module will not log anything.
Default: No log | diff --git a/config.go b/config.go index 70f0085fbc3..d4e537265da 100644 --- a/config.go +++ b/config.go @@ -28,6 +28,10 @@ type Config struct { // Default: context.Background() Context context.Context + // Environment (optional), can be checked in feature flag rules + // Default: "" + Environment string + // Retriever is the component in charge to retrieve your flag file Retriever Retriever diff --git a/docs/configuration.md b/docs/configuration.md index 588ebfd8c37..9e4d9fd21a2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,6 +10,7 @@ During the initialization you must give a [`ffclient.Config{}`](https://pkg.go.d |---|---| |`Retriever` | The configuration retriever you want to use to get your flag file
*See [Store your flag file](flag_file/index.md) for the configuration details*.| |`Context` | *(optional)*
The context used by the retriever.
Default: `context.Background()`| +|`Environment` | *(optional)*
The environment the app is running under, can be checked in feature flag rules.
Default: `""`| |`DataExporter` | *(optional)*
DataExporter defines how to export data on how your flags are used.
*see [export data section](data_collection/index.md) for more details*.| |`FileFormat`| *(optional)*
Format of your configuration file. Available formats are `yaml`, `toml` and `json`, if you omit the field it will try to unmarshal the file as a `yaml` file.
Default: `YAML`| |`Logger` | *(optional)*
Logger used to log what `go-feature-flag` is doing.
If no logger is provided the module will not log anything.
Default: No log| @@ -24,6 +25,7 @@ ffclient.Init(ffclient.Config{ PollingInterval: 3 * time.Second, Logger: log.New(file, "/tmp/log", 0), Context: context.Background(), + Environment: os.Getenv("MYAPP_ENV"), Retriever: &ffclient.FileRetriever{Path: "testdata/flag-config.yaml"}, FileFormat: "yaml", Notifiers: []ffclient.NotifierConfig{ diff --git a/internal/flag/flag.go b/internal/flag/flag.go index 10f077ec71d..74974d60428 100644 --- a/internal/flag/flag.go +++ b/internal/flag/flag.go @@ -6,8 +6,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 or not. - Value(flagName string, user ffuser.User) (interface{}, string) + // if the flag apply to the user and environment or not. + Value(flagName string, user ffuser.User, environment string) (interface{}, string) // String display correctly a flag with the right formatting String() string diff --git a/internal/flagv1/flag_data.go b/internal/flagv1/flag_data.go index 1d295e6786c..b72eb2edf27 100644 --- a/internal/flagv1/flag_data.go +++ b/internal/flagv1/flag_data.go @@ -56,14 +56,14 @@ 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) (interface{}, string) { +func (f *FlagData) Value(flagName string, user ffuser.User, environment string) (interface{}, string) { 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 } - if f.evaluateRule(user) { + if f.evaluateRule(user, environment) { if f.isInPercentage(flagName, user) { // Rule applied and user in the cohort. return f.getTrue(), VariationTrue @@ -102,7 +102,7 @@ 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) bool { +func (f *FlagData) evaluateRule(user ffuser.User, environment string) bool { // Flag disable we cannot apply it. if f.GetDisable() { return false @@ -114,7 +114,11 @@ func (f *FlagData) evaluateRule(user ffuser.User) bool { } // Evaluate the rule on the user. - return parser.Evaluate(f.getRule(), utils.UserToMap(user)) + userMap := utils.UserToMap(user) + if environment != "" { + userMap["env"] = environment + } + return parser.Evaluate(f.getRule(), userMap) } // string display correctly a flag diff --git a/internal/flagv1/flag_priv_test.go b/internal/flagv1/flag_priv_test.go index 2c9bf9d56e0..c9fcf626c11 100644 --- a/internal/flagv1/flag_priv_test.go +++ b/internal/flagv1/flag_priv_test.go @@ -20,6 +20,7 @@ func TestFlag_evaluateRule(t *testing.T) { } type args struct { user ffuser.User + env string } tests := []struct { name string @@ -77,6 +78,28 @@ func TestFlag_evaluateRule(t *testing.T) { }, want: false, }, + { + name: "Rolled out to environment", + fields: fields{ + Rule: "env == \"staging\"", + }, + args: args{ + user: ffuser.NewAnonymousUser(""), + env: "staging", + }, + want: true, + }, + { + name: "Not rolled out to environment", + fields: fields{ + Rule: "env != \"production\"", + }, + args: args{ + user: ffuser.NewAnonymousUser(""), + env: "production", + }, + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -88,7 +111,7 @@ func TestFlag_evaluateRule(t *testing.T) { False: testconvert.Interface(tt.fields.False), } - got := f.evaluateRule(tt.args.user) + got := f.evaluateRule(tt.args.user, tt.args.env) assert.Equal(t, tt.want, got) }) } diff --git a/internal/flagv1/flag_pub_test.go b/internal/flagv1/flag_pub_test.go index 8ec9fb1785e..ec6cb38a38c 100644 --- a/internal/flagv1/flag_pub_test.go +++ b/internal/flagv1/flag_pub_test.go @@ -337,7 +337,7 @@ func TestFlag_value(t *testing.T) { Rollout: &tt.fields.Rollout, } - got, variationType := f.Value(tt.args.flagName, tt.args.user) + got, variationType := f.Value(tt.args.flagName, tt.args.user, "") assert.Equal(t, tt.want.value, got) assert.Equal(t, tt.want.variationType, variationType) }) @@ -362,15 +362,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, "") assert.Equal(t, f.GetVariationValue(flagv1.VariationFalse), v) time.Sleep(1 * time.Second) - v2, _ := f.Value(flagName, user) + v2, _ := f.Value(flagName, user, "") assert.Equal(t, f.GetVariationValue(flagv1.VariationFalse), v2) time.Sleep(1 * time.Second) - v3, _ := f.Value(flagName, user) + v3, _ := f.Value(flagName, user, "") assert.Equal(t, f.GetVariationValue(flagv1.VariationTrue), v3) } @@ -447,43 +447,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, "") assert.Equal(t, f.GetVariationValue(flagv1.VariationFalse), v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user) + v, _ = f.Value(flagName, user, "") 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, "") assert.Equal(t, "Default2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user) + v, _ = f.Value(flagName, user, "") assert.Equal(t, "True2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user) + v, _ = f.Value(flagName, user, "") assert.Equal(t, "Default2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user) + v, _ = f.Value(flagName, user, "") assert.Equal(t, "Default2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user) + v, _ = f.Value(flagName, user, "") assert.Equal(t, "True2", v) time.Sleep(1 * time.Second) - v, _ = f.Value(flagName, user) + v, _ = f.Value(flagName, user, "") assert.Equal(t, "Default2", v) } diff --git a/variation.go b/variation.go index d0af5d2f003..5ac4bdced26 100644 --- a/variation.go +++ b/variation.go @@ -153,7 +153,7 @@ func (g *GoFeatureFlag) AllFlagsState(user ffuser.User) flagstate.AllFlags { allFlags := flagstate.NewAllFlags() for key, currentFlag := range flags { - flagValue, varType := currentFlag.Value(key, user) + flagValue, varType := currentFlag.Value(key, user, g.config.Environment) switch v := flagValue; v.(type) { case int, float64, bool, string, []interface{}, map[string]interface{}: allFlags.AddFlag(key, flagstate.NewFlagState(currentFlag.GetTrackEvents(), v, varType, false)) @@ -191,7 +191,7 @@ func (g *GoFeatureFlag) boolVariation(flagKey string, user ffuser.User, sdkDefau }, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res, ok := flagValue.(bool) if !ok { return model.BoolVarResult{ @@ -221,7 +221,7 @@ func (g *GoFeatureFlag) intVariation(flagKey string, user ffuser.User, sdkDefaul }, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res, ok := flagValue.(int) if !ok { // if this is a float64 we convert it to int @@ -259,7 +259,7 @@ func (g *GoFeatureFlag) float64Variation(flagKey string, user ffuser.User, sdkDe }, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res, ok := flagValue.(float64) if !ok { return model.Float64VarResult{ @@ -289,7 +289,7 @@ func (g *GoFeatureFlag) stringVariation(flagKey string, user ffuser.User, sdkDef }, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res, ok := flagValue.(string) if !ok { return model.StringVarResult{ @@ -319,7 +319,7 @@ func (g *GoFeatureFlag) jsonArrayVariation(flagKey string, user ffuser.User, sdk }, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res, ok := flagValue.([]interface{}) if !ok { return model.JSONArrayVarResult{ @@ -349,7 +349,7 @@ func (g *GoFeatureFlag) jsonVariation(flagKey string, user ffuser.User, sdkDefau }, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res, ok := flagValue.(map[string]interface{}) if !ok { return model.JSONVarResult{ @@ -399,7 +399,7 @@ func (g *GoFeatureFlag) RawVariation(flagKey string, user ffuser.User, sdkDefaul return res, err } - flagValue, variationType := f.Value(flagKey, user) + flagValue, variationType := f.Value(flagKey, user, g.config.Environment) res := model.RawVarResult{ Value: flagValue, VariationResult: computeVariationResult(f, variationType, false),