Skip to content

Commit

Permalink
feat!: refactor context provider (#191)
Browse files Browse the repository at this point in the history
* refactor!: move app lifecycle producer to demo app

* feat!: split EventProducers and ContextProducers

* feat: add ConfidenceDeviceInfoContextProducer

* fixup! feat: add ConfidenceDeviceInfoContextProducer

* fix: restructure context

* fixup! fix: restructure context

* fix!: single flow for producer

* refactor: clean up API + add readme
  • Loading branch information
nicklasl authored Jan 30, 2025
1 parent 97c763c commit 37c243e
Show file tree
Hide file tree
Showing 19 changed files with 453 additions and 134 deletions.
107 changes: 60 additions & 47 deletions Confidence/api/Confidence.api
Original file line number Diff line number Diff line change
@@ -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 <init> (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
Expand All @@ -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;
Expand All @@ -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 <init> (Landroid/content/Context;ZZZZ)V
public synthetic fun <init> (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 <init> ()V
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 <init> (Ljava/lang/String;Ljava/util/Map;Z)V
public synthetic fun <init> (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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <init> (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 <init> (Ljava/lang/String;Ljava/util/Map;Z)V
public synthetic fun <init> (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 <init> (ILjava/util/Date;Lcom/spotify/confidence/apply/EventStatus;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V
Expand Down
1 change: 0 additions & 1 deletion Confidence/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down
36 changes: 17 additions & 19 deletions Confidence/src/main/java/com/spotify/confidence/Confidence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Confidence internal constructor(
private var currentFetchJob: Job? = null

private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
private val eventProducers: MutableList<EventProducer> = mutableListOf()
private val producers: MutableList<Producer> = mutableListOf()

private val flagApplier = FlagApplierWithRetries(
client = flagApplierClient,
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ConfidenceValue>()
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<Update> = 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()
}
Loading

0 comments on commit 37c243e

Please sign in to comment.