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
7 changes: 7 additions & 0 deletions detekt_custom_safe_calls_third_party.yml
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ datadog:
- "kotlin.collections.MutableList.add(com.datadog.android.rum.model.ActionEvent.Type)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.compose.internal.data.Parameter)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.compose.internal.utils.BackgroundInfo)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.composition.CapturedBounds)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.composition.CapturedChild)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.composition.CapturedLayer)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.composition.CapturedWireframe)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.embedded.PendingBatch)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.prerequisite.SystemRequirementChecker)"
- "kotlin.collections.MutableList.add(com.datadog.android.sessionreplay.internal.processor.MutationResolver.Entry)"
Expand Down Expand Up @@ -786,6 +790,9 @@ datadog:
- "kotlin.collections.listOf(com.datadog.android.rum.model.VitalAppLaunchEvent.Interface)"
- "kotlin.collections.listOf(com.datadog.android.rum.model.VitalOperationStepEvent.Interface)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.MapperTypeWrapper)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.internal.composition.CapturedWireframe.PrivacyPlaceholder)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.internal.composition.CapturedWireframe.Shape)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.internal.composition.CapturedWireframe.WebView)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.internal.recorder.DefaultOptionSelectorDetector)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.material.internal.MaterialDrawableToColorMapper)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.material.internal.MaterialOptionSelectorDetector)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import com.datadog.android.sessionreplay.MapperTypeWrapper
import com.datadog.android.sessionreplay.SessionReplayInternalCallback
import com.datadog.android.sessionreplay.TextAndInputPrivacy
import com.datadog.android.sessionreplay.internal.composition.ActiveWindowSource
import com.datadog.android.sessionreplay.internal.composition.AndroidCapturedSnapshotProducer
import com.datadog.android.sessionreplay.internal.composition.AndroidSnapshotCaptureLifecycle
import com.datadog.android.sessionreplay.internal.composition.AndroidWindowTraversal
import com.datadog.android.sessionreplay.internal.composition.CapturePipelineSelector
import com.datadog.android.sessionreplay.internal.composition.CaptureSkippedFrameNotifier
import com.datadog.android.sessionreplay.internal.composition.CaptureTimeBudget
Expand All @@ -38,6 +40,7 @@ import com.datadog.android.sessionreplay.internal.composition.CompositionCapture
import com.datadog.android.sessionreplay.internal.composition.CompositionChangeListener
import com.datadog.android.sessionreplay.internal.composition.CompositionChangeset
import com.datadog.android.sessionreplay.internal.composition.CompositionViewOnDrawInterceptor
import com.datadog.android.sessionreplay.internal.composition.DefaultRumViewScopeProvider
import com.datadog.android.sessionreplay.internal.composition.DefaultSnapshotCompletionProcessor
import com.datadog.android.sessionreplay.internal.composition.HandlerCaptureMainThreadExecutor
import com.datadog.android.sessionreplay.internal.composition.HandlerCaptureTaskScheduler
Expand All @@ -47,6 +50,11 @@ import com.datadog.android.sessionreplay.internal.composition.SnapshotCaptureOrc
import com.datadog.android.sessionreplay.internal.composition.SnapshotCompletionQueue
import com.datadog.android.sessionreplay.internal.composition.TimeBankCaptureTimeBudget
import com.datadog.android.sessionreplay.internal.composition.TimeProviderCaptureTimeProvider
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedMapperTypeWrapper
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedTextViewMapper
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedViewGroupFallbackMapper
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedViewMapperRegistry
import com.datadog.android.sessionreplay.internal.composition.mapper.CapturedWebViewMapper
import com.datadog.android.sessionreplay.internal.embedded.EmbeddedContentSlotRegistry
import com.datadog.android.sessionreplay.internal.recorder.Recorder
import com.datadog.android.sessionreplay.internal.recorder.RecordingTimeBank
Expand Down Expand Up @@ -93,9 +101,9 @@ internal class DefaultRecorderProvider(
private val heatmapsEnabled: Boolean,
private val compositionTreeRecordingEnabled: Boolean,
private val compositionPipelineFactory: (() -> Recorder)? = null,
private val compositionSnapshotProducerFactory: (ActiveWindowSource) -> CapturedSnapshotProducer = {
CapturedSnapshotProducer { _, _ -> null }
},
private val compositionSnapshotProducerFactory: (
(ActiveWindowSource, RumContextProvider) -> CapturedSnapshotProducer
)? = null,
private val recordingTimeBankFactory: () -> TimeBank = { RecordingTimeBank() }
) : RecorderProvider {

Expand Down Expand Up @@ -154,23 +162,61 @@ internal class DefaultRecorderProvider(
application: Application
): Recorder {
val internalLogger = sdkCore.internalLogger
val timeProvider = TimeProviderCaptureTimeProvider(sdkCore.timeProvider)
val skippedFrameNotifier = CaptureSkippedFrameNotifier(sdkCore)
val windowSource = ActiveWindowSource()
val completionQueue = SnapshotCompletionQueue(
executorService = sdkCore.createSingleThreadExecutorService("sr-composition-processing"),
processor = DefaultSnapshotCompletionProcessor(
rumContextProvider = rumContextProvider,
recordWriter = recordWriter,
internalLogger = internalLogger
),
val completionQueue = createCompletionQueue(recordWriter, rumContextProvider, internalLogger)
val orchestrator = createCaptureOrchestrator(windowSource, rumContextProvider, completionQueue, internalLogger)
val interceptor = CompositionViewOnDrawInterceptor(
windowSource = windowSource,
onWindowsChanged = CompositionChangeListener { windows ->
orchestrator.requestCapture(CompositionChangeset.of(windows))
},
internalLogger = internalLogger
)
val orchestrator = SnapshotCaptureOrchestrator(
producer = compositionSnapshotProducerFactory(windowSource),
return CompositionCapturePipeline(
orchestrator = orchestrator,
lifecycle = AndroidSnapshotCaptureLifecycle(
application = application,
interceptor = interceptor,
internalLogger = internalLogger,
currentActivity = internalCallback.getCurrentActivity()
),
completionQueue = completionQueue
)
}

private fun createCompletionQueue(
recordWriter: RecordWriter,
rumContextProvider: RumContextProvider,
internalLogger: InternalLogger
): SnapshotCompletionQueue = SnapshotCompletionQueue(
executorService = sdkCore.createSingleThreadExecutorService("sr-composition-processing"),
processor = DefaultSnapshotCompletionProcessor(
rumContextProvider = rumContextProvider,
recordWriter = recordWriter,
internalLogger = internalLogger
),
internalLogger = internalLogger
)

private fun createCaptureOrchestrator(
windowSource: ActiveWindowSource,
rumContextProvider: RumContextProvider,
completionQueue: SnapshotCompletionQueue,
internalLogger: InternalLogger
): SnapshotCaptureOrchestrator {
val skippedFrameNotifier = CaptureSkippedFrameNotifier(sdkCore)
val producer = compositionSnapshotProducerFactory?.invoke(windowSource, rumContextProvider)
?: AndroidCapturedSnapshotProducer(
windowSource = windowSource,
scopeProvider = DefaultRumViewScopeProvider(rumContextProvider),
timeProvider = sdkCore.timeProvider,
traversal = AndroidWindowTraversal(mapperRegistry = builtInCapturedMappers())
)
return SnapshotCaptureOrchestrator(
producer = producer,
processor = ImmediateCapturedSnapshotProcessor(),
consumer = completionQueue,
timeProvider = timeProvider,
timeProvider = TimeProviderCaptureTimeProvider(sdkCore.timeProvider),
captureScheduler = HandlerCaptureTaskScheduler(),
mainThreadExecutor = HandlerCaptureMainThreadExecutor(),
expiryScheduler = ScheduledExecutorCaptureTaskScheduler(
Expand All @@ -187,22 +233,20 @@ internal class DefaultRecorderProvider(
},
internalLogger = internalLogger
)
val interceptor = CompositionViewOnDrawInterceptor(
windowSource = windowSource,
onWindowsChanged = CompositionChangeListener { windows ->
orchestrator.requestCapture(CompositionChangeset.of(windows))
},
internalLogger = internalLogger
)
return CompositionCapturePipeline(
orchestrator = orchestrator,
lifecycle = AndroidSnapshotCaptureLifecycle(
application = application,
interceptor = interceptor,
internalLogger = internalLogger,
currentActivity = internalCallback.getCurrentActivity()
}

private fun builtInCapturedMappers(): CapturedViewMapperRegistry {
val internalLogger = sdkCore.internalLogger
return CapturedViewMapperRegistry(
mappers = listOf(
CapturedMapperTypeWrapper(WebView::class.java, CapturedWebViewMapper()),
CapturedMapperTypeWrapper(
TextView::class.java,
CapturedTextViewMapper(internalLogger = internalLogger)
)
),
completionQueue = completionQueue
fallbackMapper = CapturedViewGroupFallbackMapper(internalLogger = internalLogger),
internalLogger = internalLogger
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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.sessionreplay.internal.composition

import android.view.View
import androidx.annotation.MainThread
import com.datadog.android.internal.time.TimeProvider
import com.datadog.android.sessionreplay.utils.DefaultViewIdentifierResolver
import com.datadog.android.sessionreplay.utils.ViewIdentifierResolver

/**
* The real [CapturedSnapshotProducer] for the plain Android View hierarchy - the workstream-3
* implementation of the extension point [SnapshotCaptureOrchestrator] drives every generation.
* Builds a fresh [CapturedIdentityFactory] per call (this workstream only produces full snapshots;
* an identity factory persisting across generations for incremental diffing is a later workstream's
* concern), walks every currently active window via [AndroidWindowTraversal], and assembles them
* under one synthetic screen root in [ActiveWindowSource.currentWindows] order (already z-ordered).
*/
internal class AndroidCapturedSnapshotProducer(
private val windowSource: ActiveWindowSource,
private val scopeProvider: RumViewScopeProvider,
private val timeProvider: TimeProvider,
private val traversal: AndroidWindowTraversal,
private val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver
) : CapturedSnapshotProducer {

@MainThread
override fun capture(context: CaptureGenerationContext, changeset: CaptureChangeset): CapturedFullSnapshot? {
val rumViewScope = scopeProvider.currentScope() ?: return null
val identityFactory = DefaultCapturedIdentityFactory(rumViewScope.scope)
val walk = walkWindows(windowSource.currentWindows(), identityFactory, context)

return walk?.let {
val root = CapturedLayer(
identity = identityFactory.screenRoot(),
kind = CapturedLayerKind.SYNTHETIC_SCREEN_ROOT,
bounds = it.windowLayers.firstOrNull()?.bounds ?: CapturedBounds(0, 0, 0, 0),
children = it.windowLayers.map { layer -> CapturedChild.Layer(layer.identity) }
)
CapturedFullSnapshot(
timestamp = timeProvider.getDeviceTimestampMillis() + rumViewScope.viewTimeOffsetMs,
scope = rumViewScope.scope,
root = root,
layers = it.layers,
wireframes = it.wireframes
)
}
}

private fun walkWindows(
windows: List<View>,
identityFactory: CapturedIdentityFactory,
context: CaptureGenerationContext
): WindowsWalkAccumulation? {
if (windows.isEmpty()) return null
val layers = mutableListOf<CapturedLayer>()
val wireframes = mutableListOf<CapturedWireframe>()
val windowLayers = mutableListOf<CapturedLayer>()
var aborted = false

for (window in windows) {
val windowIdentity = identityFactory.window(viewIdentifierResolver.resolveViewId(window).toString())
when (val result = traversal.traverseWindow(window, windowIdentity, identityFactory, context)) {
is WindowWalkResult.Present -> {
windowLayers += result.rootLayer
layers += result.layers
wireframes += result.wireframes
}
WindowWalkResult.Filtered -> Unit
WindowWalkResult.Aborted -> {
aborted = true
}
}
if (aborted) break
}

return if (aborted) null else WindowsWalkAccumulation(windowLayers, layers, wireframes)
}

private class WindowsWalkAccumulation(
val windowLayers: List<CapturedLayer>,
val layers: List<CapturedLayer>,
val wireframes: List<CapturedWireframe>
)
}
Loading
Loading