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

@vitrinka/expo

v0.2.3

Published

vitrinka toolkit for Expo apps — the journey recorder: capture manual-testing sessions (screenshots, interactions, network, notes) straight into vitrinka boards.

Downloads

1,705

Readme

@vitrinka/expo

The vitrinka toolkit for Expo apps. Today it ships one tool: the journey recorder — record manual-testing sessions (screenshot keyframes, taps, navigation, network calls, console errors, notes) from a real device straight into a vitrinka board.

What it captures and where it goes is documented in docs/PROTOCOL.md; this package's source is public precisely so you can verify it.

Install

npx expo install @vitrinka/expo react-native-view-shot react-native-keyboard-controller react-native-safe-area-context expo-file-system expo-constants

All native dependencies are peers on purpose (your app's versions win). MMKV and expo-router are optional — only needed if you import their subpaths.

Wire it up

1. Config plugin (app.json) — the recorder's build guard. It refuses any build where the ingest token could leak into a store bundle:

{
  "expo": {
    "plugins": [["@vitrinka/expo", { "allowedProfiles": ["development", "development-simulator"] }]]
  }
}

If you publish OTA updates from scripts, also set updateLaneVar to the env var your update scripts export (e.g. "updateLaneVar": "MYAPP_UPDATE_LANE") — it is the reliable signal for refusing a token on a store-facing channel; without it only best-effort argv scanning guards eas update. Prod-backed test lanes use laneMarkerVar/laneMarkerValues (see plugin/build-guard.js).

2. Metro strip (metro.config.js) — one required line (config plugins cannot modify metro config; the plugin verifies it is present):

const { withRecorderStrip } = require('@vitrinka/expo/recorder/metro');
module.exports = withRecorderStrip(config);

3. Mount (your root layout):

import { VitrinkaRecorderRoot, VitrinkaRecorderPill } from '@vitrinka/expo/recorder';
import { useExpoRouterRecorderRoute } from '@vitrinka/expo/recorder/expo-router';

export default function Layout() {
  const route = useExpoRouterRecorderRoute(); // or supply {pathname, lane} from your own nav
  return (
    <VitrinkaRecorderRoot route={route}>
      <Stack />
      <VitrinkaRecorderPill />
    </VitrinkaRecorderRoot>
  );
}

4. Enable it — only on builds that should record:

EXPO_PUBLIC_VITRINKA_URL=https://your-vitrinka.example   # the URL alone enables the recorder

Without it, the entire recorder is stripped from the bundle at compile time — the gate folds to a no-op and the metro hook redirects the recorder's modules to an empty stub.

5. Link the device — no secret is baked. The first time, a tester taps the puck (Link recorder) and gets a short code, Open vitrinka (approve on the same device) — or type the code in vitrinka on any other device. The server mints an ingest-only vkr_ token that the recorder stores (vitrinka.recorder.link, through the storage driver) and uses from then on; Unlink (long-press the puck, or the ⋯ menu) forgets it, and so does a 401 from the server. A URL addressing /w/<slug> preselects that workspace on the approve page; a token approved into another workspace is discarded and the sheet says which one to pick.

Unattended builds (CI, machine-driven runs) can still bake a token:

EXPO_PUBLIC_VITRINKA_TOKEN=<vkr_ recorder key>   # admin-minted, ingest-only — never a workspace token

Credentials in URL paths (sanitizeUrl)

The redaction engine scrubs secrets in query strings and fragments (?token=…), but it cannot know which path segments of your routes are credentials. An expo-router screen at app/reset-password/[token].tsx reports /reset-password/<token> as its pathname, and the pathname is the route of every event recorded on that screen. Name those segments with sanitizeUrl:

// Each rule rewrites a path segment that IS a credential into a placeholder.
const SECRET_PATHS: [RegExp, string][] = [
  [/\/(reset-password|invite)\/[^/?#]+/g, '/$1/[token]'],
];
const sanitizeUrl = (url: string) => SECRET_PATHS.reduce((u, [rx, to]) => u.replace(rx, to), url);

<VitrinkaRecorderRoot route={route} sanitizeUrl={sanitizeUrl}>
  • Coverage: every route the recorder records (each event's tabHost, the route of navigations, taps and keyframes) and every network request URL.
  • Input: bare pathnames, and request URLs as your code passed them (absolute or relative). Match on path segments, not on a URL you parse.
  • Order: it runs first. The query/fragment scrub and your workspace's patterns still apply to what it returns, and routes pass them too.
  • fullFidelity: it applies even under a fullFidelity policy. It is your statement about your own URLs, not a workspace redaction rule.
  • Failure: it fails closed. If it throws or returns a non-string, the recorder keeps only the URL's origin ([redacted] for a bare route), never the raw value. It logs one vitrinka: warning per sanitizer. A line your sanitizer logs reaches the console but is never recorded.
  • Not covered: free text stays under the workspace policy: request and response headers and bodies, and console lines (a router error that quotes the path, say). Add a policy pattern for a token shape that shows up in text. Keyframes show what the screen shows. The lane comes from expo-router's segments ([token], never its value) or your own lane. hideIdleGripOn matches the raw pathname.

The production-strip guarantee

Three layers, all shipped here:

  1. Gate — the exported components check EXPO_PUBLIC_VITRINKA_* as inlined literals; without them the recorder code is provably unreachable.
  2. Metro stub — withRecorderStrip redirects the recorder's entry modules to an empty stub on unconfigured builds, so Metro never bundles the subtree.
  3. Build guard — the config plugin throws on any build/OTA-publish where a baked token is present outside your allowlisted profiles (the URL alone is never gated — it is not a secret) (see plugin/build-guard.js for the full semantics: forbidden OTA channels are checked first and have no escape hatch).

We recommend asserting the strip in your own repo against your real export pipeline (bundle without recorder env; grep the output for vitrinka).

Storage

The recorder's durable queue uses a synchronous storage driver. Default: expo-file-system (zero extra installs). To use MMKV instead:

import { configureRecorderStorage } from '@vitrinka/expo/recorder/storage';
import { mmkvRecorderStorage } from '@vitrinka/expo/recorder/storage-mmkv';
configureRecorderStorage(mmkvRecorderStorage());

License

Elastic License 2.0.