feat(tracing): Wrap Expo Router push, replace, navigate, back, dismiss in addition to prefetch#6221
feat(tracing): Wrap Expo Router push, replace, navigate, back, dismiss in addition to prefetch#6221alwx wants to merge 9 commits into
push, replace, navigate, back, dismiss in addition to prefetch#6221Conversation
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
|
push, replace, navigate, back, dismiss in addition to prefetchpush, replace, navigate, back, dismiss in addition to prefetch
…nd sendDefaultPii
Expo Router `push`/`replace`/`navigate`/`back`/`dismiss` (and `prefetch`)
wrappers previously emitted the raw `href` and route `params` on both
the navigation breadcrumb and span attributes regardless of the client
options. Dynamic segments like `/users/123` and parameter values such
as `{ id: '123' }` can carry user identifiers and other PII, so the
existing `reactnavigation.ts` integration already gates them behind
`sendDefaultPii`. The new Expo Router wrappers should follow the same
contract.
This change:
- Reads `sendDefaultPii` from the client options and only attaches
`href` (raw string or stringified object form) and `params` to the
breadcrumb `data` and span attributes when it is enabled.
- Keeps the non-PII fields — `method`, `pathname` (route template),
and `route.name` — unconditional so navigations remain visible and
groupable in Sentry without leaking user data.
- Applies the same guard to the `prefetch` span's `route.href`
attribute, which was newly added in this PR.
- Adds dedicated `sendDefaultPii` test cases and updates the existing
default-off assertions for all wrapped methods.
- Adds a changelog entry for #6221.
push, replace, navigate, back, dismiss in addition to prefetchpush, replace, navigate, back, dismiss in addition to prefetch
…nd sendDefaultPii
Expo Router `push`/`replace`/`navigate`/`back`/`dismiss` (and `prefetch`)
wrappers previously emitted the raw `href` and route `params` on both
the navigation breadcrumb and span attributes regardless of the client
options. Dynamic segments like `/users/123` and parameter values such
as `{ id: '123' }` can carry user identifiers and other PII, so the
existing `reactnavigation.ts` integration already gates them behind
`sendDefaultPii`. The new Expo Router wrappers should follow the same
contract.
This change:
- Reads `sendDefaultPii` from the client options and only attaches
`href` (raw string or stringified object form) and `params` to the
breadcrumb `data` and span attributes when it is enabled.
- Keeps the non-PII fields — `method`, `pathname` (route template),
and `route.name` — unconditional so navigations remain visible and
groupable in Sentry without leaking user data.
- Applies the same guard to the `prefetch` span's `route.href`
attribute, which was newly added in this PR.
- Adds dedicated `sendDefaultPii` test cases and updates the existing
default-off assertions for all wrapped methods.
- Adds a changelog entry for #6221.
…igationIntegration The Expo Router wrapper sets a pending navigation value that the React Navigation idle-span listener is supposed to consume and apply to the resulting transaction as the `navigation.method` attribute. Previously only the setter side (in expoRouter.test.ts) was tested; the consumer side had no coverage, so a regression in reactnavigation.ts would have gone undetected. Add three integration-level tests against the live reactNavigationIntegration: - the next navigation transaction is tagged with `navigation.method` matching the pending Expo Router call - the pending value is consumed exactly once and does not leak into the following navigation - no `navigation.method` attribute is set when nothing is pending Surfaced by Warden review (4UA-DHD).
b5d287a to
febd51c
Compare
…an listener short-circuits `startIdleNavigationSpan` has several early-return paths (dispatched noops, PRELOAD with prefetch tracking, SET_PARAMS, drawer actions, and `useDispatchedActionData` calls without a route name in the payload). Previously `consumePendingExpoRouterNavigation()` lived after those returns, so a wrapped Expo Router call that ended up taking one of the short-circuit paths left its pending value in the module-level slot. The next, unrelated navigation then incorrectly inherited the wrong `navigation.method` attribute. Move the `consumePendingExpoRouterNavigation()` call to the very top of the listener so the value is always drained on the listener invocation triggered by the wrapper. Apply it to the span only if we actually create one. Add a regression test covering the dispatched-action-without-payload early-return path; the test fails on the previous code and passes with the fix. Surfaced by sentry-bot and cursor-bot reviews.
Per the Sentry trace-origin spec (https://develop.sentry.dev/sdk/telemetry/traces/trace-origin/), the `sentry.origin` value uses the format `type.category.integration-name`, where `category` is a span operation category such as `navigation`. The new origin for wrapped Expo Router push/replace/navigate/back/dismiss calls was `auto.expo_router.navigation`, which swaps the category and integration name. Align it with the existing `auto.navigation.react_navigation` and `auto.navigation.react_native_navigation` constants so server-side filtering and downstream tools recognize these as navigation spans. The pre-existing `auto.expo_router.prefetch` origin has the same inverted ordering but is left unchanged here \u2014 it is already shipped and changing it would break consumers that filter by it. Surfaced by cursor-bot review.
|
It might look scary but it's mostly tests. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 27cfa37. Configure here.
The PR also tightened PII handling for the pre-existing `prefetch` instrumentation: when `sendDefaultPii` is off (the default), `route.href` is dropped and concrete pathnames derived from string hrefs (e.g. `/users/42`) are replaced with `'unknown'` for `route.name`. Templated object hrefs are unaffected. Document this so users with dashboards or alerts filtering on prefetch `route.name`/`route.href` notice the change. Surfaced by cursor-bot review.
| }, | ||
| }); | ||
|
|
||
| setPendingExpoRouterNavigation({ |
There was a problem hiding this comment.
nit: The href, pathname, and params seem to be unused
antonis
left a comment
There was a problem hiding this comment.
LGTM 🚀
Left a nit comment and added the ready-to-merge to run the full test suite.
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| b0d3373+dirty | 3831.75 ms | 1227.29 ms | -2604.46 ms |
| ef27341+dirty | 3856.13 ms | 1231.42 ms | -2624.71 ms |
| 23598c3+dirty | 1207.00 ms | 1209.90 ms | 2.90 ms |
| 3ce5254+dirty | 1219.93 ms | 1221.90 ms | 1.96 ms |
| 04207c4+dirty | 1191.27 ms | 1189.78 ms | -1.48 ms |
| 5748023+dirty | 3840.49 ms | 1227.43 ms | -2613.05 ms |
| 9210ae6+dirty | 3815.93 ms | 1214.14 ms | -2601.79 ms |
| 4e0ba9c+dirty | 3839.22 ms | 1221.06 ms | -2618.16 ms |
| 7ac3378+dirty | 1213.37 ms | 1218.15 ms | 4.78 ms |
| 0b1b5e3+dirty | 3823.96 ms | 1220.12 ms | -2603.84 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| b0d3373+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| ef27341+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 23598c3+dirty | 3.38 MiB | 4.80 MiB | 1.42 MiB |
| 3ce5254+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 04207c4+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 5748023+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 9210ae6+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 4e0ba9c+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
| 7ac3378+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 0b1b5e3+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
📲 Install BuildsAndroid
|
iOS (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| b0d3373+dirty | 3842.49 ms | 1218.49 ms | -2624.00 ms |
| ef27341+dirty | 3835.20 ms | 1212.23 ms | -2622.97 ms |
| 23598c3+dirty | 1223.59 ms | 1229.13 ms | 5.53 ms |
| 3ce5254+dirty | 1217.70 ms | 1224.69 ms | 6.99 ms |
| 04207c4+dirty | 1228.55 ms | 1226.04 ms | -2.51 ms |
| 5748023+dirty | 3844.74 ms | 1225.49 ms | -2619.26 ms |
| 9210ae6+dirty | 3834.11 ms | 1216.64 ms | -2617.47 ms |
| 4e0ba9c+dirty | 3856.39 ms | 1234.44 ms | -2621.95 ms |
| 7ac3378+dirty | 1202.35 ms | 1198.31 ms | -4.04 ms |
| 0b1b5e3+dirty | 3820.72 ms | 1207.94 ms | -2612.78 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| b0d3373+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| ef27341+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 23598c3+dirty | 3.38 MiB | 4.80 MiB | 1.42 MiB |
| 3ce5254+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 04207c4+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 5748023+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 9210ae6+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 4e0ba9c+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
| 7ac3378+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 0b1b5e3+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |
Android (new) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ad66da3+dirty | 411.49 ms | 449.38 ms | 37.89 ms |
| bc0d8cf+dirty | 407.66 ms | 461.35 ms | 53.69 ms |
| a736b76+dirty | 405.78 ms | 458.74 ms | 52.96 ms |
| a0d8cf8+dirty | 533.71 ms | 564.25 ms | 30.54 ms |
| 41d6254+dirty | 406.20 ms | 445.52 ms | 39.32 ms |
| 7d8c8bd+dirty | 406.06 ms | 460.88 ms | 54.81 ms |
| c823bb5+dirty | 468.26 ms | 516.16 ms | 47.90 ms |
| 0b1b5e3+dirty | 425.58 ms | 476.02 ms | 50.44 ms |
| 4953e94+dirty | 398.80 ms | 431.81 ms | 33.01 ms |
| d2eadf8+dirty | 468.02 ms | 530.37 ms | 62.35 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ad66da3+dirty | 48.30 MiB | 53.49 MiB | 5.19 MiB |
| bc0d8cf+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| a736b76+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |
| a0d8cf8+dirty | 48.30 MiB | 53.49 MiB | 5.19 MiB |
| 41d6254+dirty | 48.30 MiB | 53.60 MiB | 5.30 MiB |
| 7d8c8bd+dirty | 48.30 MiB | 53.54 MiB | 5.23 MiB |
| c823bb5+dirty | 48.30 MiB | 53.58 MiB | 5.28 MiB |
| 0b1b5e3+dirty | 48.30 MiB | 53.60 MiB | 5.29 MiB |
| 4953e94+dirty | 43.94 MiB | 48.94 MiB | 5.00 MiB |
| d2eadf8+dirty | 48.30 MiB | 53.48 MiB | 5.18 MiB |

📢 Type of change
📜 Description
Extends the Expo Router instrumentation beyond
prefetchso that programmatic navigation calls produce breadcrumbs and spans, and the resulting idle navigation transaction can be attributed back to the call that triggered it.What changed:
wrapExpoRouternow wrapspush,replace,navigate,back, anddismissin addition toprefetch.navigationbreadcrumb (category: 'navigation',data.method,data.pathname),navigation.<method>span tagged withsentry.origin = auto.navigation.expo_router,navigation.method, androute.name,pendingExpoRouterNavigation) that the next React Navigation idle navigation span consumes to set its ownnavigation.methodattribute, attributing the transaction back to the originating call.__sentryWrappedflag guards against double-wrapping ifwrapExpoRouteris called multiple times on the same router.reactnavigation.ts: rawhrefand routeparams(which can encode user identifiers like/users/123or{ id: '123' }) are only attached to breadcrumbs and span attributes whensendDefaultPiiis enabled.method,pathname(the route template), androute.nameremain unconditional so navigations stay visible and groupable.💡 Motivation and Context
Fixes #6158.
Previously only
prefetchwas instrumented, so any programmaticrouter.push(...)/router.replace(...)/router.back()etc. was invisible in Sentry — no breadcrumb trail, no span, and the resulting React Navigation idle transaction had no way to know which call site initiated it. This PR closes that gap while keeping the existingprefetchbehavior intact and aligning PII handling with the rest of the navigation tracing.💚 How did you test it?
packages/core/test/tracing/expoRouter.test.tscovering:push/replace/navigateviadescribe.each— span and breadcrumb shape with string and object hrefs, error reporting, and the pending-navigation hand-off,back()anddismiss(count)with no/optional args,href, noparams) and dedicatedsendDefaultPii: truecases that verifyhrefandparamsreappear on both the breadcrumb and the span.yarn jest— 113 suites / 1481 tests passing.yarn lint:lernaclean.yarn api-report:checkup to date.📝 Checklist
sendDefaultPiiis enabled🔮 Next steps