Expo Router
Learn how to use Sentry's Expo Router instrumentation.
Sentry's React Native SDK ships first-class instrumentation for Expo Router: 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.
Add expoRouterIntegration to your Sentry.init integrations. No useNavigationContainerRef wiring is required — the integration reads Expo Router's internal navigation ref for you.
app/_layout.tsximport { isRunningInExpoGo } from "expo";
import * as Sentry from "@sentry/react-native";
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
integrations: [
Sentry.expoRouterIntegration({
enableTimeToInitialDisplay: !isRunningInExpoGo(),
}),
],
enableNativeFramesTracking: !isRunningInExpoGo(),
});
import { isRunningInExpoGo } from "expo";
import * as Sentry from "@sentry/react-native";
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
integrations: [
Sentry.expoRouterIntegration({
enableTimeToInitialDisplay: !isRunningInExpoGo(),
}),
],
enableNativeFramesTracking: !isRunningInExpoGo(),
});
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.
expoRouterIntegration requires Expo Router to be installed in your project. On non-Expo-Router projects it no-ops cleanly.
expoRouterIntegration accepts the same options as reactNavigationIntegration and forwards them through. The most common ones:
Enables automatic Time to Initial Display measurement for each navigation. Not supported in Expo Go. Default: false.
How long the instrumentation waits for the destination route to mount before discarding the navigation transaction. Default: 1000.
Drops back-navigation transactions that have no spans, which removes a lot of empty-transaction clutter in Sentry. Default: true.
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.
The integration attaches a structured representation of the active route to every navigation transaction:
| 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 is true. Without sendDefaultPii, only the templated route.name is attached, so navigations are still groupable in Sentry without leaking concrete IDs.
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.
app/(tabs)/index.tsximport { useRouter } from "expo-router";
import * as Sentry from "@sentry/react-native";
function HomeScreen() {
const router = Sentry.wrapExpoRouter(useRouter());
return (
<>
<Button
title="Open profile"
onPress={() => router.push("/users/42")}
/>
<Button
title="Prefetch details"
onPress={() => router.prefetch("/details")}
/>
</>
);
}
import { useRouter } from "expo-router";
import * as Sentry from "@sentry/react-native";
function HomeScreen() {
const router = Sentry.wrapExpoRouter(useRouter());
return (
<>
<Button
title="Open profile"
onPress={() => router.push("/users/42")}
/>
<Button
title="Prefetch details"
onPress={() => router.prefetch("/details")}
/>
</>
);
}
Wraps push, replace, navigate, back, dismiss, and prefetch. The wrapper is idempotent — calling wrapExpoRouter on an already-wrapped router is a no-op.
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.
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.
Expo Router supports a per-route ErrorBoundary export that renders a fallback when a route's component subtree throws during render. The most common shape is:
app/_layout.tsxexport { ErrorBoundary } from "expo-router";
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.
If you use getSentryExpoConfig in your metro.config.js, the SDK can auto-wrap export { ErrorBoundary } from 'expo-router' re-exports at build time. This is opt-in — enable it with autoWrapExpoRouterErrorBoundary: true (requires SDK 8.17.2 or later):
metro.config.jsconst { getSentryExpoConfig } = require("@sentry/react-native/metro");
module.exports = getSentryExpoConfig(__dirname, {
autoWrapExpoRouterErrorBoundary: true,
});
const { getSentryExpoConfig } = require("@sentry/react-native/metro");
module.exports = getSentryExpoConfig(__dirname, {
autoWrapExpoRouterErrorBoundary: true,
});
For non-Expo Metro setups (withSentryConfig), the option is off by default but available:
metro.config.jsconst { withSentryConfig } = require("@sentry/react-native/metro");
module.exports = withSentryConfig(config, {
autoWrapExpoRouterErrorBoundary: true,
});
const { withSentryConfig } = require("@sentry/react-native/metro");
module.exports = withSentryConfig(config, {
autoWrapExpoRouterErrorBoundary: true,
});
If you'd rather not rely on the Babel transform, wrap the boundary yourself with Sentry.wrapExpoRouterErrorBoundary:
app/_layout.tsximport { ErrorBoundary as ExpoErrorBoundary } from "expo-router";
import * as Sentry from "@sentry/react-native";
export const ErrorBoundary =
Sentry.wrapExpoRouterErrorBoundary(ExpoErrorBoundary);
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:
app/_layout.tsximport * as Sentry from "@sentry/react-native";
function MyErrorBoundary({ error, retry }) {
return /* your fallback UI */;
}
export const ErrorBoundary =
Sentry.wrapExpoRouterErrorBoundary(MyErrorBoundary);
import * as Sentry from "@sentry/react-native";
function MyErrorBoundary({ error, retry }) {
return /* your fallback UI */;
}
export const ErrorBoundary =
Sentry.wrapExpoRouterErrorBoundary(MyErrorBoundary);
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.pathifsendDefaultPii,route.paramsifsendDefaultPii, androute.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_boundarycategory describing the boundary render. - Tags the exception with the
expo_router_error_boundarymechanism 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.
- Slow and Frozen frames, Time To Initial Display, and Time To Full Display are only available in native builds, not in Expo Go.
expoRouterIntegrationreads Expo Router's internalrouter-storemodule. The integration logs a warning and no-ops if the installed Expo Router version doesn't expose the expected shape.
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").