React Native Quick Start
Integrate Traceway into your React Native or Expo application with the @tracewayapp/react-native (opens in a new tab) package. Plain JavaScript (no native modules, no pod install, no Gradle changes) that works in Expo Go, in bare React Native CLI projects, and in EAS builds.
Installation
npm install @tracewayapp/react-nativeSetup
Wrap your app in TracewayProvider from your entry component (typically App.tsx). Mounting the provider runs init(...) once, which installs RN's ErrorUtils global handler, the fetch / XMLHttpRequest wrappers, and the console.* mirror. The provider also catches render-time errors anywhere below it and reports them automatically, so no separate error boundary is needed:
import { TracewayProvider } from "@tracewayapp/react-native";
export default function App() {
return (
<TracewayProvider connectionString="your-token@https://traceway.example.com/api/report">
<RootNavigator />
</TracewayProvider>
);
}That's it. Every uncaught throw, render-time exception, and fetch call is captured automatically. If you want to replace a crashed subtree with a custom fallback view, TracewayErrorBoundary is still exported for that purpose, but it is deprecated as of v1.1.0 and will be removed in v2. Do not build new code around it.
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.
Capture Errors Manually
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 the provider has mounted):
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();With Options
All field names mirror the browser, Android, and Flutter SDKs so existing config can be ported as-is.
| Option | Type | Default | Description |
|---|---|---|---|
version | string | "" | App version string, attached to every report |
debug | boolean | false | Print debug info to the console |
debounceMs | number | 1500 | Milliseconds before flushing batched events |
retryDelayMs | number | 10000 | Retry delay on failed uploads |
ignoreErrors | Array<string | RegExp> | sensible defaults | Drop exceptions whose stack/message match any pattern |
beforeCapture | (exception) => boolean | none | Return false to suppress an exception |
captureLogs | boolean | true | Mirror console.{debug,log,info,warn,error} into the rolling log buffer |
captureNetwork | boolean | true | Record fetch / XMLHttpRequest calls as network actions |
captureNavigation | boolean | true | Record manual recordNavigation() calls into the action buffer |
captureDeviceInfo | boolean | true | Auto-collect device.locale and runtime.engine and attach them to every report (see Attributes) |
eventsWindowMs | number | 10000 | Rolling window kept in the log/action buffers (ms) |
eventsMaxCount | number | 200 | Hard cap applied independently to logs and actions |
distributedTraceHosts | Array<string | RegExp> | [] | Hosts that receive an outgoing traceway-trace-id header, so a server-side capture links back to the client request that caused it. React Native has no window.location.origin to infer from, so unlike the browser SDK this is opt-in: list your own backend hosts, e.g. ["api.example.com"]. Each entry is an exact hostname or a RegExp tested against the URL host |
captureHttpServerErrors | boolean | false | Also report every fetch / XHR response with status >= 500 as a synthetic exception, on top of the network action already recorded. 4xx is deliberately excluded (see DEFAULT_IGNORE_PATTERNS) |
<TracewayProvider
connectionString="your-token@https://traceway.example.com/api/report"
options={{
debug: true,
version: "2.1.0",
debounceMs: 2000,
captureNavigation: true,
}}
>The DEFAULT_IGNORE_PATTERNS export contains the built-in ignoreErrors list (Network request failed, Failed to fetch, generic 4xx, etc.), so you can extend it rather than replace it.
Distributed Tracing
By default the SDK injects nothing into outgoing requests. List your backend hosts in distributedTraceHosts to turn it on:
<TracewayProvider
connectionString="your-token@https://traceway.example.com/api/report"
options={{ distributedTraceHosts: ["api.example.com", /\.internal\.example\.com$/] }}
>
<RootNavigator />
</TracewayProvider>Each matching fetch call gets a traceway-trace-id header carrying a fresh id. If your backend reports an error while handling that request, both sides carry the same id and you can jump from the mobile exception to the server one. Entries are matched against the URL's host, so include the port when the URL has one ("localhost:3000"). XMLHttpRequest calls are still recorded as network actions, but they do not get the header, so route traffic you want linked through fetch.
getActiveDistributedTraceId() returns the id of the instrumented fetch currently in flight, and null when there is none. Use it to put that same id on work you kick off from inside such a request (a WebSocket handshake, a native module, a client the SDK does not wrap):
import { DISTRIBUTED_TRACE_HEADER, getActiveDistributedTraceId } from "@tracewayapp/react-native";
const traceId = getActiveDistributedTraceId();
if (traceId) {
headers[DISTRIBUTED_TRACE_HEADER] = traceId;
}Logs & Actions
Every captured exception ships with the last ~10 seconds of session context, attached to a sessionRecordings[] entry on the wire, the same shape every other Traceway SDK uses. Two independent rolling buffers (logs and actions) are kept in memory, each capped at 200 entries by default.
Logs
Every console.{debug, log, info, warn, error} call is mirrored into the log buffer. The original console output is preserved; the SDK only piggybacks on the call.
Disable with captureLogs: false.
Actions
Three kinds of actions are collected:
-
Network: every
fetchandXMLHttpRequestcall (method, URL, status, duration, byte counts) is recorded automatically. RN polyfills XHR (andfetchis implemented on top of it on some platforms), so both paths are covered. Calls to the Traceway endpoint itself are skipped to avoid recursion. -
Navigation: RN has no
window.historyto auto-instrument. WirerecordNavigationinto whatever navigation library you use:import { useRef } from "react"; import { NavigationContainer, 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 (opens in a new tab) drop the same call into a
useEffectkeyed onusePathname(). -
Custom: anything you record explicitly:
import { recordAction } from "@tracewayapp/react-native"; recordAction("cart", "add_item", { sku: "SKU-123", qty: 2 });
Wire shape
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, the same shape the Flutter and Android SDKs use:
{
"sessionRecordings": [
{
"exceptionId": "...",
"startedAt": "2026-04-28T14:30:53.011Z",
"endedAt": "2026-04-28T14:31:02.314Z",
"logs": [
{"type": "log", "timestamp": "2026-04-28T14:30:54.508Z", "level": "info", "message": "user tapped pay"}
],
"actions": [
{"type": "navigation", "action": "push", "from": "Home", "to": "Cart", "timestamp": "2026-04-28T14:30:53.011Z"},
{"type": "network", "method": "GET", "url": "...", "statusCode": 200, "durationMs": 86, "timestamp": "2026-04-28T14:30:54.422Z"},
{"type": "custom", "category": "cart", "name": "add_item", "data": {"sku": "SKU-1"}, "timestamp": "2026-04-28T14:30:54.605Z"}
]
}
]
}Disabling channels
Each channel can be turned off individually via TracewayProvider's options:
<TracewayProvider
connectionString={DSN}
options={{
captureLogs: false,
captureNetwork: false,
captureNavigation: false,
}}
>Attributes
Every captured exception ships with an attributes map composed of three layers (per-call wins, then global scope, then device info).
1. Device info (auto-collected)
init() stamps these two keys on every report. They need no native module and work in Expo Go:
| Key | Source |
|---|---|
device.locale | Intl.DateTimeFormat().resolvedOptions().locale |
runtime.engine | hermes / javascriptcore |
collectSyncDeviceInfo() also knows how to read os.name, os.version, screen.resolution and screen.density from Platform, Dimensions and PixelRatio, but it reaches react-native through a global require. Metro does not define one (it registers modules on global.__r), so in a real app those four keys come back empty. Pass them in yourself if you want them on the dashboard. The key names mirror what the Flutter and Android SDKs emit, so the dashboard renders them consistently across platforms:
import { setDeviceAttributes, collectSyncDeviceInfo } from "@tracewayapp/react-native";
import { Dimensions, PixelRatio, Platform } from "react-native";
const screen = Dimensions.get("screen");
setDeviceAttributes({
...collectSyncDeviceInfo(),
"os.name": Platform.OS,
"os.version": String(Platform.Version),
"screen.resolution": `${Math.round(screen.width)}x${Math.round(screen.height)}`,
"screen.density": PixelRatio.get().toFixed(1),
});Use the same call to 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(),
"device.model": Device.modelName ?? "",
"device.manufacturer": Device.manufacturer ?? "",
});To opt out of device info collection entirely, pass captureDeviceInfo: false in options.
2. Global scope (app-level identifiers)
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:
<TracewayAttributes attributes={user ? { userId: user.id, tenant: org.id } : null} />
// Or as a hook:
function App() {
useTracewayAttributes({ build_channel: "canary" });
return <RootNavigator />;
}Both accept null / undefined as "empty map", which is 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();3. Per-call attributes
captureExceptionWithAttributes(error, attrs) wins on key collision over both layers above.
Layering
For each captured exception: device info < global scope < per-call.
What Gets Captured Automatically
- Uncaught throws on the JS thread via
ErrorUtils.setGlobalHandler, RN's equivalent ofwindow.onerror. The red-box dev overlay still appears; we forward to the previous handler. - Unhandled promise rejections are not captured. React Native hands them to its own
ExceptionsManager(and only in dev builds), never throughErrorUtils, so the SDK never sees them. Catch them yourself and callcaptureException. - Render errors:
<TracewayProvider>itself acts as an error boundary. Exceptions thrown during render or in lifecycle methods are captured and re-thrown so your app behaves exactly as without Traceway. fetchcalls: wrapped atinit(), recorded with method, URL, status, duration, byte counts.XMLHttpRequestcalls: RN polyfills XHR (andfetchis implemented on top of it on some platforms); both paths are covered.- Console output:
console.{debug,log,info,warn,error}is mirrored into the log buffer that rides along the next exception.
Release Symbolication (source map upload)
A debug build gives you readable stack traces for free. A release build does not: Metro minifies the JavaScript into one bundle, and Hermes compiles that bundle to bytecode. Every production frame lands on the Issues page as an offset into index.android.bundle or main.jsbundle, with no function names and no original file. Upload the build's source map and Traceway resolves those frames server-side.
Do this on every release. A crash only resolves against the map for the exact build it came from.
Get the upload token
Symbol uploads use a dedicated upload token, not the project token in your connection string. Open the Connection page for your project, scroll to Source Maps, and click Generate Upload Token. Treat it like any other CI secret. Members with the readonly role cannot generate one.
Emit the source map
Android emits the map already. iOS needs one extra line.
Android. The React Native Gradle plugin passes -output-source-map to Hermes by default, so a release build writes the map with no config change. Only if your android/app/build.gradle overrides hermesFlags do you need to keep the flag in the list:
react {
hermesFlags = ["-O", "-output-source-map"]
}After ./gradlew assembleRelease the map is at:
android/app/build/generated/sourcemaps/react/release/index.android.bundle.mapiOS. In Xcode, open your target's Build Phases, find Bundle React Native code and images, and add this line to the script:
export SOURCEMAP_FILE="$(pwd)/../main.jsbundle.map"After an archive build the map is at ios/main.jsbundle.map.
Both paths moved between React Native versions. If yours is not there, search the build output for *.bundle.map or main.jsbundle.map.
Upload it
Traceway matches a map to a frame by filename: the frame says index.android.bundle, so the uploaded file must be named index.android.bundle.map. curl -F sends the basename of the local path, so keep the filename as the build produced it.
export TRACEWAY_URL="https://traceway.example.com"
export TRACEWAY_SOURCEMAP_TOKEN="your-upload-token"
curl --fail -X POST "$TRACEWAY_URL/api/sourcemaps/upload" \
-H "Authorization: Bearer $TRACEWAY_SOURCEMAP_TOKEN" \
-F "files=@android/app/build/generated/sourcemaps/react/release/index.android.bundle.map"A 200 with {"uploaded":1} means it landed. Uploads take effect immediately, so exceptions arriving right after a deploy resolve against the new map.
The traceway-sourcemaps CLI does the same thing and reads the two variables above. Point it at the directory holding the map:
npm install -D @tracewayapp/sourcemap-upload
npx traceway-sourcemaps --directory android/app/build/generated/sourcemaps/react/releaseSee JavaScript symbolication for how a frame is matched to its map.
Test Your Integration
Add a button that throws an exception to verify everything is wired up:
import { Button } from "react-native";
import { useTraceway } from "@tracewayapp/react-native";
function TestButton() {
const { captureException } = useTraceway();
return (
<Button
title="Send test error"
onPress={() => {
try {
throw new Error("Test error from Traceway");
} catch (e) {
captureException(e as Error);
}
}}
/>
);
}Tap the button and check your Traceway dashboard to verify the error appears with its full stack trace.
Platform Support
| 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 Android SDK. For Flutter apps, use the Flutter SDK. For browser apps, use the React SDK or generic JS SDK.