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

@obsrviq/react-native

v0.8.0

Published

Obsrviq mobile capture SDK for React Native — privacy-first screenshot session replay + network/error/custom-event recording, engineered for zero added latency on device.

Readme

@obsrviq/react-native

Mobile session replay + analytics capture for React Native (iOS + Android), feeding the same Obsrviq backend as the web tracker. Engineered around one non-negotiable: zero added latency on the device.

Status: shipped. The full pipeline is live — the pure-JS layer (public API, session/identity, network/error/console capture, transport), the native screenshot-capture TurboModule (iOS Swift / Android Kotlin), native touch capture, the backend frame-storage path, and the mobile player renderer (screenshot timeline + touch ripples + network waterfall). Native code is compiled inside the consuming app's iOS/Android build — see Building.


Why this design (the research)

We surveyed the 2026 mobile-replay landscape (Sentry, PostHog, Datadog, FullStory, Smartlook, UXCam — Sentry/PostHog/Datadog are open source and were read at the source level). Findings:

  • Nobody records video on-device for analytics replay — battery/heat/privacy kill it.
  • The two viable strategies are wireframe (serialize the view tree → reconstruct) and periodic screenshots (1 fps, masked, encoded). Both Sentry & PostHog force screenshot mode for React Native because RN's cross-platform rendering can't be reliably rebuilt from the native tree.
  • We chose screenshot-first (pixel-perfect replay) and apply the open-source playbook that makes it jank-free.

The measured reason naive screenshot replay janks: rasterizing a screen on the UI thread costs 25–155 ms/frame (Sentry measured 9–10 dropped frames/sec on their first iOS renderer). The fix is architectural, not incremental.

The zero-latency architecture

┌───────────────────────── DEVICE ─────────────────────────┐
│                                                           │
│  JS thread          Native UI thread     Background thread │
│  ─────────          ───────────────      ───────────────── │
│  init / identify    ① one fast raster ──▶ ② mask composite  │
│  track / tag           (~1 fps, change-     ③ JPEG encode    │
│  network/error/        driven, drop          ④ gzip          │
│  console capture       overlapping)          ⑤ UPLOAD ───────┼──▶ POST /v1/batch
│       │                                                     │     (type:'screen')
│       └── structured events ──▶ POST /v1/batch (type:custom/network/…)
│                                                           │
└───────────────────────────────────────────────────────────┘

Invariants (every one is a measured jank source if violated):

  1. The UI thread does only the single screenshot grab. Everything after — masking, JPEG encode, gzip, upload — runs on a background thread.
  2. The JS thread is never in the capture loop. It calls start/stop/configure once; the native timer reads native views directly (RN renders to real UIView/View, so no JS hook is needed for pixels).
  3. No pixel/frame buffer ever crosses the bridge/JSI. The native module uploads frames itself, using the siteKey + sessionId the JS layer hands it at start().
  4. Capture is change-driven, throttled to ~1 fps, and overlapping captures are dropped (an in-flight flag) so a slow frame can't snowball.
  5. Idle screens cost zero — unchanged view tree ⇒ no capture.

Platform specifics (from the source-level research):

  • iOS: raster via a raw CGContext + view.drawHierarchy(in:afterScreenUpdates:false) at native scale — not UIGraphicsImageRenderer (≈6× slower), not afterScreenUpdates:true (forces a sync layout+commit), not CALayer.render(in:) (drops content).
  • Android: PixelCopy.request(window, bitmap, listener, backgroundHandler) (API 26+, async, captures GPU/SurfaceView content) — not View.draw(Canvas) (misses hardware content → black video/maps). Reuse one RGB_565 bitmap.
  • Masking: privacy-by-default. Redaction rects are collected during the (cheap) traversal and composited onto the already-captured bitmap on the background thread — never a second screenshot.

Install

npm install @obsrviq/react-native @react-native-async-storage/async-storage
cd ios && pod install   # iOS

Requires React Native ≥ 0.76 (New Architecture). Works under the old architecture via RN's interop shim.

Quickstart

import { ObsrviqReplay } from '@obsrviq/react-native';

ObsrviqReplay.init({
  siteKey: 'pk_live_…',
  // privacy-by-default: all text + images masked. Opt out per-view with <ObsrviqMask unmask>.
});

// later — identical API to the web SDK:
ObsrviqReplay.identify('user_123', { plan: 'pro' });
ObsrviqReplay.track('checkout_started', { cart: 3 });
ObsrviqReplay.conversion('purchase', { value: 49.0 });
ObsrviqReplay.tag('vip', 'beta');

Naming: the SDK is ObsrviqReplay / ObsrviqConfig / <ObsrviqMask>. The old LumeraReplay / LumeraConfig / <LumeraMask> names are still exported as deprecated aliases, so pre-rebrand integrations keep working unchanged.

API

Mirrors @obsrviq/tracker 1:1 — init, identify, setMetadata, track, conversion, tag, startTask/endTask, setConsent, reset, stop, flush, getDiagnostics, sessionId. screen(name) records a route view (the mobile analog of a web pageview — call it from your navigation listener).

Config

Passed to init({ siteKey, … }). See src/config.ts for the authoritative list.

| Option | Default | What it does | |---|---|---| | siteKey | — | Required. Your ingest key (pk_…). | | enableReplay | true | Record the screen (native screenshot stream). | | fps | 1 | Capture cadence cap, frames/sec (change-driven underneath). | | jpegQuality | 0.4 | JPEG quality 0..1 on the encode path. | | maxCaptureDim | 1200 | Cap the longer edge of each frame (px) — the storage/bandwidth lever. 0 = full native res. | | maskAllText | true | Mask all text before any pixel is persisted. | | maskAllImages | true | Mask all images/media. | | captureTouches | true | Capture taps + swipes as a touch overlay (native, non-blocking gesture observer — never interferes with the app's own gestures). | | captureNetworkBodies | false | Capture request + response bodies. Bodies are masked on-device before they're queued (PII patterns + sensitive-key redaction — see Network body masking) and capped at 8KB. Off by default — enable deliberately. | | maskKeys | [] | Extra field names to redact by value (case-insensitive), on top of the always-on built-in list. e.g. ['accountNumber','dob']. Merged with the per-site keys you configure in the console. | | redactHeaders | ['authorization','cookie','set-cookie'] | Header allowlist redaction for captured network events (request + response). | | captureConsole | true | Mirror console.* into the session. | | sampleRate | 1 | Fraction of sessions recorded (0..1). | | continueSession | false | Resume the same session across foreground/background within the window. | | requireConsent | false | Gate all recording until setConsent(true). |

Privacy / masking

Three independent streams, each masked at capture — nothing sensitive leaves the device unmasked:

  • Screen pixels — all text and images are masked before any pixel is persisted (masking runs on the native capture thread, pre-encode). Per-view control with <ObsrviqMask> / <ObsrviqMask unmask> wrappers (call setViewMasked(reactTag, …) natively).
  • Network — header allowlist redaction (authorization/cookie/set-cookie by default; extend with redactHeaders). Request/response bodies are off by default; when enabled they are masked on-device — see Network body masking below.
  • WebSocket — connection lifecycle (open/close/error) and per-frame direction/size are always captured; the URL's query string is stripped (tokens), and text-frame previews follow captureNetworkBodies and go through the same on-device masking as HTTP bodies. Binary frames record size only, never bytes.
  • Touch — records only coordinates + phase (start/move/end), never the content under the finger.
  • Console + errorsconsole.* args and uncaught-error message/stack are PII-masked on-device (email/card/SSN/long-digit patterns), and console objects also get sensitive-key redaction — same pipeline as network bodies, so a console.log({ password }) or an error string with a card number can't leak.

Network body masking

Body capture is opt-in (captureNetworkBodies: true). Once enabled, every captured request and response body — fetch and XMLHttpRequest alike — passes through one masking chokepoint on the JS thread before it is ever queued for upload. This is the same masking pipeline the web tracker uses (src/mask.ts is a kept-in-sync port), so mobile and web redact identically.

Two layers run over each body, in order:

1. PII pattern masking (always on) — heuristics catch the obvious personal data anywhere in the text, regardless of field name:

| Pattern | Replaced with | Example | |---|---|---| | Email addresses | «email» | [email protected]«email» | | Card numbers (13–19 digits, Luhn-length) | «card» | 4111 1111 1111 1111«card» | | US SSN (###-##-####) | «ssn» | 123-45-6789«ssn» | | Long digit runs (≥9, phone/account-ish) | «number» | 5551234567«number» |

2. Sensitive-key redaction (always on + configurable) — any field whose name is sensitive has its whole value replaced with «redacted». This works on structured JSON and on non-JSON payloads (form-urlencoded, query strings) and on truncated bodies (a body cut at the 8KB cap still gets a raw-text key scan, so a secret split across the boundary can't leak):

  • Built-in keys (no config needed): password, passwd, passphrase, secret, token, authorization, apiKey/api_key/api-key, creditCard, cardNumber, cardholder, cookie, cvv, cvc, plus the standalone segments pass, pwd, auth, card, pin, otp, ssn.
  • The matcher is boundary-aware (splits camelCase and snake_case/kebab-case), so accessToken, x-api-key, authToken, cardNumber are caught — but innocent lookalikes like author, discard, shipping, username are not.
  • Custom keys — add your own field names two ways, and they combine:
    • maskKeys: ['accountNumber','dob'] at init() (effective immediately, before the first request is captured).
    • Per-site keys configured in the Obsrviq console (fetched once at startup via GET /v1/config). No app release needed — change them in the console and every session picks them up.
ObsrviqReplay.init({
  siteKey: 'pk_live_…',
  captureNetworkBodies: true,        // opt in to body capture
  maskKeys: ['accountNumber', 'dob'], // redact these values too (on top of the built-ins)
  redactHeaders: ['authorization', 'cookie', 'set-cookie', 'x-session'],
});

Timing guarantees. Built-in and init()-supplied maskKeys apply from the very first captured body. The per-site console keys arrive from GET /v1/config a moment after startup; until that request settles (bounded to ≤2s), the SDK holds the structured-event stream so a body captured in that brief window can't be sent before its site-configured keys are known. As a final backstop, a send-time pass re-applies sensitive-key redaction to every batch (bodies, console objects, and custom-event props) right before upload — so a custom key that arrived mid-session still scrubs anything captured earlier.

The masking is not reversible — masked values are gone before the data leaves the device; there is no un-mask on the server. Test your maskKeys against a real request in a debug build if you're unsure a field is covered.

Device & version analytics

Every session automatically carries a coarse (no-PII) device descriptor, and on native the SDK enriches it so the console's Audience page can break traffic down by:

| Field | Example | Source | |---|---|---| | OS | iOS / Android | Platform.OS | | OS version | 17.2 / 14 | native (UIDevice.systemVersion / Build.VERSION.RELEASE) | | Device model | iPhone15,3 / Google Pixel 8 | native (uname / Build.MANUFACTURER+MODEL) | | App version | 1.1.6 / 2.1 | native (CFBundleShortVersionString / versionName) | | App build | 14 | native (CFBundleVersion / versionCode) | | SDK version | 0.5.0 | this package |

The native fields come from a one-shot getAppInfo() read at startup; if the native module isn't linked (e.g. Expo Go), they're simply omitted and the session still records with OS + SDK version. No configuration needed — the breakdowns populate on the Audience page automatically.

Screens & journeys

screen(name) is the only source of path/journey data on native (there's no web-style automatic navigation capture) — wire it to your navigation listener:

navigationRef.addListener('state', () => ObsrviqReplay.screen(currentRouteName()));

Each call records the previous screen as the pageview's referrer, so the console's Journeys Sankey chains the from → to flow. Consecutive duplicate screens are de-duped, and logout (reset()) starts a fresh journey.

Data flow

Two independent uploaders, one session:

| Stream | Owner | Endpoint | Event type | Storage | |---|---|---|---|---| | Structured (network/websocket/error/console/custom) | JS | POST /v1/batch | network/ws/error/console/custom | Postgres events | | Screenshot frames | Native | POST /v1/batch | screen | blob + mobile_frames index | | Touch (taps/swipes) | Native | POST /v1/batch | touch | Postgres events (synced to the timeline by t) |

All three share siteKey + sessionId. No seq collision: structured events never write replay chunks; screen frames get their own frame index; touch events are small and land in the events table. The worker routes screen to the blob store + mobile_frames, keeps everything else (including touch) in events. The player overlays touch events as ripples and renders network events as a per-request waterfall with an expandable headers/body inspector.

Roadmap

  • [x] JS core — public API, session/identity, network/error/console capture, transport (src/)
  • [x] Wire format — MobileScreenEvent (type:'screen') + MobileTouchEvent (type:'touch') in @obsrviq/types
  • [x] Native iOS capture (Swift TurboModule) — screenshots + touch observer — ios/
  • [x] Native Android capture (Kotlin TurboModule) — screenshots + touch observer — android/
  • [x] Backend — route type:'screen' to blob + mobile_frames, touch to events (migration 017)
  • [x] Player — mobile screenshot renderer + device-based routing + touch ripples + network waterfall/inspector
  • [x] <ObsrviqMask> / <ObsrviqMask unmask> per-view privacy component (iOS + Android)
  • [x] Network request/response body capture (opt-in, captureNetworkBodies)
  • [x] On-device body masking — PII patterns + sensitive-key redaction (built-in + maskKeys + per-site /v1/config), parity with the web tracker (src/mask.ts); also covers console/error capture
  • [x] WebSocket capture — lifecycle + frame direction/size + opt-in masked text-frame previews (instrument/websocket.ts), parity with the web tracker

Building (native)

Native code can't be compiled in the SDK repo — build it inside a real RN app and test on devices (jank only shows on hardware, especially low-end). Codegen runs automatically during the app's iOS/Android build from src/spec/NativeObsrviqReplay.ts.