npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@newinstance/bugwatch-react-native

v0.1.8

Published

BugWatch React Native SDK — crash, error, and log observability for iOS and Android.

Readme

@newinstance/bugwatch-react-native

BugWatch React Native SDK — crash, error, and log observability for iOS and Android. Captures JS exceptions, unhandled promise rejections, and manual messages; delegates native crash/ANR detection, release-health sessions, and device context to the platform BugWatch SDKs. Events are delivered to your newinstance.cloud project in real time.

Table of contents


How it works

This package is a thin TypeScript facade over two real native SDKs:

Your app (JS/TypeScript)
  └── @newinstance/bugwatch-react-native  (this package)
        └── TurboModule bridge
              ├── iOS:     Pod BugWatchReactNative → native BugWatch iOS SDK (pod BugWatch ~>0.1.0)
              └── Android: BugWatchModule.kt       → native BugWatch Android SDK (cloud.newinstance:bugwatch:0.1.1)

The JS layer handles:

  • Resolving configuration and forwarding it to the native SDK via the bridge
  • Intercepting uncaught JS errors (ErrorUtils) and unhandled promise rejections (Hermes's native tracker or the promise polyfill)
  • Parsing JS stack traces (Hermes and V8/JSC format) and forwarding them with the event so the BugWatch worker can resolve minified frames against your uploaded source map

The native SDK handles everything else:

  • HTTP delivery to https://api.newinstance.cloud/api/v1/bugwatch/ingest/mobile
  • On-device HMAC-signed ingest tokens (x-bugwatch-token) — appSecret never leaves the device
  • Persistent disk-backed delivery queue with retry
  • Native crash capture: signal handler (iOS) + JVM uncaught exception handler (Android) + NDK signal handler (Android, 4 ABIs)
  • ANR / app-hang detection
  • Release-health session tracking
  • Device context (OS, model, locale, memory, etc.)
  • Breadcrumb history (100 most recent)

Native setup lives in the platform SDK docs — you do not re-implement any of it here. This package installs and drives the native SDKs for you, but their crash handling, symbolication, and symbol/mapping upload are documented in the iOS SDK and Android SDK repos. See Native crash and ANR reporting for exactly which sections to follow.


Requirements

| Requirement | Minimum version | |---|---| | React Native | 0.74+ (TurboModule support — 0.85+ recommended) | | iOS | 15.1+ — Podfile must enable static frameworks | | Android API level | 24+ | | Android JDK | 17 | | Expo | SDK 51+ via a Development Build / Prebuild — Expo Go is not supported |


Installation

npm install @newinstance/bugwatch-react-native
# or
yarn add @newinstance/bugwatch-react-native

iOS

Using Expo? Do not edit the Podfile by hand — see Expo. The config plugin applies the setting below on every expo prebuild.

The native BugWatch iOS SDK is written in Swift, and the React Native wrapper compiles Objective-C++ that imports the generated Swift interop header (BugWatchReactNative-Swift.h). CocoaPods only generates and exposes that header when pods are built as static frameworks, so your Podfile must enable them. Add use_frameworks! with static linkage inside your app target in ios/Podfile:

target 'YourApp' do
  use_frameworks! :linkage => :static
  # ...the rest of your target
end

Then install the pods:

cd ios && pod install

React Native autolink registers the BugWatchReactNative podspec, which declares a CocoaPods dependency on the native BugWatch pod (~>0.1.0).

Why this is required. Without static frameworks the build fails with 'BugWatchReactNative/BugWatchReactNative-Swift.h' file not found while compiling BugWatch.mm. This is the same requirement that other Swift-based native modules (Firebase, etc.) impose. :linkage => :static keeps the default static-library linking behaviour — only the module packaging changes. If your project still uses Flipper (React Native < 0.74), disable it; Flipper is incompatible with use_frameworks! and is removed by default on 0.74+.

Android

React Native autolink registers BugWatchPackage and adds the native cloud.newinstance:bugwatch:0.1.1 dependency automatically. No manual configuration is needed — it links out of the box.


Expo

This SDK contains native code, so it cannot run in Expo Go. Use a Development Build generated with the Prebuild workflow.

The package ships an Expo config plugin that applies the iOS static-frameworks requirement for you, so the native change is re-applied every time expo prebuild runs (a plain Podfile edit would be wiped out). Add the plugin to the plugins array in your app.json (or app.config.js):

{
  "expo": {
    "plugins": ["@newinstance/bugwatch-react-native"]
  }
}

Then regenerate the native projects and build a development client — you do not need to touch ios/Podfile yourself:

npx expo prebuild --clean
npx expo run:ios       # or: eas build --profile development --platform ios
npx expo run:android

The plugin sets ios.useFrameworks: "static" in the generated Podfile properties. If you already use expo-build-properties, you can set the same value there instead (our plugin is safe to use alongside it):

{
  "expo": {
    "plugins": [
      ["expo-build-properties", { "ios": { "useFrameworks": "static" } }]
    ]
  }
}

Getting your credentials

  1. Open your newinstance.cloud merchant dashboard.
  2. Navigate to BugWatch → select your project → Settings.
  3. Click "Reveal mobile credentials".
  4. Copy the Project ID (bwp_…) and App Secret (a base64url string).

Security note. The appSecret is used on-device to HMAC-sign a short-lived ingest token (5-minute expiry, per-batch nonce). The signed token is what travels over the network — appSecret itself is never transmitted. It is safe to bundle in your app binary, but treat it as a secret and avoid committing it to public repositories. Use an environment variable or a secrets manager in your CI pipeline.


Initializing the SDK

Call BugWatch.init() once, as early as possible in your app's entry point — typically index.js or the top of your root component before any <App /> renders.

// index.js  (or the very top of App.tsx before any navigation setup)
import { BugWatch } from '@newinstance/bugwatch-react-native';

BugWatch.init({
  projectId: 'bwp_your_project_id',
  appSecret: 'your_app_secret',
  environment: 'production',
  release: '1.4.2+318',
});

init() is idempotent — calling it again while the SDK is already running is a safe no-op. Automatic JS error capture is enabled by default.


Configuration reference

All options except projectId and appSecret are optional.

| Option | Type | Default | Description | |---|---|---|---| | projectId | string | — (required) | BugWatch project ID (bwp_…). Identifies your project on the ingest endpoint. | | appSecret | string | — (required) | Per-project secret used on-device to sign ingest tokens. Never transmitted. | | endpoint | string | https://api.newinstance.cloud | Ingest API base URL. Override only for local development or self-hosted backends. | | environment | string | — | Informational environment label (e.g. "production", "staging"). | | release | string | — | Release or build identifier (e.g. "1.4.2+318"). Shown in the issue list; required for source-map resolution. | | enabled | boolean | true | Master switch. When false, the SDK collects and sends nothing. Useful for debug builds. | | debug | boolean | false | Emit internal [BugWatch] diagnostic lines to the console. | | enableAutoCapture | boolean | true | Automatically intercept uncaught JS errors and unhandled promise rejections. Set to false to manage capture manually. | | sampleRate | number | 1.0 | Fraction of events to forward to the server (0.0–1.0). 1.0 sends every event. | | sensitiveFields | string[] | built-in list | Case-insensitive field names redacted from event payloads before they leave the device. The built-in list covers common credential and PII fields (password, token, apikey, ssn, etc.). | | maxQueueSize | number | 1000 | Maximum pending events held in the native queue. When full, the oldest events are dropped. | | batchSize | number | 50 | Number of events delivered in each ingest request. | | flushIntervalMs | number | 5000 | Auto-flush cadence in milliseconds. Set to 0 to disable the automatic timer. | | requestTimeoutMs | number | 15000 | Per-request network timeout in milliseconds. | | retry | RetryPolicy | 3 attempts, 500 ms base | Retry policy for failed ingest requests ({ maxAttempts, baseDelayMs, maxDelayMs }). 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. |

Options that are native-only

The native SDKs accept more options than the React Native facade forwards. The following run at their native defaults and cannot be changed from JavaScript. If you need to change them, initialize the native SDK directly from your Application subclass (Android) or your AppDelegate / @main App (iOS). Both native SDKs return the already-running instance on a second call, and native startup happens before the JS bundle loads, so your native options win and the later BugWatch.init() from JS becomes a no-op for configuration. Keep calling init() from JS anyway: it is what installs the JavaScript error hooks. Arming crash capture earlier is a bonus, since it covers crashes that happen before the bundle finishes loading.

| Native option | Platform | Native default | |---|---|---| | autoSessionTracking | both | true | | enableAutoBreadcrumbs | both | true | | enableNetworkBreadcrumbs | both | true | | networkBreadcrumbAllowedHosts | both | [] (all hosts allowed) | | networkBreadcrumbDeniedHosts | both | [] | | enableAnrTracking | Android | true | | anrThresholdMs | Android | 5000 | | enableAppHangTracking | iOS | true | | appHangThresholdMs | iOS | 2000 |


What is and is not captured

Everything below is on by default once init() has run. Nothing here needs extra code in your app.

Captured

| Crash or event class | Where it is caught | Level | Notes | |---|---|---|---| | Uncaught JS error (fatal) | ErrorUtils global handler, plus the native crash handler | fatal | Two records, see Two events per JS crash | | Unhandled promise rejection | Hermes rejection tracker, or the promise polyfill | error | Does not terminate the app | | Uncaught JVM exception | Android Thread.setDefaultUncaughtExceptionHandler | fatal | Includes crashes in other Android libraries | | Uncaught NSException | iOS NSSetUncaughtExceptionHandler | fatal | Includes RCTFatalException | | Native signal crash | POSIX signal handlers on both platforms | fatal | SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE, SIGTRAP, and SIGSYS on iOS | | Swift runtime trap | iOS SIGTRAP handler | fatal | fatalError, force-unwrap of nil, array index out of range, failed precondition | | Android NDK crash | NDK signal handler in the app process | fatal | Covers your own .so files, third-party native libraries, and the ART runtime | | Hermes / JSI / React Native C++ crash | Native signal handler | fatal | These are native faults in the app process, so the native handler sees them | | Stack overflow | Native signal handler on a dedicated alternate signal stack | fatal | Both platforms. See the note below | | ANR (main thread blocked) | Android watchdog, 5 s threshold | error | The app is not killed, so this is not a crash | | App hang (main thread blocked) | iOS watchdog, 2 s threshold | error | Same, the app is not killed | | Handled errors and log messages | captureException / captureMessage | your choice | |

Stack overflow deserves a note because it is the one crash class a naive signal handler misses: the crashing thread has no stack left for the kernel to deliver the signal on. Both SDKs register a dedicated alternate signal stack so the handler can still run. The registration is per-thread and is made on the thread that starts the SDK (the main thread in practice), so a stack overflow on a background thread is still missed.

Not captured

These are limits of the platforms, not of this SDK. No crash reporter on iOS or Android captures them.

| Not captured | Why | |---|---| | Out-of-memory kill, jetsam, watchdog termination | The OS sends SIGKILL, which cannot be caught by any handler. The prior session is finalized as exited, so your crash-free rate reads slightly optimistic | | User force-quitting the app | Indistinguishable from a clean exit | | Anything before BugWatch.init() runs | Handlers are armed by init(). Call it as early as possible, see Initializing the SDK | | A crash inside the crash handler itself | The signal is already being handled; the process dies |

Two React Native configurations that 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 is ready. React Native's JsErrorHandler only routes through ErrorUtils once the runtime reports ready; before that it uses its internal C++ pipeline.
  • The useAlwaysAvailableJSErrorHandling feature flag enabled. This is off by default. When on, React Native handles JS errors entirely in C++ and never calls the ErrorUtils global handler.

Automatic error capture

When enableAutoCapture is true (the default), the SDK wires two hooks automatically when you call init():

Uncaught JS errors — wraps ErrorUtils.setGlobalHandler. The previous handler (React Native's own redbox / crash reporter) is preserved and called after BugWatch captures the event, so existing fatal-error behaviour is unchanged. The isFatal flag React Native passes is preserved: an uncaught error that terminates the app is recorded at Severity.Fatal, and everything else at Severity.Error.

Unhandled promise rejections — prefers Hermes's built-in HermesInternal.enablePromiseRejectionTracker when available (which is the case on all modern React Native apps using the Hermes engine). Falls back to the promise/setimmediate/rejection-tracking polyfill on JSC or non-Hermes environments.

Both hooks degrade to a no-op if the underlying runtime API is unavailable (e.g. Jest, web, polyfill-stripped bundles).

To disable automatic capture and handle errors yourself:

BugWatch.init({
  projectId: 'bwp_…',
  appSecret: '…',
  enableAutoCapture: false,
});

// Then wire your own handler:
ErrorUtils.setGlobalHandler((error, isFatal) => {
  BugWatch.captureException(error, isFatal ? Severity.Fatal : Severity.Error);
});

Two events per JS crash

A fatal JS error is recorded twice, by design, and both records are useful:

  1. From the JS hook: platform: "react-native", level: fatal, carrying the parsed JS stack frames. This is the readable one, and it is the only one the backend can resolve 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 a normal 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 that message is truncated by React Native to 175 characters.

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


Manual capture

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

// Capture an Error object or any thrown value:
try {
  await riskyOperation();
} catch (err) {
  BugWatch.captureException(err);
}

// Capture a plain log message at a specific severity:
BugWatch.captureMessage('Payment flow started', Severity.Info);
BugWatch.captureMessage('Rate limit hit, retrying', Severity.Warn);
BugWatch.captureMessage('Unrecoverable state reached', Severity.Fatal);

Both methods return a client-side event ID string (bw_e_…) synchronously. This is a local handle — the native SDK assigns the authoritative delivered ID.


Native crash and ANR reporting

Native crash and ANR detection is owned entirely by the native SDKs. No extra code is required in your React Native app.

iOS: the native SDK installs POSIX handlers for SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE, SIGTRAP, and SIGSYS, plus an NSUncaughtExceptionHandler. Handlers run on a dedicated alternate signal stack, which is what makes a stack overflow reportable. Crash reports include binary images and instruction addresses so the BugWatch worker can symbolicate frames using uploaded dSYM files. An app-hang watchdog fires after appHangThresholdMs (default 2000 ms).

Android — the native SDK installs a JVM Thread.UncaughtExceptionHandler and a native NDK signal handler for SIGSEGV, SIGABRT, SIGBUS, SIGILL, SIGFPE, and SIGTRAP (supporting armeabi-v7a, arm64-v8a, x86, x86_64), also on an alternate signal stack. An ANR watchdog fires after anrThresholdMs (default 5000 ms). Reports are queue-persisted across restarts.

Both handlers chain whatever was installed before them, so Crashlytics, Play Console, and the OS tombstone still work alongside BugWatch.

Both platforms track release-health sessions automatically.

You do not implement any of this yourself, and you do not need to go hunting for a native implementation. Native crash/ANR handling and symbolication ship inside the platform SDKs that this package already installs. Their setup and the all-important symbol upload are documented in the native SDK repos — follow them directly:

  • iOSBugWatch iOS SDK — see Native crash capture and dSYM symbol upload (upload dSYMs via the BugWatch CLI, an Xcode build phase, Xcode Cloud, or CI so native crashes symbolicate in the dashboard).
  • AndroidBugWatch Android SDK — see NDK / native crash capture and R8 / ProGuard → Uploading mapping files (upload the mapping file so minified and native stack traces de-obfuscate).

User identity

Attach a user identity before or after init(). It is stored in the native SDK's scope and applied to every subsequent event.

// Set on sign-in:
BugWatch.setUser({
  id: 'u_12345',
  email: '[email protected]',
  username: 'ada_l',
});

// Clear on sign-out:
BugWatch.setUser(null);

BugWatchUser fields: id, email, username, ip — all optional. Supply only the fields your privacy policy permits.


Tags and context

Tags and context entries are string key–value pairs attached to every subsequent event. They are visible in the issue detail view in your dashboard.

BugWatch.setTag('screen', 'CheckoutScreen');
BugWatch.setTag('plan', 'pro');

BugWatch.setContext('cartId', 'c_998abc');
BugWatch.setContext('paymentProvider', 'stripe');

Use setTag for low-cardinality values you want to filter issues by. Use setContext for higher-cardinality diagnostic values.


Breadcrumbs

Breadcrumbs are a chronological trail of events attached to the next captured error or message. The native SDK keeps the 100 most recent.

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

BugWatch.addBreadcrumb({
  category: 'navigation',
  message: 'Navigated to CheckoutScreen',
  level: Severity.Info,
});

BugWatch.addBreadcrumb({
  category: 'ui.tap',
  message: 'Pay button tapped',
  level: Severity.Info,
  data: { buttonId: 'btn-pay', cartTotal: '49.99' },
});

Breadcrumb fields:

| Field | Type | Default | Description | |---|---|---|---| | category | string | — (required) | Dot-separated category (e.g. "ui.tap", "navigation", "http"). | | message | string | — | Human-readable description. | | type | string | "default" | Breadcrumb type hint (e.g. "http", "navigation", "error"). | | level | Severity | Severity.Info | Severity of this breadcrumb. | | timestamp | number | now | Unix milliseconds. Defaults to the current time. | | data | Record<string, string> | — | Arbitrary string key–value pairs attached to the breadcrumb. |

Automatic breadcrumbs

The native SDKs record these on their own. You do not add them, and they count against the same 100-crumb buffer as your manual ones.

| Category | Type | Platform | Emitted when | |---|---|---|---| | ui.lifecycle | navigation | Android | An activity is created, started, resumed, paused, or stopped. Message is "<ActivityName> <state>" | | app.lifecycle | navigation | Android | The app moves to the foreground or background | | app.lifecycle | system | iOS | app.foreground.active, app.resign.active, app.background, app.foreground, device.memory.low | | network | http | both | One crumb per outbound HTTP request, carrying method, host, path, status_code, and duration_ms in data. Recorded at warn or error level for 4xx and 5xx responses |

Network breadcrumbs work differently per platform, and this is worth knowing:

  • iOS registers a URLProtocol and adds it to URLSessionConfiguration.default, so it covers URLSession.shared and any session built from the default configuration after the SDK starts. That normally includes React Native's fetch(), which uses a default-configuration session. A session built from a custom configuration before init() ran, or one that sets its own protocolClasses, is not covered.
  • Android ships an OkHttp Interceptor that is opt-in and must be attached to your own OkHttp client from native code. React Native's fetch() uses its own internal OkHttp client, so on Android you get no automatic network breadcrumbs from JS network calls.

Query strings are stripped from the recorded path, and every crumb goes through the same sensitiveFields redaction as any other event payload.


Release and environment

Set release and environment in init() for all events, or update the release at runtime:

BugWatch.init({
  projectId: 'bwp_…',
  appSecret: '…',
  environment: __DEV__ ? 'development' : 'production',
  release: `${Application.nativeApplicationVersion}+${Application.nativeBuildVersion}`,
});

// Override later (e.g. after a hot update):
BugWatch.setRelease('1.4.3+320');

release is required for JS source-map resolution in the BugWatch worker. Use a value that uniquely identifies your JS bundle (typically appVersion+buildNumber).


Severity levels

All BugWatch SDKs share a numeric severity scale. Import the Severity enum for type-safe usage:

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

Severity.Trace  // 10
Severity.Debug  // 20
Severity.Info   // 30
Severity.Warn   // 40
Severity.Error  // 50
Severity.Fatal  // 60

captureMessage defaults to Severity.Info when no level is supplied. captureException defaults to Severity.Error.


Device context

Every event carries a device snapshot collected by the native SDK. You do not configure this and cannot opt out of individual fields.

| Field | Android | iOS | |---|---|---| | model | yes | yes | | manufacturer | yes | no | | brand | yes | no | | family | yes | yes | | osName / osVersion | yes | yes | | sdkInt | yes | no | | locale | yes | yes | | timezone | yes | yes | | simulator | yes | yes | | appVersion / appBuild | yes | yes | | packageName | yes | bundle identifier |

Alongside the device snapshot every event also carries an installId (stable per install), a sessionId, the release, the environment, your tags, contexts, user, and the breadcrumb buffer.


Sessions and release health

With session tracking on (the native default), the SDK opens a session at start and finalizes the previous run's session on the next launch. That is what drives crash-free rate in the dashboard.

  • The prior session is reported crashed when a crash artifact from that run was found on disk, and exited otherwise.
  • Session events bypass sampleRate, so crash-free rate stays accurate even at a low sample rate.
  • There is no abnormal status. A process killed by the OS for memory pressure leaves no artifact and is therefore reported exited.

How events reach the server

Nothing is sent from inside a crash handler. The process is dying, and networking is not safe there. Instead:

  1. At crash time the native handler writes a small self-contained artifact to disk, synchronously, then chains the handler that was installed before it so the app still terminates normally and any other crash reporter still runs.
  2. On the next launch the SDK reads that artifact, turns it into a fatal event, hands it to the normal delivery queue, and deletes the artifact so a crash is never reported twice.

Handled events skip step 1 and go straight to the queue. The queue itself is an append-only NDJSON file, written synchronously on the calling thread, so an event that was captured survives the process dying moments later.

Delivery is:

POST {endpoint}/api/v1/bugwatch/ingest/mobile
x-bugwatch-token: <token signed on-device>
Content-Type: application/x-ndjson

The token is an HMAC-SHA256 signature over { pid, env, iat, exp, nonce }, signed on-device with your appSecret and valid for 5 minutes. Your appSecret is never transmitted. Batches are batchSize events per request, retried with exponential backoff, and a batch that exhausts its attempts or is rejected outright is dropped so it can never wedge the pipe. Delivery resumes automatically when connectivity returns.

Because delivery is in-process, a queued event is sent on the next app launch if the app dies before it drains. There is no background job that uploads after the process is gone.


What the SDK writes to disk

All under a single cloud.newinstance.bugwatch directory: the app's filesDir on Android, Application Support on iOS. Deleting the app removes all of it.

| File | Purpose | |---|---| | pending-events.ndjson | The delivery queue, one JSON event per line | | current-session.json | The in-flight session descriptor, for release health | | last-crash.json | Android JVM crash artifact, written at crash time | | last-native-crash.json | Android NDK crash artifact | | pending_crash | iOS signal-crash artifact | | pending_crash_nsexception | iOS NSException crash artifact | | crash-context.json | iOS device/release/session snapshot, so a crash on the next launch can be enriched | | crash-breadcrumbs.ndjson | iOS breadcrumb ring mirrored for crash enrichment |


Source maps

JS stack frames from uncaught errors and captureException arrive at the BugWatch worker as minified bundle positions. The worker resolves them to original source/line using the source map you upload for the matching release.

Generating source maps

Metro (default bundler):

npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ios/main.jsbundle \
  --sourcemap-output ios/main.jsbundle.map

Hermes (enabled by default on React Native 0.70+): you need the composed Hermes bytecode source map. Your build system typically produces this as index.android.bundle.packager.map and index.android.bundle.compiler.map. Compose them with compose-source-maps.js (included in react-native):

node node_modules/react-native/scripts/compose-source-maps.js \
  index.android.bundle.packager.map \
  index.android.bundle.compiler.map \
  -o index.android.bundle.map

Uploading source maps

Use the BugWatch CLI's artifacts upload command. The release value you supply here must match the release string you passed to BugWatch.init(), or frames will not resolve.

npx @newinstance/bugwatch-cli artifacts upload ./main.jsbundle.map \
  --token "$BUGWATCH_AUTH_TOKEN" \
  --platform ios \
  --type sourcemap \
  --release "1.4.2+318"

For Expo, the bundled map lives under dist/_expo/static/js/<platform>/:

npx @newinstance/bugwatch-cli artifacts upload \
  dist/_expo/static/js/ios/index-*.hbc.map \
  --platform ios --type sourcemap --release "1.4.2+318"

Use artifacts upload, not symbols upload — the latter accepts only binary debug-symbol archives (dSYM/Mach-O/ELF) and rejects a .map file. Re-uploading the same (release, platform, type) replaces the previous map.

The raw REST upload was retired: it sent the whole file as a request body, which the edge proxy rejects for any real source map. The CLI streams straight to storage.

Native symbolication (iOS dSYM, Android R8/ProGuard mappings) is handled by the native SDKs and BugWatch worker independently of this package. Refer to the iOS and Android SDK documentation for symbol upload instructions.


Flushing before shutdown

Call flush() before your app terminates or backgrounds to ensure pending events are delivered:

// In your app's shutdown / background handler:
await BugWatch.flush();

// If you want to completely tear down the SDK (e.g. during testing):
BugWatch.close();

flush() returns a Promise<void> that resolves once the native SDK has attempted to drain its delivery queue.


Code examples

Minimal integration

// index.js
import { AppRegistry } from 'react-native';
import { BugWatch } from '@newinstance/bugwatch-react-native';
import App from './App';
import { name as appName } from './app.json';

BugWatch.init({
  projectId: 'bwp_your_project_id',
  appSecret: 'your_app_secret',
  environment: 'production',
  release: '1.0.0+1',
});

AppRegistry.registerComponent(appName, () => App);

Automatic capture handles the rest — JS crashes and unhandled rejections are forwarded to BugWatch without any additional code.


Full integration with user identity, tags, and an error boundary

// index.js
import { AppRegistry } from 'react-native';
import { BugWatch } from '@newinstance/bugwatch-react-native';
import App from './App';
import { name as appName } from './app.json';

BugWatch.init({
  projectId: process.env.BUGWATCH_PROJECT_ID!,
  appSecret: process.env.BUGWATCH_APP_SECRET!,
  environment: __DEV__ ? 'development' : 'production',
  release: `${require('./package.json').version}+${process.env.BUILD_NUMBER}`,
  // Disable in development so noise doesn't flood your dashboard:
  enabled: !__DEV__,
});

AppRegistry.registerComponent(appName, () => App);
// AuthContext.tsx — set user on sign-in / sign-out
import { BugWatch } from '@newinstance/bugwatch-react-native';

function onSignIn(user: { id: string; email: string }) {
  BugWatch.setUser({ id: user.id, email: user.email });
}

function onSignOut() {
  BugWatch.setUser(null);
}
// ErrorBoundary.tsx — React error boundary for component-tree errors
import React from 'react';
import { View, Text } from 'react-native';
import { BugWatch } from '@newinstance/bugwatch-react-native';

interface State { hasError: boolean }

export class ErrorBoundary extends React.Component<
  React.PropsWithChildren,
  State
> {
  state: State = { hasError: false };

  componentDidCatch(error: Error) {
    BugWatch.captureException(error);
  }

  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      return (
        <View>
          <Text>Something went wrong. Please restart the app.</Text>
        </View>
      );
    }
    return this.props.children;
  }
}
// CheckoutScreen.tsx — manual capture and breadcrumbs
import { BugWatch, Severity } from '@newinstance/bugwatch-react-native';

async function handlePayment(cartId: string) {
  BugWatch.setTag('screen', 'CheckoutScreen');
  BugWatch.addBreadcrumb({
    category: 'ui.tap',
    message: 'Pay button tapped',
    level: Severity.Info,
    data: { cartId },
  });

  try {
    await submitPayment(cartId);
    BugWatch.addBreadcrumb({
      category: 'payment',
      message: 'Payment submitted successfully',
      level: Severity.Info,
    });
  } catch (err) {
    BugWatch.captureException(err);
    // Show error UI...
  }
}

API reference

Every public member of the package. Import from @newinstance/bugwatch-react-native.

| Member | Signature | Notes | |---|---|---| | BugWatch.init | (options: BugWatchOptions) => void | Starts the native SDK and arms crash capture. Idempotent | | BugWatch.close | () => void | Tears down auto-capture and stops the native SDK | | BugWatch.captureException | (error: unknown, level?: Severity) => string | Defaults to Severity.Error. Returns a client-side event id | | BugWatch.captureMessage | (message: string, level?: Severity) => string | Defaults to Severity.Info | | BugWatch.setUser | (user: BugWatchUser \| null) => void | null clears the identity | | BugWatch.setTag | (key: string, value: string) => void | Indexed, filterable in the dashboard | | BugWatch.setContext | (key: string, value: string) => void | Free-form, not indexed | | BugWatch.setRelease | (release: string) => void | Overrides the release for subsequent events | | BugWatch.addBreadcrumb | (breadcrumb: Breadcrumb) => void | Bounded ring of the 100 most recent | | BugWatch.flush | () => Promise<void> | Drains the native queue and resolves when the attempt completes | | BugWatch.sdkName / BugWatch.sdkVersion | string | Static, stamped on every event | | Severity | enum | Trace 10, Debug 20, Info 30, Warn 40, Error 50, Fatal 60 | | DEFAULT_RETRY_POLICY | RetryPolicy | See the note on retry in Configuration reference | | DEFAULT_SENSITIVE_FIELDS | string[] | The built-in redaction list |

Exported types: Breadcrumb, BugWatchEvent, BugWatchOptions, BugWatchUser, NormalizedException, RetryPolicy, SdkInfo, StackFrame.

The BugWatch singleton is also the default export.


End-to-end walkthrough

This exercises every capture path the SDK has, on a real device or simulator. Do it once when you integrate; it takes about ten minutes and it is the only way to be sure symbolication is wired correctly as well as capture.

1. Initialize as early as possible

Handlers are only armed once init() runs, so anything that crashes before that is invisible. Put it at module scope in your entry file, not inside a component or a useEffect.

// index.js, before registerRootComponent / AppRegistry.registerComponent
import { BugWatch } from '@newinstance/bugwatch-react-native';

BugWatch.init({
  projectId: process.env.BUGWATCH_PROJECT_ID,
  appSecret: process.env.BUGWATCH_APP_SECRET,
  environment: __DEV__ ? 'development' : 'production',
  release: '1.0.0+1',
  debug: true,
});

2. Confirm the pipe works

BugWatch.captureMessage('e2e: hello', Severity.Info);

Dashboard → Logs. It should land within a few seconds. If it does not, stop here and fix credentials or connectivity before continuing; nothing below will work either.

3. Give the events something to say

BugWatch.setUser({ id: 'u_123', email: '[email protected]' });
BugWatch.setTag('screen', 'checkout');
BugWatch.setContext('cart_id', 'c_987');
BugWatch.addBreadcrumb({ category: 'ui.tap', message: 'Pay tapped' });

Every event from now on carries all four. Verify on the next event you send.

4. Trigger each crash class

Do these one at a time, relaunching the app between each, since crash reports are delivered on the launch after the crash.

// a. Handled error, appears immediately, no relaunch needed
try { JSON.parse('{'); } catch (e) { BugWatch.captureException(e); }

// b. Unhandled promise rejection, level error, app survives
Promise.reject(new Error('e2e: unhandled rejection'));

// c. Uncaught JS error, level fatal, app terminates in a release build
setTimeout(() => { throw new Error('e2e: uncaught js'); }, 0);

For native crashes, add a debug-only screen that calls into native. On Android, a Kotlin throw RuntimeException("e2e: jvm") from a native module covers the JVM path, and a deliberate null dereference in a JNI function covers the NDK path. On iOS, fatalError("e2e: swift trap") covers the Swift trap path and NSException(name: .genericException, reason: "e2e", userInfo: nil).raise() covers the NSException path.

After each one, relaunch the app and confirm the crash appears under Issues with level: fatal, and that the previous session shows as crashed in release health.

5. Prove symbolication, not just capture

A crash you cannot read is not much use, so verify the symbol side too. This is the step people skip.

# JS frames, required for readable React Native stacks
npx react-native bundle \
  --platform android --dev false \
  --entry-file index.js \
  --bundle-output /tmp/index.android.bundle \
  --sourcemap-output /tmp/index.android.bundle.map

npx @newinstance/bugwatch-cli artifacts upload /tmp/index.android.bundle.map \
  --release "1.0.0+1" --platform react-native --type sourcemap

# Android native frames
npx @newinstance/bugwatch-cli artifacts upload app/build/outputs/mapping/release/mapping.txt \
  --release "1.0.0+1" --platform android --type r8

# iOS native frames
npx @newinstance/bugwatch-cli symbols upload path/to/YourApp.xcarchive \
  --release "1.0.0+1" --build-number 1

The --release value must match the release you passed to init() exactly. That string is the only join key between an event and its symbols; a mismatch is the single most common reason stacks stay minified.

Re-trigger the uncaught JS error, relaunch, and check that the frames in the dashboard now show your original file names and line numbers instead of index.android.bundle:1:45231.

6. Turn debug off

debug: false,

Then ship.


Verifying the integration

  1. Initialize the SDK with debug: true temporarily. You should see [BugWatch] started (env=…, release=…) in your Metro console.

  2. Trigger a test error in a non-production build:

    // In a dev-only button or screen:
    BugWatch.captureMessage('BugWatch integration test', Severity.Info);
  3. Open your BugWatch dashboard → Logs tab. The test event should appear within a few seconds.

  4. Test automatic capture by throwing from a button press:

    onPress={() => { throw new Error('BugWatch test error'); }}

    This event will appear under the Issues tab in your dashboard.

  5. Remove debug: true before shipping to production.


Production checklist

  • [ ] projectId and appSecret are loaded from environment variables or a secrets manager, not hard-coded in source.
  • [ ] environment is set correctly ("production" for production builds).
  • [ ] release is set to a value that uniquely identifies your JS bundle (required for source-map resolution).
  • [ ] Source maps are uploaded for every production JS bundle before users receive the update (OTA or App Store).
  • [ ] debug: false (the default) — do not ship debug logging to production.
  • [ ] enabled is true in production and optionally false in local development to reduce noise.
  • [ ] Sensitive field names specific to your app are added to sensitiveFields if they are not already covered by the built-in list.
  • [ ] BugWatch.setUser(null) is called on sign-out to clear user identity.
  • [ ] await BugWatch.flush() is called in your app's shutdown / background transition handler if your app performs a controlled exit.
  • [ ] (iOS) dSYM files are uploaded for every production build — see the iOS SDK dSYM symbol upload docs (BugWatch CLI, Xcode build phase, Xcode Cloud, or CI).
  • [ ] (Android) R8/ProGuard mapping file is uploaded for every production build — see the Android SDK Uploading mapping files docs.

Troubleshooting

"BugWatch" native module is not available

If you see a warning that the TurboModule could not be found:

  • Confirm pod install has been run in ios/ after installing the package.
  • On Android, confirm your project uses the new React Native build system (React Native 0.74+).
  • Clean the build and rebuild: cd ios && xcodebuild clean / cd android && ./gradlew clean.
  • Check that autolinking ran: npx react-native config should list @newinstance/bugwatch-react-native under dependencies.

Events do not appear in the dashboard

  1. Enable debug: true and verify [BugWatch] started appears in the console.
  2. Confirm projectId matches exactly what is shown in the dashboard (it starts with bwp_).
  3. Confirm your device or simulator has network access to https://api.newinstance.cloud.
  4. Check that enabled is not false in your current build configuration.
  5. If using a sample rate below 1.0, remember that only that fraction of events are forwarded — increase to 1.0 temporarily to confirm delivery.

Hermes promise rejections are not captured

Hermes ships its own native Promise implementation. The SDK detects and uses HermesInternal.enablePromiseRejectionTracker automatically. If rejections are still not captured, ensure you are on React Native 0.74+ with Hermes enabled (the default).

Stack traces appear minified

Upload source maps for the matching release value. Verify that the release string passed to BugWatch.init() exactly matches the one used during the source-map upload step.

iOS: BugWatchReactNative-Swift.h file not found

node_modules/@newinstance/bugwatch-react-native/ios/BugWatch.mm:8:9
#import "BugWatchReactNative-Swift.h"
        ^ 'BugWatchReactNative/BugWatchReactNative-Swift.h' file not found

The pod is being built as a static library, so CocoaPods never generated the Swift interop header. Enable static frameworks in ios/Podfile:

target 'YourApp' do
  use_frameworks! :linkage => :static
end

Then cd ios && pod install and rebuild (see iOS for details). On Expo, add the @newinstance/bugwatch-react-native config plugin and re-run npx expo prebuild --clean (see Expo). Android needs no equivalent change — its native library links out of the box.

CocoaPods version conflict on BugWatch pod

The RN wrapper pod is named BugWatchReactNative (not BugWatch) precisely to avoid a naming conflict with the native iOS SDK pod. If you are also using the native iOS SDK directly in your Podfile, ensure both entries resolve to compatible versions (~>0.1.0).

Android: java.lang.NoClassDefFoundError for BugWatch classes

Confirm your minSdkVersion is at least 24. Ensure mavenCentral() is in your settings.gradle (or top-level build.gradle) repositories block so Gradle can resolve cloud.newinstance:bugwatch:0.1.1.


Known limitations

Stated plainly so you can plan around them rather than discover them during an incident.

  • Delivery needs a relaunch. Crash reports are uploaded the next time the app starts. There is no background upload job, so a user who crashes and never returns is never counted. Queued events are not lost, only delayed.
  • Out-of-memory kills are invisible. SIGKILL cannot be caught. Those runs are finalized as exited, so crash-free rate reads slightly better than reality.
  • A fatal JS error produces two issues. See Two events per JS crash. There is no de-duplication between the JS record and the native one.
  • retry is inert. It is accepted by init() but not forwarded to the native SDKs. See Configuration reference.
  • Several native options cannot be set from JavaScript. See Options that are native-only.
  • No 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.
  • Stack overflow is only covered on the SDK's start thread. The alternate signal stack is registered per-thread on the thread that calls init().
  • Nothing before init() is captured, including errors thrown while the JS bundle is still loading.

Upgrade notes

0.1.0 → 0.1.1

  • The projectKey config field accepted in previous internal builds has been replaced by separate projectId and appSecret fields. Update your init() call:

    // Before (0.1.0 internal):
    BugWatch.init({ projectKey: 'bwp_…:your_secret', … });
    
    // After (0.1.1):
    BugWatch.init({ projectId: 'bwp_…', appSecret: 'your_secret', … });
  • Android native dependency bumped from 0.1.0 to 0.1.1 (Maven Central). Run ./gradlew dependencies to confirm resolution.

  • iOS: run pod update BugWatch if your Podfile.lock pins the pod to an older version.


Links

License

MIT