Skip to content
Maple Docs
Open app
Browse the docs
On this page

Next.js instrumentation

Instrument a Next.js application with @vercel/otel and send traces, logs, and metrics to Maple.

This guide sets up OpenTelemetry in a Next.js application with @vercel/otel, so the spans Next.js emits for requests, rendering, route handlers and middleware reach Maple, along with your logs and metrics. It works with the App Router and the Pages Router.

To have a coding agent do this setup, use the maple-onboard skill, and maple-audit to check an existing setup.

Prerequisites

  • Next.js 13.4 or later. On 13.4 and 14, the instrumentation hook needs a config flag (see below). From Next.js 15 it is on by default.
  • An ingest key from Settings → Ingestion in Maple. Use the private key (maple_sk_…): instrumentation.ts runs on the server.

Install

npm install @vercel/otel \
  @opentelemetry/api \
  @opentelemetry/api-logs \
  @opentelemetry/instrumentation \
  @opentelemetry/resources \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/sdk-logs \
  @opentelemetry/sdk-metrics \
  @opentelemetry/exporter-logs-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto
pnpm add @vercel/otel \
  @opentelemetry/api \
  @opentelemetry/api-logs \
  @opentelemetry/instrumentation \
  @opentelemetry/resources \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/sdk-logs \
  @opentelemetry/sdk-metrics \
  @opentelemetry/exporter-logs-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto
bun add @vercel/otel \
  @opentelemetry/api \
  @opentelemetry/api-logs \
  @opentelemetry/instrumentation \
  @opentelemetry/resources \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/sdk-logs \
  @opentelemetry/sdk-metrics \
  @opentelemetry/exporter-logs-otlp-proto \
  @opentelemetry/exporter-metrics-otlp-proto

@vercel/otel declares the other @opentelemetry/* packages as peer dependencies, so they are installed alongside it. It includes its own OTLP trace exporter. The log and metric exporters come from the standard OpenTelemetry packages.

Configure

Create instrumentation.ts in the project root, next to next.config.ts. If your project keeps its code in src/, put the file at src/instrumentation.ts instead. Next.js calls register() once when each server runtime starts.

// instrumentation.ts
import { registerOTel, OTLPHttpProtoTraceExporter } from "@vercel/otel"
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs"
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto"
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto"

const MAPLE_ENDPOINT = "https://ingest.maple.dev" // EU: https://ingest.eu.maple.dev
const MAPLE_KEY = "YOUR_INGEST_KEY"

const headers = { authorization: `Bearer ${MAPLE_KEY}` }

export function register() {
	registerOTel({
		serviceName: "my-next-app",
		attributes: {
			"deployment.environment.name": process.env.VERCEL_ENV ?? "development",
			"vcs.repository.url.full": "https://github.com/acme/my-next-app",
			"vcs.ref.head.revision": process.env.VERCEL_GIT_COMMIT_SHA,
		},
		traceExporter: new OTLPHttpProtoTraceExporter({
			url: `${MAPLE_ENDPOINT}/v1/traces`,
			headers,
		}),
		logRecordProcessors: [
			new BatchLogRecordProcessor(new OTLPLogExporter({ url: `${MAPLE_ENDPOINT}/v1/logs`, headers })),
		],
		metricReaders: [
			new PeriodicExportingMetricReader({
				exporter: new OTLPMetricExporter({ url: `${MAPLE_ENDPOINT}/v1/metrics`, headers }),
			}),
		],
	})
}

traceExporter takes an exporter instance. OTLPHttpProtoTraceExporter is exported by @vercel/otel and sends OTLP over HTTP with protobuf encoding. It also runs in the Edge runtime.

The example puts the endpoint and key in source. An ingest key can only write telemetry to your organization. It cannot read data or call the Maple API. Keeping it in source means the SDK always starts with a complete configuration, so a deploy that is missing an environment variable cannot silently turn telemetry off. To keep the key out of source, use environment variables instead.

Next.js 13.4 and 14

Enable the instrumentation hook in next.config.ts. Skip this on Next.js 15 and later.

// next.config.ts
export default {
	experimental: { instrumentationHook: true },
}

Environment variables

When you leave out traceExporter, @vercel/otel configures an OTLP trace exporter from the standard environment variables. Set them in your hosting provider’s project settings:

OTEL_SERVICE_NAME="my-next-app"
OTEL_EXPORTER_OTLP_ENDPOINT="https://ingest.maple.dev"
OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"
OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=production,vcs.repository.url.full=https://github.com/acme/my-next-app"

instrumentation.ts then only needs registerOTel({ serviceName: "my-next-app" }). The log and metric exporters from the Configure step read OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS too, so you can construct them without a url or headers.

Auto-instrumentation

Next.js emits spans for its own work, and @vercel/otel exports them:

  • Requests: a root server span for every request to a page or route handler, named after the method and route.
  • Rendering: spans for App Router rendering, Pages Router getServerSideProps, and route handler execution.
  • fetch: outgoing fetch() calls from server code get client spans, and the trace context is propagated to the service you call.

Set NEXT_OTEL_VERBOSE=1 to get more spans from Next.js internals. Next.js does not produce metrics of its own, so the metric reader exports the metrics you record with the @opentelemetry/api meter.

Custom spans

Wrap business logic in custom spans to make it visible in the trace:

import { trace, SpanStatusCode } from "@opentelemetry/api"

const tracer = trace.getTracer("my-next-app")

export async function processOrder(orderId: string) {
	return tracer.startActiveSpan("process-order", async (span) => {
		try {
			span.setAttribute("order.id", orderId)
			span.setAttribute("payment.method", "card")
			return await chargePayment(orderId)
		} catch (error) {
			span.recordException(error as Error)
			span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message })
			throw error
		} finally {
			span.end()
		}
	})
}

Service map edges come from instrumented client spans that propagate traceparent to an instrumented callee, not from attributes such as peer.service. See Service map.

Log correlation

Log records emitted through the OpenTelemetry log SDK during an active server span carry its trace and span IDs, so Maple links each log line to its trace. To send logs from pino or winston, bridge the logger to the OpenTelemetry log SDK that registerOTel configured above.

Edge runtime

instrumentation.ts runs in the Edge runtime as well as in Node.js. The trace exporter works in both. The metric reader exports on a 60-second timer, so metrics are only reliable from the Node.js runtime.

Verify

  1. Run next dev or deploy, then open a page or call a route handler a few times.
  2. In Maple, open Explore → Traces. Spans are sent in batches every 5 seconds by default.
  3. Each request should show up as one trace with a single root server span named after the method and route (for example GET /dashboard), with rendering and fetch spans nested under it.

Your service also appears on the Services page once its first spans arrive.

Troubleshooting

  • 401 responses. The key is wrong, was copied from the other region, or the header is malformed. The header must be Authorization: Bearer YOUR_INGEST_KEY. In OTEL_EXPORTER_OTLP_HEADERS it is written Authorization=Bearer YOUR_INGEST_KEY. See Ingest API status codes.
  • Wrong protocol or path. Maple accepts OTLP over HTTP. Endpoints set in code need the full signal path (/v1/traces); OTEL_EXPORTER_OTLP_ENDPOINT takes only the base URL.
  • Network. From the machine running the app, run curl -i https://ingest.maple.dev/v1/traces -X POST. Any HTTP status code means the host can reach Maple. A timeout or DNS error means a firewall or proxy is blocking outbound HTTPS.
  • Nothing exported. Check that instrumentation.ts is in the project root, or in src/ for a src layout, and not inside app/ or pages/. On Next.js 13.4 and 14, check that experimental.instrumentationHook is set.

Next steps