Skip to content

Commit 7345650

Browse files
committed
PANA-8500: Add composition pipeline selection
1 parent 6134faa commit 7345650

12 files changed

Lines changed: 356 additions & 29 deletions

File tree

features/dd-sdk-android-session-replay/api/apiSurface

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ data class com.datadog.android.sessionreplay.SessionReplayConfiguration
4848
fun setDynamicOptimizationEnabled(Boolean): Builder
4949
fun setSystemRequirements(SystemRequirementsConfiguration): Builder
5050
fun setHeatmapsEnabled(Boolean): Builder
51+
fun setCompositionTreeRecordingEnabled(Boolean): Builder
5152
fun build(): SessionReplayConfiguration
5253
interface com.datadog.android.sessionreplay.SessionReplayInternalCallback
5354
fun getCurrentActivity(): android.app.Activity?

features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ public final class com/datadog/android/sessionreplay/SessionReplay {
6464
}
6565

6666
public final class com/datadog/android/sessionreplay/SessionReplayConfiguration {
67-
public final fun copy (Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;
68-
public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;ZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;
67+
public final fun copy (Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;ZZ)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;
68+
public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;Ljava/lang/String;Lcom/datadog/android/sessionreplay/SessionReplayPrivacy;Ljava/util/List;Ljava/util/List;Ljava/util/List;FLcom/datadog/android/sessionreplay/ImagePrivacy;ZLcom/datadog/android/sessionreplay/TouchPrivacy;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;ZLcom/datadog/android/sessionreplay/SystemRequirementsConfiguration;Lcom/datadog/android/sessionreplay/SessionReplayInternalCallback;ZZILjava/lang/Object;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;
6969
public fun equals (Ljava/lang/Object;)Z
7070
public fun hashCode ()I
7171
public fun toString ()Ljava/lang/String;
@@ -77,6 +77,7 @@ public final class com/datadog/android/sessionreplay/SessionReplayConfiguration$
7777
public synthetic fun <init> (FILkotlin/jvm/internal/DefaultConstructorMarker;)V
7878
public final fun addExtensionSupport (Lcom/datadog/android/sessionreplay/ExtensionSupport;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;
7979
public final fun build ()Lcom/datadog/android/sessionreplay/SessionReplayConfiguration;
80+
public final fun setCompositionTreeRecordingEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;
8081
public final fun setDynamicOptimizationEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;
8182
public final fun setHeatmapsEnabled (Z)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;
8283
public final fun setImagePrivacy (Lcom/datadog/android/sessionreplay/ImagePrivacy;)Lcom/datadog/android/sessionreplay/SessionReplayConfiguration$Builder;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ object SessionReplay {
6161
startRecordingImmediately = sessionReplayConfiguration.startRecordingImmediately,
6262
dynamicOptimizationEnabled = sessionReplayConfiguration.dynamicOptimizationEnabled,
6363
internalCallback = sessionReplayConfiguration.internalCallback,
64-
heatmapsEnabled = sessionReplayConfiguration.heatmapsEnabled
64+
heatmapsEnabled = sessionReplayConfiguration.heatmapsEnabled,
65+
compositionTreeRecordingEnabled = sessionReplayConfiguration.compositionTreeRecordingEnabled
6566
)
6667

6768
if (isAlreadyRegistered()) {

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
package com.datadog.android.sessionreplay
88

9+
import android.view.View
910
import androidx.annotation.FloatRange
1011
import com.datadog.android.api.InternalLogger
1112
import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder
@@ -30,7 +31,8 @@ data class SessionReplayConfiguration internal constructor(
3031
internal val dynamicOptimizationEnabled: Boolean,
3132
internal val systemRequirementsConfiguration: SystemRequirementsConfiguration,
3233
internal val internalCallback: SessionReplayInternalCallback,
33-
internal val heatmapsEnabled: Boolean
34+
internal val heatmapsEnabled: Boolean,
35+
internal val compositionTreeRecordingEnabled: Boolean
3436
) {
3537

3638
/**
@@ -78,6 +80,7 @@ data class SessionReplayConfiguration internal constructor(
7880
private var systemRequirementsConfiguration = SystemRequirementsConfiguration.NONE
7981
private var internalCallback: SessionReplayInternalCallback = NoOpSessionReplayInternalCallback()
8082
private var heatmapsEnabled = false
83+
private var compositionTreeRecordingEnabled = false
8184

8285
/**
8386
* Adds an extension support implementation. This is mostly used when you want to provide
@@ -239,6 +242,23 @@ data class SessionReplayConfiguration internal constructor(
239242
return this
240243
}
241244

245+
/**
246+
* Enables the experimental composition-tree recording pipeline.
247+
*
248+
* The default recorder builds replay wireframes from semantic information extracted from
249+
* Android [View] properties. This pipeline uses the rendered composition tree as its primary
250+
* source, preserving its structure and rendering effects to improve replay fidelity in
251+
* Android View and Jetpack Compose applications.
252+
*
253+
* Disabled by default.
254+
*
255+
* @param enabled whether composition-tree recording should be used.
256+
*/
257+
fun setCompositionTreeRecordingEnabled(enabled: Boolean): Builder {
258+
compositionTreeRecordingEnabled = enabled
259+
return this
260+
}
261+
242262
/**
243263
* Builds a [SessionReplayConfiguration] based on the current state of this Builder.
244264
*/
@@ -257,7 +277,8 @@ data class SessionReplayConfiguration internal constructor(
257277
dynamicOptimizationEnabled = dynamicOptimizationEnabled,
258278
systemRequirementsConfiguration = systemRequirementsConfiguration,
259279
internalCallback = internalCallback,
260-
heatmapsEnabled = heatmapsEnabled
280+
heatmapsEnabled = heatmapsEnabled,
281+
compositionTreeRecordingEnabled = compositionTreeRecordingEnabled
261282
)
262283
}
263284

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

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import com.datadog.android.sessionreplay.ImagePrivacy
2727
import com.datadog.android.sessionreplay.MapperTypeWrapper
2828
import com.datadog.android.sessionreplay.SessionReplayInternalCallback
2929
import com.datadog.android.sessionreplay.TextAndInputPrivacy
30+
import com.datadog.android.sessionreplay.internal.composition.CapturePipelineSelector
31+
import com.datadog.android.sessionreplay.internal.composition.CompositionCapturePipeline
3032
import com.datadog.android.sessionreplay.internal.recorder.Recorder
3133
import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder
3234
import com.datadog.android.sessionreplay.internal.recorder.mapper.ActionBarContainerMapper
@@ -67,7 +69,9 @@ internal class DefaultRecorderProvider(
6769
private val customDrawableMappers: List<DrawableToColorMapper>,
6870
private val dynamicOptimizationEnabled: Boolean,
6971
private val internalCallback: SessionReplayInternalCallback,
70-
private val heatmapsEnabled: Boolean
72+
private val heatmapsEnabled: Boolean,
73+
private val compositionTreeRecordingEnabled: Boolean,
74+
private val compositionPipelineFactory: () -> Recorder = { CompositionCapturePipeline() }
7175
) : RecorderProvider {
7276

7377
override fun provideSessionReplayRecorder(
@@ -77,24 +81,34 @@ internal class DefaultRecorderProvider(
7781
rumContextProvider: RumContextProvider,
7882
application: Application
7983
): Recorder {
80-
return SessionReplayRecorder(
81-
application,
82-
resourceDataStoreManager = resourceDataStoreManager,
83-
resourcesWriter = resourceWriter,
84-
rumContextProvider = rumContextProvider,
85-
imagePrivacy = imagePrivacy,
86-
touchPrivacyManager = touchPrivacyManager,
87-
textAndInputPrivacy = textAndInputPrivacy,
88-
recordWriter = recordWriter,
89-
timeProvider = sdkCore.timeProvider,
90-
mappers = customMappers + builtInMappers(),
91-
customOptionSelectorDetectors = customOptionSelectorDetectors,
92-
customDrawableMappers = customDrawableMappers,
93-
sdkCore = sdkCore,
94-
dynamicOptimizationEnabled = dynamicOptimizationEnabled,
95-
internalCallback = internalCallback,
96-
heatmapIdentifierRegistry = if (heatmapsEnabled) LazyHeatmapIdentifierRegistry(sdkCore) else null
97-
)
84+
return CapturePipelineSelector(
85+
compositionEnabled = compositionTreeRecordingEnabled,
86+
compositionFactory = compositionPipelineFactory,
87+
legacyFactory = {
88+
SessionReplayRecorder(
89+
application,
90+
resourceDataStoreManager = resourceDataStoreManager,
91+
resourcesWriter = resourceWriter,
92+
rumContextProvider = rumContextProvider,
93+
imagePrivacy = imagePrivacy,
94+
touchPrivacyManager = touchPrivacyManager,
95+
textAndInputPrivacy = textAndInputPrivacy,
96+
recordWriter = recordWriter,
97+
timeProvider = sdkCore.timeProvider,
98+
mappers = customMappers + builtInMappers(),
99+
customOptionSelectorDetectors = customOptionSelectorDetectors,
100+
customDrawableMappers = customDrawableMappers,
101+
sdkCore = sdkCore,
102+
dynamicOptimizationEnabled = dynamicOptimizationEnabled,
103+
internalCallback = internalCallback,
104+
heatmapIdentifierRegistry = if (heatmapsEnabled) {
105+
LazyHeatmapIdentifierRegistry(sdkCore)
106+
} else {
107+
null
108+
}
109+
)
110+
}
111+
).create()
98112
}
99113

100114
@Suppress("LongMethod")

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ internal class SessionReplayFeature(
7575
startRecordingImmediately: Boolean,
7676
dynamicOptimizationEnabled: Boolean,
7777
internalCallback: SessionReplayInternalCallback,
78-
heatmapsEnabled: Boolean
78+
heatmapsEnabled: Boolean,
79+
compositionTreeRecordingEnabled: Boolean
7980
) : this(
8081
sdkCore,
8182
customEndpointUrl,
@@ -95,7 +96,8 @@ internal class SessionReplayFeature(
9596
customDrawableMappers,
9697
dynamicOptimizationEnabled,
9798
internalCallback,
98-
heatmapsEnabled
99+
heatmapsEnabled,
100+
compositionTreeRecordingEnabled
99101
)
100102
)
101103

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.sessionreplay.internal.composition
8+
9+
import com.datadog.android.sessionreplay.internal.recorder.Recorder
10+
11+
internal class CapturePipelineSelector(
12+
private val compositionEnabled: Boolean,
13+
private val legacyFactory: () -> Recorder,
14+
private val compositionFactory: () -> Recorder
15+
) {
16+
fun create(): Recorder = if (compositionEnabled) compositionFactory() else legacyFactory()
17+
}
18+
19+
/**
20+
* Lifecycle entry point for the composition capture pipeline. Traversal and capture are added by
21+
* later workstreams; keeping this recorder isolated prevents legacy state from crossing pipelines.
22+
*/
23+
internal class CompositionCapturePipeline : Recorder {
24+
override fun registerCallbacks() = Unit
25+
26+
override fun unregisterCallbacks() = Unit
27+
28+
override fun stopProcessingRecords() = Unit
29+
30+
override fun resumeRecorders() = Unit
31+
32+
override fun stopRecorders() = Unit
33+
}

features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/SessionReplayConfigurationBuilderTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ internal class SessionReplayConfigurationBuilderTest {
7676
assertThat(sessionReplayConfiguration.customOptionSelectorDetectors).isEmpty()
7777
assertThat(sessionReplayConfiguration.dynamicOptimizationEnabled).isEqualTo(true)
7878
assertThat(sessionReplayConfiguration.heatmapsEnabled).isFalse()
79+
assertThat(sessionReplayConfiguration.compositionTreeRecordingEnabled).isFalse()
7980
}
8081

8182
@Test
@@ -89,6 +90,17 @@ internal class SessionReplayConfigurationBuilderTest {
8990
assertThat(sessionReplayConfiguration.heatmapsEnabled).isTrue()
9091
}
9192

93+
@Test
94+
fun `M enable composition tree recording W setCompositionTreeRecordingEnabled()`() {
95+
// When
96+
val result = testedBuilder.setCompositionTreeRecordingEnabled(true)
97+
val sessionReplayConfiguration = result.build()
98+
99+
// Then
100+
assertThat(result).isSameAs(testedBuilder)
101+
assertThat(sessionReplayConfiguration.compositionTreeRecordingEnabled).isTrue()
102+
}
103+
92104
@Test
93105
fun `M build config with custom site W useCustomEndpoint() and build()`(
94106
@StringForgery(regex = "https://[a-z]+\\.com(/[a-z]+)+") sessionReplayUrl: String

features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/SessionReplayConfigurationForgeryFactory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class SessionReplayConfigurationForgeryFactory : ForgeryFactory<SessionReplayCon
3636
.setMinRAMSizeMb(forge.aSmallInt())
3737
.setMinCPUCoreNumber(forge.aSmallInt())
3838
.build(),
39-
heatmapsEnabled = forge.aBool()
39+
heatmapsEnabled = forge.aBool(),
40+
compositionTreeRecordingEnabled = forge.aBool()
4041
)
4142
}
4243
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.sessionreplay.internal
8+
9+
import android.app.Application
10+
import com.datadog.android.api.feature.FeatureSdkCore
11+
import com.datadog.android.sessionreplay.ImagePrivacy
12+
import com.datadog.android.sessionreplay.SessionReplayInternalCallback
13+
import com.datadog.android.sessionreplay.TextAndInputPrivacy
14+
import com.datadog.android.sessionreplay.internal.recorder.Recorder
15+
import com.datadog.android.sessionreplay.internal.recorder.SessionReplayRecorder
16+
import com.datadog.android.sessionreplay.internal.resources.ResourceDataStoreManager
17+
import com.datadog.android.sessionreplay.internal.storage.RecordWriter
18+
import com.datadog.android.sessionreplay.internal.storage.ResourcesWriter
19+
import com.datadog.android.sessionreplay.internal.utils.RumContextProvider
20+
import org.assertj.core.api.Assertions.assertThat
21+
import org.junit.jupiter.api.Test
22+
import org.mockito.kotlin.any
23+
import org.mockito.kotlin.mock
24+
import org.mockito.kotlin.whenever
25+
26+
internal class DefaultRecorderProviderTest {
27+
28+
@Test
29+
fun `M create only legacy recorder W provideSessionReplayRecorder { flag omitted }`() {
30+
// Given
31+
val sdkCore = mock<FeatureSdkCore>()
32+
whenever(sdkCore.timeProvider).thenReturn(mock())
33+
whenever(sdkCore.internalLogger).thenReturn(mock())
34+
whenever(sdkCore.createSingleThreadExecutorService(any())).thenReturn(mock())
35+
var compositionConstructions = 0
36+
val provider = DefaultRecorderProvider(
37+
sdkCore = sdkCore,
38+
textAndInputPrivacy = TextAndInputPrivacy.MASK_ALL,
39+
imagePrivacy = ImagePrivacy.MASK_ALL,
40+
touchPrivacyManager = mock<TouchPrivacyManager>(),
41+
customMappers = emptyList(),
42+
customOptionSelectorDetectors = emptyList(),
43+
customDrawableMappers = emptyList(),
44+
dynamicOptimizationEnabled = false,
45+
internalCallback = mock<SessionReplayInternalCallback>(),
46+
heatmapsEnabled = false,
47+
compositionTreeRecordingEnabled = false,
48+
compositionPipelineFactory = {
49+
compositionConstructions++
50+
mock()
51+
}
52+
)
53+
54+
// When
55+
val result = provider.provideSessionReplayRecorder(
56+
resourceDataStoreManager = mock<ResourceDataStoreManager>(),
57+
resourceWriter = mock<ResourcesWriter>(),
58+
recordWriter = mock<RecordWriter>(),
59+
rumContextProvider = mock<RumContextProvider>(),
60+
application = mock<Application>()
61+
)
62+
63+
// Then
64+
assertThat(result).isInstanceOf(SessionReplayRecorder::class.java)
65+
assertThat(compositionConstructions).isZero()
66+
}
67+
68+
@Test
69+
fun `M create only composition recorder W provideSessionReplayRecorder { flag enabled }`() {
70+
// Given
71+
val compositionRecorder = mock<Recorder>()
72+
var compositionConstructions = 0
73+
val provider = DefaultRecorderProvider(
74+
sdkCore = mock<FeatureSdkCore>(),
75+
textAndInputPrivacy = TextAndInputPrivacy.MASK_ALL,
76+
imagePrivacy = ImagePrivacy.MASK_ALL,
77+
touchPrivacyManager = mock<TouchPrivacyManager>(),
78+
customMappers = emptyList(),
79+
customOptionSelectorDetectors = emptyList(),
80+
customDrawableMappers = emptyList(),
81+
dynamicOptimizationEnabled = false,
82+
internalCallback = mock<SessionReplayInternalCallback>(),
83+
heatmapsEnabled = false,
84+
compositionTreeRecordingEnabled = true,
85+
compositionPipelineFactory = {
86+
compositionConstructions++
87+
compositionRecorder
88+
}
89+
)
90+
91+
// When
92+
val result = provider.provideSessionReplayRecorder(
93+
resourceDataStoreManager = mock<ResourceDataStoreManager>(),
94+
resourceWriter = mock<ResourcesWriter>(),
95+
recordWriter = mock<RecordWriter>(),
96+
rumContextProvider = mock<RumContextProvider>(),
97+
application = mock<Application>()
98+
)
99+
100+
// Then
101+
assertThat(result).isSameAs(compositionRecorder)
102+
assertThat(compositionConstructions).isEqualTo(1)
103+
}
104+
}

0 commit comments

Comments
 (0)