Skip to main content

OpenTelemetry for Next.js

OpenTelemetry for Next.js uses a framework-specific setup — the @vercel/otel helper plus a dedicated instrumentation.ts file at your project root. It needs a tiny amount of setup code; no --require flag will work.

Next.js doesn't support zero-code instrumentation

Like Go, Next.js requires explicit setup — there is no agent or auto-loader. Two architectural reasons:

  • Next.js produces a standalone build that bundles only the imports it detects. The generic --require @opentelemetry/auto-instrumentations-node/register flag fails because the OTel packages get tree-shaken out.
  • Next.js exposes its own startup hook (instrumentation.ts) designed specifically for telemetry — using it produces better trace quality (correct route names like GET /products/[id], full coverage of server components).

The setup is a one-time job per app — once instrumentation.ts is in place, every route and server component traces automatically.


Before You Begin​

You'll need:

  • A Next.js application (Next.js 13.4+ for the instrumentation.ts hook)
  • The URL of your CodeXray collector (e.g., https://your-codexray.example.com)
  • A project API key from the CodeXray UI (Project Settings → API Keys)

Step 1 — Install the Required Packages​

npm install \
@opentelemetry/api \
@opentelemetry/sdk-trace-base \
@vercel/otel
PackageWhat it does
@opentelemetry/apiThe core OTel API — used to create spans, set attributes, and propagate context
@opentelemetry/sdk-trace-baseThe tracing SDK that processes and batches spans before export
@vercel/otelNext.js-aware helper that wires the SDK into the framework's startup hook — handles standalone-build quirks and server-component instrumentation

Step 2 — Create instrumentation.ts​

Create a file named instrumentation.ts at the project root (same level as next.config.js):

import { registerOTel } from "@vercel/otel";
import {
BatchSpanProcessor,
SimpleSpanProcessor,
ConsoleSpanExporter,
} from "@opentelemetry/sdk-trace-base";
import { OTLPHttpProtoTraceExporter } from "@vercel/otel";

export function register() {
registerOTel({
serviceName: process.env.OTEL_SERVICE_NAME || "your-service-name",
spanProcessors: [
// Console exporter — useful while you're verifying setup. Remove for production.
new SimpleSpanProcessor(new ConsoleSpanExporter()),
// OTLP exporter — ships spans to CodeXray
new BatchSpanProcessor(
new OTLPHttpProtoTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: {
"x-api-key": process.env.OTEL_EXPORTER_OTLP_HEADERS_KEY,
},
})
),
],
});
}

What this does:

  • Sets the service name (read from env, with a fallback)
  • Wires a batched OTLP exporter that ships spans to your CodeXray collector
  • Adds a console exporter so you can see spans in your logs while you verify setup — remove or guard with if (process.env.NODE_ENV !== "production") before going live

The register() function is called automatically by Next.js when the server starts.


Step 3 — Enable Standalone Output​

In next.config.js, set output: "standalone":

module.exports = {
output: "standalone",
// ... your existing config
};

Standalone mode produces a self-contained production build with only the files and dependencies your app actually uses — smaller images, faster cold starts. It also keeps instrumentation.ts in the build output, which is required for it to run at startup.


Step 4 — Configure via Environment Variables​

export OTEL_SERVICE_NAME="your-service-name"
export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-codexray.example.com"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_EXPORTER_OTLP_HEADERS_KEY="your-project-api-key"
export OTEL_TRACES_EXPORTER="otlp"
VariablePurpose
OTEL_SERVICE_NAMEThe name that appears in CodeXray's Applications list
OTEL_EXPORTER_OTLP_ENDPOINTYour CodeXray collector URL (no trailing slash)
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobuf (recommended)
OTEL_EXPORTER_OTLP_HEADERS_KEYAuthentication — your project API key (passed through to the x-api-key header by the code in instrumentation.ts)
OTEL_RESOURCE_ATTRIBUTESOptional — deployment.environment=production,service.version=1.3.0

For a Dockerfile, set these as ENV directives:

ENV OTEL_SERVICE_NAME="your-service-name"
ENV OTEL_EXPORTER_OTLP_ENDPOINT="https://your-codexray.example.com"
ENV OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
ENV OTEL_EXPORTER_OTLP_HEADERS_KEY="your-project-api-key"
ENV OTEL_TRACES_EXPORTER="otlp"

CMD ["node", "server.js"]

The CMD stays as node server.js — Next.js loads instrumentation.ts automatically. No --require flag, no NODE_OPTIONS.


Step 5 — Run Your Application​

Build and start as you normally would:

npm run build
npm start

Or in Docker, docker run the image. The first request out of the gate produces a trace.


If You Already Use Sentry​

Sentry ships its own OpenTelemetry initialiser, which will collide with instrumentation.ts. Disable Sentry's auto-OTel setup in sentry.server.config.ts:

Sentry.init({
// ... your existing Sentry config
skipOpenTelemetrySetup: true,
});

This keeps Sentry's error-reporting features intact while letting your instrumentation.ts own the tracing pipeline. Skip this step entirely if you're not using Sentry.


Verifying It's Working​

  1. Deploy and make a request to any route in your app
  2. Check the server logs — the ConsoleSpanExporter you added in Step 2 should print spans for each request
  3. In CodeXray, open Applications → find your service → Traces tab
  4. Traces should appear within a minute, named by HTTP method + route (e.g. GET /api/users, GET /products/[id])

If you only see console spans but nothing in CodeXray, the OTLP exporter is failing — check the server logs for HTTP errors against the collector URL, and confirm the API key.


Custom Spans for Business Logic​

The @vercel/otel setup gives you automatic spans for HTTP requests, server components, and Route Handlers. For your own business logic:

import { trace } from "@opentelemetry/api";

const tracer = trace.getTracer("my-service");

export async function processOrder(orderId: string) {
return tracer.startActiveSpan("processOrder", async (span) => {
span.setAttribute("order.id", orderId);
try {
// your business logic ...
} finally {
span.end();
}
});
}

The span nests inside the request span automatically — so it shows up under the right route in the trace viewer.


Next Steps​