Skip to content

feat(gofeatureflag): Support exporterMetadata in evaluation API #621

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

Merged
merged 6 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions providers/go-feature-flag/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
)
16 changes: 8 additions & 8 deletions providers/go-feature-flag/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ 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=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
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=
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 3 additions & 1 deletion providers/go-feature-flag/pkg/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
}

Expand Down
61 changes: 60 additions & 1 deletion providers/go-feature-flag/pkg/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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":"[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"}}`,
},
{
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":"[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"}}`,
},
{
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)
})
}
}
Loading