Skip to content

Support apps compiled against Jetpack Compose 1.10#5189

Open
markushi wants to merge 19 commits intomainfrom
markushi/fix/compose-110-api-changes
Open

Support apps compiled against Jetpack Compose 1.10#5189
markushi wants to merge 19 commits intomainfrom
markushi/fix/compose-110-api-changes

Conversation

@markushi
Copy link
Member

@markushi markushi commented Mar 12, 2026

📜 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 generates LayoutNode.getChildren$ui() instead of previous LayoutNode.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.0 replay
https://sentry-sdks.sentry.io/explore/replays/effaac5ca65b4ab0b94d7a413c3a5288

androidx.compose.ui:ui:1.8.1 replay
https://sentry-sdks.sentry.io/explore/replays/173b2e3207e44cffa0f5ce4b9bcb0518

androidx.compose.ui:ui:1.10.5 replay
https://sentry-sdks.sentry.io/explore/replays/563ef966395944878be18bcebb0d0092

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

@github-actions
Copy link
Contributor

github-actions bot commented Mar 12, 2026

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Bug Fixes 🐛

  • (android) Bump epitaph to 0.1.1 by supervacuus in #5200

Internal Changes 🔧

Deps

  • Update Native SDK to v0.13.2 by github-actions in #5181
  • Bump github/codeql-action from 4.32.4 to 4.32.6 by dependabot in #5170
  • Bump dorny/paths-filter from 3.0.2 to 4.0.1 by dependabot in #5195
  • Bump actions/create-github-app-token from 2.2.1 to 3.0.0 by dependabot in #5196
  • Bump getsentry/craft from 2.23.1 to 2.24.1 by dependabot in #5197
  • Bump reactivecircus/android-emulator-runner from 2.35.0 to 2.37.0 by dependabot in #5194

Other

  • Support apps compiled against Jetpack Compose 1.10 by markushi in #5189

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 12, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 343.50 ms 409.85 ms 66.35 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

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
Copy link

sentry bot commented Mar 12, 2026

Sentry Build Distribution

App Name App ID Version Configuration Install Page
SDK Size io.sentry.tests.size 8.35.0 (1) release Install Build

@markushi markushi marked this pull request as ready for review March 16, 2026 08:29
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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.

Copy link
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess bots' comments need addressing but looks good otherwise, thanks!

// skip any children for scrollable targets
break
}
queue.addAll(node.zSortedChildren.asMutableList().map { Pair(it, tag) })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO that's fine as well

val tag = composeHelper.extractTag(modifierInfo.modifier)
if (tag != null) {
lastKnownTag = tag
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
break
}

could break here or nope?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 😅

Comment on lines +120 to +122
composeHelper.extractTag(modifierInfo.modifier).also {
return it
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

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
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

// for backwards compatibility
// Jetpack Compose 1.8 or older
return if (getCollapsedSemanticsMethod != null) {
getCollapsedSemanticsMethod!!.invoke(node) as SemanticsConfiguration
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session replay not working

3 participants