# Instrument Next.js with OpenTelemetry

> Send Next.js traces to Telemetry Machine with @vercel/otel and Next.js's built-in OpenTelemetry spans, for routes, rendering and fetch calls.

Next.js traces its own routes, rendering and `fetch` calls once a tracer is registered. `@vercel/otel` registers one and exports its spans. It sends traces only.

## Create the dataset

Create a traces dataset and an ingest key that can write to it, with [tm-cli](https://telemetrymachine.com/docs/quick-start#tm-cli) or the [MCP server](https://telemetrymachine.com/docs/quick-start#option-1-set-up-with-a-coding-agent):

```console tab="tm-cli"
$ tm-cli datasets create --name shop-prod-traces --type traces
$ tm-cli api-keys create --name shop-web --dataset shop-prod-traces
```

```json tab="MCP"
create_dataset { "name": "shop-prod-traces", "signal_type": "traces" }
create_ingest_key { "name": "shop-web", "datasets": ["shop-prod-traces"] }
```

tm-cli shows the key once, in `api_key.api_key`. Over MCP, your agent gets it once and saves it in your app's configuration. Or [create the dataset in the app](https://telemetrymachine.com/datasets/new), which gives you a key for it.

## Install

```console
$ npm install @vercel/otel @opentelemetry/api
```

Add `instrumentation.ts` to your project's root, or to `src/` if your app is there:

```ts title="instrumentation.ts"
import { registerOTel } from '@vercel/otel';

export function register() {
  registerOTel();
}
```

## Configure

```bash
export TM_INGEST_KEY='<ingest-key>'
export OTEL_SERVICE_NAME=shop-web
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.telemetrymachine.com
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="X-API-Key=$TM_INGEST_KEY,X-Dataset-Name=shop-prod-traces"
```

## Run

Build and start your app as usual:

```console
$ npm run build
$ npm start
```

`npm run dev` sends traces too.

## Check it arrived

Send your app a few requests, then open **Traces** and choose `shop-prod-traces`. Each request is a server span named after its route, such as `GET /items/[id]`, with Next.js's spans for routing and rendering beneath it.

Or count spans by name with tm-cli or the MCP server:

```console tab="tm-cli"
$ tm-cli query shop-prod-traces --since 15m --select "name, count(*)" --group-by name
```

```json tab="MCP"
run_query {
  "dataset": "shop-prod-traces",
  "description": "Spans by name",
  "since": "15m",
  "select": "name, count(*)",
  "group_by": "name"
}
```

## Troubleshooting

- **Each request has an extra root span named just `GET`, with no `http.route`:** `@opentelemetry/auto-instrumentations-node` is loaded as well, such as through `NODE_OPTIONS`. Remove it, and keep `@vercel/otel` alone.
- **Logs and metrics:** `@vercel/otel` sends neither. Request rate, errors and latency are in the spans: `count(*)`, `status_code = 2` and `duration_ns`.

---

All pages: https://telemetrymachine.com/docs/llms.txt
