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

@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-device

expo-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_start on mount with device metadata
  • Listens to Expo Router pathname changes and fires nav events
  • Installs a global JS error handler for automatic error capture
  • 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:

  1. The app launches for the first time
  2. 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 — from expo-application (Application.nativeApplicationVersion)
  • os_namePlatform.OS ("ios" or "android")
  • os_versionPlatform.Version
  • device_model — from expo-device (Device.modelName)
  • screen_width / screen_height — from Dimensions.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 screen
  • touch_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():

  1. 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.

  2. 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 |