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

@sailfish-ai/recorder

v1.11.6

Published

Frontend session recorder for [Sailfish](https://app.sailfishqa.com). Captures the DOM, console, network activity, errors, and user interactions so replay is available in the dashboard alongside the backend traces Sailfish already collects.

Readme

@sailfish-ai/recorder

Frontend session recorder for Sailfish. Captures the DOM, console, network activity, errors, and user interactions so replay is available in the dashboard alongside the backend traces Sailfish already collects.

Works in any browser JavaScript or TypeScript app — React, Vue, Angular, Next.js, Nuxt, Svelte, Shopify, plain HTML. Ships UMD, ESM, and CJS builds; can be installed via npm or loaded as a single <script> tag from a CDN.

Installation

npm  install @sailfish-ai/recorder
# or
yarn add      @sailfish-ai/recorder
# or
pnpm add      @sailfish-ai/recorder

Quick start

ES modules / bundler (Vite, Webpack, Rollup, Next.js, etc.)

import { initRecorder } from "@sailfish-ai/recorder";

// SSR guard — don't touch browser storage during server render.
if (typeof window !== "undefined") {
  initRecorder({ apiKey: "YOUR_API_KEY" });
}

CDN (single <script> tag, auto-init)

The UMD build auto-initializes from data-* attributes on the script tag — zero JavaScript needed on your side:

<script
  src="https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs"
  data-api-key="YOUR_API_KEY"
  data-service-identifier="my-app"
  data-service-version="1.0.0"
  crossorigin="anonymous"
></script>

Drop that into the <head> of any HTML page (Shopify, Webflow, WordPress, static sites, server-rendered templates) and recording starts automatically. Errors inside the recorder are swallowed — Sailfish never breaks the host page.

CDN (manual init)

If you'd rather call initRecorder yourself — for example to pass runtime options — omit data-api-key and use the SailfishRecorder global:

<script
  src="https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs"
  crossorigin="anonymous"
></script>
<script>
  window.SailfishRecorder.initRecorder({
    apiKey: "YOUR_API_KEY",
    serviceIdentifier: "my-app",
    serviceVersion: "1.0.0",
  });
</script>

CDN delivery and bundle size

@sailfish-ai/recorder is published publicly on npm, so any major JavaScript CDN can serve it — jsDelivr, unpkg, esm.sh, esm.run, jspm. The UMD bundle is self-contained (no external dependencies to resolve) and exposes the SailfishRecorder global.

What the browser actually downloads

The UMD file on disk is ~473 KB, but browsers never see that number. CDNs do HTTP content-negotiated compression: the browser advertises Accept-Encoding: gzip, br automatically, and the CDN returns the file with a Content-Encoding: br (or gzip) header. The browser decompresses on arrival. No configuration needed by the site author — just include the <script> tag.

| Encoding (delivered) | Size on the wire | Browsers | |---|---:|---| | Brotli (br) | ~84 KB | Chrome, Firefox, Safari, Edge (all current versions) | | Gzip (gzip) | ~117 KB | Older browsers, curl without --compressed, crawlers | | Uncompressed | ~473 KB | Only if the client explicitly sends Accept-Encoding: identity |

jsDelivr and unpkg set Vary: Accept-Encoding, so each encoding is cached independently at the edge — the first user per encoding per region pays a small compression cost, every subsequent request is served straight from the cache.

CDN options

| CDN | URL pattern | Notes | |---|---|---| | jsDelivr (recommended) | https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs | Fastest p50 in our load tests, brotli over HTTP/2, global anycast. Cache stays warm across versions. | | unpkg | https://unpkg.com/@sailfish-ai/recorder@1/dist/recorder.umd.cjs | Cloudflare-backed, also fast. Good fallback. | | esm.run / jsDelivr +esm | https://esm.run/@sailfish-ai/recorder@1 | Single-file ESM for <script type="module">. Pre-bundled (~31 KB brotli over-wire). | | esm.sh | https://esm.sh/@sailfish-ai/recorder@1 | ESM with separate dependency module graph (multiple HTTP requests — slower first load). |

Version pinning:

  • @sailfish-ai/recorder@1 — track the latest 1.x (recommended; gets bug fixes).
  • @sailfish-ai/[email protected] — pin exactly (longest CDN cache TTL; use for absolute stability).

Which build format should I use?

| You're building… | Use | How | |---|---|---| | Plain HTML, Shopify, Webflow, WordPress, server-rendered template | UMD via CDN | <script src="…/recorder.umd.cjs" data-api-key="…"> | | React / Vue / Next.js / Svelte app with a bundler | ESM via npm | import { initRecorder } from "@sailfish-ai/recorder" | | Modern browser app with native ESM (no bundler) | ESM via CDN | <script type="module">import { initRecorder } from "https://esm.run/@sailfish-ai/recorder@1" | | Node.js CommonJS (unusual — this is a browser package) | CJS via npm | const { initRecorder } = require("@sailfish-ai/recorder") |

API

All functions are top-level exports of @sailfish-ai/recorder. On the CDN build they're also available as window.SailfishRecorder.*.

initRecorder(options)

Initialize the recorder. Returns Promise<void>. Safe to call once per page load; concurrent calls are coalesced.

await initRecorder({
  apiKey: "YOUR_API_KEY",         // required
  serviceIdentifier: "my-app",    // optional — your app name in the dashboard
  serviceVersion: "1.0.0",        // optional — your app version for filtering sessions
});

Options

| Field | Type | Default | Purpose | |---|---|---|---| | apiKey | string | required | Your Sailfish API key (from the dashboard). | | backendApi | string | https://api-service.sailfishqa.com | Backend endpoint. Override for self-hosted Sailfish deployments. | | serviceIdentifier | string | — | Application name shown in the dashboard. | | serviceVersion | string | — | Application version, used for session filtering. | | gitSha | string | auto-detected | Build commit SHA for session filtering. | | serviceAdditionalMetadata | Record<string, any> | — | Arbitrary metadata attached to every session. | | domainsToPropagateHeaderTo | string[] | [] | Domains (wildcards allowed) that receive the X-Sf3-Rid tracing header, letting Sailfish join frontend sessions with backend traces. | | domainsToNotPropagateHeaderTo | string[] | [] | Domains to exclude, appended to the built-in denylist. | | enableFiberTracking | boolean | false | Adds data-sf-source="file.tsx:42" attributes to DOM nodes for source-mapped coverage (React only). Pairs with the Babel plugin. | | enableIpTracking | boolean | false | Fetches the visitor IP asynchronously for session metadata. | | captureStreamingResponseBody | boolean | true | Capture prefixes of streaming (SSE, ndjson, chunked) responses. | | captureResponseBodyMaxMb | number | 10 | Max non-streaming response body to capture, in MB. 0 disables body capture. | | captureStreamPrefixKb | number | 64 | Max streaming-body prefix to capture, in KB. | | captureStreamTimeoutMs | number | 10000 | Timeout for reading streaming bodies, in ms. | | deferRecording | boolean | true | Defers the initial DOM snapshot until after first paint / idle. | | chunkSnapshot | boolean | false | Yield to the browser every 500 nodes during the initial snapshot (smoother on very large pages). | | useWsWorker | boolean | true | Run the WebSocket sender in a Web Worker. Disable if your CSP blocks worker-src blob:. | | capturePerformanceMetrics | boolean | true | Capture FCP / LCP / TBT / DCL / LOAD per page_visit_uuid via the performance plugin. Set false to skip — the plugin is dynamically imported, so opting out also skips loading web-vitals and the observers. | | reportIssueShortcuts | ShortcutsConfig | — | Custom keyboard shortcuts for the report-issue modal. | | showEngTicketFieldsInReportIssueModalDefault | boolean | false | Pre-expand Jira / Linear fields in the in-app issue modal. |

identify(userId, traits?, overwrite?)

Tag the current session with a user identity. Subsequent calls with the same userId are deduplicated.

identify("user_abc123", { email: "[email protected]", plan: "pro" });

addOrUpdateMetadata(metadata)

Attach arbitrary metadata to the current session. Values are merged with any previously-set metadata.

addOrUpdateMetadata({ experiment: "pricing-v2", cohort: "B" });

getOrSetSessionId()

Return the current session ID (generates one if none exists). Useful for correlating logs on your side with Sailfish recordings.

const sessionId = getOrSetSessionId();
console.log("Sailfish session:", sessionId);

openReportIssueModal(options?)

Open the in-app bug-report modal. If showEngTicketFields: true is passed (or set at init), Jira / Linear fields are shown by default.

openReportIssueModal({ showEngTicketFields: true });

enableFunctionSpanTracking() / disableFunctionSpanTracking() / isFunctionSpanTrackingEnabled()

Toggle per-session backend function-span tracing at runtime.

if (isFunctionSpanTrackingEnabled()) disableFunctionSpanTracking();
enableFunctionSpanTracking();

Performance metrics

The recorder automatically captures real-user performance metrics per page visit and emits them through the same WebSocket pipeline as everything else. No additional configuration is required; capture is skipped in Lighthouse / headless / WebDriver environments to avoid polluting synthetic audits.

| Metric | Meaning | Source | |---|---|---| | FCP — First Contentful Paint | Time to first text/image paint. | web-vitals onFCP | | LCP — Largest Contentful Paint | Time to the largest above-the-fold element. Closest proxy for "content is visible". | web-vitals onLCP | | TBT — Total Blocking Time | Sum of blocking time from long tasks, emitted at load. | PerformanceObserver({type:'longtask', buffered:true}) summing max(0, duration − 50) | | DCL — DOMContentLoaded | navigation.domContentLoadedEventEnd (ms since timeOrigin). | Navigation Timing Level 2 | | LOAD — load event | navigation.loadEventEnd (ms since timeOrigin). | Navigation Timing Level 2 |

FCP and LCP re-emit on SPA soft-navigations (via web-vitals' native history-API / BFCache support) — each emission is keyed to the current page_visit_uuid. TBT, DCL, and LOAD fire once per hard load.

Events arrive on the recorder WebSocket with type: 28 (the numeric id reserved for performance metrics — see backend/sailfish/rrweb/enums.py on the Sailfish backend) and the shape:

{
  "type": 28,
  "timestamp": 1700000000000,
  "sessionId": "<session_id>",
  "page_visit_uuid": "<uuid>",
  "href": "https://example.com/path",
  "data": {
    "plugin": "@sailfish-rrweb/rrweb/performance@1",
    "payload": {
      "metric": "FCP" | "LCP" | "TBT" | "DCL" | "LOAD",
      "value": 1234.5,
      "rating": "good" | "needs-improvement" | "poor",
      "navigationType": "navigate" | "reload" | "back-forward" | "back-forward-cache" | "prerender" | "restore",
      "pageVisitUuid": "<uuid>",
      "href": "https://example.com/path",
      "timestamp": 1700000000000
    }
  }
}

Disabling capture

Pass capturePerformanceMetrics: false to initRecorder to skip the plugin entirely. Because the plugin code is dynamically imported, this also avoids loading web-vitals and the long-task observer:

initRecorder({
  apiKey: "YOUR_API_KEY",
  capturePerformanceMetrics: false,
});

Benchmarking the plugin's overhead

scripts/bench-perf.js loads the same page twice — once with capture on, once with capture off — under headless Chrome, and reports CPU, JS heap, FCP/LCP, long-task time, and total load time side-by-side.

One-time app change (required before running the bench)

The bench needs a way to flip capture on/off without rebuilding your app between variants. Wire ?sf_perf=off in your initRecorder call so the URL controls the option — paste this where you call initRecorder:

const capturePerformanceMetrics =
  new URLSearchParams(window.location.search).get("sf_perf") !== "off";

initRecorder({
  apiKey: "YOUR_API_KEY",
  // …your other options…
  capturePerformanceMetrics,
});

Anything other than ?sf_perf=off (including the param being absent) leaves capture enabled, so day-to-day usage is unaffected. The toggle exists only so the bench can switch variants by URL.

Run the bench

# 1. Start your app's dev server (separate terminal). Default port 3000.
npm run start

# 2. Run the bench (defaults to http://localhost:3000, 5 runs/variant):
cd veritas/jsts-frontend
npm install                         # first time only — installs puppeteer
npm run bench-perf

# Override URL or sample count:
npm run bench-perf -- --url http://localhost:3000/some/page --runs 10

# Watch the runs in a real browser window (slower, useful for sanity-checking):
npm run bench-perf -- --headed

Output is a table of medians, p95, and Δ% between the two variants for: wall load time, DCL, load event, FCP, LCP, long-task total, long-task blocking (the TBT proxy), CDP TaskDuration / ScriptDuration / LayoutDuration, and usedJSHeapSize (MB).

Sample interpretation: a Δ ≤ noise (typically ±5% on the smaller numbers, ±2 MB on heap) means the plugin is essentially free; anything larger is a regression worth profiling.

Framework examples

React / Vite

import { useEffect } from "react";
import { initRecorder } from "@sailfish-ai/recorder";

export function App() {
  useEffect(() => {
    initRecorder({ apiKey: import.meta.env.VITE_SAILFISH_KEY });
  }, []);
  return <Routes />;
}

Next.js (App Router)

'use client' is required — initRecorder touches localStorage and must run in the browser.

"use client";
import { useEffect } from "react";
import { initRecorder } from "@sailfish-ai/recorder";

export function SailfishProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    initRecorder({ apiKey: process.env.NEXT_PUBLIC_SAILFISH_KEY! });
  }, []);
  return <>{children}</>;
}

Place <SailfishProvider> in app/layout.tsx.

Plain HTML / Shopify / Webflow / WordPress

<script
  src="https://cdn.jsdelivr.net/npm/@sailfish-ai/recorder@1/dist/recorder.umd.cjs"
  data-api-key="YOUR_API_KEY"
  crossorigin="anonymous"
></script>

Babel plugin — source-mapped coverage

@sailfish-ai/recorder/babel-plugin injects data-sf-source="file.tsx:42" attributes into your JSX at compile time so clicks and views in the Sailfish dashboard link back to the exact source line.

Vite + React

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import sailfishSourcePlugin from "@sailfish-ai/recorder/babel-plugin";

export default defineConfig({
  plugins: [
    react({ babel: { plugins: [sailfishSourcePlugin] } }),
  ],
});

Babel / CRA / Next.js

// babel.config.js
const sailfishSourcePlugin = require("@sailfish-ai/recorder/babel-plugin");
module.exports = {
  plugins: [[sailfishSourcePlugin, { allElements: false }]],
};

Pass { allElements: true } to annotate every element (default annotates only interactive ones — buttons, inputs, links, form controls).

Configuration recipes

// Self-hosted Sailfish backend
initRecorder({
  apiKey: "YOUR_KEY",
  backendApi: "https://sailfish.your-company.com",
});

// Join frontend sessions with backend traces across your API
initRecorder({
  apiKey: "YOUR_KEY",
  domainsToPropagateHeaderTo: ["api.mycompany.com", "*.mycompany.com"],
});

// Strict CSP (no blob: workers)
initRecorder({
  apiKey: "YOUR_KEY",
  useWsWorker: false,
});

// Capture more of streaming LLM response bodies
initRecorder({
  apiKey: "YOUR_KEY",
  captureStreamingResponseBody: true,
  captureStreamPrefixKb: 256,
  captureStreamTimeoutMs: 30_000,
});

SSR / Next.js note

initRecorder touches localStorage, sessionStorage, and window — none of which exist during server-side rendering. Always guard with typeof window !== "undefined" or call from useEffect / onMount / client-only equivalents.

Without a guard, frameworks like Next.js (on Vercel) will fail at build/SSR with:

ReferenceError: localStorage is not defined

Troubleshooting

  • window.SailfishRecorder is undefined after the <script> loads. You're probably using an ESM URL (esm.run, esm.sh) with a non-module <script>. Either use the UMD URL (/dist/recorder.umd.cjs) or add type="module" to the tag.
  • Refused to create a worker from 'blob:...' in the console. Your Content-Security-Policy blocks blob workers. Either allow worker-src blob: or init with useWsWorker: false.
  • Recordings never reach the dashboard. Verify the apiKey matches the project in the Sailfish dashboard, and that your ad-blocker isn't blocking api-service.sailfishqa.com. Open your browser devtools network tab and filter by sailfish.
  • SSR error: localStorage is not defined. See the SSR note above — initRecorder must run in a browser-only code path.
  • Mixed-content warnings. All Sailfish CDN and API URLs are HTTPS; make sure your page also serves over HTTPS.

License

Proprietary. See sailfishqa.com/terms for terms of service.