React Native
Quick Start

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-native

Setup

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. The rrweb recorder 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.

OptionTypeDefaultDescription
versionstring""App version string, attached to every report
debugbooleanfalsePrint debug info to the console
debounceMsnumber1500Milliseconds before flushing batched events
retryDelayMsnumber10000Retry delay on failed uploads
ignoreErrorsArray<string | RegExp>sensible defaultsDrop exceptions whose stack/message match any pattern
beforeCapture(exception) => booleannoneReturn false to suppress an exception
captureLogsbooleantrueMirror console.{debug,log,info,warn,error} into the rolling log buffer
captureNetworkbooleantrueRecord fetch / XMLHttpRequest calls as network actions
captureNavigationbooleantrueRecord manual recordNavigation() calls into the action buffer
captureDeviceInfobooleantrueAuto-collect device.locale and runtime.engine and attach them to every report (see Attributes)
eventsWindowMsnumber10000Rolling window kept in the log/action buffers (ms)
eventsMaxCountnumber200Hard cap applied independently to logs and actions
distributedTraceHostsArray<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
captureHttpServerErrorsbooleanfalseAlso 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 fetch and XMLHttpRequest call (method, URL, status, duration, byte counts) is recorded automatically. RN polyfills XHR (and fetch is 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.history to auto-instrument. Wire recordNavigation into 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 useEffect keyed on usePathname().

  • 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:

KeySource
device.localeIntl.DateTimeFormat().resolvedOptions().locale
runtime.enginehermes / 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 of window.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 through ErrorUtils, so the SDK never sees them. Catch them yourself and call captureException.
  • 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.
  • fetch calls: wrapped at init(), recorded with method, URL, status, duration, byte counts.
  • XMLHttpRequest calls: RN polyfills XHR (and fetch is 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.map

iOS. 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/release

See 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

PlatformError TrackingScreen Recording
iOS (RN ≥ 0.72)YesNo
Android (RN ≥ 0.72)YesNo
Expo (SDK 49+)Yes (works in Expo Go)No
Web (via React Native Web)YesNo

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.