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>
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. |
enableFiberTracking | boolean | true | Enable React Fiber tracking — adds data-sf-source attributes to DOM elements (dev mode only) |
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
To map UI components back to their source files, add the Babel plugin to your build configuration. This is required for coverage analysis and component tracing to work properly.
Vite
// vite.config.ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
react({
babel: {
plugins: [
['@sailfish-ai/recorder/babel-plugin', { allElements: true }],
],
},
}),
],
});
Next.js
// babel.config.js
module.exports = {
presets: ['next/babel'],
plugins:
process.env.NODE_ENV === 'development'
? [['@sailfish-ai/recorder/babel-plugin', { allElements: true }]]
: [],
};
Plugin Options
| Option | Type | Default | Description |
|---|---|---|---|
allElements | boolean | false | Add source info to all elements (not just interactive ones) |
rootDir | string | process.cwd() | Root directory to strip from file paths |
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