Skip to content

feat(android): [Part 4] Prefer Activity ui.load transactions over Nav3 transactions - #6484

Open
0xadam-brown wants to merge 4 commits into
fix/nav3-lifecycle-3-lease-withdrawalfrom
feat/nav3-lifecycle-4-ui-load-preference
Open

0xadam-brown wants to merge 4 commits into
fix/nav3-lifecycle-3-lease-withdrawalfrom
feat/nav3-lifecycle-4-ui-load-preference

Conversation

@0xadam-brown

@0xadam-brown 0xadam-brown commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

PR Stack (Nav3 Lifecycle)


📜 Description

When the user opens an Activity from a Nav3 root, that Activity's ui.load transaction is now bound to the scope instead of being blocked by the Nav3 root's nav transaction (API 29+), matching Nav2. A Nav3 transaction blocked by a closing Activity's ui.load is retried once.

Prior (and wrong) assumption Problematic scenario Before After
Nav transactions only compete with each other for the transaction slot The user opens another Activity from a nav root The nav transaction keeps the slot and blocks the new Activity's ui.load transaction† On API 29+, ui.load binds in preference to the nav transaction, as it does with Nav2

† TTID and TTFD were never affected. Both spans are children of ui.load itself, so they're sent whether or not ui.load is bound to the shared IScope. Binding only decides which transaction ambient spans (HTTP, db, etc.) attach to.

How this PR fixes things

This PR withdraws the lease when a covering Activity is created. On API 29+ this happens in onActivityPreCreated, which runs before ActivityLifecycleIntegration tries to bind ui.load, thereby allowing the ui.load transaction to beat out the nav transaction.

Below API 29 there's no onActivityPreCreated, so creation is reported from onActivityCreated, meaning the nav transaction beats out the ui.load transaction.

Known gaps

Documented in the package README.

  • A ui.action transaction from the gesture that caused the return can still block the nav transaction.
  • Spans produced between the return and the closed Activity's destruction attach to its ui.load.
  • An Activity that's left without being destroyed triggers no retry.

Addressing the last two would require updating ActivityLifecycleIntegration to unbind a finishing Activity's ui.load when it pauses. That'd change behavior for every app and not just users of Nav3, so I haven't included it here. We could follow-up at some point if we care to. Nicely, tackling things in ActivityLifecycleIntegration would have the happy side effect of simplifying the heuristics we have to use here.

💡 Motivation and Context

Nav2 lets an Activity's ui.load transaction take precedence, but Nav3 blocked it whenever the Activity was opened from a Nav3 root, so the Activity's auto-instrumented spans went elsewhere. Withdrawing the lease only on resume (#6483) also left the covering Activity's onCreate/onStart work attributed to the previous root.

addresses: JAVA-785

💚 How did you test it?

  • Unit tests
  • Integration tests
  • Had my clanker thoroughly test the pre- and post-stack behavior via the Nav3 sample app i) to verify the stack really does what it claims to and ii) to ensure it doesn't introduce any regressions. (Did so on both pre-API 29 and post-API 29 devices.)

📝 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.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec.

🔮 Next steps

Last PR in the stack. Once the stack is merged into the collection branch, squash-merge #6480 into main.

⚠️ Merge this PR using a merge commit (not squash). Only the collection branch is squash-merged into main.

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 688b4a7

@sentry

sentry Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.63.0 (1) release

⚙️ sentry-android Build Distribution Settings

@0xadam-brown 0xadam-brown changed the title feat(android): [Nav3 Lifecycle 4] Prefer an Activity's ui.load over Nav3 transactions feat(android): [Part 4] Prefer Activity ui.load transactions over Nav3 transactions Nov 6, 2026
@0xadam-brown
0xadam-brown marked this pull request as ready for review November 6, 2026 12:32
@0xadam-brown
0xadam-brown requested review from a team and adinauer as code owners November 6, 2026 12:32
@0xadam-brown
0xadam-brown requested review from markushi and runningcode and removed request for a team November 6, 2026 12:32
@0xadam-brown 0xadam-brown added the sanity-check PR needs a lightweight review for obvious issues label Nov 6, 2026
@0xadam-brown
0xadam-brown force-pushed the fix/nav3-lifecycle-3-lease-withdrawal branch from 274e94e to 6460391 Compare November 11, 2026 11:21
@0xadam-brown
0xadam-brown force-pushed the feat/nav3-lifecycle-4-ui-load-preference branch from e8905ea to 9ea9424 Compare November 11, 2026 11:21
…nsactions

The Nav3 lease is now also withdrawn when an Activity is created over the leaseholder, not only when it resumes. On API 29+, creation is reported from onActivityPreCreated, before ActivityLifecycleIntegration binds the new Activity's ui.load transaction, so the leaseholder's nav transaction no longer holds the scope's transaction slot and ui.load binds, as with Nav2. Errors and spans from the new Activity's onCreate and onStart are no longer attributed to the leaseholder on any API level.

Because ui.load now binds, returning quickly to a nav root while the closed Activity's ui.load is still running would leave the nav root's transaction blocked. The coordinator therefore retries binding the leaseholder's nav transaction once when another Activity is destroyed, within 10s of the blocked attempt. Below API 29, there's no onActivityPreCreated, so the new Activity's ui.load stays unbound as before; this is documented in the package README.
Hold a candidate's host Activity weakly so a candidate that fails to
unregister can't keep a destroyed Activity alive, replace the resumedOnly
flag with a minimumState parameter, and tidy the README footnotes.
@0xadam-brown
0xadam-brown force-pushed the fix/nav3-lifecycle-3-lease-withdrawal branch from 6460391 to 3b26f99 Compare November 11, 2026 12:17
@0xadam-brown
0xadam-brown force-pushed the feat/nav3-lifecycle-4-ui-load-preference branch from 9ea9424 to bb73582 Compare November 11, 2026 12:17
The window for retrying a nav transaction blocked by an Activity's
ui.load transaction was measured by subtracting SentryDates from
options.dateProvider. That clock is replaceable, falls back to wall-clock
time unless both dates carry a nanoTime, and doesn't count while an
Android device suspends.

Record an io.sentry.time.Deadline when the publication is blocked
instead, on the ticker from options.getMonotonicTicker(), which
StagedNavTransaction now takes as a constructor parameter so tests can
drive it with TestMonotonicTicker. A retry exactly at the end of the
10-second window no longer qualifies.
@0xadam-brown
0xadam-brown force-pushed the feat/nav3-lifecycle-4-ui-load-preference branch from bb73582 to 688b4a7 Compare November 11, 2026 12:28

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 688b4a7. Configure here.

stagedNavTransaction.publishTo(delegate)
// Don't retry b/c ui.load retry is only for returns to a nav root, not for navigation
// within it.
stagedNavTransaction.publishTo(delegate, canRetry = false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Return navigation cancels ui.load retry

Medium Severity

Returning from a covering Activity arms a one-shot ui.load retry, but a same-lease navigation before that Activity is destroyed cancels it. finishNavTransaction clears blockedPublication, and startNavTransaction then publishes with canRetry = false, so the new destination never re-arms. A Compose LaunchedEffect that navigates after resume (typical for activity results in Nav3) leaves that destination with no bound nav transaction until a later navigation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 688b4a7. Configure here.

@runningcode runningcode left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good with 1 comment for you to look at!


/**
* After [activity] is destroyed, asks the leaseholder to retry publication if an Activity's
* `ui.load` blocked it. The publisher decides whether the blocker has left and retry is allowed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

so my agent tells me that this retry isn't completely necessary.
for example, if you go to Activity B from nav screen A and press back quickly enough, the retry blocks A when it resumes

my clanker says it would simplify the code the remove the retry. ill let you decide!

This branch has not been deployed

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

Labels

sanity-check PR needs a lightweight review for obvious issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants