React Native
Track errors in your React Native or Expo app in a few minutes.
Using an AI coding agent? Have it fetch the install prompt at observerkit.com/docs/react-native-install-prompt.md.
Install the package
pnpm add @observerkit/react-nativeInitialize
Call init once, as early as possible, before AppRegistry.registerComponent. Unhandled JS errors and promise rejections are captured automatically.
import { AppRegistry } from "react-native"
import { init } from "@observerkit/react-native"
import App from "./App"
import { name as appName } from "./app.json"
init({
projectKey: "ok_proj_...",
environment: "production",
})
AppRegistry.registerComponent(appName, () => App)Using Expo Router? There is no index.js with AppRegistry — call init at the very top of your root app/_layout.tsx instead, above every other import you own.
Options
projectKeystringRequired. Your project key.autoCapturebooleanInstall global error and rejection handlers automatically. Defaults to true.environmentstringEnvironment name, e.g. "production" or "staging".endpointURLCustom ingest API URL.Device context (os, osVersion, devBuild) is attached to every event under metadata.device.
Capture errors manually
Errors are captured automatically by default. You can also report handled errors yourself with additional metadata.
import { captureException } from "@observerkit/react-native"
try {
await syncData()
} catch (error) {
captureException(error, {
metadata: { screen: "Settings" },
})
}Reliable crash delivery
A crash right before the app closes can happen too fast for the network request to finish. Provide an eventStore and ObserverKit persists fatal errors to disk first, then re-sends anything still pending automatically the next time the app launches.
Default: AsyncStorage
import AsyncStorage from "@react-native-async-storage/async-storage"
import { init, AsyncStorageStore } from "@observerkit/react-native"
init({
projectKey: "ok_proj_...",
eventStore: new AsyncStorageStore(AsyncStorage),
})Higher reliability: MMKV
react-native-mmkv writes synchronously and survives crashes that can interrupt an async write. It needs a native build, so it is not available in Expo Go.
import { MMKV } from "react-native-mmkv"
import { init, MmkvStore } from "@observerkit/react-native"
const storage = new MMKV()
init({ projectKey: "ok_proj_...", eventStore: new MmkvStore(storage) })Without an eventStore, the SDK behaves as before: a best-effort live send with no persistence or retry across launches.
Native crash reporting (Android)
When the app is built with the native module linked (bare React Native, or Expo with a development build / prebuild, not Expo Go), ObserverKit also captures errors below the JS layer:
- JVM crashes: uncaught Java/Kotlin exceptions from native modules or React Native itself. Reports are written to disk at crash time and delivered on the next launch.
- ANRs: "Application Not Responding" freezes, reported by Android on the next launch. Requires Android 11 or newer.
Native crash reporting is on by default and needs no extra setup. To disable it, pass nativeCrashReporting: false to init. It also requires autoCapture to be enabled, so setting autoCapture: false disables native crash reporting too.
Not yet captured: iOS native crashes, C/C++ (NDK) crashes, and crashes that happen before init() runs.
R8/ProGuard mapping upload
Release builds run through R8/ProGuard, so raw stack traces are obfuscated. When the Expo config plugin (@observerkit/metro/expo) is applied, the mapping upload is automatic: a Gradle hook runs after the R8 minify task and uploads mapping.txt, keyed by versionName and versionCode.
Bare React Native (no Expo plugin) uploads the mapping with the CLI:
observerkit-upload --mapping android/app/build/outputs/mapping/release/mapping.txt --version-name 1.2.0 --version-code 42The server retraces JVM crash and ANR frames with the uploaded mapping, so frames deobfuscate and grouping stays stable across releases. Rebuilding the same versionName and versionCode overwrites the stored mapping, so re-upload happens naturally on every release build. Version values come from android.defaultConfig; apps that override the version per flavor should invoke the CLI themselves.
Source maps
Without source maps, production stack traces point at minified, bundled code. The @observerkit/metro plugin injects debug IDs during the build and uploads source maps so traces resolve to your original files. Hermes only — JSC is not supported.
1. Install the Metro plugin
pnpm add -D @observerkit/metroOn pnpm, if Metro is not already a direct dependency, add it too (pnpm add -D metro) so the plugin can load Metro's default serializer.
2. Wrap your Metro config
const { getDefaultConfig } = require("@react-native/metro-config")
const withObserverkit = require("@observerkit/metro")
module.exports = withObserverkit(getDefaultConfig(__dirname))Debug-id injection only runs for release bundles; dev builds are untouched.
3. Upload source maps from your build
The upload step depends on how you build. Expo wires both native platforms automatically; bare React Native needs a snippet per platform.
Expo
Add the config plugin. expo prebuild wires both native builds automatically.
{
"expo": {
"plugins": [
["@observerkit/metro/expo", { "projectKey": "ok_proj_..." }]
]
}
}For EAS Update bundles, upload after export:
npx expo export && npx observerkit-upload --dir distBare React Native — iOS
Edit the "Bundle React Native code and images" build phase in Xcode so a source map is emitted and uploaded.
export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map"
set -e
WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh"
/bin/sh -c "$WITH_ENVIRONMENT $REACT_NATIVE_XCODE"
if [ -f "$SOURCEMAP_FILE" ]; then
npx observerkit-upload --map "$SOURCEMAP_FILE" --bundle "$CONFIGURATION_BUILD_DIR/main.jsbundle" --project-key "ok_proj_..." || echo "[ObserverKit] source map upload failed"
fiBare React Native — Android
Append to android/app/build.gradle.
tasks.configureEach { task ->
def matcher = task.name =~ /^createBundle(\w*)ReleaseJsAndAssets$/
if (matcher.matches()) {
task.doLast {
def flavor = matcher.group(1)
def variant = flavor.isEmpty()
? "release"
: flavor.substring(0, 1).toLowerCase() + flavor.substring(1) + "Release"
def mapFile = file("$buildDir/generated/sourcemaps/react/" + variant + "/index.android.bundle.map")
def packagerMapFile = file("$buildDir/intermediates/sourcemaps/react/" + variant + "/index.android.bundle.packager.map")
if (mapFile.exists()) {
def result = exec {
workingDir rootProject.projectDir.parentFile
environment "OBSERVERKIT_PROJECT_KEY", "ok_proj_..."
commandLine "npx", "observerkit-upload", "--map", mapFile.absolutePath, "--packager-map", packagerMapFile.absolutePath
ignoreExitValue true
}
if (result.exitValue != 0) {
logger.warn("[ObserverKit] source map upload failed")
}
}
}
}
}Verify
Trigger a test error in your app. If everything is set up correctly, it will appear in your dashboard within a few seconds — with a readable stack trace after a release build.
<Button
title="Throw test error"
onPress={() => {
throw new Error("test error")
}}
/>