Skip to content

Expo Router ErrorBoundary auto wrapped#6347

Merged
alwx merged 8 commits into
mainfrom
feat/auto-wrap-expo-router-error-boundary
Jun 26, 2026
Merged

Expo Router ErrorBoundary auto wrapped#6347
alwx merged 8 commits into
mainfrom
feat/auto-wrap-expo-router-error-boundary

Conversation

@alwx

@alwx alwx commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Adds a Babel plugin (sentryExpoRouterAutoWrapBabelPlugin) that auto-wraps Expo Router's per-route ErrorBoundary re-exports at build time, removing the manual call-site step introduced in #6318.

The plugin detects this in user source files:

  export { ErrorBoundary } from 'expo-router';

and rewrites it to:

  import { ErrorBoundary as __sentryOriginalExpoErrorBoundary } from 'expo-router';
  import { wrapExpoRouterErrorBoundary as __sentryWrapExpoRouterErrorBoundary } from '@sentry/react-native';
  export const ErrorBoundary = __sentryWrapExpoRouterErrorBoundary(__sentryOriginalExpoErrorBoundary);

So users get the full ErrorBoundary instrumentation (route context, navigation-span error tagging, breadcrumb) with zero code changes in their route files.

💡 Motivation and Context

#6318 shipped Sentry.wrapExpoRouterErrorBoundary but required users to edit every route file that re-exports ErrorBoundary from expo-router. That edit is trivial & easy to forget on new routes.

💚 How did you test it?

📝 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

@alwx alwx self-assigned this Jun 25, 2026
@alwx alwx changed the title Feat/auto wrap expo router error boundary Expo Router ErrorBoundary auto wrapped Jun 25, 2026
@github-actions

github-actions Bot commented Jun 25, 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).


  • Expo Router ErrorBoundary auto wrapped by alwx in #6347
  • chore(deps): update Android SDK to v8.46.0 by github-actions in #6357
  • chore(ci): Move testflight and size-analysis iOS jobs to GitHub Actions macos-26 by itaybre in #6355
  • feat(core): Use native btoa for envelope base64 encoding by alwx in #6351

🤖 This preview updates automatically when you update the PR.

@alwx alwx force-pushed the feat/auto-wrap-expo-router-error-boundary branch from 5aff82d to 8e4f285 Compare June 25, 2026 09:36
Comment thread packages/core/src/js/tools/metroconfig.ts
Comment thread packages/core/src/js/tools/metroconfig.ts Outdated
Comment thread packages/core/src/js/tools/sentryBabelTransformerUtils.ts
Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts Outdated
Comment thread CHANGELOG.md Outdated

@antonis antonis 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.

Left a few comments but overall looks good 🚀

@alwx alwx force-pushed the feat/auto-wrap-expo-router-error-boundary branch from 807a555 to 7eab5e6 Compare June 26, 2026 08:06
Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts Outdated
Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts
Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts
Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts

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

Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts
Comment thread packages/core/src/js/tools/sentryExpoRouterAutoWrapBabelPlugin.ts Outdated
@alwx alwx requested a review from antonis June 26, 2026 11:21
alwx added 8 commits June 26, 2026 13:52
…-wrap is enabled

Before this fix, enabling `autoWrapExpoRouterErrorBoundary` (including
the default-on path for `getSentryExpoConfig`) silently activated the
`@sentry/babel-plugin-component-annotate` plugin for every non-`node_modules`
file, even though the user never opted in to component annotation.

Root cause: `withSentryBabelTransformer` used to be reachable only when
`annotateReactComponents` was truthy, so `addSentryComponentAnnotatePlugin`
inside the transformer was effectively gated by that. Adding the
auto-wrap path opened a second route that bypassed that gate.

Two changes:

1. Persist an explicit `annotateReactComponents` marker in the
   transformer options whenever the user requested it (using `{}` for
   the boolean-`true` case), so the opt-in signal actually survives the
   env-variable handoff to the Babel transformer worker.

2. Guard the call site in `createSentryBabelTransformer` so the
   annotate plugin is only added when the persisted options carry an
   `annotateReactComponents` key. No opt-in, no plugin.

The 3 existing transformer tests that codified the latent bug (plugin
added when no options were persisted) are updated to set the opt-in
explicitly. Adds a regression test asserting that enabling only
`autoWrapExpoRouterErrorBoundary` does not pull in the annotate plugin.
- Switch `autoWrapExpoRouterErrorBoundary` to default `false` for both
  `getSentryExpoConfig` and `withSentryConfig`. Defaulting on was a
  blast-radius concern \u2014 every existing `getSentryExpoConfig` user would
  pick up the Babel pass silently on upgrade. Opt-in is consistent with
  the existing `annotateReactComponents` precedent and lets us flip the
  default on later once the transform has shipped without incident.

- Drop the unused `/* eslint-disable @typescript-eslint/no-explicit-any */`
  directive at the top of `sentryExpoRouterAutoWrapBabelPlugin.ts`. The
  plugin no longer uses `any`.

- Reformat the CHANGELOG entry to match the project style: short headline
  with a small usage code block, and a Fixes entry covering the Babel
  transformer gating change (PR-referenced for the Danger check).

- Wire the Expo sample to exercise the auto-wrap path through the
  explicit opt-in: `autoWrapExpoRouterErrorBoundary: true` in
  `samples/expo/metro.config.js`, route `_layout.tsx` back to the bare
  `export { ErrorBoundary } from 'expo-router'` re-export so the
  build-time transform is actually exercised in CI.
The earlier program-wide `hasMarkerImport` idempotency check ran before
inspecting the current node's specifiers, so after the first
`export { ErrorBoundary } from 'expo-router'` was rewritten and the
`@sentry/react-native` marker import was injected, any subsequent
`ErrorBoundary` re-export in the same file was silently skipped and
left unwrapped \u2014 Sentry would miss errors on those routes.

The marker check was redundant anyway: after a successful rewrite, the
re-export is no longer `export ... from 'expo-router'`, so a second
pass over the same code structurally finds nothing to transform. Remove
the program-wide check and the helper.

Adds a regression test (`wraps every ErrorBoundary re-export when
several appear in the same file`) and keeps the existing idempotency
test, which still passes by structural idempotency.
After the previous fix to wrap every `ErrorBoundary` re-export, each
rewrite re-emitted the helper imports. With two or more
`export { ErrorBoundary } from 'expo-router'` declarations in the same
file, this produced duplicate ES module import bindings \u2014 a syntax
error that Metro can reject before the bundle is built.

Track helper injection per file via `PluginPass.set/get`. The first
wrap emits both helper imports and sets a flag; subsequent wraps in the
same file reuse the existing bindings and emit only the
`export const ... = __sentryWrap(...)` statement.

Tightens the multi-export regression test to assert each helper import
appears exactly once.
The plugin was using `path.replaceWithMultiple` at the original
export's position, which placed the two helper `import` declarations
wherever the `export { ErrorBoundary } from 'expo-router'` happened to
live in the source file. If anything precedes that export (other
imports, top-level constants, etc.), the helper imports end up
mid-file. Babel parses that, but strict ESM environments \u2014 notably
Hermes \u2014 may reject mid-file `import` statements, breaking the bundle.

Use `path.scope.getProgramParent().path.unshiftContainer('body', ...)`
to push the helper imports to the top of the Program body, alongside
the file's existing imports. The wrap statement still replaces the
original export in place. Per-file dedup via `PluginPass.set/get` is
preserved so a second wrap reuses the existing bindings.

Adds a regression test that constructs a file with leading imports +
a top-level statement before the boundary re-export and asserts both
helper imports land above any non-import statement.
…g clashes

If the user's file already has an `ErrorBoundary` top-level binding
(e.g. `import { ErrorBoundary } from 'expo-router'` used locally),
emitting `export const ErrorBoundary = __sentryWrap(...)` produces a
duplicate top-level binding and breaks the Metro/Babel build.

Generate a unique local via `path.scope.generateUidIdentifier` for the
wrapped value and export it under the original name through an
`export { uid as ErrorBoundary }` specifier instead of a const-with-name
declaration. The exported name Expo Router consumes is unchanged; only
the internal binding is now collision-free.

Adds a regression test `does not clash with an existing local ErrorBoundary
binding in the same file` and updates the other tests to match the new
emission shape (`const _wrappedErrorBoundary = ...; export { ... as ErrorBoundary };`).
…ation

Two follow-ups from PR review:

1. `remainingSpecifiers` were reused directly when building the trailing
   `export { ... } from 'expo-router'` declaration on mixed re-exports.
   Babel's docs warn that reusing AST nodes across positions can corrupt
   the scope cache. Map through `t.cloneNode` so each new declaration
   owns its own specifier nodes.

2. Adds an explicit regression test for files that start with a
   `'use client'` directive. Babel's parser puts the directive on
   `Program.directives` (separate from `body`), so the existing
   `unshiftContainer('body', ...)` already preserves it \u2014 but the
   contract is worth pinning down in a test so we notice if a future
   refactor breaks it.
@alwx alwx force-pushed the feat/auto-wrap-expo-router-error-boundary branch from f0371ed to aa389da Compare June 26, 2026 11:53
@alwx

alwx commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@antonis I've rebased and checked the Sentry Warden / Cursor / whatever-else comments. Please, check again! :)

@antonis antonis added the ready-to-merge Triggers the full CI test suite label Jun 26, 2026
@sentry

sentry Bot commented Jun 26, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.16.0 (94) Release

⚙️ sentry-react-native Build Distribution Settings

@antonis antonis 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.

LGTM 🚀
Let's wait for the CI to become 🟢 to be on the safe side 🙇

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3854.81 ms 1225.81 ms -2629.00 ms
Size 4.98 MiB 6.51 MiB 1.53 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
6176a94+dirty 3836.50 ms 1217.64 ms -2618.86 ms
44c8b3f+dirty 3823.85 ms 1207.66 ms -2616.19 ms
64630e5+dirty 3842.70 ms 1218.11 ms -2624.60 ms
5257d80+dirty 3854.39 ms 1234.28 ms -2620.11 ms
3b6e9f9+dirty 3851.90 ms 1233.33 ms -2618.57 ms
88735e9+dirty 3837.02 ms 1214.40 ms -2622.62 ms
37a2091+dirty 3821.77 ms 1212.34 ms -2609.43 ms
5a010b7+dirty 3838.85 ms 1214.98 ms -2623.87 ms
68672fc+dirty 3841.58 ms 1228.89 ms -2612.69 ms
f3215d3+dirty 3842.73 ms 1219.33 ms -2623.40 ms

App size

Revision Plain With Sentry Diff
6176a94+dirty 5.15 MiB 6.68 MiB 1.53 MiB
44c8b3f+dirty 5.15 MiB 6.66 MiB 1.51 MiB
64630e5+dirty 4.98 MiB 6.46 MiB 1.49 MiB
5257d80+dirty 5.15 MiB 6.69 MiB 1.54 MiB
3b6e9f9+dirty 5.15 MiB 6.68 MiB 1.53 MiB
88735e9+dirty 4.98 MiB 6.46 MiB 1.49 MiB
37a2091+dirty 5.15 MiB 6.70 MiB 1.54 MiB
5a010b7+dirty 5.15 MiB 6.69 MiB 1.54 MiB
68672fc+dirty 5.15 MiB 6.71 MiB 1.55 MiB
f3215d3+dirty 5.15 MiB 6.67 MiB 1.52 MiB

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3851.85 ms 1216.15 ms -2635.71 ms
Size 4.98 MiB 6.50 MiB 1.52 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
0b1b5e3+dirty 3820.72 ms 1207.94 ms -2612.78 ms
5125c43+dirty 3827.94 ms 1208.79 ms -2619.15 ms
7887847+dirty 3844.89 ms 1221.67 ms -2623.22 ms
acd838e+dirty 3835.94 ms 1215.87 ms -2620.07 ms
20fbd51+dirty 3832.52 ms 1206.13 ms -2626.39 ms
a5d243c+dirty 3827.92 ms 1220.10 ms -2607.81 ms
a0d8cf8+dirty 3826.15 ms 1213.12 ms -2613.03 ms
a3265b6+dirty 3844.26 ms 1235.60 ms -2608.66 ms
3817909+dirty 1210.76 ms 1215.64 ms 4.89 ms
b04af96+dirty 3830.54 ms 1206.11 ms -2624.44 ms

App size

Revision Plain With Sentry Diff
0b1b5e3+dirty 5.15 MiB 6.70 MiB 1.54 MiB
5125c43+dirty 5.15 MiB 6.68 MiB 1.53 MiB
7887847+dirty 4.98 MiB 6.46 MiB 1.48 MiB
acd838e+dirty 5.15 MiB 6.70 MiB 1.55 MiB
20fbd51+dirty 4.98 MiB 6.46 MiB 1.49 MiB
a5d243c+dirty 5.15 MiB 6.68 MiB 1.53 MiB
a0d8cf8+dirty 5.15 MiB 6.67 MiB 1.51 MiB
a3265b6+dirty 5.15 MiB 6.68 MiB 1.53 MiB
3817909+dirty 3.38 MiB 4.73 MiB 1.35 MiB
b04af96+dirty 4.98 MiB 6.54 MiB 1.56 MiB

@github-actions

Copy link
Copy Markdown
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 544.08 ms 580.80 ms 36.72 ms
Size 49.74 MiB 54.85 MiB 5.11 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
5257d80+dirty 423.37 ms 467.54 ms 44.17 ms
1e5d96d+dirty 519.43 ms 543.62 ms 24.19 ms
4e0ba9c+dirty 452.84 ms 473.36 ms 20.52 ms
4e0b819+dirty 420.56 ms 470.08 ms 49.52 ms
a50b33d+dirty 500.81 ms 532.11 ms 31.30 ms
ad66da3+dirty 468.46 ms 533.56 ms 65.10 ms
37a2091+dirty 407.82 ms 441.22 ms 33.40 ms
68ae91b+dirty 416.44 ms 477.56 ms 61.12 ms
15d4514+dirty 406.77 ms 428.06 ms 21.29 ms
5a21b51+dirty 471.42 ms 524.22 ms 52.80 ms

App size

Revision Plain With Sentry Diff
5257d80+dirty 48.30 MiB 53.58 MiB 5.28 MiB
1e5d96d+dirty 49.74 MiB 54.81 MiB 5.07 MiB
4e0ba9c+dirty 48.30 MiB 53.49 MiB 5.19 MiB
4e0b819+dirty 49.74 MiB 54.81 MiB 5.07 MiB
a50b33d+dirty 43.75 MiB 48.08 MiB 4.33 MiB
ad66da3+dirty 48.30 MiB 53.49 MiB 5.19 MiB
37a2091+dirty 48.30 MiB 53.58 MiB 5.28 MiB
68ae91b+dirty 49.74 MiB 54.79 MiB 5.05 MiB
15d4514+dirty 48.30 MiB 53.60 MiB 5.30 MiB
5a21b51+dirty 48.30 MiB 53.49 MiB 5.19 MiB

@github-actions

Copy link
Copy Markdown
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 471.87 ms 492.67 ms 20.79 ms
Size 49.74 MiB 54.85 MiB 5.11 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
5a21b51+dirty 505.16 ms 539.20 ms 34.04 ms
bc0d8cf+dirty 407.66 ms 461.35 ms 53.69 ms
7887847+dirty 420.47 ms 460.55 ms 40.08 ms
5125c43+dirty 409.52 ms 451.00 ms 41.48 ms
7436d0f+dirty 429.58 ms 452.52 ms 22.94 ms
038a6d7+dirty 499.02 ms 527.68 ms 28.66 ms
88735e9+dirty 427.04 ms 487.37 ms 60.33 ms
d038a14+dirty 405.08 ms 444.36 ms 39.28 ms
20fbd51+dirty 594.38 ms 655.35 ms 60.97 ms
41d6254+dirty 406.20 ms 445.52 ms 39.32 ms

App size

Revision Plain With Sentry Diff
5a21b51+dirty 48.30 MiB 53.49 MiB 5.19 MiB
bc0d8cf+dirty 48.30 MiB 53.48 MiB 5.18 MiB
7887847+dirty 49.74 MiB 54.81 MiB 5.07 MiB
5125c43+dirty 48.30 MiB 53.54 MiB 5.24 MiB
7436d0f+dirty 48.30 MiB 53.60 MiB 5.30 MiB
038a6d7+dirty 48.30 MiB 53.60 MiB 5.30 MiB
88735e9+dirty 49.74 MiB 54.82 MiB 5.07 MiB
d038a14+dirty 48.30 MiB 53.49 MiB 5.19 MiB
20fbd51+dirty 49.74 MiB 54.81 MiB 5.07 MiB
41d6254+dirty 48.30 MiB 53.60 MiB 5.30 MiB

@alwx alwx merged commit 5789645 into main Jun 26, 2026
133 of 138 checks passed
@alwx alwx deleted the feat/auto-wrap-expo-router-error-boundary branch June 26, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants