@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.
- Signup / dashboard: https://app.sailfishqa.com
- Source: github.com/SailfishAI/sailfish (internal)
Installation
npm install @sailfish-ai/recorder
# or
yarn add @sailfish-ai/recorder
# or
pnpm add @sailfish-ai/recorderQuick 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 latest1.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 -- --headedOutput 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 definedTroubleshooting
window.SailfishRecorderis 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 addtype="module"to the tag.Refused to create a worker from 'blob:...'in the console. Your Content-Security-Policy blocks blob workers. Either allowworker-src blob:or init withuseWsWorker: false.- Recordings never reach the dashboard. Verify the
apiKeymatches the project in the Sailfish dashboard, and that your ad-blocker isn't blockingapi-service.sailfishqa.com. Open your browser devtools network tab and filter bysailfish. - SSR error:
localStorage is not defined. See the SSR note above —initRecordermust 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.
