From 4c75e842640b6f75a227c1b48dd2aafda0ca36f0 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Wed, 18 Dec 2024 09:28:50 +0100 Subject: [PATCH 1/2] feat(Provider): add support for tracking in the Openfeature provider --- Provider/api/Provider.api | 1 + Provider/build.gradle.kts | 2 +- .../openfeature/ConfidenceFeatureProvider.kt | 15 +++++ .../openfeature/ProviderIntegrationTest.kt | 61 +++++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) diff --git a/Provider/api/Provider.api b/Provider/api/Provider.api index fc69a285..0b8f1658 100644 --- a/Provider/api/Provider.api +++ b/Provider/api/Provider.api @@ -13,6 +13,7 @@ public final class com/spotify/confidence/openfeature/ConfidenceFeatureProvider public fun observe ()Lkotlinx/coroutines/flow/Flow; public fun onContextSet (Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/EvaluationContext;)V public fun shutdown ()V + public fun track (Ljava/lang/String;Ldev/openfeature/sdk/EvaluationContext;Ldev/openfeature/sdk/TrackingEventDetails;)V } public final class com/spotify/confidence/openfeature/ConfidenceFeatureProvider$Companion { diff --git a/Provider/build.gradle.kts b/Provider/build.gradle.kts index 82c6dbe8..8f0b0b9a 100644 --- a/Provider/build.gradle.kts +++ b/Provider/build.gradle.kts @@ -11,7 +11,7 @@ plugins { } object Versions { - const val openFeatureSDK = "0.3.0" + const val openFeatureSDK = "0.3.2" const val okHttp = "4.10.0" const val kotlinxSerialization = "1.6.0" const val coroutines = "1.7.3" diff --git a/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt b/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt index ec007dd0..e854e981 100644 --- a/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt +++ b/Provider/src/main/java/com/spotify/confidence/openfeature/ConfidenceFeatureProvider.kt @@ -14,6 +14,7 @@ import dev.openfeature.sdk.Hook import dev.openfeature.sdk.ProviderEvaluation import dev.openfeature.sdk.ProviderMetadata import dev.openfeature.sdk.Reason +import dev.openfeature.sdk.TrackingEventDetails import dev.openfeature.sdk.Value import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents @@ -130,6 +131,10 @@ class ConfidenceFeatureProvider private constructor( return generateEvaluation(key, defaultValue) } + override fun track(trackingEventName: String, context: EvaluationContext?, details: TrackingEventDetails?) { + confidence.track(trackingEventName, details?.toConfidenceValue() ?: emptyMap()) + } + private fun generateEvaluation( key: String, defaultValue: T @@ -166,6 +171,16 @@ class ConfidenceFeatureProvider private constructor( } } +private fun TrackingEventDetails.toConfidenceValue(): Map = mapOf( + "value" to (this.value?.toConfidenceValue() ?: ConfidenceValue.Null) +) + this.structure.asMap().mapValues { it.value.toConfidenceValue() } + +private fun Number.toConfidenceValue(): ConfidenceValue = when (this) { + is Int -> ConfidenceValue.Integer(this) + is Double -> ConfidenceValue.Double(this) + else -> ConfidenceValue.Null +} + internal fun Value.toConfidenceValue(): ConfidenceValue = when (this) { is Value.Structure -> ConfidenceValue.Struct(structure.mapValues { it.value.toConfidenceValue() }) is Value.Boolean -> ConfidenceValue.Boolean(this.boolean) diff --git a/Provider/src/test/java/com/spotify/confidence/openfeature/ProviderIntegrationTest.kt b/Provider/src/test/java/com/spotify/confidence/openfeature/ProviderIntegrationTest.kt index c18a8353..496ac2bc 100644 --- a/Provider/src/test/java/com/spotify/confidence/openfeature/ProviderIntegrationTest.kt +++ b/Provider/src/test/java/com/spotify/confidence/openfeature/ProviderIntegrationTest.kt @@ -3,18 +3,23 @@ package com.spotify.confidence.openfeature import android.content.Context import com.spotify.confidence.ConfidenceFactory import dev.openfeature.sdk.ImmutableContext +import dev.openfeature.sdk.ImmutableStructure import dev.openfeature.sdk.OpenFeatureAPI import dev.openfeature.sdk.Reason +import dev.openfeature.sdk.TrackingEventDetails import dev.openfeature.sdk.Value import dev.openfeature.sdk.events.EventHandler import dev.openfeature.sdk.events.OpenFeatureEvents import dev.openfeature.sdk.exceptions.ErrorCode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -176,5 +181,61 @@ class ProviderIntegrationTest { assertEquals(4, evaluationDetails.value.asStructure()?.getOrDefault("my-integer", Value.Integer(-1))?.asInteger()) } + + @Test + fun testEventTracking() = runTest { + val testDispatcher = StandardTestDispatcher(testScheduler) + val eventsHandler = EventHandler(Dispatchers.IO).apply { + publish(OpenFeatureEvents.ProviderStale) + } + val cacheDir = mockContext.getDir("events", Context.MODE_PRIVATE) + assertTrue(cacheDir.isDirectory) + assertTrue(cacheDir.listFiles().isEmpty()) + val mockConfidence = ConfidenceFactory.create(mockContext, clientSecret, dispatcher = testDispatcher) + + OpenFeatureAPI.setProvider( + ConfidenceFeatureProvider.create( + confidence = mockConfidence, + initialisationStrategy = InitialisationStrategy.ActivateAndFetchAsync, + eventHandler = eventsHandler + ), + ImmutableContext( + targetingKey = UUID.randomUUID().toString(), + attributes = mutableMapOf( + "user" to Value.Structure( + mapOf( + "country" to Value.String("SE") + ) + ) + ) + ) + ) + runBlocking { + awaitProviderReady(eventsHandler = eventsHandler) + } + + assertEquals(1, cacheDir.listFiles()?.size) + assertEquals(0, cacheDir.listFiles()?.first()?.readLines()?.size) + + OpenFeatureAPI.getClient().track("MyEventName", TrackingEventDetails(33.0, ImmutableStructure("key" to Value.String("value")))) + testScheduler.advanceUntilIdle() + val lines = cacheDir.listFiles()?.first()?.readLines() ?: emptyList() + assertEquals(1, lines.size) + val jsonString = lines.first() + assertTrue(jsonString.contains("\"eventDefinition\":\"MyEventName\"")) + println(lines.first()) + assertTrue(jsonString.contains("\"payload\":{\"value\":{\"double\":33.0},\"key\":{\"string\":\"value\"}")) + val regex = Regex( + "\"context\":\\{\"map\":\\{\"visitor_id\":\\{\"string\":\"[a-f0-9\\-]+\"}," + + "\"targeting_key\":\\{\"string\":\"[a-f0-9\\-]+\"}," + + "\"user\":\\{\"map\":\\{\"country\":\\{\"string\":\"SE\"}}}}}" + ) + assertTrue( + "Expected the context map to match the regex for visitor_id and targeting_key. Actual JSON:\n$jsonString", + regex.containsMatchIn(jsonString) + ) + } + private val flagsFileName = "confidence_flags_cache.json" + private val eventsFileName = "confidence_flags_cache.json" } \ No newline at end of file From ff58055b814d07fe34720e17074a326e42bf7ad6 Mon Sep 17 00:00:00 2001 From: Nicklas Lundin Date: Wed, 18 Dec 2024 09:29:12 +0100 Subject: [PATCH 2/2] fix: replace print with logger --- .../src/main/java/com/spotify/confidence/EventSenderEngine.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Confidence/src/main/java/com/spotify/confidence/EventSenderEngine.kt b/Confidence/src/main/java/com/spotify/confidence/EventSenderEngine.kt index 4992f67e..2be892fb 100644 --- a/Confidence/src/main/java/com/spotify/confidence/EventSenderEngine.kt +++ b/Confidence/src/main/java/com/spotify/confidence/EventSenderEngine.kt @@ -40,7 +40,7 @@ internal class EventSenderEngineImpl( } private val exceptionHandler by lazy { CoroutineExceptionHandler { _, e -> - print(e.message) + debugLogger?.logMessage(message = "EventSenderEngine error: $e", isWarning = true) } }