Frontend Build Plugin: @sailfish-ai/sf-map-utils
@sailfish-ai/sf-map-utils is the single build-time integration for your
frontend. It does two things, both at build time:
- Source-map upload — de-minifies stack traces so exceptions in the Sailfish dashboard point at your original source files and lines.
- Click-to-code element stamping — stamps interactive elements with an
opaque
data-sf-idattribute and uploads a manifest mapping each id to its source{file, line, component}. When you watch a session replay and select a click, Sailfish resolves the clicked element back to the exact line of source that rendered it.
Click-to-code is build-time only. The shipped DOM carries just a static,
opaque data-sf-id (no file paths, no source-tree leakage) — already captured
by the recorder's standard DOM serialization. There is no React Fiber walk,
no runtime click listener, and no client CPU cost. File/line resolution
happens on Sailfish's servers at replay time.
Earlier versions advertised the recorder's enableFiberTracking option (a
runtime React-Fiber walk) for source mapping. That path is now off by
default and superseded by this build plugin. See the
JavaScript/TypeScript recorder guide
for the upgrade note.
Upgrading: what you get for free
Element stamping is part of the same plugin object you already have in your
bundler config. On Vite, Rollup, esbuild, and webpack, bumping the package
version is the only action required — there is no new configuration. The
existing sfVitePlugin / sfRollupPlugin / sfEsbuildPlugin / SfWebpackPlugin
now stamp during transform and ship the manifest alongside your source maps.
| Toolchain | Customer action on upgrade |
|---|---|
| Vite | None — zero config (npm update @sailfish-ai/sf-map-utils) |
| Rollup | None — zero config |
| esbuild | None — zero config |
| webpack | None — zero config |
| Next.js (native SWC) | One-time withSailfish(...) registration |
tsc / SystemJS / UglifyJS | One-time TypeScript transformer registration |
Next.js and the tsc family require a documented one-time registration line
because their compilers do not auto-load a plugin without it. Once registered,
upgrades are automatic for them too.
Prerequisites
- Source maps enabled in your production build (so stack traces de-minify).
- Next.js ≥ 15.5 if you use the Next.js / native-SWC path. The Sailfish SWC
plugin is a Rust/wasm plugin pinned to a specific
swc_coreABI; older Next versions bundle an incompatible SWC and will reject the plugin. (The four bundler plugins — Vite/Rollup/esbuild/webpack — have no such floor.) - Turbopack is not supported for stamping — see the warning below. Use the webpack compiler for any build you want click-to-code on.
ts-patch(dev dependency) if you use thetsc/ SystemJS / UglifyJS path — installation shown in that section.
Installation
Configure your .npmrc so the Sailfish scopes resolve from the Sailfish
registry:
@sailfish-ai:registry=https://us-npm.pkg.dev/sailfish-ai/sailfishai-npm-public/
@sailfish-rrweb:registry=https://us-npm.pkg.dev/sailfish-ai/sailfishai-npm-public/
Then install as a dev dependency (recommended):
npm install -D @sailfish-ai/sf-map-utils
Zero-config bundlers
These four bundlers stamp elements and upload source maps with no extra configuration. Make sure source maps are enabled in your build, add the Sailfish plugin (if not already present), and bump the version.
Vite
import { sfVitePlugin } from "@sailfish-ai/sf-map-utils/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
sfVitePlugin({
apiKey: "<see-api-key-from-your-account-settings-page>",
}),
],
build: {
outDir: "dist",
sourcemap: true,
},
});
Rollup
import sfRollupPlugin from "@sailfish-ai/sf-map-utils/rollup";
export default {
output: {
sourcemap: true,
},
plugins: [
sfRollupPlugin({
apiKey: "<see-api-key-from-your-account-settings-page>",
}),
],
};
esbuild
import esbuild from "esbuild";
import { sfEsbuildPlugin } from "@sailfish-ai/sf-map-utils/esbuild";
esbuild
.build({
sourcemap: true,
plugins: [
sfEsbuildPlugin({
apiKey: "<see-api-key-from-your-account-settings-page>",
}),
],
})
.catch(() => process.exit(1));
webpack
import SfWebpackPlugin from "@sailfish-ai/sf-map-utils/webpack";
export default {
plugins: [
new SfWebpackPlugin({
apiKey: "<see-api-key-from-your-account-settings-page>",
}),
],
};
The webpack plugin self-injects an enforce: "pre" loader so stamping runs
before your downstream compile — no manual loader wiring required.
Plugin options (all bundlers)
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | Required | Your Sailfish API key. |
includeNodeModules | string[] | [] | Source-map upload only. Specific node_modules packages whose .js.map files to include in the upload. By default node_modules maps are excluded. This does not stamp those packages (see note below). |
allElements | boolean | false | Stamp every host element for click-to-code (not just interactive ones). Interactive-only is recommended to keep recordings small. |
drawSiteStamping | 'auto' | 'all' | 'off' | 'auto' | Stamp canvas draw-call sites so a click on a 2D / WebGL / WebGPU canvas resolves to the source line that issued the draw. 'auto' stamps only canvas-touching modules; 'all' stamps every allowlisted draw call; 'off' disables it. |
createSiteStamping | 'auto' | 'all' | 'off' | 'auto' | Stamp framework object-creation sites (new THREE.Mesh(...), new fabric.Rect(...), …) so a click on a three.js / Fabric / Konva / Pixi / Chart.js object resolves to where you created it. Pairs with the runtime register* adapters — see Click-to-code on framework canvases. Without it you still get the object's identity; with it you also get the file:line. |
rootDir | string | bundler cwd | Project root used to compute the relative paths recorded in the manifest. |
includeNodeModules does not stamp packagesincludeNodeModules controls source-map upload only — it decides which
node_modules .js.map files are uploaded for exception de-minification. It does
not enable click-to-code stamping of components published under
node_modules. Code under node_modules is never stamped, so clicks on a
third-party / design-system <Button> resolve via the ancestor walk (to the
nearest stamped element your own code rendered) rather than to the library's own
source. This is a deliberate v1 limitation — see
Known-unsupported (v1).
One-time setup toolchains
Next.js (native SWC compiler)
Next.js compiles with SWC, which loads a Rust/wasm SWC plugin via
experimental.swcPlugins. Wrap your config with the withSailfish helper from
@sailfish-ai/sf-swc-plugin — this registers the stamping plugin and keeps
source-map upload wired:
const { withSailfish } = require("@sailfish-ai/sf-swc-plugin");
/** @type {import('next').NextConfig} */
const nextConfig = {
// ...your existing config
};
module.exports = withSailfish(nextConfig, { apiKey: "<see-api-key-from-your-account-settings-page>" });
Because Next/SWC will not auto-load a plugin, this withSailfish line is a
one-time registration. After it is in place, version bumps are automatic.
Turbopack (the default next dev compiler in Next.js 15+, and opt-in for
builds via --turbo / --turbopack) does not honor
experimental.swcPlugins — the seam withSailfish registers the stamping plugin
under. To prevent click-to-code stamping from silently being a no-op,
withSailfish throws an Error by default when it detects Turbopack, failing
the build at config load. Source-map upload (exception de-minification) is
unaffected.
Recommended: run the webpack compiler for any build you want stamped:
next build --webpack # or: next dev --webpack
Equivalently, omit --turbo / --turbopack so Next falls back to webpack.
Escape hatch — mixed pipelines: if you intentionally run Turbopack for
local dev and webpack for the stamped production build, pass
allowTurbopack: true to downgrade the error to a per-build warning:
module.exports = withSailfish(nextConfig, { allowTurbopack: true });
The warning fires on every withSailfish call (not once per process), so an
accidentally un-stamped Turbopack build is never shipped silently.
The SWC plugin requires Next.js ≥ 15.5 (swc_core ABI compatibility). On older
Next versions the wasm plugin is rejected. If you can't upgrade, use one of the
four bundler plugins or the tsc transformer path instead.
The SWC plugin produces byte-identical data-sf-id values to the bundler
plugins, so a component stamped under Vite resolves to the same source under
Next. This is enforced by shared conformance fixtures in CI.
tsc / SystemJS / UglifyJS
Plain TypeScript compilation has no plugin hook of its own, so stamping is
applied through a TypeScript custom transformer shipped in
@sailfish-ai/sf-map-utils, registered via
ts-patch (or ttypescript). Source maps
are then uploaded by the post-build CLI.
-
Install
ts-patchas a dev dependency (it provides thetspccompiler that applies custom transformers):Install ts-patchnpm install -D ts-patch -
Enable source maps and register the transformer in
tsconfig.json:tsconfig.json{
"compilerOptions": {
"sourceMap": true,
"plugins": [
{ "transform": "@sailfish-ai/sf-map-utils/transformer" }
]
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
} -
Run the compiler through
ts-patch(e.g.tspcinstead oftsc) so the transformer is applied, then upload source maps + manifest after build:package.json{
"scripts": {
"build": "tspc --project tsconfig.json && npm run upload-sourcemaps",
"upload-sourcemaps": "upload-sourcemap-to-sailfish <see-api-key-from-your-account-settings-page>"
}
}
The same upload-sourcemap-to-sailfish CLI ships both your .js.map files and
the click-to-code manifest. For SystemJS / UglifyJS pipelines, run your existing
bundling/minify step first, then call the upload step — the transformer stamps
during the tsc (tspc) phase that precedes them.
Post-build CLI (source maps)
The sf-map-utils package also provides a CLI, upload-sourcemap-to-sailfish,
for uploading source maps (and the manifest) after building — useful for any
pipeline, including the tsc family above.
upload-sourcemap-to-sailfish <see-api-key-from-your-account-settings-page> [OutputDir] [IncludeNodeModules]
<ApiKey>: Required. Your Sailfish API key.[OutputDir]: Optional. Defaults todist.[IncludeNodeModules]: Optional. Comma-separated list ofnode_modulespackages to include. By defaultnode_modulesmaps are excluded.
node node_modules/.bin/upload-sourcemap-to-sailfish <see-api-key-from-your-account-settings-page> dist react,@emotion/react
How it works
- On each build a per-build UUID (
window.sfMapUuid) is minted. The recorder already sends this id with every session. - Compiled
.js/.js.mapfiles are uploaded tosource-maps/{map_uuid}/, and the click-to-code manifest (stamp-manifest.json) is uploaded into the same namespace. - At replay time, Sailfish joins a clicked element's
data-sf-idand the recording'smap_uuidto the manifest, resolving{file, line, component}and offering an editor deep link. - If a clicked element is unstamped (third-party / not interactive), resolution
walks up to the nearest stamped ancestor (the action owner) or reports
unknowngracefully.
Supported frameworks
Click-to-code stamping is React/JSX (and TSX) only. The stamping transform
parses JSX/TSX and injects data-sf-id on interactive JSX host elements; it has
no knowledge of other component models. The bundler you use is independent of
this — Vite, Rollup, esbuild, webpack, Next/SWC, and tsc are all supported
compilers, but they only stamp the JSX/TSX they compile.
| Capability | Scope |
|---|---|
| In scope (v1) | React / React + TypeScript using JSX or TSX, compiled by any supported bundler above. |
| Out of scope (v1) | Non-React component models (see Known-unsupported list). Recording still works for these; only click-to-code does not. |
(Note: source-map upload works for any framework — the React/JSX restriction applies only to click-to-code element stamping.)
Validated configurations
Configurations Sailfish has run end-to-end (capture → resolve). This table is the authoritative "known-good" matrix; entries are added as each combination is validated.
| Bundler / compiler | Framework | Versions validated | Status |
|---|---|---|---|
| Vite | React (JSX/TSX) | pending validation | stub |
| Rollup | React (JSX/TSX) | pending validation | stub |
| esbuild | React (JSX/TSX) | pending validation | stub |
| webpack | React (JSX/TSX) | pending validation | stub |
| Next.js (SWC, webpack builder) | React (JSX/TSX) | Next ≥ 15.5 | stub |
tsc (via ts-patch) | React (TSX) | TypeScript ≥ 5 | stub |
If your bundler/framework/version combination is not yet listed, stamping may still work but is unvalidated — submit a support request or ask us to validate it.
Known-unsupported (v1)
Click-to-code does not stamp the following. Recording is unaffected unless noted.
- Non-React component models: Vue, Svelte, Angular templates, Astro
(
.astro), Solid, Qwik, Web Components / custom elements. - React Native: N/A — no DOM to serialize, so there is nothing to resolve.
node_modules/ design-system components: code undernode_modulesis never stamped. Clicks on a published<Button>resolve via the ancestor walk to your nearest stamped element, or reportunknown.includeNodeModuleschanges source-map upload only, not stamping.- Turbopack: ignores
experimental.swcPlugins; use the--webpackbuilder. - Unvalidated bundlers: Parcel, Metro, Bun, Deno, and wrapper toolchains (tsup/unbuild) are not yet validated — they may inherit support but are not in the validated matrix above.
Operations & policy
These notes cover supply-chain integrity, storage retention, and operational controls for the click-to-code data path.
Supply-chain provenance & integrity
The Sailfish packages publish from a pinned path (Google Artifact Registry + npmjs.org), so the publish destination is fixed. For build-time integrity:
- Pin exact versions of
@sailfish-ai/sf-map-utilsand@sailfish-ai/sf-swc-plugin(and commit your lockfile) so CI installs a known artifact rather than a floating range. The SWC plugin ships a Rust/wasm binary that runs inside your build, so treat it like any other build-time dependency. - Verify the published wasm against the checksum Sailfish publishes for each
sf-swc-pluginrelease. A subresource-integrity / checksum value is published alongside the release so you can assert the wasm your CI resolves matches what Sailfish built. (Cryptographic signing / SLSA provenance attestation is on the roadmap; until then, version-pin + checksum-verify.) - The shared rule table that drives stamping is single-sourced and a CI guard asserts byte-identity across all three implementations, so the three compilers cannot silently diverge on what gets stamped.
Manifest storage & retention
Click-to-code manifests are stored next to your source maps under
source-maps/{map_uuid}/stamp-manifest.json in the same private bucket Sailfish
already uses for source-map upload.
- The bucket must remain private.
map_uuidis embedded in your bundle (window.sfMapUuid) and is not a secret; manifest confidentiality depends entirely on the bucket not being publicly readable. - Retention / lifecycle: manifests follow the same GCS lifecycle policy as
source maps. Configure an object-lifecycle TTL (e.g. expire blobs older than
your replay-retention window) so per-build manifests don't accumulate
unbounded — each build mints a new
map_uuidand writes a fresh manifest. - Decommissioning a service / deleting recordings removes the associated recordings; align manifest retention with your replay-data retention so source paths are not kept longer than the replays that reference them.
Observability & kill-switch
- SLO / signal: the resolve path emits success / null-reason metrics so a silent drop in click-to-code resolution (e.g. version skew, missing manifest) is observable rather than silent. If clicks stop resolving after an upgrade, check that stamping ran (the build log reports stamped-module counts) and that the manifest uploaded.
- Kill-switch: set
SAILFISH_DISABLE_STAMP=1in your build environment to disable element stamping without removing the plugin — the bundler plugins pass through unchanged and only source-map upload runs. Use this for perf-sensitive or sensitive routes, or to roll back stamping quickly without a config change. Server-side, Sailfish can disable resolution per company.
Looking to set this up for the Desktop App? See the Desktop App build-plugin guide.