diff --git a/providers/go-feature-flag-in-process/pkg/provider_module_test.go b/providers/go-feature-flag-in-process/pkg/provider_module_test.go index 8d9156ca1..2657ecb07 100644 --- a/providers/go-feature-flag-in-process/pkg/provider_module_test.go +++ b/providers/go-feature-flag-in-process/pkg/provider_module_test.go @@ -151,7 +151,7 @@ func TestProvider_module_BooleanEvaluation(t *testing.T) { }) assert.NoError(t, err) - err = of.SetProvider(provider) + err = of.SetProviderAndWait(provider) assert.NoError(t, err) client := of.NewClient("test-app") value, err := client.BooleanValueDetails(context.TODO(), tt.args.flag, tt.args.defaultValue, tt.args.evalCtx) @@ -283,7 +283,7 @@ func TestProvider_module_StringEvaluation(t *testing.T) { }) assert.NoError(t, err) - err = of.SetProvider(provider) + err = of.SetProviderAndWait(provider) assert.NoError(t, err) client := of.NewClient("test-app") value, err := client.StringValueDetails(context.TODO(), tt.args.flag, tt.args.defaultValue, tt.args.evalCtx) @@ -415,7 +415,7 @@ func TestProvider_module_FloatEvaluation(t *testing.T) { }) assert.NoError(t, err) - err = of.SetProvider(provider) + err = of.SetProviderAndWait(provider) assert.NoError(t, err) client := of.NewClient("test-app") value, err := client.FloatValueDetails(context.TODO(), tt.args.flag, tt.args.defaultValue, tt.args.evalCtx) @@ -547,7 +547,7 @@ func TestProvider_module_IntEvaluation(t *testing.T) { }) assert.NoError(t, err) - err = of.SetProvider(provider) + err = of.SetProviderAndWait(provider) assert.NoError(t, err) client := of.NewClient("test-app") value, err := client.IntValueDetails(context.TODO(), tt.args.flag, tt.args.defaultValue, tt.args.evalCtx) @@ -662,7 +662,7 @@ func TestProvider_module_ObjectEvaluation(t *testing.T) { }) assert.NoError(t, err) - err = of.SetProvider(provider) + err = of.SetProviderAndWait(provider) assert.NoError(t, err) client := of.NewClient("test-app") value, err := client.ObjectValueDetails(context.TODO(), tt.args.flag, tt.args.defaultValue, tt.args.evalCtx) diff --git a/providers/go-feature-flag/go.mod b/providers/go-feature-flag/go.mod index 9433d89ae..682bc3575 100644 --- a/providers/go-feature-flag/go.mod +++ b/providers/go-feature-flag/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.5 require ( github.com/bluele/gcache v0.0.2 github.com/open-feature/go-sdk v1.11.0 - github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.4 + github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 github.com/stretchr/testify v1.9.0 ) @@ -16,8 +16,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/providers/go-feature-flag/go.sum b/providers/go-feature-flag/go.sum index 895d56200..010cab636 100644 --- a/providers/go-feature-flag/go.sum +++ b/providers/go-feature-flag/go.sum @@ -14,10 +14,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw= -github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI= -github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.4 h1:BElq1EOES8DfLjW6UIFMNVG7w9MzoeC7JpD/1rXouhk= -github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.4/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE= +github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= +github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= +github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 h1:ZdqlGnNwhWf3luhBQlIpbglvcCzjkcuEgOEhYhr5Emc= +github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -25,10 +25,10 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go b/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go new file mode 100644 index 000000000..71462a8f0 --- /dev/null +++ b/providers/go-feature-flag/pkg/hook/evaluation_enrichment_hook.go @@ -0,0 +1,45 @@ +package hook + +import ( + "context" + "github.com/open-feature/go-sdk/openfeature" +) + +func NewEvaluationEnrichmentHook(exporterMetadata map[string]interface{}) openfeature.Hook { + return &evaluationEnrichmentHook{exporterMetadata: exporterMetadata} +} + +type evaluationEnrichmentHook struct { + exporterMetadata map[string]interface{} +} + +func (d *evaluationEnrichmentHook) After(_ context.Context, _ openfeature.HookContext, + _ openfeature.InterfaceEvaluationDetails, _ openfeature.HookHints) error { + // Do nothing, needed to satisfy the interface + return nil +} + +func (d *evaluationEnrichmentHook) Error(_ context.Context, _ openfeature.HookContext, + _ error, _ openfeature.HookHints) { + // Do nothing, needed to satisfy the interface +} + +func (d *evaluationEnrichmentHook) Before(_ context.Context, hookCtx openfeature.HookContext, _ openfeature.HookHints) (*openfeature.EvaluationContext, error) { + attributes := hookCtx.EvaluationContext().Attributes() + if goffSpecific, ok := attributes["gofeatureflag"]; ok { + switch typed := goffSpecific.(type) { + case map[string]interface{}: + typed["exporterMetadata"] = d.exporterMetadata + default: + attributes["gofeatureflag"] = map[string]interface{}{"exporterMetadata": d.exporterMetadata} + } + } else { + attributes["gofeatureflag"] = map[string]interface{}{"exporterMetadata": d.exporterMetadata} + } + newCtx := openfeature.NewEvaluationContext(hookCtx.EvaluationContext().TargetingKey(), attributes) + return &newCtx, nil +} + +func (d *evaluationEnrichmentHook) Finally(context.Context, openfeature.HookContext, openfeature.HookHints) { + // Do nothing, needed to satisfy the interface +} diff --git a/providers/go-feature-flag/pkg/provider.go b/providers/go-feature-flag/pkg/provider.go index 2d9123405..eb49c8cd3 100644 --- a/providers/go-feature-flag/pkg/provider.go +++ b/providers/go-feature-flag/pkg/provider.go @@ -79,6 +79,7 @@ func NewProviderWithContext(ctx context.Context, options ProviderOptions) (*Prov options: options, goffAPI: goffAPI, events: make(chan of.Event, 5), + hooks: []of.Hook{}, }, nil } @@ -184,9 +185,10 @@ func (p *Provider) Hooks() []of.Hook { // Init holds initialization logic of the provider func (p *Provider) Init(_ of.EvaluationContext) error { + p.hooks = append(p.hooks, hook.NewEvaluationEnrichmentHook(p.options.ExporterMetadata)) if !p.options.DisableDataCollector { dataCollectorHook := hook.NewDataCollectorHook(&p.dataCollectorManager) - p.hooks = []of.Hook{dataCollectorHook} + p.hooks = append(p.hooks, dataCollectorHook) p.dataCollectorManager.Start() } diff --git a/providers/go-feature-flag/pkg/provider_test.go b/providers/go-feature-flag/pkg/provider_test.go index 4b82d6279..a2a17474b 100644 --- a/providers/go-feature-flag/pkg/provider_test.go +++ b/providers/go-feature-flag/pkg/provider_test.go @@ -39,12 +39,20 @@ type mockClient struct { collectorCallCount int flagChangeCallCount int collectorRequests []string + requestBodies []string } func (m *mockClient) roundTripFunc(req *http.Request) *http.Response { + //read req body and store it + var bodyBytes []byte + if req.Body != nil { + bodyBytes, _ = io.ReadAll(req.Body) + req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + m.requestBodies = append(m.requestBodies, string(bodyBytes)) + } + if req.URL.Path == "/v1/data/collector" { m.collectorCallCount++ - bodyBytes, _ := io.ReadAll(req.Body) m.collectorRequests = append(m.collectorRequests, string(bodyBytes)) return &http.Response{ StatusCode: http.StatusOK, @@ -1082,5 +1090,56 @@ func TestProvider_FlagChangePolling(t *testing.T) { require.NoError(t, err) assert.Equal(t, of.TargetingMatchReason, details.Reason) }) +} + +func TestProvider_EvaluationEnrichmentHook(t *testing.T) { + tests := []struct { + name string + want string + evalCtx of.EvaluationContext + exporterMetadata map[string]interface{} + }{ + { + name: "should add the metadata to the evaluation context", + exporterMetadata: map[string]interface{}{"toto": 123, "tata": "titi"}, + evalCtx: defaultEvaluationCtx(), + want: `{"context":{"admin":true,"age":30,"anonymous":false,"company_info":{"name":"my_company","size":120},"email":"john.doe@gofeatureflag.org","firstname":"john","gofeatureflag":{"exporterMetadata":{"openfeature":true,"provider":"go","tata":"titi","toto":123}},"labels":["pro","beta"],"lastname":"doe","professional":true,"rate":3.14,"targetingKey":"d45e303a-38c2-11ed-a261-0242ac120002"}}`, + }, + { + name: "should have the default metadata if not provided", + exporterMetadata: nil, + evalCtx: defaultEvaluationCtx(), + want: `{"context":{"admin":true,"age":30,"anonymous":false,"company_info":{"name":"my_company","size":120},"email":"john.doe@gofeatureflag.org","firstname":"john","gofeatureflag":{"exporterMetadata":{"openfeature":true,"provider":"go"}},"labels":["pro","beta"],"lastname":"doe","professional":true,"rate":3.14,"targetingKey":"d45e303a-38c2-11ed-a261-0242ac120002"}}`, + }, + { + name: "should not remove other gofeatureflag specific metadata", + exporterMetadata: map[string]interface{}{"toto": 123, "tata": "titi"}, + evalCtx: of.NewEvaluationContext("d45e303a-38c2-11ed-a261-0242ac120002", map[string]interface{}{"age": 30, "gofeatureflag": map[string]interface{}{"flags": []string{"flag1", "flag2"}}}), + want: `{"context":{"age":30,"gofeatureflag":{"flags":["flag1","flag2"], "exporterMetadata":{"openfeature":true,"provider":"go","tata":"titi","toto":123}}, "targetingKey":"d45e303a-38c2-11ed-a261-0242ac120002"}}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cli := mockClient{} + options := gofeatureflag.ProviderOptions{ + Endpoint: "https://gofeatureflag.org/", + HTTPClient: NewMockClient(cli.roundTripFunc), + ExporterMetadata: tt.exporterMetadata, + } + provider, err := gofeatureflag.NewProvider(options) + defer provider.Shutdown() + assert.NoError(t, err) + err = of.SetProviderAndWait(provider) + assert.NoError(t, err) + client := of.NewClient("test-app") + _, err = client.BooleanValueDetails(context.TODO(), "bool_targeting_match", false, tt.evalCtx) + assert.NoError(t, err) + + want := tt.want + got := cli.requestBodies[0] + assert.JSONEq(t, want, got) + }) + } }