# React Native

One React Native package gives you JS error tracking plus native crash, ANR and hang coverage. JS exceptions are wired through a TurboModule into the native Android and iOS BugWatch SDKs.

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.

## Requirements

- 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. Install

```bash
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`:

```ruby
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:

```json
"plugins": [["@newinstance/bugwatch-react-native", { "useFrameworks": "static" }]]
```

```bash
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)

```ts
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. Configuration

| 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` | None | 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 API

```ts
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 capture

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

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 hook

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

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 captured

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 relaunch

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)

Hermes needs the composed source map:

```bash
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](/android) and [iOS & macOS](/ios-macos)).

## Upgrade notes

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

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

```ts
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**.

## Troubleshooting

- **"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.
