Skip to content

Commit 3645230

Browse files
committed
RUM-9345: Add Compose custom attributes for actions tracking
1 parent 131a0a2 commit 3645230

File tree

14 files changed

+276
-31
lines changed

14 files changed

+276
-31
lines changed

features/dd-sdk-android-rum/api/apiSurface

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ object com.datadog.android.rum.RumAttributes
5151
const val ACTION_TARGET_PARENT_CLASSNAME: String
5252
const val ACTION_TARGET_PARENT_RESOURCE_ID: String
5353
const val ACTION_TARGET_RESOURCE_ID: String
54+
const val ACTION_TARGET_SELECTED: String
55+
const val ACTION_TARGET_ROLE: String
5456
const val ACTION_GESTURE_DIRECTION: String
5557
const val ACTION_GESTURE_FROM_STATE: String
5658
const val ACTION_GESTURE_TO_STATE: String
@@ -300,9 +302,11 @@ interface com.datadog.android.rum.tracking.TrackingStrategy
300302
interface com.datadog.android.rum.tracking.ViewAttributesProvider
301303
fun extractAttributes(android.view.View, MutableMap<String, Any?>)
302304
class com.datadog.android.rum.tracking.ViewTarget
303-
constructor(java.lang.ref.WeakReference<android.view.View?> = WeakReference(null), String? = null)
305+
constructor(java.lang.ref.WeakReference<android.view.View?> = WeakReference(null), Node? = null)
304306
override fun equals(Any?): Boolean
305307
override fun hashCode(): Int
308+
data class com.datadog.android.rum.tracking.Node
309+
constructor(String, Map<String, Any?> = mapOf())
306310
interface com.datadog.android.rum.tracking.ViewTrackingStrategy : TrackingStrategy
307311
class com.datadog.android.sqlite.DatadogDatabaseErrorHandler : android.database.DatabaseErrorHandler
308312
constructor(String? = null, android.database.DatabaseErrorHandler = DefaultDatabaseErrorHandler())

features/dd-sdk-android-rum/api/dd-sdk-android-rum.api

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public final class com/datadog/android/rum/RumAttributes {
5353
public static final field ACTION_TARGET_PARENT_INDEX Ljava/lang/String;
5454
public static final field ACTION_TARGET_PARENT_RESOURCE_ID Ljava/lang/String;
5555
public static final field ACTION_TARGET_RESOURCE_ID Ljava/lang/String;
56+
public static final field ACTION_TARGET_ROLE Ljava/lang/String;
57+
public static final field ACTION_TARGET_SELECTED Ljava/lang/String;
5658
public static final field ACTION_TARGET_TITLE Ljava/lang/String;
5759
public static final field APPLICATION_VERSION Ljava/lang/String;
5860
public static final field CUSTOM_INV_VALUE Ljava/lang/String;
@@ -6857,6 +6859,20 @@ public final class com/datadog/android/rum/tracking/NavigationViewTrackingStrate
68576859
public final fun stopTracking ()V
68586860
}
68596861

6862+
public final class com/datadog/android/rum/tracking/Node {
6863+
public fun <init> (Ljava/lang/String;Ljava/util/Map;)V
6864+
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
6865+
public final fun component1 ()Ljava/lang/String;
6866+
public final fun component2 ()Ljava/util/Map;
6867+
public final fun copy (Ljava/lang/String;Ljava/util/Map;)Lcom/datadog/android/rum/tracking/Node;
6868+
public static synthetic fun copy$default (Lcom/datadog/android/rum/tracking/Node;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/datadog/android/rum/tracking/Node;
6869+
public fun equals (Ljava/lang/Object;)Z
6870+
public final fun getCustomAttributes ()Ljava/util/Map;
6871+
public final fun getName ()Ljava/lang/String;
6872+
public fun hashCode ()I
6873+
public fun toString ()Ljava/lang/String;
6874+
}
6875+
68606876
public abstract interface class com/datadog/android/rum/tracking/TrackingStrategy {
68616877
public abstract fun register (Lcom/datadog/android/api/SdkCore;Landroid/content/Context;)V
68626878
public abstract fun unregister (Landroid/content/Context;)V
@@ -6868,10 +6884,10 @@ public abstract interface class com/datadog/android/rum/tracking/ViewAttributesP
68686884

68696885
public final class com/datadog/android/rum/tracking/ViewTarget {
68706886
public fun <init> ()V
6871-
public fun <init> (Ljava/lang/ref/WeakReference;Ljava/lang/String;)V
6872-
public synthetic fun <init> (Ljava/lang/ref/WeakReference;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
6887+
public fun <init> (Ljava/lang/ref/WeakReference;Lcom/datadog/android/rum/tracking/Node;)V
6888+
public synthetic fun <init> (Ljava/lang/ref/WeakReference;Lcom/datadog/android/rum/tracking/Node;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
68736889
public fun equals (Ljava/lang/Object;)Z
6874-
public final fun getTag ()Ljava/lang/String;
6890+
public final fun getNode ()Lcom/datadog/android/rum/tracking/Node;
68756891
public final fun getViewRef ()Ljava/lang/ref/WeakReference;
68766892
public fun hashCode ()I
68776893
}

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ object RumAttributes {
217217
*/
218218
const val ACTION_TARGET_RESOURCE_ID: String = "action.target.resource_id"
219219

220+
/**
221+
* The touch target selection state if it is selectable.
222+
* This is only available for Jetpack Compose components.
223+
*/
224+
const val ACTION_TARGET_SELECTED: String = "action.target.selected"
225+
226+
/**
227+
* The touch target semantics role if it is available.
228+
* This is only available for Jetpack Compose components.
229+
*
230+
* @see [androidx.compose.ui.semantics.Role]
231+
*/
232+
const val ACTION_TARGET_ROLE: String = "action.target.role"
233+
220234
/**
221235
* The gesture event direction.
222236
*/

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ internal class RumEventSerializer(
215215
RumAttributes.ACTION_TARGET_CLASS_NAME,
216216
RumAttributes.ACTION_TARGET_RESOURCE_ID,
217217
RumAttributes.ACTION_TARGET_TITLE,
218+
RumAttributes.ACTION_TARGET_SELECTED,
219+
RumAttributes.ACTION_TARGET_ROLE,
218220
RumAttributes.ERROR_RESOURCE_METHOD,
219221
RumAttributes.ERROR_RESOURCE_STATUS_CODE,
220222
RumAttributes.ERROR_RESOURCE_URL

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ internal class GesturesListener(
266266
it.extractAttributes(view, attributes)
267267
}
268268
}
269-
target.tag?.let {
270-
// TODO RUM-9345: Enrich Compose action target attributes.
269+
target.node?.let {
270+
attributes.putAll(it.customAttributes)
271271
}
272272
GlobalRumMonitor.get(sdkCore).addAction(
273273
RumActionType.TAP,
@@ -289,8 +289,8 @@ internal class GesturesListener(
289289
it.extractAttributes(view, attributes)
290290
}
291291
}
292-
scrollTarget.tag?.let {
293-
// TODO RUM-9345: Enrich Compose action target attributes.
292+
scrollTarget.node?.let {
293+
attributes.putAll(it.customAttributes)
294294
}
295295
if (onUpEvent != null) {
296296
gestureDirection = resolveGestureDirection(onUpEvent)

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesUtils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal fun resolveViewTargetName(
1919
): String {
2020
return target.viewRef.get()?.let { view ->
2121
resolveTargetName(interactionPredicate, view)
22-
} ?: target.tag ?: ""
22+
} ?: target.node?.name ?: ""
2323
}
2424

2525
internal fun resolveTargetName(

features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/tracking/ViewTarget.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package com.datadog.android.rum.tracking
88

99
import android.view.View
10+
import com.datadog.android.lint.InternalApi
1011
import java.lang.ref.WeakReference
1112

1213
/**
@@ -15,14 +16,14 @@ import java.lang.ref.WeakReference
1516
*
1617
* @property viewRef The Weak Reference of Android [View] that was found.
1718
* If non-null, indicates a classic View was located.
18-
* @property tag The semantics tag associated with a Jetpack Compose component. If non-null, indicates
19+
* @property node The semantics node associated with a Jetpack Compose component. If non-null, indicates
1920
* that a Compose node with the given semantics tag was found.
2021
*
21-
* Only one of [viewRef] or [tag] is expected to be non-null, depending on the UI framework used.
22+
* Only one of [viewRef] or [node] is expected to be non-null, depending on the UI framework used.
2223
*/
2324
class ViewTarget(
2425
val viewRef: WeakReference<View?> = WeakReference(null),
25-
val tag: String? = null
26+
val node: Node? = null
2627
) {
2728

2829
override fun equals(other: Any?): Boolean {
@@ -32,14 +33,25 @@ class ViewTarget(
3233
if (other !is ViewTarget) return false
3334

3435
if (viewRef.get() != other.viewRef.get()) return false
35-
if (tag != other.tag) return false
36+
if (node != other.node) return false
3637

3738
return true
3839
}
3940

4041
override fun hashCode(): Int {
4142
var result = viewRef.get().hashCode()
42-
result = 31 * result + tag.hashCode()
43+
result = 31 * result + node.hashCode()
4344
return result
4445
}
4546
}
47+
48+
/**
49+
* Represents the result of locating a target node in Jetpack compose.
50+
* @property name the name of the target node.
51+
* @property customAttributes the custom attributes that the target node may have.
52+
*/
53+
@InternalApi
54+
data class Node(
55+
val name: String,
56+
val customAttributes: Map<String, Any?> = mapOf()
57+
)

features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.datadog.android.rum.RumActionType
1919
import com.datadog.android.rum.RumAttributes
2020
import com.datadog.android.rum.tracking.ActionTrackingStrategy
2121
import com.datadog.android.rum.tracking.InteractionPredicate
22+
import com.datadog.android.rum.tracking.Node
2223
import com.datadog.android.rum.tracking.ViewTarget
2324
import com.datadog.android.rum.utils.forge.Configurator
2425
import com.datadog.android.rum.utils.verifyLog
@@ -560,7 +561,7 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest()
560561
val y = startDownEvent.y
561562
val mockComposeActionTrackingStrategy: ActionTrackingStrategy = mock {
562563
whenever(it.findTargetForScroll(composeView, x, y))
563-
.thenReturn(ViewTarget(WeakReference(null), targetName))
564+
.thenReturn(ViewTarget(WeakReference(null), Node(targetName)))
564565
}
565566
testedListener = GesturesListener(
566567
rumMonitor.mockSdkCore,
@@ -1108,6 +1109,102 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest()
11081109
)
11091110
}
11101111

1112+
@ParameterizedTest
1113+
@ValueSource(
1114+
strings = [
1115+
GesturesListener.SCROLL_DIRECTION_DOWN,
1116+
GesturesListener.SCROLL_DIRECTION_UP,
1117+
GesturesListener.SCROLL_DIRECTION_LEFT,
1118+
GesturesListener.SCROLL_DIRECTION_RIGHT
1119+
]
1120+
)
1121+
fun `M add compose node attributes W send Scroll action`(
1122+
expectedDirection: String,
1123+
forge: Forge
1124+
) {
1125+
val mockEvent: MotionEvent = forge.getForgery()
1126+
val startDownEvent: MotionEvent = forge.getForgery()
1127+
val scrollEvent: MotionEvent = forge.getForgery()
1128+
val distancesX = forge.aFloat()
1129+
val distancesY = forge.aFloat()
1130+
val endUpEvent: MotionEvent = forge.getForgery()
1131+
val targetId = forge.anInt()
1132+
val fakeCustomTargetName = forge.anAlphabeticalString()
1133+
val validTarget: View = mockView(
1134+
id = targetId,
1135+
forEvent = mockEvent,
1136+
hitTest = true,
1137+
forge = forge,
1138+
clickable = true
1139+
)
1140+
val fakeAttributes = mapOf(
1141+
RumAttributes.ACTION_TARGET_ROLE to forge.aString()
1142+
)
1143+
val mockInteractionPredicate: InteractionPredicate = mock {
1144+
whenever(it.getTargetName(validTarget)).thenReturn(fakeCustomTargetName)
1145+
}
1146+
val mockComposeActionTrackingStrategy = mock<ActionTrackingStrategy>()
1147+
val mockAndroidActionTrackingStrategy = mock<ActionTrackingStrategy>()
1148+
mockDecorView = mockDecorView<ViewGroup>(
1149+
id = forge.anInt(),
1150+
forEvent = mockEvent,
1151+
hitTest = false,
1152+
forge = forge
1153+
) {
1154+
whenever(it.childCount).thenReturn(1)
1155+
whenever(it.getChildAt(0)).thenReturn(validTarget)
1156+
}
1157+
val expectedResourceName = forge.anAlphabeticalString()
1158+
mockResourcesForTarget(validTarget, expectedResourceName)
1159+
val expectedStopAttributes = fakeAttributes +
1160+
(RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection)
1161+
testedListener = GesturesListener(
1162+
rumMonitor.mockSdkCore,
1163+
WeakReference(mockWindow),
1164+
interactionPredicate = mockInteractionPredicate,
1165+
contextRef = WeakReference(mockAppContext),
1166+
internalLogger = mockInternalLogger,
1167+
androidActionTrackingStrategy = mockAndroidActionTrackingStrategy,
1168+
composeActionTrackingStrategy = mockComposeActionTrackingStrategy
1169+
)
1170+
stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection)
1171+
whenever(
1172+
mockAndroidActionTrackingStrategy.findTargetForScroll(
1173+
mockDecorView,
1174+
startDownEvent.x,
1175+
startDownEvent.y
1176+
)
1177+
).thenReturn(ViewTarget(viewRef = WeakReference<View?>(null)))
1178+
1179+
whenever(
1180+
mockComposeActionTrackingStrategy.findTargetForScroll(
1181+
mockDecorView,
1182+
startDownEvent.x,
1183+
startDownEvent.y
1184+
)
1185+
).thenReturn(ViewTarget(node = Node(fakeCustomTargetName, fakeAttributes)))
1186+
1187+
// When
1188+
testedListener.onDown(startDownEvent)
1189+
testedListener.onScroll(startDownEvent, scrollEvent, distancesX, distancesY)
1190+
testedListener.onUp(endUpEvent)
1191+
1192+
// Then
1193+
inOrder(rumMonitor.mockInstance) {
1194+
verify(rumMonitor.mockInstance).startAction(
1195+
eq(RumActionType.SCROLL),
1196+
eq(fakeCustomTargetName),
1197+
eq(fakeAttributes)
1198+
)
1199+
verify(rumMonitor.mockInstance).stopAction(
1200+
eq(RumActionType.SCROLL),
1201+
eq(fakeCustomTargetName),
1202+
eq(expectedStopAttributes)
1203+
)
1204+
}
1205+
verifyNoMoreInteractions(rumMonitor.mockInstance)
1206+
}
1207+
11111208
// endregion
11121209

11131210
// region internal

features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.datadog.android.rum.RumAttributes
2020
import com.datadog.android.rum.internal.instrumentation.gestures.GesturesListenerScrollSwipeTest.ScrollableListView
2121
import com.datadog.android.rum.tracking.ActionTrackingStrategy
2222
import com.datadog.android.rum.tracking.InteractionPredicate
23+
import com.datadog.android.rum.tracking.Node
2324
import com.datadog.android.rum.tracking.ViewAttributesProvider
2425
import com.datadog.android.rum.tracking.ViewTarget
2526
import com.datadog.android.rum.utils.forge.Configurator
@@ -364,7 +365,7 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() {
364365
val y = mockEvent.y
365366
val mockComposeActionTrackingStrategy: ActionTrackingStrategy = mock {
366367
whenever(it.findTargetForTap(composeView, x, y))
367-
.thenReturn(ViewTarget(WeakReference(null), targetName))
368+
.thenReturn(ViewTarget(WeakReference(null), Node(name = targetName)))
368369
}
369370
testedListener = GesturesListener(
370371
rumMonitor.mockSdkCore,
@@ -796,6 +797,76 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() {
796797
)
797798
}
798799

800+
@Test
801+
fun `M add compose node attributes W send Tap action`(forge: Forge) {
802+
val mockEvent: MotionEvent = forge.getForgery()
803+
val targetId = forge.anInt()
804+
val fakeCustomTargetName = forge.anAlphabeticalString()
805+
val validTarget: View = mockView(
806+
id = targetId,
807+
forEvent = mockEvent,
808+
hitTest = true,
809+
forge = forge,
810+
clickable = true
811+
)
812+
val fakeAttributes = mapOf(
813+
RumAttributes.ACTION_TARGET_ROLE to forge.aString(),
814+
RumAttributes.ACTION_TARGET_SELECTED to forge.aString()
815+
)
816+
val mockInteractionPredicate: InteractionPredicate = mock {
817+
whenever(it.getTargetName(validTarget)).thenReturn(fakeCustomTargetName)
818+
}
819+
val mockComposeActionTrackingStrategy = mock<ActionTrackingStrategy>()
820+
val mockAndroidActionTrackingStrategy = mock<ActionTrackingStrategy>()
821+
mockDecorView = mockDecorView<ViewGroup>(
822+
id = forge.anInt(),
823+
forEvent = mockEvent,
824+
hitTest = false,
825+
forge = forge
826+
) {
827+
whenever(it.childCount).thenReturn(1)
828+
whenever(it.getChildAt(0)).thenReturn(validTarget)
829+
}
830+
val expectedResourceName = forge.anAlphabeticalString()
831+
mockResourcesForTarget(validTarget, expectedResourceName)
832+
833+
testedListener = GesturesListener(
834+
rumMonitor.mockSdkCore,
835+
WeakReference(mockWindow),
836+
interactionPredicate = mockInteractionPredicate,
837+
contextRef = WeakReference(mockAppContext),
838+
internalLogger = mockInternalLogger,
839+
androidActionTrackingStrategy = mockAndroidActionTrackingStrategy,
840+
composeActionTrackingStrategy = mockComposeActionTrackingStrategy
841+
)
842+
843+
whenever(
844+
mockAndroidActionTrackingStrategy.findTargetForTap(
845+
mockDecorView,
846+
mockEvent.x,
847+
mockEvent.y
848+
)
849+
).thenReturn(ViewTarget(viewRef = WeakReference<View?>(null)))
850+
851+
whenever(
852+
mockComposeActionTrackingStrategy.findTargetForTap(
853+
mockDecorView,
854+
mockEvent.x,
855+
mockEvent.y
856+
)
857+
).thenReturn(ViewTarget(node = Node(fakeCustomTargetName, fakeAttributes)))
858+
859+
// When
860+
testedListener.onSingleTapUp(mockEvent)
861+
862+
// Then
863+
verify(rumMonitor.mockInstance).addAction(
864+
RumActionType.TAP,
865+
fakeCustomTargetName,
866+
fakeAttributes
867+
)
868+
}
869+
799870
// region Internal
800871

801872
private fun verifyMonitorCalledWithUserAction(

0 commit comments

Comments
 (0)