Sentry for React Native
@sentry/react-native on Expo SDK 57 - initialization, source maps, release health, and breadcrumbs wired for EAS builds. This cookbook assumes you already understand the signal stack from Observability Basics.
Search across all documentation pages
@sentry/react-native on Expo SDK 57 - initialization, source maps, release health, and breadcrumbs wired for EAS builds. This cookbook assumes you already understand the signal stack from Observability Basics.
npx expo install @sentry/react-native
npx @sentry/wizard@latest -i reactNativeThe wizard adds Metro plugin config, native hooks, and EAS build steps. For manual setup, pin versions compatible with RN 0.86:
{
"dependencies": {
"@sentry/react-native": "^6.0.0",
"expo": "~57.0.4",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Tooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3.
Quick-reference recipe card - copy-paste ready.
1. Initialize at the earliest entry point
// app/_layout.tsx - must run before other imports side effects
import * as Sentry from "@sentry/react-native";
import { Stack } from "expo-router";
import * as Application from "expo-application";
import * as Updates from "expo-updates";
const nativeVersion = Application.nativeApplicationVersion ?? "0.0.0";
const buildNumber = Application.nativeBuildVersion ?? "0";
const release = `${nativeVersion}+${buildNumber}`;
const dist = Updates.updateId ?? buildNumber;
Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
environment: process.env.EXPO_PUBLIC_APP_ENV ?? "development",
release,
dist,
enableAutoSessionTracking: true,
tracesSampleRate: 0.2,
attachStacktrace: true,
beforeSend(event) {
// Strip PII - never send raw emails or tokens
if (event.user?.email) delete event.user.email;
return event;
},
});
function RootLayout() {
return <Stack />;
}
export default Sentry.wrap(RootLayout);2. EAS build - upload source maps
// eas.json excerpt
{
"build": {
"production": {
"env": {
"SENTRY_AUTH_TOKEN": "@sentry-auth-token",
"SENTRY_ORG": "your-org",
"SENTRY_PROJECT": "your-rn-app"
}
}
}
}# After EAS Build completes, sentry-expo / wizard hooks upload:
# - Hermes source maps (JS)
# - Debug symbols for native frames (iOS dSYM, Android NDK if used)
eas build --platform all --profile production3. Navigation breadcrumbs (Expo Router)
import { useEffect } from "react";
import { usePathname } from "expo-router";
import * as Sentry from "@sentry/react-native";
export function useSentryRouteBreadcrumb() {
const pathname = usePathname();
useEffect(() => {
Sentry.addBreadcrumb({
category: "navigation",
message: pathname,
level: "info",
});
Sentry.setTag("route", pathname);
}, [pathname]);
}4. Network breadcrumb wrapper
export async function sentryFetch(input: RequestInfo, init?: RequestInit) {
const start = Date.now();
const url = typeof input === "string" ? input : input.url;
try {
const res = await fetch(input, init);
Sentry.addBreadcrumb({
category: "http",
message: `${init?.method ?? "GET"} ${url}`,
data: { status: res.status, durationMs: Date.now() - start },
level: res.ok ? "info" : "warning",
});
return res;
} catch (err) {
Sentry.addBreadcrumb({
category: "http",
message: `${init?.method ?? "GET"} ${url} failed`,
data: { durationMs: Date.now() - start },
level: "error",
});
throw err;
}
}When to reach for this:
Full bootstrap with session context, auth-aware user scope, and error boundary integration.
// src/observability/sentry.ts
import * as Sentry from "@sentry/react-native";
import * as Application from "expo-application";
import * as Updates from "expo-updates";
export function initSentry() {
const version = Application.nativeApplicationVersion ?? "0.0.0";
const build = Application.nativeBuildVersion ?? "0";
Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
environment: process.env.EXPO_PUBLIC_APP_ENV,
release: `${version}+${build}`,
dist: Updates.updateId ?? build,
enableAutoSessionTracking: true,
enableNative: true,
enableNativeCrashHandling: true,
tracesSampleRate: __DEV__ ? 1.0 : 0.15,
integrations: [
Sentry.reactNativeTracingIntegration(),
],
});
}
export function setSentryUser(userId: string | null) {
if (userId) {
Sentry.setUser({ id: userId });
} else {
Sentry.setUser(null);
}
}
export function captureWithContext(error: unknown, context?: Record<string, unknown>) {
Sentry.withScope((scope) => {
if (context) scope.setContext("extra", context);
Sentry.captureException(error);
});
}// app/_layout.tsx
import { useEffect } from "react";
import { Stack } from "expo-router";
import * as Sentry from "@sentry/react-native";
import { initSentry, setSentryUser } from "@/observability/sentry";
import { useSentryRouteBreadcrumb } from "@/observability/useSentryRouteBreadcrumb";
import { useAuth } from "@/features/auth/useAuth";
initSentry();
function RootLayout() {
const { userId } = useAuth();
useSentryRouteBreadcrumb();
useEffect(() => {
setSentryUser(userId);
}, [userId]);
return (
<Sentry.ErrorBoundary fallback={<CrashFallback />}>
<Stack />
</Sentry.ErrorBoundary>
);
}
function CrashFallback() {
return null; // your recovery UI
}
export default Sentry.wrap(RootLayout);// Pair with auth logout - clear user scope
// setSentryUser(null) on logout - see ../auth-session/mobile-auth-basics/Hermes ships bytecode in release builds. Without uploaded maps, Sentry shows index.android.bundle:1:234567.
| Artifact | When | Purpose |
|---|---|---|
Hermes .map | Every EAS production build | JS stack → .tsx lines |
| iOS dSYM | Native iOS build | Native frame → Swift/ObjC |
| ProGuard mapping | Android release | Obfuscated Java → symbols |
# Verify release exists in Sentry before shipping OTA
sentry-cli releases list | grep "1.4.2+42"release in Sentry.init must exactly match the CLI upload release stringdist differentiates OTA bundles sharing the same native release - use Updates.updateIdRelated: OTA Updates Basics - when JS changes ship without store review
Sentry Release Health tracks sessions and crash-free rates per release + environment.
// Sessions start automatically with enableAutoSessionTracking: true
// End on crash, or background beyond sessionTrackingIntervalMillis (default 30s)
Sentry.init({
// ...
enableAutoSessionTracking: true,
sessionTrackingIntervalMillis: 30000,
});production vs preview environmentsdistenvironment| Category | Source | Example |
|---|---|---|
navigation | Expo Router pathname | /checkout/payment |
http | Wrapped fetch | POST /v1/orders 201 |
auth | Login/logout handlers | refresh_token_rotated |
ui | Primary user actions | tap_place_order |
console | Sentry.captureConsoleIntegration | dev warnings only |
Sentry.addBreadcrumb({
category: "auth",
message: "token_refresh_failed",
level: "warning",
data: { reason: "invalid_grant" }, // no raw tokens
});Sentry tracing integrates with navigation and fetch - expanded in Performance Monitoring.
Sentry.startSpan({ name: "load_feed", op: "ui.load" }, async () => {
await fetchFeed();
});eas build --profile development before production.app/_layout.tsx as a side effect, or a dedicated instrumentation.ts imported first.Sentry.wrap on the root component once.captureException and rethrow from boundaries unless intentional.ErrorUtils handlers - see Global Error Handlers.1.4.2+42).Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026