diff --git a/.github/workflows/close-stale.yml b/.github/workflows/close-stale.yml index b159123450c..6fa4a0fad47 100644 --- a/.github/workflows/close-stale.yml +++ b/.github/workflows/close-stale.yml @@ -17,5 +17,5 @@ jobs: days-before-pr-stale: 45 days-before-issue-close: 10 days-before-pr-close: 10 - exempt-issue-labels: p1,p2,p3,bug,dependencies + exempt-issue-labels: p1,p2,p3,bug,dependencies,roadmap exempt-assignees: thomaspoignant diff --git a/cmd/relayproxy/api/server.go b/cmd/relayproxy/api/server.go index 65185470492..c9bda394a2e 100644 --- a/cmd/relayproxy/api/server.go +++ b/cmd/relayproxy/api/server.go @@ -90,7 +90,7 @@ func (s *Server) initRoutes() { cAllFlags := controller.NewAllFlags(s.services.GOFeatureFlagService, s.services.Metrics) cFlagEval := controller.NewFlagEval(s.services.GOFeatureFlagService, s.services.Metrics) cFlagEvalOFREP := ofrep.NewOFREPEvaluate(s.services.GOFeatureFlagService, s.services.Metrics) - cEvalDataCollector := controller.NewCollectEvalData(s.services.GOFeatureFlagService, s.services.Metrics) + cEvalDataCollector := controller.NewCollectEvalData(s.services.GOFeatureFlagService, s.services.Metrics, s.zapLog) cRetrieverRefresh := controller.NewForceFlagsRefresh(s.services.GOFeatureFlagService, s.services.Metrics) cFlagChangeAPI := controller.NewAPIFlagChange(s.services.GOFeatureFlagService, s.services.Metrics) diff --git a/cmd/relayproxy/controller/collect_eval_data.go b/cmd/relayproxy/controller/collect_eval_data.go index f43bacfb720..0225a3e8221 100644 --- a/cmd/relayproxy/controller/collect_eval_data.go +++ b/cmd/relayproxy/controller/collect_eval_data.go @@ -3,6 +3,7 @@ package controller import ( "fmt" "net/http" + "strconv" "github.com/labstack/echo/v4" ffclient "github.com/thomaspoignant/go-feature-flag" @@ -11,18 +12,21 @@ import ( "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.uber.org/zap" ) type collectEvalData struct { goFF *ffclient.GoFeatureFlag metrics metric.Metrics + logger *zap.Logger } // NewCollectEvalData initialize the controller for the /data/collector endpoint -func NewCollectEvalData(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics) Controller { +func NewCollectEvalData(goFF *ffclient.GoFeatureFlag, metrics metric.Metrics, logger *zap.Logger) Controller { return &collectEvalData{ goFF: goFF, metrics: metrics, + logger: logger, } } @@ -51,7 +55,6 @@ func (h *collectEvalData) Handler(c echo.Context) error { if reqBody == nil || reqBody.Events == nil { return echo.NewHTTPError(http.StatusBadRequest, "collectEvalData: invalid input data") } - tracer := otel.GetTracerProvider().Tracer(config.OtelTracerName) _, span := tracer.Start(c.Request().Context(), "collectEventData") defer span.End() @@ -60,6 +63,17 @@ func (h *collectEvalData) Handler(c echo.Context) error { if event.Source == "" { event.Source = "PROVIDER_CACHE" } + // force the creation date to be a unix timestamp + if event.CreationDate > 9999999999 { + h.logger.Warn( + "creationDate received is in milliseconds, we convert it to seconds", + zap.Int64("creationDate", event.CreationDate)) + // if we receive a timestamp in milliseconds, we convert it to seconds + // but since it is totally possible to have a timestamp in seconds that is bigger than 9999999999 + // we will accept timestamp up to 9999999999 (2286-11-20 18:46:39 +0100 CET) + event.CreationDate, _ = strconv.ParseInt( + strconv.FormatInt(event.CreationDate, 10)[:10], 10, 64) + } h.goFF.CollectEventData(event) } diff --git a/cmd/relayproxy/controller/collect_eval_data_test.go b/cmd/relayproxy/controller/collect_eval_data_test.go index a3652778aa0..f4042732eb3 100644 --- a/cmd/relayproxy/controller/collect_eval_data_test.go +++ b/cmd/relayproxy/controller/collect_eval_data_test.go @@ -14,11 +14,13 @@ import ( "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ffclient "github.com/thomaspoignant/go-feature-flag" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/controller" "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/metric" "github.com/thomaspoignant/go-feature-flag/exporter/fileexporter" "github.com/thomaspoignant/go-feature-flag/retriever/fileretriever" + "go.uber.org/zap" ) func Test_collect_eval_data_Handler(t *testing.T) { @@ -86,6 +88,17 @@ func Test_collect_eval_data_Handler(t *testing.T) { errorCode: http.StatusBadRequest, }, }, + { + name: "be sure that the creation date is a unix timestamp", + args: args{ + "../testdata/controller/collect_eval_data/valid_request_with_timestamp_ms.json", + }, + want: want{ + httpCode: http.StatusOK, + bodyFile: "../testdata/controller/collect_eval_data/valid_response.json", + collectedDataFile: "../testdata/controller/collect_eval_data/valid_collected_data_with_timestamp_ms.json", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -107,7 +120,9 @@ func Test_collect_eval_data_Handler(t *testing.T) { Exporter: &fileexporter.Exporter{Filename: exporterFile.Name()}, }, }) - ctrl := controller.NewCollectEvalData(goFF, metric.Metrics{}) + logger, err := zap.NewDevelopment() + require.NoError(t, err) + ctrl := controller.NewCollectEvalData(goFF, metric.Metrics{}, logger) e := echo.New() rec := httptest.NewRecorder() @@ -154,7 +169,7 @@ func Test_collect_eval_data_Handler(t *testing.T) { assert.NoError(t, err, "Impossible the expected wantBody file %s", tt.want.bodyFile) assert.Equal(t, tt.want.httpCode, rec.Code, "Invalid HTTP Code") assert.JSONEq(t, string(wantBody), replacedStr, "Invalid response wantBody") - assert.Equal(t, string(wantCollectData), string(exportedData), "Invalid exported data") + assert.JSONEq(t, string(wantCollectData), string(exportedData), "Invalid exported data") }) } } diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json b/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json index 6bc4280cd9b..e741c97a671 100644 --- a/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/collected_data_with_source_field.json @@ -1 +1,12 @@ -{"kind":"feature","contextKind":"user","userKey":"94a25909-20d8-40cc-8500-fee99b569345","creationDate":1680246000011,"key":"my-feature-flag","variation":"admin-variation","value":"string","default":false,"version":"v1.0.0","source":"EDGE"} +{ + "kind": "feature", + "contextKind": "user", + "userKey": "94a25909-20d8-40cc-8500-fee99b569345", + "creationDate": 1680246000, + "key": "my-feature-flag", + "variation": "admin-variation", + "value": "string", + "default": false, + "version": "v1.0.0", + "source": "EDGE" +} diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json b/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json index ad1a0ee66d5..9867d91a14a 100644 --- a/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/request_with_source_field.json @@ -2,7 +2,7 @@ "events": [ { "contextKind": "user", - "creationDate": 1680246000011, + "creationDate": 1680246000, "default": false, "key": "my-feature-flag", "kind": "feature", diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json index e5fb25016ea..087e078987d 100644 --- a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data.json @@ -1 +1,12 @@ -{"kind":"feature","contextKind":"user","userKey":"94a25909-20d8-40cc-8500-fee99b569345","creationDate":1680246000011,"key":"my-feature-flag","variation":"admin-variation","value":"string","default":false,"version":"v1.0.0","source":"PROVIDER_CACHE"} +{ + "kind": "feature", + "contextKind": "user", + "userKey": "94a25909-20d8-40cc-8500-fee99b569345", + "creationDate": 1680246000, + "key": "my-feature-flag", + "variation": "admin-variation", + "value": "string", + "default": false, + "version": "v1.0.0", + "source": "PROVIDER_CACHE" +} diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data_with_timestamp_ms.json b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data_with_timestamp_ms.json new file mode 100644 index 00000000000..e3e9ddbd51f --- /dev/null +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_collected_data_with_timestamp_ms.json @@ -0,0 +1,12 @@ +{ + "kind": "feature", + "contextKind": "user", + "userKey": "94a25909-20d8-40cc-8500-fee99b569345", + "creationDate": 1733230547, + "key": "my-feature-flag", + "variation": "admin-variation", + "value": "string", + "default": false, + "version": "v1.0.0", + "source": "PROVIDER_CACHE" +} diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request.json b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request.json index 13912292201..2ac990c7221 100644 --- a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request.json +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request.json @@ -2,7 +2,7 @@ "events": [ { "contextKind": "user", - "creationDate": 1680246000011, + "creationDate": 1680246000, "default": false, "key": "my-feature-flag", "kind": "feature", diff --git a/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request_with_timestamp_ms.json b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request_with_timestamp_ms.json new file mode 100644 index 00000000000..cce93b33e4b --- /dev/null +++ b/cmd/relayproxy/testdata/controller/collect_eval_data/valid_request_with_timestamp_ms.json @@ -0,0 +1,15 @@ +{ + "events": [ + { + "contextKind": "user", + "creationDate": 1733230547728, + "default": false, + "key": "my-feature-flag", + "kind": "feature", + "userKey": "94a25909-20d8-40cc-8500-fee99b569345", + "value": "string", + "variation": "admin-variation", + "version": "v1.0.0" + } + ] +} \ No newline at end of file diff --git a/examples/openfeature_web/webapp/package-lock.json b/examples/openfeature_web/webapp/package-lock.json index 32b1526b84b..48809af47c8 100644 --- a/examples/openfeature_web/webapp/package-lock.json +++ b/examples/openfeature_web/webapp/package-lock.json @@ -22,7 +22,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-env": "^1.7.0", "browser-sync": "^3.0.3", - "eslint": "^9.15.0", + "eslint": "^9.16.0", "eslint-webpack-plugin": "^4.2.0", "htmlnano": "^2.1.1", "imagemin-cli": "^8.0.0", @@ -1821,9 +1821,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6103,9 +6103,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -6113,7 +6113,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.16.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", diff --git a/examples/openfeature_web/webapp/package.json b/examples/openfeature_web/webapp/package.json index 24f5f10c41f..5faf52c4694 100644 --- a/examples/openfeature_web/webapp/package.json +++ b/examples/openfeature_web/webapp/package.json @@ -37,7 +37,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-env": "^1.7.0", "browser-sync": "^3.0.3", - "eslint": "^9.15.0", + "eslint": "^9.16.0", "eslint-webpack-plugin": "^4.2.0", "htmlnano": "^2.1.1", "imagemin-cli": "^8.0.0", diff --git a/go.mod b/go.mod index e7838375ed4..891fc0f295d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.32.5 github.com/aws/aws-sdk-go-v2/config v1.28.5 github.com/aws/aws-sdk-go-v2/credentials v1.17.46 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.40 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 github.com/aws/aws-sdk-go-v2/service/kinesis v1.32.6 github.com/aws/aws-sdk-go-v2/service/s3 v1.69.0 github.com/aws/aws-sdk-go-v2/service/sqs v1.37.1 diff --git a/go.sum b/go.sum index a4a424a50b1..302987687d8 100644 --- a/go.sum +++ b/go.sum @@ -215,8 +215,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnq github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.40 h1:CbalQNEYQljzAJ+3beY8FQBShdLNLpJzHL4h/5LSFMc= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.40/go.mod h1:1iYVr/urNWuZ7WZ1829FSE7RRTaXvzFdwrEQV8Z40cE= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41 h1:hqcxMc2g/MwwnRMod9n6Bd+t+9Nf7d5qRg7RaXKPd6o= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.41/go.mod h1:d1eH0VrttvPmrCraU68LOyNdu26zFxQFjrVSb5vdhog= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/DataCollectorHook.kt b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/DataCollectorHook.kt index 85d96ec4353..fbcf0b192a5 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/DataCollectorHook.kt +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/DataCollectorHook.kt @@ -14,7 +14,7 @@ class DataCollectorHook(private val collectorManager: DataCollectorManager) : ) { val event = Event( contextKind = "user", - creationDate = Date().time, + creationDate = Date().time / 1000L, key = ctx.flagKey, kind = "feature", userKey = ctx.ctx?.getTargetingKey(), @@ -29,7 +29,7 @@ class DataCollectorHook(private val collectorManager: DataCollectorManager) : override fun error(ctx: HookContext, error: Exception, hints: Map) { val event = Event( contextKind = "user", - creationDate = Date().time, + creationDate = Date().time / 1000L, key = ctx.flagKey, kind = "feature", userKey = ctx.ctx?.getTargetingKey(), @@ -40,4 +40,4 @@ class DataCollectorHook(private val collectorManager: DataCollectorManager) : ) collectorManager.addEvent(event) } -} \ No newline at end of file +} diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApiTest.kt b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApiTest.kt index a78b17ae744..33d96a8dfff 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApiTest.kt +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApiTest.kt @@ -21,7 +21,7 @@ class GoFeatureFlagApiTest { private var defaultEventList: List = listOf( Event( contextKind = "contextKind", - creationDate = 1721650841108, + creationDate = 1721650841, key = "flag-1", kind = "feature", userKey = "981f2662-1fb4-4732-ac6d-8399d9205aa9", @@ -140,4 +140,4 @@ class GoFeatureFlagApiTest { val got = recordedRequest.body.readUtf8() JSONAssert.assertEquals(want, got, false) } -} \ No newline at end of file +} diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result.json b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result.json index 056131cdff3..eb696a8db02 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result.json +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result.json @@ -2,7 +2,7 @@ "events": [ { "contextKind": "contextKind", - "creationDate": 1721650841108, + "creationDate": 1721650841, "key": "flag-1", "kind": "feature", "userKey": "981f2662-1fb4-4732-ac6d-8399d9205aa9", @@ -11,7 +11,7 @@ "variation": "enabled" } ], - "metadata": { + "meta": { "provider": "android", "openfeature": "true" } diff --git a/website/static/sdk-versions.json b/website/static/sdk-versions.json index 96909f2e48d..e2d52bc38e0 100644 --- a/website/static/sdk-versions.json +++ b/website/static/sdk-versions.json @@ -1 +1 @@ -{"maven":{"sdk":"1.12.2","providerKt":"0.1.0","providerJava":"0.3.0","android":"0.3.0"},"npm":{"core":"1.5.0","serverSDK":"1.16.2","providerServer":"0.7.3","providerWeb":"0.2.1"},"pypi":{"sdk":"0.7.2","provider":"0.2.1"},"nuget":{"sdk":"2.1.0","provider":"0.2.0"},"go":{"provider":"v0.2.1","sdk":"v1.13.1"}} \ No newline at end of file +{"maven":{"sdk":"1.12.2","providerKt":"0.1.0","providerJava":"0.4.0","android":"0.3.0"},"npm":{"core":"1.5.0","serverSDK":"1.16.2","webSDK":"1.3.2","providerWeb":"0.2.1"},"pypi":{"sdk":"0.7.4","provider":"0.3.0"},"nuget":{"sdk":"2.1.0","provider":"0.2.0"},"go":{"provider":"v0.2.1","sdk":"v1.13.1"}} \ No newline at end of file