PANA-5681: Add ViewIdentityResolver infrastructure#3202
PANA-5681: Add ViewIdentityResolver infrastructure#3202gonzalezreal wants to merge 1 commit intofeature/heatmapsfrom
ViewIdentityResolver infrastructure#3202Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cebbd7bc94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
...oid-internal/src/main/java/com/datadog/android/internal/identity/ViewIdentityResolverImpl.kt
Show resolved
Hide resolved
...oid-internal/src/main/java/com/datadog/android/internal/identity/ViewIdentityResolverImpl.kt
Show resolved
Hide resolved
| /** | ||
| * Sets the current screen identifier. Takes precedence over Activity-based detection. | ||
| * @param identifier the screen identifier (typically RUM view URL), or null to clear | ||
| */ |
There was a problem hiding this comment.
Do we really clear it? if so what's the case?
| @MockitoSettings(strictness = Strictness.LENIENT) | ||
| internal class ViewIdentityResolverTest { | ||
|
|
||
| private lateinit var testedManager: ViewIdentityResolverImpl |
There was a problem hiding this comment.
| private lateinit var testedManager: ViewIdentityResolverImpl | |
| private lateinit var testedViewIdentityResolver: ViewIdentityResolverImpl |
| * Cleared when screen changes (namespace depends on currentRumViewIdentifier). | ||
| */ | ||
| @Suppress("UnsafeThirdPartyFunctionCall") // WeakHashMap() is never null | ||
| private val rootScreenNamespaceCache: MutableMap<View, String> = |
There was a problem hiding this comment.
I feel the name is a bit confusing here, "screen" is a concept of heatmaps which represents the whole visible interface according to the RFC, "rootView" is a Android term meaning the root of the view tree hireacy. I think here is more like "screenNamespaceCache indexed by rootView"
|
|
||
| /** Priority 1: Use RUM view identifier if available (set via RumMonitor.startView). */ | ||
| private fun getNamespaceFromRumView(): String? { | ||
| return currentRumViewIdentifier.get()?.let { viewName -> |
There was a problem hiding this comment.
| return currentRumViewIdentifier.get()?.let { viewName -> | |
| return currentRumViewIdentifier.get()?.let { viewUrl -> |
| /** Priority 1: Use RUM view identifier if available (set via RumMonitor.startView). */ | ||
| private fun getNamespaceFromRumView(): String? { | ||
| return currentRumViewIdentifier.get()?.let { viewName -> | ||
| "$NAMESPACE_VIEW_PREFIX${escapePathComponent(viewName)}" |
There was a problem hiding this comment.
| "$NAMESPACE_VIEW_PREFIX${escapePathComponent(viewName)}" | |
| "$NAMESPACE_VIEW_PREFIX${escapePathComponent(viewUrl)}" |
| return input.replace("%", "%25").replace("/", "%2F") | ||
| } | ||
|
|
||
| private fun md5Hex(input: String): String? { |
There was a problem hiding this comment.
we have the exact same logic in MD5HashGenerator, maybe it's time to move it into internal and reuse it here.
0xnm
left a comment
There was a problem hiding this comment.
I went only through the production source files so far and left some comments.
| fun resolveViewIdentity(android.view.View): String? | ||
| companion object | ||
| const val FEATURE_CONTEXT_KEY: String | ||
| class com.datadog.android.internal.identity.ViewIdentityResolverImpl : ViewIdentityResolver |
There was a problem hiding this comment.
a small trick we use to not expose Impl classes is to create a property (in this case it will be a function create(arg)) in the companion object of ViewIdentityResolver.
| fun onWindowRefreshed(root: View) | ||
|
|
||
| /** | ||
| * Resolves the stable identity for a view (32 hex chars), or null if the view is detached. |
There was a problem hiding this comment.
nit: probably we can omit 32 hex chars from the description, it is implementation detail
| /** | ||
| * Key used to store the ViewIdentityResolver instance in the feature context. | ||
| */ | ||
| const val FEATURE_CONTEXT_KEY: String = "_dd.view_identity_resolver" |
There was a problem hiding this comment.
I think this is quite dangerous. What is the purpose to store such object in the feature context given that we have direct access to the type?
| * resources.getResourceName() which is expensive. | ||
| */ | ||
| @Suppress("UnsafeThirdPartyFunctionCall") // LinkedHashMap constructor doesn't throw | ||
| private val resourceNameCache: MutableMap<Int, String> = Collections.synchronizedMap( |
There was a problem hiding this comment.
I don't think synchronizedMap is a good choice here, maybe ConcurrentHashMap is better, given that after some time the number of reads will be much bigger than number of writes.
| @Suppress("UnsafeThirdPartyFunctionCall") // LinkedHashMap constructor doesn't throw | ||
| private val resourceNameCache: MutableMap<Int, String> = Collections.synchronizedMap( | ||
| object : LinkedHashMap<Int, String>(RESOURCE_NAME_CACHE_SIZE, DEFAULT_LOAD_FACTOR, true) { | ||
| override fun removeEldestEntry(eldest: MutableMap.MutableEntry<Int, String>?): Boolean { |
There was a problem hiding this comment.
there is androidx.cache.LruCache class though
|
|
||
| @Synchronized | ||
| override fun setCurrentScreen(identifier: String?) { | ||
| @Suppress("UnsafeThirdPartyFunctionCall") // type-safe: generics prevent VarHandle type mismatches |
There was a problem hiding this comment.
We should declare this call as safe instead, there is already java.util.concurrent.atomic.AtomicBoolean.getAndSet(kotlin.Boolean) declared.
| /** The current RUM view identifier, set via setCurrentScreen(). */ | ||
| private val currentRumViewIdentifier = AtomicReference<String?>(null) | ||
|
|
||
| @Synchronized |
There was a problem hiding this comment.
there is no need for the @Synchronized, AtomicReference.getAndSet already does atomic check guaranteeing the absence of concurency.
| @Synchronized | ||
| override fun onWindowRefreshed(root: View) { | ||
| indexTree(root) | ||
| } | ||
|
|
||
| @Synchronized | ||
| override fun resolveViewIdentity(view: View): String? { |
There was a problem hiding this comment.
given these will be called on the main thread, we should avoid using Synchronized and use more granular synchronization primitives
| /** Depth-first traversal of view hierarchy, computing and caching identity for each view. */ | ||
| private fun traverseAndIndexViews(root: View, rootCanonicalPath: String) { | ||
| // Index the root view (all cache insertions happen here for consistency) | ||
| md5Hex(rootCanonicalPath)?.let { hash -> |
There was a problem hiding this comment.
There was a point about SHA-1 vs MD5 in the RFC, did we expore it?
| } | ||
| } | ||
|
|
||
| /** Priority 2: Fall back to Activity class name if root view has Activity context. */ |
There was a problem hiding this comment.
nit: probably we need to remove such comments, they don't really add any value
What does this PR do?
Adds
ViewIdentityResolverinterface andViewIdentityResolverImpltodd-sdk-android-internal. This component assigns stable, globally unique identifiers to Android Views by hashing their canonical paths with MD5. These identifiers will be used to correlate RUM actions with Session Replay wireframes for heatmap visualization.Motivation
Support mobile heatmaps. This is PR 1 of 4, the foundational infrastructure that RUM and Session Replay will build on.
Additional Notes
Ported from #3164. Self-contained with no dependency on schema changes.
Review checklist (to be filled by reviewers)