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
11 changes: 11 additions & 0 deletions detekt_custom_safe_calls_third_party.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ datadog:
- "java.util.concurrent.atomic.AtomicReference.compareAndSet(kotlin.String?, kotlin.String?)"
- "java.util.concurrent.atomic.AtomicReference.compareAndSet(kotlin.collections.Set?, kotlin.collections.Set?)"
- "java.util.concurrent.atomic.AtomicReference.compareAndSet(UNKNOWN, UNKNOWN)"
- "java.util.concurrent.atomic.AtomicReference.compareAndSet(com.datadog.android.sessionreplay.internal.composition.CaptureGenerationContext.State?, com.datadog.android.sessionreplay.internal.composition.CaptureGenerationContext.State?)"
- "java.util.concurrent.atomic.AtomicReference.constructor()"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.sessionreplay.internal.composition.CaptureGenerationContext.State?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(android.app.Application.ActivityLifecycleCallbacks?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.SdkCore?)"
- "java.util.concurrent.atomic.AtomicReference.constructor(com.datadog.android.api.feature.FeatureEventReceiver?)"
Expand Down Expand Up @@ -510,6 +512,7 @@ datadog:
- "kotlin.collections.List.filterIsInstance()"
- "kotlin.collections.List.filterNot(kotlin.Function1)"
- "kotlin.collections.List.filterNotNull(kotlin.Function1)"
- "kotlin.collections.MutableSet.filterNot(kotlin.Function1)"
- "kotlin.collections.List.findFirstForType(java.lang.Class)"
- "kotlin.collections.List.firstOrNull()"
- "kotlin.collections.List.firstOrNull(kotlin.Function1)"
Expand Down Expand Up @@ -605,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 @@ -783,9 +790,13 @@ 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)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.MobileRecord.MobileFullSnapshotRecord)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.MobileRecord.ViewEndRecord)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe)"
- "kotlin.collections.listOf(com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.PlaceholderWireframe)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ data class SessionReplayConfiguration internal constructor(
*
* Disabled by default.
*
* Heatmap recording ([setHeatmapsEnabled]) is not supported by this pipeline yet; combining
* the two will log a warning and no heatmap data will be recorded.
*
* @param enabled whether composition-tree recording should be used.
*/
@ExperimentalSessionReplayApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,45 @@ import android.widget.SeekBar
import android.widget.TextView
import androidx.appcompat.widget.ActionBarContainer
import androidx.appcompat.widget.SwitchCompat
import com.datadog.android.api.InternalLogger
import com.datadog.android.api.feature.FeatureSdkCore
import com.datadog.android.internal.utils.ImageViewUtils
import com.datadog.android.sessionreplay.ImagePrivacy
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
import com.datadog.android.sessionreplay.internal.composition.CapturedSnapshotProducer
import com.datadog.android.sessionreplay.internal.composition.CompositionCapturePipeline
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
import com.datadog.android.sessionreplay.internal.composition.ImmediateCapturedSnapshotProcessor
import com.datadog.android.sessionreplay.internal.composition.ScheduledExecutorCaptureTaskScheduler
import com.datadog.android.sessionreplay.internal.composition.SnapshotCaptureOrchestrator
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
import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder
import com.datadog.android.sessionreplay.internal.recorder.TimeBank
import com.datadog.android.sessionreplay.internal.recorder.mapper.ActionBarContainerMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.ButtonMapper
import com.datadog.android.sessionreplay.internal.recorder.mapper.CheckBoxMapper
Expand Down Expand Up @@ -72,7 +100,11 @@ internal class DefaultRecorderProvider(
private val internalCallback: SessionReplayInternalCallback,
private val heatmapsEnabled: Boolean,
private val compositionTreeRecordingEnabled: Boolean,
private val compositionPipelineFactory: () -> Recorder = { CompositionCapturePipeline() }
private val compositionPipelineFactory: (() -> Recorder)? = null,
private val compositionSnapshotProducerFactory: (
(ActiveWindowSource, RumContextProvider) -> CapturedSnapshotProducer
)? = null,
private val recordingTimeBankFactory: () -> TimeBank = { RecordingTimeBank() }
) : RecorderProvider {

override fun provideSessionReplayRecorder(
Expand All @@ -83,10 +115,23 @@ internal class DefaultRecorderProvider(
application: Application,
embeddedContentSlotRegistry: EmbeddedContentSlotRegistry
): Recorder {
if (heatmapsEnabled && compositionTreeRecordingEnabled) {
sdkCore.internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.USER,
{ HEATMAPS_UNSUPPORTED_WITH_COMPOSITION_RECORDING_MESSAGE }
)
}
val heatmapIdentifierRegistry = if (heatmapsEnabled) LazyHeatmapIdentifierRegistry(sdkCore) else null
return CapturePipelineSelector(
compositionEnabled = compositionTreeRecordingEnabled,
compositionFactory = compositionPipelineFactory,
compositionFactory = {
compositionPipelineFactory?.invoke() ?: createCompositionPipeline(
recordWriter,
rumContextProvider,
application
)
},
legacyFactory = {
SessionReplayRecorder(
application,
Expand All @@ -111,6 +156,100 @@ internal class DefaultRecorderProvider(
).create()
}

private fun createCompositionPipeline(
recordWriter: RecordWriter,
rumContextProvider: RumContextProvider,
application: Application
): Recorder {
val internalLogger = sdkCore.internalLogger
val windowSource = ActiveWindowSource()
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
)
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 = TimeProviderCaptureTimeProvider(sdkCore.timeProvider),
captureScheduler = HandlerCaptureTaskScheduler(),
mainThreadExecutor = HandlerCaptureMainThreadExecutor(),
expiryScheduler = ScheduledExecutorCaptureTaskScheduler(
executorService = sdkCore.createScheduledExecutorService("sr-composition-expiry"),
internalLogger = internalLogger
),
timeBudget = if (dynamicOptimizationEnabled) {
TimeBankCaptureTimeBudget(
recordingTimeBankFactory(),
skippedFrameNotifier::notifySkippedFrame
)
} else {
CaptureTimeBudget.UNLIMITED
},
internalLogger = internalLogger
)
}

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

@Suppress("LongMethod")
private fun builtInMappers(): List<MapperTypeWrapper<*>> {
val viewIdentifierResolver: ViewIdentifierResolver = DefaultViewIdentifierResolver
Expand Down Expand Up @@ -268,4 +407,10 @@ internal class DefaultRecorderProvider(
null
}
}

internal companion object {
internal const val HEATMAPS_UNSUPPORTED_WITH_COMPOSITION_RECORDING_MESSAGE = "Heatmaps are not " +
"supported by the composition-tree recording pipeline yet. No heatmap data will be recorded " +
"for this session."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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.os.Handler
import android.os.Looper
import com.datadog.android.api.InternalLogger
import com.datadog.android.core.internal.utils.scheduleSafe
import com.datadog.android.internal.time.TimeProvider
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit

internal class HandlerCaptureMainThreadExecutor(
private val handler: Handler = Handler(Looper.getMainLooper())
) : CaptureMainThreadExecutor {
override fun execute(task: () -> Unit): CancellableCaptureWork {
val runnable = Runnable(task)
return if (handler.post(runnable)) {
CancellableCaptureWork { handler.removeCallbacks(runnable) }
} else {
CancellableCaptureWork.NONE
}
}
}

internal class HandlerCaptureTaskScheduler(
private val handler: Handler = Handler(Looper.getMainLooper())
) : CaptureTaskScheduler {
override fun schedule(delayNs: Long, task: () -> Unit): CancellableCaptureWork {
val runnable = Runnable(task)
val posted = if (delayNs <= 0) {
handler.post(runnable)
} else {
handler.postDelayed(runnable, delayNs.toDelayMillis())
}
return if (posted) {
CancellableCaptureWork { handler.removeCallbacks(runnable) }
} else {
CancellableCaptureWork.NONE
}
}

private fun Long.toDelayMillis(): Long {
val delayMs = TimeUnit.NANOSECONDS.toMillis(this)
return if (delayMs < 1L) 1L else delayMs
}
}

/** Expiry runs off the main looper so asynchronous generations can time out while UI is busy. */
internal class ScheduledExecutorCaptureTaskScheduler(
private val executorService: ScheduledExecutorService,
private val internalLogger: InternalLogger
) : CaptureTaskScheduler {
override fun schedule(delayNs: Long, task: () -> Unit): CancellableCaptureWork {
val future = executorService.scheduleSafe(
EXPIRY_CONTEXT,
delayNs,
TimeUnit.NANOSECONDS,
internalLogger,
Runnable(task)
) ?: return CancellableCaptureWork.NONE
return CancellableCaptureWork { future.cancel(false) }
}

override fun shutdown() {
executorService.shutdownNow()
}

private companion object {
const val EXPIRY_CONTEXT = "Session Replay composition generation expiry"
}
}

internal class TimeProviderCaptureTimeProvider(
private val timeProvider: TimeProvider
) : CaptureTimeProvider {
override fun elapsedRealtimeNanos(): Long = timeProvider.getDeviceElapsedTimeNanos()
}
Loading
Loading