Frontend Setup: JavaScript/TypeScript
This guide walks you through instrumenting a frontend application with the SF Veritas Recorder SDK.
Installation
Install the Recorder package:
npm install @sailfish-ai/recorder
Or with yarn:
yarn add @sailfish-ai/recorder
The CDN / script-tag install is
primarily for Enterprise production frontends. For Desktop App local
development, keep reading — the npm path is what lets you point the
recorder at http://localhost:6776.
Basic Setup
Add the following to your application's entry point (e.g., index.tsx, main.tsx). The key is to wrap the initialization in an environment check so it only runs during local development:
// Initialize SF Veritas Recorder ONLY in development mode
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
// Your company's allowed domains — add more here if you have additional internal hosts.
domainsToPropagateHeaderTo: ["*"],
});
});
}
// Your application code continues below...
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);
Vanilla JavaScript
// Initialize only in development
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
});
});
}
// Your application code...
console.log('Application started'); // This will appear in SF Veritas
Configuration Options
Basic Options
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | Required | API key for authentication (use "sf-veritas-local" for local mode) |
backendApi | string | "https://api-service.sailfishqa.com" | Backend API URL — use "http://localhost:6776" for local development |
Privacy & Masking
By default, any DOM element with the class sailfishSanitize (and all of its descendants) is masked: visible text is replaced with * in the recording, and form input values entered anywhere under that subtree are sanitized in the browser before they are sent to SF Veritas — so sensitive data never leaves the user's machine. You only need to add the class to the parent element you want to protect; every nested child inherits it automatically. To use a different class name, set maskTextClass in initRecorder.
| Option | Type | Default | Description |
|---|---|---|---|
maskTextClass | string | "sailfishSanitize" | CSS class name; any element with this class (and all its descendants) has visible text replaced with * in recordings, and form input values under the subtree are sanitized before they leave the browser. |
import { initRecorder } from '@sailfish-ai/recorder';
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
maskTextClass: 'pii-redact', // overrides default "sailfishSanitize"
});
<!-- Mask the whole subtree by tagging the parent only: -->
<div class="pii-redact">
<p>This text becomes ****</p>
<input value="this value is sanitized" />
</div>
Canvas Recording & Privacy
Enabling canvas recording
Canvas recording is off by default. Turn it on per-app with the recordCanvas init option, or per-company from the Sailfish dashboard (Capture Settings). A value passed to initRecorder wins over the dashboard setting.
import { initRecorder } from '@sailfish-ai/recorder';
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
recordCanvas: true, // capture <canvas> content: 2D command stream, WebGL/WebGPU video, pixel keyframes
// maskCanvasClass: 'pii-redact', // optional — custom canvas-mask class (see below; default "sailfishSanitize")
});
| Option | Type | Default | Description |
|---|---|---|---|
recordCanvas | boolean | false | Enable the pixel-perfect canvas recorder (2D command stream + WebGL/WebGPU video + keyframes). |
maskCanvasClass | string | "sailfishSanitize" | CSS class that excludes a <canvas> (or any ancestor) from recording — see below. |
When canvas recording is enabled, Sailfish captures the pixel content of your <canvas> elements. For 2D contexts it additionally captures the canvas draw command stream, which includes the literal string arguments of text draws — every fillText() / strokeText() call. Sailfish automatically redacts recognizable credit-card (Luhn-validated), SSN, and date-of-birth values from the command stream (see Automatic PII redaction below), but any other on-screen text — names, emails, account numbers — is captured verbatim. If a canvas can display sensitive data, mask it.
Controlling what is captured
Canvas recording honors the same privacy selectors as DOM masking (configured via the Privacy & Masking capture settings — maskTextSelector, blockSelector, unmaskSelector, plus the canvas-specific maskCanvasClass / data-sf-mask). Each rule matches the <canvas> element itself or any ancestor:
| Control | Effect on a matching canvas |
|---|---|
data-sf-mask attribute, the maskCanvasClass class (default sailfishSanitize), or blockSelector | Excluded entirely — no commands, no keyframes, no video. A single op:"masked" placeholder marks it in the session. |
maskTextClass / maskTextSelector | Text redacted — every fillText() / strokeText() string is masked (****). Keyframes are kept: only the masked text's pixel region is blanked (region-blanking), so the canvas stays pixel-perfect everywhere else. The canvas stays command-only (it never demotes to video). (A WebGL / WebGPU / worker canvas that matches is excluded entirely, since its text is pixels, not commands.) |
maskCanvasText (global) | Mask all canvas text — redacts drawn text on every recorded canvas. Unset = mirror-DOM: if any DOM text masking (maskTextSelector / maskTextClass) is configured, canvas text is masked too; set false to opt out. |
unmaskSelector | Override — records normally even inside a masked / blocked subtree (mirrors DOM precedence). |
Redaction style. By default masked text renders as length-preserving asterisks (
Jane→****) and masked shapes keep their footprint, recolored to a neutral redaction color — so layout is preserved while content is hidden. Tune it globally withcanvasRedactStyle: { text: 'asterisks' | 'bar', shape: 'color' | 'block' | 'remove' }, or per call onmaskCanvasDraws()(below).
<!-- Exclude a canvas (and everything under a wrapper): -->
<canvas data-sf-mask></canvas>
<div data-sf-mask><canvas><!-- not recorded --></canvas></div>
<!-- Or by class (default "sailfishSanitize", or your maskCanvasClass): -->
<canvas class="sailfishSanitize"></canvas>
Exclusion is a hard guarantee across every capture layer (2D/WebGL command stream, keyframes, and video) — not a best-effort filter.
Masking specific drawings within a canvas — maskCanvasDraws()
The selectors above operate on the whole <canvas> element — that is the only granularity CSS selectors can offer, because a canvas is drawn imperatively (fillRect, moveTo/lineTo/fill, fillText) into a single element with no sub-elements for a selector to target. To redact one shape, region, geometry, or label inside a canvas, wrap those draw calls in maskCanvasDraws(). Wrapped content is redacted in place — shapes keep their footprint but are recolored to a neutral redaction color, and text becomes length-preserving asterisks — while the masked pixel regions are blanked on every keyframe so the original never leaves the browser. The rest of the canvas stays pixel-perfect; everything outside the wrapper records normally:
import { initRecorder, maskCanvasDraws } from '@sailfish-ai/recorder';
function render(ctx) {
// Recorded normally:
ctx.fillStyle = '#0a0';
ctx.fillRect(0, 0, 800, 600);
// Redacted in place — the rectangle keeps its position/size but is recolored to
// the redaction color, and the label becomes asterisks:
maskCanvasDraws(ctx, () => {
ctx.fillStyle = '#007BFF';
ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
ctx.fillText(patientName, rectX + 8, rectY + 20); // → "***********"
});
// Drop the geometry entirely when the SHAPE itself is sensitive:
maskCanvasDraws(ctx, () => drawJSTSGeometry(sensitiveGeometry, ctx), { shape: 'remove' });
}
What happens:
- Each draw inside the callback is redacted, not silently dropped: shapes are recolored to the redaction color (
shape: 'color', the default — same geometry, redacted fill/stroke), andfillText/strokeTextare masked to length-preserving asterisks (text: 'asterisks', the default). State setters inside the block are overridden, not honored. - The masked pixel regions are blanked on every keyframe so the real pixels never ship; the canvas stays pixel-perfect outside the masked regions, and stays command-only (it never demotes to video).
Per-call style — override the global canvasRedactStyle for one block:
maskCanvasDraws(ctx, () => drawChartLabels(ctx), { text: 'bar' }); // solid bar over text
maskCanvasDraws(ctx, () => drawSecretRegion(ctx), { shape: 'block' }); // solid block over the bbox
maskCanvasDraws(ctx, () => drawSecretRegion(ctx), { shape: 'remove' }); // drop the geometry
maskCanvasDraws(ctx, () => drawLabeledShape(ctx), { keepShapes: true }); // redact only the text
text:'asterisks'(default) |'bar'(a solid redaction bar over the text box).shape:'color'(default — same shape recolored) |'block'(solid block over the bounding box) |'remove'(drop the geometry, e.g. when coordinates themselves are sensitive).keepShapes: redact only text inside the block; leave shapes visible. Note: "shapes" includes images — adrawImage()inside akeepShapesblock records and ships normally, so do not draw a sensitive image inside akeepShapesblock (use the default block,shape: 'remove', or a whole-canvas mask for sensitive images).
Guidelines:
- Keep the wrapped block self-contained — set the
fillStyle/strokeStyle/ transform you need inside it (or usectx.save()/ctx.restore()). Masked state setters are overridden, so a style you change inside but rely on outside won't carry over in replay. maskCanvasDrawsis 2D-only. For WebGL / WebGPU (whose content is pixels, not commands), exclude the whole canvas withdata-sf-mask/maskCanvasClass.- It is a safe no-op when recording is off or the canvas isn't being recorded — leave the wrappers in your render loop unconditionally.
Automatic PII redaction (content detection)
Independently of selectors, when the data-sensitivity settings record_credit_card_info, record_ssn, or record_dob are off (the default), Sailfish scans drawn text and redacts matching credit-card (Luhn-validated), SSN, and date-of-birth values in the stored command stream — even on un-masked canvases. This protects the verbatim string in the recording. Note it redacts the command text only: a keyframe or video frame of an un-masked canvas still renders the on-screen pixels, so for a canvas that can show sensitive data, use the canvas mask above for full protection. (PII split across two separate fillText() calls is not detected — mask such canvases.)
Example: a billing dashboard with sensitive and non-sensitive canvases
A dashboard renders three canvases — an account chart that draws the customer's name and account number, an invoice preview inside a PII section, and a decorative sparkline with no sensitive data. Exclude the first two, let the last record normally:
<!-- 1) Exclude with the data-sf-mask attribute — records NOTHING. -->
<canvas id="account-chart" data-sf-mask width="640" height="320"></canvas>
<!-- 2) Exclude by class — everything under .billing-pii is excluded too. -->
<section class="billing-pii">
<canvas id="invoice-preview" width="480" height="200"></canvas>
</section>
<!-- 3) No PII — records pixel-perfectly. -->
<canvas id="trend-sparkline" width="200" height="60"></canvas>
import { initRecorder } from '@sailfish-ai/recorder';
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
recordCanvas: true, // turn canvas recording on
maskCanvasClass: 'billing-pii', // (optional) your own exclude class; defaults to "sailfishSanitize"
});
// #account-chart is masked → these draws are never recorded:
const acct = document.querySelector('#account-chart').getContext('2d');
acct.fillText('Jane Doe — Acct 4111 1111 1111 1111', 16, 24);
// #trend-sparkline is not masked → recorded verbatim (no PII here):
const trend = document.querySelector('#trend-sparkline').getContext('2d');
trend.fillText('Last 7 days', 4, 12);
In replay: #account-chart and #invoice-preview show a masked placeholder (no commands, keyframes, or video were ever sent); #trend-sparkline replays pixel-perfectly.
Keep the layout but hide the text. To let a canvas's shapes/axes replay pixel-perfectly while its text is redacted, use a text-mask selector (or the global maskCanvasText) instead of excluding it. Every fillText()/strokeText() becomes ****, keyframes are kept with only the text regions blanked, and the rest of the canvas stays pixel-perfect:
<canvas class="redact-text" width="480" height="240"></canvas>
…with maskTextSelector: '.redact-text' in your Capture Settings. (If your app already masks DOM text, canvas text is masked automatically by the mirror-DOM default — no extra selector needed unless you set maskCanvasText: false.)
Automatic safety net. Even on a canvas you forgot to mask, a stray card / SSN / DOB number is redacted from the stored commands automatically (the record_* flags are off by default):
ctx.fillText('Card 4111 1111 1111 1111', 8, 40);
// stored in the recording as: "Card **** **** **** 1111"
(Remember: this protects the stored command text, not pixels — for a canvas that visibly shows sensitive data, exclude it with data-sf-mask / maskCanvasClass / blockSelector.)
HIPAA
A HIPAA-enabled account still records canvas. The data is routed to the PHI processing pipeline downstream, exactly like other recorded events — HIPAA is a routing decision, not a capture switch. To keep a specific sensitive canvas out of recording entirely, mask it with data-sf-mask / maskCanvasClass / blockSelector as above.
If you draw a cross-origin (tainted) image or video onto a canvas without proper CORS headers, the browser marks the canvas as tainted and its pixels are unreadable. Sailfish detects this, captures no pixels for that canvas, and emits a placeholder marker instead — so a tainted canvas can never leak content.
Click-to-code on framework canvases
When you record a canvas drawn by a framework, a click in replay can resolve to the logical object you clicked — a three.js Mesh, a Fabric shape, a Chart.js datum — instead of raw pixels. Register your live framework instance so Sailfish can hit-test it. Each helper is imported from @sailfish-ai/recorder, binds to that framework's own canvas, returns an unregister function, and is a safe no-op when recording is off:
import {
registerThreeScene,
registerFabricCanvas,
registerKonvaStage,
registerPixiApp,
registerChart,
} from '@sailfish-ai/recorder';
// three.js — pass your scene, camera, renderer, AND your THREE namespace
// (the SDK never bundles three.js):
const stop = registerThreeScene(scene, camera, renderer, THREE);
// the others take the framework's primary object:
registerFabricCanvas(fabricCanvas); // a fabric.Canvas
registerKonvaStage(stage); // a Konva.Stage
registerPixiApp(app); // a PIXI.Application
registerChart(chart); // a Chart.js Chart
// each returns an unregister fn — call it on teardown (component unmount, etc.):
stop();
A click then surfaces the clicked object's identity (type + label, e.g. three Mesh: playerShip). To additionally resolve the click to the source line where you created that object (new THREE.Mesh(...)), enable creation-site stamping in the build plugin — see Build Plugin (createSiteStamping). Without the build plugin you still get the object identity; with it you also get the file:line.
Service Identification
| Option | Type | Default | Description |
|---|---|---|---|
serviceIdentifier | string | "" | Unique name for your frontend service (e.g. "my-react-app") |
serviceVersion | string | "" | Version of your application |
gitSha | string | Auto-detected | Git commit SHA for version tracking |
serviceAdditionalMetadata | Record<string, any> | {} | Custom metadata to attach to sessions (e.g. { env: "development" }) |
Advanced Options
| Option | Type | Default | Description |
|---|---|---|---|
domainsToPropagateHeaderTo | string[] | [] | Allowlist: when non-empty, tracing headers are only sent to requests matching these domains (supports wildcards: *.example.com). Recommended when browser extensions cause conflicts. |
domainsToNotPropagateHeaderTo | string[] | See below | Blocklist: domains to exclude from header propagation. When used with domainsToPropagateHeaderTo, acts as exceptions within the allowed set. |
enableIpTracking | boolean | false | Fetch visitor IP in the background for session metadata |
customBaseUrl | string | — | Custom base URL for the triage/report issue interface |
reportIssueShortcuts | Partial<ShortcutsConfig> | { enabled: false } | Keyboard shortcuts for the issue reporting modal |
Network Body Capture
Control how response bodies are captured for telemetry. These options are especially important for applications that use streaming responses (SSE, ndjson, chunked transfer).
| Option | Type | Default | Description |
|---|---|---|---|
captureStreamingResponseBody | boolean | true | Capture a limited prefix of streaming response bodies (SSE, ndjson, gRPC-Web). When false, streaming responses have no body in telemetry. |
captureResponseBodyMaxMb | number | 10 | Maximum response body size in MB to capture. Responses larger than this limit are recorded without body data. Set to 0 to disable all response body capture (applies to both fetch and XHR). |
captureStreamPrefixKb | number | 64 | Maximum prefix size in KB to capture from streaming responses. Only the first N KB of the stream is captured for telemetry. |
captureStreamTimeoutMs | number | 10000 | Timeout in milliseconds for streaming prefix capture. If the stream hasn't delivered enough data within this window, whatever was captured is sent. |
Streaming Support: The recorder automatically detects streaming responses (text/event-stream, application/x-ndjson, application/stream+json, application/grpc, application/grpc-web) and handles them without blocking or buffering. Streaming responses are returned to your application immediately — body capture happens asynchronously in the background. Binary responses (application/octet-stream) skip body capture entirely.
Performance Optimization
These options reduce the recorder's impact on Total Blocking Time (TBT) by deferring and chunking the initial DOM snapshot. They are most effective on pages with large DOM trees (8,000+ nodes).
| Option | Type | Default | Description |
|---|---|---|---|
deferRecording | boolean | true | Defer heavy DOM recording (rrweb snapshot + MutationObserver) until after the page is interactive. Network interceptors, error handlers, and console capture still install immediately. Recording starts when all of the following are met: page load is complete, and one of: the browser is idle (requestIdleCallback), the user interacts (click, scroll, keydown, touchstart), or a 10-second ceiling timer fires. |
chunkSnapshot | boolean | false | Break the initial full-DOM snapshot into async chunks, yielding to the main thread every 500 nodes or 16 ms (whichever comes first). This prevents a single long task from blocking the main thread during snapshot serialization. Nodes removed between yield points are silently skipped. Best combined with deferRecording: true. |
Example — maximum TBT reduction:
initRecorder({
apiEndpoint: 'http://localhost:6776/graphql/',
deferRecording: true, // defer snapshot until after TTI
chunkSnapshot: true, // async chunked snapshot with yield points
});
How it works:
initRecorder()installs lightweight interceptors immediately (network, console, errors)- Heavy DOM recording is deferred until the page is idle or the user interacts
- When recording starts, the DOM snapshot is broken into chunks that yield to the main thread, keeping individual task durations under 50 ms
deferRecording defaults to true, so most applications benefit from deferred recording automatically. Add chunkSnapshot: true if your pages have large or deeply nested DOM trees and you observe long tasks during the initial snapshot.
Advanced Configuration
Domain Filtering
Filter network request capture by domain:
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
// Exclude third-party services from header propagation
domainsToNotPropagateHeaderTo: ['analytics.google.com', 'sentry.io'],
});
});
}
Header Propagation
Control which domains receive tracing headers for distributed tracing across frontend and backend. The option supports the same wildcard syntax that the backend SDK uses — "*.example.com", "api.example.com:8080", "api.example.com/v1/*", etc.
Allowlist mode (recommended): When domainsToPropagateHeaderTo is set, headers are only injected into requests matching those domains. This prevents browser extensions (e.g., SimilarWeb, ad blockers) from intercepting requests with Sailfish headers, which can cause browser freezes or CORS errors.
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
// Only inject tracing headers for requests to your own backend
domainsToPropagateHeaderTo: ['localhost:*', 'api.myapp.com', '*.internal.com'],
});
});
}
Combined allowlist + blocklist: When both are provided, a request must match the allowlist and not match the blocklist. This lets you broadly allow a domain while excluding specific subdomains:
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
serviceVersion: '1.0.0',
domainsToPropagateHeaderTo: ['*.mycompany.com'],
// Exclude auth subdomain that uses a third-party provider
domainsToNotPropagateHeaderTo: ['auth.mycompany.com'],
});
});
}
Completely disable propagation — pass an empty allow list:
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-frontend-app',
domainsToPropagateHeaderTo: [], // kill switch — no URL receives the header
});
});
}
In Sailfish Enterprise, domainsToPropagateHeaderTo is pre-populated with wildcard entries for every backend service we detect in your connected repositories (e.g. "*.acme.com", "api.acme.com/*"). Local-devtools installs are typically single-service and point at localhost, so the allowlist isn't critical here — but the same wildcard syntax applies when you graduate to Enterprise or run against a remote collector.
By default (when domainsToPropagateHeaderTo is empty), tracing headers are sent to all domains except a built-in denylist of common third-party services (Twitter, Gravatar, Zendesk, AWS, etc.). Setting an allowlist is recommended for production to avoid conflicts with browser extensions.
Source File Tracking (Click-to-Code)
Mapping UI elements back to their source files — so that selecting a click in a
replay jumps to the exact file:line that rendered it — is handled by the
frontend build plugin, @sailfish-ai/sf-map-utils, not by the recorder at
runtime.
The build plugin stamps interactive elements at build time (zero runtime
overhead) and uploads a source manifest alongside your source maps. See the
Build Plugin guide for per-toolchain setup and the
allElements / rootDir options.
If you already use @sailfish-ai/sf-map-utils for source maps on Vite,
Rollup, esbuild, or webpack, click-to-code turns on automatically when you
bump the package version — there is no new configuration. Next.js and the
tsc/SystemJS/UglifyJS family need a one-time registration; see the
Build Plugin guide.
Framework Examples
Create React App
// src/index.tsx
// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-react-app',
serviceVersion: '1.0.0',
});
});
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Next.js (App Router)
// app/layout.tsx
'use client';
import { useEffect } from 'react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-nextjs-app',
serviceVersion: '1.0.0',
});
});
}
}, []);
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
The code above captures browser-side telemetry (console logs, network requests, errors). To also capture server-side telemetry (API routes, server components, server actions), add an instrumentation.ts file to your project root. See the Backend JS/TS setup guide for the full instrumentation.ts pattern.
Next.js (Pages Router)
// pages/_app.tsx
import { useEffect } from 'react';
import type { AppProps } from 'next/app';
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-nextjs-app',
serviceVersion: '1.0.0',
});
});
}
}, []);
return <Component {...pageProps} />;
}
export default MyApp;
The code above captures browser-side telemetry (console logs, network requests, errors). To also capture server-side telemetry (API routes, getServerSideProps, etc.), add an instrumentation.ts file to your project root. See the Backend JS/TS setup guide for the full instrumentation.ts pattern.
Vite
// src/main.tsx
// Vite uses import.meta.env.DEV for development detection
if (import.meta.env.DEV) {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: 'sf-veritas-local',
backendApi: 'http://localhost:6776',
serviceIdentifier: 'my-vite-app',
serviceVersion: '1.0.0',
});
});
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Verifying the Setup
- Start your frontend development server
- Open your app in the browser
- In VS Code, open
SF Veritas: Show Console Logs - In your browser console, type:
console.log("Test from browser") - The log should appear in the SF Veritas Console panel
Troubleshooting
Logs not appearing
- Check CORS: Ensure your local server allows requests from your frontend origin
- Check the endpoint: Verify
backendApiis set to'http://localhost:6776'(not'http://localhost:6776/graphql/'— the SDK appends the path automatically) - Check browser console: Look for errors related to the recorder
CORS errors
If you see CORS errors in the browser console:
- The local SF Veritas server should handle CORS automatically
- Ensure you're using
http://localhost:6776not127.0.0.1 - Check if another service is running on port 6776
Network requests not captured
- If
domainsToPropagateHeaderTois set, check if the domain matches the allowlist - Check if the domain is in
domainsToNotPropagateHeaderToor the built-in denylist - Requests to the SF Veritas endpoint itself are not captured (to prevent loops)
Combining with Backend
When using both frontend and backend instrumentation:
- Frontend logs will show with source type "browser"
- Backend logs will show with your
serviceIdentifier - Use the service filter in the Console to separate them
- Network requests from frontend to backend will be visible
Frontend (browser) ──────▶ Backend (user-service) ──────▶ Database
│ │
│ console.log │ console.log
▼ ▼
┌─────────────────────────────────────┐
│ SF Veritas Console │
│ [browser] Button clicked │
│ [user-service] Handling request │
│ [user-service] Query executed │
└─────────────────────────────────────┘
How It Works
The environment variable controls when SF Veritas Recorder is active:
| Framework | Environment Check | Dev Command | Result |
|---|---|---|---|
| Create React App | process.env.NODE_ENV | npm start | ✅ Active |
| Next.js | process.env.NODE_ENV | npm run dev | ✅ Active |
| Vite | import.meta.env.DEV | npm run dev | ✅ Active |
| Any | Any | npm run build | ❌ Not bundled |
| Any | Any | Production deploy | ❌ Not loaded |
Most frontend frameworks automatically set the appropriate development flag:
- Create React App: Sets
NODE_ENV=developmentduringnpm start - Next.js: Sets
NODE_ENV=developmentduringnpm run dev - Vite: Sets
import.meta.env.DEV=trueduringnpm run dev