Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/platforms/react-native/integrations/error-boundary.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ og_image: /og-images/platforms-react-native-integrations-error-boundary.png

The React Native SDK exports an error boundary component that uses [React component APIs](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary) to automatically catch and send JavaScript errors from inside a React component tree to Sentry, and render a fallback UI.

<Alert level="info" title="Using Expo Router?">

Expo Router has its own per-route `ErrorBoundary` export convention that this component does not interact with. To capture errors that hit Expo Router's boundary, see [Capturing Errors From Expo Router's `ErrorBoundary`](/platforms/react-native/tracing/instrumentation/expo-router/#capturing-errors-from-expo-routers-errorboundary).

</Alert>

<Alert level="warning" title="Render errors only">

React error boundaries **only catch errors during rendering, in lifecycle methods, and in constructors**. They do **not** catch errors in event handlers, asynchronous code (`setTimeout`, `Promise`), or native errors. For a fallback UI that covers those cases too, use [`Sentry.GlobalErrorBoundary`](#showing-a-fallback-ui-for-fatal-errors).
Expand Down
2 changes: 1 addition & 1 deletion docs/platforms/react-native/manual-setup/expo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ To verify that everything is working as expected, build the `Release` version of

### Performance

- [Expo Router tracing](/platforms/react-native/tracing/instrumentation/expo-router/) — Navigation transitions, performance spans, and prefetch instrumentation
- [Expo Router tracing](/platforms/react-native/tracing/instrumentation/expo-router/) — Navigation transitions, performance spans, prefetch instrumentation, and per-route `ErrorBoundary` capture
- [Expo Image and Asset tracing](/platforms/react-native/tracing/instrumentation/expo-resources/) — Automatic spans for `expo-image` and `expo-asset`

## Notes
Expand Down
180 changes: 129 additions & 51 deletions docs/platforms/react-native/tracing/instrumentation/expo-router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,177 @@ description: "Learn how to use Sentry's Expo Router instrumentation."
sidebar_order: 65
---

Sentry's React Native SDK package ships with instrumentation for Expo Router. This allows you to see the performance of your navigation transitions and the errors that occur during them. This page will guide you through setting up the instrumentation and configuring it to your needs.
Sentry's React Native SDK ships first-class instrumentation for [Expo Router](https://docs.expo.dev/router/introduction/): navigation transactions with route context, prefetch and method spans, and per-route render-error capture. This page walks through the canonical setup and the surfaces it covers.

## Initialization

The code snippet below shows how to initialize the instrumentation.
Add `expoRouterIntegration` to your `Sentry.init` integrations. No `useNavigationContainerRef` wiring is required — the integration reads Expo Router's internal navigation ref for you.

```javascript {6-8, 15-16, 20-25} {filename: app/_layout.tsx}
import React from 'react';
import { Slot, useNavigationContainerRef } from 'expo-router';
```javascript {filename: app/_layout.tsx} {7}
import { isRunningInExpoGo } from 'expo';
import * as Sentry from '@sentry/react-native';

const navigationIntegration = Sentry.reactNavigationIntegration({
enableTimeToInitialDisplay: !isRunningInExpoGo(),
});

Sentry.init({
dsn: '___PUBLIC_DSN___',
// Set tracesSampleRate to 1.0 to capture 100% of transactions for tracing.
// We recommend adjusting this value in production.
// Learn more at
// https://docs.sentry.io/platforms/javascript/configuration/options/#traces-sample-rate
tracesSampleRate: 1.0,
integrations: [navigationIntegration],
integrations: [Sentry.expoRouterIntegration({
enableTimeToInitialDisplay: !isRunningInExpoGo(),
})],
enableNativeFramesTracking: !isRunningInExpoGo(),
});
```

function RootLayout() {
const ref = useNavigationContainerRef();
React.useEffect(() => {
if (ref) {
navigationIntegration.registerNavigationContainer(ref);
}
}, [ref]);
That's the whole setup. You don't need to add `reactNavigationIntegration` separately or call `registerNavigationContainer` — `expoRouterIntegration` resolves Expo Router's navigation container and configures route reporting on your behalf. If you already use `reactNavigationIntegration` directly, `expoRouterIntegration` will reuse it.

return <Slot />;
}
<Alert level="info">

export default Sentry.wrap(RootLayout);
```
`expoRouterIntegration` requires Expo Router to be installed in your project. On non-Expo-Router projects it no-ops cleanly.

</Alert>

## Options

You can configure the instrumentation by passing an options object to the constructor:
`expoRouterIntegration` accepts the same options as [`reactNavigationIntegration`](/platforms/react-native/tracing/instrumentation/react-navigation/#options) and forwards them through. The most common ones:

```javascript
Sentry.reactNavigationIntegration({
enableTimeToInitialDisplay: true, // default: false
routeChangeTimeoutMs: 1000, // default: 1000
ignoreEmptyBackNavigationTransactions: true, // default: true
});
```
### `enableTimeToInitialDisplay`

### routeChangeTimeoutMs
Enables automatic [Time to Initial Display](/platforms/react-native/tracing/instrumentation/time-to-display) measurement for each navigation. Not supported in Expo Go. Default: `false`.

This option specifies how long the instrumentation will wait for the route to mount after a change has been initiated before the transaction is discarded. The default value is `1_000`.
### `routeChangeTimeoutMs`

### enableTimeToInitialDisplay
How long the instrumentation waits for the destination route to mount before discarding the navigation transaction. Default: `1000`.

This option will enable automatic measuring of the time to initial display for each route. To learn more see [Time to Initial Display](/platforms/react-native/tracing/instrumentation/time-to-display). The default value is `false`.
### `ignoreEmptyBackNavigationTransactions`

### ignoreEmptyBackNavigationTransactions
Drops back-navigation transactions that have no spans, which removes a lot of empty-transaction clutter in Sentry. Default: `true`.

This ensures that transactions that are from routes that've been seen and don't have any spans, are not being sampled. This removes a lot of clutter, making it so that most back navigation transactions are now ignored. The default value is `true`.
### `enablePrefetchTracking`

## Prefetch Instrumentation
Creates a separate span for `PRELOAD` actions so you can see prefetch timing alongside navigation. Especially useful with Expo Router's `router.prefetch()`. Default: `false`.

<Alert level="info">
## Route and Parameter Attributes

`router.prefetch()` requires **Expo Router v5 (Expo SDK 53) or later**. On older versions the method does not exist; calling it will throw a runtime error.
The integration attaches a structured representation of the active route to every navigation transaction:

</Alert>
| Attribute | Example | PII-gated |
|-----------------------|----------------------|-----------|
| `route.name` | `/users/[id]` | No — templated, structural |
| `route.path` | `/users/42` | Yes — concrete, may contain identifiers |
| `route.params` | `{ id: '42' }` | Yes |

`route.name` is built from Expo Router's `segments`, with grouping segments (e.g. `(tabs)`, `(auth)`) stripped so it matches what users see in the URL bar. It's always safe to send.

`route.path` and `route.params` may contain user identifiers, so they're sent **only when [`sendDefaultPii`](/platforms/react-native/configuration/options/#sendDefaultPii) is `true`**. Without `sendDefaultPii`, only the templated `route.name` is attached, so navigations are still groupable in Sentry without leaking concrete IDs.

## Wrapped Router Methods

Expo Router's `router.prefetch()` preloads a route before the user navigates to it. By default, these prefetch calls are invisible in your traces. Wrapping the router with `Sentry.wrapExpoRouter()` adds an automatic `navigation.prefetch` span for each call so you can see prefetch timing alongside your other navigation spans.
`Sentry.wrapExpoRouter` instruments the imperative router methods returned by `useRouter()`. Each wrapped call emits a navigation breadcrumb, opens a short-lived span around the dispatch, and tags the next idle navigation span with the initiating method so the navigation transaction can be attributed back to the call site.

```javascript {filename:app/(tabs)/index.tsx}
```javascript {filename: app/(tabs)/index.tsx}
import { useRouter } from 'expo-router';
import * as Sentry from '@sentry/react-native';

function HomeScreen() {
const router = Sentry.wrapExpoRouter(useRouter());

return (
<Button
title="Go to Details"
onPress={() => router.prefetch('/details')}
/>
<>
<Button title="Open profile" onPress={() => router.push('/users/42')} />
<Button title="Prefetch details" onPress={() => router.prefetch('/details')} />
</>
);
}
```

The span name is `Prefetch /details` (or `Prefetch unknown` for unresolvable routes). Span attributes include `route.href` and `route.name`.
Wraps `push`, `replace`, `navigate`, `back`, `dismiss`, and `prefetch`. The wrapper is idempotent — calling `wrapExpoRouter` on an already-wrapped router is a no-op.

### Prefetch Instrumentation

<Alert level="info">

`router.prefetch()` requires **Expo Router v5 (Expo SDK 53) or later**. On older versions the method does not exist; calling it will throw a runtime error.

</Alert>

`router.prefetch()` preloads a route before the user navigates to it. By default these calls are invisible in traces. The wrapped router adds a `navigation.prefetch` span named `Prefetch /details` (or `Prefetch unknown` for unresolvable hrefs) with `route.href` and `route.name` attributes.

## Capturing Errors From Expo Router's `ErrorBoundary`

Expo Router supports a per-route [`ErrorBoundary`](https://docs.expo.dev/router/error-handling/) export that renders a fallback when a route's component subtree throws during render. The most common shape is:

```tsx {filename: app/_layout.tsx}
export { ErrorBoundary } from 'expo-router';
```

Because React considers the error handled once the boundary renders the fallback, **Sentry never sees the error** unless the SDK is wired into the boundary. The React Native SDK provides two ways to do that.

### Automatic Wrapping (Recommended)

If you use `getSentryExpoConfig` in your `metro.config.js`, the SDK auto-wraps `export { ErrorBoundary } from 'expo-router'` re-exports at build time:

```javascript {filename: metro.config.js}
const { getSentryExpoConfig } = require('@sentry/react-native/metro');

module.exports = getSentryExpoConfig(__dirname);
```

Auto-wrapping is on by default with `getSentryExpoConfig`. Aliased re-exports (`export { ErrorBoundary as Foo }`) keep the user-chosen name, and mixed re-exports (`export { ErrorBoundary, Stack }`) preserve the non-boundary specifiers.

To opt out:

```javascript {filename: metro.config.js}
module.exports = getSentryExpoConfig(__dirname, {
autoWrapExpoRouterErrorBoundary: false,
});
```

For non-Expo Metro setups (`withSentryConfig`), the option is off by default but available:

```javascript {filename: metro.config.js}
const { withSentryConfig } = require('@sentry/react-native/metro');

module.exports = withSentryConfig(config, {
autoWrapExpoRouterErrorBoundary: true,
});
```

### Manual Wrapping

If you'd rather not rely on the Babel transform, wrap the boundary yourself with `Sentry.wrapExpoRouterErrorBoundary`:

```tsx {filename: app/_layout.tsx}
import { ErrorBoundary as ExpoErrorBoundary } from 'expo-router';
import * as Sentry from '@sentry/react-native';

export const ErrorBoundary = Sentry.wrapExpoRouterErrorBoundary(ExpoErrorBoundary);
```

You can also pass your own custom boundary component — anything matching `{ error: Error; retry: () => Promise<void> }` works:

```tsx {filename: app/_layout.tsx}
import * as Sentry from '@sentry/react-native';

function MyErrorBoundary({ error, retry }) {
return /* your fallback UI */;
}

export const ErrorBoundary = Sentry.wrapExpoRouterErrorBoundary(MyErrorBoundary);
```

### What Gets Captured

For each new error instance that hits the boundary, the wrapper:

- **Captures the error to Sentry** with the route attached as context (`route.name`, `route.path` if `sendDefaultPii`, `route.params` if `sendDefaultPii`, and `route.segments`).
- **Tags the in-flight navigation transaction as errored** so the broken render shows up as a failed transaction. Only navigation-origin spans are touched — user-started custom spans are left alone.
- **Adds a breadcrumb** under the `expo-router.error_boundary` category describing the boundary render.
- **Tags the exception** with the `expo_router_error_boundary` mechanism so you can filter on it.

Then control is handed to the original boundary so the user-visible fallback UI is unchanged.

Reporting is deduplicated per error instance (across re-renders and unmount/remount cycles), and the boundary always renders the fallback even if Sentry instrumentation itself throws.

## Notes

- Slow and Frozen frames, Time To Initial Display and Time To Full Display are only available in native builds, not in Expo Go.
- Slow and Frozen frames, Time To Initial Display, and Time To Full Display are only available in native builds, not in Expo Go.
- `expoRouterIntegration` reads Expo Router's internal `router-store` module. The integration logs a warning and no-ops if the installed Expo Router version doesn't expose the expected shape.
Loading