Error tracking for React Native and Expo apps. Capture exceptions with full stack traces, plus the last ~10 seconds of console logs, HTTP calls, navigation transitions, and custom breadcrumbs — automatically.
Traceway is a completely open-source error tracking platform. You can self-host it or use Traceway Cloud.
This is the React Native counterpart to @tracewayapp/react (web) and com.tracewayapp:traceway (native Android). The wire format is identical, so the same Traceway backend ingests reports from all three.
No screen recording. Unlike the browser SDK (
@tracewayapp/frontend), this package does not record the screen. Therrwebrecorder is intentionally absent — nothing in the bundle reaches into a DOM that doesn't exist.
- Automatic capture of every uncaught throw via React Native's
ErrorUtilsglobal handler - Automatic capture of unhandled promise rejections
- Full Hermes / JavaScriptCore stack traces, normalized to Traceway's wire format
- Logs — every
console.{debug,log,info,warn,error}line from the last ~10 seconds - Actions — every
fetchandXMLHttpRequestcall, plus navigation transitions you wire in, plus custom breadcrumbs - React surface:
<TracewayProvider>(also acts as an error boundary),useTraceway()hook - Debounced, retrying batch transport over plain
fetch(no native modules, works in Expo Go) - Simple one-line setup
npm install @tracewayapp/react-nativeThe package is plain JavaScript — no native modules, no pod install, no Gradle changes. It works in Expo Go, in bare React Native CLI projects, and in EAS builds.
Wrap your app in TracewayProvider from your entry component (typically App.tsx). Mounting the provider runs init(...) once, which installs the ErrorUtils global handler, the fetch / XHR wrappers, and the console mirror. The provider also acts as an error boundary — render-time exceptions are captured and re-thrown automatically.
import { TracewayProvider } from "@tracewayapp/react-native";
export default function App() {
return (
<TracewayProvider connectionString="your-token@https://your-traceway-instance.com/api/report">
<RootNavigator />
</TracewayProvider>
);
}That's it. Every uncaught throw, unhandled promise rejection, render-time exception, and fetch call is captured automatically. TracewayErrorBoundary is still exported (deprecated) if you need a custom fallback UI for a specific subtree; it will be removed in v2.
import { useTraceway } from "@tracewayapp/react-native";
import { captureException, captureMessage, flush } from "@tracewayapp/react-native";
// Inside a component — get scoped helpers from the hook:
function CheckoutButton() {
const { captureException, recordAction } = useTraceway();
// ...
}
// Or import directly anywhere (after init has run):
try {
riskyOperation();
} catch (e) {
captureException(e as Error);
}
captureMessage("User completed checkout");
// Force send pending events (e.g. before app goes to background):
await flush();All field names mirror the browser and Android SDKs so existing config can be ported as-is.
| Option | Default | Description |
|---|---|---|
version |
"" |
App version string, attached to every report |
debug |
false |
Print debug info to the console |
debounceMs |
1500 |
Milliseconds before flushing batched events |
retryDelayMs |
10000 |
Retry delay on failed uploads |
ignoreErrors |
sensible defaults | Array of string | RegExp patterns; matching exceptions are dropped |
beforeCapture |
none | (exception) => boolean — return false to drop |
captureLogs |
true |
Mirror every console.* call into the rolling log buffer |
captureNetwork |
true |
Record fetch and XMLHttpRequest calls as network actions |
captureNavigation |
true |
Record manual recordNavigation() calls |
captureDeviceInfo |
true |
Auto-collect os.name, os.version, screen, locale, runtime engine and attach to every report |
eventsWindowMs |
10000 |
Rolling window kept in the log/action buffers (ms) |
eventsMaxCount |
200 |
Hard cap applied independently to logs and actions |
The DEFAULT_IGNORE_PATTERNS export contains the built-in ignoreErrors list (Network request failed, Failed to fetch, generic 4xx, etc.) — you can extend it rather than replace it.
Every captured exception ships with the last ~10 seconds of session context, attached to the same sessionRecordings[] entry the other Traceway SDKs use:
- Logs — every
console.{debug,log,info,warn,error}call. The originalconsoleoutput is preserved; we only piggyback on the call. - Actions are split into three channels:
- Network — every
fetchandXMLHttpRequestcall (method, URL, status, duration, byte counts). Calls to the Traceway endpoint itself are skipped to avoid recursion. - Navigation — push transitions you record manually (see below).
- Custom — anything you call
recordAction(...)with.
- Network — every
Logs and actions are kept in two separate rolling buffers, each capped at 200 entries / 10 seconds. They ship inside sessionRecordings[].logs and sessionRecordings[].actions on the wire, with startedAt / endedAt ISO 8601 timestamps spanning the captured window.
React Native has no window.history to auto-instrument. Wire recordNavigation into whatever navigation library you use:
import { useNavigationContainerRef } from "@react-navigation/native";
import { recordNavigation } from "@tracewayapp/react-native";
const navigationRef = useNavigationContainerRef();
const prevRoute = useRef<string | null>(null);
return (
<NavigationContainer
ref={navigationRef}
onStateChange={() => {
const next = navigationRef.getCurrentRoute()?.name ?? "unknown";
recordNavigation(prevRoute.current ?? "(initial)", next);
prevRoute.current = next;
}}
>
{/* ... */}
</NavigationContainer>
);For Expo Router drop the same call into a useEffect keyed on usePathname().
import { recordAction } from "@tracewayapp/react-native";
recordAction("cart", "add_item", { sku: "SKU-123", qty: 2 });Each channel can be turned off individually via TracewayProvider's options:
<TracewayProvider
connectionString={DSN}
options={{
captureLogs: false,
captureNetwork: false,
captureNavigation: false,
}}
>Every captured exception ships with an attributes map composed of three layers (per-call wins, then global scope, then device info):
Auto-stamped at init() from React Native core APIs (no native modules required, works in Expo Go). The keys mirror what the Flutter and Android SDKs emit so the dashboard renders them consistently across platforms:
| Key | Source |
|---|---|
os.name |
Platform.OS (ios / android / web) |
os.version |
Platform.Version |
screen.resolution |
Dimensions.get("screen") (whole pixels) |
screen.density |
PixelRatio.get() |
device.locale |
Intl.DateTimeFormat().resolvedOptions().locale |
runtime.engine |
hermes / javascriptcore |
To replace or extend the auto-collected map at runtime (e.g. add info from expo-device / react-native-device-info you've installed yourself):
import { setDeviceAttributes, collectSyncDeviceInfo } from "@tracewayapp/react-native";
import * as Device from "expo-device";
setDeviceAttributes({
...collectSyncDeviceInfo(), // keep the auto-collected ones
"device.model": Device.modelName ?? "",
"device.manufacturer": Device.manufacturer ?? "",
});To opt out of device info collection entirely, pass captureDeviceInfo: false in options.
Use the global scope for things tied to the user / tenant / app state, not the device: user id, organization id, build channel, feature flags, A/B bucket. Two ways to set it.
Declarative — <TracewayAttributes> or the useTracewayAttributes hook. Pass a map; the SDK diffs against the previous map and pushes only the deltas. On unmount, every key the component owned is removed.
import { TracewayAttributes, useTracewayAttributes } from "@tracewayapp/react-native";
// As a component — drop in wherever you have user/tenant context:
<TracewayAttributes attributes={user ? { userId: user.id, tenant: org.id } : null} />
// Or as a hook:
function App() {
useTracewayAttributes({ build_channel: "canary" });
return <RootNavigator />;
}The hook accepts null / undefined as "empty map" — useful while user data loads or after logout.
Imperative — setAttribute / setAttributes / removeAttribute / clearAttributes. Use these outside React component trees (auth listeners, background tasks):
import {
setAttribute,
setAttributes,
clearAttributes,
} from "@tracewayapp/react-native";
setAttribute("userId", "u_42");
setAttributes({ tenant: "acme", plan: "pro" });
// ...on logout:
clearAttributes();captureExceptionWithAttributes(error, attrs) — wins on key collision over both layers above.
For each captured exception: device info < global scope < per-call. The same precedence applies to sessions when always-on session recording is on.
- Uncaught throws on the JS thread via
ErrorUtils.setGlobalHandler— this is RN's equivalent ofwindow.onerror. RN's red-box dev overlay still appears; we forward to the previous handler. - Unhandled promise rejections —
Promise.reject(...)without a.catch()reaches the sameErrorUtilshandler. fetchcalls — wrapped at install time. Method, URL, status, duration, byte counts.XMLHttpRequestcalls — RN polyfills XHR (andfetchis implemented on top of it on some platforms); both paths are covered.- Render errors —
<TracewayProvider>itself acts as an error boundary: exceptions thrown during render or in lifecycle methods are captured and re-thrown so the app behaves exactly as without Traceway. - Console output —
console.{debug,log,info,warn,error}is mirrored into the log buffer that rides along the next exception.
| Platform | Error Tracking | Screen Recording |
|---|---|---|
| iOS (RN ≥ 0.72) | Yes | No |
| Android (RN ≥ 0.72) | Yes | No |
| Expo (SDK 49+) | Yes — works in Expo Go | No |
| Web (via React Native Web) | Yes | No |
For native Android-only apps without React Native, use the com.tracewayapp:traceway library. For browser apps, use @tracewayapp/frontend or @tracewayapp/react.
The examples/expo-demo directory is a tiny throwaway Expo app with buttons that exercise every capture path (caught throw, uncaught throw, render error, fetch error, custom action).
To point it at your own Traceway backend:
cd packages/react-native/examples/expo-demo
npm install
TRACEWAY_DSN="YOUR_TOKEN@https://your-traceway-instance.com/api/report" npx expo startThen open it in Expo Go (scan the QR code), or press i / a for iOS simulator / Android emulator.
If TRACEWAY_DSN is missing, the app falls back to a placeholder DSN that won't reach any real server — the buttons still fire so you can verify the UI.
JVM-fast Vitest unit tests cover the wire-format, the rolling event buffer, ignore-pattern handling, transport, retry logic, and stack-trace formatting against fake Hermes / JavaScriptCore stacks.
npm test --workspace=@tracewayapp/react-nativeReleases are cut through the monorepo's publish.sh — it bumps every @tracewayapp/* package in lockstep, syncs cross-package dependency references, builds, and publishes. From the repo root:
./publish.shThe script prompts for a new semver and confirms before pushing to npm. If publish fails, no commit or tag is created — fix the issue and re-run.
MIT