Skip to content

Commit 7737285

Browse files
authored
Merge pull request #2666 from DataDog/nogorodnikov/rum-10064/read-rum-context-in-non-blocking-manner-in-session-replay
RUM-10064: Read RUM context in Session Replay in non-blocking manner
2 parents 8893ced + 3eadf7d commit 7737285

File tree

24 files changed

+272
-231
lines changed

24 files changed

+272
-231
lines changed

dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureSdkCore.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ interface FeatureSdkCore : SdkCore {
7070
fun setEventReceiver(featureName: String, receiver: FeatureEventReceiver)
7171

7272
/**
73-
* Sets context update receiver for the given feature.
73+
* Sets context update receiver for the given feature. Once subscribed, current context will be emitted
74+
* immdediately if it exists.
7475
*
7576
* @param featureName Feature name.
7677
* @param listener Listener to remove.

dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/DatadogCore.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ internal class DatadogCore(
260260
)
261261
} else {
262262
feature.setContextUpdateListener(listener)
263+
features.keys.forEach {
264+
val currentContext = contextProvider?.getFeatureContext(it)
265+
if (!currentContext.isNullOrEmpty()) {
266+
listener.onContextUpdate(it, currentContext)
267+
}
268+
}
263269
}
264270
}
265271

dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/DatadogCoreTest.kt

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import com.datadog.tools.unit.annotations.TestConfigurationsProvider
3838
import com.datadog.tools.unit.extensions.TestConfigurationExtension
3939
import com.datadog.tools.unit.extensions.config.TestConfiguration
4040
import com.datadog.tools.unit.forge.aThrowable
41+
import com.datadog.tools.unit.forge.exhaustiveAttributes
4142
import com.google.gson.JsonObject
4243
import fr.xgouchet.elmyr.Forge
4344
import fr.xgouchet.elmyr.annotation.AdvancedForgery
@@ -445,7 +446,7 @@ internal class DatadogCoreTest {
445446
) {
446447
// Given
447448
val mockFeature = mock<SdkFeature>()
448-
val mockContextUpdateListener: FeatureContextUpdateReceiver = mock()
449+
val mockContextUpdateListener = mock<FeatureContextUpdateReceiver>()
449450
testedCore.features[feature] = mockFeature
450451

451452
// When
@@ -455,12 +456,68 @@ internal class DatadogCoreTest {
455456
verify(mockFeature).setContextUpdateListener(mockContextUpdateListener)
456457
}
457458

459+
@Test
460+
fun `M not invoke listener W setContextUpdateListener() { other features have no context yet }`(
461+
@StringForgery feature: String,
462+
forge: Forge
463+
) {
464+
// Given
465+
val mockFeature = mock<SdkFeature>()
466+
val mockContextUpdateListener = mock<FeatureContextUpdateReceiver>()
467+
testedCore.features[feature] = mockFeature
468+
val mockContextProvider = mock<ContextProvider>()
469+
testedCore.coreFeature.contextProvider = mockContextProvider
470+
whenever(mockContextProvider.getFeatureContext(any())) doAnswer {
471+
forge.anElementFrom(null, emptyMap<String, Any?>())
472+
}
473+
repeat(forge.aTinyInt()) {
474+
testedCore.features += forge.aString() to mock<SdkFeature>()
475+
}
476+
477+
// When
478+
testedCore.setContextUpdateReceiver(feature, mockContextUpdateListener)
479+
480+
// Then
481+
verify(mockFeature).setContextUpdateListener(mockContextUpdateListener)
482+
verifyNoInteractions(mockContextUpdateListener)
483+
}
484+
485+
@Test
486+
fun `M invoke listener W setContextUpdateListener() { other features have context }`(
487+
@StringForgery feature: String,
488+
forge: Forge
489+
) {
490+
// Given
491+
val mockFeature = mock<SdkFeature>()
492+
val mockContextUpdateListener = mock<FeatureContextUpdateReceiver>()
493+
testedCore.features[feature] = mockFeature
494+
val mockContextProvider = mock<ContextProvider>()
495+
testedCore.coreFeature.contextProvider = mockContextProvider
496+
val otherFeatures = forge.aList {
497+
forge.aString() to forge.exhaustiveAttributes()
498+
}
499+
otherFeatures.forEach {
500+
testedCore.features += it.first to mock<SdkFeature>()
501+
whenever(mockContextProvider.getFeatureContext(it.first)) doReturn it.second
502+
}
503+
504+
// When
505+
testedCore.setContextUpdateReceiver(feature, mockContextUpdateListener)
506+
507+
// Then
508+
verify(mockFeature).setContextUpdateListener(mockContextUpdateListener)
509+
otherFeatures.forEach {
510+
verify(mockContextUpdateListener).onContextUpdate(it.first, it.second)
511+
}
512+
verifyNoMoreInteractions(mockContextUpdateListener)
513+
}
514+
458515
@Test
459516
fun `M notify no feature registered W setContextUpdateListener() { feature is not registered }`(
460517
@StringForgery feature: String
461518
) {
462519
// Given
463-
val mockContextUpdateListener: FeatureContextUpdateReceiver = mock()
520+
val mockContextUpdateListener = mock<FeatureContextUpdateReceiver>()
464521

465522
// When
466523
testedCore.setContextUpdateReceiver(feature, mockContextUpdateListener)

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/DefaultRecorderProvider.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.WebViewWirefra
4242
import com.datadog.android.sessionreplay.internal.resources.ResourceDataStoreManager
4343
import com.datadog.android.sessionreplay.internal.storage.RecordWriter
4444
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
45-
import com.datadog.android.sessionreplay.internal.time.SessionReplayTimeProvider
45+
import com.datadog.android.sessionreplay.internal.utils.RumContextProvider
4646
import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector
4747
import com.datadog.android.sessionreplay.recorder.mapper.EditTextMapper
4848
import com.datadog.android.sessionreplay.recorder.mapper.ImageViewMapper
@@ -73,18 +73,19 @@ internal class DefaultRecorderProvider(
7373
resourceDataStoreManager: ResourceDataStoreManager,
7474
resourceWriter: ResourcesWriter,
7575
recordWriter: RecordWriter,
76+
rumContextProvider: RumContextProvider,
7677
application: Application
7778
): Recorder {
7879
return SessionReplayRecorder(
7980
application,
8081
resourceDataStoreManager = resourceDataStoreManager,
8182
resourcesWriter = resourceWriter,
82-
rumContextProvider = SessionReplayRumContextProvider(sdkCore),
83+
rumContextProvider = rumContextProvider,
8384
imagePrivacy = imagePrivacy,
8485
touchPrivacyManager = touchPrivacyManager,
8586
textAndInputPrivacy = textAndInputPrivacy,
8687
recordWriter = recordWriter,
87-
timeProvider = SessionReplayTimeProvider(sdkCore),
88+
timeProvider = { System.currentTimeMillis() },
8889
mappers = customMappers + builtInMappers(),
8990
customOptionSelectorDetectors = customOptionSelectorDetectors,
9091
customDrawableMappers = customDrawableMappers,

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/RecorderProvider.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import com.datadog.android.sessionreplay.internal.recorder.Recorder
1111
import com.datadog.android.sessionreplay.internal.resources.ResourceDataStoreManager
1212
import com.datadog.android.sessionreplay.internal.storage.RecordWriter
1313
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
14+
import com.datadog.android.sessionreplay.internal.utils.RumContextProvider
1415

1516
internal fun interface RecorderProvider {
1617
fun provideSessionReplayRecorder(
1718
resourceDataStoreManager: ResourceDataStoreManager,
1819
resourceWriter: ResourcesWriter,
1920
recordWriter: RecordWriter,
21+
rumContextProvider: RumContextProvider,
2022
application: Application
2123
): Recorder
2224
}

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayFeature.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ internal class SessionReplayFeature(
118118
internal var sessionReplayRecorder: Recorder = NoOpRecorder()
119119
internal var dataWriter: RecordWriter = NoOpRecordWriter()
120120
internal val initialized = AtomicBoolean(false)
121+
private val rumContextProvider = SessionReplayRumContextProvider()
121122

122123
// region Feature
123124

@@ -141,11 +142,13 @@ internal class SessionReplayFeature(
141142
)
142143

143144
dataWriter = createDataWriter()
145+
sdkCore.setContextUpdateReceiver(Feature.SESSION_REPLAY_FEATURE_NAME, rumContextProvider)
144146
sessionReplayRecorder =
145147
recorderProvider.provideSessionReplayRecorder(
146148
resourceDataStoreManager = resourceDataStoreManager,
147149
resourceWriter = resourcesFeature.dataWriter,
148150
recordWriter = dataWriter,
151+
rumContextProvider = rumContextProvider,
149152
application = appContext
150153
)
151154
sessionReplayRecorder.registerCallbacks()
@@ -170,6 +173,7 @@ internal class SessionReplayFeature(
170173

171174
override fun onStop() {
172175
stopRecording()
176+
sdkCore.removeContextUpdateReceiver(Feature.RUM_FEATURE_NAME, rumContextProvider)
173177
sessionReplayRecorder.unregisterCallbacks()
174178
sessionReplayRecorder.stopProcessingRecords()
175179
dataWriter = NoOpRecordWriter()

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/SessionReplayRumContextProvider.kt

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,32 @@
77
package com.datadog.android.sessionreplay.internal
88

99
import com.datadog.android.api.feature.Feature
10-
import com.datadog.android.api.feature.FeatureSdkCore
10+
import com.datadog.android.api.feature.FeatureContextUpdateReceiver
1111
import com.datadog.android.sessionreplay.internal.utils.RumContextProvider
1212
import com.datadog.android.sessionreplay.internal.utils.SessionReplayRumContext
1313
import java.util.UUID
1414

15-
internal class SessionReplayRumContextProvider(
16-
private val sdkCore: FeatureSdkCore
17-
) : RumContextProvider {
15+
internal class SessionReplayRumContextProvider : RumContextProvider, FeatureContextUpdateReceiver {
16+
17+
@Volatile
18+
private var rumContext = emptyMap<String, Any?>()
19+
1820
override fun getRumContext(): SessionReplayRumContext {
19-
val rumContext = sdkCore.getFeatureContext(Feature.RUM_FEATURE_NAME)
20-
return SessionReplayRumContext(
21-
applicationId = rumContext["application_id"] as? String ?: NULL_UUID,
22-
sessionId = rumContext["session_id"] as? String ?: NULL_UUID,
23-
viewId = rumContext["view_id"] as? String ?: NULL_UUID
24-
)
21+
return rumContext.let {
22+
SessionReplayRumContext(
23+
applicationId = it["application_id"] as? String ?: NULL_UUID,
24+
sessionId = it["session_id"] as? String ?: NULL_UUID,
25+
viewId = it["view_id"] as? String ?: NULL_UUID,
26+
// TODO RUM-3785 Share this property somehow, defined in RumFeature.VIEW_TIMESTAMP_OFFSET_IN_MS_KEY
27+
viewTimeOffsetMs = it["view_timestamp_offset"] as? Long ?: 0L
28+
)
29+
}
30+
}
31+
32+
override fun onContextUpdate(featureName: String, event: Map<String, Any?>) {
33+
if (featureName == Feature.RUM_FEATURE_NAME) {
34+
rumContext = event
35+
}
2536
}
2637

2738
companion object {

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/processor/RumContextDataHandler.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ internal class RumContextDataHandler(
2020

2121
@MainThread
2222
internal fun createRumContextData(): RecordedQueuedItemContext? {
23-
// we will make sure we get the timestamp on the UI thread to avoid time skewing
2423
val timestamp = timeProvider.getDeviceTimestamp()
2524

26-
// TODO RUM-836 Fetch the RumContext from the core SDKContext when available
2725
val newRumContext = rumContextProvider.getRumContext()
2826

2927
if (newRumContext.isNotValid()) {
@@ -40,7 +38,7 @@ internal class RumContextDataHandler(
4038
return null
4139
}
4240

43-
return RecordedQueuedItemContext(timestamp, newRumContext.copy())
41+
return RecordedQueuedItemContext(timestamp + newRumContext.viewTimeOffsetMs, newRumContext.copy())
4442
}
4543

4644
companion object {

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,13 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
5959
private val appContext: Application
6060
private val textAndInputPrivacy: TextAndInputPrivacy
6161
private val imagePrivacy: ImagePrivacy
62-
private val touchPrivacyManager: TouchPrivacyManager
63-
private val recordWriter: RecordWriter
64-
private val timeProvider: TimeProvider
65-
private val mappers: List<MapperTypeWrapper<*>>
6662
private val customOptionSelectorDetectors: List<OptionSelectorDetector>
6763
private val windowInspector: WindowInspector
6864
private val windowCallbackInterceptor: WindowCallbackInterceptor
6965
private val sessionReplayLifecycleCallback: LifecycleCallback
7066
private val recordedDataQueueHandler: RecordedDataQueueHandler
7167
private val viewOnDrawInterceptor: ViewOnDrawInterceptor
7268
private val internalLogger: InternalLogger
73-
private val resourceDataStoreManager: ResourceDataStoreManager
74-
private val internalCallback: SessionReplayInternalCallback
7569
private val uiHandler: Handler
7670
private var shouldRecord = false
7771

@@ -111,10 +105,6 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
111105
this.appContext = appContext
112106
this.textAndInputPrivacy = textAndInputPrivacy
113107
this.imagePrivacy = imagePrivacy
114-
this.touchPrivacyManager = touchPrivacyManager
115-
this.recordWriter = recordWriter
116-
this.timeProvider = timeProvider
117-
this.mappers = mappers
118108
this.customOptionSelectorDetectors = customOptionSelectorDetectors
119109
this.windowInspector = windowInspector
120110
this.recordedDataQueueHandler = RecordedDataQueueHandler(
@@ -126,8 +116,6 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
126116
),
127117
recordedDataQueue = ConcurrentLinkedQueue()
128118
)
129-
this.resourceDataStoreManager = resourceDataStoreManager
130-
this.internalCallback = internalCallback
131119

132120
val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver
133121
val colorStringFormatter: ColorStringFormatter = DefaultColorStringFormatter
@@ -200,6 +188,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
200188
recordedDataQueueHandler,
201189
viewOnDrawInterceptor,
202190
timeProvider,
191+
rumContextProvider,
203192
internalLogger,
204193
imagePrivacy,
205194
textAndInputPrivacy,
@@ -223,28 +212,18 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
223212
appContext: Application,
224213
textAndInputPrivacy: TextAndInputPrivacy,
225214
imagePrivacy: ImagePrivacy,
226-
touchPrivacyManager: TouchPrivacyManager,
227-
recordWriter: RecordWriter,
228-
timeProvider: TimeProvider,
229-
mappers: List<MapperTypeWrapper<*>> = emptyList(),
230215
customOptionSelectorDetectors: List<OptionSelectorDetector>,
231216
windowInspector: WindowInspector = WindowInspector,
232217
windowCallbackInterceptor: WindowCallbackInterceptor,
233218
sessionReplayLifecycleCallback: LifecycleCallback,
234219
viewOnDrawInterceptor: ViewOnDrawInterceptor,
235220
recordedDataQueueHandler: RecordedDataQueueHandler,
236-
resourceDataStoreManager: ResourceDataStoreManager,
237221
uiHandler: Handler,
238-
internalLogger: InternalLogger,
239-
internalCallback: SessionReplayInternalCallback
222+
internalLogger: InternalLogger
240223
) {
241224
this.appContext = appContext
242225
this.textAndInputPrivacy = textAndInputPrivacy
243226
this.imagePrivacy = imagePrivacy
244-
this.touchPrivacyManager = touchPrivacyManager
245-
this.recordWriter = recordWriter
246-
this.timeProvider = timeProvider
247-
this.mappers = mappers
248227
this.customOptionSelectorDetectors = customOptionSelectorDetectors
249228
this.windowInspector = windowInspector
250229
this.recordedDataQueueHandler = recordedDataQueueHandler
@@ -253,8 +232,6 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder {
253232
this.sessionReplayLifecycleCallback = sessionReplayLifecycleCallback
254233
this.uiHandler = uiHandler
255234
this.internalLogger = internalLogger
256-
this.resourceDataStoreManager = resourceDataStoreManager
257-
this.internalCallback = internalCallback
258235
}
259236

260237
override fun stopProcessingRecords() {

features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/WindowCallbackInterceptor.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import com.datadog.android.sessionreplay.internal.TouchPrivacyManager
1515
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler
1616
import com.datadog.android.sessionreplay.internal.recorder.callback.NoOpWindowCallback
1717
import com.datadog.android.sessionreplay.internal.recorder.callback.RecorderWindowCallback
18+
import com.datadog.android.sessionreplay.internal.utils.RumContextProvider
1819
import com.datadog.android.sessionreplay.internal.utils.TimeProvider
1920
import java.util.WeakHashMap
2021

2122
internal class WindowCallbackInterceptor(
2223
private val recordedDataQueueHandler: RecordedDataQueueHandler,
2324
private val viewOnDrawInterceptor: ViewOnDrawInterceptor,
2425
private val timeProvider: TimeProvider,
26+
private val rumContextProvider: RumContextProvider,
2527
private val internalLogger: InternalLogger,
2628
private val imagePrivacy: ImagePrivacy,
2729
private val textAndInputPrivacy: TextAndInputPrivacy,
@@ -57,6 +59,7 @@ internal class WindowCallbackInterceptor(
5759
recordedDataQueueHandler,
5860
toWrap,
5961
timeProvider,
62+
rumContextProvider,
6063
viewOnDrawInterceptor,
6164
internalLogger,
6265
textAndInputPrivacy,

0 commit comments

Comments
 (0)