Segi (extn.ai) error-tracking SDK for React Native.
Segi ships official Next.js / browser SDKs but no React Native SDK. This package is a thin, dependency-free client that talks directly to the Segi ingest endpoint, plus a native module that captures iOS and Android native crashes (not just JS errors).
| Layer | iOS | Android |
|---|---|---|
Uncaught JS errors (ErrorUtils) |
✅ | ✅ |
| Unhandled promise rejections | ✅ | ✅ |
React render crashes (SegiErrorBoundary) |
✅ | ✅ |
| Native uncaught exceptions | ✅ NSException |
✅ JVM Thread handler |
| Native signals (NDK / C++) | ✅ SIGSEGV/SIGABRT/… |
✅ SIGSEGV/SIGABRT/… (NDK sigaction) |
| App hangs / ANR (main-thread) | ✅ watchdog (opt-in) | ✅ watchdog (opt-in) |
Manual captureSegiException / captureSegiMessage |
✅ | ✅ |
Native crashes can't be sent during the crash itself, so they are persisted to disk and replayed to Segi on the next app launch.
Every event is automatically enriched the way Sentry's RN SDK enriches its events:
| Feature | What it does |
|---|---|
| Breadcrumbs | A trail attached to each event. Automatic: console.*, fetch + XMLHttpRequest (method/url/status/duration), app foreground/background. Manual: addSegiBreadcrumb, addSegiNavigationBreadcrumb. Ring-buffered (maxBreadcrumbs, default 50), filterable via setSegiBeforeBreadcrumb. |
| Global scope | setSegiUser / setSegiTag(s) / setSegiExtra(s) / setSegiContext persist across all events — set once, attached everywhere. |
| Device / OS / app context | contexts.device/os/app/runtime auto-built from RN core (model, OS version, screen, Hermes/JSC, simulator flag) — no extra deps. |
| Sampling | sampleRate (0..1) probabilistically drops events. Fatal crashes always bypass it. |
| Dedup | Identical consecutive events inside dedupeWindowMs (default 2s) are suppressed. |
| Offline retry | Events that fail to send are queued and retried (flushSegiRetryQueue, also auto-flushed after the next success). |
| UI taps | SegiTouchEventBoundary records a ui.tap breadcrumb per touch. |
attachStacktrace |
Adds a synthetic stack to captureSegiMessage events. |
| PII scrubbing | Runs over the whole envelope — breadcrumbs and contexts included. |
Captured (in-process handlers):
- iOS:
NSException(symbolicatedcallStackSymbols) + POSIX signalsSIGSEGV/SIGABRT/SIGBUS/SIGFPE/SIGILL/SIGTRAP/SIGSYS, on an alternate signal stack so stack-overflow crashes are caught. NSException→abort()is de-duplicated. - Android: JVM uncaught exceptions (full Java stack) + NDK/C++ signals via
sigaction+_Unwind_Backtrace. Frames are written as#NN pc <module-offset> <lib.so> (<symbol>+<off>)— symbolicate withndk-stack/addr2lineagainst the unstripped.so(offsets are load-base-relative). - Both: re-entrancy guarded; previous handlers (e.g. Sentry) are chained and the signal is re-raised so the OS tombstone / other reporters still fire.
Not capturable in-process (no on-device crash reporter can catch these; out of scope):
- OS terminations: iOS OOM/Jetsam, watchdog
0x8BADF00D,SIGKILL; Android low-memory kills. - Crashes before
initSegi()/ module load runs (earliest app startup). - (App hangs / ANRs are covered now via the opt-in
startAppHangWatchdog, but a hang that the OS turns into a hard kill is still subject to the OS-termination limit above.) - Full source-level symbolication happens off-device: ship dSYM (iOS) / unstripped
.so(Android) to symbolicate the frames the SDK records.
npm install @bareecorporation/segi-react-native
# iOS
cd ios && pod installAutolinking handles both platforms (RN 0.71+). No manual native registration needed.
// index.js — as early as possible, before rendering the app.
import {
initSegi,
installSegiGlobalHandlers,
} from '@bareecorporation/segi-react-native';
initSegi({
projectKey: 'segi_pk_live_xxxxxxxxxxxxxxxx', // project key (allowedDomains: [] for native)
environment: 'production',
release: '1.4.2', // app version or CodePush label
// enableNativeCrashTracking: true (default) — installs native handlers + replays prior crashes
});
installSegiGlobalHandlers(); // JS uncaught errors + unhandled promise rejections
// Optional: detect main-thread hangs / ANRs (reports the main thread stack).
import { startAppHangWatchdog } from '@bareecorporation/segi-react-native';
startAppHangWatchdog({ thresholdMs: 5000 });On Hermes, unhandled rejections use the runtime's native
HermesInternal.enablePromiseRejectionTracker; JSC keeps the React Native promise
polyfill fallback. onCapturedError can mirror the same global error to a second
reporter without replacing Segi's handler:
installSegiGlobalHandlers({
onCapturedError: (error, details) => secondaryReporter(error, details),
});The watchdog pings the UI/main thread; if it stays unresponsive past thresholdMs
(default 5000), it captures the main thread stack (Android Looper; iOS via mach
thread_suspend + frame-pointer unwind) and records an ApplicationNotResponding event,
replayed to Segi on the next launch.
import { SegiErrorBoundary } from '@bareecorporation/segi-react-native/error-boundary';
export default function App() {
return (
<SegiErrorBoundary fallback={(error, reset) => <Crash error={error} onRetry={reset} />}>
<RootNavigator />
</SegiErrorBoundary>
);
}import { captureSegiException, captureSegiMessage } from '@bareecorporation/segi-react-native';
try {
await pay();
} catch (e) {
captureSegiException(e, {
tags: { feature: 'checkout' },
extra: { orderId },
user: { id: userId },
screen: 'CheckoutScreen',
});
}
captureSegiMessage('coupon applied without discount', { level: 'warning' });| Export | Description |
|---|---|
initSegi(config) |
Initialise. Installs native handlers + replays prior native crashes. |
installSegiGlobalHandlers(opts?) |
Hook JS uncaught errors + unhandled rejections. |
captureSegiException(err, ctx?) |
Report an exception (fire-and-forget). |
captureSegiMessage(msg, ctx?) |
Report a message (default level info). |
setSegiBeforeSend(fn) |
Transform or drop (return null) every event before send. |
flushNativeCrashes() |
Manually replay persisted native crashes. Returns the count. |
startAppHangWatchdog({thresholdMs?}) |
Detect main-thread hangs / ANRs. |
stopAppHangWatchdog() |
Stop the main-thread watchdog. |
setSegiUser(user) |
Attach a user to all events (null clears). |
setSegiTag(k,v) / setSegiTags(obj) |
Global tags. |
setSegiExtra(k,v) / setSegiExtras(obj) |
Global extras. |
setSegiContext(name, obj) |
Named context group (null removes). |
configureSegiScope(update) / clearSegiScope() |
Bulk set / reset scope. |
addSegiBreadcrumb(b) |
Record a breadcrumb. |
addSegiNavigationBreadcrumb(from, to) |
Navigation breadcrumb helper. |
setSegiBeforeBreadcrumb(fn) |
Transform/drop breadcrumbs. |
setSegiMaxBreadcrumbs(n) |
Ring-buffer size. |
flushSegiRetryQueue() |
Retry events that failed to send (e.g. on AppState 'active'). |
setSegiBeforeSend(fn) |
Transform or drop every event before send. |
isSegiEnabled() / logSegiStatus() |
Status helpers. |
isNativeCrashTrackingAvailable() |
Whether the native module is linked. |
SegiErrorBoundary |
React error boundary (/error-boundary entry). |
SegiTouchEventBoundary |
UI-tap breadcrumbs (/touch-boundary entry). |
import {
setSegiUser, setSegiTag, setSegiContext,
addSegiBreadcrumb, addSegiNavigationBreadcrumb,
} from '@bareecorporation/segi-react-native';
// set once after login — attached to every later event
setSegiUser({ id: userId, email });
setSegiTag('plan', 'pro');
setSegiContext('subscription', { id, status });
// manual breadcrumbs (console / fetch / app-state are automatic)
addSegiBreadcrumb({ category: 'checkout', message: 'coupon applied', data: { code } });
// in your navigation listener
addSegiNavigationBreadcrumb(prevRoute, nextRoute);// record a ui.tap breadcrumb per touch
import { SegiTouchEventBoundary } from '@bareecorporation/segi-react-native/touch-boundary';
<SegiTouchEventBoundary><App /></SegiTouchEventBoundary>;| Field | Default | Notes |
|---|---|---|
projectKey |
— | Required. Without it the SDK stays disabled. |
ingestUrl |
https://segiapi.extn.ai/api/ingest/events |
Override endpoint. |
environment |
production |
|
release |
— | App version / CodePush label. |
enabled |
true |
Master kill switch. |
timeoutMs |
3000 |
Per-event network timeout. |
debug |
false |
console.debug diagnostics. |
defaultTags |
— | Tags on every event. |
sampleRate |
1 |
Probability (0..1) an event is sent. Fatal crashes bypass. |
maxBreadcrumbs |
50 |
Breadcrumb ring-buffer size. |
enableAutoBreadcrumbs |
true |
true/false or { console?, network?, appState? }. |
attachStacktrace |
false |
Synthetic stack on captureSegiMessage. |
sendDefaultPii |
false |
Include mildly-identifying device context. |
dedupeWindowMs |
2000 |
Suppress identical consecutive events; 0 disables. |
dist |
— | Distribution / build number. |
enableNativeCrashTracking |
true |
Install native handlers + replay prior crashes. |
symbolication |
— | Resolve Hermes frames on-device. See below. |
Release bundles are Hermes bytecode, so JS frames arrive as bytecode offsets and the same bug fragments into a new issue per release:
at anonymous (address at /var/.../CodePush/<hash>/CodePush/main.jsbundle:1:6667710)
The SDK can resolve those frames on-device, before the event is sent, so the ingest server needs no sourcemap support and grouping keys on real source positions.
It needs two things:
-
A symbol index next to the JS bundle. Your build pipeline generates it from the composed Hermes sourcemap (metro map + hermes map) and ships it beside the bundle — for CodePush, inside the update zip as
CodePush/segi-symbols.json. Format (parallel arrays,offdelta-encoded,src: -1marks "no data from here"):{"v":1,"sources":["/src/a.ts"],"names":["fetchPlaces"], "off":[1000,1000],"src":[0,-1],"line":[32,-1],"name":[0,-1]}Keep the whole map out of it — an index with only your own sources at full mapping fidelity plus function entry points for
node_modulesis ~500KB compressed for a 12MB bundle, and resolves your own frames exactly. -
A file reader, because RN core has no filesystem:
import {FileSystem} from 'react-native-file-access'; initSegi({ projectKey: 'segi_pk_live_…', symbolication: {readTextFile: path => FileSystem.readFile(path, 'utf8')}, });
The index path is derived from the crash's own frames (the bundle's absolute path is in the frame text), so no bundle-manager API is involved and OTA/store bundles work the same way.
Behavior worth knowing:
- Only app-bundle frames are rewritten.
InternalBytecode.js(Hermes async/Promise polyfills),[native code]and native frames are left alone — running their offsets through the index would invent plausible but wrong locations. - An address past a
src: -1boundary is left raw. An unresolved frame beats a wrong one. - A missing/oversized/malformed index disables symbolication for that bundle after one attempt; events still send with raw stacks.
- The parsed index is released
retainMs(default 30s) after the last use, so it does not stay resident between error bursts. fatalevents skip a cold index load — the app is dying and a file read can cost the report. They are symbolicated when the index is already resident, and native crashes replayed on the next launch go through the normal path.
Every payload is recursively PII-scrubbed before send: keys matching
password, *token, *secret, apiKey, authorization, cardNumber, rrn,
residentNumber are masked to [Filtered], and sensitive headers
(authorization, cookie, x-api-key, …) are dropped. Use setSegiBeforeSend for
additional redaction or to drop events entirely.
The key environment determines allowedDomains. Native apps send with no Origin
header, so a production key (unrestricted) is recommended. Server/native ingest is
not blocked by the domain allowlist regardless.
MIT © Baree Corporation