@ensora/react-native
v0.1.0
Published
Mobile-native observability and behavioral analytics SDK for React Native and Expo
Readme
@ensora/react-native
React Native and Expo SDK for the Ensora mobile observability platform. Drop in <EnsoraProvider> and get session analytics, screen-flow tracking, error capture, and touch heatmaps with zero configuration.
What's captured automatically
| Event | Trigger | Requires |
|---|---|---|
| session_start | App launch, foreground resume after 30 min background | nothing |
| nav | Every Expo Router pathname change | Expo Router |
| error | Unhandled JS exceptions and promise rejections | nothing |
| touch | Any touch with x/y coordinates | touchCapture: true in config |
Everything else uses the manual track() API.
Installation
npm install @ensora/react-native \
@react-native-async-storage/async-storage \
expo-application \
expo-deviceexpo-application and expo-device are optional — the SDK falls back gracefully to "unknown" / "0.0.0" if they are not installed.
Quick start
1. Wrap your root layout
// app/_layout.tsx (Expo Router)
import { Stack } from 'expo-router';
import { EnsoraProvider } from '@ensora/react-native';
export default function RootLayout() {
return (
<EnsoraProvider
config={{
apiKey: 'pk_live_...',
ingestURL: 'https://ingest.yourapp.com',
}}
>
<Stack />
</EnsoraProvider>
);
}That's all you need. The provider automatically:
- Fires
session_starton mount with device metadata - Listens to Expo Router pathname changes and fires
navevents - Installs a global JS error handler for automatic
errorcapture - Flushes the event queue every 5 seconds
- Flushes immediately when the app backgrounds
- Persists unsent events in AsyncStorage and retries on next launch
2. Track custom events
import { useTrack } from '@ensora/react-native';
export function SignupButton() {
const track = useTrack();
return (
<Pressable onPress={() => track('signup_clicked', { plan: 'starter' })}>
<Text>Sign Up</Text>
</Pressable>
);
}3. Access the client directly
import { useEnsora } from '@ensora/react-native';
export function CheckoutScreen() {
const client = useEnsora();
const handlePurchase = async () => {
try {
await processPurchase();
client.track('purchase_completed', { amount: 29, currency: 'USD' });
} catch (error) {
client.captureError(error as Error);
}
};
}Configuration
<EnsoraProvider
config={{
apiKey: 'pk_live_...', // required — from POST /v1/mgmt/projects/:id/keys
ingestURL: 'https://...', // required — no trailing slash
sessionTimeout: 1800000, // optional — ms before new session on foreground; default 30 min
touchCapture: true, // optional — heatmap coordinate capture; default false
debug: true, // optional — log all events to console; default false
}}
>API reference
<EnsoraProvider config={...}>
Root provider. Must wrap your entire app. Creates and manages the EnsoraClient instance.
useEnsora(): EnsoraClient
Returns the EnsoraClient from context. Throws if called outside <EnsoraProvider>.
useTrack()
Shorthand hook. Returns a stable track(eventName, properties) function.
const track = useTrack();
track('button_pressed', { button_id: 'cta_hero' });client.track(eventName, properties?)
Record a custom analytics event.
client.track('video_played', { video_id: 'abc', duration_s: 142 });client.screen(screenName, prevScreenName)
Record a navigation event manually. Called automatically when using Expo Router.
client.screen('/profile', '/settings');client.captureError(error, isFatal?)
Record an error event. Called automatically by the global error handler, but can be called manually from catch blocks.
try {
await fetchData();
} catch (err) {
client.captureError(err as Error);
}client.captureTouch(x, y, touchType)
Record a touch coordinate event. Called automatically when touchCapture: true.
client.flush()
Force-flush the event queue immediately. Useful before critical user actions.
await client.flush();client.destroy()
Stop the client, remove event listeners, clear the flush interval. Called automatically by EnsoraProvider on unmount.
<EnsoraErrorBoundary fallback={...}>
React Error Boundary that automatically calls client.captureError() when a child component throws during render.
<EnsoraErrorBoundary fallback={<Text>Something went wrong</Text>}>
<RiskyComponent />
</EnsoraErrorBoundary>Offline support
Events are persisted in AsyncStorage under the key @ensora/queue before being sent. If the network is unavailable:
- Events accumulate in the queue (capped at 500; oldest dropped when full)
- Each flush attempt retries up to 3 times with exponential backoff (500 ms → 1 s → 2 s)
- On
503(server unavailable): re-queued for next flush - On
4xx(bad request): permanently dropped - On app relaunch: the queue is replayed automatically
Session lifecycle
A new session is created when:
- The app launches for the first time
- The app returns to the foreground after being backgrounded for longer than
sessionTimeout(default: 30 minutes)
Each session fires a session_start event with:
app_version— fromexpo-application(Application.nativeApplicationVersion)os_name—Platform.OS("ios"or"android")os_version—Platform.Versiondevice_model— fromexpo-device(Device.modelName)screen_width/screen_height— fromDimensions.get('screen')
Touch capture
Enable with touchCapture: true. A root-level PanResponder observes all touches without consuming them (child interactions are unaffected). Each touch records:
x,y— coordinates relative to the screentouch_type—"tap"(< 10 px movement),"swipe"(≥ 10 px), or"long_press"(> 500 ms hold)screen_name— the current Expo Router pathname
Touch data is aggregated on the backend into heatmaps accessible via GET /v1/heatmaps.
Error capture
Two handlers are installed by initialize():
ErrorUtils.setGlobalHandler— catches all unhandled JS exceptions (RN's global handler). The previous handler is always called after Ensora's handler, so existing crash reporters (Sentry, Bugsnag) continue to work.HermesInternal.enablePromiseRejectionTracker— catches unhandled promise rejections on the Hermes engine (default in React Native 0.70+).
Use <EnsoraErrorBoundary> to capture errors during React rendering (neither of the above covers render errors).
Without Expo Router
If you use React Navigation instead of Expo Router, disable automatic nav capture by not including expo-router in your project. Call client.screen() manually from your navigation listeners:
// React Navigation example
useEffect(() => {
return navigation.addListener('focus', () => {
client.screen(route.name, prevRoute?.name ?? '/');
});
}, [navigation]);Testing
npm test # 24 unit tests (queue, session, client)
npm run typecheck # TypeScript strict mode check
npm run integration # end-to-end test against local backend (requires make run)The integration test sends all 6 event types to http://localhost:8080 and verifies each row lands in the correct ClickHouse table.
Compatibility
| Dependency | Minimum version |
|---|---|
| react-native | 0.73 |
| expo | SDK 51 |
| expo-router | 3.x (optional) |
| @react-native-async-storage/async-storage | 1.19 |
| Node.js (dev) | 20 |
