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

RUM-7797 Anonymous RUM Identifier #2487

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion dd-sdk-android-core/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ data class com.datadog.android.api.context.ProcessInfo
data class com.datadog.android.api.context.TimeInfo
constructor(Long, Long, Long, Long)
data class com.datadog.android.api.context.UserInfo
constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, Map<kotlin.String, kotlin.Any?> = emptyMap())
constructor(kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, Map<kotlin.String, kotlin.Any?> = emptyMap())
interface com.datadog.android.api.feature.Feature
val name: String
fun onInitialize(android.content.Context)
Expand Down Expand Up @@ -123,6 +123,7 @@ interface com.datadog.android.api.feature.FeatureSdkCore : com.datadog.android.a
fun removeEventReceiver(String)
fun createSingleThreadExecutorService(String): java.util.concurrent.ExecutorService
fun createScheduledExecutorService(String): java.util.concurrent.ScheduledExecutorService
fun setAnonymousId(java.util.UUID?)
interface com.datadog.android.api.feature.StorageBackedFeature : Feature
val requestFactory: com.datadog.android.api.net.RequestFactory
val storageConfiguration: com.datadog.android.api.storage.FeatureStorageConfiguration
Expand Down
13 changes: 8 additions & 5 deletions dd-sdk-android-core/api/dd-sdk-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -281,18 +281,20 @@ public final class com/datadog/android/api/context/TimeInfo {

public final class com/datadog/android/api/context/UserInfo {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/api/context/UserInfo;
public static synthetic fun copy$default (Lcom/datadog/android/api/context/UserInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/api/context/UserInfo;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/util/Map;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/api/context/UserInfo;
public static synthetic fun copy$default (Lcom/datadog/android/api/context/UserInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/api/context/UserInfo;
public fun equals (Ljava/lang/Object;)Z
public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/api/context/UserInfo;
public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/api/context/UserInfo;
public final fun getAdditionalProperties ()Ljava/util/Map;
public final fun getAnonymousId ()Ljava/lang/String;
public final fun getEmail ()Ljava/lang/String;
public final fun getId ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
Expand Down Expand Up @@ -355,6 +357,7 @@ public abstract interface class com/datadog/android/api/feature/FeatureSdkCore :
public abstract fun registerFeature (Lcom/datadog/android/api/feature/Feature;)V
public abstract fun removeContextUpdateReceiver (Ljava/lang/String;Lcom/datadog/android/api/feature/FeatureContextUpdateReceiver;)V
public abstract fun removeEventReceiver (Ljava/lang/String;)V
public abstract fun setAnonymousId (Ljava/util/UUID;)V
public abstract fun setContextUpdateReceiver (Ljava/lang/String;Lcom/datadog/android/api/feature/FeatureContextUpdateReceiver;)V
public abstract fun setEventReceiver (Ljava/lang/String;Lcom/datadog/android/api/feature/FeatureEventReceiver;)V
public abstract fun updateFeatureContext (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ import kotlin.jvm.Throws

/**
* Holds information about the current User.
* @property anonymousId a unique anonymous identifier for the device, or null
* @property id a unique identifier for the user, or null
* @property name the name of the user, or null
* @property email the email address of the user, or null
* @property additionalProperties a dictionary of custom properties attached to the current user
*/
data class UserInfo(
val anonymousId: String? = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of naming, should we make it more explicit that this is an identifier of the device between sessions? anonymousDeviceId?

Copy link
Member Author

@maciejburda maciejburda Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that conceptually browser SDK team decided to avoid that. Initially they used deviceId and they went for more generic naming after a couple of iterations.

val id: String? = null,
val name: String? = null,
val email: String? = null,
Expand All @@ -37,6 +39,9 @@ data class UserInfo(
@Suppress("StringLiteralDuplication")
internal fun toJson(): JsonElement {
val json = JsonObject()
anonymousId?.let { idNonNull ->
json.addProperty("anonymous_id", idNonNull)
}
id?.let { idNonNull ->
json.addProperty("id", idNonNull)
}
Expand Down Expand Up @@ -79,6 +84,7 @@ data class UserInfo(
@Suppress("StringLiteralDuplication", "ThrowsCount")
fun fromJsonObject(jsonObject: JsonObject): UserInfo {
try {
val anonymousId = jsonObject.get("anonymous_id")?.asString
val id = jsonObject.get("id")?.asString
val name = jsonObject.get("name")?.asString
val email = jsonObject.get("email")?.asString
Expand All @@ -88,7 +94,7 @@ data class UserInfo(
additionalProperties[entry.key] = entry.value
}
}
return UserInfo(id, name, email, additionalProperties)
return UserInfo(anonymousId, id, name, email, additionalProperties)
} catch (e: IllegalStateException) {
throw JsonParseException(
"Unable to parse json into type UserInfo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package com.datadog.android.api.feature

import com.datadog.android.api.InternalLogger
import com.datadog.android.api.SdkCore
import java.util.UUID
import java.util.concurrent.ExecutorService
import java.util.concurrent.ScheduledExecutorService

Expand All @@ -16,6 +17,7 @@ import java.util.concurrent.ScheduledExecutorService
*
* SDK core is always guaranteed to implement this interface.
*/
@Suppress("TooManyFunctions")
interface FeatureSdkCore : SdkCore {

/**
Expand Down Expand Up @@ -104,4 +106,11 @@ interface FeatureSdkCore : SdkCore {
* @param executorContext Context to be used for logging and naming threads running on this executor.
*/
fun createScheduledExecutorService(executorContext: String): ScheduledExecutorService

/**
* Allows the given feature to set the anonymous ID for the SDK.
*
* @param anonymousId Anonymous ID to set.
*/
fun setAnonymousId(anonymousId: UUID?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import com.datadog.android.privacy.TrackingConsent
import com.google.gson.JsonObject
import java.io.File
import java.util.Locale
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.ScheduledExecutorService
Expand Down Expand Up @@ -268,6 +269,10 @@ internal class DatadogCore(
return coreFeature.createScheduledExecutorService(executorContext)
}

override fun setAnonymousId(anonymousId: UUID?) {
coreFeature.userInfoProvider.setAnonymousId(anonymousId.toString())
}

override fun isCoreActive(): Boolean = isActive

// endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal class NoOpContextProvider : ContextProvider {
cellularTechnology = null
),
deviceInfo = DeviceInfo("", "", "", DeviceType.OTHER, "", "", "", "", ""),
userInfo = UserInfo(null, null, null, emptyMap()),
userInfo = UserInfo(null, null, null, null, emptyMap()),
trackingConsent = TrackingConsent.NOT_GRANTED,
appBuildId = null,
featuresContext = emptyMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver
import com.datadog.android.privacy.TrackingConsent
import com.google.gson.JsonObject
import java.io.File
import java.util.UUID
import java.util.concurrent.Callable
import java.util.concurrent.Delayed
import java.util.concurrent.ExecutionException
Expand Down Expand Up @@ -128,6 +129,8 @@ internal object NoOpInternalSdkCore : InternalSdkCore {
return NoOpScheduledExecutorService()
}

override fun setAnonymousId(anonymousId: UUID?) = Unit

// endregion

// region InternalSdkCore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ internal class DatadogUserInfoProvider(
)
}

override fun setAnonymousId(id: String?) {
internalUserInfo = internalUserInfo.copy(
anonymousId = id
)
}

override fun addUserProperties(properties: Map<String, Any?>) {
internalUserInfo = internalUserInfo.copy(
additionalProperties = internalUserInfo.additionalProperties + properties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,7 @@ internal interface MutableUserInfoProvider : UserInfoProvider {
extraInfo: Map<String, Any?>
)

fun setAnonymousId(id: String?)

fun addUserProperties(properties: Map<String, Any?>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
import java.util.Collections
import java.util.Locale
import java.util.UUID
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -228,6 +229,22 @@ internal class DatadogCoreTest {
verify(mockUserInfoProvider).setUserInfo(id, name, email, fakeUserProperties)
}

@Test
fun `M update anonymousId W setAnonymousId()`(
forge: Forge
) {
// Given
val uuid = forge.getForgery<UUID>()
val mockUserInfoProvider = mock<MutableUserInfoProvider>()
testedCore.coreFeature.userInfoProvider = mockUserInfoProvider

// When
testedCore.setAnonymousId(uuid)

// Then
verify(mockUserInfoProvider).setAnonymousId(uuid.toString())
}

@Test
fun `M set additional user info W addUserProperties() is called`(
@StringForgery(type = StringForgeryType.HEXADECIMAL) id: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,92 @@ internal class DatadogUserInfoProviderTest {
assertThat(testedProvider.getUserInfo().additionalProperties).isEqualTo(fakeExpectedUserProperties)
}

@Test
fun `M enriches empty user info with anonymousId W setAnonymousId`(
@StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String
) {
// When
testedProvider.setAnonymousId(anonymousId)

// Then
testedProvider.getUserInfo().let {
assertThat(it.anonymousId).isEqualTo(anonymousId)
assertThat(it.id).isNull()
assertThat(it.name).isNull()
assertThat(it.email).isNull()
assertThat(it.additionalProperties).isEmpty()
}
}

@Test
fun `M enriches existing user info with anonymousId W setAnonymousId`(
@StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String,
@StringForgery(type = StringForgeryType.HEXADECIMAL) id: String,
@StringForgery name: String,
@StringForgery(regex = "\\w+@\\w+") email: String,
@MapForgery(
key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]),
value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)])
) fakeUserProperties: Map<String, String>
) {
// Given
testedProvider.setUserInfo(id, name, email, fakeUserProperties)

// When
testedProvider.setAnonymousId(anonymousId)

// Then
testedProvider.getUserInfo().let {
assertThat(it.anonymousId).isEqualTo(anonymousId)
assertThat(it.id).isEqualTo(id)
assertThat(it.name).isEqualTo(name)
assertThat(it.email).isEqualTo(email)
assertThat(it.additionalProperties).isEqualTo(fakeUserProperties)
}
}

@Test
fun `M clears the anonymousId W setAnonymousId { null }`(
@StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String
) {
// Given
testedProvider.setAnonymousId(anonymousId)

// When
testedProvider.setAnonymousId(null)

// Then
assertThat(testedProvider.getUserInfo().anonymousId).isNull()
}

@Test
fun `M clears the anonymousId and keeps existing user info W setAnonymousId { null }`(
@StringForgery(type = StringForgeryType.HEXADECIMAL) anonymousId: String,
@StringForgery(type = StringForgeryType.HEXADECIMAL) id: String,
@StringForgery name: String,
@StringForgery(regex = "\\w+@\\w+") email: String,
@MapForgery(
key = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)]),
value = AdvancedForgery(string = [StringForgery(StringForgeryType.ALPHA_NUMERICAL)])
) fakeUserProperties: Map<String, String>
) {
// Given
testedProvider.setUserInfo(id, name, email, fakeUserProperties)
testedProvider.setAnonymousId(anonymousId)

// When
testedProvider.setAnonymousId(null)

// Then
testedProvider.getUserInfo().let {
assertThat(it.anonymousId).isNull()
assertThat(it.id).isEqualTo(id)
assertThat(it.name).isEqualTo(name)
assertThat(it.email).isEqualTo(email)
assertThat(it.additionalProperties).isEqualTo(fakeUserProperties)
}
}

@Test
fun `M use immutable values W setUserInfo { removing properties }()`(
forge: Forge,
Expand Down
5 changes: 5 additions & 0 deletions features/dd-sdk-android-rum/api/dd-sdk-android-rum.api
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ public final class com/datadog/android/rum/RumActionType : java/lang/Enum {
public static fun values ()[Lcom/datadog/android/rum/RumActionType;
}

public final class com/datadog/android/rum/RumAnonymousIdentifierManager {
public static final field INSTANCE Lcom/datadog/android/rum/RumAnonymousIdentifierManager;
public static final fun manageAnonymousId (ZLcom/datadog/android/api/storage/datastore/DataStoreHandler;Lcom/datadog/android/api/feature/FeatureSdkCore;)V
}

public final class com/datadog/android/rum/RumAttributes {
public static final field ACTION_GESTURE_DIRECTION Ljava/lang/String;
public static final field ACTION_GESTURE_FROM_STATE Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ object Rum {

sdkCore.registerFeature(rumFeature)

sdkCore.getFeature(rumFeature.name)?.dataStore?.let {
RumAnonymousIdentifierManager.manageAnonymousId(
rumConfiguration.featureConfiguration.trackAnonymousUser,
it,
sdkCore
)
}

val rumMonitor = createMonitor(sdkCore, rumFeature)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Expand Down
Loading