Skip to main content

Backend Setup: JavaScript/TypeScript

This guide walks you through instrumenting a Node.js application (JavaScript or TypeScript) with the SF Veritas SDK.

Installation

Install the SF Veritas package:

npm install @sailfish-ai/sf-veritas

Or with yarn:

yarn add @sailfish-ai/sf-veritas

Basic Setup

Add the following to your application's entry point (e.g., index.ts, app.ts, or server.ts). The key is to wrap the initialization in a NODE_ENV check so it only runs during local development:

// Initialize SF Veritas ONLY in development mode
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'my-backend-service',
serviceVersion: '1.0.0',
// Your company's allowed domains — add more here if you have additional internal hosts.
domainsToPropagateHeadersTo: ["*"],
});
});
}

// Your application code continues below...
import express from 'express';

const app = express();
// ... rest of your application

Configuration Options

OptionTypeRequiredDescription
apiKeystringYesUse "sf-veritas-local" for local development
apiGraphqlEndpointstringYesURL of the local collector (default port 6776)
serviceIdentifierstringYesUnique name for your service
serviceVersionstringNoVersion of your service
gitShastringNoGit commit SHA; auto-detected from common CI env vars
debugbooleanNoEnable verbose debug logging

Header-Propagation Options

Control which outbound domains receive Sailfish's distributed-tracing header (X-Sf3-Rid). Mostly relevant once you take the SDK beyond the local collector — if you're just running against localhost:6776 in dev you can ignore these.

OptionTypeDefaultDescription
domainsToPropagateHeadersTostring[]["*"]Allowlist of domains that SHOULD receive the tracing header. Wildcard syntax: "*", "*.example.com", "api.example.com:8080", "api.example.com/v1/*". Default ["*"] = allow all. Set to [] to disable propagation entirely.
domainsToNotPropagateHeadersTostring[][]Denylist of domains to exclude. Takes precedence over the allowlist.
retryOnClientError'all' | 'idempotent' | 'none''all'How to handle 400/403 responses on requests with our tracing header. 'idempotent' is safest for payment/booking backends. Also settable via SF_RETRY_ON_CLIENT_ERROR.
How the three lists combine
propagate = matches(url, domainsToPropagateHeadersTo)
&& NOT matches(url, domainsToNotPropagateHeadersTo)
&& NOT matches(url, BUILT_IN_DEFAULTS)

Built-in defaults are the well-known analytics / auth / CDN domains (Twitter, Gravatar, Google APIs, AWS, Zendesk, Smooch) that should never receive Sailfish tracing headers regardless of customer config.

Configuration File

For more control over what gets captured, create a .sailfish file in your project root:

{
"capture": {
"console": true,
"exceptions": true,
"functions": true,
"network": true
},
"sampling": {
"rate": 1.0,
"maxEntriesPerSecond": 1000
},
"filters": {
"excludePaths": [
"node_modules",
"dist"
],
"excludeFunctions": [
"internalHelper"
]
}
}

Configuration Options

capture

OptionDefaultDescription
consoletrueCapture console.log/info/warn/error
exceptionstrueCapture unhandled exceptions
functionstrueCapture function execution traces
networktrueCapture outgoing HTTP requests

sampling

OptionDefaultDescription
rate1.0Sampling rate (0.0 to 1.0, where 1.0 = 100%)
maxEntriesPerSecond1000Rate limit for telemetry entries

filters

OptionDefaultDescription
excludePaths[]File paths to exclude from tracing
excludeFunctions[]Function names to exclude from tracing

Framework Examples

Express.js

// server.ts
import express from 'express';

// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'express-api',
serviceVersion: '1.0.0',
});
});
}

const app = express();

app.get('/api/users', (req, res) => {
console.log('Fetching users'); // This will appear in SF Veritas Console
// ...
});

app.listen(3000, () => {
console.log('Server started on port 3000');
});

NestJS

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'nestjs-api',
serviceVersion: '1.0.0',
});
}

const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

Fastify

// app.ts
import Fastify from 'fastify';

// Initialize SF Veritas only in development
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'fastify-api',
serviceVersion: '1.0.0',
});
});
}

const fastify = Fastify({ logger: true });

fastify.get('/', async (request, reply) => {
console.log('Handling request');
return { hello: 'world' };
});

fastify.listen({ port: 3000 });

Koa

// app.ts
import Koa from 'koa';
import Router from '@koa/router';

if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'koa-api',
serviceVersion: '1.0.0',
});
});
}

const app = new Koa();
const router = new Router();

router.get('/', (ctx) => {
ctx.body = { hello: 'world' };
});

app.use(router.routes());
app.listen(3000);

Hapi

// server.ts
import Hapi from '@hapi/hapi';

if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'hapi-api',
serviceVersion: '1.0.0',
});
});
}

const init = async () => {
const server = Hapi.server({ port: 3000, host: 'localhost' });

server.route({
method: 'GET',
path: '/',
handler: () => ({ hello: 'world' }),
});

await server.start();
console.log('Server running on %s', server.info.uri);
};

init();

Hono

// app.ts
import { Hono } from 'hono';
import { serve } from '@hono/node-server';

if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'hono-api',
serviceVersion: '1.0.0',
});
});
}

const app = new Hono();

app.get('/', (c) => c.json({ hello: 'world' }));

serve({ fetch: app.fetch, port: 3000 });

Apollo Server (GraphQL)

// index.ts
import { setupInterceptors } from '@sailfish-ai/sf-veritas';

if (process.env.NODE_ENV === 'development') {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'graphql-api',
serviceVersion: '1.0.0',
});
}

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const server = new ApolloServer({
typeDefs: `type Query { hello: String }`,
resolvers: { Query: { hello: () => 'Hello, world!' } },
});

startStandaloneServer(server, { listen: { port: 4000 } });

Mercurius (Fastify GraphQL)

// app.ts
import fastify from 'fastify';
import mercurius from 'mercurius';

if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'graphql-api',
serviceVersion: '1.0.0',
});
});
}

const app = fastify();

const schema = `
type Query {
hello: String
}
`;

const resolvers = {
Query: {
hello: () => 'Hello from Mercurius!',
},
};

app.register(mercurius, { schema, resolvers, graphiql: true });

app.listen({ port: 3000 });

Nuxt.js

Create a server plugin to initialize Sailfish in development:

// server/plugins/sailfish.ts
export default defineNitroPlugin(async () => {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');

setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'nuxt-app',
serviceVersion: '1.0.0',
});
}
});

MeteorJS

// server/instrumentation.js
export async function register() {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');

setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'meteor-app',
serviceVersion: '1.0.0',
});
}
}
// server/main.js
import { register } from './instrumentation';

register().catch((err) => {
console.error('[Instrumentation] Failed to initialize:', err);
});

// ... your Meteor server code
info

Install via meteor npm install @sailfish-ai/sf-veritas. Meteor uses its own package manager, but npm packages work through meteor npm.

Next.js

Next.js has a built-in Instrumentation hook that runs before the application starts. Create an instrumentation.ts file in your project root (next to next.config.ts):

// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');

setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'my-nextjs-app',
serviceVersion: '1.0.0',
});
}
}
}
Why instrumentation.ts?

Next.js calls the register() function once when a new Next.js server instance is started. The NEXT_RUNTIME check ensures the interceptors only run in the Node.js runtime (not Edge Runtime). This is the recommended way to initialize server-side telemetry in Next.js.

Function Span Profiling (Webpack Plugin)

To capture function execution traces (flamechart), add the FuncspanWebpackPlugin to your next.config.ts:

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
webpack: (config, { isServer, nextRuntime }) => {
if (isServer && nextRuntime === 'nodejs') {
const { FuncspanWebpackPlugin } = require(
'@sailfish-ai/sf-veritas/funcspan-webpack'
);

config.plugins.push(
new FuncspanWebpackPlugin({
enabled: true,
includeNodeModules: [], // Only instrument your application code
})
);

config.devtool = 'source-map';
}
return config;
},
};

export default nextConfig;

Environment Variables

Add a .env.local file for local development:

# .env.local
SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/

Then reference it in your instrumentation.ts:

// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');

setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint:
process.env.SAILFISH_GRAPHQL_ENDPOINT ||
'http://localhost:6776/graphql/',
serviceIdentifier: 'my-nextjs-app',
serviceVersion: '1.0.0',
});
}
}
}
Full-stack Next.js

If your Next.js app has a frontend UI, you can also add the frontend recorder to capture browser-side telemetry (console logs, network requests, errors). The two work independently — instrumentation.ts captures server-side activity, while the frontend recorder captures client-side activity.

Async Queue Tracing

Sailfish automatically stitches producer → broker → consumer into a single trace when your code uses an instrumented async-queue library. Background jobs and Kafka events show up in the same trace as the HTTP request that enqueued them.

HTTP POST /enqueue ──►  queue.add() / producer.send() ──►  broker  ──►  worker.processJob / consumer.run
(inbound hop) (ENQUEUE hop) (CONSUME hop)
│─────────────── shared session + pageVisit ───────────────│

Supported libraries (zero-config)

LibraryProducerConsumer
BullMQQueue.add, Queue.addBulk, FlowProducer.addWorker.processJob
kafkajsproducer.send, producer.sendBatch, producer.transaction().sendconsumer.run({ eachMessage / eachBatch })
@confluentinc/kafka-javascriptboth KafkaJS-compat and node-rdkafka modesboth modes

Each works automatically — just import the library and the SDK's patches wire up on setupInterceptors().

Example

if (process.env.NODE_ENV === 'development') {
const { setupInterceptors } = require('@sailfish-ai/sf-veritas');
setupInterceptors({
apiKey: 'sf-veritas-local',
serviceIdentifier: 'order-service',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
});
}

import { Queue } from 'bullmq';
const queue = new Queue('orders', { connection: { host: 'redis' } });

// In your HTTP handler:
await queue.add('send-receipt', { orderId });

Open the Desktop App Console — you'll see one linked trace: the HTTP request → an ENQUEUE hop for send-receipt → a CONSUME hop (from the worker process) → any downstream HTTP calls the job makes.

Advanced BullMQ + Kafka scenarios

  • FlowProducer (BullMQ parent/child trees) — every node (parent and every descendant) gets its own rotated requestId while sharing session + pageVisit.
  • Delayed / repeating jobs ({ delay, repeat: { every, limit } } or cron patterns) — your options are preserved; each dispatched instance gets a distinct requestId.
  • Kafka transactional producers (producer.transaction()) — hops are emitted synchronously at send() with method: "ENQUEUE_TX" so the UI can visually distinguish them.

Runtime controls

Set SF_DISABLE_QUEUE_PATCHES=true to skip every queue-library patch without disabling the rest of the SDK.

On startup, Sailfish logs one info line listing which libraries were patched, skipped (not installed), and failed — useful for confirming the integration.

Caveats

  • BullMQ sandboxed processors (new Worker(queueName, '/path/to/processor.js')) spawn a child Node process. Sailfish still emits the inbound CONSUME hop from the parent process, but outbound HTTP / nested queue.add calls INSIDE the sandboxed file will only be traced if that file ALSO calls setupInterceptors().
  • Kafka transactions emit hops on send(), not on commit(). An aborted transaction still leaves its hop in the timeline.

Verifying the Setup

  1. Start your application
  2. In VS Code, open the Command Palette (Ctrl+Shift+P / Cmd+Shift+P)
  3. Run SF Veritas: Show Console Logs
  4. Trigger some activity in your application
  5. You should see logs appearing in the Console panel

Performance

The SDK uses a Worker thread to send telemetry completely off the main event loop. All HTTP I/O (log collection, exception reporting, network hop tracking) happens in a background thread, resulting in near-zero overhead on your application's request handling.

EnvironmentTelemetry ModeOverhead
Standard Node.js (12+)Worker thread (batched)< 1%
Edge runtimes (see below)Direct fetch (main thread)~5-20% depending on log volume

Build Plugin: Zero-Cost Log Source Location

When you use a Sailfish build plugin (Webpack, Vite, Rollup, esbuild, or TSC), the SDK automatically injects source file and line number into every console.log/error/warn/info/debug call at build time — eliminating all runtime stack capture overhead (0µs per log call).

The build plugin appends a hidden sentinel to each console call that the SDK detects and strips before output. Your logs look exactly the same.

Setup per bundler:

Vite
// vite.config.ts
import { funcspanVitePlugin } from '@sailfish-ai/sf-veritas/plugins/funcspanVitePlugin';

export default defineConfig({
plugins: [
funcspanVitePlugin({ debug: false })
]
});
Webpack
// webpack.config.js
const { FuncspanWebpackPlugin } = require('@sailfish-ai/sf-veritas/plugins/funcspanWebpackPlugin');

module.exports = {
plugins: [
new FuncspanWebpackPlugin({ debug: false })
]
};
TypeScript (tsc)
{
"scripts": {
"build": "tsc && node -e \"require('@sailfish-ai/sf-veritas/plugins/funcspanTscPlugin').cli()\""
}
}
tip

The build plugin also enables automatic function span instrumentation (Flamechart). If you're already using the Sailfish build plugin, console location injection is enabled automatically.

Environment Variables

VariableDefaultDescription
SF_LOG_LOCATIONautoSource location mode: auto (build-inject if available, else V8), v8 (runtime stack capture), off (disabled)
SF_FLUSH_TELEMETRY_IN_BATCH0Set to 1 to send telemetry as batched JSON arrays (fewer HTTP connections). Set to 0 for individual POSTs (default)
SF_BATCH_FLUSH_INTERVAL_MS50How often the Worker thread flushes queued telemetry (ms). Lower = less latency, higher = fewer HTTP calls
SF_NBPOST_DISABLE_BATCHING0Set to 1 to disable Worker thread entirely (edge runtime fallback)
SF_NBPOST_BATCH_MAX50Maximum items queued before forced flush
SF_MAIN_BATCH_SIZE0Main-thread batch size: 0 (microtask batch), 1 (per-item)
SF_WORKER_MAX_CONCURRENT10Max concurrent HTTP requests from Worker to backend. Prevents overload at high volume

Edge Runtime Limitations

On platforms that do not support Node.js worker_threads, the SDK automatically falls back to sending telemetry via direct fetch() on the main thread. This is detected at runtime — no configuration needed. The fallback works correctly but has higher overhead under heavy logging workloads.

Platforms without worker_threads support:

PlatformRuntimeNotes
Cloudflare WorkersV8 isolatesLightweight edge compute at CDN PoPs; no Node.js APIs
Vercel Edge FunctionsV8 (Edge Runtime)Runs at Vercel's edge network; no worker_threads
Netlify Edge FunctionsDenoDeno-based edge runtime at CDN PoPs
Deno DeployDenoGlobal serverless Deno; has Web Workers but not Node.js worker_threads
AWS Lambda@EdgeRestricted Node.jsCloudFront-attached Lambda; worker_threads disabled (standard Lambda works fine)
Fastly ComputeWASMWebAssembly-based edge compute; no Node.js
What are edge runtimes?

Edge runtimes are lightweight, globally-distributed compute environments that run at CDN Points of Presence (PoPs) close to end users. They execute small functions (typically < 50ms CPU time) for tasks like request routing, authentication, A/B testing, and response transformation. They trade Node.js API completeness for extremely low latency and global distribution. Full application backends typically run on standard Node.js — not edge runtimes.

Platforms where worker_threads works normally:

  • Node.js 12+ (all current LTS versions: 18, 20, 22)
  • AWS Lambda (standard), Google Cloud Functions, Azure Functions
  • Docker, Kubernetes, ECS, EKS, GKE, AKS
  • Any standard Node.js hosting (Railway, Render, Heroku, DigitalOcean App Platform, Fly.io)

Troubleshooting

No logs appearing

  1. Check the local server: Run SF Veritas: Start Local Server if not running
  2. Verify the endpoint: Ensure apiGraphqlEndpoint matches your server port
  3. Check console output: Look for SF Veritas initialization messages in your terminal

Connection refused errors

  1. Verify the SF Veritas Desktop App is installed and running
  2. Check that the local server is running (look for server status in the Desktop App)
  3. Ensure port 6776 is not blocked by a firewall

High memory usage

  1. Reduce the sampling rate in .sailfish config
  2. Add paths to excludePaths to reduce trace volume
  3. Lower maxEntriesPerSecond to rate limit telemetry

Multi-Service Setup

When running multiple services locally, give each a unique serviceIdentifier:

// user-service/server.ts
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'user-service',
serviceVersion: '1.0.0',
});
});
}

// order-service/server.ts
if (process.env.NODE_ENV === 'development') {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors({
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'order-service',
serviceVersion: '1.0.0',
});
});
}

Use the service filter in the Console to switch between services.

How It Works

The NODE_ENV environment variable controls when SF Veritas is active:

CommandNODE_ENVSF Veritas
npm run devdevelopment✅ Active
npm run start (production)production❌ Not loaded
npm run buildproduction❌ Not included

Most Node.js frameworks automatically set NODE_ENV=development when you run npm run dev:

  • Express: Set via cross-env or your dev script
  • NestJS: Set automatically by nest start --watch
  • Fastify: Set via cross-env or your dev script

If your framework doesn't set it automatically, add it to your package.json:

{
"scripts": {
"dev": "NODE_ENV=development ts-node src/server.ts",
"start": "NODE_ENV=production node dist/server.js"
}
}

Enterprise Setup

Enterprise Only

This section applies to users with a Sailfish Enterprise account. If you're using the Desktop App for local development only, the basic setup above is all you need.

Enterprise users need two configurations — one for local development (Desktop App) and one for staging/production (Sailfish cloud). The SDK should detect which environment it's running in and configure itself accordingly.

Dual-Mode Configuration

// instrumentation.ts or at your app's entry point
import type { SetupConfig } from '@sailfish-ai/sf-veritas';

function getSailfishConfig(): SetupConfig | null {
if (process.env.NODE_ENV === 'development') {
// Local development — send to Desktop App
return {
apiKey: 'sf-veritas-local',
apiGraphqlEndpoint: 'http://localhost:6776/graphql/',
serviceIdentifier: 'my-backend-service',
serviceVersion: '1.0.0',
};
}

if (process.env.SAILFISH_API_KEY) {
// Staging/Production — send to Sailfish cloud
// Do NOT set apiGraphqlEndpoint — the SDK defaults to the cloud endpoint
return {
apiKey: process.env.SAILFISH_API_KEY,
serviceIdentifier: 'my-backend-service',
serviceVersion: '1.0.0',
};
}

return null; // SF Veritas disabled
}

const config = getSailfishConfig();
if (config) {
import('@sailfish-ai/sf-veritas').then(({ setupInterceptors }) => {
setupInterceptors(config);
});
}

Environment Variables for Enterprise

Set these in your staging/production deployment:

# Required — your company's Enterprise API key
SAILFISH_API_KEY=your-enterprise-api-key

# Required — the git commit SHA for this build
GIT_SHA=$(git rev-parse HEAD)

# Optional — override the endpoint (only for local dev, do NOT set in production)
# SAILFISH_GRAPHQL_ENDPOINT=http://localhost:6776/graphql/

GIT_SHA

The GIT_SHA environment variable allows Sailfish to correlate telemetry with specific commits. The SDK reads it automatically from process.env.GIT_SHA (with a fallback to VERCEL_GIT_COMMIT_SHA on Vercel).

Set it in your build pipeline:

# Docker
docker build --build-arg GIT_SHA=$(git rev-parse HEAD) .

# In Dockerfile
ARG GIT_SHA
ENV GIT_SHA=$GIT_SHA

# CI/CD (GitHub Actions)
env:
GIT_SHA: ${{ github.sha }}

URL Override

You can override the GraphQL endpoint in two ways:

  1. Environment variable: SAILFISH_GRAPHQL_ENDPOINT
  2. Constructor argument: apiGraphqlEndpoint in setupInterceptors()
EnvironmentEndpointHow
Local devhttp://localhost:6776/graphql/Set apiGraphqlEndpoint or SAILFISH_GRAPHQL_ENDPOINT
Staging/ProductionDefault (cloud)Do not set either — the SDK uses https://api-service.sailfishqa.com/graphql/ automatically

Next Steps