New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@apitraffic/node

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@apitraffic/node

One-install ApiTraffic SDK for Node. Bundles the framework plugin, the WAF and the APM bridge behind a single entry point, and mounts whichever pieces you switch on. Contains no telemetry logic of its own — every feature is the same package you would othe

Source
npmnpm
Version
2.2.0
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source




Website   •   Docs   •   Node SDK Documentation   •   X   •   Discord

npm version license

@apitraffic/node is the whole ApiTraffic SDK behind a single install - the framework plugin for Express, Fastify, Koa or Hapi, the WAF and the APM bridge, mounted with one call. It captures every inbound request and response your API serves, the outbound http/https/fetch calls it makes, and (with APM) the database, cache and queue work inside each request.

It contains no telemetry logic of its own. Every feature is the same package you would otherwise install by hand, so nothing behaves differently here than it does standalone - and you can drop down to the individual packages at any time without changing how anything works.

Contents

Which package should I install?

You wantInstall
Everything, one install, one call@apitraffic/node
Just request logging, smallest footprint@apitraffic/express (or -fastify, -koa, -hapi)
Request logging + firewall, chosen individuallythe framework package, plus @apitraffic/waf
Everything, on NestJS@apitraffic/nestjs: this package as a Nest module
Everything, on Meteor@apitraffic/meteor: this package mounted on Meteor's WebApp
Everything, on LoopBack 4@apitraffic/loopback: this package as a LoopBack component
Everything, on AdonisJS@apitraffic/adonisjs: this package as an AdonisJS provider and middleware
Everything, on Sails@apitraffic/sails: this package as a Sails hook

Both routes are fully supported. This one trades a little disk for a lot less wiring. Mounting both a standalone framework package and @apitraffic/node is safe: each request is still reported once.

Requirements

  • Node 20+ (engines: >=20.0.0). The 1.x packages still run on Node 18.
  • A supported framework, installed in your app. Express ^4.17.0 || ^5.0.0, Fastify ^4.0.0 || ^5.0.0, Koa ^2.13.0 || ^3.0.0 or @hapi/hapi ^21.0.0. These are optional peer dependencies: this package does not install your framework for you, and works with whichever you already have.
  • An ingest token and a bucket ID from your ApiTraffic account. The setup page in ApiTraffic creates the token for you if you are an owner or admin. See API tokens.
  • Outbound HTTPS from your servers to ingest.apitraffic.io and api.apitraffic.io. Live configuration changes (redaction rules, exclusions, firewall policy) are pushed over a realtime connection to Ably, so also allow its realtime hosts; if that connection is blocked, live updates wait for a restart and nothing else is affected.
  • For APM only: the OpenTelemetry packages listed under APM.

Installation

npm install @apitraffic/node
pnpm add @apitraffic/node
yarn add @apitraffic/node

Quick start

Set your credentials in the environment. Every framework call reads them when you pass no options, which keeps the token out of source control:

API_TRAFFIC_TOKEN=your-ingest-token
API_TRAFFIC_BUCKET=your-bucket-id

Node does not read .env files on its own. Start your app with node --env-file=.env server.js (Node 20.6 or later), or make require('dotenv/config'); the first line of your entry file.

Then mount ApiTraffic before your routes:

const express = require('express');
const apitraffic = require('@apitraffic/node');

const app = express();

app.use(express.json());         // your body parsers first, so the WAF can inspect bodies
app.use(apitraffic.express());   // then ApiTraffic, before your routes

app.get('/', (req, res) => res.json({ ok: true }));

app.listen(3000);

Restart the app and make a request to any route registered after ApiTraffic. The setup page in ApiTraffic moves to Traffic is flowing when the first request arrives.

ES modules and TypeScript

The package is CommonJS and works from ES modules and TypeScript as well. From an ES module, import apitraffic from '@apitraffic/node' and named imports such as import { express } from '@apitraffic/node' both work. In TypeScript:

import express from 'express';
import * as apitraffic from '@apitraffic/node';
import type { ApiTrafficOptions } from '@apitraffic/node';

const options: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };

const app = express();
app.use(express.json());
app.use(apitraffic.express(options));

Add it to your framework

Register ApiTraffic before your routes: middleware added after a route never sees the requests that route answers. On Express and Koa, mount your body parsers before ApiTraffic so the firewall's body rules can inspect the parsed body. On Fastify and Hapi the firewall runs after the body is parsed automatically.

Express

const express = require('express');
const apitraffic = require('@apitraffic/node');

const app = express();

app.use(express.json());         // your body parsers first, so the WAF can inspect bodies
app.use(apitraffic.express());   // then ApiTraffic, before your routes

app.get('/', (req, res) => res.json({ ok: true }));

apitraffic.express() returns an array of middleware (capture, then the firewall), which app.use() accepts directly.

Fastify

const fastify = require('fastify')();
const apitraffic = require('@apitraffic/node');

fastify.register(apitraffic.fastify());  // no `await`: CommonJS has no top-level await, and Fastify loads plugins before listen()

The plugin is wrapped with fastify-plugin, so it applies to the whole server rather than only the scope you register it in. The firewall runs as a preHandler hook, after the body is parsed.

Koa

const Koa = require('koa');
const { bodyParser } = require('@koa/bodyparser');
const apitraffic = require('@apitraffic/node');

const app = new Koa();

app.use(bodyParser());           // your body parser first, so the WAF can inspect bodies
app.use(apitraffic.koa());       // then ApiTraffic, before your routes

Hapi

const Hapi = require('@hapi/hapi');
const apitraffic = require('@apitraffic/node');

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

  await server.register(apitraffic.hapi());   // before server.start()

  await server.start();
};

init();

The plugin is registered with once: true, so registering it twice is skipped rather than mounting a second firewall. The firewall runs on onPreHandler, where the payload is parsed.

NestJS

Install @apitraffic/nestjs, which is this package as a Nest module, and import it once in your root module:

import { Module } from '@nestjs/common';
import { ApiTrafficModule } from '@apitraffic/nestjs';

@Module({ imports: [ApiTrafficModule.forRoot()] })
export class AppModule {}

Meteor

Install @apitraffic/meteor, which is this package mounted on Meteor's WebApp, and call it once from your server's main module:

import { mount } from '@apitraffic/meteor';

mount();

LoopBack 4

Install @apitraffic/loopback, which is this package as a LoopBack component, and register it in your application's constructor:

import { ApiTrafficComponent } from '@apitraffic/loopback';

this.component(ApiTrafficComponent);

AdonisJS

Install @apitraffic/adonisjs, which is this package as an AdonisJS provider and middleware. One command installs it and registers them:

node ace add @apitraffic/adonisjs

Sails

Install @apitraffic/sails, which is this package as a Sails hook. Sails loads it when it lifts; options go in config/apitraffic.js:

module.exports.apitraffic = {
  token: process.env.API_TRAFFIC_TOKEN,
  bucket: process.env.API_TRAFFIC_BUCKET,
};

Placing the pieces yourself

apitraffic.parts('express' | 'fastify' | 'adonisjs', options) returns the pieces express() and fastify() mount, and their AdonisJS equivalents, for an integration that has to put them in place itself (@apitraffic/nestjs, @apitraffic/meteor, @apitraffic/loopback, @apitraffic/adonisjs and @apitraffic/sails use it):

  • adapter - the framework package, set up as the one the helpers act through (or undefined if it could not load);
  • options - your options without this package's own waf key, for adapter.middleware(options);
  • firewall - to mount where the body is parsed (Express middleware, a Fastify preHandler hook, or an AdonisJS middleware (ctx, next)).

Mount adapter.middleware first, then firewall. For adonisjs the adapter is @apitraffic/express, run on the raw Node request and response: mount it as adapter.middleware(options, { view }), with a view(req) that reads what Express would know (ip, protocol, url, method, body, route) from the Adonis context, as @apitraffic/adonisjs does.

Configuration

Options are passed to the framework call (apitraffic.express({ ... }), apitraffic.fastify({ ... }), …) or set as API_TRAFFIC_* environment variables. Most applications need only API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET. Everything that decides what is captured, redacted or blocked is set in your ApiTraffic account and pushed to the SDK, not configured here.

Precedence

An option passed in code wins over its environment variable, which wins over the built-in default. You can mix the two - the token from the environment and debug in code, say. Settings from your ApiTraffic account (redaction rules, exclusions, the firewall policy, APM capture switches, host telemetry) load at startup and update in real time, without a restart.

A few settings follow a different rule:

SettingRule
offlineWafModeAPI_TRAFFIC_OFFLINE_WAF_MODE wins over the code option, so you can change it during an incident without a deploy. Only exactly closed (lowercase) means closed: any other non-empty value, such as CLOSED, still overrides the option and fails open.
Firewall on or offOff if either waf: false is passed or API_TRAFFIC_WAF_ENABLED is exactly false or 0, or off in any case. This variable does not use the switch parser below: FALSE, no or a value with spaces leaves the firewall on. Neither can turn on a firewall your account has not enabled.
captureStatementParams, captureRedisKeysOn if your code or environment turns it on, or the bucket's APM Capture settings do. The dashboard can add capture, never remove what your code asked for.
captureCallSiteThe bucket's APM Capture setting applies unless you set it in code or the environment. Set locally it can only turn code location on for the SDK's own spans: a local false does not override a bucket that has it on. The APM bridge reads API_TRAFFIC_CAPTURE_CALL_SITE (or its own captureCallSite option) itself, so a false there does turn it off for OpenTelemetry spans.
captureStackDepthThe bucket's APM Capture setting applies unless you set it in code or the environment, which then wins.
interceptOutboundA blank value ("", null, or an empty environment variable) turns outbound capture off. For the other switches, limits, sample rates, token, bucket and accountSid, a blank value counts as not set (apiHost and ingestHost fall back only when empty; a value of spaces is used as given). Two exceptions: passing debug at all, even as undefined, overrides API_TRAFFIC_DEBUG, and a blank projectRoot in code skips API_TRAFFIC_PROJECT_ROOT.

Switches accept true/false, yes/no, on/off or 1/0 in any case, as strings or booleans; byte caps and counts must be whole numbers greater than 0 (captureStackDepth may also be 0); sample rates are numbers from 0 to 1. An invalid value is reported once on the console and ignored. debug and strict are not parsed from strings in code - pass true or false - and API_TRAFFIC_DEBUG turns debug logging on only when it is exactly true (any case). The bundled APM bridge is the exception: it also reads 1, yes and on as on.

Credentials and connection

OptionEnvironment variableTypeDefaultDetails
tokenAPI_TRAFFIC_TOKENstring-Required. Ingest token from your ApiTraffic account. Without it the SDK logs an error and disables telemetry; your app still starts.
bucketAPI_TRAFFIC_BUCKETstring-Required. The bucket captured traffic is sent to.
accountSidAPI_TRAFFIC_ACCOUNT_SIDstringLearned from your account settingsOnly needed by a process that mints embed tokens without running the ApiTraffic middleware.
apiHostAPI_TRAFFIC_API_HOSTstringapi.apitraffic.ioHost the SDK loads its settings from. A bare host name means https://.
ingestHostAPI_TRAFFIC_INGEST_HOSTstringingest.apitraffic.ioHost captured traffic is sent to. A bare host name means https://.
debugAPI_TRAFFIC_DEBUGbooleanfalseLog SDK activity to the console.
strict-booleanfalseMakes the base client's setup() throw on misconfiguration (missing token or bucket) instead of logging. This package runs setup in the background and catches that error, so strict does not stop your app from booting, and the missing-token message is then printed only when API_TRAFFIC_DEBUG is set. Leave it off.

Delivery honours the standard HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY environment variables. The SDK warns once if apiHost or ingestHost is plain http:// and not this machine.

Capture and delivery

OptionEnvironment variableTypeDefaultDetails
interceptOutboundAPI_TRAFFIC_INTERCEPT_OUTBOUNDswitchtrueCapture outbound calls made with http, https and fetch. Off leaves those modules untouched.
captureFetchAPI_TRAFFIC_CAPTURE_FETCHswitchtrueCapture outbound global fetch calls (requires interceptOutbound). Telemetry only: an egress firewall policy still applies to fetch.
redactSensitiveKeysAPI_TRAFFIC_REDACT_SENSITIVE_KEYSswitchtrueMask credential-shaped body keys and query parameters (password, token, secret, api_key, …) in addition to your redaction rules.
maxBodyBytesAPI_TRAFFIC_MAX_BODY_BYTESbytes524288 (512 KB)Largest request or response body sent. A larger body is cut to the cap and ends with …[truncated by ApiTraffic: N bytes total]. Applied after redaction.
maxRedactBodyBytesAPI_TRAFFIC_MAX_REDACT_BODY_BYTESbytes1048576 (1 MB)Largest body the SDK redacts. A larger body is replaced by a placeholder before any redaction work.
maxFetchBodyLengthAPI_TRAFFIC_MAX_FETCH_BODY_LENGTHbytes524288 (512 KB)Largest outbound request or response body captured.
requestSampleRateAPI_TRAFFIC_REQUEST_SAMPLE_RATErate1Fraction of captured requests sent. 5xx responses are always sent.
ingestBatchAPI_TRAFFIC_INGEST_BATCHswitchtrueSend several payloads per request when your account and the ingest host support it.
ingestGzipAPI_TRAFFIC_INGEST_GZIPswitchtrueCompress those batches when your account asks for it.

Spans and APM

These control the spans recorded inside each request - from the APM bridge and from your own span() / startSpan() calls.

OptionEnvironment variableTypeDefaultDetails
captureSpansAPI_TRAFFIC_CAPTURE_SPANSswitchtrueMaster switch for spans and per-request span rollups.
captureStatementsAPI_TRAFFIC_CAPTURE_STATEMENTSswitchtrueInclude the normalized SQL or command text (where id = ?) in spans.
captureStatementParamsAPI_TRAFFIC_CAPTURE_STATEMENT_PARAMSswitchfalseAlso keep statements as issued, with their bind values. These carry your users' data.
captureRedisKeysAPI_TRAFFIC_CAPTURE_REDIS_KEYSswitchfalseInclude the concrete Redis key, unmasked and not run through redaction rules.
captureCallSiteAPI_TRAFFIC_CAPTURE_CALL_SITEswitchtrue (bucket setting)Record the application code location that started each span. Unset follows the bucket.
captureStackDepthAPI_TRAFFIC_CAPTURE_STACK_DEPTHwhole number ≥ 010 (bucket setting)Application frames kept per span; 1 keeps the call site alone.
projectRootAPI_TRAFFIC_PROJECT_ROOTstringDetectedDirectory code locations are reported relative to.
redisKeyPatterns-array of strings or RegExps[]Patterns for Redis keys whose variable parts the automatic masking cannot infer.
maxSpansPerRequestAPI_TRAFFIC_MAX_SPANS_PER_REQUESTcount500Spans kept per request. Work past the cap is still counted in rollups.
spanSampleRateAPI_TRAFFIC_SPAN_SAMPLE_RATErate1Fraction of requests that keep full span detail.
maxSpanMetaBytesAPI_TRAFFIC_MAX_SPAN_META_BYTESbytes2048Size cap on each span's metadata, after redaction.

The APM preload (register()) has options of its own - enabledInstrumentations, disabledInstrumentations, instrumentHttp, serviceName, force and more. They are listed in the @apitraffic/apm README and the configuration reference.

Firewall

OptionEnvironment variableTypeDefaultDetails
wafAPI_TRAFFIC_WAF_ENABLEDfalse | objectUnsetLocal override for the firewall. false, or the variable set to exactly false or 0 (or off in any case), refuses it whatever the dashboard says. An object passes settings to @apitraffic/waf (and enabled: false in it turns the firewall off too). true is accepted and means the same as unset. Normally left unset - see Enabling features.
waf.offlineWafModeAPI_TRAFFIC_OFFLINE_WAF_MODE'inherit' | 'open' | 'closed''inherit'Passed to @apitraffic/waf. In this package it has no practical effect at startup: the firewall is only loaded once a policy is available (live, cached or pinned), so a server that has never loaded one serves every request unfiltered whatever this says. The variable wins over the option.
waf.onEvent-function-Called for every inbound block or detection.
waf.onEgressEvent-function-Called for every outbound block or detection.
waf.debug-booleanThe SDK's debugLog firewall errors to the console (a policy that fails to compile, an evaluation that throws); decisions go to waf.onEvent. Unset, it follows the SDK's own debug setting.
const apitraffic = require('@apitraffic/node');

app.use(apitraffic.express({
  waf: {
    onEvent: (event) => logger.warn({ waf: event }, 'firewall event')
  }
}));

Note: with @apitraffic/node the firewall fails open until the first policy is available. A server that starts while ApiTraffic is unreachable, with no cached or pinned copy of your policy, is not protected until a policy loads. To have a policy at boot, commit offline definitions. To refuse traffic until one loads (offlineWafMode: 'closed'), mount @apitraffic/waf directly instead.

Offline definitions and server labels

Environment variableDefaultDetails
API_TRAFFIC_DEFINITIONS_PATHUnsetPath of the pinned definitions file (normally .apitraffic/definitions/sdk.json). When set, it is the only place the SDK looks.
API_TRAFFIC_CACHE_DIRnode_modules/.cache/apitrafficWhere the SDK keeps its last-known-good copy of your settings.
API_TRAFFIC_FALLBACK_MAX_AGE_DAYSUnset (no limit)Skip a pinned file older than this many days.
API_TRAFFIC_SERVICE_NAME, API_TRAFFIC_SERVICE_VERSION, API_TRAFFIC_REGION, API_TRAFFIC_GIT_SHAUnsetLabel each server and the requests it serves. Common platform variables (AWS_REGION, GIT_SHA, VERCEL_GIT_COMMIT_SHA, …) are used as fallbacks.

Every option, its parsing rules and the full fallback lists are in the SDK configuration reference.

Enabling features

There is nothing to switch on in code. Everything is mounted by the calls above, and what actually runs is decided in ApiTraffic and pushed to every connected server - the WAF, data redaction, request exclusions, span capture and host telemetry are all controlled that way. Turning a feature on is a change in the dashboard, not a deploy.

FeatureWhere to turn it onCode change
Redaction and exclusionsAccount → Redactions / ExclusionsNo
FirewallAccount → Firewall (WAF)No
Host telemetryBucket Settings → Host TelemetryNo
APM capture switchesBucket Settings → APM CaptureNo
APM spansYour start commandYes: a preload flag, once

Mounting is not the same as loading. The WAF engine is only loaded once a live policy arrives, so an account that never enables it pays a single null check per request - no rules in memory, no boot-time cost.

Overriding locally

Two switches take precedence over the dashboard, and exist for the situations a dashboard cannot cover. Both turn the firewall off; neither can turn it on:

app.use(apitraffic.express({ waf: false }));   // refuse the firewall in code
API_TRAFFIC_WAF_ENABLED=false                  # kill switch, no deploy needed

Use the first when your change process requires that anything able to reject production traffic ships through a deploy rather than a click. Use the second during an incident - it works even when ApiTraffic is unreachable, including when ApiTraffic is the problem.

APM

APM is the one piece this cannot fold into the call above, and the reason is unavoidable: OpenTelemetry instruments libraries by patching them as they are required, so it has to run before your application requires pg, redis, http and the rest. By the time a middleware factory runs, that moment has passed.

So it needs a preload. OpenTelemetry is an optional peer of @apitraffic/apm, which this package depends on (its README lists the supported ranges) - install it when you want APM:

npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/instrumentation @opentelemetry/auto-instrumentations-node

Then start your app with:

node -r @apitraffic/node/register app.js

With credentials in a .env file: node --env-file=.env -r @apitraffic/node/register app.js. The preload reads its configuration from API_TRAFFIC_* environment variables, because a preload has nowhere to take arguments from.

If you cannot change the start command, make this the very first line of your entry file, above every other require - it also lets you pass options in code:

require('@apitraffic/node').register({ serviceName: 'orders-api' });

register() returns { registered, reason?, loadedTooEarly? } and never throws. If the OpenTelemetry packages are missing or fail to start, APM is skipped - with a one-line reason on the console - and your application boots normally. If your database or cache client was already loaded when register() ran, it produces no spans, and the console says which modules were loaded too early.

ES modules start with node --import and OpenTelemetry's loader hook instead of -r:

// otel-hook.mjs
import { register } from 'node:module';

register('@opentelemetry/instrumentation/hook.mjs', import.meta.url);
node --import ./otel-hook.mjs --import @apitraffic/node/register ./server.mjs

This preload is for applications with no other tracing: it owns the process's OpenTelemetry tracer provider and installs only a curated set of instrumentations, with no exporters or propagators. If your application already runs OpenTelemetry, skip the preload and add ApiTrafficSpanProcessor from @apitraffic/apm to your own provider; with Datadog or New Relic loaded the preload steps aside on its own. See the @apitraffic/apm README and Set up APM.

What gets captured

  • Inbound requests your framework serves after ApiTraffic is mounted: method, URL, headers, query string, request and response bodies, status and timing. Aborted requests are reported with status 499.
  • Outbound calls made with http, https (including libraries built on them, such as axios and got) and the global fetch, with their request and response bodies (up to maxFetchBodyLength, 512 KB by default). Switch them off with interceptOutbound or captureFetch. See Outbound requests.
  • Spans from the APM bridge (queries, cache calls, queues, RPC, outbound calls the SDK cannot see itself) and from your own span() / startSpan() calls, plus per-request rollups.
  • Tags and traces you add with tag() and trace(), and the firewall's decision on each request it matched.

Bodies are capped: over maxBodyBytes a body is cut to the cap and marked as truncated (over maxRedactBodyBytes it is replaced by a placeholder), and a body written in pieces or streamed (res.write(), Server-Sent Events, a stream returned from a handler) is captured from the bytes written, up to maxBodyBytes; the framework package READMEs list the few exceptions. Bodies that cannot be captured safely (nested more than 256 levels deep, unreadable, or not processable by your redaction rules) are replaced with a [body not captured: …] placeholder, and the rest of the request is still sent. requestSampleRate sends a fraction of requests; server errors are always sent.

Data security

  • Redaction happens in your process. Your account's redaction rules are applied before anything leaves the server, and nothing is sent before they have loaded.
  • Sensitive keys are masked by default. Credential-shaped body keys and query parameters (password, token, secret, api_key, …) are redacted whatever your own rules say; credential headers are always masked. Turn the key masking off with redactSensitiveKeys: false / API_TRAFFIC_REDACT_SENSITIVE_KEYS=false.
  • A request whose redaction cannot be completed is dropped, never sent unredacted.
  • Exclusions stop matching requests from being sent at all, configured in the dashboard with no code change. See Exclusions.
  • SQL values stay in your process unless you turn on captureStatementParams; spans carry the normalized statement (where id = ?).

Treat the ingest token as a secret. It can't read captured traffic or mint embed tokens, but whoever holds it can send traffic into your bucket and read the SDK's settings, which include your redaction rules and firewall policy. Keep it in a secret store, never in client-side code, images or a repository, and rotate it if it may have been exposed.

Tagging, tracing and spans

const apitraffic = require('@apitraffic/node');

app.post('/v1/checkout', async (req, res) => {
  apitraffic.tag('accountId', 'acct_123');               // searchable in the request log
  apitraffic.trace('reached the discount branch');       // a timestamped note on this request

  const order = await apitraffic.span('custom', 'price cart', () => priceCart(req.body));
  res.json(order);
});
FunctionDetails
tag(key, value)Attach a searchable key-value pair. Keys are 1–64 characters (a letter first, then letters, digits, _, . or -); values longer than 255 characters are cut; at most 32 tag keys per request. See the tag rules.
trace(message)Record a timestamped note (up to 255 characters, 250 per request; a longer message is dropped, not cut).
span(type, name, fn, meta?)Run fn, time it, and record it as a span. Returns what fn returns (sync or promise) and re-throws its error after marking the span failed. time() is the same function.
startSpan(type, name, meta?)Start a span for work that does not fit one function. Returns a handle with setMeta(meta) and end({ ok?, meta? }).
addSpan(span)Add an already-built span to the current request.
getRequestManager() / getContext()The current request's state, or undefined outside a request. Most applications never need them.

type is one of db.query, redis, cache, http.out, queue, rpc, graphql or custom. All of these are safe to call from anywhere. Outside a request they do nothing rather than throwing, and a tag that breaks a rule is dropped and counted, never thrown into your handler.

Embed tokens

The embeddable request log, and embedded reports and dashboards, authenticate with a short-lived JWT that locks in the bucket, the filters and what the embed may show. It is minted on your server with an Administrator API token with no access scopes, set as API_TRAFFIC_EMBED_TOKEN (the ingest token the SDK reports with cannot mint embed tokens), and this package mints it:

const apitraffic = require('@apitraffic/node');

// Behind your own sign-in (requireUser is your auth middleware): an embed
// token is a read credential, so never mint one for an anonymous caller.
app.get('/api/embed-token', requireUser, async (req, res, next) => {
  try {
    const { token, expiresAt } = await apitraffic.embedToken({
      // Structured: the id is escaped, so a value such as `x" OR method == "GET`
      // cannot add a condition of its own.
      filters: { tags: { customerId: req.user.id } },
      ttl: 900
    });

    res.json({ token, expiresAt });
  } catch (err) {
    next(err); // Express 4 does not catch a rejected async handler for you
  }
});

The host, bucket and account come from the SDK you already configured and the token from API_TRAFFIC_EMBED_TOKEN, so a call names only what is specific to this embed. Everything is optional, except reportSid or dashboardSid for a report or dashboard embed:

OptionDefaultDetails
bucketSidThe bucket the SDK reports to (request log only)The bucket the embed is scoped to. A report or dashboard keeps its own scope unless you pass one.
view'request-log''request-log', 'report' (with reportSid) or 'dashboard' (with dashboardSid). The report or dashboard must allow embedding.
filters{}environmentSid, criteria, streamViewSid, tags (request log) or filter, environmentSid, tags (report and dashboard) - signed into the token, so the browser cannot widen them. tags ({ customerId: id }) is written into the criteria (for a report or dashboard, the filter) safely; if you build criteria yourself, put values in double quotes through escapeCriteriaValue().
timeRange / timeRangesThe report's own / allReport and dashboard embeds: the range the embed opens with ({ relative: 'last_7d' }) and the ranges the viewer may pick.
ttl3600Token lifetime in seconds (60–86400).
theme'light''light', 'dark' or 'auto'.
tabsThe API's default setRequest log embeds only (the API refuses it for a report or dashboard): the request detail tabs it may show. [] for a list with no expandable detail.
features{}Request log embeds only: copyRequest, includeBodies.
accountSidFrom your SDK settingsOnly needed in a process that does not run the ApiTraffic middleware. Also read from API_TRAFFIC_ACCOUNT_SID.
token / apiHostAPI_TRAFFIC_EMBED_TOKEN, then the SDK's ownOverride the credential or host for one call. If it falls back to the SDK's ingest token, the API answers 403.

It resolves with { token, expiresAt, tabs, features }. Data behind a tab you do not grant is never returned to the browser - the tabs are an access boundary, not a display setting. See the embed documentation for the full list.

This is the one call in the package that rejects rather than going quiet. A token has a single useful outcome, and it is produced inside your own await, where a failure is a 500 on that route instead of a broken embed with nothing to explain it.

API_TRAFFIC_EMBED_TOKEN is a read credential. An Administrator API token can mint embed tokens for any bucket in its account, with any filters or none, so whoever holds it can read all of the account's captured traffic. Keep it in a secret store (never in client-side code, images or a repository), set it only on servers that mint embed tokens, and rotate it immediately if it may have been exposed.

Delivery health

const apitraffic = require('@apitraffic/node');

console.log(apitraffic.stats());
// { pending, queued, delivered, recovered, dropped, droppedRejected, breakerOpen, ... }

Useful when you want to assert in your own monitoring that telemetry is actually leaving the process: payloads in flight and queued, delivered, recovered after a retry, dropped by cause, and whether the circuit breaker is open. The same counters are reported to ApiTraffic in real time.

Shutting down

The SDK keeps a realtime connection open for live configuration, which is right for a server and wrong for a script, a queue worker or a test run that is meant to finish. Stop it when you are done:

const server = app.listen(3000);

process.once('SIGTERM', async () => {
  await new Promise((resolve) => server.close(resolve)); // stop accepting, let in-flight requests finish
  await apitraffic.shutdown({ flushTimeoutMs: 2000 });        // deliver what was captured, then release everything
  process.exit(0); // a handler replaces Node's default exit on SIGTERM
});

shutdown() stops capturing, delivers what is in flight and queued (waiting at most flushTimeoutMs, default 2 s; whatever is left is dropped and counted), closes the realtime connection, stops background timers and removes the http/https/fetch hooks. It also stops APM tracing that register() started. It installs no signal handlers - you decide when - never throws, resolves with { flushed, remaining }, and a later setup starts the SDK again.

To deliver what has been captured without stopping - before a serverless function is frozen, say - use await apitraffic.flush({ timeoutMs: 1000 }) (default 2 s). Anything not delivered in time stays queued.

Setup never waits for the realtime connection: where it is blocked, live config updates are off and nothing else is affected. See Shutting down.

TypeScript

Declarations ship with the package. ApiTrafficOptions types every option, including waf; a value of the wrong type, or a misspelled key in an object literal, is a compile error, and at runtime unknown keys are still passed through.

import type { ApiTrafficOptions } from '@apitraffic/node';

const options: ApiTrafficOptions = { debug: true, waf: { offlineWafMode: 'open' } };

Switches and limits accept a string too, so { captureSpans: process.env.CAPTURE_SPANS } type-checks; debug and strict stay boolean.

TypeWhat it is
ApiTrafficOptionsOptions for every framework call: ApiTrafficSetupOptions plus waf.
ApiTrafficSetupOptionsThe base SDK options, re-exported from @apitraffic/utilities.
WafConfigureOptionsThe object form of waf, re-exported from @apitraffic/waf.
ApmOptions, RegisterResultOptions and result of register(), re-exported from @apitraffic/apm.
ApiTrafficExpressHandler, ApiTrafficFastifyPlugin, ApiTrafficKoaMiddleware, ApiTrafficHapiPluginWhat each framework call returns.
ApiTrafficPartsWhat parts() returns.
SpanHandle, SpanMetaThe handle startSpan() returns, and span metadata.
EmbedTokenOptions, EmbedTokenResultOptions and result of embedToken().

Failure behaviour

The SDK is built so that it can never be the reason your application fails.

  • A missing or invalid token disables telemetry and logs a clear message. Your application still boots.
  • If ApiTraffic is unreachable, requests are served normally. A short outage is absorbed by an in-memory retry queue; a longer one sheds data rather than growing without bound. See If ApiTraffic is unreachable.
  • If the realtime connection fails, you lose live config updates and nothing else.
  • Nothing is sent before your account's client-side redaction rules have loaded.
  • If a bundled package cannot load, that feature is disabled with a one-line console message and the rest of the SDK keeps working.
  • tag() and trace() never throw into your handlers, and every helper does nothing outside a request.

Want the app to refuse to boot without credentials? Check API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET yourself before you start the server. strict: true does not do it here: setup runs in the background and its error is caught.

Troubleshooting

Set API_TRAFFIC_DEBUG=true and restart: the SDK explains what it is doing on the console.

The setup page never leaves "Listening for your application". The SDK has not connected. Check that the process was restarted after you set API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET, and that the variables actually reach the process (Node does not load .env by itself). Even without debug mode the SDK prints ApiTraffic: An APITraffic API Token must be set… for a missing token, and ApiTraffic: token rejected (HTTP 401)… or (HTTP 403) for one that is invalid or cannot access the bucket. Make sure outbound HTTPS to api.apitraffic.io and ingest.apitraffic.io is allowed, and that any proxy is set in HTTPS_PROXY.

SDK connected, but no requests appear. ApiTraffic is registered after the route you are calling, the request is answered by middleware mounted before ApiTraffic (a health check or static file handler), or an exclusion matches it.

The firewall does not inspect request bodies. On Express and Koa, mount your body parser before apitraffic.express() or apitraffic.koa(). If a router has its own body parser mounted after ApiTraffic, mount apitraffic.express() (or apitraffic.koa()) again on that router, after its parser: the firewall then evaluates the request again with the body, without counting it twice against rate limits or reporting it twice.

APM shows no spans. APM needs the preload (node -r @apitraffic/node/register) or register() as the first line of your entry file, and the OpenTelemetry packages installed. Check the console: register() prints why it did not start and which modules were already loaded.

The installed @apitraffic/waf has no firewall for this framework. Two different versions of the SDK packages are installed. The server SDK packages (@apitraffic/node, the framework packages, @apitraffic/waf, @apitraffic/apm and @apitraffic/utilities) share one version number from 2.0.0 on, so align their versions (for example with npm ls @apitraffic/waf).

My script or test run never exits. The realtime connection keeps the event loop alive. Call await apitraffic.shutdown() when the work is done.

Changelog and license

Release notes are in CHANGELOG.md. Released under the MIT License.

Community

Follow @apitraffic/node on npm to stay updated on new releases.

Follow us on our Blog and on X.

Chat with the ApiTraffic team and other members on Discord and follow our tutorials and other videos on YouTube.

ApiTraffic Discord

ApiTraffic YouTube

ApiTraffic on X

How to contribute

Here are some ways you can help improve ApiTraffic:

  • Give ApiTraffic a try and share your feedback to help make ApiTraffic even better for you. Feel free to reach out to us on Discord!
  • Join our Discord to connect with other members, share your experiences, and learn from one another.
  • Report bugs or request features by emailing support@apitraffic.io or asking on Discord.

Keywords

apitraffic

FAQs

Package last updated on 25 Oct 2026

Related posts