> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apitraffic.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Enable APM

> Install the OpenTelemetry bridge and start capturing spans

APM is an opt-in add-on to your existing ApiTraffic integration. Your framework integration
([Express](https://docs.apitraffic.io/integrations/express.md), [Fastify](https://docs.apitraffic.io/integrations/fastify.md), [Hapi](https://docs.apitraffic.io/integrations/hapi.md),
[Koa](https://docs.apitraffic.io/integrations/koa.md), [Strapi](https://docs.apitraffic.io/integrations/strapi.md)) keeps capturing requests, responses and
outbound HTTP; adding `@apitraffic/apm` adds everything that happens *inside* the request —
queries, cache calls, queues and RPC.

All of the OpenTelemetry weight lives in the APM package, so nothing is installed into your
application unless you add it.

## Install

```bash npm theme={null}
npm install @apitraffic/apm \
  @opentelemetry/api \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/instrumentation \
  @opentelemetry/auto-instrumentations-node
```

```bash yarn theme={null}
yarn add @apitraffic/apm \
  @opentelemetry/api \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/instrumentation \
  @opentelemetry/auto-instrumentations-node
```

```bash pnpm theme={null}
pnpm add @apitraffic/apm \
  @opentelemetry/api \
  @opentelemetry/sdk-trace-node \
  @opentelemetry/instrumentation \
  @opentelemetry/auto-instrumentations-node
```

The OpenTelemetry packages are peer dependencies. If you are already running OpenTelemetry you
only need `@apitraffic/apm` and the packages you have.

## Option 1 — Zero configuration

Call `register()` **before your application boots**, so the auto-instrumentations can patch
`pg`, `redis`, `mysql2` and friends before your code requires them. The usual way is a
preload file:

```javascript instrument.js theme={null}
require('@apitraffic/apm').register();
```

```bash theme={null}
node -r ./instrument.js ./server.js
```

Or, if you prefer a single entry point, make it the very first import:

```javascript server.js theme={null}
require('@apitraffic/apm').register();

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

const app = express();
app.use(apitraffic.express({ token: process.env.API_TRAFFIC_TOKEN, bucket: process.env.API_TRAFFIC_BUCKET }));
```

`register()` is idempotent and fail-open: if OpenTelemetry is missing, or another APM already owns
the tracer provider, it returns quietly and your application starts normally.

> [!WARNING]
>
> Registering after your database or cache client has been required means those modules are never
> patched, and their operations produce no spans. This is the most common reason a request has no
> waterfall.

## Option 2 — You already run OpenTelemetry

Do not double-instrument. Add our span processor to the provider you already have, and we ingest
the spans you are already producing:

```typescript theme={null}
import { NodeSDK } from '@opentelemetry/sdk-node';
import { ApiTrafficSpanProcessor } from '@apitraffic/apm';

const sdk = new NodeSDK({
  spanProcessors: [
    new ApiTrafficSpanProcessor(),
    // ...your existing processors
  ],
  instrumentations: [/* your existing instrumentations */],
});

sdk.start();
```

An `ApiTrafficSpanExporter` is also exported for pipelines that prefer a `SpanExporter` behind a
`BatchSpanProcessor`. Both funnel through the same mapping.

## Configuration

Options can be passed to `register()` or set as environment variables. An explicit option wins
over the environment variable, which wins over the default.

| Option | Environment variable | Default | Meaning |
| - | - | - | - |
| `captureSpans` | `API_TRAFFIC_CAPTURE_SPANS` | `true` | Master switch for span capture. |
| `captureStatements` | `API_TRAFFIC_CAPTURE_STATEMENTS` | `true` | Include the normalized statement (`where id = ?`) on the span. |
| `captureStatementParams` | `API_TRAFFIC_CAPTURE_STATEMENT_PARAMS` | `false` | Also keep the statement as issued and its bind values. See [capture settings](https://docs.apitraffic.io/apm/capture-settings.md). |
| `captureRedisKeys` | `API_TRAFFIC_CAPTURE_REDIS_KEYS` | `false` | Include the concrete Redis key instead of only the masked shape. |
| `captureCallSite` | `API_TRAFFIC_CAPTURE_CALL_SITE` | `true` | Record the application file, line, function and caller chain that started the operation. Set explicitly to override the bucket setting. |
| `captureStackDepth` | `API_TRAFFIC_CAPTURE_STACK_DEPTH` | `10` | Application frames kept per span, innermost first (1–50). `1` keeps the call site alone. |
| `maxSpansPerRequest` | `API_TRAFFIC_MAX_SPANS_PER_REQUEST` | `500` | Hard cap on retained spans per request; overflow is counted. |
| `spanSampleRate` | `API_TRAFFIC_SPAN_SAMPLE_RATE` | `1.0` | Fraction of requests that retain span detail. Rollups are always counted. |
| `maxSpanMetaBytes` | `API_TRAFFIC_MAX_SPAN_META_BYTES` | `2048` | Per-span metadata cap. Values are dropped first, then the statement. |
| `redisKeyPatterns` | — | `[]` | Extra patterns used to mask Redis keys into shapes. |
| `sendStatementTextOnce` | `API_TRAFFIC_SEND_STATEMENT_TEXT_ONCE` | `false` | Send each operation's statement and caller stack once per process per resend interval instead of on every span. See [statement text once](#statement-text-once-per-operation). |
| `statementTextResendSeconds` | `API_TRAFFIC_STATEMENT_TEXT_RESEND_SECONDS` | `3600` | How long delivered text is left off before it is sent again. |
| `statementTextCacheSize` | `API_TRAFFIC_STATEMENT_TEXT_CACHE_SIZE` | `5000` | Operations remembered per process (least recently used are forgotten first). |
| `disabledInstrumentations` | `API_TRAFFIC_DISABLED_INSTRUMENTATIONS` | `[]` | OpenTelemetry instrumentation names to turn off. |
| `instrumentHttp` | `API_TRAFFIC_INSTRUMENT_HTTP` | `false` | Enable OpenTelemetry's `http`/`https` client instrumentation. Off because the base client already records those calls with bodies; turning it on double-counts. |
| `synthesizeOutboundRecords` | `API_TRAFFIC_SYNTHESIZE_OUTBOUND_RECORDS` | `true` | Backfill outbound records for `fetch`/`undici`, which the native hook cannot see. |
| `serviceName` | `API_TRAFFIC_SERVICE_NAME` | — | Service name reported to OpenTelemetry. |
| `debug` | `API_TRAFFIC_DEBUG` | `false` | Verbose internal logging. |

```javascript theme={null}
require('@apitraffic/apm').register({
  serviceName: 'orders-api',
  spanSampleRate: 0.25,
  disabledInstrumentations: ['@opentelemetry/instrumentation-fs']
});
```

`captureStatementParams` and `captureRedisKeys` can also be turned on from the dashboard for a
bucket, without a redeploy and with an automatic expiry, and `captureCallSite` /
`captureStackDepth` are owned by the dashboard unless you set them here. See
[capture settings](https://docs.apitraffic.io/apm/capture-settings.md).

## Statement text and privacy

Statements and call stacks are stored and shown only for accounts with statement text enabled,
everywhere: request pages, shared links, embeds, workflows, the API, the MCP tools and the
Performance analytics. For other accounts they are removed before a request is stored (and again
on every read, for requests stored earlier); spans keep their operation, fingerprint and call
site. See [capture settings](https://docs.apitraffic.io/apm/capture-settings.md#statement-text-is-stored-only-for-accounts-that-opt-in).

## Statement text once per operation

A query's normalized statement and caller stack are the same every time it runs, and they are the
largest part of a span. With `sendStatementTextOnce` on, a process sends them with the first span
of each operation (fingerprint) and leaves them off later spans of that operation for
`statementTextResendSeconds`. Those spans keep their fingerprint, name, type and call site, and
list what they left off in `textOmitted`; the request page fills the text back in from the other
spans of the request and from your account's fingerprint dictionary.

* Text counts as sent only once the payload was delivered, and it is sent again after the resend
  interval, so a restart or a failed write on our side heals within that interval.
* A caller stack is left off only when that exact stack was already sent for the operation; an
  operation called from several places keeps sending each new stack.
* Each process keeps its own memory, so a fleet sends each statement once per process per interval.

It is off by default. Turn it on only once the processors that ingest your data maintain the
fingerprint dictionary (earlier processors read the statement from every span). Those processors
keep the dictionary for every request with spans from an account with statement text enabled,
whether or not the account's plan includes span analytics and whether or not span rows are
shipped to the analytics store, so resolution does not depend on the plan.

The dictionary holds statement text only for accounts with statement text enabled: for other
accounts, repeated spans on the request page show their fingerprint and call site, with a note
that query text is not stored for the account. For an account that stores text, a statement the
dictionary cannot supply yet (its first write failed, or the lookup timed out) is shown as
temporarily unavailable; it heals with the next resend.

The memory is per destination: when a process is set up again with a different bucket or API
token, it starts empty and sends every statement again.

This is a base-client option: pass it with your other ApiTraffic options or set the environment
variable; `register()` of the bridge does not take it.

## Outbound HTTP

Outbound calls made with `http`/`https` (including axios, got and request) are captured by the
base client, with full request and response bodies. Calls made with `fetch`/`undici` are captured
by the APM bridge as metadata-only spans. You do not need to configure anything for this; leaving
`instrumentHttp` off is what keeps a native call from being recorded twice.

## Verify

1. Start your application and exercise an endpoint that queries a database or calls another API.
2. Open the request in **Traffic** and select the **Waterfall** tab. Each query, cache call and
   outbound request appears on the timeline with its duration and metadata.
3. Open **Performance** to see the same operations aggregated across requests.

## Troubleshooting

> **A request has no waterfall at all**
>
> APM was probably not registered in that process, or was registered too late. `register()` must
> run before the database/cache modules are required — use `node -r ./instrument.js` to be sure.
> Enable `debug: true` to see whether the bridge started and whether OpenTelemetry was found.

> **Only outbound HTTP appears, no queries**
>
> The driver's instrumentation was not applied. Check that the driver is one of the supported
> modules, that it is required *after* `register()`, and that it is not listed in
> `disabledInstrumentations`.

> **Waterfall stops partway through the request**
>
> The per-request span cap (`maxSpansPerRequest`, default 500) was reached — typically an N+1.
> The dropped count is retained on the request, and the rollups still include the dropped work.

> **Queries show `where id = ?` instead of the values**
>
> That is the default. Values are stripped in-process before anything is sent. Turn them on
> deliberately and temporarily from [capture settings](https://docs.apitraffic.io/apm/capture-settings.md).

> **Outbound calls appear twice**
>
> OpenTelemetry's `http` instrumentation is enabled alongside the native hook. Leave
> `instrumentHttp` off (the default) unless you have disabled the native hook.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.