Sentry
Set up Sentry for crash reporting in an Expo app with ReadyNative: JS + native crash reporting · Expo Go OK (JS errors only)
Expo Go: yes
Runs in Expo Go; no dev build needed for this module.
This reports crashes and handled errors to Sentry: src/lib/crash.ts calls Sentry.init once crash consent is given (only when the DSN is set, with environment from APP_VARIANT, errors only, no traces), crash.capture(err, ctx) maps to captureException with ctx as extra, and CrashBoundary is a Sentry.ErrorBoundary whose fallback is the kit's ErrorState with Retry. CrashProvider (order 40) mounts that boundary app-wide, with its own ThemeProvider since it sits above the ui provider. If you set up with --with-examples there's an example at /examples/crash and tests for both the capture path and the boundary. @/lib/crash keeps the same API whichever option you pick - Sentry is the one service option here, and it's what you want the moment real users can hit a bug you can't reproduce.
Setup
bun run setup --crash sentry- Create a project at sentry.io (platform: React Native), then open [Settings] → [Projects] → your project → [Client Keys (DSN)] and copy the DSN into
.envasEXPO_PUBLIC_SENTRY_DSN. It's public and safe in the client. Without it the SDK isn't initialised,crash.captureonly logs in dev, and the example shows "Configure EXPO_PUBLIC_SENTRY_DSN". - Take the org and project slugs from the project URL and set them as the build-time secrets
SENTRY_ORGandSENTRY_PROJECT- never with anEXPO_PUBLIC_prefix. - Create a token at [Settings] → [Auth Tokens] with the scopes
project:releasesandorg:read, and set it asSENTRY_AUTH_TOKEN. These three are only used at build time, to upload source maps. - If you install with bun, run
bun pm trust @sentry/clionce on an existing tree. bun blocks lifecycle scripts by default and@sentry/clidownloads its binary inpostinstall; without it, source-map uploads fail. The module addstrustedDependencies: ["@sentry/cli"]topackage.json, so a freshbun installhandles it for you. - The config plugin
["@sentry/react-native", { organization: "", project: "", url }]is added to.readynative.jsonand applied byapp.config.ts. The empty org and project make the plugin fall back to theSENTRY_ORGandSENTRY_PROJECTenv vars at build time, so there's nothing to edit. (The plugin id is the package name, not@sentry/react-native/expo: same code, andexpo installwould otherwise try to add it to the dynamic config and fail.)
Going to production?
Put SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN in EAS (eas env:set --environment production --visibility sensitive) so release builds and eas update can upload source maps - without them, stack traces stay minified. Use a separate Sentry project (or at least rely on the environment tag, which follows APP_VARIANT) so production issues aren't buried in your dev noise.
- Run
bun run doctor- every row for this module should be green.
The keys, in one place:
| Key | Where to get it |
|---|---|
EXPO_PUBLIC_SENTRY_DSN | https://sentry.io/settings/projects/ → project → Client Keys (DSN). Public; safe in the client. |
SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN (EAS secrets, never EXPO_PUBLIC_) | org/project slugs from the project URL; token from https://sentry.io/settings/auth-tokens/ (scopes project:releases, org:read). Build time only, for source-map upload. |
Dashboards: Issues at https://sentry.io/organizations/<org>/issues/ · Releases (source maps) at …/releases/ · Client Keys. Deps: @sentry/react-native ~7.11.0 - the version Expo SDK 57 pins, so 8.x isn't an option until that pin moves (npx expo install --fix downgrades anything newer).
Usage
Report a handled error with context:
import { crash } from "@/lib/crash";
crash.capture(err, { screen: "checkout", orderId });Wrap a risky subtree so a render error doesn't take the app down:
import { CrashBoundary } from "@/lib/crash";
<CrashBoundary>{riskySubtree}</CrashBoundary>;Tag the signed-in user, so issues say who hit them:
import * as Sentry from "@sentry/react-native";
Sentry.setUser({ id: user.id });Consent and PII
Sentry.init runs only once consent.get().crash is true (and the DSN is set). Until then no JS or native crash report, session or client report leaves the device - native crash files from a previous run wait for the next init. Withdrawing consent calls Sentry.close(); consent again runs a new Sentry.init. beforeSend / beforeSendTransaction still return null while crash consent is false, and crash.capture is a no-op then. With consent/consent that is the EU/EEA, UK, CH, CA, BR until the sheet is answered; the Settings → Privacy toggle takes effect live. isCrashConfigured only means the DSN is set; isCrashRunning() tells you whether the SDK is initialised.
Everything that does go out passes scrubEvent(): user.ip_address, user.email, user.username, request.headers, request.cookies, request.query_string and request.data are dropped, the query string is stripped from request.url, and emails ([email]), JWTs, Bearer … tokens and token= / password= / api_key= / secret= values are masked in the message, logentry, exception values, extra (secret-named keys become [redacted]) and breadcrumbs. beforeBreadcrumb (scrubBreadcrumb) strips query strings and fragments from data.url, data.from and data.to on http and navigation breadcrumbs. user.id stays so you can count affected users. sendDefaultPii is off. ctx is scrubbed too, but still keep crash.capture(err, ctx) contexts free of personal data.
In Sentry, turn on [Project Settings] → [Security & Privacy] → "Prevent Storing of IP Addresses": the connection still reveals the device IP to Sentry, which also derives geo data from it.
Gotchas
- There's no Metro plugin (
withSentryConfiginmetro.config.js, which the ui module owns), so JS bundles carry no Sentry Debug IDs and uploads match by release and dist instead. Runnpx sentry-expo-upload-sourcemaps distaftereas update; EAS native builds upload through the plugin's gradle and xcode scripts. Add the Metro plugin yourself if you want Debug IDs. Sentry.wrap(RootLayout)- touch breadcrumbs and the profiler - isn't applied, because core owns_layout.tsx. Wrap it there if you want them.bun run setup --crash noneremoves everything; with--with-examples,/examples/crashthen shows "Module not installed".- Expo Go covers JS errors, breadcrumbs and the boundary. Native crashes, offline caching and
Sentry.nativeCrash()need a dev build.
Check it works (the Examples steps need a tree set up with --with-examples):
- Put a real
EXPO_PUBLIC_SENTRY_DSNin.env, runbun run start -- -c, and open the app on a device (Expo Go is fine for JS errors). - Examples → "Crash reporting" → Capture test error: Issues shows
Error: Test error from /examples/crashwithextra.source = examples/crashwithin about a minute, environmentdev. - Tap Throw render error: the card shows "Something went wrong · Render error from /examples/crash" with Retry, the rest of the screen still works, and a second issue appears with
mechanism: react-error-boundary. Tap Disarm, then Retry, and "Renders fine until you throw." is back. - On a dev build only, add
Sentry.nativeCrash()to a button and relaunch: a native crash issue appears (symbolication needsSENTRY_AUTH_TOKEN). - Remove the DSN and restart: the example shows "Configure EXPO_PUBLIC_SENTRY_DSN" and the app still boots.
Remove it
While modules/ exists (a tree set up with --keep-modules), setup does all of it:
bun run setup --crash none --yes --keep-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- Delete the files that are still there (the demo screens are gone already unless you set up with
--with-examples):src/app/examples/crash.tsx,src/lib/__tests__/crash-boundary.test.tsx,src/lib/__tests__/crash-sentry.test.ts,src/screens/examples/crash-example-screen.tsx. - Replace, don't delete
src/lib/crash.ts: core code imports it, so swap in the no-op version frommodules/crash/none/files/of a fresh clone of your tier repo - same exports, nothing behind them. - Uninstall the dependencies:
bun remove @sentry/react-native. - Drop the config plugin
@sentry/react-nativefrom.readynative.json→modules.app.expo.plugins(that is whereapp.config.tsreads it from), then rebuild the dev build. - Unwrap the provider: delete
<CrashProvider>and its import fromsrc/providers.tsx. - Remove the env keys
EXPO_PUBLIC_SENTRY_DSNfrom.env,.env.exampleand your EAS environment, andEXPO_PUBLIC_SENTRY_DSNfromsrc/lib/env.ts. - Update the privacy declarations: remove this module's entries from
.readynative.json→modules.app.expo.ios.privacyManifests, then re-runbun run gen:privacyand revise your store privacy answers. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/ARCHITECTURE.md.
Reference
Everything below is generated from modules/crash/sentry/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --crash sentryModule id: crash/sentry (the default for this category).
Dependencies
| Package | Version | Kind |
|---|---|---|
@sentry/react-native | ~7.11.0 | dependency (expo install) |
Config plugins
Merged into app.config.ts through .readynative.json (modules.app):
@sentry/react-native(with options)
Environment keys
| Key | Required | Server-only | Example | Docs |
|---|---|---|---|---|
EXPO_PUBLIC_SENTRY_DSN | yes | no | https://examplePublicKey@o0.ingest.sentry.io/0 | dashboard |
Keys go in .env (see .env.example). Required keys are checked by bun run doctor; Server-only keys have no EXPO_PUBLIC_ prefix, are read only by API routes and never reach the bundle.
Privacy
Play Data safety draft: collects Crash logs, Diagnostics (performance), Device info; shared with Sentry (processor). Source of truth: vendor disclosure.
Apple privacy manifest data types (composed into ios.privacyManifests by setup):
| Type | Linked to user | Tracking | Purposes |
|---|---|---|---|
CrashData | no | no | AppFunctionality |
PerformanceData | no | no | AppFunctionality |
OtherDiagnosticData | no | no | AppFunctionality |
Providers
Rendered in src/providers.tsx (lower order = outermost):
| Order | Provider | From |
|---|---|---|
| 40 | CrashProvider | @/lib/crash |
Doctor checks
- env:
EXPO_PUBLIC_SENTRY_DSN
After setup
- Sentry: create a React Native project at https://sentry.io, copy its DSN (Settings > Projects > <project> > Client Keys) into EXPO_PUBLIC_SENTRY_DSN.
- Sentry: for source maps set EAS secrets SENTRY_ORG, SENTRY_PROJECT and SENTRY_AUTH_TOKEN (https://sentry.io/settings/auth-tokens/, scopes project:releases + org:read); the config plugin uploads on native builds,
npx sentry-expo-upload-sourcemaps distaftereas update. - Sentry: call
crash.capture(new Error("test"))from any screen (or Examples > Crash reporting with--with-examples) → the issue appears under Issues within a minute.
Files
5 files copied to the project root
src/app/examples/crash.tsxsrc/lib/__tests__/crash-boundary.test.tsxsrc/lib/__tests__/crash-sentry.test.tssrc/lib/crash.tssrc/screens/examples/crash-example-screen.tsx