Skip to content

Merge remote-tracking branch 'origin/main' into alwx/feature/wrap-expโ€ฆ

81dc094
Select commit
Loading
Failed to load commit list.
Merged

feat(tracing): Wrap Expo Router push, replace, navigate, back, dismiss in addition to prefetch #6221

Merge remote-tracking branch 'origin/main' into alwx/feature/wrap-expโ€ฆ
81dc094
Select commit
Loading
Failed to load commit list.
@sentry/warden / warden completed Jun 3, 2026 in 19m 34s

5 issues

Low

Pending Expo Router navigation consumed and applied to app-restart span - `packages/core/src/js/tracing/reactnavigation.ts:358`

When Android triggers an Activity restart (isAppRestart = true), consumePendingExpoRouterNavigation() is called unconditionally, and because the downstream if (pendingExpoRouter && latestNavigationSpan) guard has no !isAppRestart check, a restart span can be incorrectly attributed navigation.method from an unrelated prior Expo Router call.

Test 'returns the router unchanged if prefetch method does not exist' no longer matches behavior after push is wrapped - `packages/core/test/tracing/expoRouter.test.ts:51-54`

The test at line 51 passes { push: jest.fn() } and asserts expect(wrapped).toBe(router), with a name implying the router is left untouched when prefetch is absent. After this PR, wrapExpoRouter unconditionally wraps push (as well as replace, navigate, back, dismiss) in place, so router.push IS now a different function reference than what was passed in. The assertion still passes only because wrapExpoRouter mutates and returns the same object โ€” reference equality is preserved while the contract implied by the test name (push unchanged) is not. Consider renaming the test and asserting on the method reference (e.g. expect(wrapped.push).toBe(originalPush) โ€” which will fail and reveal the new behavior), or replace this case with one that uses a router lacking any known method (similar to the existing 'returns the router unchanged if no known methods exist' test).

dismiss test omits pending navigation assertion, leaving the hand-off feature untested for that method - `packages/core/test/tracing/expoRouter.test.ts:440`

The dismiss test verifies the span and breadcrumb but never asserts consumePendingExpoRouterNavigation(), so the key setPendingExpoRouterNavigation hand-off (the main new mechanism described in the PR) is not verified for dismiss, unlike the equivalent back test which does assert it.

Stale pending navigation if `startInactiveSpan` throws inside `wrapNavigationMethod` - `packages/core/src/js/tracing/expoRouter.ts:80-91`

In wrapNavigationMethod (line 170โ€“191 of the file), setPendingExpoRouterNavigation is called before startInactiveSpan, but only the original.apply call is inside the try/catch that invokes clearPendingExpoRouterNavigation on error. If startInactiveSpan throws, the pending is set and never cleared, so the next unrelated idle navigation span will be incorrectly tagged with a stale navigation.method.

Also found at:

  • packages/core/test/tracing/expoRouter.test.ts:431-438
  • packages/core/test/tracing/expoRouter.test.ts:13-16
Stale `pendingExpoRouterNavigation` when navigation silently does nothing (e.g. `back()` on empty stack) - `packages/core/src/js/tracing/expoRouter.ts:170-186`

If back() or dismiss() returns normally without throwing but does NOT trigger a React Navigation __unsafe_action__ dispatch (e.g. back() called on an empty navigation stack), the pending value set by setPendingExpoRouterNavigation is never consumed โ€” only the catch path calls clearPendingExpoRouterNavigation(). A subsequent unrelated React Navigation dispatch (e.g. a deep-link or programmatic navigation outside the wrapped API) will then consume the stale pending and tag its idle navigation span with the wrong navigation.method.

4 skills analyzed
Skill Findings Duration Cost
security-review 0 1m 32s $0.32
code-review 3 7m 24s $2.44
find-bugs 2 18m 55s $4.45
gha-security-review 0 7m 45s $0.13

โฑ 35m 36s ยท 2.9M in / 237.2k out ยท $7.33