Skip to content
~/sph.sh

Sentry Integration with React Native Expo: A Practical Quick Guide

Step-by-step guide to integrating Sentry error monitoring into a React Native Expo app. Covers SDK initialization, Expo Router instrumentation, session replay, source map uploads for EAS Build and EAS Update, and common pitfalls to avoid.

This guide walks through integrating Sentry into a React Native Expo application, from installation to production-grade source map uploads. It covers the critical configuration details, Expo Go limitations, session replay setup, and the gotchas that tend to waste hours if you encounter them unprepared.

Prerequisites

Before starting, you need:

  • An Expo project using SDK 50 or later (SDK 52+ recommended)
  • A Sentry account (free tier provides 5,000 errors/month)
  • A Sentry React Native project created in the dashboard
  • Three identifiers from Sentry: Organization slug, Project name, and DSN
  • An Organization Auth Token generated from Sentry settings

Step 1: Installation

The Sentry wizard automates most of the setup. Run it from your project root:

bash
npx @sentry/wizard@latest -i reactNative

The wizard handles Metro config, Expo plugin config, and basic SDK initialization. If you prefer manual setup or the wizard does not work in your environment, install the package directly:

bash
npx expo install @sentry/react-native

Manual setup requires configuring three files yourself, which the following steps cover in detail.

Step 2: Metro Configuration

Sentry needs to hook into the Metro bundler for source map generation. Replace your metro.config.js with:

javascript
// metro.config.jsconst { getSentryExpoConfig } = require("@sentry/react-native/metro");
const config = getSentryExpoConfig(__dirname);
module.exports = config;

If you already have a custom Metro config, wrap your existing config with getSentryExpoConfig instead of replacing it entirely.

Step 3: Expo Plugin Configuration

Add the Sentry plugin to your app.json (or app.config.js):

json
{  "expo": {    "plugins": [      [        "@sentry/react-native/expo",        {          "url": "https://sentry.io/",          "project": "my-expo-app",          "organization": "my-org"        }      ]    ]  }}

Warning: Never put authToken in app.json or app.config.js. This config gets embedded in the app bundle and is accessible at runtime. Set the auth token as an environment variable or EAS secret instead. This was a documented security issue in the Expo SDK 50 migration docs.

Step 4: SDK Initialization with Expo Router

The main initialization goes in your root layout file. This example includes performance monitoring with Expo Router navigation tracking, environment configuration, and session replay:

typescript
// app/_layout.tsximport { Stack, useNavigationContainerRef } from "expo-router";import * as Sentry from "@sentry/react-native";import { isRunningInExpoGo } from "expo";import React from "react";
const navigationIntegration = Sentry.reactNavigationIntegration({  enableTimeToInitialDisplay: !isRunningInExpoGo(),});
Sentry.init({  dsn: "https://[email protected]/0",
  // Environment configuration  environment: __DEV__ ? "development" : "production",
  // Performance monitoring  tracesSampleRate: __DEV__ ? 1.0 : 0.2,  enableNativeFramesTracking: !isRunningInExpoGo(),
  // Session replay  replaysOnErrorSampleRate: 1.0,  replaysSessionSampleRate: __DEV__ ? 1.0 : 0.1,
  // Integrations  integrations: [    navigationIntegration,    Sentry.mobileReplayIntegration(),  ],});
function RootLayout() {  const ref = useNavigationContainerRef();
  React.useEffect(() => {    if (ref?.current) {      navigationIntegration.registerNavigationContainer(ref);    }  }, [ref]);
  return <Stack />;}
export default Sentry.wrap(RootLayout);

A few things worth noting in this configuration:

  • tracesSampleRate: 0.2 in production captures 20% of transactions. Setting this to 1.0 in production will burn through your quota quickly and can impact app performance.
  • replaysOnErrorSampleRate: 1.0 captures session replays for every error, which is the most valuable use case. General session replays at 0.1 provide a sample without excessive data.
  • isRunningInExpoGo() guards prevent enabling native-only features in Expo Go, where they would silently fail or crash.

Step 5: Understanding Expo Go vs Development Builds

This is one of the most common sources of confusion. Expo Go does not include Sentry's native modules, which limits functionality significantly.

The isRunningInExpoGo() guard prevents enabling native-only features when running in Expo Go:

typescript
Sentry.init({  // ...  enableNativeFramesTracking: !isRunningInExpoGo(),  profilesSampleRate: isRunningInExpoGo() ? 0 : 1.0,});

For full Sentry functionality, always validate with a development build or EAS Build before shipping.

Step 6: Error Boundaries

Sentry provides an ErrorBoundary component that catches React rendering errors and reports them automatically:

tsx
import * as Sentry from "@sentry/react-native";import { View, Text, TouchableOpacity } from "react-native";
function ErrorFallback({  error,  resetError,}: {  error: Error;  resetError: () => void;}) {  return (    <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>      <Text style={{ fontSize: 18, fontWeight: "bold" }}>        Something went wrong      </Text>      <Text style={{ color: "#666", marginTop: 8 }}>        {error.message}      </Text>      <TouchableOpacity        onPress={resetError}        style={{          marginTop: 16,          padding: 12,          backgroundColor: "#362d59",          borderRadius: 8,        }}      >        <Text style={{ color: "#fff" }}>Try Again</Text>      </TouchableOpacity>    </View>  );}
export function ScreenWithErrorBoundary() {  return (    <Sentry.ErrorBoundary      fallback={({ error, resetError }) => (        <ErrorFallback error={error} resetError={resetError} />      )}      beforeCapture={(scope) => {        scope.setTag("component", "MainScreen");      }}    >      <MainScreenContent />    </Sentry.ErrorBoundary>  );}

The beforeCapture callback lets you add context tags, making it easier to filter and group errors in the Sentry dashboard.

Note: In React development mode, React rethrows errors caught by error boundaries to the global error handler, which can cause duplicate reports. Test error boundary behavior in production builds.

Step 7: Source Maps -- EAS Build and EAS Update

Getting readable stack traces in production requires properly uploaded source maps. The process differs between EAS Build and EAS Update.

EAS Build (Automatic)

For native builds, source map upload is automatic. Just set the auth token as an EAS secret:

bash
eas env:create --name SENTRY_AUTH_TOKEN --value "sntrys_YOUR_TOKEN_HERE" --scope project --visibility secret --environment production

That is it. The Sentry Expo plugin handles the rest during the build process.

EAS Update (Manual)

OTA updates do not automatically upload source maps. After every eas update, you need to upload them manually:

bash
npx expo export --platform allnpx sentry-expo-upload-sourcemaps dist

Automate this in your CI/CD pipeline to avoid shipping updates with unreadable stack traces.

Step 8: Tagging EAS Update Metadata

When using OTA updates, tagging Sentry events with update metadata makes it possible to filter errors by update version:

typescript
import * as Sentry from "@sentry/react-native";import * as Updates from "expo-updates";
function tagSentryWithUpdateInfo() {  const manifest = Updates.manifest;  const metadata =    manifest && "metadata" in manifest ? manifest.metadata : undefined;  const updateGroup =    metadata && typeof metadata === "object" && "updateGroup" in metadata      ? (metadata as { updateGroup: string }).updateGroup      : undefined;
  const scope = Sentry.getGlobalScope();
  scope.setTag("expo-update-id", Updates.updateId ?? "embedded");  scope.setTag("expo-is-embedded-update", String(Updates.isEmbeddedLaunch));
  if (updateGroup) {    scope.setTag("expo-update-group-id", updateGroup);  }}
// Call this early in your app lifecycle, after Sentry.init()tagSentryWithUpdateInfo();

This is particularly useful when an OTA update introduces a regression -- you can immediately identify which update group caused the spike in errors.

Step 9: Verifying the Integration

Before going to production, verify that errors appear correctly in your Sentry dashboard with a simple test component:

tsx
import * as Sentry from "@sentry/react-native";import { Button, View } from "react-native";
export function SentryTestButtons() {  return (    <View style={{ gap: 12, padding: 16 }}>      <Button        title="Test JS Error"        onPress={() => {          Sentry.captureException(new Error("Test JS error from Expo"));        }}      />
      <Button        title="Test Unhandled Error"        onPress={() => {          throw new Error("Unhandled test error");        }}      />
      <Button        title="Test Native Crash"        onPress={() => {          Sentry.nativeCrash();        }}      />    </View>  );}

Check the Sentry dashboard after triggering each error. Verify that stack traces are properly symbolicated -- you should see your original source file names and line numbers, not minified code.

Tip: The native crash test only works in development builds, not in Expo Go. If pressing the button does nothing, you are likely running in Expo Go.

Troubleshooting

Source Maps Show Minified Code

Cause: Source maps were not uploaded or the release/dist values do not match between the app and the uploaded maps.

Fix: Use automatic release naming (the default). If you set custom release or dist values in Sentry.init(), the automatic upload script cannot detect them. Either stick with defaults or manually upload source maps with matching release/dist values.

Cause: registerNavigationContainer was never called with the navigation ref.

Fix: Make sure useNavigationContainerRef() is called in the same component where Sentry is initialized, and register the ref in a useEffect.

Duplicate Error Reports in Development

Cause: React's development mode rethrows errors caught by error boundaries to the global handler.

Fix: Test error boundary behavior in production builds. You can also disable Sentry in development with enabled: !__DEV__ if the noise is distracting.

sentry-expo Package Errors

Cause: The sentry-expo package was deprecated with the release of Expo SDK 50 and is no longer maintained.

Fix: Migrate to @sentry/react-native directly. Sentry provides a migration guide in their documentation.

High Event Volume Burning Through Quota

Cause: Sample rates set too high for production.

Fix: Use conservative rates: tracesSampleRate: 0.1-0.2, replaysSessionSampleRate: 0.1. Keep replaysOnErrorSampleRate: 1.0 since error replays are the most valuable.

Version Compatibility

ComponentMinimum VersionRecommended
@sentry/react-native7.x8.x (latest)
Expo SDK5052+
React Native0.73+0.76+
iOS Deployment Target15.0 (for v8)15.0+
Android Gradle Plugin7.4.0 (for v8)8.x

Note: Version 8 updates native SDKs (Cocoa v9, CLI v3, Android Gradle Plugin v6) with breaking changes in minimum version requirements. Check the release notes before upgrading.

Key Takeaways

  1. Use the wizard -- npx @sentry/wizard@latest -i reactNative handles most setup automatically.
  2. Development builds are essential -- Expo Go only captures JavaScript errors. Native crashes, frame tracking, and session replay require a development build.
  3. Secure your auth token -- Never put SENTRY_AUTH_TOKEN in app.json. Use EAS secrets or environment variables.
  4. EAS Update needs manual source map upload -- Unlike EAS Build, OTA updates do not automatically upload source maps. Automate sentry-expo-upload-sourcemaps in your CI pipeline.
  5. Use conservative sample rates in production -- 10-20% for traces, 10% for session replays, 100% for error replays.
  6. Tag OTA updates -- Use expo-updates metadata on Sentry scope so you can filter errors by update version.

References

Related Posts