ObserverKit

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

terminal
pnpm add @observerkit/react-native

Initialize

Call init once, as early as possible, before AppRegistry.registerComponent. Unhandled JS errors and promise rejections are captured automatically.

index.js
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.

settings-screen.tsx
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

index.js
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.

index.js
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 42

The 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

terminal
pnpm add -D @observerkit/metro

On 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

metro.config.js
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.

app.json
{
  "expo": {
    "plugins": [
      ["@observerkit/metro/expo", { "projectKey": "ok_proj_..." }]
    ]
  }
}

For EAS Update bundles, upload after export:

npx expo export && npx observerkit-upload --dir dist

Bare 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"
fi

Bare React Native — Android

Append to android/app/build.gradle.

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")
  }}
/>