Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of evaluation context enrichment #1285

Merged
merged 10 commits into from
Nov 20, 2023
11 changes: 10 additions & 1 deletion cmd/relayproxy/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,18 @@ type Config struct {
// APIKeys list of API keys that authorized to use endpoints
APIKeys []string `mapstructure:"apiKeys" koanf:"apikeys"`

// StartAsAwsLambda (optional) if true the relay proxy will start ready to be launch as AWS Lambda
// StartAsAwsLambda (optional) if true, the relay proxy will start ready to be launched as AWS Lambda
StartAsAwsLambda bool `mapstructure:"startAsAwsLambda" koanf:"startasawslambda"`

// EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation.
// It is useful to add common attributes to all the evaluations, such as a server version, environment, ...
//
// All those fields will be included in the custom attributes of the evaluation context,
// if in the evaluation context you have a field with the same name,
// it will be overridden by the evaluationContextEnrichment.
// Default: nil
EvaluationContextEnrichment map[string]interface{} `mapstructure:"evaluationContextEnrichment" koanf:"evaluationcontextenrichment"` //nolint: lll

// ---- private fields

// apiKeySet is the internal representation of the list of api keys configured
Expand Down
21 changes: 11 additions & 10 deletions cmd/relayproxy/service/gofeatureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,17 @@
notif = append(notif, notifiers...)

f := ffclient.Config{
PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond,
Logger: zap.NewStdLog(logger),
Context: context.Background(),
Retriever: mainRetriever,
Retrievers: retrievers,
Notifiers: notif,
FileFormat: proxyConf.FileFormat,
DataExporter: exp,
StartWithRetrieverError: proxyConf.StartWithRetrieverError,
EnablePollingJitter: proxyConf.EnablePollingJitter,
PollingInterval: time.Duration(proxyConf.PollingInterval) * time.Millisecond,
Logger: zap.NewStdLog(logger),
Context: context.Background(),
Retriever: mainRetriever,
Retrievers: retrievers,
Notifiers: notif,
FileFormat: proxyConf.FileFormat,
DataExporter: exp,
StartWithRetrieverError: proxyConf.StartWithRetrieverError,
EnablePollingJitter: proxyConf.EnablePollingJitter,
EvaluationContextEnrichment: proxyConf.EvaluationContextEnrichment,

Check warning on line 88 in cmd/relayproxy/service/gofeatureflag.go

View check run for this annotation

Codecov / codecov/patch

cmd/relayproxy/service/gofeatureflag.go#L78-L88

Added lines #L78 - L88 were not covered by tests
}

return ffclient.New(f)
Expand Down
10 changes: 9 additions & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ type Config struct {
// No notification will be sent neither.
// Default: false
Offline bool

// EvaluationContextEnrichment (optional) will be merged with the evaluation context sent during the evaluation.
// It is useful to add common attributes to all the evaluation, such as a server version, environment, ...
//
// All those fields will be included in the custom attributes of the evaluation context,
// if in the evaluation context you have a field with the same name, it will override the common one.
// Default: nil
EvaluationContextEnrichment map[string]interface{}
}

// GetRetrievers returns a retriever.Retriever configure with the retriever available in the config.
Expand All @@ -78,7 +86,7 @@ func (c *Config) GetRetrievers() ([]retriever.Retriever, error) {
}

retrievers := make([]retriever.Retriever, 0)
// If we have both Retriever and Retrievers fields configured we are 1st looking at what is available
// If we have both Retriever and Retrievers fields configured, we are 1st looking at what is available
// in Retriever before looking at what is in Retrievers.
if c.Retriever != nil {
retrievers = append(retrievers, c.Retriever)
Expand Down
1 change: 1 addition & 0 deletions examples/retriever_file/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func main() {
user1 := ffcontext.
NewEvaluationContextBuilder("aea2fdc1-b9a0-417a-b707-0c9083de68e3").
AddCustom("anonymous", true).
AddCustom("environment", "dev").
Build()
user2 := ffcontext.NewEvaluationContext("332460b9-a8aa-4f7a-bc5d-9cc33632df9a")
user3 := ffcontext.NewEvaluationContextBuilder("785a14bf-d2c5-4caa-9c70-2bbc4e3732a5").
Expand Down
18 changes: 14 additions & 4 deletions internal/flag/context.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package flag

type Context 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
// EvaluationContextEnrichment will be merged with the evaluation context sent during the evaluation.
// It is useful to add common attributes to all the evaluation, such as a server version, environment, ...
//
// All those fields will be included in the custom attributes of the evaluation context,
// if in the evaluation context you have a field with the same name, it will override the common one.
// Default: nil
EvaluationContextEnrichment map[string]interface{}

// DefaultSdkValue is the default value of the SDK when calling the variation.
DefaultSdkValue interface{}
}

func (s *Context) AddIntoEvaluationContextEnrichment(key string, value interface{}) {
if s.EvaluationContextEnrichment == nil {
s.EvaluationContextEnrichment = make(map[string]interface{})
}
s.EvaluationContextEnrichment[key] = value
}
57 changes: 57 additions & 0 deletions internal/flag/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package flag_test

import (
"github.com/stretchr/testify/assert"
"github.com/thomaspoignant/go-feature-flag/internal/flag"
"testing"
)

func TestContext_AddIntoEvaluationContextEnrichment(t *testing.T) {
type args struct {
key string
value interface{}
}
tests := []struct {
name string
EvaluationContextEnrichment map[string]interface{}
args args
expected interface{}
}{
{
name: "Add a new key to a nil map",
EvaluationContextEnrichment: nil,
args: args{
key: "env",
value: "prod",
},
expected: "prod",
},
{
name: "Add a new key to an existing map",
EvaluationContextEnrichment: map[string]interface{}{"john": "doe"},
args: args{
key: "env",
value: "prod",
},
expected: "prod",
},
{
name: "Override an existing key",
EvaluationContextEnrichment: map[string]interface{}{"env": "dev"},
args: args{
key: "env",
value: "prod",
},
expected: "prod",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &flag.Context{
EvaluationContextEnrichment: tt.EvaluationContextEnrichment,
}
s.AddIntoEvaluationContextEnrichment(tt.args.key, tt.args.value)
assert.Equal(t, tt.expected, s.EvaluationContextEnrichment[tt.args.key])
})
}
}
5 changes: 3 additions & 2 deletions internal/flag/internal_flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package flag
import (
"fmt"
"github.com/thomaspoignant/go-feature-flag/ffcontext"
"maps"
"time"

"github.com/thomaspoignant/go-feature-flag/internal/internalerror"
Expand Down Expand Up @@ -63,8 +64,8 @@ func (f *InternalFlag) Value(
) (interface{}, ResolutionDetails) {
f.applyScheduledRolloutSteps()

if flagContext.Environment != "" {
evaluationCtx.AddCustomAttribute("env", flagContext.Environment)
if flagContext.EvaluationContextEnrichment != nil {
maps.Copy(evaluationCtx.GetCustom(), flagContext.EvaluationContextEnrichment)
}

if f.IsDisable() || f.isExperimentationOver() {
Expand Down
Loading