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

@clickstreamhq/react-native

v0.1.0-alpha.1

Published

React Native + Expo SDK for ClickStream. Hermes-safe tracker (no crypto.subtle / no DOM) with screen()/tap()/identify(), offline batching, AppState session rotation, and a pre-wired @clickstreamhq/signals re-export.

Readme

@clickstreamhq/react-native

The React Native + Expo SDK for ClickStream. A Hermes-safe tracker — no crypto.subtle, no DOM, no window/document — with screen() / tap() / identify(), offline-durable batching, automatic session rotation, and a pre-wired @clickstreamhq/signals read API so you can personalize a screen from live visitor context.

Peer deps (optional): react, react-native · Runtime dep: @clickstreamhq/signals

No Origin spoofing. No fake web URLs. Native apps authenticate with a dedicated mobile key (cs_mob_live_…) issued by your ClickStream dashboard. Mobile keys are exempt from the browser-provenance gate at the collector, so there is no Origin/Referer header to fake and no pretend https:// page URL — you send real screen names.

Install

# npm
npm install @clickstreamhq/react-native @clickstreamhq/signals @react-native-async-storage/async-storage
# or pnpm / yarn
pnpm add @clickstreamhq/react-native @clickstreamhq/signals @react-native-async-storage/async-storage

@react-native-async-storage/async-storage is what persists the visitor id, session, and the offline event queue across app restarts. It's optional — the SDK falls back to an in-memory store — but production apps should pass it, or every cold start looks like a brand-new visitor.

Expo: npx expo install @react-native-async-storage/async-storage.

First 5 minutes

1. Create the client once

Create the tracker in a module so the whole app shares one instance.

// lib/clickstream.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AppState } from 'react-native';
import { createClickStream } from '@clickstreamhq/react-native';

export const cs = createClickStream({
  // Your dashboard's dedicated MOBILE key (Developers → Install → Mobile app).
  apiKey: 'cs_mob_live_xxxxxxxxxxxx',
  // Your verified first-party tracking domain (the collector). Required.
  endpoint: 'https://t.yourapp.com',

  // Persistence + lifecycle wiring.
  storage: AsyncStorage,        // survives app restarts (offline queue, visitor id)
  appStateProvider: AppState,   // drives 30-min idle session rotation on foreground

  // Used to build device.userAgent (the field the collector reads): "Acme/2.1.0 iOS".
  appName: 'Acme',
  appVersion: '2.1.0',
});

That's it — events now persist offline and flush automatically.

2. Track screens and taps

import { cs } from './lib/clickstream';

// Screen view. The name IS the path — no URL required.
cs.screen('CheckoutScreen', { title: 'Checkout' });

// A tap (custom event, category "tap").
cs.tap('add_to_cart', { sku: 'SKU-123', value: 49.99 });

// Any custom event.
cs.trackEvent('signup_completed', { plan: 'pro' });

screen(), tap(), trackEvent(), and identify() are fire-and-forget — they never throw into your UI and never block a render. The SDK batches them and flushes on size, on a timer, and whenever the app returns to the foreground.

Wire screen() to your navigator so every route logs automatically:

// App.tsx — React Navigation
import { NavigationContainer } from '@react-navigation/native';
import { cs } from './lib/clickstream';

export default function App() {
  return (
    <NavigationContainer
      onStateChange={(state) => {
        const route = state?.routes[state.index];
        if (route) cs.screen(route.name);
      }}
    >
      {/* ...navigators... */}
    </NavigationContainer>
  );
}

3. Identify a known user

Hashing runs on-device in pure JS (no native module, no WebCrypto polyfill): email → SHA-256 (hem) + MD5 (hemMd5), phone → SHA-256 of the normalized E.164 number. These hash identically to the web SDK, so the same person resolves to one identity across web and mobile.

cs.identify('[email protected]', {
  phone: '+1 (415) 555-1234',
  userId: 'u_42',
});

Raw email/phone are forwarded only when identity-resolution consent is granted (the collector encrypts them per-site behind the password-gated reveal flow). The hashes are always safe to send.

4. Consent & privacy

// Turn collection on/off at runtime. analytics:false halts ALL transport.
cs.setConsent({ analytics: true, identityResolution: true });

// On logout / "forget me": clears the visitor id, session, and queued events,
// then mints a fresh anonymous visitor id.
await cs.reset();

Events recorded while analytics consent is off are dropped at call time — they are never queued or sent, even if consent is granted a moment later.

5. Read Signals to personalize a screen

The Signals developer API gives you live visitor context — bot classification, identity status, and behavioral scores (intent, frustration, churn risk, …). The mobile package pre-wires it to the visitor id your tracker already stores, so there's no cookie or DOM involved.

// lib/clickstream.ts (continued)
import { wireSignals } from '@clickstreamhq/react-native/signals';
import { cs } from './lib/clickstream';

// Configure Signals against the same mobile key + endpoint + stored visitor id.
wireSignals(cs);
// screens/Pricing.tsx
import { useEffect, useState } from 'react';
import { getVisitorOrNull, type VisitorContext } from '@clickstreamhq/react-native/signals';

export function Pricing() {
  const [visitor, setVisitor] = useState<VisitorContext | null>(null);

  useEffect(() => {
    // Fails open to null if Signals is unavailable — your default UI still renders.
    getVisitorOrNull().then(setVisitor);
  }, []);

  const highIntent = (visitor?.scores?.intent ?? 0) > 70;
  return highIntent ? <UrgentOffer /> : <StandardPricing />;
}

Subscribe to live updates while a screen is mounted:

import { onVisitor } from '@clickstreamhq/react-native/signals';

const sub = onVisitor((visitor) => {
  // called on each poll with the latest snapshot
});
// later, on unmount:
sub.unsubscribe();

The full Signals surface (getVisitor, getVisitorOrNull, onVisitor, waitFor, isBot, isHighIntent, isIdentified, applySignals, …) is re-exported from @clickstreamhq/react-native/signals, so you import from one place.

How it works

| Concern | Behavior | |---|---| | Visitor id | A cs_… id minted once and persisted via your storage adapter. Survives restarts; rotates only on reset(). | | Session | Rotates after 30 min of inactivity (configurable via sessionTimeoutMs), re-checked on every app foreground. | | Batching | Buffers up to batchSize events (default 20), then flushes. Also flushes on a timer (flushIntervalMs, default 10s) and on foreground. The on-the-wire request is always split into the collector's 25-event cap. | | Offline | The queue is persisted through your storage adapter. A failed flush leaves events queued; they replay on the next flush or app launch. Bounded to maxQueueSize (default 1000, oldest dropped). | | Retry | Bounded retry with backoff on 5xx/429/network errors. A permanent 4xx drops the batch so it never jams the queue. | | Platform | Every event sets device.clientPlatform (default 'react-native'; pass platform: 'ios' \| 'android') and device.userAgent. These are the fields the collector reads to relax the screen schema and branch native bot detection. |

Configuration

createClickStream({
  apiKey: 'cs_mob_live_…',     // required: dashboard mobile key
  endpoint: 'https://t.…',     // required: your first-party collector domain
  platform: 'ios',             // 'ios' | 'android' | 'react-native' (default)
  storage: AsyncStorage,       // StorageAdapter; default in-memory
  appStateProvider: AppState,  // session rotation on foreground
  appName: 'Acme',             // → device.userAgent
  appVersion: '2.1.0',         // → device.userAgent
  userAgent: 'Acme/2.1 iOS',   // full override of device.userAgent
  viewportWidth: 390,          // device.viewport.width
  viewportHeight: 844,         // device.viewport.height
  batchSize: 20,               // auto-flush buffer threshold
  flushIntervalMs: 10_000,     // timer flush (0 disables)
  sessionTimeoutMs: 1_800_000, // 30-min idle session window
  maxRetries: 5,               // delivery attempts per batch
  maxQueueSize: 1_000,         // offline-queue ceiling
  consent: { analytics: true, identityResolution: true },
});

Custom storage adapter

Any object with getItem / setItem / removeItem (sync or async) works:

import { MMKV } from 'react-native-mmkv';
const mmkv = new MMKV();

createClickStream({
  apiKey: 'cs_mob_live_…',
  endpoint: 'https://t.yourapp.com',
  storage: {
    getItem: (k) => mmkv.getString(k) ?? null,
    setItem: (k, v) => mmkv.set(k, v),
    removeItem: (k) => mmkv.delete(k),
  },
});

License

MIT