From 24277c34cae16290268b0f504249f9b877c42b5c Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Mon, 13 Jan 2025 17:48:18 +0100 Subject: [PATCH 1/3] feat: Support Exporter Metadata GO Feature Flag exporter is now supporting adding extra params as metadata. Those params are static informations provided during the initialisation to be added in the meta field when calling the exporter. In the GO Feature Flag exporter we will have those information available in the exporter. Signed-off-by: Thomas Poignant --- .../pkg/controller/goff_api.go | 5 +++- .../pkg/controller/goff_api_test.go | 6 +++-- .../pkg/model/data_collector_request.go | 4 +-- providers/go-feature-flag/pkg/provider.go | 9 +++++++ .../go-feature-flag/pkg/provider_options.go | 4 +++ .../go-feature-flag/pkg/provider_test.go | 26 +++++++++++++++---- 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/providers/go-feature-flag/pkg/controller/goff_api.go b/providers/go-feature-flag/pkg/controller/goff_api.go index cf900a95d..772d4d983 100644 --- a/providers/go-feature-flag/pkg/controller/goff_api.go +++ b/providers/go-feature-flag/pkg/controller/goff_api.go @@ -22,6 +22,8 @@ type GoFeatureFlagApiOptions struct { // (This feature is available only if you are using GO Feature Flag relay proxy v1.7.0 or above) // Default: null APIKey string + // Metadata (optional) If we set metadata, it will be sent with every data collection requests along with the events. + Metadata map[string]interface{} } type GoFeatureFlagAPI struct { @@ -39,9 +41,10 @@ func NewGoFeatureFlagAPI(options GoFeatureFlagApiOptions) GoFeatureFlagAPI { func (g *GoFeatureFlagAPI) CollectData(events []model.FeatureEvent) error { u, _ := url.Parse(g.options.Endpoint) u.Path = path.Join(u.Path, "v1", "data", "collector") + reqBody := model.DataCollectorRequest{ Events: events, - Meta: map[string]string{"provider": "go", "openfeature": "true"}, + Meta: g.options.Metadata, } jsonData, err := json.Marshal(reqBody) diff --git a/providers/go-feature-flag/pkg/controller/goff_api_test.go b/providers/go-feature-flag/pkg/controller/goff_api_test.go index 21f578187..f28d98049 100644 --- a/providers/go-feature-flag/pkg/controller/goff_api_test.go +++ b/providers/go-feature-flag/pkg/controller/goff_api_test.go @@ -30,6 +30,7 @@ func Test_CollectDataAPI(t *testing.T) { options: controller.GoFeatureFlagApiOptions{ Endpoint: "http://localhost:1031", APIKey: "", + Metadata: map[string]interface{}{"openfeature": true, "provider": "go"}, }, events: []model.FeatureEvent{ { @@ -68,7 +69,7 @@ func Test_CollectDataAPI(t *testing.T) { headers.Set(controller.ContentTypeHeader, controller.ApplicationJson) return headers }(), - wantReqBody: "{\"events\":[{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"ABCD\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"},{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"EFGH\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"}],\"meta\":{\"openfeature\":\"true\",\"provider\":\"go\"}}", + wantReqBody: "{\"events\":[{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"ABCD\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"},{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"EFGH\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"}],\"meta\":{\"openfeature\":true,\"provider\":\"go\"}}", }, { name: "Valid api call with API Key", @@ -76,6 +77,7 @@ func Test_CollectDataAPI(t *testing.T) { options: controller.GoFeatureFlagApiOptions{ Endpoint: "http://localhost:1031", APIKey: "my-key", + Metadata: map[string]interface{}{"openfeature": true, "provider": "go"}, }, events: []model.FeatureEvent{ { @@ -115,7 +117,7 @@ func Test_CollectDataAPI(t *testing.T) { headers.Set(controller.AuthorizationHeader, controller.BearerPrefix+"my-key") return headers }(), - wantReqBody: "{\"events\":[{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"ABCD\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"},{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"EFGH\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"}],\"meta\":{\"openfeature\":\"true\",\"provider\":\"go\"}}", + wantReqBody: "{\"events\":[{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"ABCD\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"},{\"kind\":\"feature\",\"contextKind\":\"user\",\"userKey\":\"EFGH\",\"creationDate\":1722266324,\"key\":\"random-key\",\"variation\":\"variationA\",\"value\":\"YO\",\"default\":false,\"version\":\"\",\"source\":\"SERVER\"}],\"meta\":{\"openfeature\":true,\"provider\":\"go\"}}", }, { name: "Request failed", diff --git a/providers/go-feature-flag/pkg/model/data_collector_request.go b/providers/go-feature-flag/pkg/model/data_collector_request.go index d49fef9ea..985d80275 100644 --- a/providers/go-feature-flag/pkg/model/data_collector_request.go +++ b/providers/go-feature-flag/pkg/model/data_collector_request.go @@ -1,6 +1,6 @@ package model type DataCollectorRequest struct { - Events []FeatureEvent `json:"events"` - Meta map[string]string `json:"meta"` + Events []FeatureEvent `json:"events"` + Meta map[string]interface{} `json:"meta"` } diff --git a/providers/go-feature-flag/pkg/provider.go b/providers/go-feature-flag/pkg/provider.go index 1ecacf9b0..3f8f78369 100644 --- a/providers/go-feature-flag/pkg/provider.go +++ b/providers/go-feature-flag/pkg/provider.go @@ -53,10 +53,19 @@ func NewProviderWithContext(ctx context.Context, options ProviderOptions) (*Prov })) ofrepProvider := ofrep.NewProvider(options.Endpoint, ofrepOptions...) cacheCtrl := controller.NewCache(options.FlagCacheSize, options.FlagCacheTTL, options.DisableCache) + + // Adding metadata to the GO Feature Flag provider to be sent to the exporter + if options.GOFeatureFlagMetadata == nil { + options.GOFeatureFlagMetadata = make(map[string]interface{}) + } + options.GOFeatureFlagMetadata["provider"] = "go" + options.GOFeatureFlagMetadata["openfeature"] = true + goffAPI := controller.NewGoFeatureFlagAPI(controller.GoFeatureFlagApiOptions{ Endpoint: options.Endpoint, HTTPClient: options.HTTPClient, APIKey: options.APIKey, + Metadata: options.GOFeatureFlagMetadata, }) dataCollectorManager := controller.NewDataCollectorManager( goffAPI, diff --git a/providers/go-feature-flag/pkg/provider_options.go b/providers/go-feature-flag/pkg/provider_options.go index 871568819..9366f8abc 100644 --- a/providers/go-feature-flag/pkg/provider_options.go +++ b/providers/go-feature-flag/pkg/provider_options.go @@ -62,6 +62,10 @@ type ProviderOptions struct { // Use -1 if you want to deactivate polling. // default: 120000ms FlagChangePollingInterval time.Duration + + // GOFeatureFlagMetadata (optional) is the metadata we send to the GO Feature Flag relay proxy when we report the + // evaluation data usage. + GOFeatureFlagMetadata map[string]interface{} } func (o *ProviderOptions) Validation() error { diff --git a/providers/go-feature-flag/pkg/provider_test.go b/providers/go-feature-flag/pkg/provider_test.go index aa832241a..1d633f738 100644 --- a/providers/go-feature-flag/pkg/provider_test.go +++ b/providers/go-feature-flag/pkg/provider_test.go @@ -3,8 +3,10 @@ package gofeatureflag_test import ( "bytes" "context" + "encoding/json" "fmt" gofeatureflag "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg" + "github.com/open-feature/go-sdk-contrib/providers/go-feature-flag/pkg/model" of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,11 +38,14 @@ type mockClient struct { callCount int collectorCallCount int flagChangeCallCount int + collectorRequests []string } func (m *mockClient) roundTripFunc(req *http.Request) *http.Response { 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, } @@ -977,11 +982,12 @@ func TestProvider_DataCollectorHook(t *testing.T) { t.Run("DataCollectorHook is called for success and call API", func(t *testing.T) { cli := mockClient{} options := gofeatureflag.ProviderOptions{ - Endpoint: "https://gofeatureflag.org/", - HTTPClient: NewMockClient(cli.roundTripFunc), - DisableCache: false, - DataFlushInterval: 100 * time.Millisecond, - DisableDataCollector: false, + Endpoint: "https://gofeatureflag.org/", + HTTPClient: NewMockClient(cli.roundTripFunc), + DisableCache: false, + DataFlushInterval: 100 * time.Millisecond, + DisableDataCollector: false, + GOFeatureFlagMetadata: map[string]interface{}{"toto": 123, "tata": "titi"}, } provider, err := gofeatureflag.NewProvider(options) defer provider.Shutdown() @@ -1003,6 +1009,16 @@ func TestProvider_DataCollectorHook(t *testing.T) { time.Sleep(500 * time.Millisecond) assert.Equal(t, 1, cli.callCount) assert.Equal(t, 1, cli.collectorCallCount) + + // convert cli.collectorRequests[0] to DataCollectorRequest + var dataCollectorRequest model.DataCollectorRequest + err = json.Unmarshal([]byte(cli.collectorRequests[0]), &dataCollectorRequest) + assert.Equal(t, map[string]interface{}{ + "openfeature": true, + "provider": "go", + "tata": "titi", + "toto": float64(123), + }, dataCollectorRequest.Meta) }) t.Run("DataCollectorHook is called for errors and call API", func(t *testing.T) { From 2836828bd5730906e2a2905749243c6bf4a63ee3 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Tue, 14 Jan 2025 17:23:09 +0100 Subject: [PATCH 2/3] rename field to ExporterMetadata Signed-off-by: Thomas Poignant --- .../go-feature-flag/pkg/controller/goff_api.go | 6 +++--- .../pkg/controller/goff_api_test.go | 12 ++++++------ providers/go-feature-flag/pkg/provider.go | 16 ++++++++-------- .../go-feature-flag/pkg/provider_options.go | 7 +++++-- providers/go-feature-flag/pkg/provider_test.go | 12 ++++++------ 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/providers/go-feature-flag/pkg/controller/goff_api.go b/providers/go-feature-flag/pkg/controller/goff_api.go index 772d4d983..3a9057f51 100644 --- a/providers/go-feature-flag/pkg/controller/goff_api.go +++ b/providers/go-feature-flag/pkg/controller/goff_api.go @@ -22,8 +22,8 @@ type GoFeatureFlagApiOptions struct { // (This feature is available only if you are using GO Feature Flag relay proxy v1.7.0 or above) // Default: null APIKey string - // Metadata (optional) If we set metadata, it will be sent with every data collection requests along with the events. - Metadata map[string]interface{} + // ExporterMetadata (optional) If we set metadata, it will be sent with every data collection requests along with the events. + ExporterMetadata map[string]interface{} } type GoFeatureFlagAPI struct { @@ -44,7 +44,7 @@ func (g *GoFeatureFlagAPI) CollectData(events []model.FeatureEvent) error { reqBody := model.DataCollectorRequest{ Events: events, - Meta: g.options.Metadata, + Meta: g.options.ExporterMetadata, } jsonData, err := json.Marshal(reqBody) diff --git a/providers/go-feature-flag/pkg/controller/goff_api_test.go b/providers/go-feature-flag/pkg/controller/goff_api_test.go index f28d98049..74ee3de17 100644 --- a/providers/go-feature-flag/pkg/controller/goff_api_test.go +++ b/providers/go-feature-flag/pkg/controller/goff_api_test.go @@ -28,9 +28,9 @@ func Test_CollectDataAPI(t *testing.T) { name: "Valid api call", wantErr: assert.NoError, options: controller.GoFeatureFlagApiOptions{ - Endpoint: "http://localhost:1031", - APIKey: "", - Metadata: map[string]interface{}{"openfeature": true, "provider": "go"}, + Endpoint: "http://localhost:1031", + APIKey: "", + ExporterMetadata: map[string]interface{}{"openfeature": true, "provider": "go"}, }, events: []model.FeatureEvent{ { @@ -75,9 +75,9 @@ func Test_CollectDataAPI(t *testing.T) { name: "Valid api call with API Key", wantErr: assert.NoError, options: controller.GoFeatureFlagApiOptions{ - Endpoint: "http://localhost:1031", - APIKey: "my-key", - Metadata: map[string]interface{}{"openfeature": true, "provider": "go"}, + Endpoint: "http://localhost:1031", + APIKey: "my-key", + ExporterMetadata: map[string]interface{}{"openfeature": true, "provider": "go"}, }, events: []model.FeatureEvent{ { diff --git a/providers/go-feature-flag/pkg/provider.go b/providers/go-feature-flag/pkg/provider.go index 3f8f78369..2d9123405 100644 --- a/providers/go-feature-flag/pkg/provider.go +++ b/providers/go-feature-flag/pkg/provider.go @@ -55,17 +55,17 @@ func NewProviderWithContext(ctx context.Context, options ProviderOptions) (*Prov cacheCtrl := controller.NewCache(options.FlagCacheSize, options.FlagCacheTTL, options.DisableCache) // Adding metadata to the GO Feature Flag provider to be sent to the exporter - if options.GOFeatureFlagMetadata == nil { - options.GOFeatureFlagMetadata = make(map[string]interface{}) + if options.ExporterMetadata == nil { + options.ExporterMetadata = make(map[string]interface{}) } - options.GOFeatureFlagMetadata["provider"] = "go" - options.GOFeatureFlagMetadata["openfeature"] = true + options.ExporterMetadata["provider"] = "go" + options.ExporterMetadata["openfeature"] = true goffAPI := controller.NewGoFeatureFlagAPI(controller.GoFeatureFlagApiOptions{ - Endpoint: options.Endpoint, - HTTPClient: options.HTTPClient, - APIKey: options.APIKey, - Metadata: options.GOFeatureFlagMetadata, + Endpoint: options.Endpoint, + HTTPClient: options.HTTPClient, + APIKey: options.APIKey, + ExporterMetadata: options.ExporterMetadata, }) dataCollectorManager := controller.NewDataCollectorManager( goffAPI, diff --git a/providers/go-feature-flag/pkg/provider_options.go b/providers/go-feature-flag/pkg/provider_options.go index 9366f8abc..e67380e76 100644 --- a/providers/go-feature-flag/pkg/provider_options.go +++ b/providers/go-feature-flag/pkg/provider_options.go @@ -63,9 +63,12 @@ type ProviderOptions struct { // default: 120000ms FlagChangePollingInterval time.Duration - // GOFeatureFlagMetadata (optional) is the metadata we send to the GO Feature Flag relay proxy when we report the + // ExporterMetadata (optional) is the metadata we send to the GO Feature Flag relay proxy when we report the // evaluation data usage. - GOFeatureFlagMetadata map[string]interface{} + // + // ‼️Important: If you are using a GO Feature Flag relay proxy before version v1.41.0, the information of this + // field will not be added to your feature events. + ExporterMetadata map[string]interface{} } func (o *ProviderOptions) Validation() error { diff --git a/providers/go-feature-flag/pkg/provider_test.go b/providers/go-feature-flag/pkg/provider_test.go index 1d633f738..9130527f7 100644 --- a/providers/go-feature-flag/pkg/provider_test.go +++ b/providers/go-feature-flag/pkg/provider_test.go @@ -982,12 +982,12 @@ func TestProvider_DataCollectorHook(t *testing.T) { t.Run("DataCollectorHook is called for success and call API", func(t *testing.T) { cli := mockClient{} options := gofeatureflag.ProviderOptions{ - Endpoint: "https://gofeatureflag.org/", - HTTPClient: NewMockClient(cli.roundTripFunc), - DisableCache: false, - DataFlushInterval: 100 * time.Millisecond, - DisableDataCollector: false, - GOFeatureFlagMetadata: map[string]interface{}{"toto": 123, "tata": "titi"}, + Endpoint: "https://gofeatureflag.org/", + HTTPClient: NewMockClient(cli.roundTripFunc), + DisableCache: false, + DataFlushInterval: 100 * time.Millisecond, + DisableDataCollector: false, + ExporterMetadata: map[string]interface{}{"toto": 123, "tata": "titi"}, } provider, err := gofeatureflag.NewProvider(options) defer provider.Shutdown() From bf122469043b4999908722c4312016e15adaf0bd Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 23 Jan 2025 10:36:41 +0100 Subject: [PATCH 3/3] check error in test Signed-off-by: Thomas Poignant --- providers/go-feature-flag/pkg/provider_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/providers/go-feature-flag/pkg/provider_test.go b/providers/go-feature-flag/pkg/provider_test.go index 9130527f7..4b82d6279 100644 --- a/providers/go-feature-flag/pkg/provider_test.go +++ b/providers/go-feature-flag/pkg/provider_test.go @@ -1013,6 +1013,7 @@ func TestProvider_DataCollectorHook(t *testing.T) { // convert cli.collectorRequests[0] to DataCollectorRequest var dataCollectorRequest model.DataCollectorRequest err = json.Unmarshal([]byte(cli.collectorRequests[0]), &dataCollectorRequest) + assert.NoError(t, err) assert.Equal(t, map[string]interface{}{ "openfeature": true, "provider": "go",