Skip to main content

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:

  1. Source-map upload — de-minifies stack traces so exceptions in the Sailfish dashboard point at your original source files and lines.
  2. Click-to-code element stamping — stamps interactive elements with an opaque data-sf-id attribute 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.
Zero runtime overhead

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.

Replaces the old runtime fiber path

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.

ToolchainCustomer action on upgrade
ViteNone — zero config (npm update @sailfish-ai/sf-map-utils)
RollupNone — zero config
esbuildNone — zero config
webpackNone — zero config
Next.js (native SWC)One-time withSailfish(...) registration
tsc / SystemJS / UglifyJSOne-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_core ABI; 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 the tsc / SystemJS / UglifyJS path — installation shown in that section.

Installation

Configure your .npmrc so the Sailfish scopes resolve from the Sailfish registry:

.npmrc
@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):

Install sf-map-utils
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

vite.config.ts
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

rollup.config.js
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

esbuild.config.mjs
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

webpack.config.mjs
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)

OptionTypeDefaultDescription
apiKeystringRequiredYour Sailfish API key.
includeNodeModulesstring[][]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).
allElementsbooleanfalseStamp 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.
rootDirstringbundler cwdProject root used to compute the relative paths recorded in the manifest.
includeNodeModules does not stamp packages

includeNodeModules 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:

next.config.js
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 is not supported by the stamping plugin

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:

Use the webpack builder for stamped builds
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:

next.config.js — mixed Turbopack + webpack pipeline
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.

Minimum Next.js version

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.

Stamping parity

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.

  1. Install ts-patch as a dev dependency (it provides the tspc compiler that applies custom transformers):

    Install ts-patch
    npm install -D ts-patch
  2. 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"]
    }
  3. Run the compiler through ts-patch (e.g. tspc instead of tsc) 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.

Basic CLI command
upload-sourcemap-to-sailfish <see-api-key-from-your-account-settings-page> [OutputDir] [IncludeNodeModules]
  • <ApiKey>: Required. Your Sailfish API key.
  • [OutputDir]: Optional. Defaults to dist.
  • [IncludeNodeModules]: Optional. Comma-separated list of node_modules packages to include. By default node_modules maps are excluded.
Locally-installed example
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.map files are uploaded to source-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-id and the recording's map_uuid to 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 unknown gracefully.

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.

CapabilityScope
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 / compilerFrameworkVersions validatedStatus
ViteReact (JSX/TSX)pending validationstub
RollupReact (JSX/TSX)pending validationstub
esbuildReact (JSX/TSX)pending validationstub
webpackReact (JSX/TSX)pending validationstub
Next.js (SWC, webpack builder)React (JSX/TSX)Next ≥ 15.5stub
tsc (via ts-patch)React (TSX)TypeScript ≥ 5stub

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 under node_modules is never stamped. Clicks on a published <Button> resolve via the ancestor walk to your nearest stamped element, or report unknown. includeNodeModules changes source-map upload only, not stamping.
  • Turbopack: ignores experimental.swcPlugins; use the --webpack builder.
  • 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-utils and @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-plugin release. 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_uuid is 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_uuid and 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=1 in 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.

Local Development

Looking to set this up for the Desktop App? See the Desktop App build-plugin guide.