diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt index af7ad5dc96..a3c5b72f73 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/Rum.kt @@ -171,7 +171,8 @@ object Rum { rumAppStartupTelemetryReporter = rumAppStartupTelemetryReporter ) }, - insightsCollector = rumFeature.insightsCollector + insightsCollector = rumFeature.insightsCollector, + viewIdentityResolver = rumFeature.viewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt index d423047d53..279b636402 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/RumAttributes.kt @@ -100,6 +100,31 @@ object RumAttributes { */ const val INTERNAL_INSTRUMENTATION_TYPE: String = "_dd.instrumentation_type" + /** + * Stable view identity for heatmap correlation (maps to schema field permanent_id). + */ + internal const val INTERNAL_ACTION_TARGET_IDENTITY: String = "_dd.action.target.permanent_id" + + /** + * Width of the action target element (in pixels). + */ + internal const val INTERNAL_ACTION_TARGET_WIDTH: String = "_dd.action.target.width" + + /** + * Height of the action target element (in pixels). + */ + internal const val INTERNAL_ACTION_TARGET_HEIGHT: String = "_dd.action.target.height" + + /** + * X coordinate of the touch position relative to the target element (in pixels). + */ + internal const val INTERNAL_ACTION_POSITION_X: String = "_dd.action.position.x" + + /** + * Y coordinate of the touch position relative to the target element (in pixels). + */ + internal const val INTERNAL_ACTION_POSITION_Y: String = "_dd.action.position.y" + // endregion // region Resource diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt index b0447744b0..fade467639 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/RumFeature.kt @@ -34,6 +34,9 @@ import com.datadog.android.event.EventMapper import com.datadog.android.event.MapperSerializer import com.datadog.android.event.NoOpEventMapper import com.datadog.android.internal.flags.RumFlagEvaluationMessage +import com.datadog.android.internal.identity.NoOpViewIdentityResolver +import com.datadog.android.internal.identity.ViewIdentityResolver +import com.datadog.android.internal.identity.ViewIdentityResolverImpl import com.datadog.android.internal.system.BuildSdkVersionProvider import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.internal.thread.isMainThread @@ -178,6 +181,7 @@ internal class RumFeature( internal var displayInfoProvider: InfoProvider = NoOpDisplayInfoProvider() internal val rumContextUpdateReceivers = mutableSetOf() internal var insightsCollector: InsightsCollector = NoOpInsightsCollector() + internal var viewIdentityResolver: ViewIdentityResolver = NoOpViewIdentityResolver() private val lateCrashEventHandler by lazy { lateCrashReporterFactory(sdkCore as InternalSdkCore) } internal var rumAppStartupDetector: RumAppStartupDetector? = null @@ -222,6 +226,12 @@ internal class RumFeature( telemetryConfigurationSampleRate = configuration.telemetryConfigurationSampleRate backgroundEventTracking = configuration.backgroundEventTracking trackFrustrations = configuration.trackFrustrations + viewIdentityResolver = ViewIdentityResolverImpl(appContext.packageName) + // Store in feature context for cross-feature access (e.g., Session Replay) + sdkCore.updateFeatureContext(name) { context -> + context[ViewIdentityResolver.FEATURE_CONTEXT_KEY] = viewIdentityResolver + } + batteryInfoProvider = DefaultBatteryInfoProvider( applicationContext = appContext, timeProvider = sdkCore.timeProvider @@ -238,7 +248,8 @@ internal class RumFeature( configuration.interactionPredicate, composeActionTrackingStrategy = configuration.composeActionTrackingStrategy, buildSdkVersionProvider, - sdkCore.internalLogger + sdkCore.internalLogger, + viewIdentityResolver ) } else { NoOpUserActionTrackingStrategy() @@ -354,6 +365,7 @@ internal class RumFeature( anrDetectorRunnable?.stop() vitalExecutorService = NoOpScheduledExecutorService() sessionListener = NoOpRumSessionListener() + viewIdentityResolver = NoOpViewIdentityResolver() cleanupInfoProviders() @@ -863,14 +875,16 @@ internal class RumFeature( interactionPredicate: InteractionPredicate, composeActionTrackingStrategy: ActionTrackingStrategy, buildSdkVersionProvider: BuildSdkVersionProvider, - internalLogger: InternalLogger + internalLogger: InternalLogger, + viewIdentityResolver: ViewIdentityResolver ): UserActionTrackingStrategy { val gesturesTracker = provideGestureTracker( customProviders = touchTargetExtraAttributesProviders, interactionPredicate = interactionPredicate, composeActionTrackingStrategy = composeActionTrackingStrategy, - internalLogger = internalLogger + internalLogger = internalLogger, + viewIdentityResolver = viewIdentityResolver ) return if (buildSdkVersionProvider.isAtLeastQ) { UserActionTrackingStrategyApi29(gesturesTracker) @@ -883,7 +897,8 @@ internal class RumFeature( customProviders: Array, interactionPredicate: InteractionPredicate, composeActionTrackingStrategy: ActionTrackingStrategy, - internalLogger: InternalLogger + internalLogger: InternalLogger, + viewIdentityResolver: ViewIdentityResolver ): DatadogGesturesTracker { val defaultProviders = arrayOf(JetpackViewAttributesProvider()) val providers = customProviders + defaultProviders @@ -891,7 +906,8 @@ internal class RumFeature( providers, interactionPredicate, composeActionsTrackingStrategy = composeActionTrackingStrategy, - internalLogger + internalLogger, + viewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt index ea867f6cc9..6f53fd0f8f 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/event/RumEventSerializer.kt @@ -322,7 +322,12 @@ internal class RumEventSerializer( RumAttributes.INTERNAL_TIMESTAMP, RumAttributes.INTERNAL_ERROR_TYPE, RumAttributes.INTERNAL_ERROR_SOURCE_TYPE, - RumAttributes.INTERNAL_ERROR_IS_CRASH + RumAttributes.INTERNAL_ERROR_IS_CRASH, + RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY, + RumAttributes.INTERNAL_ACTION_POSITION_X, + RumAttributes.INTERNAL_ACTION_POSITION_Y, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH, + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT ) // this are attributes which may come after the calls made by cross-platform SDKs (they are diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt index e06f49982b..be2fb784ae 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScope.kt @@ -12,6 +12,7 @@ import com.datadog.android.api.feature.EventWriteScope import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.rum.RumActionType +import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.RumContext @@ -28,7 +29,7 @@ import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.math.max -@Suppress("LongParameterList") +@Suppress("LongParameterList", "TooManyFunctions") internal class RumActionScope( override val parentScope: RumScope, private val sdkCore: InternalSdkCore, @@ -71,8 +72,6 @@ internal class RumActionScope( private var sent = false internal var stopped = false - // endregion - @WorkerThread override fun handleEvent( event: RumRawEvent, @@ -270,6 +269,12 @@ internal class RumActionScope( frustrations.add(ActionEvent.Type.ERROR_TAP) } + val viewIdentity = actionAttributes[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] as? String + val positionX = actionAttributes[RumAttributes.INTERNAL_ACTION_POSITION_X] as? Long + val positionY = actionAttributes[RumAttributes.INTERNAL_ACTION_POSITION_Y] as? Long + val targetWidth = actionAttributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] as? Long + val targetHeight = actionAttributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] as? Long + sdkCore.newRumEventWriteOperation(datadogContext, writeScope, writer) { val user = datadogContext.userInfo val hasReplay = featuresContextResolver.resolveViewHasReplay( @@ -354,7 +359,19 @@ internal class RumActionScope( session = ActionEvent.DdSession( sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition() ), - configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate) + configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate), + action = ActionEvent.DdAction( + position = positionX?.let { x -> + positionY?.let { y -> + ActionEvent.Position(x = x, y = y) + } + }, + target = ActionEvent.DdActionTarget( + permanentId = viewIdentity, + width = targetWidth, + height = targetHeight + ) + ) ), connectivity = networkInfo.toActionConnectivity(), service = datadogContext.service, diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt index da42899374..5e9be99bbe 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScope.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumSessionListener @@ -55,7 +56,8 @@ internal class RumApplicationScope( private val batteryInfoProvider: InfoProvider, private val displayInfoProvider: InfoProvider, private val rumSessionScopeStartupManagerFactory: () -> RumSessionScopeStartupManager, - private val insightsCollector: InsightsCollector + private val insightsCollector: InsightsCollector, + private val viewIdentityResolver: ViewIdentityResolver ) : RumScope, RumViewChangedListener { override val parentScope: RumScope? = null @@ -85,7 +87,8 @@ internal class RumApplicationScope( batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, rumSessionScopeStartupManagerFactory = rumSessionScopeStartupManagerFactory, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) ) @@ -207,7 +210,8 @@ internal class RumApplicationScope( batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, rumSessionScopeStartupManagerFactory = rumSessionScopeStartupManagerFactory, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) childScopes.add(newSession) if (event !is RumRawEvent.StartView) { diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt index 8c8221ddf9..5ed0f147c9 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScope.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.storage.NoOpDataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.profiling.ProfilerStopEvent import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType @@ -60,7 +61,8 @@ internal class RumSessionScope( private val sessionMaxDurationNanos: Long = DEFAULT_SESSION_MAX_DURATION_NS, rumSessionTypeOverride: RumSessionType?, private val rumSessionScopeStartupManagerFactory: () -> RumSessionScopeStartupManager, - insightsCollector: InsightsCollector + insightsCollector: InsightsCollector, + viewIdentityResolver: ViewIdentityResolver ) : RumScope { internal var sessionId = RumContext.NULL_UUID @@ -97,7 +99,8 @@ internal class RumSessionScope( accessibilitySnapshotManager = accessibilitySnapshotManager, batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, - insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) internal val activeView: RumViewScope? diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt index f7832a0c79..b7fbdab92b 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScope.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.metrics.MethodCallSamplingRate +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumSessionType @@ -60,7 +61,8 @@ internal class RumViewManagerScope( private val accessibilitySnapshotManager: AccessibilitySnapshotManager, private val batteryInfoProvider: InfoProvider, private val displayInfoProvider: InfoProvider, - private val insightsCollector: InsightsCollector + private val insightsCollector: InsightsCollector, + private val viewIdentityResolver: ViewIdentityResolver ) : RumScope { private val interactionToNextViewMetricResolver: InteractionToNextViewMetricResolver = @@ -295,7 +297,8 @@ internal class RumViewManagerScope( accessibilitySnapshotManager = accessibilitySnapshotManager, batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) applicationDisplayed = true childrenScopes.add(viewScope) @@ -378,7 +381,8 @@ internal class RumViewManagerScope( accessibilitySnapshotManager = accessibilitySnapshotManager, batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) } @@ -421,7 +425,8 @@ internal class RumViewManagerScope( accessibilitySnapshotManager = accessibilitySnapshotManager, batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt index a08c7ee194..7098d01e4a 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScope.kt @@ -18,6 +18,7 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.internal.attributes.LocalAttribute import com.datadog.android.internal.attributes.ViewScopeInstrumentationType +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.rum.RumActionType @@ -89,7 +90,8 @@ internal open class RumViewScope( private val accessibilitySnapshotManager: AccessibilitySnapshotManager, private val batteryInfoProvider: InfoProvider, private val displayInfoProvider: InfoProvider, - private val insightsCollector: InsightsCollector + private val insightsCollector: InsightsCollector, + private val viewIdentityResolver: ViewIdentityResolver ) : RumScope { internal val url = key.url.replace('.', '/') @@ -182,6 +184,16 @@ internal open class RumViewScope( cpuVitalMonitor.register(cpuVitalListener) memoryVitalMonitor.register(memoryVitalListener) frameRateVitalMonitor.register(frameRateVitalListener) + + viewIdentityResolver.setCurrentScreen(url) + + val rumContext = parentScope.getRumContext() + if (rumContext.syntheticsTestId != null) { + logSynthetics("_dd.application.id", rumContext.applicationId) + logSynthetics("_dd.session.id", rumContext.sessionId) + logSynthetics("_dd.view.id", viewId) + } + networkSettledMetricResolver.viewWasCreated(eventTime.nanoTime) interactionToNextViewMetricResolver.onViewCreated(viewId, eventTime.nanoTime) slowFramesListener?.onViewCreated(viewId, startedNanos) @@ -472,7 +484,8 @@ internal open class RumViewScope( accessibilitySnapshotManager = accessibilitySnapshotManager, batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) } @@ -1676,7 +1689,8 @@ internal open class RumViewScope( accessibilitySnapshotManager: AccessibilitySnapshotManager, batteryInfoProvider: InfoProvider, displayInfoProvider: InfoProvider, - insightsCollector: InsightsCollector + insightsCollector: InsightsCollector, + viewIdentityResolver: ViewIdentityResolver ): RumViewScope { val networkSettledMetricResolver = NetworkSettledMetricResolver( networkSettledResourceIdentifier, @@ -1713,7 +1727,8 @@ internal open class RumViewScope( accessibilitySnapshotManager = accessibilitySnapshotManager, batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt index 8b4fecb2fd..da11344c64 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTracker.kt @@ -10,6 +10,7 @@ import android.content.Context import android.view.Window import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.tracking.ActionTrackingStrategy import com.datadog.android.rum.tracking.InteractionPredicate import com.datadog.android.rum.tracking.ViewAttributesProvider @@ -19,7 +20,8 @@ internal class DatadogGesturesTracker( internal val targetAttributesProviders: Array, internal val interactionPredicate: InteractionPredicate, private val composeActionsTrackingStrategy: ActionTrackingStrategy, - private val internalLogger: InternalLogger + private val internalLogger: InternalLogger, + private val viewIdentityResolver: ViewIdentityResolver ) : GesturesTracker { // region GesturesTracker @@ -109,7 +111,8 @@ internal class DatadogGesturesTracker( interactionPredicate = interactionPredicate, contextRef = WeakReference(context), composeActionTrackingStrategy = composeActionsTrackingStrategy, - internalLogger = internalLogger + internalLogger = internalLogger, + viewIdentityResolver = viewIdentityResolver ) ) } diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt index 34dc2e2a1f..820a2a11b9 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListener.kt @@ -14,6 +14,7 @@ import android.view.Window import androidx.core.view.isVisible import com.datadog.android.api.InternalLogger import com.datadog.android.api.SdkCore +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.GlobalRumMonitor import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumAttributes @@ -37,7 +38,8 @@ internal class GesturesListener( private val contextRef: Reference, private val internalLogger: InternalLogger, private val composeActionTrackingStrategy: ActionTrackingStrategy = NoOpActionTrackingStrategy(), - private val androidActionTrackingStrategy: ActionTrackingStrategy = AndroidActionTrackingStrategy() + private val androidActionTrackingStrategy: ActionTrackingStrategy = AndroidActionTrackingStrategy(), + private val viewIdentityResolver: ViewIdentityResolver ) : GestureListenerCompat() { private var scrollEventType: RumActionType? = null @@ -226,7 +228,7 @@ internal class GesturesListener( onUpEvent.y ) downTarget?.takeIf { it == upTarget }?.let { target -> - sendTapEventWithTarget(target) + sendTapEventWithTarget(target, onUpEvent.x, onUpEvent.y) } } } @@ -257,19 +259,26 @@ internal class GesturesListener( private fun handleTapUp(decorView: View?, e: MotionEvent) { if (decorView != null) { findTarget(decorView, e.x, e.y)?.let { target -> - sendTapEventWithTarget(target) + sendTapEventWithTarget(target, e.x, e.y) } } } - private fun sendTapEventWithTarget(target: ViewTarget) { + private fun sendTapEventWithTarget(target: ViewTarget, touchX: Float = 0f, touchY: Float = 0f) { val attributes = mutableMapOf() target.viewRef.get()?.let { view -> - val targetId: String = contextRef.get().resourceIdName(view.id) - attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] = view.targetClassName() - attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] = targetId - attributesProviders.forEach { - it.extractAttributes(view, attributes) + addViewAttributes(view, attributes) + + if (view.isAttachedToWindow) { + val locationInWindow = IntArray(2) + @Suppress("UnsafeThirdPartyFunctionCall") // locationInWindow is non-null with exactly 2 elements + view.getLocationInWindow(locationInWindow) + + val relativeX = (touchX - locationInWindow[0]).toLong() + val relativeY = (touchY - locationInWindow[1]).toLong() + + attributes[RumAttributes.INTERNAL_ACTION_POSITION_X] = relativeX + attributes[RumAttributes.INTERNAL_ACTION_POSITION_Y] = relativeY } } target.node?.let { @@ -288,12 +297,7 @@ internal class GesturesListener( ): MutableMap { val attributes = mutableMapOf() scrollTarget.viewRef.get()?.let { view -> - val targetId: String = contextRef.get().resourceIdName(view.id) - attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] = view.targetClassName() - attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] = targetId - attributesProviders.forEach { - it.extractAttributes(view, attributes) - } + addViewAttributes(view, attributes) } scrollTarget.node?.let { attributes.putAll(it.customAttributes) @@ -305,6 +309,20 @@ internal class GesturesListener( return attributes } + private fun addViewAttributes(view: View, attributes: MutableMap) { + val targetId: String = contextRef.get().resourceIdName(view.id) + attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] = view.targetClassName() + attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] = targetId + attributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] = view.width.toLong() + attributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] = view.height.toLong() + viewIdentityResolver.resolveViewIdentity(view)?.let { viewIdentity -> + attributes[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] = viewIdentity + } + attributesProviders.forEach { + it.extractAttributes(view, attributes) + } + } + private fun resolveGestureDirection(endEvent: MotionEvent): String { val diffX = endEvent.x - onTouchDownXPos val diffY = endEvent.y - onTouchDownYPos diff --git a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt index dd1916c8dd..81407dfd8e 100644 --- a/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt +++ b/features/dd-sdk-android-rum/src/main/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitor.kt @@ -27,6 +27,7 @@ import com.datadog.android.core.internal.utils.getSafe import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.core.metrics.MethodCallSamplingRate import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.internal.telemetry.InternalTelemetryEvent.ApiUsage.AddOperationStepVital.ActionType import com.datadog.android.internal.thread.NamedCallable @@ -105,7 +106,8 @@ internal class DatadogRumMonitor( batteryInfoProvider: InfoProvider, displayInfoProvider: InfoProvider, private val rumSessionScopeStartupManagerFactory: () -> RumSessionScopeStartupManager, - insightsCollector: InsightsCollector + insightsCollector: InsightsCollector, + viewIdentityResolver: ViewIdentityResolver ) : RumMonitor, AdvancedRumMonitor { internal var rootScope = RumApplicationScope( @@ -128,7 +130,8 @@ internal class DatadogRumMonitor( batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, rumSessionScopeStartupManagerFactory = rumSessionScopeStartupManagerFactory, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) internal var debugListener: RumDebugListener? = null @@ -200,14 +203,14 @@ internal class DatadogRumMonitor( override fun addAction(type: RumActionType, name: String, attributes: Map) { val eventTime = getEventTime(attributes) handleEvent( - RumRawEvent.StartAction(type, name, false, attributes.toMap(), eventTime) + RumRawEvent.StartAction(type, name, false, attributes, eventTime) ) } override fun startAction(type: RumActionType, name: String, attributes: Map) { val eventTime = getEventTime(attributes) handleEvent( - RumRawEvent.StartAction(type, name, true, attributes.toMap(), eventTime) + RumRawEvent.StartAction(type, name, true, attributes, eventTime) ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt index 035b3bac8e..93ab7dc5b9 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/assertj/ActionEventAssert.kt @@ -559,6 +559,56 @@ internal class ActionEventAssert(actual: ActionEvent) : return this } + fun hasPermanentId(expected: String?): ActionEventAssert { + assertThat(actual.dd.action?.target?.permanentId) + .overridingErrorMessage( + "Expected event data to have dd.action.target.permanent_id $expected " + + "but was ${actual.dd.action?.target?.permanentId}" + ) + .isEqualTo(expected) + return this + } + + fun hasTargetWidth(expected: Long?): ActionEventAssert { + assertThat(actual.dd.action?.target?.width) + .overridingErrorMessage( + "Expected event data to have dd.action.target.width $expected " + + "but was ${actual.dd.action?.target?.width}" + ) + .isEqualTo(expected) + return this + } + + fun hasTargetHeight(expected: Long?): ActionEventAssert { + assertThat(actual.dd.action?.target?.height) + .overridingErrorMessage( + "Expected event data to have dd.action.target.height $expected " + + "but was ${actual.dd.action?.target?.height}" + ) + .isEqualTo(expected) + return this + } + + fun hasPositionX(expected: Long?): ActionEventAssert { + assertThat(actual.dd.action?.position?.x) + .overridingErrorMessage( + "Expected event data to have dd.action.position.x $expected " + + "but was ${actual.dd.action?.position?.x}" + ) + .isEqualTo(expected) + return this + } + + fun hasPositionY(expected: Long?): ActionEventAssert { + assertThat(actual.dd.action?.position?.y) + .overridingErrorMessage( + "Expected event data to have dd.action.position.y $expected " + + "but was ${actual.dd.action?.position?.y}" + ) + .isEqualTo(expected) + return this + } + companion object { internal const val TIMESTAMP_THRESHOLD_MS = 50L internal fun assertThat(actual: ActionEvent): ActionEventAssert = diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt index b788c3b442..8321964f86 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumActionScopeTest.kt @@ -14,6 +14,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.EventType import com.datadog.android.rum.RumActionType +import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumResourceKind import com.datadog.android.rum.RumResourceMethod @@ -3045,7 +3046,7 @@ internal class RumActionScopeTest { name, false, emptyMap(), - timeWithOffset(TEST_INACTIVITY_MS * 2 + 1) + eventTime = timeWithOffset(TEST_INACTIVITY_MS * 2 + 1) ) val result2 = testedScope.handleEvent(fakeEvent, fakeDatadogContext, mockEventWriteScope, mockWriter) @@ -3160,6 +3161,110 @@ internal class RumActionScopeTest { ) } + @Test + fun `M populate heatmap fields W sendAction() {viewIdentity present}`( + @StringForgery fakeViewIdentity: String, + @LongForgery fakeWidth: Long, + @LongForgery fakeHeight: Long, + @LongForgery fakePosX: Long, + @LongForgery fakePosY: Long + ) { + // Given + val attributesWithDimensions = fakeAttributes + + (RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY to fakeViewIdentity) + + (RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to fakeWidth) + + (RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to fakeHeight) + + (RumAttributes.INTERNAL_ACTION_POSITION_X to fakePosX) + + (RumAttributes.INTERNAL_ACTION_POSITION_Y to fakePosY) + testedScope = RumActionScope( + parentScope = mockParentScope, + sdkCore = rumMonitor.mockSdkCore, + waitForStop = false, + eventTime = fakeEventTime, + initialType = fakeType, + initialName = fakeName, + initialAttributes = attributesWithDimensions, + serverTimeOffsetInMs = fakeServerOffset, + inactivityThresholdMs = TEST_INACTIVITY_MS, + maxDurationMs = TEST_MAX_DURATION_MS, + featuresContextResolver = mockFeaturesContextResolver, + trackFrustrations = true, + sampleRate = fakeSampleRate, + rumSessionTypeOverride = fakeRumSessionType, + insightsCollector = mockInsightsCollector + ) + + // When + testedScope.handleEvent( + mockEvent(TEST_INACTIVITY_MS + 1), + fakeDatadogContext, + mockEventWriteScope, + mockWriter + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + assertThat(lastValue) + .hasPermanentId(fakeViewIdentity) + .hasTargetWidth(fakeWidth) + .hasTargetHeight(fakeHeight) + .hasPositionX(fakePosX) + .hasPositionY(fakePosY) + } + } + + @Test + fun `M have null viewIdentity W sendAction() {viewIdentity not present}`( + @LongForgery fakeWidth: Long, + @LongForgery fakeHeight: Long, + @LongForgery fakePosX: Long, + @LongForgery fakePosY: Long + ) { + // Given + val attributesWithDimensions = fakeAttributes + + (RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to fakeWidth) + + (RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to fakeHeight) + + (RumAttributes.INTERNAL_ACTION_POSITION_X to fakePosX) + + (RumAttributes.INTERNAL_ACTION_POSITION_Y to fakePosY) + testedScope = RumActionScope( + parentScope = mockParentScope, + sdkCore = rumMonitor.mockSdkCore, + waitForStop = false, + eventTime = fakeEventTime, + initialType = fakeType, + initialName = fakeName, + initialAttributes = attributesWithDimensions, + serverTimeOffsetInMs = fakeServerOffset, + inactivityThresholdMs = TEST_INACTIVITY_MS, + maxDurationMs = TEST_MAX_DURATION_MS, + featuresContextResolver = mockFeaturesContextResolver, + trackFrustrations = true, + sampleRate = fakeSampleRate, + rumSessionTypeOverride = fakeRumSessionType, + insightsCollector = mockInsightsCollector + ) + + // When + testedScope.handleEvent( + mockEvent(TEST_INACTIVITY_MS + 1), + fakeDatadogContext, + mockEventWriteScope, + mockWriter + ) + + // Then + argumentCaptor { + verify(mockWriter).write(eq(mockEventBatchWriter), capture(), eq(EventType.DEFAULT)) + assertThat(lastValue) + .hasPermanentId(null) + .hasTargetWidth(fakeWidth) + .hasTargetHeight(fakeHeight) + .hasPositionX(fakePosX) + .hasPositionY(fakePosY) + } + } + // region Internal private fun mockEvent(timeOffset: Long = 0L): RumRawEvent { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeAttributePropagationTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeAttributePropagationTest.kt index 7f39dbeb9e..9e164ec770 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeAttributePropagationTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeAttributePropagationTest.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.feature.FeatureScope import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.FeaturesContextResolver @@ -130,6 +131,9 @@ internal class RumApplicationScopeAttributePropagationTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockSlowFramesListener: SlowFramesListener @@ -219,7 +223,8 @@ internal class RumApplicationScopeAttributePropagationTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt index da963967e0..04a0d55993 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumApplicationScopeTest.kt @@ -17,6 +17,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumActionType import com.datadog.android.rum.RumSessionListener @@ -106,6 +107,9 @@ internal class RumApplicationScopeTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockFrameRateVitalMonitor: VitalMonitor @@ -198,7 +202,8 @@ internal class RumApplicationScopeTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeAttributePropagationTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeAttributePropagationTest.kt index e0e1a3b5ab..790ddef6f6 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeAttributePropagationTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeAttributePropagationTest.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.RumSessionListener import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.FeaturesContextResolver @@ -112,6 +113,9 @@ internal class RumSessionScopeAttributePropagationTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockRumFeatureScope: FeatureScope @@ -195,7 +199,8 @@ internal class RumSessionScopeAttributePropagationTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt index 68f350725e..8b7ab730c0 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumSessionScopeTest.kt @@ -20,6 +20,7 @@ import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.DeterministicSampler import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.profiling.ProfilerStopEvent import com.datadog.android.internal.sampling.SessionSamplingIdProvider import com.datadog.android.internal.tests.stub.StubTimeProvider @@ -128,6 +129,9 @@ internal class RumSessionScopeTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockSessionListener: RumSessionListener @@ -1786,7 +1790,8 @@ internal class RumSessionScopeTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = { mockRumSessionScopeStartupManager }, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) if (withMockChildScope) { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeAttributePropagationTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeAttributePropagationTest.kt index 41ff05e093..8ff0dc0629 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeAttributePropagationTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeAttributePropagationTest.kt @@ -14,6 +14,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.RumSessionType import com.datadog.android.rum.internal.FeaturesContextResolver import com.datadog.android.rum.internal.domain.InfoProvider @@ -148,6 +149,9 @@ internal class RumViewManagerScopeAttributePropagationTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @BeforeEach fun `set up`(forge: Forge) { fakeParentAttributes = forge.exhaustiveAttributes() @@ -176,7 +180,8 @@ internal class RumViewManagerScopeAttributePropagationTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionTypeOverride = fakeRumSessionType, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt index fa9e27198b..611f556b7d 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewManagerScopeTest.kt @@ -15,6 +15,7 @@ import com.datadog.android.api.storage.DataWriter import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.RumErrorSource @@ -147,6 +148,9 @@ internal class RumViewManagerScopeTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @BoolForgery var fakeTrackFrustrations: Boolean = true @@ -191,7 +195,8 @@ internal class RumViewManagerScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) } @@ -585,7 +590,8 @@ internal class RumViewManagerScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedScope.applicationDisplayed = true val fakeEvent = forge.validBackgroundEvent() @@ -622,7 +628,8 @@ internal class RumViewManagerScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedScope.childrenScopes.add(mockChildScope) whenever(mockChildScope.isActive()) doReturn true @@ -662,7 +669,8 @@ internal class RumViewManagerScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedScope.applicationDisplayed = true val fakeEvent = forge.validBackgroundEvent() @@ -735,7 +743,8 @@ internal class RumViewManagerScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedScope.childrenScopes.add(mockChildScope) whenever(mockChildScope.isActive()) doReturn true @@ -776,7 +785,8 @@ internal class RumViewManagerScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedScope.stopped = true val fakeEvent = forge.applicationStartedEvent() diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeAttributePropagationTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeAttributePropagationTest.kt index ba6268257f..dd5b147228 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeAttributePropagationTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeAttributePropagationTest.kt @@ -18,6 +18,7 @@ import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.EventType import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.RumAttributes import com.datadog.android.rum.RumErrorSource import com.datadog.android.rum.RumSessionType @@ -110,6 +111,9 @@ internal class RumViewScopeAttributePropagationTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockResolver: FirstPartyHostHeaderTypeResolver @@ -660,7 +664,8 @@ internal class RumViewScopeAttributePropagationTest { accessibilitySnapshotManager: AccessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider: InfoProvider = mockBatteryInfoProvider, displayInfoProvider: InfoProvider = mockDisplayInfoProvider, - insightsCollector: InsightsCollector = mockInsightsCollector + insightsCollector: InsightsCollector = mockInsightsCollector, + viewIdentityResolver: ViewIdentityResolver = mockViewIdentityResolver ) = RumViewScope( parentScope = parentScope, sdkCore = sdkCore, @@ -685,7 +690,8 @@ internal class RumViewScopeAttributePropagationTest { batteryInfoProvider = batteryInfoProvider, displayInfoProvider = displayInfoProvider, rumSessionTypeOverride = rumSessionType, - insightsCollector = insightsCollector + insightsCollector = insightsCollector, + viewIdentityResolver = viewIdentityResolver ) // endregion diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt index 54c2ad2a9d..720bb99219 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/domain/scope/RumViewScopeTest.kt @@ -18,6 +18,7 @@ import com.datadog.android.api.storage.EventType import com.datadog.android.core.InternalSdkCore import com.datadog.android.core.feature.event.ThreadDump import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.internal.utils.loggableStackTrace import com.datadog.android.rum.RumActionType @@ -173,6 +174,9 @@ internal class RumViewScopeTest { @Mock private lateinit var mockInsightsCollector: InsightsCollector + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockMemoryVitalMonitor: VitalMonitor @@ -9451,7 +9455,8 @@ internal class RumViewScopeTest { accessibilitySnapshotManager = mockAccessibilitySnapshotManager, batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) data class RumRawEventData(val event: RumRawEvent, val viewKey: RumScopeKey) diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/AbstractGesturesListenerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/AbstractGesturesListenerTest.kt index d23c139ea3..2f69ab6ff4 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/AbstractGesturesListenerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/AbstractGesturesListenerTest.kt @@ -14,6 +14,7 @@ import android.view.View import android.view.Window import com.datadog.android.Datadog import com.datadog.android.api.InternalLogger +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.utils.config.GlobalRumMonitorTestConfiguration import com.datadog.android.rum.utils.forge.Configurator import com.datadog.tools.unit.annotations.TestConfigurationsProvider @@ -62,6 +63,9 @@ internal abstract class AbstractGesturesListenerTest { @Mock lateinit var mockInternalLogger: InternalLogger + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + // region Tests @BeforeEach @@ -116,6 +120,7 @@ internal abstract class AbstractGesturesListenerTest { whenever(it.id).thenReturn(id) whenever(it.isClickable).thenReturn(clickable) whenever(it.visibility).thenReturn(if (visible) View.VISIBLE else View.GONE) + whenever(it.isAttachedToWindow).thenReturn(true) whenever(it.getLocationInWindow(any())).doAnswer { val array = it.arguments[0] as IntArray @@ -124,6 +129,13 @@ internal abstract class AbstractGesturesListenerTest { null } + whenever(it.getLocationOnScreen(any())).doAnswer { + val array = it.arguments[0] as IntArray + array[0] = locationOnScreenArray[0] + array[1] = locationOnScreenArray[1] + null + } + val diffPosX = abs(forEvent.x - locationOnScreenArray[0]).toInt() val diffPosY = abs(forEvent.y - locationOnScreenArray[1]).toInt() if (!hitTest && failHitTestBecauseOfWidthHeight) { diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt index 008823d453..6242f19f53 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/DatadogGesturesTrackerTest.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.Window import com.datadog.android.api.InternalLogger import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.rum.internal.tracking.NoOpInteractionPredicate import com.datadog.android.rum.tracking.ActionTrackingStrategy import com.datadog.android.rum.tracking.InteractionPredicate @@ -65,6 +66,9 @@ internal class DatadogGesturesTrackerTest : ObjectTest() @Mock lateinit var mockSdkCore: FeatureSdkCore + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @BeforeEach fun `set up`() { testedTracker = @@ -72,7 +76,8 @@ internal class DatadogGesturesTrackerTest : ObjectTest() emptyArray(), mockInteractionPredicate, mockActionTrackingStrategy, - mockInternalLogger + mockInternalLogger, + mockViewIdentityResolver ) whenever(mockActivity.window).thenReturn(mockWindow) whenever(mockSdkCore.internalLogger) doReturn mockInternalLogger @@ -83,7 +88,8 @@ internal class DatadogGesturesTrackerTest : ObjectTest() forge.aList { StubViewAttributesProvider(anAlphabeticalString()) }.toTypedArray(), NoOpInteractionPredicate(), NoOpActionTrackingStrategy(), - mockInternalLogger + mockInternalLogger, + mockViewIdentityResolver ) } @@ -98,7 +104,8 @@ internal class DatadogGesturesTrackerTest : ObjectTest() }.toTypedArray(), NoOpInteractionPredicate(), NoOpActionTrackingStrategy(), - mockInternalLogger + mockInternalLogger, + mockViewIdentityResolver ) } @@ -113,7 +120,8 @@ internal class DatadogGesturesTrackerTest : ObjectTest() }.toTypedArray(), StubInteractionPredicate(), mockActionTrackingStrategy, - mockInternalLogger + mockInternalLogger, + mockViewIdentityResolver ) } diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt index 42a8f77ed3..b95bf2d3fe 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerScrollSwipeTest.kt @@ -36,6 +36,7 @@ import org.junit.jupiter.params.provider.ValueSource import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any +import org.mockito.kotlin.argThat import org.mockito.kotlin.eq import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock @@ -100,7 +101,9 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes = expectedStartAttributes + (RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection) @@ -108,7 +111,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -171,7 +175,9 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes = expectedStartAttributes + (RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection) @@ -179,7 +185,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -234,11 +241,15 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes1 = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStartAttributes2 = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes1 = expectedStartAttributes1 + (RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection1) @@ -249,7 +260,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -322,7 +334,9 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes1 = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes1 = expectedStartAttributes1 + @@ -332,7 +346,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -399,13 +414,16 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(nonScrollingTarget, expectedResourceName) val expectedStartAttributes = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to nonScrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to nonScrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to nonScrollingTarget.height.toLong() ) testedListener = GesturesListener( rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -416,8 +434,23 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() testedListener.onUp(endUpEvent) // Then - verify(rumMonitor.mockInstance) - .addAction(RumActionType.TAP, "", expectedStartAttributes) + verify(rumMonitor.mockInstance).addAction( + eq(RumActionType.TAP), + eq(""), + argThat { attributes -> + val classMatches = attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] == + expectedStartAttributes[RumAttributes.ACTION_TARGET_CLASS_NAME] + val resourceMatches = attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] == + expectedStartAttributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] + val widthMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] == + expectedStartAttributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] + val heightMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] == + expectedStartAttributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] + classMatches && resourceMatches && widthMatches && heightMatches && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_X) && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_Y) + } + ) verifyNoMoreInteractions(rumMonitor.mockInstance) } @@ -452,7 +485,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -505,7 +539,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -568,7 +603,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), internalLogger = mockInternalLogger, - composeActionTrackingStrategy = mockComposeActionTrackingStrategy + composeActionTrackingStrategy = mockComposeActionTrackingStrategy, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -634,7 +670,9 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes = expectedStartAttributes + (RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection) @@ -642,7 +680,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -712,7 +751,9 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.simpleName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes = expectedStartAttributes + (RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection) @@ -720,7 +761,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -780,7 +822,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() WeakReference(mockWindow), interactionPredicate = mockInteractionPredicate, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -843,7 +886,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() WeakReference(mockWindow), interactionPredicate = mockInteractionPredicate, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -906,7 +950,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() WeakReference(mockWindow), interactionPredicate = mockInteractionPredicate, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -969,11 +1014,15 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() mockResourcesForTarget(scrollingTarget, expectedResourceName) val expectedStartAttributes1 = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStartAttributes2 = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to scrollingTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to scrollingTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to scrollingTarget.height.toLong() ) val expectedStopAttributes1 = expectedStartAttributes1 + (RumAttributes.ACTION_GESTURE_DIRECTION to expectedDirection1) @@ -983,7 +1032,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -1043,7 +1093,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) testedListener.onUp(startDownEvent) testedListener.onDown(endUpEvent) @@ -1088,7 +1139,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() contextRef = WeakReference(mockAppContext), androidActionTrackingStrategy = mockAndroidActionTrackingStrategy, composeActionTrackingStrategy = mockComposeActionTrackingStrategy, - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -1165,7 +1217,8 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() contextRef = WeakReference(mockAppContext), internalLogger = mockInternalLogger, androidActionTrackingStrategy = mockAndroidActionTrackingStrategy, - composeActionTrackingStrategy = mockComposeActionTrackingStrategy + composeActionTrackingStrategy = mockComposeActionTrackingStrategy, + viewIdentityResolver = mockViewIdentityResolver ) stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) whenever( @@ -1205,6 +1258,242 @@ internal class GesturesListenerScrollSwipeTest : AbstractGesturesListenerTest() verifyNoMoreInteractions(rumMonitor.mockInstance) } + // region View Identity Tests + + @Test + fun `M include view identity W onScroll() { viewIdentityResolver returns value }`( + forge: Forge + ) { + // Given + val startDownEvent: MotionEvent = forge.getForgery() + val scrollEvent: MotionEvent = forge.getForgery() + val endUpEvent: MotionEvent = forge.getForgery() + val fakeViewIdentity = forge.anAlphabeticalString(size = 32) + val expectedDirection = GesturesListener.SCROLL_DIRECTION_DOWN + stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) + + val scrollingTarget: ScrollableView = mockView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(scrollingTarget) + } + whenever(mockViewIdentityResolver.resolveViewIdentity(scrollingTarget)).thenReturn(fakeViewIdentity) + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onDown(startDownEvent) + testedListener.onScroll(startDownEvent, scrollEvent, forge.aFloat(), forge.aFloat()) + testedListener.onUp(endUpEvent) + + // Then + verify(rumMonitor.mockInstance).startAction( + eq(RumActionType.SCROLL), + any(), + argThat { + this[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] == fakeViewIdentity + } + ) + verify(rumMonitor.mockInstance).stopAction( + eq(RumActionType.SCROLL), + any(), + argThat { + this[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] == fakeViewIdentity + } + ) + } + + @Test + fun `M not include view identity W onScroll() { viewIdentityResolver returns null }`( + forge: Forge + ) { + // Given + val startDownEvent: MotionEvent = forge.getForgery() + val scrollEvent: MotionEvent = forge.getForgery() + val endUpEvent: MotionEvent = forge.getForgery() + val expectedDirection = GesturesListener.SCROLL_DIRECTION_DOWN + stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) + + val scrollingTarget: ScrollableView = mockView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(scrollingTarget) + } + whenever(mockViewIdentityResolver.resolveViewIdentity(scrollingTarget)).thenReturn(null) + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onDown(startDownEvent) + testedListener.onScroll(startDownEvent, scrollEvent, forge.aFloat(), forge.aFloat()) + testedListener.onUp(endUpEvent) + + // Then + verify(rumMonitor.mockInstance).startAction( + eq(RumActionType.SCROLL), + any(), + argThat { + !this.containsKey(RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY) + } + ) + verify(rumMonitor.mockInstance).stopAction( + eq(RumActionType.SCROLL), + any(), + argThat { + !this.containsKey(RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY) + } + ) + } + + @Test + fun `M include view identity W onSwipe() { viewIdentityResolver returns value }`( + forge: Forge + ) { + // Given + val startDownEvent: MotionEvent = forge.getForgery() + val scrollEvent: MotionEvent = forge.getForgery() + val endUpEvent: MotionEvent = forge.getForgery() + val fakeViewIdentity = forge.anAlphabeticalString(size = 32) + val expectedDirection = GesturesListener.SCROLL_DIRECTION_DOWN + stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) + + val scrollingTarget: ScrollableView = mockView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(scrollingTarget) + } + whenever(mockViewIdentityResolver.resolveViewIdentity(scrollingTarget)).thenReturn(fakeViewIdentity) + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onDown(startDownEvent) + testedListener.onScroll(startDownEvent, scrollEvent, forge.aFloat(), forge.aFloat()) + testedListener.onFling(startDownEvent, endUpEvent, forge.aFloat(), forge.aFloat()) + testedListener.onUp(endUpEvent) + + // Then + verify(rumMonitor.mockInstance).startAction( + eq(RumActionType.SCROLL), + any(), + argThat { + this[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] == fakeViewIdentity + } + ) + verify(rumMonitor.mockInstance).stopAction( + eq(RumActionType.SWIPE), + any(), + argThat { + this[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] == fakeViewIdentity + } + ) + } + + @Test + fun `M not include view identity W onSwipe() { viewIdentityResolver returns null }`( + forge: Forge + ) { + // Given + val startDownEvent: MotionEvent = forge.getForgery() + val scrollEvent: MotionEvent = forge.getForgery() + val endUpEvent: MotionEvent = forge.getForgery() + val expectedDirection = GesturesListener.SCROLL_DIRECTION_DOWN + stubStopMotionEvent(endUpEvent, startDownEvent, expectedDirection) + + val scrollingTarget: ScrollableView = mockView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = startDownEvent, + hitTest = true, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(scrollingTarget) + } + whenever(mockViewIdentityResolver.resolveViewIdentity(scrollingTarget)).thenReturn(null) + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onDown(startDownEvent) + testedListener.onScroll(startDownEvent, scrollEvent, forge.aFloat(), forge.aFloat()) + testedListener.onFling(startDownEvent, endUpEvent, forge.aFloat(), forge.aFloat()) + testedListener.onUp(endUpEvent) + + // Then + verify(rumMonitor.mockInstance).startAction( + eq(RumActionType.SCROLL), + any(), + argThat { + !this.containsKey(RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY) + } + ) + verify(rumMonitor.mockInstance).stopAction( + eq(RumActionType.SWIPE), + any(), + argThat { + !this.containsKey(RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY) + } + ) + } + + // endregion + // endregion // region internal diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt index b360aacd36..82a9bad213 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/instrumentation/gestures/GesturesListenerTapTest.kt @@ -36,6 +36,7 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.argThat +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -60,7 +61,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -130,7 +132,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -170,7 +173,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -215,7 +219,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -259,7 +264,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -296,7 +302,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -324,7 +331,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -372,7 +380,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), internalLogger = mockInternalLogger, - composeActionTrackingStrategy = mockComposeActionTrackingStrategy + composeActionTrackingStrategy = mockComposeActionTrackingStrategy, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -404,7 +413,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) val expectedResourceName = forge.anAlphabeticalString() mockResourcesForTarget(mockDecorView, expectedResourceName) @@ -446,7 +456,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -486,7 +497,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(mockWindow), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -508,7 +520,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { rumMonitor.mockSdkCore, WeakReference(null), contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -542,7 +555,9 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { mockResourcesForTarget(validTarget, expectedResourceName) var expectedAttributes: MutableMap = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to validTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to validTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to validTarget.height.toLong() ) val providers = Array(forge.anInt(min = 0, max = 10)) { mock { @@ -561,16 +576,29 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { WeakReference(mockWindow), providers, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When testedListener.onSingleTapUp(mockEvent) // Then verify(rumMonitor.mockInstance).addAction( - RumActionType.TAP, - "", - expectedAttributes + eq(RumActionType.TAP), + eq(""), + argThat { attributes -> + val classMatches = attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] == + expectedAttributes[RumAttributes.ACTION_TARGET_CLASS_NAME] + val resourceMatches = attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] == + expectedAttributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] + val widthMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] + val heightMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] + classMatches && resourceMatches && widthMatches && heightMatches && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_X) && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_Y) + } ) } @@ -605,7 +633,9 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { mockResourcesForTarget(validTarget, expectedResourceName) var expectedAttributes: MutableMap = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to validTarget.javaClass.simpleName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to validTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to validTarget.height.toLong() ) val providers = Array(forge.anInt(min = 0, max = 10)) { @@ -625,16 +655,29 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { WeakReference(mockWindow), providers, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When testedListener.onSingleTapUp(mockEvent) // Then verify(rumMonitor.mockInstance).addAction( - RumActionType.TAP, - "", - expectedAttributes + eq(RumActionType.TAP), + eq(""), + argThat { attributes -> + val classMatches = attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] == + expectedAttributes[RumAttributes.ACTION_TARGET_CLASS_NAME] + val resourceMatches = attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] == + expectedAttributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] + val widthMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] + val heightMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] + classMatches && resourceMatches && widthMatches && heightMatches && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_X) && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_Y) + } ) } @@ -668,7 +711,9 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { mockResourcesForTarget(validTarget, expectedResourceName) val expectedAttributes: MutableMap = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to validTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to validTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to validTarget.height.toLong() ) testedListener = GesturesListener( @@ -676,7 +721,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { WeakReference(mockWindow), interactionPredicate = mockInteractionPredicate, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -684,9 +730,21 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { // Then verify(rumMonitor.mockInstance).addAction( - RumActionType.TAP, - fakeCustomTargetName, - expectedAttributes + eq(RumActionType.TAP), + eq(fakeCustomTargetName), + argThat { attributes -> + val classMatches = attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] == + expectedAttributes[RumAttributes.ACTION_TARGET_CLASS_NAME] + val resourceMatches = attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] == + expectedAttributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] + val widthMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] + val heightMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] + classMatches && resourceMatches && widthMatches && heightMatches && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_X) && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_Y) + } ) } @@ -719,7 +777,9 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { mockResourcesForTarget(validTarget, expectedResourceName) val expectedAttributes: MutableMap = mutableMapOf( RumAttributes.ACTION_TARGET_CLASS_NAME to validTarget.javaClass.canonicalName, - RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName + RumAttributes.ACTION_TARGET_RESOURCE_ID to expectedResourceName, + RumAttributes.INTERNAL_ACTION_TARGET_WIDTH to validTarget.width.toLong(), + RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT to validTarget.height.toLong() ) testedListener = GesturesListener( @@ -727,7 +787,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { WeakReference(mockWindow), interactionPredicate = mockInteractionPredicate, contextRef = WeakReference(mockAppContext), - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -735,9 +796,142 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { // Then verify(rumMonitor.mockInstance).addAction( - RumActionType.TAP, - "", - expectedAttributes + eq(RumActionType.TAP), + eq(""), + argThat { attributes -> + val classMatches = attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] == + expectedAttributes[RumAttributes.ACTION_TARGET_CLASS_NAME] + val resourceMatches = attributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] == + expectedAttributes[RumAttributes.ACTION_TARGET_RESOURCE_ID] + val widthMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_WIDTH] + val heightMatches = attributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] == + expectedAttributes[RumAttributes.INTERNAL_ACTION_TARGET_HEIGHT] + classMatches && resourceMatches && widthMatches && heightMatches && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_X) && + attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_Y) + } + ) + } + + @Test + fun `M calculate correct relative position W tap { position relative to target }`( + forge: Forge + ) { + // Given + val targetX = 100 + val targetY = 200 + val touchX = 150f + val touchY = 250f + + val mockEvent: MotionEvent = mock { + whenever(it.x).thenReturn(touchX) + whenever(it.y).thenReturn(touchY) + } + + val targetId = forge.anInt() + val validTarget: View = mockView( + id = targetId, + forEvent = mockEvent, + hitTest = true, + forge = forge, + clickable = true + ) + // Mock getLocationInWindow to return a specific position + whenever(validTarget.getLocationInWindow(any())).doAnswer { invocation -> + val array = invocation.arguments[0] as IntArray + array[0] = targetX + array[1] = targetY + null + } + // Ensure the view dimensions are large enough for the hit test to pass + whenever(validTarget.width).thenReturn((touchX - targetX).toInt() + 1) + whenever(validTarget.height).thenReturn((touchY - targetY).toInt() + 1) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = false, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(validTarget) + } + val expectedResourceName = forge.anAlphabeticalString() + mockResourcesForTarget(validTarget, expectedResourceName) + + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onSingleTapUp(mockEvent) + + // Then - position should be relative to the target element + val expectedRelativeX = (touchX - targetX).toLong() + val expectedRelativeY = (touchY - targetY).toLong() + verify(rumMonitor.mockInstance).addAction( + eq(RumActionType.TAP), + eq(""), + argThat { attributes -> + attributes[RumAttributes.INTERNAL_ACTION_POSITION_X] == expectedRelativeX && + attributes[RumAttributes.INTERNAL_ACTION_POSITION_Y] == expectedRelativeY + } + ) + } + + @Test + fun `M not include position W tap { view not attached to window }`( + forge: Forge + ) { + // Given + val mockEvent: MotionEvent = forge.getForgery() + val targetId = forge.anInt() + val validTarget: View = mockView( + id = targetId, + forEvent = mockEvent, + hitTest = true, + forge = forge, + clickable = true + ) + // Mock view as not attached to window + whenever(validTarget.isAttachedToWindow).thenReturn(false) + + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = false, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(validTarget) + } + val expectedResourceName = forge.anAlphabeticalString() + mockResourcesForTarget(validTarget, expectedResourceName) + + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onSingleTapUp(mockEvent) + + // Then - position attributes should NOT be included when view is not attached + verify(rumMonitor.mockInstance).addAction( + eq(RumActionType.TAP), + eq(""), + argThat { attributes -> + !attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_X) && + !attributes.containsKey(RumAttributes.INTERNAL_ACTION_POSITION_Y) && + attributes[RumAttributes.ACTION_TARGET_CLASS_NAME] == validTarget.javaClass.canonicalName + } ) } @@ -776,7 +970,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { contextRef = WeakReference(mockAppContext), androidActionTrackingStrategy = mockAndroidActionTrackingStrategy, composeActionTrackingStrategy = mockComposeActionTrackingStrategy, - internalLogger = mockInternalLogger + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -837,7 +1032,8 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { contextRef = WeakReference(mockAppContext), internalLogger = mockInternalLogger, androidActionTrackingStrategy = mockAndroidActionTrackingStrategy, - composeActionTrackingStrategy = mockComposeActionTrackingStrategy + composeActionTrackingStrategy = mockComposeActionTrackingStrategy, + viewIdentityResolver = mockViewIdentityResolver ) whenever( @@ -867,6 +1063,136 @@ internal class GesturesListenerTapTest : AbstractGesturesListenerTest() { ) } + // region View Identity Tests + + @Test + fun `M include view identity W onTap() { viewIdentityResolver returns value }`( + forge: Forge + ) { + // Given + val mockEvent: MotionEvent = forge.getForgery() + val fakeViewIdentity = forge.anAlphabeticalString(size = 32) + val validTarget: View = mockView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = true, + forge = forge, + clickable = true + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = false, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(validTarget) + } + whenever(mockViewIdentityResolver.resolveViewIdentity(validTarget)).thenReturn(fakeViewIdentity) + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onSingleTapUp(mockEvent) + + // Then + verify(rumMonitor.mockInstance).addAction( + eq(RumActionType.TAP), + any(), + argThat { + this[RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY] == fakeViewIdentity + } + ) + } + + @Test + fun `M not include view identity W onTap() { viewIdentityResolver returns null }`( + forge: Forge + ) { + // Given + val mockEvent: MotionEvent = forge.getForgery() + val validTarget: View = mockView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = true, + forge = forge, + clickable = true + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = false, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(validTarget) + } + whenever(mockViewIdentityResolver.resolveViewIdentity(validTarget)).thenReturn(null) + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onSingleTapUp(mockEvent) + + // Then + verify(rumMonitor.mockInstance).addAction( + eq(RumActionType.TAP), + any(), + argThat { + !this.containsKey(RumAttributes.INTERNAL_ACTION_TARGET_IDENTITY) + } + ) + } + + @Test + fun `M call resolveViewIdentity W onTap() { view is tapped }`( + forge: Forge + ) { + // Given + val mockEvent: MotionEvent = forge.getForgery() + val validTarget: View = mockView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = true, + forge = forge, + clickable = true + ) + mockDecorView = mockDecorView( + id = forge.anInt(), + forEvent = mockEvent, + hitTest = false, + forge = forge + ) { + whenever(it.childCount).thenReturn(1) + whenever(it.getChildAt(0)).thenReturn(validTarget) + } + testedListener = GesturesListener( + rumMonitor.mockSdkCore, + WeakReference(mockWindow), + contextRef = WeakReference(mockAppContext), + internalLogger = mockInternalLogger, + viewIdentityResolver = mockViewIdentityResolver + ) + + // When + testedListener.onSingleTapUp(mockEvent) + + // Then + verify(mockViewIdentityResolver).resolveViewIdentity(validTarget) + } + + // endregion + // region Internal private fun verifyMonitorCalledWithUserAction( diff --git a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt index 26cfd84023..2c3d263142 100644 --- a/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt +++ b/features/dd-sdk-android-rum/src/test/kotlin/com/datadog/android/rum/internal/monitor/DatadogRumMonitorTest.kt @@ -21,6 +21,7 @@ import com.datadog.android.core.feature.event.ThreadDump import com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver import com.datadog.android.core.sampling.DeterministicSampler import com.datadog.android.core.sampling.Sampler +import com.datadog.android.internal.identity.ViewIdentityResolver import com.datadog.android.internal.telemetry.InternalTelemetryEvent import com.datadog.android.rum.DdRumContentProvider import com.datadog.android.rum.ExperimentalRumApi @@ -188,6 +189,9 @@ internal class DatadogRumMonitorTest { @Mock lateinit var mockSlowFramesListener: SlowFramesListener + @Mock + lateinit var mockViewIdentityResolver: ViewIdentityResolver + @Mock lateinit var mockRumFeatureScope: FeatureScope @@ -300,7 +304,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedMonitor.rootScope = mockApplicationScope } @@ -332,7 +337,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -407,7 +413,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedMonitor.start() val mockCallback = mock<(String?) -> Unit>() @@ -450,7 +457,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedMonitor.start() val mockCallback = mock<(String?) -> Unit>() @@ -2033,7 +2041,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -2073,7 +2082,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) // When @@ -2114,7 +2124,8 @@ internal class DatadogRumMonitorTest { displayInfoProvider = mockDisplayInfoProvider, rumSessionTypeOverride = null, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) whenever(mockExecutorService.isShutdown).thenReturn(true) @@ -2350,7 +2361,8 @@ internal class DatadogRumMonitorTest { batteryInfoProvider = mockBatteryInfoProvider, displayInfoProvider = mockDisplayInfoProvider, rumSessionScopeStartupManagerFactory = mock(), - insightsCollector = mockInsightsCollector + insightsCollector = mockInsightsCollector, + viewIdentityResolver = mockViewIdentityResolver ) testedMonitor.startView(key, name, attributes) // When diff --git a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/test/elmyr/MappingContextForgeryFactory.kt b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/test/elmyr/MappingContextForgeryFactory.kt index ead043700d..5b49d14d73 100644 --- a/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/test/elmyr/MappingContextForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay-compose/src/test/kotlin/com/datadog/android/sessionreplay/compose/test/elmyr/MappingContextForgeryFactory.kt @@ -20,7 +20,8 @@ internal class MappingContextForgeryFactory : ForgeryFactory { imagePrivacy = forge.getForgery(), textAndInputPrivacy = forge.getForgery(), touchPrivacyManager = mock(), - interopViewCallback = mock() + interopViewCallback = mock(), + viewIdentityProvider = mock() ) } } diff --git a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt index 8d2c51853d..e8cd6358f5 100644 --- a/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay-material/src/test/kotlin/com/datadog/android/sessionreplay/material/forge/MappingContextForgeryFactory.kt @@ -20,7 +20,8 @@ internal class MappingContextForgeryFactory : ForgeryFactory { imagePrivacy = forge.getForgery(), hasOptionSelectorParent = forge.aBool(), touchPrivacyManager = mock(), - interopViewCallback = mock() + interopViewCallback = mock(), + viewIdentityProvider = mock() ) } } diff --git a/features/dd-sdk-android-session-replay/api/apiSurface b/features/dd-sdk-android-session-replay/api/apiSurface index 535d530ea3..b06323743e 100644 --- a/features/dd-sdk-android-session-replay/api/apiSurface +++ b/features/dd-sdk-android-session-replay/api/apiSurface @@ -86,16 +86,18 @@ interface com.datadog.android.sessionreplay.internal.recorder.obfuscator.StringO interface com.datadog.android.sessionreplay.recorder.InteropViewCallback fun map(android.view.View, MappingContext): List data class com.datadog.android.sessionreplay.recorder.MappingContext - constructor(SystemInformation, com.datadog.android.sessionreplay.utils.ImageWireframeHelper, com.datadog.android.sessionreplay.TextAndInputPrivacy, com.datadog.android.sessionreplay.ImagePrivacy, com.datadog.android.sessionreplay.internal.TouchPrivacyManager, Boolean = false, InteropViewCallback) + constructor(SystemInformation, com.datadog.android.sessionreplay.utils.ImageWireframeHelper, com.datadog.android.sessionreplay.TextAndInputPrivacy, com.datadog.android.sessionreplay.ImagePrivacy, com.datadog.android.sessionreplay.internal.TouchPrivacyManager, Boolean = false, InteropViewCallback, ViewIdentityProvider) interface com.datadog.android.sessionreplay.recorder.OptionSelectorDetector fun isOptionSelector(android.view.ViewGroup): Boolean data class com.datadog.android.sessionreplay.recorder.SystemInformation constructor(com.datadog.android.sessionreplay.utils.GlobalBounds, Int = Configuration.ORIENTATION_UNDEFINED, Float, String? = null) +interface com.datadog.android.sessionreplay.recorder.ViewIdentityProvider + fun resolveIdentity(android.view.View): String? abstract class com.datadog.android.sessionreplay.recorder.mapper.BaseAsyncBackgroundWireframeMapper : BaseWireframeMapper constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) override fun map(T, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): List protected open fun resolveViewBackground(android.view.View, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback, com.datadog.android.api.InternalLogger): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? - protected open fun resolveBackgroundAsShapeWireframe(android.view.View, com.datadog.android.sessionreplay.utils.GlobalBounds, Int, Int, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle?): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ShapeWireframe? + protected open fun resolveBackgroundAsShapeWireframe(android.view.View, com.datadog.android.sessionreplay.utils.GlobalBounds, Int, Int, com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle?, com.datadog.android.sessionreplay.recorder.MappingContext): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe.ShapeWireframe? protected open fun resolveBackgroundAsImageWireframe(android.view.View, com.datadog.android.sessionreplay.utils.GlobalBounds, Int, Int, com.datadog.android.sessionreplay.recorder.MappingContext, com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback): com.datadog.android.sessionreplay.model.MobileSegment.Wireframe? companion object open class com.datadog.android.sessionreplay.recorder.mapper.BaseViewGroupMapper : BaseAsyncBackgroundWireframeMapper, TraverseAllChildrenMapper @@ -103,6 +105,7 @@ open class com.datadog.android.sessionreplay.recorder.mapper.BaseViewGroupMapper abstract class com.datadog.android.sessionreplay.recorder.mapper.BaseWireframeMapper : WireframeMapper constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) protected fun resolveViewId(android.view.View): Long + protected fun resolveViewIdentity(android.view.View, com.datadog.android.sessionreplay.recorder.ViewIdentityProvider): String? protected fun resolveShapeStyle(android.graphics.drawable.Drawable, Float, com.datadog.android.api.InternalLogger): com.datadog.android.sessionreplay.model.MobileSegment.ShapeStyle? class com.datadog.android.sessionreplay.recorder.mapper.EditTextMapper : TextViewMapper constructor(com.datadog.android.sessionreplay.utils.ViewIdentifierResolver, com.datadog.android.sessionreplay.utils.ColorStringFormatter, com.datadog.android.sessionreplay.utils.ViewBoundsResolver, com.datadog.android.sessionreplay.utils.DrawableToColorMapper) @@ -295,35 +298,35 @@ data class com.datadog.android.sessionreplay.model.MobileSegment sealed class Wireframe abstract fun toJson(): com.google.gson.JsonElement data class ShapeWireframe : Wireframe - constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null) + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String? = null) val type: kotlin.String override fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): ShapeWireframe fun fromJsonObject(com.google.gson.JsonObject): ShapeWireframe data class TextWireframe : Wireframe - constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, TextStyle, TextPosition? = null) + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, TextStyle, TextPosition? = null, kotlin.String? = null) val type: kotlin.String override fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): TextWireframe fun fromJsonObject(com.google.gson.JsonObject): TextWireframe data class ImageWireframe : Wireframe - constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.Boolean? = null) + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String? = null, kotlin.String? = null, kotlin.String? = null, kotlin.Boolean? = null, kotlin.String? = null) val type: kotlin.String override fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): ImageWireframe fun fromJsonObject(com.google.gson.JsonObject): ImageWireframe data class PlaceholderWireframe : Wireframe - constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, kotlin.String? = null) + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, kotlin.String? = null, kotlin.String? = null) val type: kotlin.String override fun toJson(): com.google.gson.JsonElement companion object fun fromJson(kotlin.String): PlaceholderWireframe fun fromJsonObject(com.google.gson.JsonObject): PlaceholderWireframe data class WebviewWireframe : Wireframe - constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, kotlin.Boolean? = null) + constructor(kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, kotlin.Long, WireframeClip? = null, ShapeStyle? = null, ShapeBorder? = null, kotlin.String, kotlin.Boolean? = null, kotlin.String? = null) val type: kotlin.String override fun toJson(): com.google.gson.JsonElement companion object 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 5676f43536..6ca06ece74 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 @@ -958,12 +958,13 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe$Companion; - public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V - public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V + public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component10 ()Ljava/lang/String; public final fun component11 ()Ljava/lang/String; public final fun component12 ()Ljava/lang/Boolean; + public final fun component13 ()Ljava/lang/String; public final fun component2 ()J public final fun component3 ()J public final fun component4 ()J @@ -972,8 +973,8 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; public final fun component9 ()Ljava/lang/String; - public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe; + public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ImageWireframe; @@ -983,6 +984,7 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun getHeight ()J public final fun getId ()J public final fun getMimeType ()Ljava/lang/String; + public final fun getPermanentId ()Ljava/lang/String; public final fun getResourceId ()Ljava/lang/String; public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun getType ()Ljava/lang/String; @@ -1006,8 +1008,8 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe$Companion; - public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;)V - public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component2 ()J public final fun component3 ()J @@ -1015,8 +1017,9 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun component5 ()J public final fun component6 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; public final fun component7 ()Ljava/lang/String; - public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe; + public final fun component8 ()Ljava/lang/String; + public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$PlaceholderWireframe; @@ -1024,6 +1027,7 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun getHeight ()J public final fun getId ()J public final fun getLabel ()Ljava/lang/String; + public final fun getPermanentId ()Ljava/lang/String; public final fun getType ()Ljava/lang/String; public final fun getWidth ()J public final fun getX ()J @@ -1041,8 +1045,8 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe$Companion; - public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;)V - public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;)V + public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component2 ()J public final fun component3 ()J @@ -1051,8 +1055,9 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun component6 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; - public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; + public final fun component9 ()Ljava/lang/String; + public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; @@ -1060,6 +1065,7 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; public final fun getHeight ()J public final fun getId ()J + public final fun getPermanentId ()Ljava/lang/String; public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun getType ()Ljava/lang/String; public final fun getWidth ()J @@ -1077,11 +1083,12 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe$Companion; - public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;)V - public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;Ljava/lang/String;)V + public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component10 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle; public final fun component11 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition; + public final fun component12 ()Ljava/lang/String; public final fun component2 ()J public final fun component3 ()J public final fun component4 ()J @@ -1090,8 +1097,8 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; public final fun component9 ()Ljava/lang/String; - public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe; + public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$TextWireframe; @@ -1099,6 +1106,7 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; public final fun getHeight ()J public final fun getId ()J + public final fun getPermanentId ()Ljava/lang/String; public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun getText ()Ljava/lang/String; public final fun getTextPosition ()Lcom/datadog/android/sessionreplay/model/MobileSegment$TextPosition; @@ -1120,10 +1128,11 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final class com/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe : com/datadog/android/sessionreplay/model/MobileSegment$Wireframe { public static final field Companion Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe$Companion; - public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;)V - public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)V + public synthetic fun (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()J public final fun component10 ()Ljava/lang/Boolean; + public final fun component11 ()Ljava/lang/String; public final fun component2 ()J public final fun component3 ()J public final fun component4 ()J @@ -1132,8 +1141,8 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun component7 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun component8 ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder; public final fun component9 ()Ljava/lang/String; - public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe; + public final fun copy (JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe;JJJJJLcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeBorder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe; public fun equals (Ljava/lang/Object;)Z public static final fun fromJson (Ljava/lang/String;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe; public static final fun fromJsonObject (Lcom/google/gson/JsonObject;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$WebviewWireframe; @@ -1141,6 +1150,7 @@ public final class com/datadog/android/sessionreplay/model/MobileSegment$Wirefra public final fun getClip ()Lcom/datadog/android/sessionreplay/model/MobileSegment$WireframeClip; public final fun getHeight ()J public final fun getId ()J + public final fun getPermanentId ()Ljava/lang/String; public final fun getShapeStyle ()Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; public final fun getSlotId ()Ljava/lang/String; public final fun getType ()Ljava/lang/String; @@ -1468,8 +1478,8 @@ public abstract interface class com/datadog/android/sessionreplay/recorder/Inter } public final class com/datadog/android/sessionreplay/recorder/MappingContext { - public fun (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;)V - public synthetic fun (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider;)V + public synthetic fun (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lcom/datadog/android/sessionreplay/recorder/SystemInformation; public final fun component2 ()Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper; public final fun component3 ()Lcom/datadog/android/sessionreplay/TextAndInputPrivacy; @@ -1477,8 +1487,9 @@ public final class com/datadog/android/sessionreplay/recorder/MappingContext { public final fun component5 ()Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager; public final fun component6 ()Z public final fun component7 ()Lcom/datadog/android/sessionreplay/recorder/InteropViewCallback; - public final fun copy (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;)Lcom/datadog/android/sessionreplay/recorder/MappingContext; - public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/recorder/MappingContext; + public final fun component8 ()Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider; + public final fun copy (Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider;)Lcom/datadog/android/sessionreplay/recorder/MappingContext; + public static synthetic fun copy$default (Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/recorder/SystemInformation;Lcom/datadog/android/sessionreplay/utils/ImageWireframeHelper;Lcom/datadog/android/sessionreplay/TextAndInputPrivacy;Lcom/datadog/android/sessionreplay/ImagePrivacy;Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager;ZLcom/datadog/android/sessionreplay/recorder/InteropViewCallback;Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider;ILjava/lang/Object;)Lcom/datadog/android/sessionreplay/recorder/MappingContext; public fun equals (Ljava/lang/Object;)Z public final fun getHasOptionSelectorParent ()Z public final fun getImagePrivacy ()Lcom/datadog/android/sessionreplay/ImagePrivacy; @@ -1487,6 +1498,7 @@ public final class com/datadog/android/sessionreplay/recorder/MappingContext { public final fun getSystemInformation ()Lcom/datadog/android/sessionreplay/recorder/SystemInformation; public final fun getTextAndInputPrivacy ()Lcom/datadog/android/sessionreplay/TextAndInputPrivacy; public final fun getTouchPrivacyManager ()Lcom/datadog/android/sessionreplay/internal/TouchPrivacyManager; + public final fun getViewIdentityProvider ()Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -1513,12 +1525,16 @@ public final class com/datadog/android/sessionreplay/recorder/SystemInformation public fun toString ()Ljava/lang/String; } +public abstract interface class com/datadog/android/sessionreplay/recorder/ViewIdentityProvider { + public abstract fun resolveIdentity (Landroid/view/View;)Ljava/lang/String; +} + public abstract class com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper : com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper { public static final field Companion Lcom/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper$Companion; public fun (Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver;Lcom/datadog/android/sessionreplay/utils/ColorStringFormatter;Lcom/datadog/android/sessionreplay/utils/ViewBoundsResolver;Lcom/datadog/android/sessionreplay/utils/DrawableToColorMapper;)V public fun map (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Ljava/util/List; protected fun resolveBackgroundAsImageWireframe (Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; - protected fun resolveBackgroundAsShapeWireframe (Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; + protected fun resolveBackgroundAsShapeWireframe (Landroid/view/View;Lcom/datadog/android/sessionreplay/utils/GlobalBounds;IILcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle;Lcom/datadog/android/sessionreplay/recorder/MappingContext;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe$ShapeWireframe; protected fun resolveViewBackground (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/MappingContext;Lcom/datadog/android/sessionreplay/utils/AsyncJobStatusCallback;Lcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/sessionreplay/model/MobileSegment$Wireframe; } @@ -1537,6 +1553,7 @@ public abstract class com/datadog/android/sessionreplay/recorder/mapper/BaseWire protected final fun getViewIdentifierResolver ()Lcom/datadog/android/sessionreplay/utils/ViewIdentifierResolver; protected final fun resolveShapeStyle (Landroid/graphics/drawable/Drawable;FLcom/datadog/android/api/InternalLogger;)Lcom/datadog/android/sessionreplay/model/MobileSegment$ShapeStyle; protected final fun resolveViewId (Landroid/view/View;)J + protected final fun resolveViewIdentity (Landroid/view/View;Lcom/datadog/android/sessionreplay/recorder/ViewIdentityProvider;)Ljava/lang/String; } public final class com/datadog/android/sessionreplay/recorder/mapper/EditTextMapper : com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper { diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/image-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/image-wireframe-schema.json index cadaa44715..ba3db7eba2 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/image-wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/image-wireframe-schema.json @@ -36,6 +36,11 @@ "type": "boolean", "description": "Flag describing an image wireframe that should render an empty state placeholder", "readOnly": false + }, + "permanentId": { + "type": "string", + "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate wireframes with RUM action events.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/placeholder-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/placeholder-wireframe-schema.json index b3ae802c09..7374ea7fc6 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/placeholder-wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/placeholder-wireframe-schema.json @@ -21,6 +21,11 @@ "type": "string", "description": "Label of the placeholder", "readOnly": false + }, + "permanentId": { + "type": "string", + "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate wireframes with RUM action events.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-wireframe-schema.json index 4aec81e7b4..74b1bf95e4 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/shape-wireframe-schema.json @@ -16,6 +16,11 @@ "description": "The type of the wireframe.", "const": "shape", "readOnly": true + }, + "permanentId": { + "type": "string", + "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate wireframes with RUM action events.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/text-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/text-wireframe-schema.json index 0eb8233ba5..34c8ee7677 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/text-wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/text-wireframe-schema.json @@ -27,6 +27,11 @@ }, "textPosition": { "$ref": "text-position-schema.json" + }, + "permanentId": { + "type": "string", + "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate wireframes with RUM action events.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/webview-wireframe-schema.json b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/webview-wireframe-schema.json index bc82e7036e..0ccb1170dd 100644 --- a/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/webview-wireframe-schema.json +++ b/features/dd-sdk-android-session-replay/src/main/json/schemas/session-replay/mobile/webview-wireframe-schema.json @@ -26,6 +26,11 @@ "type": "boolean", "description": "Whether this webview is visible or not.", "readOnly": true + }, + "permanentId": { + "type": "string", + "description": "A globally unique and stable identifier for this UI element, computed as the hash of the element's path (32 lowercase hex characters). Used to correlate wireframes with RUM action events.", + "readOnly": true } } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/NoOpViewIdentityProvider.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/NoOpViewIdentityProvider.kt new file mode 100644 index 0000000000..fa5e8c5e49 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/NoOpViewIdentityProvider.kt @@ -0,0 +1,18 @@ +/* + * 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 + +import android.view.View +import com.datadog.android.sessionreplay.recorder.ViewIdentityProvider + +/** + * No-op implementation used when view identity tracking is disabled. + */ +internal object NoOpViewIdentityProvider : ViewIdentityProvider { + + override fun resolveIdentity(view: View): String? = null +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt index 99581d2f67..bb53115def 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/SessionReplayRecorder.kt @@ -92,6 +92,7 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder { internalCallback: SessionReplayInternalCallback ) { val internalLogger = sdkCore.internalLogger + val rumContextDataHandler = RumContextDataHandler( rumContextProvider, timeProvider, @@ -161,14 +162,15 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder { internalLogger = internalLogger, onDrawListenerProducer = DefaultOnDrawListenerProducer( snapshotProducer = SnapshotProducer( - DefaultImageWireframeHelper( + imageWireframeHelper = DefaultImageWireframeHelper( logger = internalLogger, resourceResolver = resourceResolver, viewIdentifierResolver = viewIdentifierResolver, viewUtilsInternal = ViewUtilsInternal(), - imageTypeResolver = ImageTypeResolver() + imageTypeResolver = ImageTypeResolver(), + sdkCore = sdkCore ), - TreeViewTraversal( + treeViewTraversal = TreeViewTraversal( mappers = mappers, defaultViewMapper = defaultVWM, decorViewMapper = DecorViewMapper(defaultVWM, viewIdentifierResolver), @@ -179,10 +181,10 @@ internal class SessionReplayRecorder : OnWindowRefreshedCallback, Recorder { viewUtilsInternal = ViewUtilsInternal(), internalLogger = internalLogger ), - ComposedOptionSelectorDetector( + optionSelectorDetector = ComposedOptionSelectorDetector( customOptionSelectorDetectors + DefaultOptionSelectorDetector() ), - touchPrivacyManager, + touchPrivacyManager = touchPrivacyManager, internalLogger = internalLogger ), recordedDataQueueHandler = recordedDataQueueHandler, 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 a450f28390..5c5eca425b 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 @@ -20,6 +20,7 @@ import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.OptionSelectorDetector import com.datadog.android.sessionreplay.recorder.SystemInformation +import com.datadog.android.sessionreplay.recorder.ViewIdentityProvider import com.datadog.android.sessionreplay.utils.ImageWireframeHelper import java.util.LinkedList @@ -37,7 +38,8 @@ internal class SnapshotProducer( systemInformation: SystemInformation, textAndInputPrivacy: TextAndInputPrivacy, imagePrivacy: ImagePrivacy, - recordedDataQueueRefs: RecordedDataQueueRefs + recordedDataQueueRefs: RecordedDataQueueRefs, + viewIdentityProvider: ViewIdentityProvider ): Node? { return convertViewToNode( rootView, @@ -50,7 +52,8 @@ internal class SnapshotProducer( interopViewCallback = DefaultInteropViewCallback( treeViewTraversal, recordedDataQueueRefs - ) + ), + viewIdentityProvider = viewIdentityProvider ), LinkedList(), recordedDataQueueRefs diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewIdentityResolverAdapter.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewIdentityResolverAdapter.kt new file mode 100644 index 0000000000..b280c06acd --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/ViewIdentityResolverAdapter.kt @@ -0,0 +1,24 @@ +/* + * 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 + +import android.view.View +import com.datadog.android.internal.identity.ViewIdentityResolver +import com.datadog.android.sessionreplay.recorder.ViewIdentityProvider + +/** + * Adapter that wraps [ViewIdentityResolver] from the internal module + * to expose it as session-replay's public [ViewIdentityProvider] interface. + */ +internal class ViewIdentityResolverAdapter( + private val resolver: ViewIdentityResolver +) : ViewIdentityProvider { + + override fun resolveIdentity(view: View): String? { + return resolver.resolveViewIdentity(view) + } +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListener.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListener.kt index 9d75b12fda..55dec61d4e 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListener.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListener.kt @@ -19,8 +19,10 @@ import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueHandler import com.datadog.android.sessionreplay.internal.async.RecordedDataQueueRefs import com.datadog.android.sessionreplay.internal.recorder.Debouncer import com.datadog.android.sessionreplay.internal.recorder.SnapshotProducer +import com.datadog.android.sessionreplay.internal.recorder.ViewIdentityResolverAdapter import com.datadog.android.sessionreplay.internal.recorder.withinSRBenchmarkSpan import com.datadog.android.sessionreplay.internal.utils.MiscUtils +import com.datadog.android.sessionreplay.internal.utils.getViewIdentityResolver import java.lang.ref.WeakReference internal class WindowsOnDrawListener( @@ -54,11 +56,20 @@ internal class WindowsOnDrawListener( override fun run() { val rootViews = weakReferencedDecorViews.mapNotNull { it.get() } - // is is very important to have the windows sorted by their z-order + // It is very important to have the windows sorted by their z-order val context = rootViews.firstOrNull()?.context ?: return val systemInformation = miscUtils.resolveSystemInformation(context) val item = recordedDataQueueHandler.addSnapshotItem(systemInformation) ?: return + // Fetch viewIdentityResolver lazily from RUM's feature context (see getViewIdentityResolver docs) + val viewIdentityResolver = sdkCore.getViewIdentityResolver() + + // onWindowRefreshed is a no-op when using NoOpViewIdentityResolver (heatmap tracking disabled) + rootViews.forEach { viewIdentityResolver.onWindowRefreshed(it) } + + // Wrap the internal resolver with the public adapter interface + val viewIdentityProvider = ViewIdentityResolverAdapter(viewIdentityResolver) + val nodes = sdkCore.internalLogger.measureMethodCallPerf( METHOD_CALL_CALLER_CLASS, METHOD_CALL_CAPTURE_RECORD, @@ -73,7 +84,8 @@ internal class WindowsOnDrawListener( systemInformation = systemInformation, textAndInputPrivacy = textAndInputPrivacy, imagePrivacy = imagePrivacy, - recordedDataQueueRefs = recordedDataQueueRefs + recordedDataQueueRefs = recordedDataQueueRefs, + viewIdentityProvider = viewIdentityProvider ) } } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ActionBarContainerMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ActionBarContainerMapper.kt index 29595f696a..740ba3a6d7 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ActionBarContainerMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ActionBarContainerMapper.kt @@ -60,6 +60,7 @@ internal class ActionBarContainerMapper( y = bounds.y, width = bounds.width, height = bounds.height, + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), shapeStyle = shapeStyle, border = null ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt index c4fb1f894b..f8b7b5e799 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/CheckableTextViewMapper.kt @@ -84,8 +84,9 @@ internal abstract class CheckableTextViewMapper( y = checkBoxBounds.y, width = checkBoxBounds.width, height = checkBoxBounds.height, - border = shapeBorder, - shapeStyle = shapeStyle + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), + shapeStyle = shapeStyle, + border = shapeBorder ) ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapper.kt index 507c1a9f06..3490cb7aae 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapper.kt @@ -38,6 +38,7 @@ internal class HiddenViewMapper( y = viewGlobalBounds.y, width = viewGlobalBounds.width, height = viewGlobalBounds.height, + permanentId = mappingContext.viewIdentityProvider.resolveIdentity(view), label = HIDDEN_VIEW_PLACEHOLDER_TEXT ) ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt index fdc949fbc6..c1f3b709c2 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ProgressBarWireframeMapper.kt @@ -67,7 +67,7 @@ internal open class ProgressBarWireframeMapper

( val defaultColor = getDefaultColor(view) val trackColor = getColor(view.progressTintList, view.drawableState) ?: defaultColor - buildNonActiveTrackWireframe(view, trackBounds, trackColor)?.let(wireframes::add) + buildNonActiveTrackWireframe(view, trackBounds, trackColor, mappingContext)?.let(wireframes::add) val hasProgress = !view.isIndeterminate val showProgress = @@ -110,7 +110,8 @@ internal open class ProgressBarWireframeMapper

( private fun buildNonActiveTrackWireframe( view: P, trackBounds: GlobalBounds, - trackColor: Int + trackColor: Int, + mappingContext: MappingContext ): MobileSegment.Wireframe? { val nonActiveTrackId = viewIdentifierResolver.resolveChildUniqueIdentifier(view, NON_ACTIVE_TRACK_KEY_NAME) ?: return null @@ -124,6 +125,7 @@ internal open class ProgressBarWireframeMapper

( y = trackBounds.y, width = trackBounds.width, height = trackBounds.height, + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), shapeStyle = MobileSegment.ShapeStyle( backgroundColor = backgroundColor, opacity = view.alpha diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt index c25c7ce16d..21ca22c17d 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/SwitchCompatMapper.kt @@ -204,8 +204,9 @@ internal open class SwitchCompatMapper( y = trackBounds.y.densityNormalized(pixelsDensity).toLong(), width = trackBounds.width.densityNormalized(pixelsDensity).toLong(), height = trackBounds.height.densityNormalized(pixelsDensity).toLong(), - border = null, - shapeStyle = trackShapeStyle + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), + shapeStyle = trackShapeStyle, + border = null ) wireframes.add(trackWireframe) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt index cb25e76431..918e07926b 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/UnsupportedViewMapper.kt @@ -48,6 +48,7 @@ internal class UnsupportedViewMapper( y = viewGlobalBounds.y, width = viewGlobalBounds.width, height = viewGlobalBounds.height, + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), label = resolveViewTitle(view) ) ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt index da6c35ea8b..bdde055076 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/ViewWireframeMapper.kt @@ -50,6 +50,7 @@ internal class ViewWireframeMapper( viewGlobalBounds.y, viewGlobalBounds.width, viewGlobalBounds.height, + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), shapeStyle = shapeStyle, border = null ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt index 2f84e4ab0a..3437652390 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelper.kt @@ -16,9 +16,11 @@ import android.widget.TextView import androidx.annotation.UiThread import androidx.annotation.VisibleForTesting import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.internal.utils.densityNormalized import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.internal.recorder.ViewUtilsInternal +import com.datadog.android.sessionreplay.internal.utils.getViewIdentityResolver import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.resources.DrawableCopier @@ -36,7 +38,8 @@ internal class DefaultImageWireframeHelper( private val resourceResolver: ResourceResolver, private val viewIdentifierResolver: ViewIdentifierResolver, private val viewUtilsInternal: ViewUtilsInternal, - private val imageTypeResolver: ImageTypeResolver + private val imageTypeResolver: ImageTypeResolver, + private val sdkCore: FeatureSdkCore ) : ImageWireframeHelper { @Suppress("ReturnCount") @@ -248,6 +251,8 @@ internal class DefaultImageWireframeHelper( val drawableWidthDp = drawableProperties.drawableWidth.densityNormalized(density).toLong() val drawableHeightDp = drawableProperties.drawableHeight.densityNormalized(density).toLong() + val permanentId = sdkCore.getViewIdentityResolver().resolveViewIdentity(view) + if (imagePrivacy == ImagePrivacy.MASK_ALL) { return createContentPlaceholderWireframe( id = id, @@ -256,7 +261,8 @@ internal class DefaultImageWireframeHelper( width = drawableWidthDp, height = drawableHeightDp, label = MASK_ALL_CONTENT_LABEL, - clipping = clipping + clipping = clipping, + permanentId = permanentId ) } @@ -269,7 +275,8 @@ internal class DefaultImageWireframeHelper( width = drawableWidthDp, height = drawableHeightDp, label = MASK_CONTEXTUAL_CONTENT_LABEL, - clipping = clipping + clipping = clipping, + permanentId = permanentId ) } @@ -280,6 +287,7 @@ internal class DefaultImageWireframeHelper( y, width = drawableWidthDp, height = drawableHeightDp, + permanentId = permanentId, shapeStyle = shapeStyle, border = border, clip = clipping, @@ -427,7 +435,8 @@ internal class DefaultImageWireframeHelper( width: Long, height: Long, label: String, - clipping: MobileSegment.WireframeClip? + clipping: MobileSegment.WireframeClip?, + permanentId: String? = null ): MobileSegment.Wireframe.PlaceholderWireframe { return MobileSegment.Wireframe.PlaceholderWireframe( id, @@ -436,7 +445,8 @@ internal class DefaultImageWireframeHelper( width, height, label = label, - clip = clipping + clip = clipping, + permanentId = permanentId ) } diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/ViewIdentityResolverExt.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/ViewIdentityResolverExt.kt new file mode 100644 index 0000000000..db282ae9e8 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/internal/utils/ViewIdentityResolverExt.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.utils + +import com.datadog.android.api.feature.Feature +import com.datadog.android.api.feature.FeatureSdkCore +import com.datadog.android.internal.identity.NoOpViewIdentityResolver +import com.datadog.android.internal.identity.ViewIdentityResolver + +/** + * Retrieves [ViewIdentityResolver] from RUM's feature context, or [NoOpViewIdentityResolver] if unavailable. + */ +internal fun FeatureSdkCore.getViewIdentityResolver(): ViewIdentityResolver { + val rumContext = getFeatureContext(Feature.RUM_FEATURE_NAME) + return rumContext[ViewIdentityResolver.FEATURE_CONTEXT_KEY] as? ViewIdentityResolver + ?: NoOpViewIdentityResolver() +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/MappingContext.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/MappingContext.kt index e02e0e1692..d28a55413a 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/MappingContext.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/MappingContext.kt @@ -24,6 +24,8 @@ import com.datadog.android.sessionreplay.utils.ImageWireframeHelper * is an option selector type (e.g. time picker, date picker, drop - down list) * @param interopViewCallback the callback for Jetpack Compose semantics tree to call * when there is an interop view to map. + * @param viewIdentityProvider provides stable view identities for heatmap correlation + * (uses no-op implementation if heatmap tracking is disabled) */ data class MappingContext( val systemInformation: SystemInformation, @@ -32,5 +34,6 @@ data class MappingContext( val imagePrivacy: ImagePrivacy, val touchPrivacyManager: TouchPrivacyManager, val hasOptionSelectorParent: Boolean = false, - val interopViewCallback: InteropViewCallback + val interopViewCallback: InteropViewCallback, + val viewIdentityProvider: ViewIdentityProvider ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/ViewIdentityProvider.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/ViewIdentityProvider.kt new file mode 100644 index 0000000000..fadbc5f3b7 --- /dev/null +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/ViewIdentityProvider.kt @@ -0,0 +1,25 @@ +/* + * 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.recorder + +import android.view.View + +/** + * Provides stable identity hashes for Views, enabling correlation between + * Session Replay wireframes and RUM action events (heatmaps). + */ +interface ViewIdentityProvider { + + /** + * Resolves a stable identity hash for the given view. + * + * @param view the view to resolve identity for + * @return the stable identity hash, or null if the view's identity cannot be determined + * (e.g., detached view, or identity tracking is disabled) + */ + fun resolveIdentity(view: View): String? +} diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt index 75d2eb57b9..6a02ecd1d3 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseAsyncBackgroundWireframeMapper.kt @@ -93,7 +93,8 @@ abstract class BaseAsyncBackgroundWireframeMapper ( bounds = bounds, width = width, height = height, - shapeStyle = shapeStyle + shapeStyle = shapeStyle, + mappingContext = mappingContext ) } } @@ -106,13 +107,15 @@ abstract class BaseAsyncBackgroundWireframeMapper ( * @param width the view width. * @param height the view height. * @param shapeStyle the optional [MobileSegment.ShapeStyle] to use. + * @param mappingContext the [MappingContext] which contains contextual data, useful for mapping. */ protected open fun resolveBackgroundAsShapeWireframe( view: View, bounds: GlobalBounds, width: Int, height: Int, - shapeStyle: MobileSegment.ShapeStyle? + shapeStyle: MobileSegment.ShapeStyle?, + mappingContext: MappingContext ): MobileSegment.Wireframe.ShapeWireframe? { val id = uniqueIdentifierGenerator.resolveChildUniqueIdentifier( view, @@ -127,6 +130,7 @@ abstract class BaseAsyncBackgroundWireframeMapper ( y = bounds.y, width = width.densityNormalized(density).toLong(), height = height.densityNormalized(density).toLong(), + permanentId = resolveViewIdentity(view, mappingContext.viewIdentityProvider), shapeStyle = shapeStyle, border = null ) diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt index aff15ffee3..2e3d667a9f 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/BaseWireframeMapper.kt @@ -10,6 +10,7 @@ import android.graphics.drawable.Drawable import android.view.View import com.datadog.android.api.InternalLogger import com.datadog.android.sessionreplay.model.MobileSegment +import com.datadog.android.sessionreplay.recorder.ViewIdentityProvider import com.datadog.android.sessionreplay.utils.ColorStringFormatter import com.datadog.android.sessionreplay.utils.DrawableToColorMapper import com.datadog.android.sessionreplay.utils.ViewBoundsResolver @@ -42,6 +43,30 @@ abstract class BaseWireframeMapper( return viewIdentifierResolver.resolveViewId(view) } + /** + * Resolves the stable identity for a view, used for heatmap correlation. + * This identity is stable across sessions and is based on the view's canonical path. + * + * ## When to use this method + * + * Use `resolveViewIdentity(view, viewIdentityProvider)` when the wireframe directly corresponds + * to a View in the Android hierarchy. This includes both interactive elements (buttons, etc.) + * and non-interactive elements (text labels, images, etc.) - any View the user might tap on. + * + * Omit the identity (defaults to null) when the wireframe is synthetic - i.e., it doesn't + * correspond to a real View: + * - Visual sub-components (e.g., progress bar fill, seek bar thumb, picker dividers) + * - System-level decorations (e.g., window background in DecorViewMapper) + * + * @param view the view to resolve identity for + * @param viewIdentityProvider the provider for generating stable view identities + * @return the stable identity hash, or null if the view's canonical path cannot be determined + * (e.g., detached view) + */ + protected fun resolveViewIdentity(view: View, viewIdentityProvider: ViewIdentityProvider): String? { + return viewIdentityProvider.resolveIdentity(view) + } + /** * Resolves the [MobileSegment.ShapeStyle] based on the [View] drawables. */ diff --git a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt index fbe67b00e7..3a8124d6bd 100644 --- a/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt +++ b/features/dd-sdk-android-session-replay/src/main/kotlin/com/datadog/android/sessionreplay/recorder/mapper/TextViewMapper.kt @@ -136,6 +136,7 @@ open class TextViewMapper( y = viewGlobalBounds.y, width = viewGlobalBounds.width, height = viewGlobalBounds.height, + permanentId = resolveViewIdentity(textView, mappingContext.viewIdentityProvider), shapeStyle = null, border = null, text = capturedText, diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt index 22fca29ec6..0bf2b8d8e8 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/forge/MappingContextForgeryFactory.kt @@ -20,7 +20,8 @@ internal class MappingContextForgeryFactory : ForgeryFactory { textAndInputPrivacy = forge.getForgery(), imagePrivacy = forge.getForgery(), touchPrivacyManager = mock(), - interopViewCallback = mock() + interopViewCallback = mock(), + viewIdentityProvider = mock() ) } } 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 c85287ca79..ae4aacef68 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 @@ -19,6 +19,7 @@ import com.datadog.android.sessionreplay.internal.recorder.SnapshotProducer.Comp import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.SystemInformation +import com.datadog.android.sessionreplay.recorder.ViewIdentityProvider import com.datadog.android.sessionreplay.setSessionReplayImagePrivacy import com.datadog.android.sessionreplay.setSessionReplayTextAndInputPrivacy import com.datadog.android.sessionreplay.utils.ImageWireframeHelper @@ -72,6 +73,9 @@ internal class SnapshotProducerTest { @Mock lateinit var mockTouchPrivacyManager: TouchPrivacyManager + @Mock + lateinit var mockViewIdentityProvider: ViewIdentityProvider + @Forgery lateinit var fakeSystemInformation: SystemInformation @@ -112,7 +116,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -139,7 +144,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -166,7 +172,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -193,7 +200,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -224,7 +232,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -261,7 +270,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) val argumentCaptor = argumentCaptor() @@ -296,7 +306,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) val argumentCaptor = argumentCaptor() @@ -340,7 +351,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -373,7 +385,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -413,7 +426,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then @@ -460,7 +474,8 @@ internal class SnapshotProducerTest { fakeSystemInformation, fakeTextAndInputPrivacy, fakeImagePrivacy, - mockRecordedDataQueueRefs + mockRecordedDataQueueRefs, + mockViewIdentityProvider ) // Then diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListenerTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListenerTest.kt index cb9b30eefe..afc0a109f8 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListenerTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/listener/WindowsOnDrawListenerTest.kt @@ -145,6 +145,7 @@ internal class WindowsOnDrawListenerTest { eq(fakeSystemInformation), eq(fakeTextAndInputPrivacy), eq(fakeImagePrivacy), + any(), any() ) ) @@ -212,7 +213,8 @@ internal class WindowsOnDrawListenerTest { systemInformation = any(), textAndInputPrivacy = eq(fakeTextAndInputPrivacy), imagePrivacy = eq(fakeImagePrivacy), - recordedDataQueueRefs = argCaptor.capture() + recordedDataQueueRefs = argCaptor.capture(), + viewIdentityProvider = any() ) assertThat(argCaptor.firstValue.recordedDataQueueItem).isEqualTo(fakeSnapshotQueueItem) verify(mockRecordedDataQueueHandler).tryToConsumeItems() @@ -304,7 +306,7 @@ internal class WindowsOnDrawListenerTest { "Capture Record" ) ).thenReturn(mockPerformanceMetric) - whenever(mockSnapshotProducer.produce(any(), any(), any(), any(), any())).thenReturn(null) + whenever(mockSnapshotProducer.produce(any(), any(), any(), any(), any(), any())).thenReturn(null) whenever(mockRecordedDataQueueHandler.addSnapshotItem(any())) .thenReturn(fakeSnapshotQueueItem) fakeSnapshotQueueItem.pendingJobs.set(0) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapperTest.kt index b4dc7e8d76..6097420bc1 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/mapper/HiddenViewMapperTest.kt @@ -13,6 +13,7 @@ import com.datadog.android.sessionreplay.internal.recorder.mapper.HiddenViewMapp import com.datadog.android.sessionreplay.model.MobileSegment import com.datadog.android.sessionreplay.recorder.MappingContext import com.datadog.android.sessionreplay.recorder.SystemInformation +import com.datadog.android.sessionreplay.recorder.ViewIdentityProvider import com.datadog.android.sessionreplay.utils.AsyncJobStatusCallback import com.datadog.android.sessionreplay.utils.GlobalBounds import com.datadog.android.sessionreplay.utils.ViewBoundsResolver @@ -59,6 +60,9 @@ internal class HiddenViewMapperTest { @Mock lateinit var mockInternalLogger: InternalLogger + @Mock + lateinit var mockViewIdentityProvider: ViewIdentityProvider + private lateinit var testedViewMapper: HiddenViewMapper @BeforeEach @@ -69,6 +73,9 @@ internal class HiddenViewMapperTest { whenever(mockMappingContext.systemInformation) .thenReturn(mockSystemInformation) + whenever(mockMappingContext.viewIdentityProvider) + .thenReturn(mockViewIdentityProvider) + whenever(mockSystemInformation.screenDensity) .thenReturn(1f) diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt index b044c865f9..8b26b6ab36 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/internal/recorder/resources/DefaultImageWireframeHelperTest.kt @@ -19,6 +19,7 @@ import android.util.DisplayMetrics import android.view.View import android.widget.TextView import com.datadog.android.api.InternalLogger +import com.datadog.android.api.feature.FeatureSdkCore import com.datadog.android.sessionreplay.IMAGE_DIMEN_CONSIDERED_PII_IN_DP import com.datadog.android.sessionreplay.ImagePrivacy import com.datadog.android.sessionreplay.forge.ForgeConfigurator @@ -119,6 +120,9 @@ internal class DefaultImageWireframeHelperTest { @Mock lateinit var mockContext: Context + @Mock + lateinit var mockSdkCore: FeatureSdkCore + @LongForgery var fakeViewId: Long = 0L @@ -189,7 +193,8 @@ internal class DefaultImageWireframeHelperTest { resourceResolver = mockResourceResolver, viewIdentifierResolver = mockViewIdentifierResolver, viewUtilsInternal = mockViewUtilsInternal, - imageTypeResolver = mockImageTypeResolver + imageTypeResolver = mockImageTypeResolver, + sdkCore = mockSdkCore ) } diff --git a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/AbstractWireframeMapperTest.kt b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/AbstractWireframeMapperTest.kt index fe03e71a4d..2b5d86b877 100644 --- a/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/AbstractWireframeMapperTest.kt +++ b/features/dd-sdk-android-session-replay/src/test/kotlin/com/datadog/android/sessionreplay/recorder/mapper/AbstractWireframeMapperTest.kt @@ -159,7 +159,13 @@ internal abstract class AbstractWireframeMapperTest