Skip to content

Commit 4db01f1

Browse files
authored
Merge pull request #1684 from DataDog/jmoskovich/rum-1613/traversal-mapper
RUM-1613 Add mapper interface for traversing all children
2 parents fd0cae0 + 8147727 commit 4db01f1

7 files changed

Lines changed: 115 additions & 30 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
@@ -43,6 +43,7 @@ class com.datadog.android.sessionreplay.internal.recorder.mapper.MaskTextViewMap
4343
open class com.datadog.android.sessionreplay.internal.recorder.mapper.TextViewMapper : BaseAsyncBackgroundWireframeMapper<android.widget.TextView>
4444
constructor()
4545
override fun map(android.widget.TextView, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback): List<com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
46+
interface com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper<T: android.view.View, S: com.datadog.android.sessionreplay.model.MobileSegment.Wireframe> : WireframeMapper<T, S>
4647
interface com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper<T: android.view.View, S: com.datadog.android.sessionreplay.model.MobileSegment.Wireframe>
4748
fun map(T, com.datadog.android.sessionreplay.internal.recorder.MappingContext, com.datadog.android.sessionreplay.internal.AsyncJobStatusCallback = NoOpAsyncJobStatusCallback()): List<S>
4849
object com.datadog.android.sessionreplay.utils.StringUtils

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ public class com/datadog/android/sessionreplay/internal/recorder/mapper/TextView
131131
public fun map (Landroid/widget/TextView;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List;
132132
}
133133

134+
public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/TraverseAllChildrenMapper : com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper {
135+
}
136+
134137
public abstract interface class com/datadog/android/sessionreplay/internal/recorder/mapper/WireframeMapper {
135138
public abstract fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/internal/recorder/MappingContext;Lcom/datadog/android/sessionreplay/internal/AsyncJobStatusCallback;)Ljava/util/List;
136139
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ internal class SnapshotProducer(
4141
val traversedTreeView = treeViewTraversal.traverse(view, mappingContext, recordedDataQueueRefs)
4242
val nextTraversalStrategy = traversedTreeView.nextActionStrategy
4343
val resolvedWireframes = traversedTreeView.mappedWireframes
44-
if (nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE) {
44+
if (nextTraversalStrategy == TraversalStrategy.STOP_AND_DROP_NODE) {
4545
return null
4646
}
47-
if (nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE) {
47+
if (nextTraversalStrategy == TraversalStrategy.STOP_AND_RETURN_NODE) {
4848
return Node(wireframes = resolvedWireframes, parents = parents)
4949
}
5050

5151
val childNodes = LinkedList<Node>()
5252
if (view is ViewGroup &&
5353
view.childCount > 0 &&
54-
nextTraversalStrategy == TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
54+
nextTraversalStrategy == TraversalStrategy.TRAVERSE_ALL_CHILDREN
5555
) {
5656
val childMappingContext = resolveChildMappingContext(view, mappingContext)
5757
val parentsCopy = LinkedList(parents).apply { addAll(resolvedWireframes) }

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

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
1111
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
1212
import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper
1313
import com.datadog.android.sessionreplay.internal.recorder.mapper.QueueableViewMapper
14+
import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper
1415
import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper
1516
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
1617
import com.datadog.android.sessionreplay.model.MobileSegment
@@ -38,11 +39,16 @@ internal class TreeViewTraversal(
3839
val resolvedWireframes: List<MobileSegment.Wireframe>
3940

4041
// try to resolve from the exhaustive type mappers
41-
val exhaustiveTypeMapper = mappers.findFirstForType(view::class.java)
42+
val mapper = mappers.findFirstForType(view::class.java)
4243

43-
if (exhaustiveTypeMapper != null) {
44-
val queueableViewMapper = QueueableViewMapper(exhaustiveTypeMapper, recordedDataQueueRefs)
45-
traversalStrategy = TraversalStrategy.STOP_AND_RETURN_NODE
44+
if (mapper != null) {
45+
val queueableViewMapper =
46+
QueueableViewMapper(mapper, recordedDataQueueRefs)
47+
traversalStrategy = if (mapper is TraverseAllChildrenMapper) {
48+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
49+
} else {
50+
TraversalStrategy.STOP_AND_RETURN_NODE
51+
}
4652
resolvedWireframes = queueableViewMapper.map(view, mappingContext)
4753
} else if (isDecorView(view)) {
4854
traversalStrategy = TraversalStrategy.TRAVERSE_ALL_CHILDREN
@@ -73,10 +79,10 @@ internal class TreeViewTraversal(
7379
val mappedWireframes: List<MobileSegment.Wireframe>,
7480
val nextActionStrategy: TraversalStrategy
7581
)
82+
}
7683

77-
enum class TraversalStrategy {
78-
TRAVERSE_ALL_CHILDREN,
79-
STOP_AND_RETURN_NODE,
80-
STOP_AND_DROP_NODE
81-
}
84+
internal enum class TraversalStrategy {
85+
TRAVERSE_ALL_CHILDREN,
86+
STOP_AND_RETURN_NODE,
87+
STOP_AND_DROP_NODE
8288
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.recorder.mapper
8+
9+
import android.view.View
10+
import com.datadog.android.sessionreplay.model.MobileSegment
11+
12+
/**
13+
* Maps a View to a [List] of [MobileSegment.Wireframe].
14+
* This is mainly used internally by the SDK but if you want to provide a different
15+
* Session Replay representation for a specific View type you can implement this on your end.
16+
* Note that mappers using this interface also traverse all the children of the view
17+
* instead of just the immediate one. This means that you will need to have mappers
18+
* for all child views of the view the mapper is traversing.
19+
*/
20+
interface TraverseAllChildrenMapper<in T : View, out S : MobileSegment.Wireframe> :
21+
WireframeMapper<T, S>

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ internal class SnapshotProducerTest {
6969
val mockRoot: View = mock()
7070
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
7171
fakeViewWireframes,
72-
TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE
72+
TraversalStrategy.STOP_AND_DROP_NODE
7373
)
7474
whenever(mockTreeViewTraversal.traverse(eq(mockRoot), any(), any()))
7575
.thenReturn(fakeTraversedTreeView)
@@ -93,7 +93,7 @@ internal class SnapshotProducerTest {
9393
val fakeRoot = forge.aMockView<View>()
9494
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
9595
fakeViewWireframes,
96-
TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE
96+
TraversalStrategy.STOP_AND_RETURN_NODE
9797
)
9898
whenever(mockTreeViewTraversal.traverse(eq(fakeRoot), any(), any()))
9999
.thenReturn(fakeTraversedTreeView)
@@ -118,7 +118,7 @@ internal class SnapshotProducerTest {
118118
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
119119
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
120120
fakeViewWireframes,
121-
TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE
121+
TraversalStrategy.STOP_AND_RETURN_NODE
122122
)
123123
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
124124
.thenReturn(fakeTraversedTreeView)
@@ -143,7 +143,7 @@ internal class SnapshotProducerTest {
143143
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
144144
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
145145
fakeViewWireframes,
146-
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
146+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
147147
)
148148
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
149149
.thenReturn(fakeTraversedTreeView)
@@ -172,7 +172,7 @@ internal class SnapshotProducerTest {
172172
}
173173
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
174174
fakeViewWireframes,
175-
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
175+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
176176
)
177177
whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView)
178178

@@ -204,7 +204,7 @@ internal class SnapshotProducerTest {
204204
}
205205
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
206206
fakeViewWireframes,
207-
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
207+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
208208
)
209209
whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView)
210210
whenever(mockOptionSelectorDetector.isOptionSelector(mockRoot)).thenReturn(true)
@@ -237,7 +237,7 @@ internal class SnapshotProducerTest {
237237
}
238238
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
239239
fakeViewWireframes,
240-
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
240+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
241241
)
242242
whenever(mockTreeViewTraversal.traverse(any(), any(), any())).thenReturn(fakeTraversedTreeView)
243243
whenever(mockOptionSelectorDetector.isOptionSelector(mockRoot)).thenReturn(false)
@@ -267,14 +267,14 @@ internal class SnapshotProducerTest {
267267
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
268268
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
269269
fakeViewWireframes,
270-
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
270+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
271271
)
272272
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
273273
.thenReturn(fakeTraversedTreeView)
274274
.thenReturn(
275275
fakeTraversedTreeView.copy(
276276
nextActionStrategy =
277-
TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE
277+
TraversalStrategy.STOP_AND_RETURN_NODE
278278
)
279279
)
280280
var expectedSnapshot = fakeRoot.toNode(viewMappedWireframes = fakeViewWireframes)
@@ -303,14 +303,14 @@ internal class SnapshotProducerTest {
303303
val fakeRoot = forge.aMockViewWithChildren(2, 0, 2)
304304
val fakeTraversedTreeView = TreeViewTraversal.TraversedTreeView(
305305
fakeViewWireframes,
306-
TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN
306+
TraversalStrategy.TRAVERSE_ALL_CHILDREN
307307
)
308308
whenever(mockTreeViewTraversal.traverse(any(), any(), any()))
309309
.thenReturn(fakeTraversedTreeView)
310310
.thenReturn(
311311
fakeTraversedTreeView.copy(
312312
nextActionStrategy =
313-
TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE
313+
TraversalStrategy.STOP_AND_DROP_NODE
314314
)
315315
)
316316
val expectedSnapshot = fakeRoot.toNode(viewMappedWireframes = fakeViewWireframes)

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

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.datadog.android.sessionreplay.forge.ForgeConfigurator
1717
import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs
1818
import com.datadog.android.sessionreplay.internal.recorder.mapper.DecorViewMapper
1919
import com.datadog.android.sessionreplay.internal.recorder.mapper.MapperTypeWrapper
20+
import com.datadog.android.sessionreplay.internal.recorder.mapper.TraverseAllChildrenMapper
2021
import com.datadog.android.sessionreplay.internal.recorder.mapper.ViewWireframeMapper
2122
import com.datadog.android.sessionreplay.internal.recorder.mapper.WireframeMapper
2223
import com.datadog.android.sessionreplay.model.MobileSegment
@@ -90,7 +91,9 @@ internal class TreeViewTraversalTest {
9091
)
9192
val fakeTypes: List<Class<*>> = mockViews.map { it::class.java }
9293
val fakeTypeToMapperMap: Map<Class<*>, WireframeMapper<View, *>> = fakeTypes
93-
.associateWith { mock() }
94+
.associateWith {
95+
mock()
96+
}
9497
val fakeTypeMapperWrappers = fakeTypes.map {
9598
val mapper = fakeTypeToMapperMap[it]!!
9699
MapperTypeWrapper(it, mapper)
@@ -121,7 +124,7 @@ internal class TreeViewTraversalTest {
121124
// Then
122125
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
123126
assertThat(traversedTreeView.nextActionStrategy)
124-
.isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_RETURN_NODE)
127+
.isEqualTo(TraversalStrategy.STOP_AND_RETURN_NODE)
125128
}
126129

127130
@Test
@@ -161,7 +164,58 @@ internal class TreeViewTraversalTest {
161164
// Then
162165
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
163166
assertThat(traversedTreeView.nextActionStrategy)
164-
.isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN)
167+
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
168+
}
169+
170+
@Test
171+
fun `M use TRAVERSE_ALL_CHILDREN traversal strategy W traverse { TraverseAllChildrenMapper }`(
172+
forge: Forge
173+
) {
174+
// Given
175+
val fakeViewMappedWireframes: List<MobileSegment.Wireframe> = forge.aList { getForgery() }
176+
val mockViews: List<View> = listOf(
177+
forge.aMockView<RadioButton>(),
178+
forge.aMockView<CompoundButton>(),
179+
forge.aMockView<CheckedTextView>(),
180+
forge.aMockView<Button>(),
181+
forge.aMockView<TextView>()
182+
)
183+
val fakeTypes: List<Class<*>> = mockViews.map { it::class.java }
184+
val fakeTypeToMapperMap: Map<Class<*>, TraverseAllChildrenMapper<View, *>> = fakeTypes
185+
.associateWith {
186+
mock()
187+
}
188+
val fakeTypeMapperWrappers = fakeTypes.map {
189+
val mapper = fakeTypeToMapperMap[it]!!
190+
MapperTypeWrapper(it, mapper)
191+
}
192+
val mockView = forge.anElementFrom(mockViews)
193+
whenever(
194+
fakeTypeToMapperMap[mockView::class.java]!!.map(
195+
eq(mockView),
196+
eq(fakeMappingContext),
197+
any()
198+
)
199+
)
200+
.thenReturn(fakeViewMappedWireframes)
201+
testedTreeViewTraversal = TreeViewTraversal(
202+
fakeTypeMapperWrappers,
203+
mockViewMapper,
204+
mockDecorViewMapper,
205+
mockViewUtilsInternal
206+
)
207+
208+
// When
209+
val traversedTreeView = testedTreeViewTraversal.traverse(
210+
mockView,
211+
fakeMappingContext,
212+
mockRecordedDataQueueRefs
213+
)
214+
215+
// Then
216+
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
217+
assertThat(traversedTreeView.nextActionStrategy)
218+
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
165219
}
166220

167221
@Test
@@ -193,7 +247,7 @@ internal class TreeViewTraversalTest {
193247
// Then
194248
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
195249
assertThat(traversedTreeView.nextActionStrategy)
196-
.isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN)
250+
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
197251
}
198252

199253
@Test
@@ -226,7 +280,7 @@ internal class TreeViewTraversalTest {
226280
// Then
227281
assertThat(traversedTreeView.mappedWireframes).isEqualTo(fakeViewMappedWireframes)
228282
assertThat(traversedTreeView.nextActionStrategy)
229-
.isEqualTo(TreeViewTraversal.TraversalStrategy.TRAVERSE_ALL_CHILDREN)
283+
.isEqualTo(TraversalStrategy.TRAVERSE_ALL_CHILDREN)
230284
}
231285

232286
// endregion
@@ -250,7 +304,7 @@ internal class TreeViewTraversalTest {
250304
// Then
251305
assertThat(traversedTreeView.mappedWireframes).isEmpty()
252306
assertThat(traversedTreeView.nextActionStrategy)
253-
.isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE)
307+
.isEqualTo(TraversalStrategy.STOP_AND_DROP_NODE)
254308
}
255309

256310
// endregion
@@ -274,7 +328,7 @@ internal class TreeViewTraversalTest {
274328
// Then
275329
assertThat(traversedTreeView.mappedWireframes).isEmpty()
276330
assertThat(traversedTreeView.nextActionStrategy)
277-
.isEqualTo(TreeViewTraversal.TraversalStrategy.STOP_AND_DROP_NODE)
331+
.isEqualTo(TraversalStrategy.STOP_AND_DROP_NODE)
278332
}
279333

280334
// endregion

0 commit comments

Comments
 (0)