SDK Documentation
Introduction
VantaTrace is a real-time error tracking, telemetry, and observability SDK for Node.js applications and microservices. It captures exceptions, runtime metrics, and request context, then streams them to your centralized VantaTrace dashboard.
Zero Blocking
All payloads are dispatched asynchronously via setImmediate — no request latency added.
Smart Batching
Events are batched and flushed every 500 ms, or immediately for critical severity.
Fail-Safe
SDK errors are caught internally — it will never crash your host application.
Auto Deduplication
Duplicate error objects are filtered via WeakSet to prevent double-reporting.
Installation
Install the SDK from npm. It works with any Node.js framework, and nothing it pulls in runs on your request path — its single dependency is a Babel helper used only by the optional build-time plugin.
npm install @vantatrace/sdk@types/ package needed.Initialization
Create a single VantaTrace instance in your main entry file (e.g. app.js or server.ts) and export it for use across your service.
import { VantaTrace } from '@vantatrace/sdk';
const vantaTrace = new VantaTrace({
apiKey: 'ep_live_xxxxxxxxxxxxxxxxxxxxxxxx', // Your project API key
debug: false, // Enable verbose SDK logs in development
// ── try/catch capture ────────────────────────────────────────────────
// An error you handle yourself never reaches errorHandler() or the
// global handlers, so it needs its own capture path. Details in the
// "try/catch Capture" section below.
autoCapture: {
http5xx: true, // ON by default — a 5xx answer with no error reported
httpClientErrors: true, // ON by default — same for 4xx (401 + 404 excluded)
// Opt-in: recovers the REAL Error object from inside your catch blocks
// using the V8 inspector. Auto-disabled when NODE_ENV=production —
// use the Babel plugin there instead (zero runtime cost).
caughtExceptions: process.env.NODE_ENV !== 'production',
},
});Configuration Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| apiKey | string | Yes | — | Your project API key from the VantaTrace dashboard. Prefix determines environment (ep_live_ → live, ep_test_ → test). With no key the SDK runs in dry-run mode and discards every event, logging a startup warning regardless of debug. |
| debug | boolean | No | false | When true, the SDK prints verbose logs to stdout (trace IDs, payloads, transport status). |
| autoCapture | AutoCaptureOptions | No | — | Capture of errors that never escape your code — http5xx and httpClientErrors (both on by default) plus the opt-in caughtExceptions inspector watcher. See try/catch Capture below. |
| apiUrl | string | No | https://api.vantatrace.com/api/events | Override the ingestion endpoint. Useful for on-premise or staging deployments. |
| maskingKeys | string[] | No | [] | Extra field names (exact match, case-insensitive) to redact from log metadata — domain-specific PII the generic password/token/secret matching would not catch. Merged with the built-in list. |
| rateLimit | RateLimitOptions | No | — | Volume ceilings on captured events: maxPerMinute (default 1000), maxPerFingerprintPerMinute (default 150) and sampleRate (default 1). Protects your app and the pipeline during an event storm; getDropStats() reports what was dropped. |
Environments & API Keys
VantaTrace derives the environment directly from your API key — no manual environment config needed:
ep_live_…liveep_test_…testGlobal Handlers
Call initGlobalHandlers() once after initialization to register a safety net that automatically captures:
- Synchronous uncaught exceptions — process.on('uncaughtException') — runtime crashes, typos, undefined refs.
- Unhandled promise rejections — process.on('unhandledRejection') — re-thrown errors inside async catch blocks.
- console.error() calls — Auto-patched — any Error passed to console.error is forwarded.
- Winston logger errors — Auto-patched at runtime if the winston package is installed.
- Pino logger errors — Auto-patched at runtime if the pino package is installed.
// Call once, right after initialization
vantaTrace.initGlobalHandlers();
// This registers:
// 1. process.on('uncaughtException', ...) – synchronous crashes
// 2. process.on('unhandledRejection', ...) – async/await rejections
// 3. console.error patch – errors via console.error
// 4. Winston auto-patch (if installed)
// 5. Pino auto-patch (if installed)Express Middleware
VantaTrace provides two Express middlewares that work together to capture full HTTP request context alongside every error.
requestHandler()
Mount this at the very top of your middleware stack, before any routes or body parsers. It seeds an AsyncLocalStorage context with request metadata (route, method, IP, sanitized headers, sanitized body).
errorHandler()
Mount this at the very bottom of your middleware stack, after all routes and controllers. It intercepts any error passed to next(err) and forwards it to VantaTrace while calling next(err) so your own error handler still runs.
import express from 'express';
import { VantaTrace } from '@vantatrace/sdk';
const app = express();
const vantaTrace = new VantaTrace({
apiKey: process.env.VANTATRACE_API_KEY!,
});
// ① Mount requestHandler FIRST — before any routes or body parsers
app.use(vantaTrace.requestHandler());
app.use(express.json());
// ... your routes ...
app.get('/api/orders', async (req, res) => {
const orders = await fetchOrders(); // any thrown error is auto-captured
res.json(orders);
});
// ② Mount errorHandler LAST — after all routes and controllers
app.use(vantaTrace.errorHandler());
// Your own error response handler (still called via next(err))
app.use((err, req, res, next) => {
res.status(500).json({ message: 'Internal Server Error' });
});
app.listen(3000);try/catch Capture
errorHandler() and the global handlers only see errors that escape your code. An error you catch yourself is, as far as Express and process are concerned, handled — so it reaches neither:
app.get('/api/checkout', async (req, res) => {
try {
await chargeCard(req.body);
} catch (error) {
// Nothing escapes this block, so errorHandler() and the global
// handlers never see it. Without try/catch capture, this failure is
// invisible everywhere except your own logs.
res.status(500).json({ error: 'Payment failed' });
}
});JavaScript has no language-level hook for a caught exception — try/catch is resolved entirely inside the V8 VM, which is why most error trackers require a manual capture call in every catch block. VantaTrace covers it in three layers instead, and the one you want depends on where the code runs:
const isProd = process.env.NODE_ENV === 'production';
const vantaTrace = new VantaTrace({
apiKey: process.env.VANTATRACE_API_KEY,
autoCapture: {
// ① Failed-request safety net — already ON, no config needed.
// The route above is reported as a synthetic HttpServerError with
// the method, route, status and the response body your catch block
// sent back. Pass false to turn it off.
http5xx: true,
// ② The same for 4xx answers your code returns on purpose
// (res.status(422).json(...)), at 'warning' severity.
// 401 and 404 are excluded by default as routine traffic.
httpClientErrors: true,
// ③ Deep capture — the real Error object, not a synthetic one.
// Auto-disabled under NODE_ENV=production; use the Babel plugin there.
caughtExceptions: !isProd,
},
});
// Optional: detach the inspector session on graceful shutdown.
process.on('SIGTERM', () => vantaTrace.shutdown());Failed-Request Safety Net
Enabled by default — nothing to configure. When a request finishes 5xx and no error was reported for it, the SDK emits a synthetic HttpServerError (severity critical) carrying the method, route, status code and full request context. The response body your catch block actually sent is attached as metadata.responseBody — usually the most concrete clue available when the Error object itself was swallowed.
4xx answers are covered too, as HttpClientError at warning severity — a res.status(422).json(...) with no throw is exactly the kind of handled-and-invisible failure this exists for. 401 and 404 are excluded by default; pass { exclude: [404] } to change the list, or false to disable 4xx capture entirely.
Deep Capture via the V8 Inspector (opt-in)
With pause-on-exceptions enabled, V8 notifies an in-process node:inspector session at every throw site, before the catch block runs. VantaTrace uses that to recover the real error object — including engine-generated ReferenceError and TypeError — with its full stack and the active request context, without touching a single catch block.
autoCapture: {
caughtExceptions: {
// 'request-failure' (default): buffer caught exceptions and report them
// only if the request ends >= 500 — zero noise from errors your code
// genuinely recovered from.
// 'always': report every caught exception immediately, severity 'warning'.
// Use this outside HTTP (queue consumers, cron jobs).
report: 'request-failure',
// Libraries throw and catch internally as control flow, which is noise.
includeNodeModules: false,
// Recording ceiling, protects throw-heavy hot loops.
maxPerMinute: 120,
// Required to run the inspector watcher under NODE_ENV=production.
// Without it the watcher is skipped and a warning is logged.
allowInProduction: false,
},
}Duplicates are suppressed: an error that is caught, rethrown and then reaches errorHandler() (or a manual captureException) is reported exactly once.
NODE_ENV=production the SDK skips it and logs a warning pointing at the Babel plugin. Set allowInProduction: true to run it there anyway.Babel Plugin — the production path
The plugin injects the capture call into every catch block at build time, so production gets the same real-error capture with no inspector attached and no runtime cost at all.
// .babelrc — or babel.config.js
{
"presets": ["@babel/preset-env"],
"plugins": ["@vantatrace/sdk/babel-plugin"]
}// What you write:
try {
doSomething();
} catch (error) {
res.status(500).json({ error: 'Failed' });
}
// What the plugin compiles it to — no inspector, no runtime cost:
import { captureExceptionGlobal } from '@vantatrace/sdk/runtime';
try {
doSomething();
} catch (error) {
captureExceptionGlobal(error);
res.status(500).json({ error: 'Failed' });
}
// Opt a block out with a comment:
try {
parseOptionalConfig();
} catch (error) { // vantatrace-ignore
// expected control flow — not a defect worth reporting
}It skips a block when the catch has no binding (catch { }), when the binding is destructured, when you already call captureException there yourself, or when a // vantatrace-ignore comment marks it.
@babel/preset-env, or sourceType: "unambiguous"). Without one, a plain CommonJS file gets an ESM import injected and fails at runtime with Cannot use import statement outside a module. Next.js picks up a .babelrc in the project root automatically and switches that project off SWC.Security & Redaction
PII and secrets are redacted recursively, everywhere the SDK captures data — not just top-level fields or standard headers.
Default Redaction
- Headers — authorization, cookie, set-cookie, x-api-key, proxy-authorization.
- Request bodies & query params — Any nested field named like password, passwd, pwd, secret, token, apiKey, accessKey, privateKey, authorization, credential, cookie, session, ssn, creditCard/cardNumber, cvv/cvc or otp — matched anywhere in the name, case-insensitive, at any depth, including inside arrays.
- Short names, matched as whole words — pin, mpin, pincode, pass, auth, key, mfa and 2fa are matched per key segment (split on dashes, underscores and camelCase humps) so X-MPIN and shippingPin are redacted while shipping, passenger and author are left intact.
- Secrets inside free text — A credential interpolated into an error message, a stack frame or a raw URL is matched by shape — Bearer tokens, JWTs, key=value pairs, SSNs, card-shaped digit runs, email addresses — by the ingestion worker rather than in your process, so it is redacted before storage but does reach the ingestion endpoint. Avoid putting credentials in error messages you throw.
requestHandler(), before anything leaves your process. The ingestion worker then re-scrubs every event independently — key names and secret-shaped strings — before persisting it, so data stays sanitized even for events sent by an older SDK version or posted directly to the ingest API.Capturing Errors
captureException(error, context?)
The primary method for manual error capture. Use inside try/catch blocks to send errors with rich business context.
try {
await chargeCard(userId, amount);
} catch (error) {
vantaTrace.captureException(error, {
userId: 'user_482',
route: '/api/checkout',
method: 'POST',
severity: 'critical',
metadata: {
amount: 29.99,
currency: 'USD',
},
});
throw error; // re-throw if needed
}Severity Helpers
Convenience wrappers that set the severity field automatically:
// Equivalent to captureException with severity: 'critical'
vantaTrace.captureCritical(error, { userId: 'user_42' });
// Equivalent to captureException with severity: 'warning'
vantaTrace.captureWarning(error, { route: '/api/webhook' });
// Equivalent to captureException with severity: 'info'
vantaTrace.captureInfo(error, { metadata: { source: 'scheduler' } });| Option | Type | Required | Default | Description |
|---|---|---|---|---|
| captureCritical() | method | No | — | Forwards error with severity: "critical". Dashboard shows a red badge. |
| captureWarning() | method | No | — | Forwards error with severity: "warning". Dashboard shows an amber badge. |
| captureInfo() | method | No | — | Forwards error with severity: "info". Dashboard shows a blue badge. |
Context Object
The optional second argument to all capture methods accepts a VantaTraceContext object:
// The full VantaTraceContext object you can pass to captureException:
{
userId?: string; // Authenticated user ID
route?: string; // API route (e.g. '/api/checkout')
method?: string; // HTTP method (e.g. 'POST')
ip?: string; // Client IP address
headers?: Record<string, any>; // Sanitized HTTP headers
severity?: 'critical' | 'warning' | 'info';
metadata?: Record<string, any>; // Any custom key-value payload
}userId, route, method, ip, and headers are auto-populated from the request via AsyncLocalStorage — you only need to pass additional metadata.Logger Integrations
When initGlobalHandlers() is called, VantaTrace automatically detects and patches popular loggers. No additional configuration is required.
Winston
A custom winston-transport is injected at runtime. Any Error object logged via logger.error() is forwarded to VantaTrace.
import winston from 'winston';
import { VantaTrace } from '@vantatrace/sdk';
const vantaTrace = new VantaTrace({
apiKey: process.env.VANTATRACE_API_KEY!,
});
// Auto-patches Winston when initGlobalHandlers() is called.
// Any logger.error(error) call where the argument is an Error
// will be automatically forwarded to VantaTrace.
vantaTrace.initGlobalHandlers();
const logger = winston.createLogger({
transports: [new winston.transports.Console()],
});
// This error will appear in your VantaTrace dashboard:
logger.error(new Error('Database connection refused'));Pino
Pino's write() prototype method is patched. Errors passed directly or inside { err } / { error } objects are captured.
import pino from 'pino';
import { VantaTrace } from '@vantatrace/sdk';
const vantaTrace = new VantaTrace({
apiKey: process.env.VANTATRACE_API_KEY!,
});
// Auto-patches Pino when initGlobalHandlers() is called.
vantaTrace.initGlobalHandlers();
const logger = pino();
// Any of these patterns will forward errors to VantaTrace:
logger.error(new Error('Connection timeout'));
logger.error({ err: new Error('DB query failed') }, 'Database error');
logger.error({ error: new Error('Payment failed') }, 'Payment error');console.error Patch
console.error is monkey-patched to intercept any Error argument. The original console.error is still called — VantaTrace only adds its own capture on top.
Trace ID Correlation
Every captured error is tagged with a unique 32-character hex Trace ID. You can retrieve the ID bound to the current async request context using the static method VantaTrace.getActiveTraceId().
This allows you to correlate your own log lines (Winston, Pino, or custom) with the exact error event visible in the VantaTrace dashboard.
import { VantaTrace } from '@vantatrace/sdk';
// Get the active trace ID bound to the current async request context.
// Use this to correlate your own log entries with the VantaTrace dashboard event.
const traceId = VantaTrace.getActiveTraceId();
// Example: attach traceId to your response headers for distributed tracing
app.use(vantaTrace.requestHandler());
app.get('/api/orders', (req, res) => {
const traceId = VantaTrace.getActiveTraceId();
res.setHeader('X-Trace-Id', traceId ?? '');
// ...
});Ready to get started?
Create a free account