Next.js
Integration
Section titled “Integration”Add observability to your Next.js App Router application and see HTTP routes, server actions, and (optionally) browser page loads in Kopai. This guide follows the official Next.js OpenTelemetry guide and offers two paths:
@vercel/otel— the recommended default. Four lines of code. Covers server-side HTTP spans.- Manual OpenTelemetry SDK — when you need browser traces, end-to-end distributed tracing from client to server, or control over processors, exporters, and sampling.
Both approaches read the OTLP endpoint from the standard OTEL_EXPORTER_OTLP_ENDPOINT environment variable.
Prerequisites
Section titled “Prerequisites”- Node.js 22+
- A Next.js 15 App Router project (
npx create-next-app@latest) - Kopai running locally:
npx @kopai/app startOption 1: @vercel/otel
Section titled “Option 1: @vercel/otel”Install the package:
pnpm add @vercel/otelCreate src/instrumentation.ts at the root of your src/ directory:
import { registerOTel } from "@vercel/otel";
export function register() { registerOTel({ serviceName: "my-nextjs-app" });}That’s it. Next.js automatically detects the register() hook and initializes OTel before serving the first request. You’ll get HTTP spans for every route and API handler.
Set the OTLP endpoint and start the dev server:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"pnpm devOption 2: Manual OpenTelemetry SDK
Section titled “Option 2: Manual OpenTelemetry SDK”Use this when you need:
- Browser spans (document load, fetch calls)
- End-to-end distributed tracing — browser spans as parents of the server spans they trigger
- Control over span processors, exporters, or sampling
Server-side
Section titled “Server-side”Install the Node SDK:
pnpm add @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http \ @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/semantic-conventionsCreate src/instrumentation.ts with a runtime guard so the Node SDK is never imported into the Edge runtime:
export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("./instrumentation.node"); }}Create src/instrumentation.node.ts:
import { NodeSDK } from "@opentelemetry/sdk-node";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";import { resourceFromAttributes } from "@opentelemetry/resources";import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "my-nextjs-app-server", }), spanProcessor: new SimpleSpanProcessor(new OTLPTraceExporter()),});
sdk.start();
process.on("SIGTERM", () => { sdk.shutdown().catch(() => process.exit(0));});Browser-side
Section titled “Browser-side”Install the browser packages:
pnpm add @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \ @opentelemetry/context-zone @opentelemetry/instrumentation \ @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-fetchThe browser can’t POST directly to http://localhost:4318 due to CORS. Add a same-origin proxy at src/app/api/otel/route.ts:
import { NextResponse } from "next/server";
export async function POST(request: Request) { const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318"; const body = await request.arrayBuffer();
const res = await fetch(`${endpoint}/v1/traces`, { method: "POST", headers: { "Content-Type": "application/json" }, body, });
return new NextResponse(null, { status: res.status });}Initialize the browser tracer in a client component at src/app/otel-provider.tsx:
"use client";
import { useEffect, useRef } from "react";import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";import { resourceFromAttributes } from "@opentelemetry/resources";import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";import { ZoneContextManager } from "@opentelemetry/context-zone";import { registerInstrumentations } from "@opentelemetry/instrumentation";import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
export default function OtelProvider({ children,}: { children: React.ReactNode;}) { const initialized = useRef(false);
useEffect(() => { if (initialized.current) return; initialized.current = true;
const provider = new WebTracerProvider({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "my-nextjs-app-browser", }), spanProcessors: [ new BatchSpanProcessor(new OTLPTraceExporter({ url: "/api/otel" })), ], });
provider.register({ contextManager: new ZoneContextManager() });
registerInstrumentations({ instrumentations: [ new DocumentLoadInstrumentation(), new FetchInstrumentation({ propagateTraceHeaderCorsUrls: [/.*/], ignoreUrls: [/\/api\/otel/], }), ], }); }, []);
return <>{children}</>;}Wrap your root layout with it in src/app/layout.tsx:
import OtelProvider from "./otel-provider";
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="en"> <body> <OtelProvider>{children}</OtelProvider> </body> </html> );}FetchInstrumentation injects traceparent headers into every outgoing fetch call, so the browser spans become parents of the server spans your API routes create — giving you a single trace spanning client and server.
Verify
Section titled “Verify”Start the app and generate traffic:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"pnpm devOpen http://localhost:3000 and click around. Then query the Kopai CLI:
# List recent tracesnpx @kopai/cli traces search --json
# Inspect a specific trace (copy a traceId from above)npx @kopai/cli traces get <trace-id> --jsonWith the manual SDK setup, a single trace should contain both my-nextjs-app-browser and my-nextjs-app-server spans in a parent-child relationship.
Sending to Kopai.app in the cloud
Section titled “Sending to Kopai.app in the cloud”Change the environment variables to point at the cloud endpoint with your API key:
export OTEL_EXPORTER_OTLP_ENDPOINT="https://otlp-http.kopai.app"export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_BACKEND_TOKEN"No code changes needed — both approaches read these variables automatically. For the manual SDK setup, the /api/otel proxy route forwards requests from the browser to whatever endpoint it reads at runtime.
Working Examples
Section titled “Working Examples”Two complete runnable examples, one per approach:
- Next.js + @vercel/otel — minimal server-side setup
- Next.js + Manual OTel SDK — server + browser + distributed tracing