Skip to content

fix(android): Nav3 multi-Activity support now plays nicely with nav roots that don't have a SentryNavEffect - #6480

Open
0xadam-brown wants to merge 1 commit into
mainfrom
feat/nav3-lifecycle
Open

0xadam-brown wants to merge 1 commit into
mainfrom
feat/nav3-lifecycle

Conversation

@0xadam-brown

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

Copy link
Copy Markdown
Member

PR Stack (Nav3 Lifecycle)


📜 Description

Part 3 of our Nav3 support for multi-Activity and multi-Fragment apps. In part 1 we provided the basic mechanism for coordinating multiple SentryNavEffects; and in part 2 we adapted our approach to the quirks of ViewPager2 and other oddball Fragment lifecycles.

Both of those PR stacks assumed that any Activities or Fragments that had a SentryNavEffect would only launch other Activities or Fragments that also had SentryNavEffects. This PR stack handles situations where Activities or Fragments with SentryNavEffects interact with Activities or Fragments without them.

Background.

A nav root is an Activity or Fragment that owns a nav back stack and passes it to a SentryNavEffect. (An Activity or Fragment that doesn't have a SentryNavEffect is called a non-candidate root.) When the user navigates to a new destination, SentryNavEffect records the screen name, the nav context and one nav transaction.

Each nav root has its own SentryNavEffect instance and all instances write to the same IScope with a single transaction slot. Auto-instrumented spans (HTTP, database, and so on) attach to whatever transaction is in that slot. With multi-Activity or multi-Fragment apps, several nav roots can be alive at the same time (e.g., an Activity covered by another Activity, or neighboring ViewPager2 pages), meaning SentryNavEffect is subject to the classical headaches of shared global state.

NavLeaseCoordinator was (and remains) our answer to those headaches. It prevents multiple, simultaneously active SentryNavEffects from clobbering each others' updates via a nav lease, viz., by only allowing the nav root holding the lease to write nav data to the IScope or bind to its transaction slot. When the lease is transferred, the old holder's data is removed and its transaction is unbound but left running.

The gaps in our existing implementation

Our original lease model left three gaps that could cause stale nav data to be left on the shared IScope as the user moved around the host app:

Prior (and wrong) assumption Problematic scenario Before After
A nav transaction only needs to start when the user navigates forward The user returns to a nav root after its nav transaction finished No transaction until the user navigates somewhere in the nav root's nav graph, meaning spans produced on return can be dropped A new nav transaction starts for the returned-to nav root if its original transaction finished while unbound (#6482)
All Activities or Fragments in an app are nav roots The user opens an Activity or ViewPager2 page without a SentryNavEffect The covered nav root keeps the lease. The new screen's errors pick up the old screen name and back stack, and its spans go to the old nav transaction The lease is withdrawn: the old data is removed from the scope, and the transaction is unbound but kept for the user's return (#6483)
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 (#6484)

† 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.

Unchanged

  • sentry-android-core isn't modified. #6484 works around ActivityLifecycleIntegration's callback timing instead.
  • Navigation never displaces a transaction that is already running in the shared IScope slot.
  • Bringing the app back to the foreground still starts no transaction (same as Nav2).

Known limitations

All limitations are documented in the package README.

[1] A ui.action from the gesture that caused a return can still block the nav transaction.

[2] We rely on the pausing of the nav root holding the lease in order to remove its nav data + transaction from the shared IScope in situations where a non-candidate root covers the leaseholder. If the nav root can be covered without being paused, we won't know about it and we won't remove its (arguably stale) data from the IScope. That happens with Fragment dialogs, Compose dialogs, and bottom sheets.

[3] Below API 29, an Activity opened from a nav root still doesn't get its ui.load transaction bound and we bind the nav transaction instead. The ui.load transaction is created and sent in both cases, but it's not bound to the transaction for pre-API 29 devices, and so it won't be the parent of any HTTP, SQLite or other ambient spans (the nav transaction will be instead).

Note: Updating pre-API 29 devices to match requires modifying ActivityLifecycleIntegration. I haven't included that change here because it would have impacts beyond Nav3. We could do it in a follow-up if folks care to. Nicely, it'd let us simplify the API 29+ approach implemented here, as well.

💡 Motivation and Context

Absent this PR stack, apps that mix Nav3 with plain Activities, ViewPager2 or other Activities could see stale nav data or spans attributed to the wrong screen. They could also lose spans produced when the user returned to a screen, and see ui.load behavior that differs from Nav2.

See the originating Slack convo here.

addresses: JAVA-823 and JAVA-785

💚 How did you test it?

See the individual PRs.

📝 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

#skip-changelog

Merge the stack PRs bottom to top using merge commits, then squash-merge this PR.

@0xadam-brown 0xadam-brown mentioned this pull request Nov 6, 2026
5 of 9 tasks
@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 feat(android): Nav3 multi-Activity support now plays nicely with Activities that don't have a SentryNavEffect Nov 6, 2026
@0xadam-brown 0xadam-brown changed the title feat(android): Nav3 multi-Activity support now plays nicely with Activities that don't have a SentryNavEffect fix(android): Nav3 multi-Activity support now plays nicely with Activities that don't have a SentryNavEffect Nov 6, 2026
@0xadam-brown 0xadam-brown changed the title fix(android): Nav3 multi-Activity support now plays nicely with Activities that don't have a SentryNavEffect fix(android): Nav3 multi-Activity support now plays nicely with nav roots that don't have a SentryNavEffect Nov 6, 2026
@0xadam-brown
0xadam-brown marked this pull request as ready for review November 6, 2026 12:31

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant