Skip to content
Draft
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 @@ -252,8 +252,12 @@ internal class DatadogCore(
feature.featureContextLock.writeLock().safeTryWithLock(1, TimeUnit.SECONDS, internalLogger) {
val currentContext = feature.featureContext
updateCallback(currentContext)
featureContextUpdateReceivers.forEach {
it.onContextUpdate(featureName, currentContext)
if (featureContextUpdateReceivers.isNotEmpty()) {
@Suppress("UnsafeThirdPartyFunctionCall") // currentContext is non-null
val contextSnapshot = HashMap(currentContext)
Comment on lines +256 to +257

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is it just for an additional guard? onContextUpdate argument for the context is of Map type, so it is not mutable unless receiver tries to cast to the mutable map explicitly (unlikely, I think).

as a price to pay - extra object allocation and iteration to copy.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Map prevents writes through that reference, but the producer still mutates the underlying object. Session replay retains that reference, for example, so subsequent updates change its previous context too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

but the producer still mutates the underlying object. Session replay retains that reference, for example, so subsequent updates change its previous context too.

can you point to the code you are referencing? HashMap(currentContext) is also only top-level copy, not the deep one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

SessionReplayRumContextProvider.onContextUpdate() retains the map with rumContext = context. On the next update, publishRumContext() clears and repopulates that same map before notifying receivers, so previousViewId already contains the new value and the view change is missed.

Yes it's a shallow snapshot. I think it's enough for this issue because the IDs are immutable strings and the producer replaces top-level entries.

featureContextUpdateReceivers.forEach {
it.onContextUpdate(featureName, contextSnapshot)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.times
Expand Down Expand Up @@ -422,6 +423,42 @@ internal class DatadogCoreTest {
verify(mockFeatureContextUpdateListener).onContextUpdate(feature, fakeContext)
}

@Test
fun `M publish independent snapshots W updateFeatureContext()`(
@StringForgery fakeFeatureName: String,
@StringForgery fakeKey: String,
@StringForgery fakeFirstValue: String,
@StringForgery fakeSecondValue: String
) {
// Given
val mockFeature =
mock<SdkFeature>().apply {
whenever(featureContextLock) doReturn ReentrantReadWriteLock()
whenever(featureContext) doReturn mutableMapOf<String, Any?>()
}
testedCore.features[fakeFeatureName] = mockFeature
val mockReceiver = mock<FeatureContextUpdateReceiver>()
testedCore.setContextUpdateReceiver(mockReceiver)

// When
testedCore.updateFeatureContext(fakeFeatureName, useContextThread = false) {
it[fakeKey] = fakeFirstValue
}
testedCore.updateFeatureContext(fakeFeatureName, useContextThread = false) {
it.clear()
it[fakeKey] = fakeSecondValue
}

// Then
argumentCaptor<Map<String, Any?>> {
verify(mockReceiver, times(2)).onContextUpdate(eq(fakeFeatureName), capture())
assertThat(firstValue).isEqualTo(mapOf(fakeKey to fakeFirstValue))
assertThat(secondValue).isEqualTo(mapOf(fakeKey to fakeSecondValue))
assertThat(firstValue).isNotSameAs(mockFeature.featureContext)
assertThat(secondValue).isNotSameAs(mockFeature.featureContext)
}
}

@Test
fun `M update feature context W updateFeatureContext() { concurrent access }`(
@StringForgery feature: String,
Expand Down
5 changes: 3 additions & 2 deletions dd-sdk-android-internal/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,15 @@ enum com.datadog.android.internal.profiling.quota.QuotaReason
- TIMEOUT
- API_ERROR
data class com.datadog.android.internal.rum.RumContext
constructor(String = NULL_UUID, String = NULL_UUID, Boolean = false, String? = null, String? = null, String? = null, String? = null, RumSessionState = RumSessionState.NOT_TRACKED, RumSessionStartReason = RumSessionStartReason.USER_APP_LAUNCH, RumViewType = RumViewType.NONE, String? = null, String? = null, Long = 0L, Long = 0L, Boolean = false, Boolean = false, Float = SAMPLE_ALL_RATE)
constructor(String, RumSessionContext? = null, String? = null, String? = null, String? = null, String? = null, RumViewType = RumViewType.NONE, String? = null, String? = null, Long = 0L, Long = 0L, Boolean = false)
companion object
const val NULL_UUID: String
const val SAMPLE_ALL_RATE: Float
fun Map<String, Any?>.rumContext(): RumContext?
object com.datadog.android.internal.rum.RumSessionConstants
const val EMPTY_RUM_SESSION_ID: String
const val TRACKED_SESSION_STATE: String
data class com.datadog.android.internal.rum.RumSessionContext
constructor(String, RumSessionState, RumSessionStartReason, Boolean, Float, Boolean)
enum com.datadog.android.internal.rum.RumSessionStartReason
constructor(String)
- USER_APP_LAUNCH
Expand Down
59 changes: 34 additions & 25 deletions dd-sdk-android-internal/api/dd-sdk-android-internal.api
Original file line number Diff line number Diff line change
Expand Up @@ -494,37 +494,27 @@ public final class com/datadog/android/internal/rum/RumContext {
public static final field Companion Lcom/datadog/android/internal/rum/RumContext$Companion;
public static final field NULL_UUID Ljava/lang/String;
public static final field SAMPLE_ALL_RATE F
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZZF)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZZFILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZ)V
public synthetic fun <init> (Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/String;
public final fun component10 ()Lcom/datadog/android/internal/rum/RumViewType;
public final fun component11 ()Ljava/lang/String;
public final fun component12 ()Ljava/lang/String;
public final fun component13 ()J
public final fun component14 ()J
public final fun component15 ()Z
public final fun component16 ()Z
public final fun component17 ()F
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Z
public final fun component10 ()J
public final fun component11 ()J
public final fun component12 ()Z
public final fun component2 ()Lcom/datadog/android/internal/rum/RumSessionContext;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/String;
public final fun component7 ()Ljava/lang/String;
public final fun component8 ()Lcom/datadog/android/internal/rum/RumSessionState;
public final fun component9 ()Lcom/datadog/android/internal/rum/RumSessionStartReason;
public final fun copy (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZZF)Lcom/datadog/android/internal/rum/RumContext;
public static synthetic fun copy$default (Lcom/datadog/android/internal/rum/RumContext;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZZFILjava/lang/Object;)Lcom/datadog/android/internal/rum/RumContext;
public final fun component7 ()Lcom/datadog/android/internal/rum/RumViewType;
public final fun component8 ()Ljava/lang/String;
public final fun component9 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZ)Lcom/datadog/android/internal/rum/RumContext;
public static synthetic fun copy$default (Lcom/datadog/android/internal/rum/RumContext;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumViewType;Ljava/lang/String;Ljava/lang/String;JJZILjava/lang/Object;)Lcom/datadog/android/internal/rum/RumContext;
public fun equals (Ljava/lang/Object;)Z
public final fun getActionId ()Ljava/lang/String;
public final fun getApplicationId ()Ljava/lang/String;
public final fun getHasReplay ()Z
public final fun getHasTimeseries ()Z
public final fun getSessionId ()Ljava/lang/String;
public final fun getSessionSampleRate ()F
public final fun getSessionStartReason ()Lcom/datadog/android/internal/rum/RumSessionStartReason;
public final fun getSessionState ()Lcom/datadog/android/internal/rum/RumSessionState;
public final fun getSession ()Lcom/datadog/android/internal/rum/RumSessionContext;
public final fun getSyntheticsResultId ()Ljava/lang/String;
public final fun getSyntheticsTestId ()Ljava/lang/String;
public final fun getViewId ()Ljava/lang/String;
Expand All @@ -534,7 +524,6 @@ public final class com/datadog/android/internal/rum/RumContext {
public final fun getViewType ()Lcom/datadog/android/internal/rum/RumViewType;
public final fun getViewUrl ()Ljava/lang/String;
public fun hashCode ()I
public final fun isSessionActive ()Z
public fun toString ()Ljava/lang/String;
}

Expand All @@ -548,7 +537,27 @@ public final class com/datadog/android/internal/rum/RumContextKt {
public final class com/datadog/android/internal/rum/RumSessionConstants {
public static final field EMPTY_RUM_SESSION_ID Ljava/lang/String;
public static final field INSTANCE Lcom/datadog/android/internal/rum/RumSessionConstants;
public static final field TRACKED_SESSION_STATE Ljava/lang/String;
}

public final class com/datadog/android/internal/rum/RumSessionContext {
public fun <init> (Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;ZFZ)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Lcom/datadog/android/internal/rum/RumSessionState;
public final fun component3 ()Lcom/datadog/android/internal/rum/RumSessionStartReason;
public final fun component4 ()Z
public final fun component5 ()F
public final fun component6 ()Z
public final fun copy (Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;ZFZ)Lcom/datadog/android/internal/rum/RumSessionContext;
public static synthetic fun copy$default (Lcom/datadog/android/internal/rum/RumSessionContext;Ljava/lang/String;Lcom/datadog/android/internal/rum/RumSessionState;Lcom/datadog/android/internal/rum/RumSessionStartReason;ZFZILjava/lang/Object;)Lcom/datadog/android/internal/rum/RumSessionContext;
public fun equals (Ljava/lang/Object;)Z
public final fun getHasTimeseries ()Z
public final fun getId ()Ljava/lang/String;
public final fun getSampleRate ()F
public final fun getStartReason ()Lcom/datadog/android/internal/rum/RumSessionStartReason;
public final fun getState ()Lcom/datadog/android/internal/rum/RumSessionState;
public fun hashCode ()I
public final fun isActive ()Z
public fun toString ()Ljava/lang/String;
}

public final class com/datadog/android/internal/rum/RumSessionStartReason : java/lang/Enum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,46 +14,36 @@ import com.datadog.android.internal.FeatureContextKeys
* Public for access across SDK modules. The data-class contract preserves the copying and
* equality semantics used by RUM scopes; it is an SDK-internal API.
*
* @property applicationId RUM application identifier, or [NULL_UUID] when unavailable.
* @property sessionId RUM session identifier, or [NULL_UUID] before a session is created.
* @property isSessionActive Whether the session scope is active.
* @property applicationId Configured RUM application identifier.
* @property session Session information, or null when no session is available.
* @property viewId Identifier of the associated view, if any.
* @property viewName Name of the associated view, if any.
* @property viewUrl URL of the associated view, if any.
* @property actionId Identifier of the associated action, if any.
* @property sessionState Tracking state of the session.
* @property sessionStartReason Trigger that created the session.
* @property viewType Category of the associated view, or [RumViewType.NONE] when unavailable.
* @property syntheticsTestId Identifier of the Synthetics test, if any.
* @property syntheticsResultId Identifier of the Synthetics test result, if any.
* @property viewTimestamp Start timestamp of the view in milliseconds.
* @property viewTimestampOffset Server time offset used by the view, in milliseconds.
* @property hasReplay Whether the view has Session Replay data.
* @property hasTimeseries Whether the session has collected timeseries data.
* @property sessionSampleRate Session sampling rate as a percentage, from 0 to 100.
*/
data class RumContext(
val applicationId: String = NULL_UUID,
val sessionId: String = NULL_UUID,
val isSessionActive: Boolean = false,
val applicationId: String,
val session: RumSessionContext? = null,
val viewId: String? = null,
val viewName: String? = null,
val viewUrl: String? = null,
val actionId: String? = null,
val sessionState: RumSessionState = RumSessionState.NOT_TRACKED,
val sessionStartReason: RumSessionStartReason = RumSessionStartReason.USER_APP_LAUNCH,
val viewType: RumViewType = RumViewType.NONE,
val syntheticsTestId: String? = null,
val syntheticsResultId: String? = null,
val viewTimestamp: Long = 0L,
val viewTimestampOffset: Long = 0L,
val hasReplay: Boolean = false,
val hasTimeseries: Boolean = false,
val sessionSampleRate: Float = SAMPLE_ALL_RATE
val hasReplay: Boolean = false
) {

companion object {
/** Sentinel used when an application or session identifier is unavailable. */
/** Legacy sentinel used at boundaries that require an identifier. */
const val NULL_UUID: String = RumSessionConstants.EMPTY_RUM_SESSION_ID

/** Sampling percentage that keeps every session. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,4 @@ object RumSessionConstants {
* ID equals this value.
*/
const val EMPTY_RUM_SESSION_ID: String = "00000000-0000-0000-0000-000000000000"

/**
* Value the RUM session state takes when the session is tracked (sampled in).
*/
const val TRACKED_SESSION_STATE: String = "TRACKED"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.rum

/**
* Snapshot of an existing RUM session, including sampled-out and expired sessions.
*
* @property id RUM session identifier.
* @property state Tracking state of the session.
* @property startReason Trigger that created the session.
* @property isActive Whether the session scope is active.
* @property sampleRate Session sampling rate as a percentage, from 0 to 100.
* @property hasTimeseries Whether the session has collected timeseries data.
*/
data class RumSessionContext(
val id: String,
val state: RumSessionState,
val startReason: RumSessionStartReason,
val isActive: Boolean,
val sampleRate: Float,
val hasTimeseries: Boolean
)
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ enum class RumSessionState(val asString: String) {
NOT_TRACKED("NOT_TRACKED"),

/** Session is sampled for RUM event collection. */
TRACKED(RumSessionConstants.TRACKED_SESSION_STATE),
TRACKED("TRACKED"),

/** Session has expired due to inactivity. */
EXPIRED("EXPIRED")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,27 @@
package com.datadog.android.internal.tests.elmyr

import com.datadog.android.internal.rum.RumContext
import com.datadog.android.internal.rum.RumSessionStartReason
import com.datadog.android.internal.rum.RumSessionState
import com.datadog.android.internal.rum.RumSessionContext
import com.datadog.android.internal.rum.RumViewType
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.ForgeryFactory
import java.util.UUID

class RumContextForgeryFactory : ForgeryFactory<RumContext> {
override fun getForgery(forge: Forge): RumContext {
val applicationId = forge.getForgery<UUID>().toString()
val session = forge.aNullable { getForgery<RumSessionContext>() }
?: return RumContext(applicationId = applicationId)
return RumContext(
applicationId = forge.getForgery<UUID>().toString(),
sessionId = forge.getForgery<UUID>().toString(),
isSessionActive = forge.aBool(),
applicationId = applicationId,
session = session,
viewId = forge.aNullable { getForgery<UUID>().toString() },
viewName = forge.aNullable { forge.anAlphaNumericalString() },
viewUrl = forge.aStringMatching("http(s?)://[a-z]+\\.com/[a-z]+"),
actionId = forge.aNullable { getForgery<UUID>().toString() },
sessionState = forge.aValueFrom(RumSessionState::class.java),
sessionStartReason = forge.aValueFrom(RumSessionStartReason::class.java),
viewType = forge.aValueFrom(RumViewType::class.java),
syntheticsTestId = forge.aNullable { forge.anAlphaNumericalString() },
syntheticsResultId = forge.aNullable { forge.anAlphaNumericalString() },
hasTimeseries = forge.aBool(),
sessionSampleRate = forge.aFloat(min = 0f, max = 100f)
syntheticsResultId = forge.aNullable { forge.anAlphaNumericalString() }
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.tests.elmyr

import com.datadog.android.internal.rum.RumSessionContext
import com.datadog.android.internal.rum.RumSessionStartReason
import com.datadog.android.internal.rum.RumSessionState
import fr.xgouchet.elmyr.Forge
import fr.xgouchet.elmyr.ForgeryFactory
import java.util.UUID

class RumSessionContextForgeryFactory : ForgeryFactory<RumSessionContext> {
override fun getForgery(forge: Forge): RumSessionContext = RumSessionContext(
id = forge.getForgery<UUID>().toString(),
state = forge.aValueFrom(RumSessionState::class.java),
startReason = forge.aValueFrom(RumSessionStartReason::class.java),
isActive = forge.aBool(),
sampleRate = forge.aFloat(min = 0f, max = 100f),
hasTimeseries = forge.aBool()
)
}
2 changes: 2 additions & 0 deletions detekt_custom_safe_calls_third_party.yml
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,7 @@ datadog:
- "kotlin.collections.MutableSet.contains(kotlin.String)"
- "kotlin.collections.MutableSet.contains(kotlin.String?)"
- "kotlin.collections.MutableSet.isEmpty()"
- "kotlin.collections.MutableSet.isNotEmpty()"
- "kotlin.collections.MutableSet.remove(kotlin.String)"
- "kotlin.collections.MutableSet.filter(kotlin.Function1)"
- "kotlin.collections.MutableSet.firstOrNull()"
Expand Down Expand Up @@ -940,6 +941,7 @@ datadog:
- "kotlin.String.removePrefix(kotlin.CharSequence)"
- "kotlin.String.removeSuffix(kotlin.CharSequence)"
- "kotlin.String.takeIf(kotlin.Function1)"
- "kotlin.String.takeUnless(kotlin.Function1)"
- "kotlin.String.toBigIntegerOrNull()"
- "kotlin.String.toBoolean()"
- "kotlin.String.toBooleanStrictOrNull()"
Expand Down
Loading
Loading