diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 125ef59455..3ea2d33334 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -43,6 +43,7 @@ class com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMap open class com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper : BaseAsyncBackgroundWireframeMapper constructor() override fun map(android.widget.TextView, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback): List +interface com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper : WireframeMapper interface com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback = NoOpAsyncJobStatusCallback()): List object com.datadog.android.sessionreplay.utils.StringUtils diff --git a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api index 0e710e8d78..ea2d676062 100644 --- a/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api +++ b/features/dd-sdk-android-session-replay/api/dd-sdk-android-session-replay.api @@ -131,6 +131,9 @@ public class com/datadog/android/sessionreplay/internal/recorder/mapper/TextView public fun map (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List; } +public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper { +} + public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper { public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List; } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt index 2c220eef10..8a213cc2e2 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducer.kt @@ -41,17 +41,17 @@ internal class SnapshotProducer( val traversedTreeView = treeViewTraversal.traverse(view, mappingContext, recordedDataQueueRefs) val nextTraversalStrategy = traversedTreeView.nextActionStrategy val resolvedWireframes = traversedTreeView.mappedWireframes - if (nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE) { + if (nextTraversalStrategy == TraversalStrategy.STOP_AND_DROP_NODE) { return null } - if (nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE) { + if (nextTraversalStrategy == TraversalStrategy.STOP_AND_RETURN_NODE) { return Node(wireframes = resolvedWireframes, parents = parents) } val childNodes = LinkedList() if (view is ViewGroup && view.childCount > 0 && - nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + nextTraversalStrategy == TraversalStrategy.TRAVERSE_ALL_CHILDREN ) { val childMappingContext = resolveChildMappingContext(view, mappingContext) val parentsCopy = LinkedList(parents).apply { addAll(resolvedWireframes) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt index 1a80128ef1..60f2ba28d4 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversal.kt @@ -11,6 +11,7 @@ import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper import com.datadog.android.sessionreplay.internal.recorder.mapper.QueueableViewMapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.model.MobileSegment @@ -38,11 +39,16 @@ internal class TreeViewTraversal( val resolvedWireframes: List // try to resolve from the exhaustive type mappers - val exhaustiveTypeMapper = mappers.findFirstForType(view::class.java) + val mapper = mappers.findFirstForType(view::class.java) - if (exhaustiveTypeMapper != null) { - val queueableViewMapper = QueueableViewMapper(exhaustiveTypeMapper, recordedDataQueueRefs) - traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE + if (mapper != null) { + val queueableViewMapper = + QueueableViewMapper(mapper, recordedDataQueueRefs) + traversalStrategy = if (mapper is TraverseAllChildrenMapper) { + TraversalStrategy.TRAVERSE_ALL_CHILDREN + } else { + TraversalStrategy.STOP_AND_RETURN_NODE + } resolvedWireframes = queueableViewMapper.map(view, mappingContext) } else if (isDecorView(view)) { traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN @@ -73,10 +79,10 @@ internal class TreeViewTraversal( val mappedWireframes: List, val nextActionStrategy: TraversalStrategy ) +} - enum class TraversalStrategy { - TRAVERSE_ALL_CHILDREN, - STOP_AND_RETURN_NODE, - STOP_AND_DROP_NODE - } +internal enum class TraversalStrategy { + TRAVERSE_ALL_CHILDREN, + STOP_AND_RETURN_NODE, + STOP_AND_DROP_NODE } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper.kt new file mode 100644 index 0000000000..3aab9c4dab --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper.kt @@ -0,0 +1,21 @@ +/* + * 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.recorder.mapper + +import android.view.View +import com.datadog.android.sessionreplay.model.MobileSegment + +/** + * Maps a View to a [List] of [MobileSegment.Wireframe]. + * This is mainly used internally by the SDK but if you want to provide a different + * Session Replay representation for a specific View type you can implement this on your end. + * Note that mappers using this interface also traverse all the children of the view + * instead of just the immediate one. This means that you will need to have mappers + * for all child views of the view the mapper is traversing. + */ +interface TraverseAllChildrenMapper : + WireframeMapper diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt index 9aaba94d98..dd753b4a1e 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/SnapshotProducerTest.kt @@ -69,7 +69,7 @@ internal class SnapshotProducerTest { val mockRoot: View = mock() val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE + TraversalStrategy.STOP_AND_DROP_NODE ) whenever(mockTreeViewTraversal.traverse(eq(mockRoot), any(), any())) .thenReturn(fakeTraversedTreeView) @@ -93,7 +93,7 @@ internal class SnapshotProducerTest { val fakeRoot = forge.aMockView() val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE + TraversalStrategy.STOP_AND_RETURN_NODE ) whenever(mockTreeViewTraversal.traverse(eq(fakeRoot), any(), any())) .thenReturn(fakeTraversedTreeView) @@ -118,7 +118,7 @@ internal class SnapshotProducerTest { val fakeRoot = forge.aMockViewWithChildren(2, 0, 2) val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE + TraversalStrategy.STOP_AND_RETURN_NODE ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())) .thenReturn(fakeTraversedTreeView) @@ -143,7 +143,7 @@ internal class SnapshotProducerTest { val fakeRoot = forge.aMockViewWithChildren(2, 0, 2) val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + TraversalStrategy.TRAVERSE_ALL_CHILDREN ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())) .thenReturn(fakeTraversedTreeView) @@ -172,7 +172,7 @@ internal class SnapshotProducerTest { } val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + TraversalStrategy.TRAVERSE_ALL_CHILDREN ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView) @@ -204,7 +204,7 @@ internal class SnapshotProducerTest { } val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + TraversalStrategy.TRAVERSE_ALL_CHILDREN ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView) whenever(mockOptionSelectorDetector.isOptionSelector(mockRoot)).thenReturn(true) @@ -237,7 +237,7 @@ internal class SnapshotProducerTest { } val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + TraversalStrategy.TRAVERSE_ALL_CHILDREN ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView) whenever(mockOptionSelectorDetector.isOptionSelector(mockRoot)).thenReturn(false) @@ -267,14 +267,14 @@ internal class SnapshotProducerTest { val fakeRoot = forge.aMockViewWithChildren(2, 0, 2) val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + TraversalStrategy.TRAVERSE_ALL_CHILDREN ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())) .thenReturn(fakeTraversedTreeView) .thenReturn( fakeTraversedTreeView.copy( nextActionStrategy = - TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE + TraversalStrategy.STOP_AND_RETURN_NODE ) ) var expectedSnapshot = fakeRoot.toNode(viewMappedWireframes = fakeViewWireframes) @@ -303,14 +303,14 @@ internal class SnapshotProducerTest { val fakeRoot = forge.aMockViewWithChildren(2, 0, 2) val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView( fakeViewWireframes, - TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN + TraversalStrategy.TRAVERSE_ALL_CHILDREN ) whenever(mockTreeViewTraversal.traverse(any(), any(), any())) .thenReturn(fakeTraversedTreeView) .thenReturn( fakeTraversedTreeView.copy( nextActionStrategy = - TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE + TraversalStrategy.STOP_AND_DROP_NODE ) ) val expectedSnapshot = fakeRoot.toNode(viewMappedWireframes = fakeViewWireframes) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversalTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversalTest.kt index be261021d6..85aa0abc38 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversalTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/TreeViewTraversalTest.kt @@ -17,6 +17,7 @@ import com.datadog.android.sessionreplay.forge.ForgeConfigurator import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper +import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper import com.datadog.android.sessionreplay.model.MobileSegment @@ -90,7 +91,9 @@ internal class TreeViewTraversalTest { ) val fakeTypes: List> = mockViews.map { it::class.java } val fakeTypeToMapperMap: Map, WireframeMapper> = fakeTypes - .associateWith { mock() } + .associateWith { + mock() + } val fakeTypeMapperWrappers = fakeTypes.map { val mapper = fakeTypeToMapperMap[it]!! MapperTypeWrapper(it, mapper) @@ -121,7 +124,7 @@ internal class TreeViewTraversalTest { // Then assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes) assertThat(traversedTreeView.nextActionStrategy) - .isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE) + .isEqualTo(TraversalStrategy.STOP_AND_RETURN_NODE) } @Test @@ -161,7 +164,58 @@ internal class TreeViewTraversalTest { // Then assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes) assertThat(traversedTreeView.nextActionStrategy) - .isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN) + .isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN) + } + + @Test + fun `M use TRAVERSE_ALL_CHILDREN traversal strategy W traverse { TraverseAllChildrenMapper }`( + forge: Forge + ) { + // Given + val fakeViewMappedWireframes: List = forge.aList { getForgery() } + val mockViews: List = listOf( + forge.aMockView(), + forge.aMockView(), + forge.aMockView(), + forge.aMockView