@apitraffic/utilities
Core utilities library that provides shared functionality for all ApiTraffic Node.js framework SDKs. This package contains common utilities for request management, outbound interception, data handling, and configuration management.
Installation
npm install @apitraffic/utilities
Features
- Request Management - Centralized request tracking and context management
- Outbound Interception - Automatic
http/https and fetch request interception and monitoring
- Configuration Management - Unified configuration handling with environment variable support
- Data Processing - Request/response body handling and duration calculation
- Context Management - Request context and tagging system
- Settings Management - Dynamic settings retrieval and caching
Quick Start
const { setup, RequestManager, config } = require('@apitraffic/utilities');
await setup({
token: 'your-api-token',
bucket: 'your-bucket-id',
ingestHost: 'ingest.apitraffic.io',
apiHost: 'api.apitraffic.io',
interceptOutbound: true,
debug: false
});
const requestManager = new RequestManager(config);
requestManager.tag('environment', 'production');
API Reference
Setup Function
Initialize the ApiTraffic utilities with your configuration.
const { setup } = require('@apitraffic/utilities');
await setup(options, context);
Options
token | string | Yes | Your ApiTraffic API token |
bucket | string | Yes | Your ApiTraffic bucket ID |
ingestHost | string | No | Ingest endpoint host (default: from env) |
apiHost | string | No | API endpoint host (default: from env) |
interceptOutbound | boolean | No | Enable outbound request interception (default: true) |
captureFetch | boolean | No | Capture outbound fetch calls (default: true) |
maxFetchBodyLength | number | No | Byte cap per captured fetch request/response body (default: 524288, i.e. 512 KB) |
maxBodyBytes | number | No | Byte cap per captured request/response body, applied after redaction; longer bodies are truncated with a marker (default: 524288, i.e. 512 KB; env API_TRAFFIC_MAX_BODY_BYTES) |
requestSampleRate | number | No | Fraction (0–1) of requests sent to ApiTraffic. Server errors (5xx) are always sent (default: 1.0; env API_TRAFFIC_REQUEST_SAMPLE_RATE) |
debug | boolean | No | Enable debug logging (default: false) |
Environment Variables
You can also configure using environment variables:
API_TRAFFIC_TOKEN - Your API token
API_TRAFFIC_BUCKET - Your bucket ID
API_TRAFFIC_INGEST_HOST - Ingest endpoint host
API_TRAFFIC_API_HOST - API endpoint host
API_TRAFFIC_INTERCEPT_OUTBOUND - Enable outbound interception
API_TRAFFIC_CAPTURE_FETCH - Capture outbound fetch calls
API_TRAFFIC_MAX_FETCH_BODY_LENGTH - Byte cap per captured fetch body (default: 524288)
API_TRAFFIC_REDACT_SENSITIVE_KEYS - Mask credential-shaped body keys and query parameters by default (default: true)
API_TRAFFIC_INGEST_BATCH - Post captured requests to ingest in batches when your account and the ingest host offer it (default: true)
API_TRAFFIC_INGEST_GZIP - Gzip those batches when your account asks for it (default: true)
API_TRAFFIC_DEBUG - Enable debug mode
A gzipped batch compresses several captured payloads (already redacted) in
one stream, so how well it compresses depends on what those payloads have in
common. Where an attacker could both inject chosen content into captured
traffic and observe the encrypted sizes of the SDK's outbound requests to
ingest, set ingestGzip: false (API_TRAFFIC_INGEST_GZIP=false), or ask for
the account's ingest.batch.gzip flag to be turned off, to send batches
uncompressed.
Built-in redaction
Before anything leaves the process, values under credential-shaped names are
replaced with [*** API REDACTED ***], whatever the account's own rules say —
in JSON and application/x-www-form-urlencoded bodies and in the captured
URL's query string (inbound and outbound). The rules aim at names that hold
a secret, not names that mention one:
password, passwd, passcode, secret, apikey, accesskey,
privatekey, credential, signature, assertion, jwt anywhere in the
name (separators ignored: api_key, x-api-key and apiKey all match);
- names ending in
token (access_token, csrfToken), but not pagination
cursors (pageToken, nextToken) or counts (max_tokens);
- the words
auth, pass, pwd, otp, sig, mfa, totp;
secret_key-style keys and auth_code-style codes; session, sessionid,
sid, authorization, cookie;
- in the query string only:
key, code and the word pin.
Booleans, null and empty values are kept. Bodies are edited in place — only
the masked values change, so nothing else about a captured body is altered.
Multipart and XML bodies are not key-masked. Credential headers
(authorization, cookie, x-api-key, …) are always redacted.
Turn the key masking off with redactSensitiveKeys: false (or the env var
above). The SDK also honours an account-level redaction.sensitiveKeys: false
in its settings, for when the dashboard offers that switch. Account rules
scoped to request.query apply to the captured URL's query string, and key
rules apply to form bodies.
Outbound fetch capture
Outbound http/https calls (axios, got, node-fetch’s http path, …) are captured
by patching http.request. Global fetch never goes through it — it drives
Node’s built-in undici straight onto a socket — so it has its own hook, installed
with the rest of the outbound interception. A fetch call is reported with the
same detail as an axios one: URL, method, the headers actually put on the wire
(taken from undici’s sendHeaders diagnostic event, so host,
content-length and accept-encoding are included), request and response
bodies, status, response headers and timing, plus an inline http.out span on
the request that made the call.
Capture never alters the call: the caller gets back the exact Response object,
and bodies are read from clone()s. Two consequences worth knowing:
-
Payloads are capped at maxFetchBodyLength, 512 KB by default (the http
path's cap is 3 MB), because reading a copy of a response tees the stream — a
higher cap holds a second copy of a large download in memory alongside your
own. Bodies past the cap are stored truncated and flagged. Raise it to
whatever your payloads and headroom justify:
await setup({ maxFetchBodyLength: 512 * 1024 * 1024 });
API_TRAFFIC_MAX_FETCH_BODY_LENGTH=536870912
-
A request body that is a stream, FormData or Blob, and non-textual or
text/event-stream responses, are reported as metadata only
(bodyCaptured: false) rather than being consumed or rewritten.
Set captureFetch: false (or API_TRAFFIC_CAPTURE_FETCH=false) to leave
globalThis.fetch untouched.
The hook itself needs nothing newer than Node 18 (verified on
18.20 and 20.18). On a runtime without a global fetch the hook simply doesn't
install, and if the undici wire-header event is unavailable the headers the caller
set are reported instead.
Two properties worth knowing about the hook itself:
- The hook delegates to whatever
globalThis.fetch was at install time and marks
itself, so installing twice (or from a duplicated copy of this package) does not
stack wrappers, and another library wrapping fetch afterwards still runs above
it. Code that replaces globalThis.fetch outright after setup (rather than
wrapping it) removes the hook, and those calls go uncaptured.
- Wire headers are attributed per call by undici request identity (via
undici:request:create in the caller's async context), not by URL, so
concurrent identical requests — including ones dispatched out of order or queued
on a pooled connection — cannot swap header sets.
RequestManager
Manages request context, tags, and traces for individual requests.
const { RequestManager } = require('@apitraffic/utilities');
const manager = new RequestManager(settings);
Methods
tag(key, value)
Add a tag to the current request context.
manager.tag('user_id', '12345');
manager.tag('environment', 'production');
removeTag(key)
Remove a tag from the current request context.
manager.removeTag('user_id');
trace(message)
Add a trace message to the request.
manager.trace('Database query executed');
Utility Functions
getDuration
Calculate duration between two time points.
const { getDuration } = require('@apitraffic/utilities');
const startTime = process.hrtime();
const duration = getDuration(startTime);
handleResponseBody
Process and handle response body data.
const { handleResponseBody } = require('@apitraffic/utilities');
const processedBody = handleResponseBody(responseData, options);
sendToApiTraffic
Send request data to ApiTraffic service.
const { sendToApiTraffic } = require('@apitraffic/utilities');
await sendToApiTraffic(options, payload);
Span fingerprint helpers
This package is the single source of truth for how ApiTraffic turns a raw
operation into a stable fingerprint (so identical SQL/Redis/HTTP/custom
operations collapse to one identity for GROUP BY fingerprint analytics). The
@apitraffic/apm OTel bridge imports these same helpers, so a manually-timed
span and an OTel-captured span for the same operation get an identical
fingerprint. Inputs are raw — normalization/masking happens inside.
const {
normalizeSql,
maskRedisKey,
fingerprintSql,
fingerprintRedis,
fingerprintHttpOut,
fingerprintGeneric,
fingerprintFrom,
} = require('@apitraffic/utilities');
maskRedisKey and the *Redis* helpers accept an optional
redisKeyPatterns: Array<string | RegExp> to override the built-in
variable-segment heuristic for key shapes it can't infer on its own.
Configuration
Access the global configuration object:
const { config } = require('@apitraffic/utilities');
console.log(config.apiToken);
console.log(config.bucketSid);
console.log(config.ingestHost);
Context Management
Manage request context across your application:
const { context } = require('@apitraffic/utilities');
const currentContext = context.get();
context.set('requestId', 'req-123');
Requirements
- Node.js 20.0.0 or higher
- Valid ApiTraffic account and API token
Dependencies
This package includes the following dependencies:
@apitraffic/data-redaction - Data redaction utilities
@apitraffic/javascript-helpers - JavaScript helper functions
@faker-js/faker - Data generation utilities
ably - Real-time messaging
axios - HTTP client
ksuid - Unique ID generation
pako - Data compression
systeminformation - System information gathering
What is captured
The framework packages report each inbound request as the client received it.
The request and the response are captured when the response is committed
— when its first bytes are handed to the socket (onCommit) — not when it
finishes, which for a large response is only once the client has read it. The
capture runs after those bytes are written, so it never delays the response,
and a write that throws (an invalid header value or status code) commits
nothing: the error response that follows is what is reported.
Request body means the same on every framework: the parsed body as the
application has it when it responds — including changes its middleware, hooks
or handler made before responding, and none made afterwards. Usually that is
the commit itself. Where a layer holds the bytes back past the application's
call — compression or express-session mounted after the SDK, an async
Fastify onSend hook, Hapi's gzip — the request alone is snapshotted when the
application starts responding (onCommit's start), the one capture step that
runs ahead of the response's bytes and only on that path. It is kept cheap: a
parsed body whose request declared more than maxRedactBodyBytes
(Content-Length) is replaced by the placeholder without being serialized, and
any other parsed body is serialized by the native serializer and settled into
exactly what the bounded serializer makes of it (nesting cap, size limit,
Buffers) when the payload is prepared, off the response path.
@apitraffic/express | req.body | changes after res.send() / res.end() |
@apitraffic/fastify | request.body | changes after reply.send() (and in onResponse hooks) |
@apitraffic/koa | ctx.request.body | changes after the response is written |
@apitraffic/hapi | request.payload | changes once the response is transmitted (and in onPostResponse extensions) |
Response body is what the client was sent: on Fastify, the payload handed
to reply.raw — after the application's own onSend hooks (one that scrubs a
field for the client) — or, for a stream or a content-encoded body, the payload
as it reached the SDK's onSend. A content-encoded (gzip) body is never
reported as its compressed bytes, on any framework.
Status and timing are recorded when the response finishes (499 for a client
that disconnected first). A body nested more than 256 levels deep — parsed, or
JSON text the framework left as a string, when account redaction rules would
parse it — or one that cannot be read or redacted is sent as a [body not captured: …] placeholder and counted in the
bodyNotCaptured delivery stat. A request whose redaction cannot be completed
even without its bodies is never sent: it is dropped and counted as
droppedUnredactable.
Delivery and proxies
Each captured request is sent to ingest as one JSON POST, prepared and sent
off the response path (on setImmediate, after the response has been
written), on the SDK's own keep-alive agents, with bounded concurrency, a
small retry queue and a circuit breaker. Ingest posts use Node's http/https
directly. When HTTP_PROXY / HTTPS_PROXY / ALL_PROXY (or their
npm_config_* forms) apply to the ingest URL's protocol, the posts go through
axios instead, as before, so its proxy support (NO_PROXY, CONNECT tunnelling
for HTTPS) still applies. The proxy variables are re-read at most every 10
seconds. The SDK's own calls (ingest, settings, host telemetry, embed tokens)
are never captured as outbound calls of the application.
Error Handling
The utilities include comprehensive error handling:
try {
await setup({
token: 'invalid-token',
bucket: 'invalid-bucket'
});
} catch (error) {
console.error('Setup failed:', error.message);
}
Common errors:
- Missing API token or bucket ID
- Invalid Node.js version (< 18.0.0)
- Network connectivity issues
- Invalid configuration parameters
Debug Mode
Enable debug mode to see detailed logging:
await setup({
token: 'your-token',
bucket: 'your-bucket',
debug: true
});
Or via environment variable:
export API_TRAFFIC_DEBUG=true
License
MIT License - see LICENSE file for details.
Support
For support and documentation, visit ApiTraffic.io or contact sdk@apitraffic.io.