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.tsx
Copied
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 registerNavigationContainerexpoRouterIntegration resolves Expo Router's navigation container and configures route reporting on your behalf. If you already use reactNavigationIntegration directly, expoRouterIntegration will reuse it.

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:

AttributeExamplePII-gated
route.name/users/[id]No — templated, structural
route.path/users/42Yes — 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.tsx
Copied
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() 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.tsx
Copied
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.js
Copied
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.js
Copied
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.tsx
Copied
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.tsx
Copied
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.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.

  • 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.
Was this helpful?
Help improve this content
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").