From f2b732fb06eaa76c3ba65f8129ad450b5003e4c7 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 23 Jan 2025 10:59:54 +0100 Subject: [PATCH] feat: Support Exporter Metadata (#614) Signed-off-by: Thomas Poignant --- .../go-feature-flag/pkg/controller/goff_api.go | 5 ++++- .../pkg/controller/goff_api_test.go | 14 ++++++++------ .../pkg/model/data_collector_request.go | 4 ++-- providers/go-feature-flag/pkg/provider.go | 15 ++++++++++++--- .../go-feature-flag/pkg/provider_options.go | 7 +++++++ providers/go-feature-flag/pkg/provider_test.go | 17 +++++++++++++++++ 6 files changed, 50 insertions(+), 12 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..3a9057f51 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 + // 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 { @@ -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.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 21f578187..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,8 +28,9 @@ func Test_CollectDataAPI(t *testing.T) { name: "Valid api call", wantErr: assert.NoError, options: controller.GoFeatureFlagApiOptions{ - Endpoint: "http://localhost:1031", - APIKey: "", + Endpoint: "http://localhost:1031", + APIKey: "", + ExporterMetadata: map[string]interface{}{"openfeature": true, "provider": "go"}, }, events: []model.FeatureEvent{ { @@ -68,14 +69,15 @@ 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", wantErr: assert.NoError, options: controller.GoFeatureFlagApiOptions{ - Endpoint: "http://localhost:1031", - APIKey: "my-key", + Endpoint: "http://localhost:1031", + APIKey: "my-key", + ExporterMetadata: 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..2d9123405 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.ExporterMetadata == nil { + options.ExporterMetadata = make(map[string]interface{}) + } + options.ExporterMetadata["provider"] = "go" + options.ExporterMetadata["openfeature"] = true + goffAPI := controller.NewGoFeatureFlagAPI(controller.GoFeatureFlagApiOptions{ - Endpoint: options.Endpoint, - HTTPClient: options.HTTPClient, - APIKey: options.APIKey, + 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 871568819..e67380e76 100644 --- a/providers/go-feature-flag/pkg/provider_options.go +++ b/providers/go-feature-flag/pkg/provider_options.go @@ -62,6 +62,13 @@ type ProviderOptions struct { // Use -1 if you want to deactivate polling. // default: 120000ms FlagChangePollingInterval time.Duration + + // ExporterMetadata (optional) is the metadata we send to the GO Feature Flag relay proxy when we report the + // evaluation data usage. + // + // ‼️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 aa832241a..4b82d6279 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, } @@ -982,6 +987,7 @@ func TestProvider_DataCollectorHook(t *testing.T) { DisableCache: false, DataFlushInterval: 100 * time.Millisecond, DisableDataCollector: false, + ExporterMetadata: map[string]interface{}{"toto": 123, "tata": "titi"}, } provider, err := gofeatureflag.NewProvider(options) defer provider.Shutdown() @@ -1003,6 +1009,17 @@ 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.NoError(t, err) + 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) {