Support apps compiled against Jetpack Compose 1.10#5189
Support apps compiled against Jetpack Compose 1.10#5189
Conversation
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Bug Fixes 🐛
Internal Changes 🔧Deps
Other
🤖 This preview updates automatically when you update the PR. |
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8687935 | 332.52 ms | 362.23 ms | 29.71 ms |
| 27d7cf8 | 309.43 ms | 364.27 ms | 54.85 ms |
| f064536 | 329.00 ms | 395.62 ms | 66.62 ms |
| abfcc92 | 337.38 ms | 427.39 ms | 90.00 ms |
| ae7fed0 | 293.84 ms | 380.22 ms | 86.38 ms |
| d15471f | 361.89 ms | 378.07 ms | 16.18 ms |
| 1df7eb6 | 397.04 ms | 429.64 ms | 32.60 ms |
| 9fbb112 | 401.87 ms | 515.87 ms | 114.00 ms |
| 6405ec5 | 310.88 ms | 354.56 ms | 43.69 ms |
| 62b579c | 318.48 ms | 367.71 ms | 49.24 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8687935 | 1.58 MiB | 2.19 MiB | 619.17 KiB |
| 27d7cf8 | 1.58 MiB | 2.12 MiB | 549.42 KiB |
| f064536 | 1.58 MiB | 2.20 MiB | 633.90 KiB |
| abfcc92 | 1.58 MiB | 2.13 MiB | 557.31 KiB |
| ae7fed0 | 1.58 MiB | 2.12 MiB | 551.77 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| 1df7eb6 | 1.58 MiB | 2.10 MiB | 532.97 KiB |
| 9fbb112 | 1.58 MiB | 2.11 MiB | 539.18 KiB |
| 6405ec5 | 1.58 MiB | 2.12 MiB | 552.23 KiB |
| 62b579c | 0 B | 0 B | 0 B |
Previous results on branch: markushi/fix/compose-110-api-changes
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a1de9cc | 314.30 ms | 357.84 ms | 43.54 ms |
| e0f5f6c | 368.21 ms | 440.94 ms | 72.73 ms |
| 448b99c | 320.90 ms | 344.98 ms | 24.08 ms |
| 82fc2e8 | 307.88 ms | 311.33 ms | 3.45 ms |
| 64b3ffa | 299.61 ms | 357.85 ms | 58.24 ms |
| 373d470 | 310.83 ms | 361.18 ms | 50.35 ms |
| adf6f50 | 327.50 ms | 367.76 ms | 40.26 ms |
| 1188096 | 338.00 ms | 393.98 ms | 55.98 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| a1de9cc | 0 B | 0 B | 0 B |
| e0f5f6c | 0 B | 0 B | 0 B |
| 448b99c | 0 B | 0 B | 0 B |
| 82fc2e8 | 0 B | 0 B | 0 B |
| 64b3ffa | 0 B | 0 B | 0 B |
| 373d470 | 0 B | 0 B | 0 B |
| adf6f50 | 0 B | 0 B | 0 B |
| 1188096 | 0 B | 0 B | 0 B |
Sentry Build Distribution
|
…sentry/sentry-java into markushi/fix/compose-110-api-changes
sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Click/scroll flags never reset between sibling nodes
- Added flag resets at the start of each in-bounds node examination to prevent incorrect gesture target detection across siblings.
Or push these changes by commenting:
@cursor push 98ce20ca72
Preview (98ce20ca72)
diff --git a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt
--- a/sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt
+++ b/sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt
@@ -58,6 +58,8 @@
while (!queue.isEmpty()) {
val node = queue.poll() ?: continue
if (node.isPlaced && layoutNodeBoundsContain(rootLayoutNode, node, x, y)) {
+ isClickable = false
+ isScrollable = false
val modifiers = node.getModifierInfo()
for (index in modifiers.indices) {This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt
Outdated
Show resolved
Hide resolved
...roid-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt
Show resolved
Hide resolved
...ndroid-replay/src/main/java/io/sentry/android/replay/viewhierarchy/SentryLayoutNodeHelper.kt
Show resolved
Hide resolved
...roid-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt
Outdated
Show resolved
Hide resolved
...roid-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt
Outdated
Show resolved
Hide resolved
romtsn
left a comment
There was a problem hiding this comment.
i guess bots' comments need addressing but looks good otherwise, thanks!
sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt
Show resolved
Hide resolved
...roid-replay/src/main/java/io/sentry/android/replay/viewhierarchy/ComposeViewHierarchyNode.kt
Outdated
Show resolved
Hide resolved
| // skip any children for scrollable targets | ||
| break | ||
| } | ||
| queue.addAll(node.zSortedChildren.asMutableList().map { Pair(it, tag) }) |
There was a problem hiding this comment.
Children not explored when parent is out of bounds
Medium Severity
The queue.addAll call that enqueues child nodes for BFS traversal was moved inside the if (node.isPlaced && layoutNodeBoundsContain(...)) block. In the old code, children were always added to the queue regardless of the parent's placement or bounds, meaning siblings and deeper descendants were always explored. Now, if a parent node's bounds don't contain the click coordinates, its entire subtree is pruned. This misses targets where a child extends beyond its parent's bounds (e.g., via Modifier.offset), since the clickable child is never visited.
There was a problem hiding this comment.
IMHO that's fine as well
...ndroid-replay/src/main/java/io/sentry/android/replay/viewhierarchy/SentryLayoutNodeHelper.kt
Show resolved
Hide resolved
| val tag = composeHelper.extractTag(modifierInfo.modifier) | ||
| if (tag != null) { | ||
| lastKnownTag = tag | ||
| } |
There was a problem hiding this comment.
| } | |
| break | |
| } |
could break here or nope?
There was a problem hiding this comment.
yeah we could simply the first tag found instead of returning the last one, could save a few cycles at no real cost. I guest 99% of the time there's anyway just one tag, and for the remaining 1% returning the first vs. the last is both equally right / wrong 😅
…sentry/sentry-java into markushi/fix/compose-110-api-changes
| composeHelper.extractTag(modifierInfo.modifier).also { | ||
| return it | ||
| } |
There was a problem hiding this comment.
Bug: The extractTag function incorrectly uses .also { return it }, causing it to return after checking the first modifier, even if the result is null, preventing other modifiers from being checked.
Severity: MEDIUM
Suggested Fix
Replace .also { return it } with ?.let { return it }. This ensures the function only returns when a non-null tag is found, allowing the loop to continue checking other modifiers if the result is null.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
sentry-compose/src/androidMain/kotlin/io/sentry/compose/gestures/ComposeGestureTargetLocator.kt#L120-L122
Potential issue: The `extractTag` function iterates through a `LayoutNode`'s modifiers
to find a test tag. However, it uses
`composeHelper.extractTag(modifierInfo.modifier).also { return it }`. The `return it`
inside the `also` block is a non-local return that exits the `extractTag` function on
the very first iteration of the loop, regardless of whether a tag was found. This means
if the first modifier in the list does not contain a tag, the function will incorrectly
return `null` without checking any subsequent modifiers. As a result, UI elements will
not be correctly tagged if their tag modifier is not the first in the modifier list.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| val modifierInfo = modifiers[index] | ||
| composeHelper.extractTag(modifierInfo.modifier).also { | ||
| return it | ||
| } |
There was a problem hiding this comment.
extractTag returns on first modifier, skipping remaining
High Severity
The .also { return it } performs a non-local return from extractTag on the very first loop iteration, regardless of whether the result is null or not. If the first modifier in a LayoutNode isn't a tag modifier (e.g. it's a ClickableElement), the function returns null without ever checking subsequent modifiers — even if one of them holds the tag. This effectively breaks tag detection for any node whose tag modifier isn't listed first. The correct pattern is ?.let { return it }, which only returns for non-null results. The sibling implementation in ComposeViewHierarchyExporter.setTag() (which has a comment saying it "needs to be in-sync with ComposeGestureTargetLocator") correctly uses an explicit if (tag != null) check.
| // for backwards compatibility | ||
| // Jetpack Compose 1.8 or older | ||
| return if (getCollapsedSemanticsMethod != null) { | ||
| getCollapsedSemanticsMethod!!.invoke(node) as SemanticsConfiguration |
There was a problem hiding this comment.
Non-nullable cast in fallback causes over-masking on old Compose
Medium Severity
The reflection fallback in retrieveSemanticsConfiguration casts the result as non-nullable SemanticsConfiguration instead of SemanticsConfiguration?. On Compose versions below 1.10, node.semanticsConfiguration throws NoSuchMethodError, triggering the fallback that invokes getCollapsedSemantics$ui_release via reflection. That method can return null for layout nodes without semantics (e.g. Box, Column, Row). Kotlin's null as SemanticsConfiguration throws a TypeCastException/NullPointerException, which propagates to the outer catch in fromComposeNode and force-masks the node with shouldMask = true. This results in excessive masking of non-semantic layout nodes on older Compose versions.



📜 Description
Fixes #5086
Also fixes yet another issue with detecting scroll / click targets within Jetpack Compose.
Starting with Jetpack Compose 1.10, some internal APIs JVM methods dropped the
_release()suffix. E.g. the compiler now generatesLayoutNode.getChildren$ui()instead of previousLayoutNode.getChildren$ui_release().This PR wraps those methods and introduces a compat layer - so they work across multiple versions during runtime.
It required some "extra" gradle setup I'm not to happy about (as AGP does not seem to support different class paths per source set like we did for AGP), but it seems to work good enough.
androidx.compose.ui:ui:1.3.0replayhttps://sentry-sdks.sentry.io/explore/replays/effaac5ca65b4ab0b94d7a413c3a5288
androidx.compose.ui:ui:1.8.1replayhttps://sentry-sdks.sentry.io/explore/replays/173b2e3207e44cffa0f5ce4b9bcb0518
androidx.compose.ui:ui:1.10.5replayhttps://sentry-sdks.sentry.io/explore/replays/563ef966395944878be18bcebb0d0092
📝 Checklist
sendDefaultPIIis enabled.🔮 Next steps