Skip to content

Nav3: Decide whether returning to a Nav3 root should start a new nav transaction #6454

Open

Description

@0xadam-brown

Background

A "Nav3 root" is an Activity or Fragment (including a ViewPager2 page) that owns a nav back stack and calls SentryNavEffect. Today a new nav transaction is started under a Nav3 root in two cases:

  • the root's nav back stack is initialized (i.e., the root was just created, it initialized the nav back stack and passed it to SentryNavEffect), or
  • the root's nav back stack changed.

When the user comes back to a root whose nav transaction has already finished, no new nav transaction is started. This can happen when the user:

  1. swipes back to a ViewPager2 page the user already visited,
  2. goes back to an earlier Activity with its own SentryNavEffect, or
  3. dismisses an overlay Fragment or dialog that has its own SentryNavEffect.

...but it only happens if the earlier Activity or Fragment retains the same, already-initialized (and unchanged**) nav back stack. E.g., if the Activity or Fragment happens to be destroyed and recreated before the user goes back or dismisses the overlay, then a new transaction will be created due to nav back stack initialization. Whether that happens depends on OS memory and component management policies rather than anything the user does, and so Sentry's behavior can appear random.

Happily, errors continue to be attributed correctly regardless, as breadcrumbs, the screen name, and the nav context still update on every return. But what can be lost are the spans for work performed after the return, such as a data refresh when the screen resumes.

The current approach matches what we do for Nav2 and the Activity integration. Neither starts a nav transaction when the user returns to an existing root. This issue asks whether we should change that approach for Nav3?

** If the user starts navigating around the nav subgraph owned by the Nav3 root, then nav transactions will be started again and spans will be captured. The issue discussed here is limited to what happens when the user transitions between Nav3 roots without a corresponding change to the incoming root's nav back stack.

To decide

1. What does a nav transaction mean?

  • "The nav back stack changed": The current behavior, which matches what we do for Nav2. Returning to a Nav3 root isn't itself a navigation event unless the root initializes or updates its nav back stack. (In particular, any unfinished nav transaction started by the root being returned to will be reinstated; but if the transaction finished in the meantime, no new nav transaction is started.)
  • "The nav back stack changed OR the user returned to a Nav3 root": An alternative approach. Returning to a Nav3 root is deemed to be a navigation event and always generates a new nav transaction.

2. Should the new behavior be on or off by default?

The proposed rule: when a root gains the nav lease and has no running transaction, start one.

Opt-in (SentryNavOptions flag, default off) On by default
Existing users No change Behavior changes for setups that already ship multi-root support
Nav transaction counts on dashboards Unchanged Go up and mean something different
Matches Nav2 and the Activity integration Yes, by default No
Competes with user-interaction transactions for the scope Only when enabled Always
ViewPager2 behavior by default Still depends on Fragment retention Predictable

New features in this SDK are opt-in by default, which favours a flag. The default could be flipped later based on feedback.

3. Should return transactions be marked?

For example, add a navigation.trigger attribute with "return" on these transactions. Users could then filter or exclude them while still charting them with other nav transactions (same op and name).

No navigation.trigger attribute exists today, so this means either:

  • adding it to all nav transactions (e.g., push, pop, return), or
  • setting it only on return transactions.

Alternative considered

Start a transaction on return only for ViewPager2 (when the lease moves between roots in the same Activity). Rejected for now:

  • It needs each root to track its host Activity, which isn't implemented yet.
  • "Return" would mean different things in different setups, which is harder to explain and document.

Constraints

  • Land after fix(tracing): Stop idle spans from keeping idle transactions alive until their deadline (JAVA-795) #6396. Until then, idle nav transactions can hold the scope for up to 30 s. Return transactions would then block other transactions, and page changes in that window would lose theirs.
  • Expect empty transactions. A root's startup work usually happened long before the return, so many return transactions will have no spans. Empty idle transactions are dropped and don't use quota, but they only help screens that do work when they resume.

Implementation sketch

Clanker-generated 🤖

Opt-in and on-by-default cost about the same to build. Either way, the follow-up needs more than #6398's staging: StagedNavTransaction forgets a transaction once it finishes, so on a return there's no request left to stage again.

Needed regardless of approach

  • A trigger: detect that a root gained the nav lease and has no running transaction (NavLeaseCoordinator / NavScopes).
  • A new request: a transaction context, options, arguments, view names and the back stack for the root's current top. Today BackStackObserver builds these only when the back stack changes.
  • A start: through StagedNavTransaction, which already discards a start that's blocked by another bound transaction.
  • Supporting changes: rotate the propagation context, add the SentryNavOptions flag (public API), add the optional navigation.trigger marker, and add tests and docs.

Approach 1: Replay through BackStackObserver

On lease gain without a running transaction, the lifecycle package tells the root's observer, which starts a transaction for its current top without emitting a breadcrumb or changing the screen.

  • Pros: builds the request the same way as normal navigation, with current data (including a fresh app context). The marker fits naturally here.
  • Cons: needs a callback from the lifecycle package back to the observer. The observer has to keep its last back stack data, and the change touches both layers.

Approach 2: Remember the last request in StagedNavTransaction

Keep the last context, options and recorded data (arguments, app, Navigation context) after the transaction finishes. On lease gain without a running transaction, stage a copy and publish it.

  • Pros: stays inside the lifecycle package, which is in line with its encapsulation policy, and the change is small.
  • Cons: replays data captured at the original navigation, so the app context could be minutes old. Each copy needs a fresh TransactionContext (new trace and sampling decision), and the transaction handle has to be able to copy itself.

Approach 3: A coordinator hook, with the observer building the request

A variant of approach 1: NavLeaseCoordinator gains a "lease gained, no running transaction" event on LeasedNavState. NavScopes forwards it to a callback that SentryNavEffect wires to the observer.

  • Pros: the decision about when to start stays in the coordinator, and building the request stays in the observer. The responsibilities are the clearest of the three.
  • Cons: the most new plumbing.

Approach 1 or 3 is preferred, since both build the request from current data.

Activity

  1. linear-code commented on Nov 3, 2026

    @linear-code
  2. changed the title [-]Nav3: Decide whether returning to a Navigation 3 root should start a nav transaction[/-] [+]Nav3: Decide whether returning to a Nav3 root should start a nav transaction[/+] on Nov 3, 2026
  3. changed the title [-]Nav3: Decide whether returning to a Nav3 root should start a nav transaction[/-] [+]Nav3: Decide whether returning to a Nav3 root should start a new nav transaction[/+] on Nov 3, 2026
  4. self-assigned this
    on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions