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

@anyreplay/react-native

v0.3.0

Published

Session replay for React Native: records the committed element tree, not pixels, and ships it to AnyReplay.

Readme

@anyreplay/react-native

Session replay for React Native apps.

npm install @anyreplay/react-native
import { AnyReplay, init, trackNavigation } from '@anyreplay/react-native';

init({ projectKey: 'ar_pk_live_…' });

export default function Root() {
  return (
    <AnyReplay>
      <NavigationContainer ref={(ref) => { if (ref) trackNavigation(ref); }}>
        <App />
      </NavigationContainer>
    </AnyReplay>
  );
}

No native module and no rebuild, so it works in Expo Go as well as in development and production builds.

Requirements: React 18+ and React Native 0.72+, with the New Architecture (Fabric) — on by default from React Native 0.76 and Expo SDK 52, Expo Go included. The recorder measures what is on screen through Fabric; on the old architecture it logs one warning (recording needs the New Architecture (Fabric)) and records taps, screen names and events, but no picture of the screen.

What it records, and why not screenshots

Every other mobile replay tool records the screen as images — PostHog and Sentry both do on React Native specifically, because the JavaScript side cannot reach the native view tree without a native module.

This one records structure and words instead. A screenshot is pixels, and pixels cannot be translated; a tool whose reason to exist is showing a session in a language the reviewer reads cannot throw the words away on the way in.

So the recorder reads the layer JavaScript can reach: the tree React has committed. On each tick (every 500 ms by default) it walks that tree and notes, for each host element, what it is and what it says — then measures through Fabric where it sits on screen. React knows what is on screen; the measurement knows where it is; neither alone is enough.

The result is a wireframe, not a photograph. That is honest about what was captured, and it is what makes a Turkish session watchable in English.

It is also what makes it cheap. Smartlook reports 0.5 MB per minute at two screenshots a second, UXCam 50–500 KB. Here the first frame of a screen is a few kilobytes and every frame after it carries only what changed — often a handful of numbers — and an idle screen sends nothing at all.

Privacy

By default a recording does not contain what a person types into a TextInput, the same as the browser SDK: every value is replaced with asterisks on the device, so the replay shows the field being filled in without showing what went into it. To see what people typed, opt out:

  • maskAllInputs: false — TextInput values are recorded (the fields below stay masked).
  • maskAllTyping: true — every TextInput masked, and not weakened by maskAllInputs: false set elsewhere in your configuration.
  • testID containing ar-mask — one view, or everything below it.
  • maskImages: true — no image URLs at all.

Some fields are masked in every mode and no option records them:

| Masked without being asked | Read from | |---|---| | A secure field | secureTextEntry | | A password, new password or one-time code | textContentType, autoComplete, keyboardType: 'visible-password' | | A card number, security code, expiry or cardholder name | textContentType: 'creditCard…', autoComplete: 'cc-…' | | A field whose label, placeholder or testID names one of the above | placeholder, accessibilityLabel, testID, nativeID | | A value of 13–19 digits that passes the card-number check | the value itself | | A card number still being typed: digits only, starting 2–6, grouped like a card, more than six digits | the value itself — at most the first six digits are ever recorded | | A card number inside longer text — a note, a message, a multi-line field | its digits, as soon as a seventh is typed; the field and the rest of its text stay recorded |

A masked element keeps its box and loses its words: the reviewer sees that something was typed, and where, and never what. The value is replaced on the device — it is not sent and then hidden.

Where a phone knows less than a browser

A browser field carries type, autocomplete and a name the framework wrote, so the floor almost always has something to read. In React Native a <TextInput /> with no props says nothing about itself at all, and there is no name attribute to fall back on. With maskAllInputs: false such a field is recorded, consistently with the web, and only the card-number check on its value can catch it.

Two more differences worth knowing:

  • A post code is not payment data here, and neither is an email address or a phone number — textContentType: 'postalCode', 'emailAddress' and 'telephoneNumber' follow maskAllInputs like any other field. The browser SDK treats them the same way.
  • There is no contenteditable on a phone, so maskAllTyping covers exactly what maskAllInputs covers. What it adds is that it cannot be weakened.

If you opt out and some screens carry anything you would not want a teammate to read, give those fields a textContentType, an autoComplete or a testID that says what it is.

Options

| | Default | | |---|---|---| | projectKey | — | From the dashboard. Required. | | ingestUrl | https://in.anyreplay.com | Self-hosted deployments only. | | sampleRate | 1 | Decided once per visitor, so a recorded person stays recorded. | | snapshotIntervalMs | 500 | The main cost lever. Slower is cheaper and coarser. | | maskAllInputs | true | Mask every TextInput. false records typed values; the floor above applies either way. | | maskAllTyping | false | The same, and maskAllInputs: false no longer weakens it. | | maskTestID | ar-mask | Views whose testID contains this are masked. | | maskImages | false | Record no image URLs at all. | | requireConsent | false | Record nothing until consent(true). | | storage | in memory | Give it AsyncStorage to recognise a returning visitor. | | appId | — | Your bundle id / package name. Needed once the project lists allowed domains; add it under Allowed apps too. | | appVersion | — | Your app's version, shown with each session. React Native cannot read it without a native module. | | recordErrors | true | Uncaught errors, with stacks, through ErrorUtils. Your own handler still runs. | | recordConsole | true | console.error and console.warn; never log, info or debug. | | maxEventsPerChunk | 200 | Send a chunk when this many events are buffered. | | flushIntervalMs | 5000 | Send a chunk at least this often. | | maxBufferedEvents | 2000 | Kept while offline; the oldest are dropped beyond it. | | debug | false | Log warnings: an unmounted <AnyReplay>, a refused request. |

maxConsecutiveFailures is still accepted so older code compiles, and ignored since 0.2.0: a failed delivery never stops the recording.

import AsyncStorage from '@react-native-async-storage/async-storage';
init({ projectKey: '…', storage: AsyncStorage });

An app sends no web origin, so a project that lists allowed domains refuses it until the app is listed under Allowed apps in the project's settings and appId names it. The id is a filter, not a secret — anyone with the public key can send one — so keep relying on quotas and rate limits, not on it.

import * as Application from 'expo-application';
init({
  projectKey: '…',
  appId: Application.applicationId ?? undefined,          // com.example.shop
  appVersion: Application.nativeApplicationVersion ?? undefined,
});

Each session reports platform: 'react-native', this package's name and version, and on Android the device model.

API

| | | |---|---| | init(options) | Starts recording. Safe to call twice; the second is ignored. | | stop() | Stops, and detaches everything. | | consent(granted) | Grants or withdraws consent. | | identify({ userId, email }) | Attaches an identity to the session. Held until the session's first chunk is accepted, then sent once. | | screen(name) | A screen change, if you are not using trackNavigation. | | track(name, properties?) | Tags this moment, as in the browser SDK: name 1–64 chars of [A-Za-z0-9_.:-], properties a JSON object ≤ 4 KB. | | trackError(error) | Records an error you caught yourself. | | trackNavigation(ref) | Follows a react-navigation container. | | flush() | Sends what is buffered. | | getSessionId() | The current session id, for correlating with your own logs. |

Taps, delivery and sessions

  • A tap is recorded once, when the finger lifts, and only if it moved less than 10 points — a scroll, a swipe or a drag is not a tap. <AnyReplay> listens to touches in the capture phase (onTouchStartCapture and friends); it never takes part in the responder system, so it cannot claim a gesture.
  • Outages are waited out, not given up on. A failed delivery (no network, a 5xx, a 429) is retried after 2 s, then 4 s, 8 s … at most a minute, with jitter, and at least as long as Retry-After asks; coming back to the foreground tries again at once. Up to maxBufferedEvents are kept, and after an outage they leave as several chunks of at most 200 events and about 256 KB each. If the oldest have to be dropped, the next thing sent is a fresh screen size, screen name and full snapshot.
  • A refusal stops the recording for the launch: 402, 403 and other 4xx. After a 402 (a quota or a session cap) the app does not try again for two minutes, across launches.
  • Back after 30 idle minutes is a new session, as a relaunch would be.
  • Rotation sends the new screen size and a whole screen.
  • iPads and Android tablets report themselves as tablets.

What it does not do yet

  • Lists are recorded as what is on screen. A FlatList reports the rows it has rendered, so scrolling back through a replay shows the rows that existed at that moment, not the whole list.
  • Images by URL or bundled only. An image loaded from a URL is recorded by its URL; an image shipped in the app bundle is uploaded once per project, by content hash. Photos from the device's caches or photo library are never read, and maskImages: true records no image at all.
  • Custom native components are recorded as Unknown — a box in the right place with no text, because there is nothing readable to capture.