diff --git a/Confidence/api/Confidence.api b/Confidence/api/Confidence.api index 8812604b..97d7511e 100644 --- a/Confidence/api/Confidence.api +++ b/Confidence/api/Confidence.api @@ -1,26 +1,3 @@ -public final class com/spotify/confidence/AndroidLifecycleEventProducer : android/app/Application$ActivityLifecycleCallbacks, androidx/lifecycle/DefaultLifecycleObserver, com/spotify/confidence/EventProducer { - public static final field Companion Lcom/spotify/confidence/AndroidLifecycleEventProducer$Companion; - public fun (Landroid/app/Application;Z)V - public fun contextChanges ()Lkotlinx/coroutines/flow/Flow; - public fun events ()Lkotlinx/coroutines/flow/Flow; - public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityDestroyed (Landroid/app/Activity;)V - public fun onActivityPaused (Landroid/app/Activity;)V - public fun onActivityResumed (Landroid/app/Activity;)V - public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V - public fun onActivityStarted (Landroid/app/Activity;)V - public fun onActivityStopped (Landroid/app/Activity;)V - public fun onCreate (Landroidx/lifecycle/LifecycleOwner;)V - public fun stop ()V -} - -public final class com/spotify/confidence/AndroidLifecycleEventProducer$Companion { -} - -public final class com/spotify/confidence/AndroidLifecycleEventProducerKt { - public static final fun getReferrer (Landroid/app/Activity;)Landroid/net/Uri; -} - public final class com/spotify/confidence/BuildConfig { public static final field BUILD_TYPE Ljava/lang/String; public static final field DEBUG Z @@ -46,7 +23,7 @@ public final class com/spotify/confidence/Confidence : com/spotify/confidence/Co public final fun putContext (Ljava/util/Map;Ljava/util/List;)V public fun removeContext (Ljava/lang/String;)V public fun stop ()V - public fun track (Lcom/spotify/confidence/EventProducer;)V + public fun track (Lcom/spotify/confidence/Producer;)V public fun track (Ljava/lang/String;Ljava/util/Map;)V public synthetic fun withContext (Ljava/util/Map;)Lcom/spotify/confidence/Contextual; public fun withContext (Ljava/util/Map;)Lcom/spotify/confidence/EventSender; @@ -56,6 +33,28 @@ public abstract interface class com/spotify/confidence/ConfidenceContextProvider public abstract fun getContext ()Ljava/util/Map; } +public final class com/spotify/confidence/ConfidenceDeviceInfoContextProducer : com/spotify/confidence/Producer { + public static final field APP_BUILD_CONTEXT_KEY Ljava/lang/String; + public static final field APP_NAMESPACE_CONTEXT_KEY Ljava/lang/String; + public static final field APP_VERSION_CONTEXT_KEY Ljava/lang/String; + public static final field Companion Lcom/spotify/confidence/ConfidenceDeviceInfoContextProducer$Companion; + public static final field DEVICE_BRAND_CONTEXT_KEY Ljava/lang/String; + public static final field DEVICE_MANUFACTURER_CONTEXT_KEY Ljava/lang/String; + public static final field DEVICE_MODEL_CONTEXT_KEY Ljava/lang/String; + public static final field DEVICE_TYPE_CONTEXT_KEY Ljava/lang/String; + public static final field LOCALE_CONTEXT_KEY Ljava/lang/String; + public static final field OS_NAME_CONTEXT_KEY Ljava/lang/String; + public static final field OS_VERSION_CONTEXT_KEY Ljava/lang/String; + public static final field PREFERRED_LANGUAGES_CONTEXT_KEY Ljava/lang/String; + public fun (Landroid/content/Context;ZZZZ)V + public synthetic fun (Landroid/content/Context;ZZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun stop ()V + public fun updates ()Lkotlinx/coroutines/flow/Flow; +} + +public final class com/spotify/confidence/ConfidenceDeviceInfoContextProducer$Companion { +} + public final class com/spotify/confidence/ConfidenceError { public fun ()V } @@ -286,6 +285,7 @@ public final class com/spotify/confidence/ConfidenceValue$List$Companion { public final class com/spotify/confidence/ConfidenceValue$Null : com/spotify/confidence/ConfidenceValue { public static final field INSTANCE Lcom/spotify/confidence/ConfidenceValue$Null; public final fun serializer ()Lkotlinx/serialization/KSerializer; + public fun toString ()Ljava/lang/String; } public final class com/spotify/confidence/ConfidenceValue$String : com/spotify/confidence/ConfidenceValue { @@ -406,32 +406,10 @@ public final class com/spotify/confidence/Evaluation { public fun toString ()Ljava/lang/String; } -public final class com/spotify/confidence/Event { - public fun (Ljava/lang/String;Ljava/util/Map;Z)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Z - public final fun copy (Ljava/lang/String;Ljava/util/Map;Z)Lcom/spotify/confidence/Event; - public static synthetic fun copy$default (Lcom/spotify/confidence/Event;Ljava/lang/String;Ljava/util/Map;ZILjava/lang/Object;)Lcom/spotify/confidence/Event; - public fun equals (Ljava/lang/Object;)Z - public final fun getData ()Ljava/util/Map; - public final fun getName ()Ljava/lang/String; - public final fun getShouldFlush ()Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public abstract interface class com/spotify/confidence/EventProducer { - public abstract fun contextChanges ()Lkotlinx/coroutines/flow/Flow; - public abstract fun events ()Lkotlinx/coroutines/flow/Flow; - public abstract fun stop ()V -} - public abstract interface class com/spotify/confidence/EventSender : com/spotify/confidence/Contextual { public abstract fun flush ()V public abstract fun stop ()V - public abstract fun track (Lcom/spotify/confidence/EventProducer;)V + public abstract fun track (Lcom/spotify/confidence/Producer;)V public abstract fun track (Ljava/lang/String;Ljava/util/Map;)V public abstract fun withContext (Ljava/util/Map;)Lcom/spotify/confidence/EventSender; } @@ -490,6 +468,11 @@ public final class com/spotify/confidence/LoggingLevel : java/lang/Enum { public static fun values ()[Lcom/spotify/confidence/LoggingLevel; } +public abstract interface class com/spotify/confidence/Producer { + public abstract fun stop ()V + public abstract fun updates ()Lkotlinx/coroutines/flow/Flow; +} + public abstract interface class com/spotify/confidence/ProviderCache { public abstract fun get ()Lcom/spotify/confidence/FlagResolution; public abstract fun refresh (Lcom/spotify/confidence/FlagResolution;)V @@ -536,6 +519,36 @@ public final class com/spotify/confidence/Result$Success : com/spotify/confidenc public fun toString ()Ljava/lang/String; } +public abstract interface class com/spotify/confidence/Update { +} + +public final class com/spotify/confidence/Update$ContextUpdate : com/spotify/confidence/Update { + public fun (Ljava/util/Map;)V + public final fun component1 ()Ljava/util/Map; + public final fun copy (Ljava/util/Map;)Lcom/spotify/confidence/Update$ContextUpdate; + public static synthetic fun copy$default (Lcom/spotify/confidence/Update$ContextUpdate;Ljava/util/Map;ILjava/lang/Object;)Lcom/spotify/confidence/Update$ContextUpdate; + public fun equals (Ljava/lang/Object;)Z + public final fun getContext ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/spotify/confidence/Update$Event : com/spotify/confidence/Update { + public fun (Ljava/lang/String;Ljava/util/Map;Z)V + public synthetic fun (Ljava/lang/String;Ljava/util/Map;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Z + public final fun copy (Ljava/lang/String;Ljava/util/Map;Z)Lcom/spotify/confidence/Update$Event; + public static synthetic fun copy$default (Lcom/spotify/confidence/Update$Event;Ljava/lang/String;Ljava/util/Map;ZILjava/lang/Object;)Lcom/spotify/confidence/Update$Event; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Ljava/util/Map; + public final fun getName ()Ljava/lang/String; + public final fun getShouldFlush ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/spotify/confidence/apply/ApplyInstance { public static final field Companion Lcom/spotify/confidence/apply/ApplyInstance$Companion; public synthetic fun (ILjava/util/Date;Lcom/spotify/confidence/apply/EventStatus;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V diff --git a/Confidence/build.gradle.kts b/Confidence/build.gradle.kts index 22ca84a1..cafda7ba 100644 --- a/Confidence/build.gradle.kts +++ b/Confidence/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation( "org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.kotlinxSerialization}" ) - implementation("androidx.lifecycle:lifecycle-process:2.6.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}") testImplementation("junit:junit:${Versions.junit}") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.coroutines}") diff --git a/Confidence/src/main/java/com/spotify/confidence/Confidence.kt b/Confidence/src/main/java/com/spotify/confidence/Confidence.kt index 4d1406b8..edee2ef5 100644 --- a/Confidence/src/main/java/com/spotify/confidence/Confidence.kt +++ b/Confidence/src/main/java/com/spotify/confidence/Confidence.kt @@ -46,7 +46,7 @@ class Confidence internal constructor( private var currentFetchJob: Job? = null private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher) - private val eventProducers: MutableList = mutableListOf() + private val producers: MutableList = mutableListOf() private val flagApplier = FlagApplierWithRetries( client = flagApplierClient, @@ -276,31 +276,29 @@ class Confidence internal constructor( activate() } - override fun track(eventProducer: EventProducer) { + override fun track(producer: Producer) { coroutineScope.launch { - eventProducer - .events() - .collect { event -> - eventSenderEngine.emit( - event.name, - event.data, - getContext() - ) - if (event.shouldFlush) { - eventSenderEngine.flush() + producer.updates().collect { update -> + when (update) { + is Update.Event -> { + eventSenderEngine.emit( + update.name, + update.data, + getContext() + ) + if (update.shouldFlush) { + eventSenderEngine.flush() + } } + is Update.ContextUpdate -> putContext(update.context) } + } + producers.add(producer) } - - coroutineScope.launch { - eventProducer.contextChanges() - .collect(this@Confidence::putContext) - } - eventProducers.add(eventProducer) } override fun stop() { - for (producer in eventProducers) { + for (producer in producers) { producer.stop() } if (parent == null) { diff --git a/Confidence/src/main/java/com/spotify/confidence/ConfidenceDeviceInfoContextProducer.kt b/Confidence/src/main/java/com/spotify/confidence/ConfidenceDeviceInfoContextProducer.kt new file mode 100644 index 00000000..9320defa --- /dev/null +++ b/Confidence/src/main/java/com/spotify/confidence/ConfidenceDeviceInfoContextProducer.kt @@ -0,0 +1,139 @@ +package com.spotify.confidence + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.util.Log +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import java.util.Locale + +/** + * Helper class to produce device information context for the Confidence context. + * + * @param applicationContext the application context. + * @param withAppInfo whether to include app information in the context. + * @param withDeviceInfo whether to include device information in the context. + * @param withOsInfo whether to include OS information in the context. + * @param withLocale whether to include locale information in the context. + * + * The values appended to the Context come primarily from the Android Build class and the application context. + * + * AppInfo contains: + * - version: the version name of the app. + * - build: the version code of the app. + * - namespace: the package name of the app. + * + * DeviceInfo contains: + * - manufacturer: the manufacturer of the device. + * - brand: the brand of the device. + * - model: the model of the device. + * - type: the type of the device. + * + * OsInfo contains: + * - name: the name of the OS. + * - version: the version of the OS. + * + * Locale contains: + * - locale: the locale of the device. + * - preferred_languages: the preferred languages of the device. + * + * The context is only updated when the producer is initialized and then static. + * + */ +class ConfidenceDeviceInfoContextProducer( + applicationContext: Context, + withAppInfo: Boolean = false, + withDeviceInfo: Boolean = false, + withOsInfo: Boolean = false, + withLocale: Boolean = false +) : Producer { + private val staticContext: ConfidenceFieldsType + private val packageInfo: PackageInfo? = try { + @Suppress("DEPRECATION") + applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(DebugLogger.TAG, "Failed to get package info", e) + null + } + + init { + val context = mutableMapOf() + if (withAppInfo) { + val currentVersion = ConfidenceValue.String(packageInfo?.versionName ?: "") + val currentBuild = ConfidenceValue.String(packageInfo?.getVersionCodeAsString() ?: "") + val bundleId = ConfidenceValue.String(applicationContext.packageName) + context["app"] = ConfidenceValue.Struct( + mapOf( + APP_VERSION_CONTEXT_KEY to currentVersion, + APP_BUILD_CONTEXT_KEY to currentBuild, + APP_NAMESPACE_CONTEXT_KEY to bundleId + ) + ) + } + + if (withDeviceInfo) { + context["device"] = ConfidenceValue.Struct( + mapOf( + DEVICE_MANUFACTURER_CONTEXT_KEY to ConfidenceValue.String(Build.MANUFACTURER), + DEVICE_BRAND_CONTEXT_KEY to ConfidenceValue.String(Build.BRAND), + DEVICE_MODEL_CONTEXT_KEY to ConfidenceValue.String(Build.MODEL), + DEVICE_TYPE_CONTEXT_KEY to ConfidenceValue.String("android") + ) + ) + } + + if (withOsInfo) { + context["os"] = ConfidenceValue.Struct( + mapOf( + OS_NAME_CONTEXT_KEY to ConfidenceValue.String("android"), + OS_VERSION_CONTEXT_KEY to ConfidenceValue.Double(Build.VERSION.SDK_INT.toDouble()) + ) + ) + } + + if (withLocale) { + val preferredLang = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val locales = applicationContext.resources.configuration.locales + (0 until locales.size()).map { locales.get(it).toString() } + } else { + listOf(Locale.getDefault().toString()) + } + val localeIdentifier = Locale.getDefault().toString() + val localeInfo = mapOf( + LOCALE_CONTEXT_KEY to ConfidenceValue.String(localeIdentifier), + PREFERRED_LANGUAGES_CONTEXT_KEY to ConfidenceValue.List(preferredLang.map(ConfidenceValue::String)) + ) + // these are on the top level + context += localeInfo + } + staticContext = context + } + + override fun updates(): Flow = flowOf(Update.ContextUpdate(staticContext)) + + override fun stop() {} + + companion object { + const val APP_VERSION_CONTEXT_KEY = "version" + const val APP_BUILD_CONTEXT_KEY = "build" + const val APP_NAMESPACE_CONTEXT_KEY = "namespace" + const val DEVICE_MANUFACTURER_CONTEXT_KEY = "manufacturer" + const val DEVICE_BRAND_CONTEXT_KEY = "brand" + const val DEVICE_MODEL_CONTEXT_KEY = "model" + const val DEVICE_TYPE_CONTEXT_KEY = "type" + const val OS_NAME_CONTEXT_KEY = "name" + const val OS_VERSION_CONTEXT_KEY = "version" + const val LOCALE_CONTEXT_KEY = "locale" + const val PREFERRED_LANGUAGES_CONTEXT_KEY = "preferred_languages" + } +} + +private fun PackageInfo.getVersionCodeAsString(): String = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + this.longVersionCode.toString() + } else { + @Suppress("DEPRECATION") + this.versionCode.toString() + } \ No newline at end of file diff --git a/Confidence/src/main/java/com/spotify/confidence/ConfidenceValue.kt b/Confidence/src/main/java/com/spotify/confidence/ConfidenceValue.kt index 1f0794a3..e4085948 100644 --- a/Confidence/src/main/java/com/spotify/confidence/ConfidenceValue.kt +++ b/Confidence/src/main/java/com/spotify/confidence/ConfidenceValue.kt @@ -9,33 +9,51 @@ import kotlinx.serialization.json.Json @Serializable(ConfidenceValueSerializer::class) sealed interface ConfidenceValue { @Serializable - data class String(val string: kotlin.String) : ConfidenceValue + data class String(val string: kotlin.String) : ConfidenceValue { + override fun toString() = string + } @Serializable - data class Double(val double: kotlin.Double) : ConfidenceValue + data class Double(val double: kotlin.Double) : ConfidenceValue { + override fun toString() = double.toString() + } @Serializable - data class Boolean(val boolean: kotlin.Boolean) : ConfidenceValue + data class Boolean(val boolean: kotlin.Boolean) : ConfidenceValue { + override fun toString() = boolean.toString() + } @Serializable - data class Integer(val integer: Int) : ConfidenceValue + data class Integer(val integer: Int) : ConfidenceValue { + override fun toString() = integer.toString() + } @Serializable - data class Struct(val map: Map) : ConfidenceValue + data class Struct(val map: Map) : ConfidenceValue { + override fun toString() = map.toString() + } @Serializable data class List(val list: kotlin.collections.List) : - ConfidenceValue + ConfidenceValue { + override fun toString() = list.toString() + } @Serializable - data class Date(@Serializable(DateSerializer::class) val date: java.util.Date) : ConfidenceValue + data class Date(@Serializable(DateSerializer::class) val date: java.util.Date) : ConfidenceValue { + override fun toString() = date.toString() + } @Serializable data class Timestamp(@Serializable(DateTimeSerializer::class) val dateTime: java.util.Date) : - ConfidenceValue + ConfidenceValue { + override fun toString() = dateTime.toString() + } @Serializable - object Null : ConfidenceValue + object Null : ConfidenceValue { + override fun toString() = "null" + } companion object { fun stringList(list: kotlin.collections.List) = diff --git a/Confidence/src/main/java/com/spotify/confidence/DebugLogger.kt b/Confidence/src/main/java/com/spotify/confidence/DebugLogger.kt index 580ae02d..64d433fb 100644 --- a/Confidence/src/main/java/com/spotify/confidence/DebugLogger.kt +++ b/Confidence/src/main/java/com/spotify/confidence/DebugLogger.kt @@ -4,14 +4,15 @@ import android.util.Log import kotlinx.serialization.json.JsonElement import java.net.URLEncoder -private const val TAG = "Confidence" - internal interface DebugLogger { fun logEvent(action: String, event: EngineEvent) fun logMessage(message: String, isWarning: Boolean = false, throwable: Throwable? = null) fun logFlag(action: String, details: String? = null) fun logContext(action: String, context: Map) fun logResolve(flag: String, context: JsonElement) + companion object { + const val TAG = "Confidence" + } } internal class DebugLoggerImpl(private val filterLevel: LoggingLevel, private val clientKey: String) : DebugLogger { @@ -60,10 +61,10 @@ internal class DebugLoggerImpl(private val filterLevel: LoggingLevel, private va private fun log(messageLevel: LoggingLevel, message: String) { if (messageLevel >= filterLevel) { when (messageLevel) { - LoggingLevel.VERBOSE -> Log.v(TAG, message) - LoggingLevel.DEBUG -> Log.d(TAG, message) - LoggingLevel.WARN -> Log.w(TAG, message) - LoggingLevel.ERROR -> Log.e(TAG, message) + LoggingLevel.VERBOSE -> Log.v(DebugLogger.TAG, message) + LoggingLevel.DEBUG -> Log.d(DebugLogger.TAG, message) + LoggingLevel.WARN -> Log.w(DebugLogger.TAG, message) + LoggingLevel.ERROR -> Log.e(DebugLogger.TAG, message) LoggingLevel.NONE -> { // do nothing } diff --git a/Confidence/src/main/java/com/spotify/confidence/EventProducer.kt b/Confidence/src/main/java/com/spotify/confidence/EventProducer.kt deleted file mode 100644 index e1e99d58..00000000 --- a/Confidence/src/main/java/com/spotify/confidence/EventProducer.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.spotify.confidence - -import kotlinx.coroutines.flow.Flow - -data class Event( - val name: String, - val data: Map, - val shouldFlush: Boolean = false -) - -interface EventProducer { - fun events(): Flow - fun contextChanges(): Flow> - - fun stop() -} \ No newline at end of file diff --git a/Confidence/src/main/java/com/spotify/confidence/EventSender.kt b/Confidence/src/main/java/com/spotify/confidence/EventSender.kt index 99414fe5..3b71da7d 100644 --- a/Confidence/src/main/java/com/spotify/confidence/EventSender.kt +++ b/Confidence/src/main/java/com/spotify/confidence/EventSender.kt @@ -12,10 +12,13 @@ interface EventSender : Contextual { ) /** - * Track Android-specific events like activities. - * @param eventProducer an eventProducer that produces the event, e.g. AndroidLifecycleEventProducer. + * Track Android-specific events like activities or Track Context updates. + * Please note that this method is collecting data in a coroutine scope and will be + * executed on the dispatcher that was defined with the creation of the Confidence instance. + * + * @param producer a producer that produces the events or context updates. */ - fun track(eventProducer: EventProducer) + fun track(producer: Producer) /** * Safely stop a Confidence instance diff --git a/Confidence/src/main/java/com/spotify/confidence/Producer.kt b/Confidence/src/main/java/com/spotify/confidence/Producer.kt new file mode 100644 index 00000000..9f09d365 --- /dev/null +++ b/Confidence/src/main/java/com/spotify/confidence/Producer.kt @@ -0,0 +1,23 @@ +package com.spotify.confidence + +import kotlinx.coroutines.flow.Flow + +sealed interface Update { + data class Event( + val name: String, + val data: Map, + val shouldFlush: Boolean = false + ) : Update + + data class ContextUpdate(val context: Map) : Update +} + +/** + * A producer is a class that can produce updates to the confidence system. + * Currently, the only supported updates are a context update or an event update where an Event will be + * sent using the event sender engine. + */ +interface Producer { + fun stop() + fun updates(): Flow +} \ No newline at end of file diff --git a/Confidence/src/test/java/com/spotify/confidence/ConfidenceProducerIntegrationTest.kt b/Confidence/src/test/java/com/spotify/confidence/ConfidenceProducerIntegrationTest.kt new file mode 100644 index 00000000..6120f033 --- /dev/null +++ b/Confidence/src/test/java/com/spotify/confidence/ConfidenceProducerIntegrationTest.kt @@ -0,0 +1,65 @@ +package com.spotify.confidence + +import com.spotify.confidence.Update.Event +import com.spotify.confidence.fakes.FakeDiskStorage +import com.spotify.confidence.fakes.FakeEventSenderEngine +import com.spotify.confidence.fakes.FakeFlagApplierClient +import com.spotify.confidence.fakes.FakeFlagResolver +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Before +import org.junit.Test + +class ConfidenceProducerIntegrationTest { + + private lateinit var engine: FakeEventSenderEngine + + @Before + fun setUp() { + engine = FakeEventSenderEngine() + } + + class MyProducer : Producer { + private val flow = MutableSharedFlow() + override fun updates(): Flow = flow + override fun stop() {} + suspend fun emitContextChange(map: Map) = flow.emit(Update.ContextUpdate(map)) + suspend fun emitEvent(event: Event) = flow.emit(event) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun testSomething() = runTest { + val testDispatcher = UnconfinedTestDispatcher(testScheduler) +// val testDispatcher = StandardTestDispatcher(testScheduler) //does not work... why? + val producerUnderTest = MyProducer() + val confidence = Confidence( + clientSecret = "secret", + dispatcher = testDispatcher, + diskStorage = FakeDiskStorage(), + eventSenderEngine = engine, + flagResolver = FakeFlagResolver(), + debugLogger = null, + flagApplierClient = FakeFlagApplierClient() + ) + + confidence.track(producerUnderTest) + + producerUnderTest.emitContextChange( + mapOf("key" to ConfidenceValue.String("value")) + ) + producerUnderTest.emitEvent( + Event("event", mapOf("key" to ConfidenceValue.String("value"))) + ) + + Assert.assertEquals("value", (confidence.getContext()["key"] as? ConfidenceValue.String)?.string) + Assert.assertEquals(1, engine.list.size) + Assert.assertEquals(ConfidenceValue.String("value"), engine.list.first().third["key"]) + + confidence.stop() + } +} \ No newline at end of file diff --git a/Confidence/src/test/java/com/spotify/confidence/DebugLoggerImplTest.kt b/Confidence/src/test/java/com/spotify/confidence/DebugLoggerImplTest.kt index 63a379da..12e1a298 100644 --- a/Confidence/src/test/java/com/spotify/confidence/DebugLoggerImplTest.kt +++ b/Confidence/src/test/java/com/spotify/confidence/DebugLoggerImplTest.kt @@ -72,7 +72,7 @@ class DebugLoggerImplTest { @Test fun logContextLogsOnVerboseLevel() { verboseLogger.logContext("action", mapOf("key" to ConfidenceValue.String("value"))) - verify { Log.v("Confidence", "[action] {key=String(string=value)}") } + verify { Log.v("Confidence", "[action] {key=value}") } } @Test diff --git a/Confidence/src/test/java/com/spotify/confidence/fakes/FakeDiskStorage.kt b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeDiskStorage.kt new file mode 100644 index 00000000..74063237 --- /dev/null +++ b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeDiskStorage.kt @@ -0,0 +1,25 @@ +package com.spotify.confidence.fakes + +import com.spotify.confidence.FlagResolution +import com.spotify.confidence.apply.ApplyInstance +import com.spotify.confidence.cache.DiskStorage + +class FakeDiskStorage() : DiskStorage { + override fun store(flagResolution: FlagResolution) { + TODO("Not yet implemented") + } + + override fun read(): FlagResolution = FlagResolution.EMPTY + + override fun clear() { + TODO("Not yet implemented") + } + + override fun writeApplyData(applyData: Map>) { + TODO("Not yet implemented") + } + + override fun readApplyData(): MutableMap> { + return emptyMap>().toMutableMap() + } +} \ No newline at end of file diff --git a/Confidence/src/test/java/com/spotify/confidence/fakes/FakeEventSenderEngine.kt b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeEventSenderEngine.kt new file mode 100644 index 00000000..a89a08d0 --- /dev/null +++ b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeEventSenderEngine.kt @@ -0,0 +1,26 @@ +package com.spotify.confidence.fakes + +import com.spotify.confidence.ConfidenceFieldsType +import com.spotify.confidence.ConfidenceValue +import com.spotify.confidence.EventSenderEngine +import kotlinx.coroutines.channels.Channel +import java.io.File + +class FakeEventSenderEngine() : EventSenderEngine { + + val list: MutableList>> = mutableListOf() + + override fun onLowMemoryChannel(): Channel> { + TODO("Not yet implemented") + } + + override fun emit(eventName: String, data: ConfidenceFieldsType, context: Map) { + list.add(Triple(eventName, data, context)) + } + + override fun flush() { + } + + override fun stop() { + } +} \ No newline at end of file diff --git a/Confidence/src/test/java/com/spotify/confidence/fakes/FakeFlagApplierClient.kt b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeFlagApplierClient.kt new file mode 100644 index 00000000..cf1d5d76 --- /dev/null +++ b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeFlagApplierClient.kt @@ -0,0 +1,11 @@ +package com.spotify.confidence.fakes + +import com.spotify.confidence.Result +import com.spotify.confidence.client.AppliedFlag +import com.spotify.confidence.client.FlagApplierClient + +class FakeFlagApplierClient : FlagApplierClient { + override suspend fun apply(flags: List, resolveToken: String): Result { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/Confidence/src/test/java/com/spotify/confidence/fakes/FakeFlagResolver.kt b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeFlagResolver.kt new file mode 100644 index 00000000..8db46407 --- /dev/null +++ b/Confidence/src/test/java/com/spotify/confidence/fakes/FakeFlagResolver.kt @@ -0,0 +1,12 @@ +package com.spotify.confidence.fakes + +import com.spotify.confidence.ConfidenceValue +import com.spotify.confidence.FlagResolution +import com.spotify.confidence.FlagResolver +import com.spotify.confidence.Result + +class FakeFlagResolver : FlagResolver { + override suspend fun resolve(flags: List, context: Map): Result { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/ConfidenceDemoApp/build.gradle.kts b/ConfidenceDemoApp/build.gradle.kts index 1ae4167c..b88a87f2 100644 --- a/ConfidenceDemoApp/build.gradle.kts +++ b/ConfidenceDemoApp/build.gradle.kts @@ -84,6 +84,7 @@ android { dependencies { implementation(project(":Confidence")) + implementation("androidx.lifecycle:lifecycle-process:2.6.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}") implementation( "androidx.compose.runtime:runtime-livedata:${Versions.liveData}") implementation( "androidx.core:core-ktx:${Versions.core}") diff --git a/Confidence/src/main/java/com/spotify/confidence/AndroidLifecycleEventProducer.kt b/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/AndroidLifecycleEventProducer.kt similarity index 89% rename from Confidence/src/main/java/com/spotify/confidence/AndroidLifecycleEventProducer.kt rename to ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/AndroidLifecycleEventProducer.kt index 0a6dac41..1194343b 100644 --- a/Confidence/src/main/java/com/spotify/confidence/AndroidLifecycleEventProducer.kt +++ b/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/AndroidLifecycleEventProducer.kt @@ -1,4 +1,4 @@ -package com.spotify.confidence +package com.example.confidencedemoapp import android.app.Activity import android.app.Application @@ -14,19 +14,21 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner +import com.spotify.confidence.ConfidenceValue +import com.spotify.confidence.Producer +import com.spotify.confidence.Update +import com.spotify.confidence.Update.Event import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch class AndroidLifecycleEventProducer( private val application: Application, private val trackActivities: Boolean -) : Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver, EventProducer { +) : Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver, Producer { private val eventsFlow = MutableSharedFlow() - private val contextFlow = MutableStateFlow>(mapOf()) private val sharedPreferences by lazy { application.getSharedPreferences("CONFIDENCE_EVENTS", Context.MODE_PRIVATE) } @@ -58,13 +60,6 @@ class AndroidLifecycleEventProducer( trackApplicationLifecycleEvents() } - @Synchronized - private fun updateContext(map: Map) { - val context = contextFlow.value.toMutableMap() - context += map - contextFlow.value = context - } - override fun onActivityStarted(activity: Activity) { activity.trackActivity("activity-started") } @@ -125,12 +120,6 @@ class AndroidLifecycleEventProducer( val currentVersion = ConfidenceValue.String(packageInfo?.versionName ?: "") val currentBuild = ConfidenceValue.String(packageInfo?.getVersionCode().toString() ?: "") - val addedContext = mapOf( - APP_VERSION_KEY to currentVersion, - APP_BUILD_KEY to currentBuild - ) - updateContext(addedContext) - val previousBuild: ConfidenceValue.String? = sharedPreferences .getString(APP_BUILD, null) ?.let(ConfidenceValue::String) @@ -154,9 +143,8 @@ class AndroidLifecycleEventProducer( } } - override fun events(): Flow = eventsFlow + override fun updates(): Flow = eventsFlow - override fun contextChanges(): Flow> = contextFlow override fun stop() { if (trackActivities) { application.unregisterActivityLifecycleCallbacks(this) @@ -172,10 +160,6 @@ class AndroidLifecycleEventProducer( private const val APP_BUILD = "APP_BUILD" private const val LEGACY_APP_BUILD = "LEGACY_APP_BUILD" - // Context keys - private const val APP_VERSION_KEY = "app_version" - private const val APP_BUILD_KEY = "app_build" - // Event keys private const val APP_INSTALLED_EVENT = "app-installed" private const val APP_UPDATED_EVENT = "app-updated" diff --git a/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt b/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt index ce125db6..d55f712f 100644 --- a/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt +++ b/ConfidenceDemoApp/src/main/java/com/example/confidencedemoapp/MainVm.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import com.spotify.confidence.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.Date @@ -48,10 +49,20 @@ class MainVm(app: Application) : AndroidViewModel(app) { loggingLevel = LoggingLevel.VERBOSE ) confidence.track(AndroidLifecycleEventProducer(getApplication(), false)) + confidence.track( + ConfidenceDeviceInfoContextProducer( + applicationContext = getApplication(), + withAppInfo = true, + withOsInfo = true, + withDeviceInfo = true, + withLocale = true + ) + ) + eventSender = confidence.withContext(mutableMap) viewModelScope.launch { - if(confidence.isStorageEmpty()) { + if (confidence.isStorageEmpty()) { confidence.fetchAndActivate() } else { confidence.activate() @@ -77,8 +88,11 @@ class MainVm(app: Application) : AndroidViewModel(app) { }.toComposeColor() _message.postValue(messageValue) _color.postValue(colorFlag) - _surfaceText.postValue(confidence.getContext().entries.map { "${it.key}=${it.value}"}.joinToString { it }) - eventSender.track("navigate", mapOf("my_date" to ConfidenceValue.Date(Date()), "my_time" to ConfidenceValue.Timestamp(Date()))) + _surfaceText.postValue(confidence.getContext().entries.map { "${it.key}=${it.value}" }.joinToString { it }) + eventSender.track( + "navigate", + mapOf("my_date" to ConfidenceValue.Date(Date()), "my_time" to ConfidenceValue.Timestamp(Date())) + ) } fun updateContext() { diff --git a/README.md b/README.md index a0a23fde..8ad8cf4f 100644 --- a/README.md +++ b/README.md @@ -94,16 +94,19 @@ All `context` data set on the `Confidence` instance will be appended to the even confidence.track("button-tapped", mapOf("button_id" to ConfidenceValue.String("purchase_button"))) ``` -The Confidence SDK has support for `EventProducer`. This is a way to programmatically emit context changes and events into streams +The Confidence SDK has support for `Producer`s. This is a way for SDK integrators to programmatically emit context changes and events into streams which can be consumed by the SDK to automatically emit events or to automatically update context data. -The Confidence SDK comes with a pre-defined event producer to emit some application lifecycle events: `AndroidLifecycleEventProducer`. To use it: +The Confidence SDK comes with a pre-defined producer to set some context data: `ConfidenceDeviceInfoContextProducer`. To use it: ```kotlin -import com.spotify.confidence.AndroidLifecycleEventProducer +import com.spotify.confidence.ConfidenceDeviceInfoContextProducer confidence.track( - AndroidLifecycleEventProducer( - application = getApplication(), - trackActivities = false // or true + ConfidenceDeviceInfoContextProducer( + applicationContext = getApplication(), + withAppInfo = true, // defaults to false + withOsInfo = true, // defaults to false + withDeviceInfo = true, // defaults to false + withLocale = true // defaults to false ) ) ```