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 for Sailfish Enterprise.

Auto-Installation

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

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

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

setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-api/src/index.ts', // 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.
domainsToPropagateHeadersTo: ["*"],
});

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

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

Configuration Options

OptionTypeRequiredDescription
apiKeystringYesYour Sailfish Enterprise API key
serviceIdentifierstringYesUnique identifier in <org>/<repo>/<path> format
serviceVersionstringNoVersion of your service
gitShastringNoGit commit SHA; auto-detected from common CI env vars
debugbooleanNoEnable verbose debug logging
serviceAdditionalMetadataRecord<string, any>NoCustom metadata to attach to every telemetry event

Header-Propagation Options

Control which outbound domains receive Sailfish's distributed-tracing header (X-Sf3-Rid). The allowlist and denylist work together; the denylist wins on conflict.

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 header propagation entirely.
domainsToNotPropagateHeadersTostring[][]Denylist of domains to exclude. Takes precedence over the allowlist. Good for suppressing headers to third parties that reject unknown request headers (e.g. some payment gateways, search APIs).
retryOnClientError'all' | 'idempotent' | 'none''all'How to handle 400/403 responses on requests carrying our tracing header. 'all' retries every method without the header. 'idempotent' restricts retry to GET/HEAD/OPTIONS — safer for payment/booking backends that may commit side effects before returning 4xx. 'none' disables retry. Also settable via SF_RETRY_ON_CLIENT_ERROR env var.
Auto-Installation populates this for you

If you onboarded via Auto-Installation PRs, domainsToPropagateHeadersTo is pre-populated with wildcard entries for every service we detected in your connected repositories (for example "*.acme.com", "api.acme.com/*"). You can extend or override this in code at any time. The Sailfish cloud ingest also ignores headers from any domain outside your registered service list, so the worst-case of a too-permissive allowlist is additional unnecessary headers — not a data-integrity issue.

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.

Example — combining the three lists:

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

setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/web-api/src/server.ts',
// Allow-list: only our services get the header (Auto-Installation
// will populate this automatically for discovered services).
domainsToPropagateHeadersTo: [
'*.acme.com',
'api.internal.acme.io/v1/*',
'payments-service.acme.com:8443',
],
// Deny-list: even if allow-list matches, suppress these explicitly.
domainsToNotPropagateHeadersTo: [
'legacy-api.acme.com', // old service doesn't accept unknown headers
],
// If an allowed domain 400s on our header, opt into the safer retry mode:
retryOnClientError: 'idempotent',
});

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 { setupInterceptors } from '@sailfish-ai/sf-veritas';

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

import express from 'express';

const app = express();

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

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

NestJS

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

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

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

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

Fastify

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

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

import Fastify from 'fastify';

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 { setupInterceptors } from '@sailfish-ai/sf-veritas';

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

import Koa from 'koa';
import Router from '@koa/router';

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 { setupInterceptors } from '@sailfish-ai/sf-veritas';

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

import Hapi from '@hapi/hapi';

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 { setupInterceptors } from '@sailfish-ai/sf-veritas';

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

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

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';

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

Mercurius is a GraphQL adapter for Fastify. Initialize SF Veritas before importing Fastify and Mercurius:

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

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

import fastify from 'fastify';
import mercurius from 'mercurius';

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 });
tip

Sailfish auto-detects Mercurius as the primary framework and Fastify as an additional framework. Both appear in your service's framework metadata.

Nuxt.js

Nuxt.js uses Nitro as its server engine. Create a server plugin to initialize Sailfish:

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

setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/nuxt-app/server/plugins/sailfish.ts', // Format: <org>/<repo>/<path-to-this-file>
serviceVersion: '1.0.0',
});
});
Why a server plugin?

Nuxt server plugins run once when the Nitro server starts, before any requests are handled. This is the recommended way to initialize server-side telemetry in Nuxt. The SDK automatically detects that it's running inside a Nuxt/Nitro environment.

MeteorJS

Meteor uses its own build system and module loader. Create an instrumentation file and import it from your server entry point:

// server/instrumentation.js
import { Meteor } from 'meteor/meteor';

export async function register() {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');

setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/meteor-app/server/instrumentation.js', // Format: <org>/<repo>/<path-to-this-file>
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
Meteor package system

Meteor uses its own package manager rather than npm's node_modules. Install @sailfish-ai/sf-veritas via meteor npm install @sailfish-ai/sf-veritas. The SDK automatically detects the Meteor runtime environment.

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') {
const { setupInterceptors } = await import('@sailfish-ai/sf-veritas');

setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/my-nextjs-app/instrumentation.ts', // Format: <org>/<repo>/<path-to-this-file>
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;
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

import { setupInterceptors } from '@sailfish-ai/sf-veritas';
setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/order-service/src/main.ts',
serviceVersion: '1.0.0',
});

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

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

In the Sailfish dashboard you will 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.

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).

The SDK auto-detects GIT_SHA from common CI/CD platforms:

CI PlatformEnvironment Variable
GitHub ActionsGITHUB_SHA
GitLab CICI_COMMIT_SHA
CircleCICIRCLE_SHA1
JenkinsGIT_COMMIT
Bitbucket PipelinesBITBUCKET_COMMIT
Travis CITRAVIS_COMMIT
Azure DevOpsBUILD_SOURCEVERSION
AWS CodeBuildCODEBUILD_RESOLVED_SOURCE_VERSION
VercelVERCEL_GIT_COMMIT_SHA

For custom CI systems, set it manually:

# 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 }}

Verifying the Setup

  1. Deploy your application with the Sailfish SDK configured
  2. Trigger some activity in your application
  3. Open the Sailfish dashboard
  4. You should see telemetry appearing in your project

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, console.error, console.warn, console.info, and console.debug call at build time. This eliminates all runtime stack capture overhead (0µs per log call instead of ~1µs).

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 — but Sailfish knows the exact source location without any runtime cost.

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 })
]
};
Rollup
// rollup.config.js
import { funcspanRollupPlugin } from '@sailfish-ai/sf-veritas/plugins/funcspanRollupPlugin';

export default {
plugins: [
funcspanRollupPlugin({ debug: false })
]
};
esbuild
// esbuild.config.js
const { funcspanEsbuildPlugin } = require('@sailfish-ai/sf-veritas/plugins/funcspanEsbuildPlugin');

require('esbuild').build({
plugins: [
funcspanEsbuildPlugin({ debug: false })
]
});
TypeScript (tsc)
// package.json
{
"scripts": {
"build": "tsc && node -e \"require('@sailfish-ai/sf-veritas/plugins/funcspanTscPlugin').cli()\""
}
}
tip

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

Environment Variables

VariableDefaultDescription
SF_LOG_LOCATIONautoSource location capture 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, compatible with all backends)
SF_BATCH_FLUSH_INTERVAL_MS50How often the Worker thread flushes queued telemetry to the backend (milliseconds). Lower = less latency, higher = fewer HTTP calls
SF_NBPOST_DISABLE_BATCHING0Set to 1 to disable Worker thread entirely and use direct main-thread fetch (edge runtime fallback)
SF_NBPOST_BATCH_MAX50Maximum items queued before forced flush (regardless of timer)
SF_MAIN_BATCH_SIZE0Main-thread batch size before posting to worker: 0 (microtask batch), 1 (per-item)
SF_WORKER_MAX_CONCURRENT10Maximum concurrent HTTP requests from the Worker thread to the backend. Prevents overwhelming the backend at high telemetry volume. Increase for high-throughput backends, decrease for rate-limited endpoints

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 API key: Ensure apiKey is set to your Enterprise API key
  2. Check the service identifier: Ensure serviceIdentifier uses the <org>/<repo>/<path> format
  3. Check console output: Look for SF Veritas initialization messages in your terminal

Connection errors

  1. Ensure your deployment can reach https://api-service.sailfish.ai
  2. Check that outbound HTTPS (port 443) is not blocked by a firewall or network policy

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, give each a unique serviceIdentifier following the <org>/<repo>/<path> format:

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

setupInterceptors({
apiKey: "<see-api-key-from-your-account-settings-page>",
serviceIdentifier: 'acme-corp/user-service/src/server.ts', // Format: <org>/<repo>/<path-to-this-file>
serviceVersion: '1.0.0',
});

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

setupInterceptors({
apiKey: "<ApiKey />",
serviceIdentifier: 'acme-corp/order-service/src/server.ts', // Format: <org>/<repo>/<path-to-this-file>
serviceVersion: '1.0.0',
});

Use the service filter in the Sailfish dashboard to switch between services.

Next Steps


Local Development

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