Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Provider): Add support for Tracking from OpenFeature #190

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
1 change: 1 addition & 0 deletions Provider/api/Provider.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion Provider/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <T> generateEvaluation(
key: String,
defaultValue: T
Expand Down Expand Up @@ -166,6 +171,16 @@ class ConfidenceFeatureProvider private constructor(
}
}

private fun TrackingEventDetails.toConfidenceValue(): Map<String, ConfidenceValue> = 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
}
Loading