The Sazabi React Native SDK is an early preview. Options, event attributes,
and capture behavior may change between releases, so pin the version you
install and read the changelog before you upgrade.
@sazabi/react-native, the official Sazabi React Native SDK. It emits a session-scoped stream of screen changes, touches, console output, JavaScript errors, and network calls as OTLP log records, and injects W3C trace context into your API requests so a symptom in the app and its backend cause share a trace id.
About
@sazabi/react-native is a pure JavaScript library you install in your own React Native or Expo app. No native module, no config plugin, no pod install; it runs in Expo Go. It lets you:
- Follow a single user’s session from launch to error, across app launches when you give it a storage adapter
- Catch JavaScript errors that reach React Native’s global error handler, fatal ones included, errors thrown while React renders, and unhandled promise rejections on Hermes, with stack traces
- See every touch as an episode, with taps and rage taps derived from them
- Join mobile network calls to backend logs on a shared
trace_id - Use AI to investigate a mobile session end to end
What gets captured
Every event carriessession.id, session.window_id (one app launch), session.distinct_id, app.screen.name, app.screen.url, and an app.event_type naming the kind of event. The same keys the browser SDK emits, so one query reads both; the platform is on the resource (os.name, os.version, telemetry.sdk.name: "@sazabi/react-native", telemetry.sdk.language: "javascript").
By default the SDK also mirrors
console.error and console.warn into the stream, leaving the original console behavior untouched.
A development build reports every uncaught error as fatal, so expect fatal rows there. The console.error line React Native prints for a reported error is not mirrored as a second log row. Rejection tracking needs the Hermes engine; an app on JavaScriptCore records no unhandledrejection rows. In a development build the SDK prints the same “Possible unhandled promise rejection” warning React Native’s own tracker would. Hermes has one rejection-tracker slot; if another crash reporter owns it, see Other crash reporters. Errors thrown while React renders are recorded by <SazabiRoot>, which must be the outermost component; it records the error as fatal, then rethrows it, so the app still crashes as it would without the SDK.
Every row of a crash batch delivered from the crash outbox carries app.outbox.id, one id per batch. An app on AsyncStorage can deliver a crash batch twice; deduplicate on that id.
app.screen.url and url.path carry the screen’s path as your router reports it. When a screen opens from a deep link, its query string is kept, values included, as the browser SDK keeps the page URL. Only the navigation row’s param list is reduced to keys.
Native crashes, ANRs, app-start and frame metrics, and session replay are not captured. Release builds ship Hermes bytecode, and the SDK does not upload source maps, so production stack traces are bytecode offsets.
Configuration
Delivery
408, 429, 5xx, network failures, and 30-second timeouts retry with exponential backoff andRetry-After. A 413 splits the batch; a single event that still cannot fit is dropped. Other 4xx responses drop the batch with one warning per status per launch. The crash outbox retains retryable reports for a later launch rather than using a backoff timer. Unlike queued uploads, a stored report rejected with 413 is deleted without splitting it.
The queue has a 1 MiB budget as well as maxQueuedEvents. The outbox keeps up to 20 batches within 512 K characters (524,288 UTF-16 code units). Both drop the oldest first; a single oversized crash batch can exceed the character budget. The next delivered row after an in-memory queue eviction or refused queued batch reports the loss as app.sdk.dropped_events. Retries can produce duplicate deliveries. See Delivery for timing and Crash outbox for storage caveats.
Privacy controls
- Touch names come from developer-authored props only:
accessibilityLabel,testID, or asazabiNameprop. Child text is never read. AddsazabiMaskto any ancestor to suppress names under it. - The navigation row records param keys, not values. A deep link’s query string stays in the screen path.
- Request and response bodies are never captured.
- Sensitive URL parameters are redacted. The values of tokens, OAuth codes, API keys, and presigned-URL signatures in a request URL become
[redacted]; add your own names withnetwork.redactQueryParams. Credentials in the URL (user:pass@) are never recorded. - Consent gating. Pass
consent: () => boolean | Promise<boolean>and the SDK installs nothing until it resolves true. - Client-asserted identity.
session.distinct_idis whatever the app claims. Treat it as a claim, not as proof of identity. - Contained failures. Each SDK hook catches its own errors, so a failure inside the SDK is not thrown into your app code.
shutdown()removes the network, console, and touch instrumentation; the global error handler and the Hermes rejection tracker cannot be handed back, so they stay installed and do nothing.
Prerequisites
- A React Native app on 0.76 or later, or an Expo app on SDK 52 or later.
- A Sazabi project.
- Optional: a key-value store for the
storageadapter, such as@react-native-async-storage/async-storage.
Set up in the dashboard
1
Open the log source
Go to Settings > Log streams, choose Sazabi React Native SDK, and click Add.2
Copy your intake URL
Sazabi shows your complete intake URL at the top of the setup screen. That URL is all the SDK needs: pass it asintakeUrl, with or without the /v1/logs path.3
Install the SDK
storage adapter in the next step. Leave it
out if your app already has another key-value store.4
Initialize in your entry file
Import the register entrypoint as the first import of your entry module, the file yourpackage.json main field points to: index.js
in a React Native CLI app, index.ts in an Expo app. Under expo-router,
main is expo-router/entry, so add an index.js that imports the SDK
first and expo-router/entry last, and set "main": "index.js". Then
call init():fetch is built on XMLHttpRequest, so one patch sees
fetch, axios, and direct XHR callers. The storage adapter is
optional: any key-value store with getItem, setItem, and removeItem
(AsyncStorage, an MMKV wrapper, expo-sqlite/kv-store) keeps the session
across launches and makes the crash outbox durable. Without it the
session lasts one launch.In a React Native CLI app, put the intake URL in a constant or read it
with react-native-config; process.env is not available at runtime.
In an Expo app you can read it from an EXPO_PUBLIC_* variable.A promise-returning adapter such as AsyncStorage may not finish the crash
outbox write before the process dies, so the SDK also sends the batch at
once, and a crash may arrive twice (both copies carry the same
app.outbox.id). A synchronous store such as MMKV writes the outbox
before the process ends and waits until the next launch to send the crash. Upload retries can still produce duplicate deliveries.init() is idempotent and catches its own errors. Under
react-native-web it installs nothing and logs a warning that the
browser SDK
owns that host, so a universal app can initialize both.5
Capture touches and screens
Wrap your app in<SazabiRoot> and pass your navigation container ref to
trackNavigation(). expo-router exposes the same ref through its
useNavigationContainerRef. Make <SazabiRoot> the outermost component,
outside your own error boundaries and providers: it also records render
errors, and only sees what renders inside it.app/_layout.tsx:<SazabiRoot> is one full-size View that observes touches without
claiming them, so gestures reach your components untouched. It is also an
error boundary: a render error is recorded as fatal and then rethrown,
so the app crashes (or shows its red box) as before. The sazabiName and
sazabiMask props are typed on View, Pressable, Text, and the
Touchable* components once you import the SDK. See
React Native SDK.6
Correlate with your backend
Every request the app makes carries atraceparent header by default: a
native app has no browser origin or CORS preflight. Intake uploads are
excluded; in development, Metro dev-server traffic and URLs containing
/symbolicate are excluded from capture and propagation too. Use
network.allowlist to limit propagation to your own APIs:api.example.com), a domain with every
subdomain (.example.com), or an origin with an optional path prefix. It
matches by origin or host, never as a substring, so a lookalike host gets
no header. A RegExp is tested against the full URL.Your backend has to run each request inside the incoming trace context
for its log lines to share the same trace_id. If it runs OpenTelemetry,
that extraction already happens by default; set
network: { sampledFlag: true } if you also want your backend tracer to
record those traces. The log join works either way.7
Attribute sessions to users (optional)
reset() rotates the session and launch ids, so later activity on the
same device never threads into the previous user’s timeline.8
Hand off the change
Commit these changes on a new branch, push it, and open a pull request. Keep the intake URL in your app’s configuration or environment, and commit only configuration that references it.Set up with the CLI
You can also register the Sazabi React Native SDK log source with the Sazabi CLI (installed and authenticated, see CLI reference). Registering the source mints the same intake URL the dashboard shows above, the only value the SDK needs:Verify
- Use your app: launch it and tap through a few screens. The SDK flushes every 5 seconds by default, and again when the app goes to the background, so events land within about 10 seconds.
-
Check the app: in the Metro console or the device’s network inspector, POSTs to your intake host’s
/v1/logsshould return 200. - Confirm arrival: open the Intake page in the Sazabi dashboard and confirm records from the Sazabi React Native SDK source appear, or ask the agent in a thread: “Show me recent mobile sessions.”
Troubleshooting
No requests to intake at all: confirminit() runs in your entry file. Check the Metro console for the [@sazabi/react-native] configuration warning, which means intakeUrl or serviceName was empty, or the app is running in a browser. If you pass a consent callback, confirm it resolves true.
Requests return 401: the intake URL’s embedded key was deactivated, or the URL belongs to another project. Check the log stream in Settings > Log streams and copy the current intake URL.
Errors and requests arrive, but no touches or screens: the root is not wrapped in <SazabiRoot>, or trackNavigation() was never called with the container ref.
Some requests are missing from the stream: another library captured XMLHttpRequest.prototype.send before the SDK did. Make import "@sazabi/react-native/register"; the first import of the module your package.json main field points to. Under expo-router that is your own index.js, not app/_layout.tsx.
A fatal error’s events never arrived: without a storage adapter there is no crash outbox. With a promise-returning adapter such as AsyncStorage the write may not finish before the process dies and the immediate send may fail; a synchronous store such as MMKV closes that gap. A render error is recorded only when it is thrown inside <SazabiRoot>. The outbox is posted on the next launch. Retryable failures stay for a later launch; permanent 4xx failures are deleted with a warning, so correct the intake URL before generating more events.
Unhandled rejections are missing: rejection tracking needs the Hermes engine, so an app on JavaScriptCore records none. On Hermes, another SDK took the engine’s single rejection-tracker slot after this one; the slot cannot be read or wrapped, so neither side can tell. Initialize @sazabi/react-native last and hand the other SDK’s capture call to it as errors.previousRejectionTracker, or pass errors: { unhandledRejections: false } and let the other SDK report them. A development build prints one [@sazabi/react-native] note when the slot is taken with neither option set. See Other crash reporters.
A warning about a stored crash report that is not valid JSON: the SDK discards that entry instead of posting it and removes it from storage on the same launch, so the warning does not come back on the next one. Nothing else is lost.
Jest fails to load the SDK: the SDK ships ES modules. Add @sazabi/react-native to your transformIgnorePatterns exception; see Jest. Metro resolves @sazabi/react-native/register through the package’s react-native field even without package exports.
Two copies of the SDK in one bundle: keep one copy and import init and SazabiRoot from it. Initialization is shared, but components from another copy can miss touches or render errors.
Stream shows Inactive: not an error. It means no telemetry arrived in the last hour. If you expected telemetry, check that the build calling init() has shipped and someone has launched it since, and that a consent gate is not resolving false.