Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions dd-sdk-android-internal/api/apiSurface
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
data class com.datadog.android.heatmaps.CrossPlatformHeatmapActionData
constructor(List<String>, String, Long, Long, Long?, Long?)
interface com.datadog.android.heatmaps.HeatmapIdentifierRegistryProvider
val heatmapIdentifierRegistry: com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistry
fun heatmapViewKey(android.view.View): Long
interface com.datadog.android.internal.attributes.LocalAttribute
enum Key
constructor(String)
Expand Down Expand Up @@ -69,10 +74,6 @@ interface com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistry
fun getHeatmapIdentifier(Long, String): HeatmapIdentifier?
companion object
fun create(): HeatmapIdentifierRegistry
interface com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistryProvider
val heatmapIdentifierRegistry: HeatmapIdentifierRegistry
fun heatmapViewKey(android.view.View): Long
fun android.view.View.isValidTapTarget(): Boolean
class com.datadog.android.internal.lifecycle.ProcessLifecycleMonitor : android.app.Application.ActivityLifecycleCallbacks
constructor(Callback)
val activitiesResumedCounter: java.util.concurrent.atomic.AtomicInteger
Expand Down Expand Up @@ -283,6 +284,7 @@ fun StringBuilder.appendIfNotEmpty(String)
fun StringBuilder.appendIfNotEmpty(Char)
fun String.toBase64(): String
fun String.fromBase64(): String?
fun android.view.View.isValidTapTarget(): Boolean
fun Thread.safeGetThreadId(): Long
fun Thread.State.asString(): String
fun Array<StackTraceElement>.loggableStackTrace(): String
Expand Down
45 changes: 33 additions & 12 deletions dd-sdk-android-internal/api/dd-sdk-android-internal.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
public final class com/datadog/android/heatmaps/CrossPlatformHeatmapActionData {
public fun <init> (Ljava/util/List;Ljava/lang/String;JJLjava/lang/Long;Ljava/lang/Long;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()J
public final fun component4 ()J
public final fun component5 ()Ljava/lang/Long;
public final fun component6 ()Ljava/lang/Long;
public final fun copy (Ljava/util/List;Ljava/lang/String;JJLjava/lang/Long;Ljava/lang/Long;)Lcom/datadog/android/heatmaps/CrossPlatformHeatmapActionData;
public static synthetic fun copy$default (Lcom/datadog/android/heatmaps/CrossPlatformHeatmapActionData;Ljava/util/List;Ljava/lang/String;JJLjava/lang/Long;Ljava/lang/Long;ILjava/lang/Object;)Lcom/datadog/android/heatmaps/CrossPlatformHeatmapActionData;
public fun equals (Ljava/lang/Object;)Z
public final fun getElementPath ()Ljava/util/List;
public final fun getPositionX ()J
public final fun getPositionY ()J
public final fun getTargetHeight ()Ljava/lang/Long;
public final fun getTargetWidth ()Ljava/lang/Long;
public final fun getViewUrl ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public abstract interface class com/datadog/android/heatmaps/HeatmapIdentifierRegistryProvider {
public abstract fun getHeatmapIdentifierRegistry ()Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;
}

public final class com/datadog/android/heatmaps/HeatmapViewKeyKt {
public static final fun heatmapViewKey (Landroid/view/View;)J
}

public abstract interface class com/datadog/android/internal/attributes/LocalAttribute {
}

Expand Down Expand Up @@ -150,24 +179,12 @@ public final class com/datadog/android/internal/heatmaps/HeatmapIdentifierRegist
public final fun create ()Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;
}

public abstract interface class com/datadog/android/internal/heatmaps/HeatmapIdentifierRegistryProvider {
public abstract fun getHeatmapIdentifierRegistry ()Lcom/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry;
}

public final class com/datadog/android/internal/heatmaps/HeatmapViewKeyKt {
public static final fun heatmapViewKey (Landroid/view/View;)J
}

public final class com/datadog/android/internal/heatmaps/NoOpHeatmapIdentifierRegistry : com/datadog/android/internal/heatmaps/HeatmapIdentifierRegistry {
public fun <init> ()V
public fun getHeatmapIdentifier (JLjava/lang/String;)Lcom/datadog/android/internal/heatmaps/HeatmapIdentifier;
public fun setHeatmapIdentifiers (Ljava/util/Map;Ljava/lang/String;)V
}

public final class com/datadog/android/internal/heatmaps/TapTargetUtilsKt {
public static final fun isValidTapTarget (Landroid/view/View;)Z
}

public final class com/datadog/android/internal/lifecycle/ProcessLifecycleMonitor : android/app/Application$ActivityLifecycleCallbacks {
public fun <init> (Lcom/datadog/android/internal/lifecycle/ProcessLifecycleMonitor$Callback;)V
public final fun getActivitiesResumedCounter ()Ljava/util/concurrent/atomic/AtomicInteger;
Expand Down Expand Up @@ -653,6 +670,10 @@ public final class com/datadog/android/internal/utils/StringExtKt {
public static final fun toBase64 (Ljava/lang/String;)Ljava/lang/String;
}

public final class com/datadog/android/internal/utils/TapTargetUtilsKt {
public static final fun isValidTapTarget (Landroid/view/View;)Z
}

public final class com/datadog/android/internal/utils/ThreadExtKt {
public static final fun asString (Ljava/lang/Thread$State;)Ljava/lang/String;
public static final fun loggableStackTrace ([Ljava/lang/StackTraceElement;)Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.heatmaps

/**
* Heatmap data for a RUM action, used by cross-platform SDKs to correlate a tap action with
* a specific UI element.
*
* @property elementPath the path segments from the root component down to the tapped element
* (e.g. `["root", "container", "submitButton"]`), using the same naming convention as the
* cross-platform Session Replay layer so that the resulting identifier matches the wireframe.
* @property viewUrl the RUM view URL returned by `_RumInternalProxy.getCurrentViewUrl()` at tap time.
* @property positionX the x-coordinate of the tap relative to the target element, in dp.
* @property positionY the y-coordinate of the tap relative to the target element, in dp.
* @property targetWidth the width of the tapped element, in dp, or null if unavailable.
* @property targetHeight the height of the tapped element, in dp, or null if unavailable.
*/
data class CrossPlatformHeatmapActionData(
val elementPath: List<String>,
val viewUrl: String,
val positionX: Long,
val positionY: Long,
val targetWidth: Long?,
val targetHeight: Long?
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.heatmaps
package com.datadog.android.heatmaps

import com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistry

/**
* Implemented by SDK features that own a [HeatmapIdentifierRegistry], allowing peer features
* to obtain a typed reference via [com.datadog.android.api.feature.FeatureScope.unwrap].
*/
interface HeatmapIdentifierRegistryProvider {
/** The [HeatmapIdentifierRegistry] owned by this feature. */
/**
* The registry that maps view identity keys to their stable [HeatmapIdentifier]s for this
* feature's current screen. Session Replay writes identifiers into this registry during
* each traversal; the RUM layer reads from it when a tap action is sent.
*/
val heatmapIdentifierRegistry: HeatmapIdentifierRegistry
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.heatmaps
package com.datadog.android.heatmaps

import android.view.View

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package com.datadog.android.internal.heatmaps

import com.datadog.android.internal.utils.toHexString
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
Expand All @@ -29,6 +30,11 @@ data class HeatmapIdentifier(val rawValue: String) {
* Creates a [HeatmapIdentifier] for the given UI element by hashing its canonical path,
* or null if hashing fails.
*
* All string parameters must be **raw, unencoded** values — this function applies
* percent-encoding internally. Cross-platform SDKs must pass raw strings (e.g. the
* literal screen name returned by `getCurrentViewUrl()`, not a pre-encoded version)
* so that the identifier is always produced by the same Android encoding logic.
*
* @param elementPath the path segments from the root view down to this element,
* where each segment identifies a view in the hierarchy (e.g. its resource entry name).
* @param screenName the current RUM view URL, used to scope identifiers to a screen.
Expand All @@ -37,16 +43,25 @@ data class HeatmapIdentifier(val rawValue: String) {
* @param appPackageName the application package name (e.g. `com.example.app`),
* used to globally namespace identifiers across apps.
* @param onHashingFailure invoked with the caught exception if hashing fails.
* Callers should forward this to telemetry.
*/
fun create(
elementPath: List<String>,
screenName: String,
appPackageName: String,
onHashingFailure: (Throwable) -> Unit = {}
): HeatmapIdentifier? {
val path = canonicalPath(elementPath, screenName, appPackageName)
return sha256Hex(path, onHashingFailure)?.let { HeatmapIdentifier(it) }
return try {
val path = canonicalPath(elementPath, screenName, appPackageName)
HeatmapIdentifier(sha256Hex(path))
} catch (e: NoSuchAlgorithmException) {
// Unreachable on Android: SHA-256 is always available.
onHashingFailure(e)
null
} catch (e: UnsupportedEncodingException) {
// Thrown by URLEncoder.encode if UTF-8 is unavailable (unreachable on Android).
onHashingFailure(e)
null
}
}

private fun canonicalPath(
Expand All @@ -63,21 +78,15 @@ data class HeatmapIdentifier(val rawValue: String) {
}
}

@Suppress("UnsafeThirdPartyFunctionCall") // UTF-8 is always available on Android; cannot throw
@Suppress("UnsafeThirdPartyFunctionCall") // UnsupportedEncodingException caught in create()
private fun escape(input: String): String = URLEncoder.encode(input, "UTF-8")

private fun sha256Hex(input: String, onHashingFailure: (Throwable) -> Unit): String? {
return try {
val digest = MessageDigest.getInstance("SHA-256")
// digest(ByteArray) does not throw: the input is non-null (toByteArray always
// returns a non-null array) and DigestException is only declared on the
// offset/length overload, not this one.
@Suppress("UnsafeThirdPartyFunctionCall")
digest.digest(input.toByteArray(Charsets.UTF_8)).toHexString()
} catch (e: NoSuchAlgorithmException) {
onHashingFailure(e)
null
}
@Suppress("UnsafeThirdPartyFunctionCall") // SHA-256 is always available on Android
private fun sha256Hex(input: String): String {
val digest = MessageDigest.getInstance("SHA-256")
// digest(ByteArray): input is non-null, DigestException not thrown on this overload.
@Suppress("UnsafeThirdPartyFunctionCall")
return digest.digest(input.toByteArray(Charsets.UTF_8)).toHexString()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.heatmaps
package com.datadog.android.internal.utils

import android.view.View

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.heatmaps
package com.datadog.android.heatmaps

import android.view.View
import android.view.ViewParent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.internal.heatmaps
package com.datadog.android.internal.utils

import android.view.View
import com.datadog.android.internal.forge.Configurator
Expand Down
2 changes: 2 additions & 0 deletions features/dd-sdk-android-rum/api/apiSurface
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ enum com.datadog.android.rum.RumSessionType
- SYNTHETICS
- USER
class com.datadog.android.rum._RumInternalProxy
fun addActionWithHeatmap(RumActionType, String, com.datadog.android.heatmaps.CrossPlatformHeatmapActionData, Map<String, Any?>)
fun getCurrentViewUrl(): String?
fun addLongTask(Long, String)
fun updatePerformanceMetric(RumPerformanceMetric, Double)
fun updateExternalRefreshRate(Double)
Expand Down
2 changes: 2 additions & 0 deletions features/dd-sdk-android-rum/api/dd-sdk-android-rum.api
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,10 @@ public final class com/datadog/android/rum/RumSessionType : java/lang/Enum {

public final class com/datadog/android/rum/_RumInternalProxy {
public static final field Companion Lcom/datadog/android/rum/_RumInternalProxy$Companion;
public final fun addActionWithHeatmap (Lcom/datadog/android/rum/RumActionType;Ljava/lang/String;Lcom/datadog/android/heatmaps/CrossPlatformHeatmapActionData;Ljava/util/Map;)V
public final fun addLongTask (JLjava/lang/String;)V
public final fun enableJankStatsTracking (Landroid/app/Activity;)V
public final fun getCurrentViewUrl ()Ljava/lang/String;
public final fun setInternalViewAttribute (Ljava/lang/String;Ljava/lang/Object;)V
public final fun setSyntheticsAttribute (Ljava/lang/String;Ljava/lang/String;)V
public final fun setSyntheticsAttributeFromIntent (Landroid/content/Intent;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ object Rum {
return DatadogRumMonitor(
applicationId = rumFeature.applicationId,
sdkCore = sdkCore,
appPackageName = rumFeature.appContext.packageName,
sessionEndedMetricDispatcher = sessionEndedMetricDispatcher,
sessionSampler = sessionSampler,
writer = rumFeature.dataWriter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.app.Activity
import android.content.Intent
import com.datadog.android.api.InternalLogger
import com.datadog.android.event.EventMapper
import com.datadog.android.heatmaps.CrossPlatformHeatmapActionData
import com.datadog.android.internal.telemetry.InternalTelemetryEvent.ApiUsage.NetworkInstrumentation.LibraryType
import com.datadog.android.lint.InternalApi
import com.datadog.android.rum.RumConfiguration.Builder
Expand Down Expand Up @@ -42,6 +43,17 @@ import com.datadog.android.telemetry.model.TelemetryConfigurationEvent
class _RumInternalProxy internal constructor(private val rumMonitor: AdvancedRumMonitor) {
@Volatile private var handledSyntheticsAttribute = false

fun addActionWithHeatmap(
type: RumActionType,
name: String,
crossPlatformHeatmapActionData: CrossPlatformHeatmapActionData,
attributes: Map<String, Any?>
) {
rumMonitor.addActionWithHeatmapAttributes(type, name, crossPlatformHeatmapActionData, attributes)
}

fun getCurrentViewUrl(): String? = rumMonitor.getCurrentViewUrl()

fun addLongTask(durationNs: Long, target: String) {
rumMonitor.addLongTask(durationNs, target)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ import com.datadog.android.core.internal.utils.scheduleSafe
import com.datadog.android.event.EventMapper
import com.datadog.android.event.MapperSerializer
import com.datadog.android.event.NoOpEventMapper
import com.datadog.android.heatmaps.HeatmapIdentifierRegistryProvider
import com.datadog.android.internal.flags.RumFlagEvaluationMessage
import com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistry
import com.datadog.android.internal.heatmaps.HeatmapIdentifierRegistryProvider
import com.datadog.android.internal.system.BuildSdkVersionProvider
import com.datadog.android.internal.telemetry.InternalTelemetryEvent
import com.datadog.android.internal.thread.isMainThread
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.datadog.android.rum.RumSessionType
import com.datadog.android.rum.internal.FeaturesContextResolver
import com.datadog.android.rum.internal.domain.RumContext
import com.datadog.android.rum.internal.domain.Time
import com.datadog.android.rum.internal.heatmaps.HeatmapActionResolver
import com.datadog.android.rum.internal.instrumentation.insights.InsightsCollector
import com.datadog.android.rum.internal.monitor.StorageEvent
import com.datadog.android.rum.internal.toAction
Expand Down Expand Up @@ -46,8 +47,7 @@ internal class RumActionScope(
internal val sampleRate: Float,
private val rumSessionTypeOverride: RumSessionType?,
private val insightsCollector: InsightsCollector,
internal val heatmapData: HeatmapActionData? = null,
private val heatmapIdentifierRegistry: HeatmapIdentifierRegistry? = null
internal val heatmapResolver: HeatmapActionResolver? = null
) : RumScope {

private val inactivityThresholdNs = TimeUnit.MILLISECONDS.toNanos(inactivityThresholdMs)
Expand Down Expand Up @@ -357,7 +357,7 @@ internal class RumActionScope(
sessionPrecondition = rumContext.sessionStartReason.toActionSessionPrecondition()
),
configuration = ActionEvent.Configuration(sessionSampleRate = sampleRate),
action = heatmapData?.let { resolveHeatmapAction(it, rumContext.viewUrl.orEmpty()) }
action = heatmapResolver?.resolve(rumContext.viewUrl.orEmpty())
),
connectivity = networkInfo.toActionConnectivity(),
service = datadogContext.service,
Expand All @@ -382,20 +382,6 @@ internal class RumActionScope(
sent = true
}

private fun resolveHeatmapAction(data: HeatmapActionData, viewUrl: String): ActionEvent.DdAction? {
val permanentId = heatmapIdentifierRegistry
?.getHeatmapIdentifier(data.viewKey, viewUrl)
?.rawValue ?: return null
return ActionEvent.DdAction(
position = ActionEvent.Position(x = data.positionX, y = data.positionY),
target = ActionEvent.DdActionTarget(
permanentId = permanentId,
width = data.targetWidth,
height = data.targetHeight
)
)
}

// endregion

companion object {
Expand Down Expand Up @@ -429,8 +415,19 @@ internal class RumActionScope(
sampleRate = sampleRate,
rumSessionTypeOverride = rumSessionTypeOverride,
insightsCollector = insightsCollector,
heatmapData = event.heatmapData,
heatmapIdentifierRegistry = heatmapIdentifierRegistry
heatmapResolver = when {
event.crossPlatformHeatmapActionData != null && event.appPackageName != null ->
HeatmapActionResolver.CrossPlatform(
data = event.crossPlatformHeatmapActionData,
appPackageName = event.appPackageName,
logger = sdkCore.internalLogger
)
event.nativeHeatmapActionData != null -> HeatmapActionResolver.Native(
data = event.nativeHeatmapActionData,
registry = heatmapIdentifierRegistry
)
else -> null
}
)
}
}
Expand Down
Loading
Loading