@tailglow/react-native
v0.4.0
Published
React Native SDK for Tailglow. Auto-collects JS errors, console activity, app lifecycle, and screen views via React Navigation / Expo Router integrations.
Maintainers
Readme
@tailglow/react-native
React Native SDK for Tailglow. Auto-collects JS errors, console activity, app lifecycle, and screen views (via React Navigation or Expo Router).
Stability
0.x is unstable. Breaking changes may ship in any minor (0.1.0 → 0.2.0) release until 1.0.
Supported runtimes
| Runtime | Floor |
| ------------ | --------------------------------- |
| React Native | >=0.74 |
| Expo SDK | >=51 (which uses RN 0.74) |
| Hermes | preferred (default since RN 0.70) |
| JSC | supported |
ESM only. No CJS bundle.
Install
bun add @tailglow/react-native
# or: npm install @tailglow/react-native
# Optional, for cold-start queue persistence:
bun add @react-native-async-storage/async-storageUsage
// App.tsx
import AsyncStorage from "@react-native-async-storage/async-storage";
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { createAsyncStorageAdapter, Tailglow } from "@tailglow/react-native";
import { useEffect } from "react";
import { AppState, Dimensions, NativeModules, Platform } from "react-native";
const tg = new Tailglow({
url: "https://ingest.tailglow.io",
key: "tg_ingest_your_key",
// Inject RN modules (the SDK doesn't import react-native directly,
// so the package stays usable in test environments).
appState: AppState,
platform: Platform,
dimensions: Dimensions.get("window"),
nativeConstants: NativeModules.PlatformConstants,
// Persistent queue across cold starts.
storageAdapter: createAsyncStorageAdapter(AsyncStorage),
// Optional context.
context: { release: "2.1.0", environment: "production" }
});
export default function App() {
const navigationRef = useNavigationContainerRef();
useEffect(() => {
tg.attachNavigation(navigationRef); // → automatic type=page_view records on the events collection
}, []);
return <NavigationContainer ref={navigationRef}>{/* your screens */}</NavigationContainer>;
}After this, the SDK automatically captures:
- JS errors: uncaught exceptions via
ErrorUtils.setGlobalHandler. OnisFatal=true, the SDK firespersistRemaining()(fire-and-forget) so the queued event has a chance to land in AsyncStorage before the runtime tears down. Best-effort, not guaranteed. - Promise rejections:
HermesInternal.enablePromiseRejectionTracker(Hermes default since RN 0.70), with a fallbackunhandledrejectionlistener for JSC. Some bundler/version combos may miss rejections; ifautoConsoleis on, the user-visible "Possible Unhandled Promise Rejection" string still gets captured via the console wrapper. - Console activity:
console.error/console.warnemit records by default;log/info/debugfeed the breadcrumb buffer attached to the next captured error but don't emit by themselves. Configurable viaautoConsole. - App lifecycle: on
AppStatechange to background or inactive, the SDK callspersistRemaining()first (fast AsyncStorage write) and then attempts a best-effort flush. Records survive even when the OS suspends the runtime mid-network because they're already on disk for the next launch. Server-sideevent_iddedupes if records get sent both ways. - Screen views: every navigation transition emits a
type: "page_view"record (to the configuredeventscollection) withfrom_screen,to_screen,duration_ms,nav_type,params. Time-on-screen excludes time the app was backgrounded. Each transition also leaves anavigationbreadcrumb (screen names only, neverparams) attached to the next captured error. - Device: one-time
devicerecord on init (platform, OS version, screen, brand/model when available).
Reliability notes
- Fatal JS crashes: capture is best-effort. The runtime may die before the AsyncStorage write completes. With a configured
storageAdapterthe chance of survival is non-zero but not 100%. - The final screen's duration can be lost if the app is force-killed without returning to active. The duration of the previous screen (already captured on transition) is unaffected.
- Scope: this SDK captures JS-layer behavior. Native module crashes (iOS Objective-C / Swift, Android Java / Kotlin) are not in scope.
Expo Router
Expo Router uses React Navigation under the hood, so the same attachNavigation helper works. Get the underlying navigation container ref from useNavigationContainerRef, exported by expo-router:
// app/_layout.tsx
import { useNavigationContainerRef } from "expo-router";
import { useEffect } from "react";
import { tg } from "./tailglow"; // your Tailglow singleton
export default function Layout() {
const navigationRef = useNavigationContainerRef();
useEffect(() => {
tg.attachNavigation(navigationRef);
}, []);
return /* your <Stack /> or <Tabs /> */;
}page_view records use from_screen / to_screen fields populated from the Expo Router route names (which match the file system paths under app/).
Manual capture
try {
await checkout();
} catch (err) {
tg.captureException(err, { user_step: "checkout", cart_total: 99 });
}
tg.captureMessage("payment validator returned null", { level: "warning" });
tg.track("purchase", { amount: 99, currency: "USD" });Tracking interactions: <TailglowPressable> and useTailglow()
React-aware components live at the @tailglow/react-native/components subpath (separate from the main package entry, which stays loadable in non-RN tooling environments). Both react and react-native must be installed at the customer's site; they are declared as peer dependencies on this package.
For ergonomic click tracking without writing tg.track() in every onPress handler, mount <TailglowProvider> once at the root and use <TailglowPressable> in place of <Pressable>:
// At app root
// In any component
import { TailglowPressable, TailglowProvider } from "@tailglow/react-native/components";
import { tg } from "./tailglow";
function App() {
return (
<TailglowProvider tg={tg}>
<NavigationContainer>{/* screens */}</NavigationContainer>
</TailglowProvider>
);
}
function CheckoutButton() {
return (
<TailglowPressable
event="checkout_clicked"
props={{ cart_total: 247 }}
onPress={() => router.push("/payment")}
>
<Text>Checkout</Text>
</TailglowPressable>
);
}Behavior:
- Fires
tg.track(event, props)BEFORE calling the customer'sonPress. If the customer's handler throws or navigates, the track has already fired. - Without an
eventprop, behaves as a transparentPressable(no track call). Lets you use the same component everywhere and opt into tracking per usage. - Without
<TailglowProvider>mounted above, also behaves as a transparentPressable. Components remain renderable in tests / isolated screens. - Telemetry failures are swallowed, never block the customer's onPress.
For inline access in custom handlers, use the hook:
import { useTailglow } from "@tailglow/react-native/components";
function MyComponent() {
const tg = useTailglow();
return (
<Pressable onPress={() => tg?.track("dismissed_modal", { source: "swipe" })}>
<Text>Dismiss</Text>
</Pressable>
);
}The hook returns null when no provider is mounted, so the ?. is meaningful.
Console capture defaults
autoConsole: ["error", "warn"] by default. console.error and console.warn emit records on the wire; log / info / debug still feed the breadcrumb buffer attached to the next captured error but don't emit by themselves.
// Capture everything as records (data-lake mode)
new Tailglow({ ..., autoConsole: ["log", "warn", "info", "debug", "error"] });
// Disable entirely (no wrapping, no breadcrumbs from console)
new Tailglow({ ..., autoConsole: [] });Cold-start persistence
Pass an AsyncStorage adapter to storageAdapter so records that didn't flush before the OS killed the JS runtime get restored on next launch:
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createAsyncStorageAdapter } from "@tailglow/react-native";
new Tailglow({
...,
storageAdapter: createAsyncStorageAdapter(AsyncStorage)
});The adapter is lazy. If you don't pass it, the SDK still works in-memory; you just lose any records that hadn't successfully POSTed before the app was killed.
Identity
tg.identify("usr_123"); // set the user ID
tg.unidentify(); // clear
tg.setDeviceId("dev_456"); // optional, customer-supplied stable device ID
tg.rotateSession(); // force a new session at workflow boundaries
tg.setContext({ plan: "pro" }); // sticky fields stamped on every recordConfiguration
| Option | Type | Default | Description |
| ------------------ | --------------------- | -------------------------------------------------- | --------------------------------------- |
| url | string | required | Ingest endpoint |
| key | string | required | Ingest key (tg_ingest_...) |
| context | object | | Sticky fields stamped on every record |
| userId | string | | Initial user ID |
| deviceId | string | | Initial device ID |
| storageAdapter | StorageAdapter | | Wrap with createAsyncStorageAdapter() |
| flushInterval | number | 30000 | Auto-flush interval (ms) |
| flushSize | number | 100 | Auto-flush at this record count |
| maxBatchBytes | number | 15000000 | Max bytes per batch |
| maxQueueSize | number | 10000 | Max in-memory records |
| maxRecordBytes | number | 1000000 (1 MB) | Drop records larger than this |
| sessionTimeout | number | 1800000 | Session inactivity timeout |
| sampleRate | number | 1.0 | Sticky sampling rate |
| errorBurst | object | {threshold:10,window_ms:1000,cooldown_ms:120000} | Per-fingerprint rate limit |
| breadcrumbBuffer | number | 100 | Breadcrumb ring size |
| redact | object | | URL token / email / field redaction |
| onBeforeSend | function | | Filter records pre-queue |
| onTransportError | function | | Permanent transport-failure callback |
| autoErrors | boolean | true | Wire global error + rejection handlers |
| autoConsole | string[] | ["error","warn"] | Levels that emit records |
| autoDevice | boolean | true | Send device record on init |
| autoAppState | boolean | true | Subscribe to AppState for flush-on-bg |
| appState | AppState | | Required if autoAppState !== false |
| platform | Platform | | Required if autoDevice !== false |
| dimensions | Dimensions.get(...) | | Required if autoDevice !== false |
| nativeConstants | object | | Optional; adds brand/model fields |
| debug | boolean | false | Console-log SDK activity |
