Skip to content

Commit e981b00

Browse files
committed
PANA-8729: Heatmaps support in the new pipeline
1 parent c654911 commit e981b00

17 files changed

Lines changed: 665 additions & 87 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,9 @@ data class SessionReplayConfiguration internal constructor(
257257
*
258258
* Disabled by default.
259259
*
260-
* Heatmap recording ([setHeatmapsEnabled]) is not supported by this pipeline yet; combining
261-
* the two will log a warning and no heatmap data will be recorded.
260+
* Heatmap recording ([setHeatmapsEnabled]) is supported by this pipeline for native
261+
* [View] content. Jetpack Compose content does not yet produce heatmap data when this
262+
* pipeline is enabled - this matches the default recorder, which has the same limitation.
262263
*
263264
* @param enabled whether composition-tree recording should be used.
264265
*/

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

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.appcompat.widget.ActionBarContainer
2323
import androidx.appcompat.widget.SwitchCompat
2424
import com.datadog.android.api.InternalLogger
2525
import com.datadog.android.api.feature.FeatureSdkCore
26+
import com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistry
2627
import com.datadog.android.internal.utils.ImageViewUtils
2728
import com.datadog.android.sessionreplay.ImagePrivacy
2829
import com.datadog.android.sessionreplay.MapperTypeWrapper
@@ -60,6 +61,7 @@ import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedVie
6061
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedViewMapperRegistry
6162
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedWebViewMapper
6263
import com.datadog.android.sessionreplay.internal.embedded.EmbeddedContentSlotRegistry
64+
import com.datadog.android.sessionreplay.internal.recorder.HeatmapIdentifierResolver
6365
import com.datadog.android.sessionreplay.internal.recorder.Recorder
6466
import com.datadog.android.sessionreplay.internal.recorder.RecordingTimeBank
6567
import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder
@@ -125,13 +127,6 @@ internal class DefaultRecorderProvider(
125127
application: Application,
126128
embeddedContentSlotRegistry: EmbeddedContentSlotRegistry
127129
): Recorder {
128-
if (heatmapsEnabled && compositionTreeRecordingEnabled) {
129-
sdkCore.internalLogger.log(
130-
InternalLogger.Level.WARN,
131-
InternalLogger.Target.USER,
132-
{ HEATMAPS_UNSUPPORTED_WITH_COMPOSITION_RECORDING_MESSAGE }
133-
)
134-
}
135130
val heatmapIdentifierRegistry = if (heatmapsEnabled) LazyHeatmapIdentifierRegistry(sdkCore) else null
136131
return CapturePipelineSelector(
137132
compositionEnabled = compositionTreeRecordingEnabled,
@@ -142,7 +137,8 @@ internal class DefaultRecorderProvider(
142137
recordWriter,
143138
rumContextProvider,
144139
application,
145-
embeddedContentSlotRegistry
140+
embeddedContentSlotRegistry,
141+
heatmapIdentifierRegistry
146142
)
147143
},
148144
legacyFactory = {
@@ -176,10 +172,21 @@ internal class DefaultRecorderProvider(
176172
recordWriter: RecordWriter,
177173
rumContextProvider: RumContextProvider,
178174
application: Application,
179-
embeddedContentSlotRegistry: EmbeddedContentSlotRegistry
175+
embeddedContentSlotRegistry: EmbeddedContentSlotRegistry,
176+
heatmapIdentifierRegistry: HeatmapIdentifierRegistry?
180177
): Recorder {
181178
val internalLogger = sdkCore.internalLogger
182179
val windowSource = ActiveWindowSource()
180+
// Native View content only - see setCompositionTreeRecordingEnabled's own doc for why
181+
// Jetpack Compose content doesn't get heatmap identifiers here either, same as the legacy
182+
// pipeline.
183+
val heatmapResolver = heatmapIdentifierRegistry?.let {
184+
HeatmapIdentifierResolver(
185+
appPackageName = application.packageName,
186+
registry = it,
187+
internalLogger = internalLogger
188+
)
189+
}
183190
val resourceResolverBundle = buildResourceResolver(
184191
appContext = application,
185192
sdkCore = sdkCore,
@@ -189,7 +196,12 @@ internal class DefaultRecorderProvider(
189196
rumContextProvider = rumContextProvider,
190197
embeddedContentSlotRegistry = embeddedContentSlotRegistry,
191198
eventProcessingExecutorName = "sr-composition-event-processing",
192-
drawablesExecutorName = "sr-composition-drawables"
199+
drawablesExecutorName = "sr-composition-drawables",
200+
// This pipeline's own drain trigger only runs as a side effect of a full capture
201+
// generation completing, not on every View.onDraw() like the legacy pipeline - so it
202+
// needs its own eager drain to avoid the resource queue's expiry window on a screen
203+
// that renders once and then sits idle. See ResourceItemCreationHandler's own doc.
204+
eagerResourceDrain = true
193205
)
194206
val completionQueue = createCompletionQueue(
195207
recordWriter,
@@ -202,7 +214,8 @@ internal class DefaultRecorderProvider(
202214
rumContextProvider,
203215
completionQueue,
204216
resourceResolverBundle.resourceResolver,
205-
internalLogger
217+
internalLogger,
218+
heatmapResolver
206219
)
207220
val interceptor = CompositionViewOnDrawInterceptor(
208221
windowSource = windowSource,
@@ -243,16 +256,18 @@ internal class DefaultRecorderProvider(
243256
internalLogger = internalLogger
244257
)
245258

259+
@Suppress("LongParameterList")
246260
private fun createCaptureOrchestrator(
247261
windowSource: ActiveWindowSource,
248262
rumContextProvider: RumContextProvider,
249263
completionQueue: SnapshotCompletionQueue,
250264
resourceResolver: ResourceResolver,
251-
internalLogger: InternalLogger
265+
internalLogger: InternalLogger,
266+
heatmapResolver: HeatmapIdentifierResolver?
252267
): SnapshotCaptureOrchestrator {
253268
val skippedFrameNotifier = CaptureSkippedFrameNotifier(sdkCore)
254269
val producer = compositionSnapshotProducerFactory?.invoke(windowSource, rumContextProvider)
255-
?: defaultCompositionSnapshotProducer(windowSource, rumContextProvider)
270+
?: defaultCompositionSnapshotProducer(windowSource, rumContextProvider, heatmapResolver)
256271
val mainThreadExecutor = HandlerCaptureMainThreadExecutor()
257272
// Shared rather than one-per-consumer: sdkCore.createScheduledExecutorService allocates a
258273
// brand-new background thread with no pooling, and PixelFallbackSnapshotProcessor has no
@@ -289,7 +304,8 @@ internal class DefaultRecorderProvider(
289304

290305
private fun defaultCompositionSnapshotProducer(
291306
windowSource: ActiveWindowSource,
292-
rumContextProvider: RumContextProvider
307+
rumContextProvider: RumContextProvider,
308+
heatmapResolver: HeatmapIdentifierResolver?
293309
): AndroidCapturedSnapshotProducer = AndroidCapturedSnapshotProducer(
294310
windowSource = windowSource,
295311
scopeProvider = DefaultRumViewScopeProvider(rumContextProvider),
@@ -299,7 +315,8 @@ internal class DefaultRecorderProvider(
299315
composeHostDecomposer = compositionHostDecomposer,
300316
rootImagePrivacy = imagePrivacy,
301317
rootTextAndInputPrivacy = textAndInputPrivacy,
302-
internalLogger = sdkCore.internalLogger
318+
internalLogger = sdkCore.internalLogger,
319+
heatmapResolver = heatmapResolver
303320
)
304321
)
305322

@@ -482,10 +499,4 @@ internal class DefaultRecorderProvider(
482499
null
483500
}
484501
}
485-
486-
internal companion object {
487-
internal const val HEATMAPS_UNSUPPORTED_WITH_COMPOSITION_RECORDING_MESSAGE = "Heatmaps are not " +
488-
"supported by the composition-tree recording pipeline yet. No heatmap data will be recorded " +
489-
"for this session."
490-
}
491502
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ internal class AndroidCapturedSnapshotProducer(
4343
override fun capture(context: CaptureGenerationContext, changeset: CaptureChangeset): CaptureOutput? {
4444
val rumViewScope = scopeProvider.currentScope() ?: return null
4545
val identityFactory = identityFactoryFor(rumViewScope.scope)
46-
val walk = walkWindows(windowSource.currentWindows(), identityFactory, context)
46+
val walk = walkWindows(windowSource.currentWindows(), identityFactory, context, rumViewScope.viewUrl)
4747

4848
return walk?.let {
4949
val root = CapturedLayer(
@@ -72,10 +72,12 @@ internal class AndroidCapturedSnapshotProducer(
7272
return DefaultCapturedIdentityFactory(scope).also { retainedIdentityFactory = it }
7373
}
7474

75+
@MainThread
7576
private fun walkWindows(
7677
windows: List<View>,
7778
identityFactory: CapturedIdentityFactory,
78-
context: CaptureGenerationContext
79+
context: CaptureGenerationContext,
80+
viewUrl: String?
7981
): WindowsWalkAccumulation? {
8082
if (windows.isEmpty()) return null
8183
val layers = mutableListOf<CapturedLayer>()
@@ -86,7 +88,9 @@ internal class AndroidCapturedSnapshotProducer(
8688

8789
for (window in windows) {
8890
val windowIdentity = identityFactory.window(viewIdentifierResolver.resolveViewId(window).toString())
89-
when (val result = traversal.traverseWindow(window, windowIdentity, identityFactory, context)) {
91+
when (
92+
val result = traversal.traverseWindow(window, windowIdentity, identityFactory, context, viewUrl)
93+
) {
9094
is WindowWalkResult.Present -> {
9195
windowLayers += result.rootLayer
9296
layers += result.layers

0 commit comments

Comments
 (0)