Skip to content

Commit ec4421e

Browse files
feat(gofeatureflag): Support exporterMetadata in evaluation API (#621)
Signed-off-by: Thomas Poignant <[email protected]>
1 parent 6b300ad commit ec4421e

File tree

6 files changed

+123
-18
lines changed

6 files changed

+123
-18
lines changed

providers/go-feature-flag-in-process/pkg/provider_module_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func TestProvider_module_BooleanEvaluation(t *testing.T) {
151151
})
152152
assert.NoError(t, err)
153153

154-
err = of.SetProvider(provider)
154+
err = of.SetProviderAndWait(provider)
155155
assert.NoError(t, err)
156156
client := of.NewClient("test-app")
157157
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) {
283283
})
284284
assert.NoError(t, err)
285285

286-
err = of.SetProvider(provider)
286+
err = of.SetProviderAndWait(provider)
287287
assert.NoError(t, err)
288288
client := of.NewClient("test-app")
289289
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) {
415415
})
416416
assert.NoError(t, err)
417417

418-
err = of.SetProvider(provider)
418+
err = of.SetProviderAndWait(provider)
419419
assert.NoError(t, err)
420420
client := of.NewClient("test-app")
421421
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) {
547547
})
548548
assert.NoError(t, err)
549549

550-
err = of.SetProvider(provider)
550+
err = of.SetProviderAndWait(provider)
551551
assert.NoError(t, err)
552552
client := of.NewClient("test-app")
553553
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) {
662662
})
663663
assert.NoError(t, err)
664664

665-
err = of.SetProvider(provider)
665+
err = of.SetProviderAndWait(provider)
666666
assert.NoError(t, err)
667667
client := of.NewClient("test-app")
668668
value, err := client.ObjectValueDetails(context.TODO(), tt.args.flag, tt.args.defaultValue, tt.args.evalCtx)

providers/go-feature-flag/go.mod

+2-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ toolchain go1.22.5
77
require (
88
github.com/bluele/gcache v0.0.2
99
github.com/open-feature/go-sdk v1.11.0
10-
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.4
10+
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5
1111
github.com/stretchr/testify v1.9.0
1212
)
1313

@@ -16,8 +16,7 @@ require (
1616
github.com/go-logr/logr v1.4.2 // indirect
1717
github.com/kr/pretty v0.3.1 // indirect
1818
github.com/pmezard/go-difflib v1.0.0 // indirect
19-
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
20-
golang.org/x/text v0.16.0 // indirect
19+
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
2120
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2221
gopkg.in/yaml.v3 v3.0.1 // indirect
2322
)

providers/go-feature-flag/go.sum

+8-8
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
1414
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
1515
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1616
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
17-
github.com/open-feature/go-sdk v1.11.0 h1:4cp9rXl16ZvlMCef7O+I3vQSXae8DzAF0SfV9mvYInw=
18-
github.com/open-feature/go-sdk v1.11.0/go.mod h1:+rkJhLBtYsJ5PZNddAgFILhRAAxwrJ32aU7UEUm4zQI=
19-
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.4 h1:BElq1EOES8DfLjW6UIFMNVG7w9MzoeC7JpD/1rXouhk=
20-
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.4/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE=
17+
github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo=
18+
github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg=
19+
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 h1:ZdqlGnNwhWf3luhBQlIpbglvcCzjkcuEgOEhYhr5Emc=
20+
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5/go.mod h1:jrD4UG3ZCzuwImKHlyuIN2iWeYjlOX5+zJ/sX45efuE=
2121
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
2222
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2323
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2424
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
2525
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
2626
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2727
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
28-
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
29-
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
30-
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
31-
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
28+
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
29+
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
30+
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
31+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
3232
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3333
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
3434
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package hook
2+
3+
import (
4+
"context"
5+
"github.com/open-feature/go-sdk/openfeature"
6+
)
7+
8+
func NewEvaluationEnrichmentHook(exporterMetadata map[string]interface{}) openfeature.Hook {
9+
return &evaluationEnrichmentHook{exporterMetadata: exporterMetadata}
10+
}
11+
12+
type evaluationEnrichmentHook struct {
13+
exporterMetadata map[string]interface{}
14+
}
15+
16+
func (d *evaluationEnrichmentHook) After(_ context.Context, _ openfeature.HookContext,
17+
_ openfeature.InterfaceEvaluationDetails, _ openfeature.HookHints) error {
18+
// Do nothing, needed to satisfy the interface
19+
return nil
20+
}
21+
22+
func (d *evaluationEnrichmentHook) Error(_ context.Context, _ openfeature.HookContext,
23+
_ error, _ openfeature.HookHints) {
24+
// Do nothing, needed to satisfy the interface
25+
}
26+
27+
func (d *evaluationEnrichmentHook) Before(_ context.Context, hookCtx openfeature.HookContext, _ openfeature.HookHints) (*openfeature.EvaluationContext, error) {
28+
attributes := hookCtx.EvaluationContext().Attributes()
29+
if goffSpecific, ok := attributes["gofeatureflag"]; ok {
30+
switch typed := goffSpecific.(type) {
31+
case map[string]interface{}:
32+
typed["exporterMetadata"] = d.exporterMetadata
33+
default:
34+
attributes["gofeatureflag"] = map[string]interface{}{"exporterMetadata": d.exporterMetadata}
35+
}
36+
} else {
37+
attributes["gofeatureflag"] = map[string]interface{}{"exporterMetadata": d.exporterMetadata}
38+
}
39+
newCtx := openfeature.NewEvaluationContext(hookCtx.EvaluationContext().TargetingKey(), attributes)
40+
return &newCtx, nil
41+
}
42+
43+
func (d *evaluationEnrichmentHook) Finally(context.Context, openfeature.HookContext, openfeature.HookHints) {
44+
// Do nothing, needed to satisfy the interface
45+
}

providers/go-feature-flag/pkg/provider.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func NewProviderWithContext(ctx context.Context, options ProviderOptions) (*Prov
7979
options: options,
8080
goffAPI: goffAPI,
8181
events: make(chan of.Event, 5),
82+
hooks: []of.Hook{},
8283
}, nil
8384
}
8485

@@ -184,9 +185,10 @@ func (p *Provider) Hooks() []of.Hook {
184185

185186
// Init holds initialization logic of the provider
186187
func (p *Provider) Init(_ of.EvaluationContext) error {
188+
p.hooks = append(p.hooks, hook.NewEvaluationEnrichmentHook(p.options.ExporterMetadata))
187189
if !p.options.DisableDataCollector {
188190
dataCollectorHook := hook.NewDataCollectorHook(&p.dataCollectorManager)
189-
p.hooks = []of.Hook{dataCollectorHook}
191+
p.hooks = append(p.hooks, dataCollectorHook)
190192
p.dataCollectorManager.Start()
191193
}
192194

providers/go-feature-flag/pkg/provider_test.go

+60-1
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,20 @@ type mockClient struct {
3939
collectorCallCount int
4040
flagChangeCallCount int
4141
collectorRequests []string
42+
requestBodies []string
4243
}
4344

4445
func (m *mockClient) roundTripFunc(req *http.Request) *http.Response {
46+
//read req body and store it
47+
var bodyBytes []byte
48+
if req.Body != nil {
49+
bodyBytes, _ = io.ReadAll(req.Body)
50+
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
51+
m.requestBodies = append(m.requestBodies, string(bodyBytes))
52+
}
53+
4554
if req.URL.Path == "/v1/data/collector" {
4655
m.collectorCallCount++
47-
bodyBytes, _ := io.ReadAll(req.Body)
4856
m.collectorRequests = append(m.collectorRequests, string(bodyBytes))
4957
return &http.Response{
5058
StatusCode: http.StatusOK,
@@ -1082,5 +1090,56 @@ func TestProvider_FlagChangePolling(t *testing.T) {
10821090
require.NoError(t, err)
10831091
assert.Equal(t, of.TargetingMatchReason, details.Reason)
10841092
})
1093+
}
1094+
1095+
func TestProvider_EvaluationEnrichmentHook(t *testing.T) {
1096+
tests := []struct {
1097+
name string
1098+
want string
1099+
evalCtx of.EvaluationContext
1100+
exporterMetadata map[string]interface{}
1101+
}{
1102+
{
1103+
name: "should add the metadata to the evaluation context",
1104+
exporterMetadata: map[string]interface{}{"toto": 123, "tata": "titi"},
1105+
evalCtx: defaultEvaluationCtx(),
1106+
want: `{"context":{"admin":true,"age":30,"anonymous":false,"company_info":{"name":"my_company","size":120},"email":"[email protected]","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"}}`,
1107+
},
1108+
{
1109+
name: "should have the default metadata if not provided",
1110+
exporterMetadata: nil,
1111+
evalCtx: defaultEvaluationCtx(),
1112+
want: `{"context":{"admin":true,"age":30,"anonymous":false,"company_info":{"name":"my_company","size":120},"email":"[email protected]","firstname":"john","gofeatureflag":{"exporterMetadata":{"openfeature":true,"provider":"go"}},"labels":["pro","beta"],"lastname":"doe","professional":true,"rate":3.14,"targetingKey":"d45e303a-38c2-11ed-a261-0242ac120002"}}`,
1113+
},
1114+
{
1115+
name: "should not remove other gofeatureflag specific metadata",
1116+
exporterMetadata: map[string]interface{}{"toto": 123, "tata": "titi"},
1117+
evalCtx: of.NewEvaluationContext("d45e303a-38c2-11ed-a261-0242ac120002", map[string]interface{}{"age": 30, "gofeatureflag": map[string]interface{}{"flags": []string{"flag1", "flag2"}}}),
1118+
want: `{"context":{"age":30,"gofeatureflag":{"flags":["flag1","flag2"], "exporterMetadata":{"openfeature":true,"provider":"go","tata":"titi","toto":123}}, "targetingKey":"d45e303a-38c2-11ed-a261-0242ac120002"}}`,
1119+
},
1120+
}
1121+
1122+
for _, tt := range tests {
1123+
t.Run(tt.name, func(t *testing.T) {
1124+
cli := mockClient{}
1125+
options := gofeatureflag.ProviderOptions{
1126+
Endpoint: "https://gofeatureflag.org/",
1127+
HTTPClient: NewMockClient(cli.roundTripFunc),
1128+
ExporterMetadata: tt.exporterMetadata,
1129+
}
1130+
provider, err := gofeatureflag.NewProvider(options)
1131+
defer provider.Shutdown()
1132+
assert.NoError(t, err)
1133+
err = of.SetProviderAndWait(provider)
1134+
assert.NoError(t, err)
1135+
client := of.NewClient("test-app")
10851136

1137+
_, err = client.BooleanValueDetails(context.TODO(), "bool_targeting_match", false, tt.evalCtx)
1138+
assert.NoError(t, err)
1139+
1140+
want := tt.want
1141+
got := cli.requestBodies[0]
1142+
assert.JSONEq(t, want, got)
1143+
})
1144+
}
10861145
}

0 commit comments

Comments
 (0)