Skip to content

Commit

Permalink
Make all dd attribute sources reactive instead of individual coroutines
Browse files Browse the repository at this point in the history
This works around the issue over at
DataDog/dd-sdk-android#2132 which does not let
one call setUserInfo without having to also clear all previously set
attributes.
With this change, we instead keep a flow for all of our source of truth,
and combine them together so if any single one of them change, we
re-apply all of them together with one `setUserInfo` call.
We do the same for RUM too, all inside DatadogAttributesManager
  • Loading branch information
StylianosGakis committed Jul 23, 2024
1 parent 12a4231 commit f678ffb
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.hedvig.android.auth
import android.util.Base64
import com.hedvig.android.auth.storage.AuthTokenStorage
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
Expand All @@ -19,7 +20,7 @@ class MemberIdService(
authTokens?.accessToken?.token?.let { stringToken ->
extractMemberIdFromAccessToken(stringToken)
}
}
}.distinctUntilChanged()
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
Expand All @@ -31,8 +32,8 @@ internal class DeviceIdDataStoreImpl(
}
}

override fun observeDeviceId(): Flow<String> {
return dataStore.data.map { it[key] ?: "" }
override fun observeDeviceId(): Flow<String> { // todo return null instead of "" for uninitialized cases?
return dataStore.data.map { it[key] ?: "" }.distinctUntilChanged()
}

private suspend fun generateDeviceId() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

interface DemoManager {
Expand All @@ -19,7 +20,7 @@ internal class DataStoreDemoManager(
override fun isDemoMode(): Flow<Boolean> {
return dataStore.data.map {
it[demoModeKey] ?: false
}
}.distinctUntilChanged()
}

override suspend fun setDemoMode(demoMode: Boolean) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,16 @@ import com.datadog.android.trace.AndroidTracer
import com.datadog.android.trace.Trace
import com.datadog.android.trace.TraceConfiguration
import com.hedvig.android.core.buildconstants.HedvigBuildConstants
import com.hedvig.android.core.common.ApplicationScope
import com.hedvig.android.core.datastore.DeviceIdDataStore
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributesManager
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import io.opentracing.util.GlobalTracer
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import timber.log.Timber

// Used in /app/src/main/AndroidManifest.xml
abstract class DatadogInitializer : Initializer<Unit>, KoinComponent {
private val hedvigBuildConstants by inject<HedvigBuildConstants>()
private val deviceIdDataStore by inject<DeviceIdDataStore>()
private val applicationScope by inject<ApplicationScope>()
private val datadogAttributesManager by inject<DatadogAttributesManager>()

override fun create(context: Context) {
val clientToken = "pub185bcba7ed324e83d068b80e25a81359"
Expand Down Expand Up @@ -96,13 +88,5 @@ abstract class DatadogInitializer : Initializer<Unit>, KoinComponent {
.build()

Timber.plant(DatadogLoggingTree(logger))

applicationScope.launch {
val deviceId = deviceIdDataStore.observeDeviceId().first()
datadogAttributesManager.storeAttribute(DEVICE_ID_KEY, deviceId)
logcat(LogPriority.VERBOSE) { "Datadog stored device id attribute: $deviceId" }
}
}
}

private const val DEVICE_ID_KEY = "device_id"
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,52 @@ package com.hedvig.android.datadog.core.attributestracking

import com.datadog.android.Datadog
import com.datadog.android.rum.GlobalRumMonitor
import com.hedvig.android.core.common.ApplicationScope
import com.hedvig.android.initializable.Initializable
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch

interface DatadogAttributesManager {
fun storeAttribute(key: String, value: Any?)

fun deleteAttribute(key: String)
interface DatadogMemberIdProvider {
/**
* The implementation must return something, even null, immediatelly. To avoid blocking the rest of the attributes
* from being read in the [combine] which combines all of them together
*/
fun provide(): Flow<Pair<String, String?>>
}

internal class DatadogAttributesManagerImpl : DatadogAttributesManager {
override fun storeAttribute(key: String, value: Any?) {
val sdkCore = Datadog.getInstance()
sdkCore.addUserProperties(mapOf(key to value))
GlobalRumMonitor.get(sdkCore).addAttribute(key, value)
}
interface DatadogAttributeProvider {
/**
* The implementation must return something, even null, immediatelly. To avoid blocking the rest of the attributes
* from being read in the [combine] which combines all of them together
*/
fun provide(): Flow<Pair<String, Any?>>
}

override fun deleteAttribute(key: String) {
internal class DatadogAttributesManager(
private val applicationScope: ApplicationScope,
private val memberIdProvider: DatadogMemberIdProvider,
private val attributeProviders: Set<DatadogAttributeProvider>,
) : Initializable {
override fun initialize() {
val sdkCore = Datadog.getInstance()
sdkCore.addUserProperties(mapOf(key to null))
GlobalRumMonitor.get(sdkCore).removeAttribute(key)
val rumMonitor = GlobalRumMonitor.get(sdkCore)
applicationScope.launch {
combine(
memberIdProvider.provide(),
combine(attributeProviders.map { it.provide() }) { it.toMap() },
) { memberId: Pair<String, String?>, attributes: Map<String, Any?> ->
memberId to attributes
}.collect { (memberIdAttribute, attributes): Pair<Pair<String, String?>, Map<String, Any?>> ->
logcat(LogPriority.VERBOSE) { "DatadogAttributesManager storing: $memberIdAttribute, $attributes" }
sdkCore.setUserInfo(id = memberIdAttribute.second, name = null, email = null, extraInfo = attributes)
rumMonitor.addAttribute(memberIdAttribute.first, memberIdAttribute.second)
for (attribute in attributes) {
rumMonitor.addAttribute(attribute.key, attribute.value)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.hedvig.android.datadog.core.attributestracking

import com.hedvig.android.core.datastore.DeviceIdDataStore
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

internal class DeviceIdProvider(
private val deviceIdDataStore: DeviceIdDataStore,
) : DatadogAttributeProvider {
override fun provide(): Flow<Pair<String, Any?>> {
return deviceIdDataStore
.observeDeviceId()
.onEach { deviceId ->
logcat(LogPriority.VERBOSE) { "Datadog stored device id attribute: $deviceId" }
}
.map { deviceId ->
DEVICE_ID_KEY to deviceId
}
}

companion object {
private const val DEVICE_ID_KEY = "device_id"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.hedvig.android.datadog.core.attributestracking

import com.hedvig.android.auth.MemberIdService
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach

internal class DatadogMemberIdProviderImpl(
private val memberIdService: MemberIdService,
) : DatadogMemberIdProvider {
override fun provide(): Flow<Pair<String, String?>> {
return memberIdService
.getMemberId()
.map { MEMBER_ID_TRACKING_KEY to it }
.onEach { (key, memberId) ->
logcat(LogPriority.INFO) {
if (memberId == null) {
"Removing from global RUM attribute:$MEMBER_ID_TRACKING_KEY"
} else {
"Appending to global RUM attribute:$MEMBER_ID_TRACKING_KEY = $memberId"
}
}
}
}

companion object {
/**
* Key to be passed to [com.hedvig.android.datadog.core.attributestracking.DatadogAttributesManager] to track the
* member ID after we've logged into the app
*/
private const val MEMBER_ID_TRACKING_KEY = "member_id"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,32 @@ package com.hedvig.android.datadog.core.di

import com.hedvig.android.auth.MemberIdService
import com.hedvig.android.core.common.ApplicationScope
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributeProvider
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributesManager
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributesManagerImpl
import com.hedvig.android.datadog.core.memberid.DatadogMemberIdUpdater
import com.hedvig.android.datadog.core.attributestracking.DatadogMemberIdProvider
import com.hedvig.android.datadog.core.attributestracking.DatadogMemberIdProviderImpl
import com.hedvig.android.datadog.core.attributestracking.DeviceIdProvider
import com.hedvig.android.initializable.Initializable
import io.opentracing.Tracer
import io.opentracing.util.GlobalTracer
import org.koin.dsl.bind
import org.koin.dsl.module

val datadogModule = module {
single<DatadogMemberIdUpdater> {
DatadogMemberIdUpdater(
get<DatadogAttributesManager>(),
get<MemberIdService>(),
single<Tracer> { GlobalTracer.get() }

single<DatadogMemberIdProvider> {
DatadogMemberIdProviderImpl(get<MemberIdService>())
}
single<DeviceIdProvider> {
DeviceIdProvider(get())
} bind DatadogAttributeProvider::class

single<DatadogAttributesManager> {
DatadogAttributesManager(
get<ApplicationScope>(),
get<DatadogMemberIdProvider>(),
getAll<DatadogAttributeProvider>().toSet(),
)
} bind Initializable::class
single<Tracer> { GlobalTracer.get() }
single<DatadogAttributesManager> { DatadogAttributesManagerImpl() }
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
package com.hedvig.android.datadog.demo.tracking

import com.hedvig.android.core.common.ApplicationScope
import com.hedvig.android.core.demomode.DemoManager
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributesManager
import com.hedvig.android.initializable.Initializable
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributeProvider
import com.hedvig.android.logger.LogPriority
import com.hedvig.android.logger.logcat
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class DatadogDemoModeTracking(
private val applicationScope: ApplicationScope,
private val demoManager: DemoManager,
private val datadogAttributesManager: DatadogAttributesManager,
) : Initializable {
override fun initialize() {
applicationScope.launch {
demoManager.isDemoMode().collect { isDemoMode ->
logcat(LogPriority.INFO) { "Demo mode changed to:$isDemoMode" }
datadogAttributesManager.storeAttribute(IS_DEMO_MODE_TRACKING_KEY, isDemoMode)
}
) : DatadogAttributeProvider {
override fun provide(): Flow<Pair<String, Any?>> {
return demoManager.isDemoMode().map { isDemoMode ->
logcat(LogPriority.INFO) { "Demo mode changed to:$isDemoMode" }
IS_DEMO_MODE_TRACKING_KEY to isDemoMode
}
}
}

private const val IS_DEMO_MODE_TRACKING_KEY = "is_demo_mode"
companion object {
private const val IS_DEMO_MODE_TRACKING_KEY = "is_demo_mode"
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.hedvig.android.datadog.demo.tracking.di

import com.hedvig.android.core.common.ApplicationScope
import com.hedvig.android.core.demomode.DemoManager
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributesManager
import com.hedvig.android.datadog.core.attributestracking.DatadogAttributeProvider
import com.hedvig.android.datadog.demo.tracking.DatadogDemoModeTracking
import com.hedvig.android.initializable.Initializable
import org.koin.dsl.bind
import org.koin.dsl.module

val datadogDemoTrackingModule = module {
single<DatadogDemoModeTracking> {
DatadogDemoModeTracking(get<ApplicationScope>(), get<DemoManager>(), get<DatadogAttributesManager>())
} bind Initializable::class
DatadogDemoModeTracking(get<DemoManager>())
} bind DatadogAttributeProvider::class
}

0 comments on commit f678ffb

Please sign in to comment.