From 6ce496090d717834506a56e74d6f21fff9de5484 Mon Sep 17 00:00:00 2001 From: Thomas Poignant Date: Thu, 16 Jan 2025 18:51:04 +0100 Subject: [PATCH] feat: Support exporter metadata in Android provider (#2944) * feat: Support exporter metadata in Android provider Signed-off-by: Thomas Poignant * adding test Signed-off-by: Thomas Poignant --------- Signed-off-by: Thomas Poignant --- .../openfeature/bean/GoFeatureFlagOptions.kt | 11 +++- .../controller/GoFeatureFlagApi.kt | 6 +- .../gofeatureflag/openfeature/hook/Events.kt | 2 +- .../openfeature/GoFeatureFlagProviderTest.kt | 55 +++++++++++++++++++ .../controller/GoFeatureFlagApiTest.kt | 20 +++++++ .../openfeature/hook/valid_result.json | 2 +- .../hook/valid_result_metadata.json | 20 +++++++ 7 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result_metadata.json diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/bean/GoFeatureFlagOptions.kt b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/bean/GoFeatureFlagOptions.kt index bc49d79c483..cd36133cd42 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/bean/GoFeatureFlagOptions.kt +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/bean/GoFeatureFlagOptions.kt @@ -47,6 +47,15 @@ data class GoFeatureFlagOptions( * when calling the evaluation API. * default: 1000 ms */ - val flushIntervalMs: Long = 300000 + val flushIntervalMs: Long = 300000, + + /** + * (optional) exporter metadata is a set of key-value that will be added to the metadata when calling the + * exporter API. All those informations will be added to the event produce by the exporter. + * + * ‼️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. + */ + val exporterMetadata: Map = emptyMap(), ) diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApi.kt b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApi.kt index bb1db78bf12..a34de64cc2e 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApi.kt +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/controller/GoFeatureFlagApi.kt @@ -52,7 +52,11 @@ class GoFeatureFlagApi(private val options: GoFeatureFlagOptions) { } val mediaType = "application/json".toMediaTypeOrNull() - val requestBody = gson.toJson(Events(events)).toRequestBody(mediaType) + + val metadata = options.exporterMetadata.toMutableMap() + metadata["provider"] = "android" + metadata["openfeature"] = true + val requestBody = gson.toJson(Events(events, metadata)).toRequestBody(mediaType) val reqBuilder = okhttp3.Request.Builder() .url(urlBuilder.build()) .post(requestBody) diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/Events.kt b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/Events.kt index 16b151ae7d9..628bf0f413a 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/Events.kt +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/main/java/org/gofeatureflag/openfeature/hook/Events.kt @@ -3,5 +3,5 @@ package org.gofeatureflag.openfeature.hook data class Events( val events: List?, - val meta: Map = mapOf("provider" to "android", "openfeature" to "true") + val meta: Map = emptyMap() ) \ No newline at end of file diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/GoFeatureFlagProviderTest.kt b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/GoFeatureFlagProviderTest.kt index 05a9b04d04c..77dfbf7b216 100644 --- a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/GoFeatureFlagProviderTest.kt +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/java/org/gofeatureflag/openfeature/GoFeatureFlagProviderTest.kt @@ -176,4 +176,59 @@ class GoFeatureFlagProviderTest { val got2 = Gson().fromJson(recordedRequest2.body.readUtf8(), Events::class.java) assertEquals(3, got2.events?.size) } + + @Test + fun `should call the hook and send metadata`() { + val jsonFilePath = + javaClass.classLoader?.getResource("org.gofeatureflag.openfeature.ofrep/valid_api_response.json")?.file + val jsonString = String(Files.readAllBytes(Paths.get(jsonFilePath))) + mockWebServer!!.enqueue(MockResponse().setBody(jsonString).setResponseCode(200)) + mockWebServer!!.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + mockWebServer!!.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + val options = + GoFeatureFlagOptions( + endpoint = mockWebServer!!.url("/").toString(), + flushIntervalMs = 100, + pollingIntervalInMillis = 10000, + exporterMetadata = mapOf("device" to "Pixel 4", "appVersion" to "1.0.0") + ) + + val provider = GoFeatureFlagProvider(options) + val ctx = ImmutableContext(targetingKey = "123") + runBlocking { + OpenFeatureAPI.setProviderAndWait( + provider = provider, + dispatcher = Dispatchers.IO, + initialContext = ctx + ) + } + + val client = OpenFeatureAPI.getClient() + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + Thread.sleep(1000) + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + client.getStringValue("title-flag", "default") + Thread.sleep(1000) + mockWebServer!!.takeRequest() + val recordedRequest: RecordedRequest = mockWebServer!!.takeRequest() + val got = Gson().fromJson(recordedRequest.body.readUtf8(), Events::class.java) + assertEquals(6, got.events?.size) + assertEquals("Pixel 4", got.meta["device"]) + assertEquals("1.0.0", got.meta["appVersion"]) + assertEquals("android", got.meta["provider"]) + assertEquals(true, got.meta["openfeature"]) + val recordedRequest2: RecordedRequest = mockWebServer!!.takeRequest() + val got2 = Gson().fromJson(recordedRequest2.body.readUtf8(), Events::class.java) + assertEquals(3, got2.events?.size) + assertEquals("Pixel 4", got2.meta["device"]) + assertEquals("1.0.0", got2.meta["appVersion"]) + assertEquals("android", got2.meta["provider"]) + assertEquals(true, got2.meta["openfeature"]) + } } \ 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 33d96a8dfff..3f33cf695e8 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 @@ -140,4 +140,24 @@ class GoFeatureFlagApiTest { val got = recordedRequest.body.readUtf8() JSONAssert.assertEquals(want, got, false) } + + @Test + fun `should have a valid body request when using exporter metadata`(): Unit = runBlocking { + mockWebServer!!.enqueue(MockResponse().setBody("{}").setResponseCode(200)) + val api = + GoFeatureFlagApi( + GoFeatureFlagOptions( + endpoint = mockWebServer!!.url("/").toString(), + apiKey = "my-api-key", + exporterMetadata = mapOf("appVersion" to "1.0.0", "device" to "Pixel 4") + ) + ) + api.postEventsToDataCollector(defaultEventList) + val recordedRequest: RecordedRequest = mockWebServer!!.takeRequest() + val jsonFilePath = + javaClass.classLoader?.getResource("org/gofeatureflag/openfeature/hook/valid_result_metadata.json")?.file + val want = File(jsonFilePath.toString()).readText(Charsets.UTF_8) + val got = recordedRequest.body.readUtf8() + JSONAssert.assertEquals(want, got, false) + } } 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 eb696a8db02..a8a26c0f930 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 @@ -13,6 +13,6 @@ ], "meta": { "provider": "android", - "openfeature": "true" + "openfeature": true } } diff --git a/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result_metadata.json b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result_metadata.json new file mode 100644 index 00000000000..535cd35b323 --- /dev/null +++ b/openfeature/providers/kotlin-provider/gofeatureflag-kotlin-provider/src/test/resources/org/gofeatureflag/openfeature/hook/valid_result_metadata.json @@ -0,0 +1,20 @@ +{ + "events": [ + { + "contextKind": "contextKind", + "creationDate": 1721650841, + "key": "flag-1", + "kind": "feature", + "userKey": "981f2662-1fb4-4732-ac6d-8399d9205aa9", + "value": true, + "default": false, + "variation": "enabled" + } + ], + "meta": { + "provider": "android", + "openfeature": true, + "appVersion": "1.0.0", + "device": "Pixel 4" + } +}