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-nativeiOS: the Podfile target must use static frameworks or the build fails with BugWatchReactNative-Swift.h file not found:
use_frameworks! :linkage => :staticThen 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:iosThe 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
| Option | Default | What it does |
|---|---|---|
enabled | true | Master switch; when false, capture short-circuits in JS before the bridge |
endpoint | https://api.newinstance.cloud | Override for dev or self-hosted backends |
environment / release | - | Labels; release must equal your source-map upload string |
enableAutoCapture | true | Global JS handler + unhandled promise rejections |
sampleRate | 1.0 | Client-side sampling |
sensitiveFields | built-in list | Extra redaction keys |
batchSize / flushIntervalMs / maxQueueSize | 50 / 5000 / 1000 | Delivery and offline queue (native side) |
requestTimeoutMs | 15000 | Per-request timeout |
retry | { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8000 } | Exponential backoff |
debug | false | Logs [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. Severityvalues are numeric 10 to 60;captureMessagedefaults toSeverity.Info.setContext(key, value)takes string values; breadcrumbs take an object with a requiredcategory(typedefaults todefault,dataisRecord<string, string>; the native side keeps the last 100).setUserkeeps onlyid,email,username,ip;setUser(null)clears on sign-out.close()tears down auto-capture and restores the previousErrorUtilshandler.
Automatic capturelink
- JS errors via the
ErrorUtilsglobal handler plus unhandled promise rejection tracking; prior handlers are chained, not replaced. React Native'sisFatalflag is preserved: an uncaught error that terminates the app is recorded atSeverity.Fatal, an unhandled rejection (which does not terminate the app) atSeverity.Error. - Stack parsing handles Hermes (
fn@file:line:col) and JSC/V8 (at fn (file:line:col)); frames fromnode_modulesare marked not-in-app. - In environments without
ErrorUtilsor 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.
- 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. - From the native crash handler - React Native deliberately terminates the process after a fatal JS error (
JavascriptExceptionon Android,RCTFatalExceptionon iOS). The native SDK catches that as an ordinary native crash, so you also get aplatform: "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
JsErrorHandleronly routes throughErrorUtilsonce the runtime is ready; before that it uses its internal C++ pipeline. - The
useAlwaysAvailableJSErrorHandlingfeature flag enabled. It is off by default. When on, React Native handles JS errors entirely in C++ and never calls theErrorUtilsglobal 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 option | Platform | Native default |
|---|---|---|
autoSessionTracking | both | true |
enableAutoBreadcrumbs | both | true |
enableNetworkBreadcrumbs | both | true |
networkBreadcrumbAllowedHosts / networkBreadcrumbDeniedHosts | both | [] |
enableAnrTracking / anrThresholdMs | Android | true / 5000 |
enableAppHangTracking / appHangThresholdMs | iOS | true / 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 captured | Why |
|---|---|
| Out-of-memory kills, jetsam, watchdog terminations | The 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 app | Indistinguishable from a clean exit |
Anything before BugWatch.init() runs | Handlers 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 Android | React 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 SDK | sigaltstack 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
--releasestring must equalinit.releaseexactly; 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-nativeand upload one composed map per release. If iOS and Android need different maps, give each platform its own release string (for example1.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:
projectKeysplit intoprojectId+appSecret; runpod update BugWatchif 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 installand rebuild.npx react-native configconfirms 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 missingmavenCentral()repository. - JS stacks unreadable: wrong release string, a dev-mode map, or a non-composed Hermes map.
- The wrapper pod is
BugWatchReactNative(depends on podBugWatch ~> 0.1.2); if you also use the iOS SDK directly, check both resolve compatibly.