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.
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
- Open the Sailfish dashboard
- Log in with your enterprise email
- Navigate to Settings > Configuration
- 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
| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your Sailfish Enterprise API key |
serviceIdentifier | string | Yes | Unique identifier in <org>/<repo>/<path> format |
serviceVersion | string | No | Version of your service |
gitSha | string | No | Git commit SHA; auto-detected from common CI env vars |
debug | boolean | No | Enable verbose debug logging |
serviceAdditionalMetadata | Record<string, any> | No | Custom 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.
| Option | Type | Default | Description |
|---|---|---|---|
domainsToPropagateHeadersTo | string[] | ["*"] | 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. |
domainsToNotPropagateHeadersTo | string[] | [] | 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. |
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.
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
| Option | Default | Description |
|---|---|---|
console | true | Capture console.log/info/warn/error |
exceptions | true | Capture unhandled exceptions |
functions | true | Capture function execution traces |
network | true | Capture outgoing HTTP requests |
sampling
| Option | Default | Description |
|---|---|---|
rate | 1.0 | Sampling rate (0.0 to 1.0, where 1.0 = 100%) |
maxEntriesPerSecond | 1000 | Rate limit for telemetry entries |
filters
| Option | Default | Description |
|---|---|---|
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 });
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',
});
});
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 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',
});
}
}
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;
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)
| Library | Producer | Consumer |
|---|---|---|
| BullMQ | Queue.add, Queue.addBulk, FlowProducer.add | Worker.processJob |
| kafkajs | producer.send, producer.sendBatch, producer.transaction().send | consumer.run({ eachMessage / eachBatch }) |
| @confluentinc/kafka-javascript | both KafkaJS-compat and node-rdkafka modes | both 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 atsend()withmethod: "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 / nestedqueue.addcalls INSIDE the sandboxed file will only be traced if that file ALSO callssetupInterceptors(). - Kafka transactions emit hops on
send(), not oncommit(). 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 Platform | Environment Variable |
|---|---|
| GitHub Actions | GITHUB_SHA |
| GitLab CI | CI_COMMIT_SHA |
| CircleCI | CIRCLE_SHA1 |
| Jenkins | GIT_COMMIT |
| Bitbucket Pipelines | BITBUCKET_COMMIT |
| Travis CI | TRAVIS_COMMIT |
| Azure DevOps | BUILD_SOURCEVERSION |
| AWS CodeBuild | CODEBUILD_RESOLVED_SOURCE_VERSION |
| Vercel | VERCEL_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
- Deploy your application with the Sailfish SDK configured
- Trigger some activity in your application
- Open the Sailfish dashboard
- 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.
| Environment | Telemetry Mode | Overhead |
|---|---|---|
| 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()\""
}
}
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
| Variable | Default | Description |
|---|---|---|
SF_LOG_LOCATION | auto | Source location capture mode: auto (build-inject if available, else V8), v8 (runtime stack capture), off (disabled) |
SF_FLUSH_TELEMETRY_IN_BATCH | 0 | Set 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_MS | 50 | How often the Worker thread flushes queued telemetry to the backend (milliseconds). Lower = less latency, higher = fewer HTTP calls |
SF_NBPOST_DISABLE_BATCHING | 0 | Set to 1 to disable Worker thread entirely and use direct main-thread fetch (edge runtime fallback) |
SF_NBPOST_BATCH_MAX | 50 | Maximum items queued before forced flush (regardless of timer) |
SF_MAIN_BATCH_SIZE | 0 | Main-thread batch size before posting to worker: 0 (microtask batch), 1 (per-item) |
SF_WORKER_MAX_CONCURRENT | 10 | Maximum 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:
| Platform | Runtime | Notes |
|---|---|---|
| Cloudflare Workers | V8 isolates | Lightweight edge compute at CDN PoPs; no Node.js APIs |
| Vercel Edge Functions | V8 (Edge Runtime) | Runs at Vercel's edge network; no worker_threads |
| Netlify Edge Functions | Deno | Deno-based edge runtime at CDN PoPs |
| Deno Deploy | Deno | Global serverless Deno; has Web Workers but not Node.js worker_threads |
| AWS Lambda@Edge | Restricted Node.js | CloudFront-attached Lambda; worker_threads disabled (standard Lambda works fine) |
| Fastly Compute | WASM | WebAssembly-based edge compute; no Node.js |
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
- Check the API key: Ensure
apiKeyis set to your Enterprise API key - Check the service identifier: Ensure
serviceIdentifieruses the<org>/<repo>/<path>format - Check console output: Look for SF Veritas initialization messages in your terminal
Connection errors
- Ensure your deployment can reach
https://api-service.sailfish.ai - Check that outbound HTTPS (port 443) is not blocked by a firewall or network policy
High memory usage
- Reduce the sampling rate in
.sailfishconfig - Add paths to
excludePathsto reduce trace volume - Lower
maxEntriesPerSecondto 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
- Set up your frontend application (optional)
- Check the Sailfish dashboard to verify telemetry is flowing
Looking to set up SF Veritas for local development with the Desktop App? See the Desktop App JS/TS guide.