Skip to content

feat(core): Capture Expo Router ErrorBoundary errors#6318

Open
alwx wants to merge 5 commits into
mainfrom
feat/expo-router-error-boundary
Open

feat(core): Capture Expo Router ErrorBoundary errors#6318
alwx wants to merge 5 commits into
mainfrom
feat/expo-router-error-boundary

Conversation

@alwx

@alwx alwx commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • New feature

📜 Description

Adds Sentry.wrapRouterErrorBoundary — wraps Expo Router's per-route ErrorBoundary so render-phase errors that hit the fallback are captured with route context (route.name, route.path, route.params), the active navigation transaction is marked errored, and a breadcrumb is emitted. Concrete path/params are gated behind sendDefaultPii.

💡 Motivation and Context

Closes #6160.

💚 How did you test it?

New unit tests in expoRouterErrorBoundary.test.tsx. Full suite green locally.

📝 Checklist

  • I added tests to verify 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.
  • All tests passing
  • No breaking changes

🔮 Next steps

Investigate Metro/Babel auto-instrumentation of export { ErrorBoundary } from 'expo-router' re-exports as a follow-up.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(core): Capture Expo Router ErrorBoundary errors by alwx in #6318
  • chore(core): Bump react-native and metro devDependencies to 0.86.0 by antonis in #6316
  • chore: Bump macOS sample to react-native-macos 0.81.7 by antonis in #6315
  • chore(e2e): Bump react-native devDependency to 0.86.0 by antonis in #6313
  • fix(core): Forward user geo as an object so the native scope keeps it by antonis in #6309
  • fix(ios): Remove manual geo handling, use sentry-cocoa native support by antonis in #6289
  • fix(deps): bump js-yaml from ^4.1.1 to ^4.2.0 by antonis in #6298
  • fix: update React Native repo URL to new GitHub org by antonis in #6290
  • chore(deps): update JavaScript SDK to v10.59.0 by github-actions in #6321
  • chore(deps): bump concurrent-ruby from 1.3.6 to 1.3.7 in /samples/react-native-macos by dependabot in #6327
  • chore(deps): bump undici from 6.24.1 to 6.27.0 by dependabot in #6328
  • chore(deps): bump actions/checkout from 6.0.3 to 7.0.0 by dependabot in #6324
  • fix(ios): [RN 0.87] remove unused React/RCTTextView.h import by cortinico in #6322
  • chore(deps): bump actions/setup-java from 5.2.0 to 5.3.0 by dependabot in #6326
  • chore(deps): bump ruby/setup-ruby from 1.313.0 to 1.314.0 by dependabot in #6325
  • chore(deps): update Android SDK to v8.44.1 by github-actions in #6323

🤖 This preview updates automatically when you update the PR.

@alwx alwx force-pushed the feat/expo-router-error-boundary branch from e252cc5 to f313648 Compare June 18, 2026 12:38
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor
Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 7368b03

@sentry-warden sentry-warden Bot 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.

Sentry capture fires during render phase, risking duplicate events or missed deduplication on remount

Calling reportRouterBoundaryError in the render body rather than in a useEffect means the capture can fire during an abandoned Concurrent Mode render (error reported but boundary never committed) and will re-fire after any genuine unmount+remount because useRef resets on remount — the ref-based dedup only prevents duplicates within the same component lifetime. Consider moving the logic to useEffect(() => { if (props.error && reportedErrorRef.current !== props.error) { reportedErrorRef.current = props.error; reportRouterBoundaryError(props.error); } }, [props.error]).

Evidence
  • expoRouterErrorBoundary.tsx lines 56-58: the guard and captureException call are in the function body, not inside any useEffect.
  • useRef is initialized to null on every fresh mount; when Expo Router or a parent unmounts and remounts the boundary (e.g., navigation stack pop+push with the same error prop), a new ref starts at null, bypassing the dedup check and emitting a second event for the same error.
  • In React 18 Concurrent Mode, renders that are interrupted and abandoned will still have executed lines 57-58, reporting to Sentry before the fallback UI ever commits.
  • The test suite (expoRouterErrorBoundary.test.tsx) only covers same-instance rerenders and does not exercise the unmount→remount path, so this scenario is untested.

Identified by Warden code-review

Comment thread packages/core/src/js/tracing/expoRouterErrorBoundary.tsx Outdated
@alwx alwx marked this pull request as ready for review June 18, 2026 13:10
@alwx alwx marked this pull request as draft June 18, 2026 13:10
@alwx

alwx commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

it's still a draft, don't review yet

Comment thread packages/core/src/js/tracing/expoRouterErrorBoundary.tsx
@divineniiquaye

Copy link
Copy Markdown

Really needed this, looking forward for it been merged

alwx added 2 commits June 23, 2026 14:39
Add `Sentry.wrapRouterErrorBoundary` to wrap Expo Router's per-route
`ErrorBoundary` so render-phase errors that hit the fallback are
captured with route context, the active navigation transaction is
marked errored, and a breadcrumb is emitted.

Closes #6160
Move the Sentry reporting side-effects out of the wrapped boundary's
render body and into a `useEffect` keyed on the error instance. Running
`captureException`, `addBreadcrumb`, and the navigation span status
mutation during render violated React's purity rule and could fire from
abandoned Concurrent Mode renders before the fallback ever committed,
while also leaving a window where a discarded render would set the dedup
ref and suppress the real report.

Replace the per-instance `useRef` dedup with a module-scoped `WeakSet`
of reported errors so an unmount → remount cycle with the same error
instance (e.g. parent caches the error and re-renders the boundary) no
longer produces duplicate events. `WeakSet` keeps the dedup table from
growing unbounded.

Wrap the reporting call in try/catch and log via `logger.error` so a
failure inside Sentry instrumentation can never abort the wrapped
boundary's render and break Expo Router's fallback UI.

Move the changelog entry from the already-released 8.15.0 section to
Unreleased and point the link at PR #6318 instead of the issue, per
DangerJS guidance.

Adds tests covering the unmount/remount dedup path and the
Sentry-throws-still-renders-fallback path.
@alwx alwx force-pushed the feat/expo-router-error-boundary branch from f313648 to a3d2be3 Compare June 23, 2026 12:44
@alwx alwx marked this pull request as ready for review June 23, 2026 12:45
`logger.error` is typed to accept a single string message; passing the
caught `unknown` value as a second argument failed `tsc` with TS2345.
Stringify the caught value into the message instead — matches the
existing `logger.error` usage convention elsewhere in the SDK.
Comment thread packages/core/src/js/tracing/expoRouterErrorBoundary.tsx
Adding the error to the `reportedErrors` WeakSet before
`reportRouterBoundaryError` ran meant a transient failure inside Sentry
instrumentation permanently suppressed capture for that error instance:
the try/catch swallowed the failure, the error stayed in the set, and
later renders/remounts of the boundary with the same error short-circuit
on the `has(error)` guard and never retry.

Move the `.add(error)` call inside the try block, after the capture
succeeds. A transient failure no longer poisons the dedup table — the
next render of the boundary gets another shot at reporting the error.

Adds a test `retries capture on the next render after a transient
reporting failure` to cover the regression.

@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 e3fccdd. Configure here.

Comment thread packages/core/src/js/tracing/expoRouterErrorBoundary.tsx
`markActiveNavigationSpanErrored` was reading the span origin from a
non-standard `root.attributes['sentry.origin']` shape, which is not the
public API exposed by Sentry spans in this SDK. The convention used
elsewhere (see `tracing/span.ts`) is `spanToJSON(span).origin`. In
production the cast-based lookup almost always returned `undefined`, so
the `startsWith('auto.navigation.')` guard short-circuited and open
navigation transactions were never tagged with `SPAN_STATUS_ERROR` —
defeating one of the three contracts the ErrorBoundary wrapper promises.

Switch to `spanToJSON(root).origin` and update the test mocks to expose
the origin through a mocked `spanToJSON` instead of a fabricated
`attributes` map, so the tests now exercise the real read path.
@alwx

alwx commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@antonis can be reviewed again

@getsentry getsentry deleted a comment from sentry-warden Bot Jun 23, 2026
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.

Surface Expo Router's ErrorBoundary and route-level errors

2 participants