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

@apitraffic/hapi

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@apitraffic/hapi

Hapi plugin for sending request data to ApiTraffic.

latest
Source
npmnpm
Version
3.0.0
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source




Website   •   Docs   •   hapi Guide   •   X   •   Discord

@apitraffic/hapi

npm version license

@apitraffic/hapi is the ApiTraffic hapi plugin. It captures every request your hapi API serves (method, URL, headers, bodies, status, timing and the route that matched) and the outbound http, https and fetch calls your app makes while serving it, and sends them to your ApiTraffic bucket. Setup is one plugin, registered before the server starts.

Redaction, exclusions and span capture are configured in your ApiTraffic account and pushed to every connected server, so most changes need no code change or deploy.

Contents

Which package should I use?

Most applications should install @apitraffic/node instead. It is one package for every Node framework, with the firewall included (and APM one more install away, @apitraffic/apm), and every feature enabled from your ApiTraffic account rather than in code. It uses this package for hapi, so request capture behaves the same either way. This package remains published and supported: use it when you want the smallest possible install and are happy to add the firewall and APM yourself.

You wantInstall
Everything, one install, one call@apitraffic/node
Request capture only, smallest footprint@apitraffic/hapi
Request capture plus the firewall, chosen individually@apitraffic/hapi and @apitraffic/waf

Mounting both this package and @apitraffic/node is safe: each request is still reported once.

Requirements

  • Node.js 20 or later.
  • @hapi/hapi 21 (a peer dependency: ^21.0.0). This package does not install hapi for you.
  • 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.

Installation

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

Quick start

Set your credentials in the environment:

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

Node does not read .env files by itself: start with node --env-file=.env server.js (Node 20.6 or later), or make require('dotenv/config'); the first line of your entry file.

CommonJS

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

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

  // Before server.start(). Reads API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET.
  await server.register({ plugin: apiTraffic.plugin });

  server.route({ method: 'GET', path: '/', handler: () => ({ ok: true }) });

  await server.start();
};

init();

CommonJS has no top-level await, so register the plugin inside an async function. To pass credentials in code instead of the environment, give them as the plugin options: server.register({ plugin: apiTraffic.plugin, options: { token, bucket } }).

ES modules and TypeScript

import Hapi from '@hapi/hapi';
import { plugin } from '@apitraffic/hapi';

const server = Hapi.server({ port: 3000 });
await server.register({ plugin });
await server.start();

The package is published as CommonJS. Named imports work from TypeScript and from Node ES modules, and from a native Node ES module import apiTraffic from '@apitraffic/hapi' gives you the whole module. In TypeScript compiled to CommonJS, use import * as apiTraffic from '@apitraffic/hapi' (a default import does not work there: depending on your settings it fails to compile or is undefined at runtime).

Verify the connection

Restart the app, then call a route. The setup page in ApiTraffic shows that the SDK is connected and waiting for a request, then Traffic is flowing when the request arrives. If nothing happens, set API_TRAFFIC_DEBUG=true and restart to see what the SDK is doing.

Where to register it

Register the plugin with server.register() and await it before server.start(). Where you register it relative to your routes does not matter: the plugin works through server extensions (onRequest and onPostResponse), so it covers every route on the server, whether the route is added before or after registration. hapi parses payloads itself, so there is no body parser to order it against.

The plugin is named apiTrafficPlugin and registered with once: true: registering it twice is skipped rather than failing, and each request is reported once. That includes registering it alongside @apitraffic/node.

Error responses

A thrown error or a Boom error is reported with the error response hapi sent and its status. You do not need to change anything. To add context to failures, tag them in an onPreResponse extension, which runs inside the request:

server.ext('onPreResponse', (request, h) => {
  const response = request.response;
  if (response.isBoom) {
    apiTraffic.tag('errorStatus', String(response.output.statusCode));
    apiTraffic.trace(`failed: ${response.message}`);
  }
  return h.continue;
});

Configuration

Options come from three places:

  • Options you pass in code, to server.register({ plugin: apiTraffic.plugin, options: { ... } }).
  • API_TRAFFIC_* environment variables, read when the SDK starts.
  • Settings from your ApiTraffic account: redaction rules, exclusions, APM capture switches and host telemetry. The SDK loads them on startup and receives changes in real time, without a restart.

An option passed in code wins over its environment variable, which wins over the built-in default. You can mix them, for example the token from the environment and debug in code. Every option is optional in code; token and bucket are required from one source or the other.

A few settings follow a different rule:

SettingRule
captureStatementParams, captureRedisKeysOn if either 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 ("" or 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.

How values are parsed. Switches and numeric limits accept strings as well as booleans and numbers, so { captureSpans: process.env.CAPTURE_SPANS } works. Switches accept true/false, yes/no, on/off or 1/0 in any case; byte caps and counts must be whole numbers greater than 0 (captureStackDepth may be 0); sample rates are numbers from 0 to 1. A blank value counts as not provided, so the next source applies. An invalid value ("abc", -5, 1.5 for a rate) is reported once on the console and ignored.

debug and strict are not parsed from strings in code: pass true or false. API_TRAFFIC_DEBUG turns debug logging on only when it is exactly true (any case); 1 or yes do not. (The package's own warnings about errors it swallowed print whenever the variable is set to any non-empty value.)

Credentials and connection

OptionEnvironment variableTypeDefaultDetails
tokenAPI_TRAFFIC_TOKENstring-Required. Your ingest token. Without it the SDK logs an error and disables telemetry for the process; your app still starts.
bucketAPI_TRAFFIC_BUCKETstring-Required. The ID of the bucket captured traffic is sent to. The setup page and Bucket Settings show it.
accountSidAPI_TRAFFIC_ACCOUNT_SIDstringFrom your account settingsOnly needed by a process that mints embed tokens without running this plugin.
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. Useful while installing; leave it off in production.
clientIp-functionrequest.info.remoteAddress(request) => string: the client's address. Hapi has no "trust proxy" setting, so behind a load balancer or proxy remoteAddress is that hop, and every request is recorded with its address (IP lists, behavioural scoring and IP reputation then see one client, or skip a private address). Return the address from the header your edge sets and clients cannot write, for example (request) => request.headers['do-connecting-ip'] on DigitalOcean App Platform or request.headers['cf-connecting-ip'] behind Cloudflare. Trust such a header only if your server accepts connections from the edge alone: reached directly, a client can send any value. Never the left-hand entry of X-Forwarded-For: clients write it. Since 2.4.0. Only the first entry of a list, and only an IP address, is taken; anything else (or a throw) falls back to remoteAddress. @apitraffic/waf 2.4.0 or later enforces against the same address.
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.

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

Capture

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).
redactSensitiveKeysAPI_TRAFFIC_REDACT_SENSITIVE_KEYSswitchtrueMask credential-shaped body keys and query parameters (password, token, secret, api_key and similar) in addition to your redaction rules. Credential headers are always masked.
maxBodyBytesAPI_TRAFFIC_MAX_BODY_BYTESbytes524288 (512 KB)Largest request or response body sent to ApiTraffic. 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, for http, https and fetch calls.
requestSampleRateAPI_TRAFFIC_REQUEST_SAMPLE_RATErate1Fraction of captured requests sent. Requests with a 5xx status are always sent, so a rate of 0 sends server errors only. A blank value never means 0: it counts as not set.

Delivery

OptionEnvironment variableTypeDefaultDetails
ingestBatchAPI_TRAFFIC_INGEST_BATCHswitchtrueSend several payloads per request to ApiTraffic when your account and the ingest host both 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: database, cache, queue and outbound work from the APM bridge, and your own spans from span() and startSpan().

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 (file:line) that started each span.
captureStackDepthAPI_TRAFFIC_CAPTURE_STACK_DEPTHwhole number, 0 or more10 (bucket setting)Application frames kept per span; 1 keeps the call site alone.
projectRootAPI_TRAFFIC_PROJECT_ROOTstringDetectedDirectory code locations are reported relative to. Set it when a bundled or serverless build has removed .git and package.json.
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 the rollups.
spanSampleRateAPI_TRAFFIC_SPAN_SAMPLE_RATErate1Fraction of requests that keep full span detail. Rollups are computed for every request.
maxSpanMetaBytesAPI_TRAFFIC_MAX_SPAN_META_BYTESbytes2048Size cap on each span's metadata, after redaction.

sendStatementTextOnce (API_TRAFFIC_SEND_STATEMENT_TEXT_ONCE), statementTextResendSeconds and statementTextCacheSize reduce payload size by sending each query's text once per process; they are read from the options at runtime but are not part of the ApiTrafficOptions type, so in TypeScript set them with their environment variables (or pass them through a cast).

The full reference, including offline definitions and server labels, is SDK configuration.

What gets captured

Each request is reported as the client received it. Request and response are captured when the response is committed, after its first bytes are written, so capture never delays the response.

  • Request body: request.payload as your app has it when the response is sent, including changes made before responding. Changes made later, for example in an onPostResponse extension, are not reported. When hapi compresses the response, the request is captured when the headers are written.
  • Response body: the payload hapi sent. A stream your handler returns is captured from the bytes sent, up to maxBodyBytes; file downloads served with range support are not copied. Responses with status 204 or 304 have no body.
  • Errors: a thrown error or a Boom error is reported with the error response hapi sent and its status.
  • Status and timing are recorded when the response finishes. A client that disconnects first is reported with status 499, unless it had already received the whole body.
  • Route: the route path hapi matched, such as /v1/orders/{orderId}. Catch-all routes such as /{p*} are grouped by ApiTraffic's own endpoint detection.

Requests made with server.inject() in tests are captured too.

Outbound calls your app makes with http, https, fetch, axios and similar clients are captured automatically and linked to the request that made them. Turn this off with interceptOutbound: false, or just fetch with captureFetch: false. See Outbound requests.

Limits. Bodies over maxBodyBytes are cut to that size and marked as truncated (over maxRedactBodyBytes they are replaced by a placeholder), and requestSampleRate sends a fraction of requests (server errors are always sent). 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.

Data security

Redacting data

Data can be redacted either before it leaves your server or once it arrives at ApiTraffic for processing. Redaction settings do not require any code changes: they are configured in your ApiTraffic account and pushed down to each connected server. Client-side rules are applied in your process before anything is sent, and nothing is sent before they have loaded. A request whose redaction cannot be completed is dropped, never sent unredacted.

On top of your rules, credential headers are always masked, and redactSensitiveKeys (on by default) masks credential-shaped body keys and query parameters. Learn more about redacting data.

Request exclusions

Exclusions stop requests that match certain criteria (a health check, an internal endpoint) from being sent at all. Like redactions, they are configured in your ApiTraffic account and need no code changes. Learn more about excluding requests.

Tagging, tracing and spans

These helpers attach to the request in progress. Call them from route handlers, from hooks and extensions that run inside the request, and from anything they call, including across await. Outside a request they do nothing, and none of them ever throws into your code.

const apiTraffic = require('@apitraffic/hapi');

server.route({
  method: 'GET',
  path: '/v1/orders/{orderId}',
  handler: async (request) => {
    apiTraffic.tag('customerId', String(request.auth.credentials.id));
    apiTraffic.trace('loading order');

    return apiTraffic.span('db.query', 'load order', () => orders.find(request.params.orderId));
  }
});

Tagging

tag(key, value) attaches a searchable key-value pair, for example a customer or tenant ID, so you can find every API request for that customer in the request log. There is no code-side setup: tags are searchable as soon as they arrive.

  • key is 1 to 64 characters: a letter first, then letters, digits, _, . or - (so accountId, not Account Id). Keys starting with redaction., apitraffic. or at. are reserved and dropped; waf. is the firewall's own namespace (such a key is stored among the firewall's tags), so do not use it.
  • value is a string. Numbers and booleans are accepted at runtime and stored as text, though the TypeScript signature takes a string. Values longer than 255 characters are cut to 255.
  • A request holds at most 32 tag keys. Setting an existing key again replaces its value.
  • A tag that breaks a rule is dropped and counted in Settings → Dimensions, never thrown into your handler. See the tag rules.

Tracing

trace(message) records a timestamped note on the request: console.log for production. Use it to mark a code path or surface data that helps troubleshooting. A message longer than 255 characters is dropped, not cut, and a request keeps at most 250 of them.

Spans

span(type, name, fn, meta?) runs fn, times it and records it as a span on the request's waterfall. It returns exactly what fn returns, works with synchronous functions and promises, and re-throws fn's error unchanged after marking the span failed. time() is the same function under another name.

startSpan(type, name, meta?) is for work that does not fit one function. It returns a handle with setMeta(meta) and end({ ok, meta }); call end() once when the work finishes:

const handle = apiTraffic.startSpan('queue', 'publish order.created');
try {
  await queue.publish('order.created', order);
  handle.end();
} catch (error) {
  handle.end({ ok: false, meta: { error: error.code } });
  throw error;
}

type is one of db.query, redis, cache, http.out, queue, rpc, graphql or custom. meta is redacted and capped at maxSpanMetaBytes. With captureSpans off, spans are not recorded and the function still runs.

getRequestManager() and getContext() return the current request's manager and the SDK's context store (or undefined outside a request). Most applications never need them; prefer tag, trace and span. Full limits are in Per-request APIs.

Firewall and APM

This package is request capture only. Both of these are included and pre-wired in @apitraffic/node; with this package you add them yourself.

Firewall. Install @apitraffic/waf and add it as an onPreHandler extension after registering the plugin, where hapi has already parsed the payload:

const apiTraffic = require('@apitraffic/hapi');
const waf = require('@apitraffic/waf');

await server.register({ plugin: apiTraffic.plugin });
server.ext('onPreHandler', waf.hapi());

Behind a load balancer or proxy, set this plugin's clientIp. The firewall enforces against the address it resolves, from @apitraffic/waf 2.4.0 (earlier versions enforce against the socket's address):

await server.register({
  plugin: apiTraffic.plugin,
  options: { clientIp: (request) => request.headers['do-connecting-ip'] },
});
server.ext('onPreHandler', waf.hapi());

The firewall does nothing until you create a policy under Firewall (WAF) in ApiTraffic. See Set up the firewall.

APM. Database, cache and queue spans come from OpenTelemetry, which instruments libraries by patching them as they are required, so it has to start before your application requires pg, redis and the rest. By the time a plugin runs, that moment has passed, so APM cannot be switched on from here. If your app already runs OpenTelemetry, add ApiTrafficSpanProcessor from @apitraffic/apm to your provider. If it has no tracing at all, the bundled preload in @apitraffic/node sets it up. See Set up APM.

Embed tokens

The embeddable request log authenticates with a short-lived JWT that locks in the bucket, the filters and the request detail tabs 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 with embedToken():

const apiTraffic = require('@apitraffic/hapi');

server.route({
  method: 'GET',
  path: '/api/embed-token',
  // An embed token is a read credential: require your auth strategy, so
  // request.auth.credentials is the signed-in user.
  options: { auth: 'session' },
  handler: async (request) => {
    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: String(request.auth.credentials.id) } },
      ttl: 900
    });

    return { token, expiresAt };
  }
});

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.
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().
ttl3600Token lifetime in seconds (60–86400).
theme'light''light', 'dark' or 'auto'.
tabsThe API's default setRequest detail tabs this embed may show. [] for a list with no expandable detail. Request log only: the API refuses it for a report or dashboard.
features{}copyRequest, includeBodies. Request log only: the API refuses it for a report or dashboard.
view'request-log''request-log', 'report' (with reportSid) or 'dashboard' (with dashboardSid). The report or dashboard must allow embedding.
reportSid / dashboardSid-The saved report or dashboard to embed, for view: 'report' or 'dashboard'.
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.
accountSidFrom your SDK settingsOnly needed in a process that does not run this plugin. 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.

Data behind a tab you do not grant is never returned to the browser: the tabs are an access boundary, not a display setting. Every option is documented on the embed page.

Unlike the rest of the SDK, this call rejects rather than going quiet: a token has one useful outcome, so a failure is a 500 on your own 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), and rotate it immediately if it may have been exposed.

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:

process.once('SIGTERM', async () => {
  await server.stop();
  await apiTraffic.shutdown({ flushTimeoutMs: 2000 }); // deliver what was captured, then release everything
  process.exit(0); // a handler replaces Node's default exit on SIGTERM
});

In tests that use server.inject(), call await apiTraffic.shutdown() in your teardown, or the test run will not exit.

shutdown({ flushTimeoutMs }) 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 installs no signal handlers (you decide when), never throws, and resolves with { flushed, remaining }.

To deliver what has been captured without stopping (before a serverless function is frozen, say), use await apiTraffic.flush({ timeoutMs: 1000 }). It also resolves with { flushed, remaining }, and anything not delivered in time stays queued. See Shutting down.

Delivery stats

deliveryStats() reports where this process's captured requests went, so you can assert in your own monitoring that telemetry is actually leaving the process. It never throws.

console.log(apiTraffic.deliveryStats());
// { captured, disabled, settingsLoaded, tokenRejected, ...delivery counters }

captured is the number of requests this plugin captured; the delivery counters (delivered, queued, dropped by cause, circuit breaker state and so on) come from the base SDK.

TypeScript

The package ships its own type declarations. ApiTrafficOptions lists every option in Configuration except the statement-text trio: a value of the wrong type, or a misspelled key in an object literal, is a compile error. Keys it does not list are still passed through at runtime.

ExportWhat it is
ApiTrafficOptionsThe plugin options (the base SDK's setup options).
import type { ApiTrafficOptions } from '@apitraffic/hapi';
import { plugin } from '@apitraffic/hapi';

const options: ApiTrafficOptions = { debug: process.env.NODE_ENV !== 'production' };
await server.register({ plugin, options });

The helpers are typed too: tag(key: string, value: string), trace(content: string), span()/time() return the type fn returns, and shutdown()/flush() return Promise<{ flushed: boolean; remaining: number }>.

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 and serves.
  • 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 configuration updates and nothing else.
  • Nothing is sent before your account's client-side redaction rules have loaded.
  • If the plugin's own per-request setup fails, the request is still served, just not captured.
  • 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

Start with API_TRAFFIC_DEBUG=true and a restart: the SDK then logs what it is doing. Without debug mode it still prints ApiTraffic: An APITraffic API Token must be set… for a missing token, and ApiTraffic: token rejected (HTTP 401)… or (HTTP 403) for a token that is invalid or cannot access the bucket.

The setup page never shows the SDK as connected
  • The process was restarted after you set API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET, and the variables actually reach it. Node does not load .env by itself.
  • Outbound HTTPS to api.apitraffic.io and ingest.apitraffic.io is allowed from where the app runs. Behind a proxy, set HTTPS_PROXY.
Requests are not captured
  • server.register({ plugin: apiTraffic.plugin }) must be awaited before server.start().
  • Restart the process after setting API_TRAFFIC_TOKEN and API_TRAFFIC_BUCKET, and check that they reach it.
  • An exclusion may match the request.
SyntaxError: await is only valid in async functions

CommonJS has no top-level await. Put server.register() and server.start() inside an async function, as in the quick start.

Test runs with server.inject() never exit

The SDK keeps a realtime connection open. Call await apiTraffic.shutdown() in your test teardown.

Outbound calls are missing

Check that interceptOutbound is not off (an empty API_TRAFFIC_INTERCEPT_OUTBOUND turns it off). See Outbound requests for the cases that are not captured.

The script or test run never exits

The realtime connection keeps the event loop alive. Call await apiTraffic.shutdown() when the work is done. See Shutting down.

Peer dependency warnings or version mismatches

Check that your hapi version is in the supported range under Requirements, and that you are on Node.js 20 or later. Installing this package next to @apitraffic/node is fine, even at a different version: each request is reported once.

More answers are in the hapi guide.

Sample application

The setup guide walks through a complete application. The ApiTraffic source (not the npm package) also includes a working sample in examples/basic. The sample is a small hapi 21 app with ApiTraffic registered and two routes:

  • GET /: returns a JSON greeting.
  • GET /authors: makes an outbound call with axios, adds a tag and traces, and returns the result, so you can see an outbound request linked to its inbound one.

It listens on localhost, on port 4000 unless you set PORT.

To run it, set your credentials and start it:

cd examples/basic
export API_TRAFFIC_TOKEN=your-ingest-token
export API_TRAFFIC_BUCKET=your-bucket-id
npm install
npm start

The sample installs @apitraffic/hapi from npm, not from this repository. Open http://localhost:4000/ and http://localhost:4000/authors, then look in your bucket's request log in ApiTraffic.

Documentation

Related packages: @apitraffic/node (everything in one install), @apitraffic/waf (firewall), @apitraffic/apm (APM bridge).

Contributing, changelog and license

Community

Follow @apitraffic/hapi 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

hapi

FAQs

Package last updated on 03 Nov 2026

Related posts