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

expo-session-capture

v1.0.0

Published

Plug-and-play visual session capture for Expo / React Native apps

Readme

Expo Session Capture

🚧 BETA — This product is currently in beta. Request early access to get your API key and start capturing sessions.

Plug-and-play visual session capture for Expo / React Native apps.

Captures low-resolution screenshots, taps, scrolls, and screen navigations with deterministic sampling, throttle, hard frame cap, periodic flush, idle detection, and automatic background flush — without any native code.

Works in Expo Go and EAS managed builds. No config plugins. No custom dev client.

See it in action → Check out the demo app for a fully working example that exercises every SDK feature.


Table of Contents


Installation

Install the SDK and its peer dependencies:

npx expo install react-native-view-shot expo-constants expo-device
npm install expo-session-capture

Quick Start

1. Get your API key

Expo Session Capture is currently in beta. To get your API key, request early access and we'll set you up with an organisation and credentials.

2. Wrap your app in SessionCaptureProvider

// app/_layout.tsx  (Expo Router)
import { Stack, useNavigationContainerRef } from 'expo-router';
import { SessionCaptureProvider, NavigationTracker } from 'expo-session-capture';

export default function RootLayout() {
  const navigationRef = useNavigationContainerRef();

  return (
    <SessionCaptureProvider
      apiKey="sc_live_xxxxxxxxxxxxx"
      endpointUrl="https://api.server-less.com"
      samplingRate={0.1}   // capture 10 % of users
    >
      <Stack />
      <NavigationTracker navigationRef={navigationRef} />
    </SessionCaptureProvider>
  );
}

That's it — taps, screenshots, scrolls, and navigation events are now captured and uploaded automatically.

3. (Optional) Identify the user after login

import { useSessionCapture } from 'expo-session-capture';

function LoginScreen() {
  const { identify } = useSessionCapture();

  const handleLogin = async () => {
    const user = await api.login(email, password);
    identify(user.id); // link this session to the real user
  };
}

4. (Optional) Use TrackedPressable for enriched tap data

import { TrackedPressable } from 'expo-session-capture';

<TrackedPressable
  trackingLabel="Buy now"
  trackingCategory="conversion"
  tapScreen="ProductScreen"
  onPress={handleBuy}
>
  <Text>Buy now</Text>
</TrackedPressable>

5. (Optional) Track scroll depth with TrackedScrollView

import { TrackedScrollView } from 'expo-session-capture';

<TrackedScrollView scrollThreshold={200}>
  {/* long scrollable content */}
</TrackedScrollView>

API Reference

SessionCaptureProvider

The root context provider. Wrap your entire app (or the part you want to capture) in this component.

import { SessionCaptureProvider } from 'expo-session-capture';

Props

All props from SessionCaptureConfig plus:

| Prop | Type | Default | Description | |---|---|---|---| | apiKey | string | required | API key from your dashboard. Used to authenticate uploads. | | endpointUrl | string | required | Base URL of your backend. The SDK appends /ingest automatically. | | userId | string | auto-generated | Stable user identifier for deterministic sampling. If omitted an anonymous UUID is created; call identify() later. | | samplingRate | number | 0.1 | Fraction of users to sample (0 – 1). 1.0 = capture everyone. | | maxFrames | number | 500 | Hard cap on screenshots per session. | | throttleMs | number | 200 | Minimum ms between interaction-triggered captures. | | imageQuality | number | 0.1 | JPEG quality (0 – 1). Lower = smaller payload. | | imageWidth | number | screen width | Width in px for captured screenshots. | | imageHeight | number | screen height | Height in px for captured screenshots. | | flushIntervalMs | number | 10000 | How often (ms) buffered data is uploaded. | | periodicCaptureMs | number | 1000 | Interval (ms) for automatic background screenshots. 0 disables. | | idleTimeoutMs | number | 10000 | Ms of inactivity before periodic captures pause. 0 disables idle detection. | | enableGlobalPressCapture | boolean | true | Auto-capture all Pressable / TouchableOpacity / TouchableHighlight taps. |


useSessionCapture

React hook to access the capture context. Must be called inside a <SessionCaptureProvider>.

import { useSessionCapture } from 'expo-session-capture';

Return value (CaptureContextValue)

| Field | Type | Description | |---|---|---| | manager | CaptureManager | The underlying manager instance. Use manager.capturedFrames to read the current frame count. | | rootRef | RefObject<View> | Ref to the root view being screenshotted. | | isActive | boolean | Whether this user was sampled and capture is running. | | identify | (userId: string) => void | Associate the session with a real user after login. Replaces the anonymous ID for all future uploads. | | userId | string | The current user ID (anonymous or identified). | | isAnonymous | boolean | true until identify() is called or a userId prop is provided. |

Usage

function StatusBar() {
  const { isActive, manager, userId, isAnonymous, identify } = useSessionCapture();

  return (
    <Text>
      {isActive ? `Capturing · ${manager.capturedFrames} frames` : 'Not sampled'}
      {' · '}
      {isAnonymous ? 'Anonymous' : userId}
    </Text>
  );
}

NavigationTracker

A renderless component that listens to React Navigation state changes and emits navigation events (screen transitions) onto the tracking bus.

import { NavigationTracker } from 'expo-session-capture';

Props

| Prop | Type | Default | Description | |---|---|---|---| | navigationRef | { current: any } | auto-resolved | A React Navigation NavigationContainerRef. Pass the ref from useNavigationContainerRef() (expo-router) or the ref on <NavigationContainer>. If omitted, the component tries to resolve it from @react-navigation/native at runtime. |

Tracked triggers

The component automatically infers how each navigation was triggered:

| Trigger | Meaning | |---|---| | push | Programmatic navigation or NAVIGATE action | | back-button | GO_BACK or POP action | | swipe-back | iOS swipe-back gesture (no explicit action, depth decreased) | | tab | Tab switch | | pop | POP_TO_TOP | | replace | REPLACE action | | unknown | Could not be determined |

Placement

Place it inside <SessionCaptureProvider>, alongside your navigator:

<SessionCaptureProvider ...>
  <Stack />
  <NavigationTracker navigationRef={navigationRef} />
</SessionCaptureProvider>

TrackedPressable

Drop-in replacement for React Native's <Pressable> that emits an explicit tracking event on every press. The global press-capture layer automatically skips these handlers so there are never duplicate events.

import { TrackedPressable } from 'expo-session-capture';

Props

All standard PressableProps plus:

| Prop | Type | Default | Description | |---|---|---|---| | trackingLabel | string | — | Human-readable label for the tap (e.g. "Add to cart"). | | trackingCategory | string | — | Logical category (e.g. "conversion", "navigation"). | | tapScreen | string | — | Screen name to associate with the tap event. | | trackingMetadata | Record<string, unknown> | — | Arbitrary extra data sent with the event. |

Example

<TrackedPressable
  trackingLabel="Remove item"
  trackingCategory="cart"
  tapScreen="CartScreen"
  onPress={() => removeItem(id)}
>
  <Text>Remove</Text>
</TrackedPressable>

TrackedScrollView

Drop-in replacement for React Native's <ScrollView> that captures a screenshot when scrolling ends and the vertical offset has changed by more than scrollThreshold pixels since the last capture.

import { TrackedScrollView } from 'expo-session-capture';

Props

All standard ScrollViewProps plus:

| Prop | Type | Default | Description | |---|---|---|---| | scrollThreshold | number | 200 | Minimum vertical offset change (px) before a screenshot is taken. |

Example

<TrackedScrollView scrollThreshold={150}>
  {articles.map(a => <ArticleCard key={a.id} article={a} />)}
</TrackedScrollView>

Utility Exports

These are exported for advanced use cases. Most apps won't need them directly.

| Export | Description | |---|---| | CaptureManager | Class that manages throttled screenshot capture, buffering, and batch upload. Accessed via useSessionCapture().manager. | | shouldSample(userId, rate) | Pure function — returns true if the user should be sampled at the given rate. Deterministic (same input → same output). | | installGlobalPressCapture() | Monkey-patches React.createElement to auto-capture all pressable taps. Called automatically when enableGlobalPressCapture is true. | | emitTrackingEvent(event) | Emit a custom tracking event onto the internal bus. | | onTrackingEvent(handler) | Subscribe to all tracking events. Returns an unsubscribe function. |

Types

All TypeScript types are exported for use in your own code:

import type {
  SessionCaptureConfig,
  CapturedFrame,
  TapEvent,
  ScrollEvent,
  NavigationEvent,
  TrackingEvent,
  UploadPayload,
  CaptureContextValue,
  DeviceInfo,
} from 'expo-session-capture';

How It Works

Deterministic sampling

The same userId always maps to the same sampled/not-sampled bucket (stable hash of the user ID). A user is either always captured or never captured within a given rate — no inconsistent experiences across sessions.

Global press capture

When enableGlobalPressCapture is true (default), the SDK patches React.createElement at startup to intercept onPress on all Pressable, TouchableOpacity, and TouchableHighlight components. Labels are inferred from accessibilityLabel, aria-label, or testID. Handlers created by TrackedPressable are automatically skipped to avoid duplicates.

Screenshot capture

Screenshots are taken via react-native-view-shot on the root <View> ref. Captures are triggered by:

  1. User interaction — a tap or meaningful scroll triggers an immediate capture plus a follow-up ~300 ms later to record the resulting UI change.
  2. Periodic timer — a background screenshot every periodicCaptureMs (default 1 s).
  3. Navigation — a frame before and two frames after every screen transition.

All captures are throttled by throttleMs and capped at maxFrames.

Idle detection

If no interaction (tap, scroll, navigation) occurs for idleTimeoutMs, periodic captures are paused. They resume automatically on the next interaction.

Flush & upload

Buffered frames, taps, scrolls, and navigation events are uploaded to {endpointUrl}/ingest every flushIntervalMs (default 10 s). A flush also fires automatically when the app moves to background or becomes inactive (AppState change).

Non-blocking

All capture and upload operations are fire-and-forget. Errors are silently swallowed so the SDK never crashes or degrades the host app.


Examples

Minimal setup (Expo Router)

import { Stack, useNavigationContainerRef } from 'expo-router';
import { SessionCaptureProvider, NavigationTracker } from 'expo-session-capture';

export default function RootLayout() {
  const navigationRef = useNavigationContainerRef();

  return (
    <SessionCaptureProvider
      apiKey="sc_live_xxxxxxxxxxxxx"
      endpointUrl="https://api.server-less.com"
    >
      <Stack />
      <NavigationTracker navigationRef={navigationRef} />
    </SessionCaptureProvider>
  );
}

With user identification

<SessionCaptureProvider
  apiKey="sc_live_xxxxxxxxxxxxx"
  endpointUrl="https://api.server-less.com"
  userId={currentUser?.id}          // sampled deterministically
  samplingRate={0.25}               // 25 % of users
  maxFrames={200}
  flushIntervalMs={15_000}
>
  {children}
</SessionCaptureProvider>

Identifying a user after login

const { identify, isAnonymous } = useSessionCapture();

async function onLogin() {
  const user = await api.login(email, password);
  identify(user.id); // session is now linked to this user
}

Reading capture status

const { isActive, manager } = useSessionCapture();

if (isActive) {
  console.log(`Captured ${manager.capturedFrames} frames so far`);
}

License

MIT