React Native

React Native SDK: JS exception capture wired through a TurboModule into the native Android and iOS BugWatch SDKs, so one package gives you JS error tracking plus native crash, ANR and hang coverage.

Fetching data with TanStack Query? Add @newinstance/bugwatch-tanstack on top of this and every failed query and mutation is reported automatically, with a breadcrumb trail. See Install an SDK -> JavaScript & TypeScript -> TanStack Query. Spans are web only, since this SDK has no span API.

Requirementslink

  • React Native 0.74+ (new architecture), Android minSdk 24 with JDK 17, iOS 15.1+
  • Credentials come from the dashboard project's Settings under Reveal mobile credentials; tokens are signed per batch with a 5 minute expiry and a nonce

1 - Installlink

npm install @newinstance/bugwatch-react-native

iOS: the Podfile target must use static frameworks or the build fails with BugWatchReactNative-Swift.h file not found:

use_frameworks! :linkage => :static

Then cd ios && pod install. Disable Flipper if it is still present (incompatible with use_frameworks!). Android needs only mavenCentral() in settings.gradle; autolinking pulls cloud.newinstance:bugwatch:0.1.2.

Expo (SDK 47+, verified through 57): add the config plugin and regenerate native projects:

"plugins": [["@newinstance/bugwatch-react-native", { "useFrameworks": "static" }]]
npx expo prebuild --clean
npx expo run:ios

The plugin writes ios.useFrameworks: "static" into Podfile properties and throws if another plugin sets a conflicting value. Expo Go is not supported; build a dev client.

2 - Initialise (before your root component renders)link

import { BugWatch, Severity } from '@newinstance/bugwatch-react-native';

BugWatch.init({
  projectId: Config.BUGWATCH_PROJECT_ID,
  appSecret: Config.BUGWATCH_APP_SECRET,
  environment: __DEV__ ? 'development' : 'production',
  release: '1.4.2+318',
});

3 - Configurationlink

OptionDefaultWhat it does
enabledtrueMaster switch; when false, capture short-circuits in JS before the bridge
endpointhttps://api.newinstance.cloudOverride for dev or self-hosted backends
environment / release-Labels; release must equal your source-map upload string
enableAutoCapturetrueGlobal JS handler + unhandled promise rejections
sampleRate1.0Client-side sampling
sensitiveFieldsbuilt-in listExtra redaction keys
batchSize / flushIntervalMs / maxQueueSize50 / 5000 / 1000Delivery and offline queue (native side)
requestTimeoutMs15000Per-request timeout
retry{ maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8000 }Exponential backoff
debugfalseLogs [BugWatch] started (env=..., release=...) and delivery diagnostics to Metro

4 - Capture APIlink

const id = BugWatch.captureException(err);
BugWatch.captureMessage('Sync finished', Severity.Info);
BugWatch.setUser({ id: 'u_123', email: 'ada@example.com' });
BugWatch.setUser(null);
BugWatch.setTag('tenant', 'acme');
BugWatch.setContext('payment.provider', 'paystack');
BugWatch.addBreadcrumb({ category: 'ui', message: 'Tapped checkout' });
BugWatch.setRelease('1.4.3+319');
await BugWatch.flush();
BugWatch.close();
  • Capture methods return a client-side bw_e_… id synchronously; the native layer assigns the delivered id.
  • Severity values are numeric 10 to 60; captureMessage defaults to Severity.Info.
  • setContext(key, value) takes string values; breadcrumbs take an object with a required category (type defaults to default, data is Record<string, string>; the native side keeps the last 100).
  • setUser keeps only id, email, username, ip; setUser(null) clears on sign-out.
  • close() tears down auto-capture and restores the previous ErrorUtils handler.

Automatic capturelink

  • JS errors via the ErrorUtils global handler plus unhandled promise rejection tracking; prior handlers are chained, not replaced. React Native's isFatal flag is preserved: an uncaught error that terminates the app is recorded at Severity.Fatal, an unhandled rejection (which does not terminate the app) at Severity.Error.
  • Stack parsing handles Hermes (fn@file:line:col) and JSC/V8 (at fn (file:line:col)); frames from node_modules are marked not-in-app.
  • In environments without ErrorUtils or a native module (Jest, web), auto-capture degrades to a no-op instead of throwing.
  • Native crashes, ANRs and hangs, sessions, lifecycle and network breadcrumbs, and the persistent offline queue come from the underlying native SDKs.

A fatal JS error produces two eventslink

This is by design and both records are useful.

  1. From the JS hook - platform: "react-native", level: fatal, carrying parsed JS frames. This is the readable one, and the only one the backend resolves against your uploaded source map.
  2. From the native crash handler - React Native deliberately terminates the process after a fatal JS error (JavascriptException on Android, RCTFatalException on iOS). The native SDK catches that as an ordinary native crash, so you also get a platform: "android" / platform: "ios" fatal whose JS stack rides along as unresolved text inside the exception message. On iOS React Native truncates that message to 175 characters.

The second record is what keeps release health honest: it sets crashedLastRun, which marks the previous session crashed rather than exited. Both reach the dashboard as separate issues; there is no cross-record de-duplication.

Two React Native configurations bypass the JS hooklink

In both cases the app still crashes and the native handler still records a fatal event. You lose only the readable, source-map-resolvable JS record.

  • An error thrown before the JS runtime reports ready. React Native's JsErrorHandler only routes through ErrorUtils once the runtime is ready; before that it uses its internal C++ pipeline.
  • The useAlwaysAvailableJSErrorHandling feature flag enabled. It is off by default. When on, React Native handles JS errors entirely in C++ and never calls the ErrorUtils global handler.

Options that are native-onlylink

The native SDKs accept more options than the React Native facade forwards. These run at their native defaults and cannot be changed from JavaScript. To change them, initialise the native SDK directly from your Application subclass (Android) or AppDelegate / @main App (iOS): both natives return the already-running instance on a second call and native startup precedes the JS bundle, so your native options win and the later BugWatch.init() becomes a no-op for configuration. Keep calling init() from JS regardless, since that is what installs the JavaScript error hooks.

Native optionPlatformNative default
autoSessionTrackingbothtrue
enableAutoBreadcrumbsbothtrue
enableNetworkBreadcrumbsbothtrue
networkBreadcrumbAllowedHosts / networkBreadcrumbDeniedHostsboth[]
enableAnrTracking / anrThresholdMsAndroidtrue / 5000
enableAppHangTracking / appHangThresholdMsiOStrue / 2000

retry is accepted by init() but is not currently forwarded to the native SDKs; both platforms use their own default policy (3 attempts, 200 ms initial delay, 5000 ms cap). Setting it has no effect today.

What is never capturedlink

Platform limits, not SDK limits.

Not capturedWhy
Out-of-memory kills, jetsam, watchdog terminationsThe OS sends SIGKILL, which no handler can intercept. The prior session finalises as exited, so crash-free rate reads slightly optimistic
The user force-quitting the appIndistinguishable from a clean exit
Anything before BugWatch.init() runsHandlers are armed by init(). Call it at module scope in your entry file, not inside a component or a useEffect
Automatic network breadcrumbs from fetch() on AndroidReact Native's fetch() uses its own OkHttp client, and the BugWatch interceptor is opt-in and native-side. iOS has no such gap: its URLProtocol covers default-configuration sessions
A stack overflow on a thread other than the one that started the SDKsigaltstack is registered per-thread

Delivery needs a relaunchlink

Nothing is uploaded from inside a crash handler. The handler writes an artifact to disk synchronously and the next launch uploads it. There is no background job that uploads after the process is gone, so a user who crashes and never returns is never counted. Queued events are delayed, never lost.

5 - Symbolication (CI)link

Hermes needs the composed source map:

npx react-native bundle --platform android --dev false \
  --entry-file index.js --bundle-output main.jsbundle --sourcemap-output main.jsbundle.map
node node_modules/react-native/scripts/compose-source-maps.js \
  main.jsbundle.map android/app/build/.../index.android.bundle.map -o composed.map
npx @newinstance/bugwatch-cli artifacts upload composed.map \
  --release "1.4.2+318" --platform react-native --type sourcemap --token "$BUGWATCH_CI_KEY"
  • The --release string must equal init.release exactly; re-uploading the same release, platform and type replaces the previous map.
  • One source map per release: symbolication consults a single map per release, so keep --platform react-native and upload one composed map per release. If iOS and Android need different maps, give each platform its own release string (for example 1.4.2+318-ios / 1.4.2+318-android) rather than uploading two maps under one release.
  • Expo maps land at dist/_expo/static/js/<platform>/index-*.hbc.map.
  • Also upload the Android R8 mapping and iOS dSYMs for the native layer (see Android and iOS & macOS).

Upgrade noteslink

  • 0.1.0 to 0.1.1: projectKey split into projectId + appSecret; run pod update BugWatch if you pinned the pod.
  • 0.1.2 added the Expo config plugin.
  • 0.1.3 to 0.1.6 are maintenance releases with no option or API surface changes.

Confirm the connectionlink

The native SDKs perform a handshake with BugWatch on start; from JavaScript run it on demand or read the last result:

const check = await BugWatch.testConnection();
// { status: 'connected' | 'rejected' | 'unreachable' | 'unknown', reason?, hint?, httpStatus?, serverTimeMs?, clockSkewMs?, checkedAt }
const last = await BugWatch.getConnectionState();

A rejected result carries the server's exact reason (signature_invalid, project_unavailable, mobile_ingest_disabled, token_expired, clock_ahead) and a fix hint. Nothing is ingested or billed. The dashboard's project setup page shows the same handshake per platform under Mobile SDK connection.

Troubleshootinglink

  • "BugWatch native module not found": you are in Expo Go, or you did not rebuild after install; build a dev client or run pod install and rebuild. npx react-native config confirms autolinking sees the package.
  • iOS build errors about Swift headers: the target is missing use_frameworks! :linkage => :static.
  • Release-only Android crash NoClassDefFoundError: minSdk below 24 or a missing mavenCentral() repository.
  • JS stacks unreadable: wrong release string, a dev-mode map, or a non-composed Hermes map.
  • The wrapper pod is BugWatchReactNative (depends on pod BugWatch ~> 0.1.2); if you also use the iOS SDK directly, check both resolve compatibly.