Skip to main content

Frontend Setup: JavaScript/TypeScript

Enterprise Configuration

This guide covers frontend instrumentation for Sailfish Enterprise. Key differences from local development:

  • API key: Use your Enterprise API key (shown as "<ApiKey />" below -- auto-replaced when you're logged in)
  • Backend API: Do not set backendApi -- the SDK defaults to the Sailfish cloud endpoint automatically
  • Service identifier: Use the <org>/<repo>/<path-to-this-file> format (e.g., acme-corp/web-app/src/index.tsx)
  • No environment gating: Enterprise telemetry runs in all environments (staging, production)

If you connected GitHub and received Auto-Installation PRs, the API key, service identifier, and graphql endpoint are already configured for you.

This guide walks you through instrumenting a frontend application with the SF Veritas Recorder SDK for Sailfish Enterprise.

Getting Your API Key

  1. Open the Sailfish dashboard
  2. Log in with your enterprise email
  3. Navigate to Settings > Configuration
  4. Copy your company's API key

Installation

Install the Recorder package:

npm install @sailfish-ai/recorder

Or with yarn:

yarn add @sailfish-ai/recorder
No build step?

If your frontend doesn't have a bundler (static HTML, CMS embeds, Shopify / WordPress / Webflow, server-rendered pages), see the CDN / script-tag install guide for a zero-dependency <script>-tag install.

Basic Setup

Add the following to your application's entry point (e.g., index.tsx, main.tsx):

import { initRecorder } from '@sailfish-ai/recorder';

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/index.tsx', // Format: <org>/<repo>/<path-to-this-file>
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

import { initRecorder } from '@sailfish-ai/recorder';

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/main.js', // Format: <org>/<repo>/<path-to-this-file>
serviceVersion: '1.0.0',
});

// Your application code...
console.log('Application started'); // This will appear in Sailfish

Configuration Options

Basic Options

OptionTypeDefaultDescription
apiKeystringRequiredYour Sailfish Enterprise API key

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 Sailfish — 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.

OptionTypeDefaultDescription
maskTextClassstring"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: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/index.tsx',
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

OptionTypeDefaultDescription
serviceIdentifierstring""Unique identifier in <org>/<repo>/<path> format
serviceVersionstring""Version of your application
gitShastringAuto-detectedGit commit SHA for version tracking
serviceAdditionalMetadataRecord<string, any>{}Custom metadata to attach to sessions

Advanced Options

OptionTypeDefaultDescription
domainsToPropagateHeaderTostring[][]Allowlist: when non-empty, tracing headers are only sent to requests matching these domains (supports wildcards: *.example.com). Recommended when browser extensions cause conflicts.
domainsToNotPropagateHeaderTostring[]See belowBlocklist: domains to exclude from header propagation. When used with domainsToPropagateHeaderTo, acts as exceptions within the allowed set.
enableFiberTrackingbooleantrueEnable React Fiber tracking -- adds data-sf-source attributes to DOM elements
enableIpTrackingbooleanfalseFetch visitor IP in the background for session metadata
customBaseUrlstring--Custom base URL for the triage/report issue interface
reportIssueShortcutsPartial<ShortcutsConfig>{ enabled: false }Keyboard shortcuts for the issue reporting modal

Network Body Capture

OptionTypeDefaultDescription
captureStreamingResponseBodybooleantrueCapture a limited prefix of streaming response bodies
captureResponseBodyMaxMbnumber10Maximum response body size in MB to capture
captureStreamPrefixKbnumber64Maximum prefix size in KB to capture from streaming responses
captureStreamTimeoutMsnumber10000Timeout in milliseconds for streaming prefix capture

Performance Optimization

OptionTypeDefaultDescription
deferRecordingbooleantrueDefer heavy DOM recording until after the page is interactive
chunkSnapshotbooleanfalseBreak the initial full-DOM snapshot into async chunks

Advanced Configuration

Domain Filtering

Filter network request capture by domain:

import { initRecorder } from '@sailfish-ai/recorder';

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/index.tsx', // Format: <org>/<repo>/<path-to-this-file>
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.

import { initRecorder } from '@sailfish-ai/recorder';

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/index.tsx',
serviceVersion: '1.0.0',
// Only inject tracing headers for requests to your own backend
domainsToPropagateHeaderTo: ['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:

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/index.tsx',
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:

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-app/src/index.tsx',
serviceVersion: '1.0.0',
domainsToPropagateHeaderTo: [], // kill switch — no URL receives the header
});
Auto-Installation populates this for you

If you onboarded via Auto-Installation PRs, domainsToPropagateHeaderTo is pre-populated with wildcard entries for every backend service we detected in your connected repositories (e.g. "*.acme.com", "api.acme.com/*"). Browser CORS rules mean the header is only sent where your server explicitly opts in via Access-Control-Allow-Headers, so the worst case of a too-permissive allowlist is the header being stripped by the browser — not sent to unintended origins.

info

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.

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: [
['@sailfish-ai/recorder/babel-plugin', { allElements: true }],
],
};

Framework Examples

Create React App

// src/index.tsx
import { initRecorder } from '@sailfish-ai/recorder';

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/react-app/src/index.tsx', // Format: <org>/<repo>/<path-to-this-file>
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(() => {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/nextjs-app/app/layout.tsx', // Format: <org>/<repo>/<path-to-this-file>
serviceVersion: '1.0.0',
});
});
}, []);

return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Server-side instrumentation

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(() => {
import('@sailfish-ai/recorder').then(({ initRecorder }) => {
initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/nextjs-app/pages/_app.tsx', // Format: <org>/<repo>/<path-to-this-file>
serviceVersion: '1.0.0',
});
});
}, []);

return <Component {...pageProps} />;
}

export default MyApp;

Vite

// src/main.tsx
import { initRecorder } from '@sailfish-ai/recorder';

initRecorder({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/vite-app/src/main.tsx', // Format: <org>/<repo>/<path-to-this-file>
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

  1. Deploy your frontend with the recorder configured
  2. Open your app in the browser
  3. Open the Sailfish dashboard
  4. Trigger some activity -- you should see telemetry appearing

Troubleshooting

Logs not appearing

  1. Check the API key: Ensure apiKey is set to your Enterprise API key
  2. Check browser console: Look for errors related to the recorder
  3. Check network tab: Verify requests to api-service.sailfish.ai are succeeding

CORS errors

  1. The Sailfish cloud endpoint handles CORS automatically
  2. If you see CORS errors, check that you are not overriding backendApi

Network requests not captured

  1. If domainsToPropagateHeaderTo is set, check if the domain matches the allowlist
  2. Check if the domain is in domainsToNotPropagateHeaderTo or the built-in denylist
  3. Requests to the Sailfish endpoint itself are not captured (to prevent loops)

Combining with Backend

When using both frontend and backend instrumentation:

  1. Frontend logs will show with source type "browser"
  2. Backend logs will show with your serviceIdentifier
  3. Use the service filter in the Sailfish dashboard to separate them
  4. Network requests from frontend to backend will be visible

Next Steps


Local Development

Looking to set up SF Veritas for local development with the Desktop App? See the Desktop App frontend guide.