# Next.js Instrument Next.js applications with OpenTelemetry for Kopai # Integration Add observability to your [Next.js](https://nextjs.org) 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](https://nextjs.org/docs/app/guides/open-telemetry) and offers two paths: - **[`@vercel/otel`](#option-1-vercelotel)** — the recommended default. Four lines of code. Covers server-side HTTP spans. - **[Manual OpenTelemetry SDK](#option-2-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 - Node.js 22+ - A Next.js 15 App Router project (`npx create-next-app@latest`) - Kopai running locally: **npm:** ```bash npx @kopai/app start ``` **pnpm:** ```bash pnpm dlx @kopai/app start ``` **yarn:** ```bash yarn dlx @kopai/app start ``` ## Option 1: @vercel/otel Install the package: **npm:** ```bash npm install @vercel/otel ``` **pnpm:** ```bash pnpm add @vercel/otel ``` **yarn:** ```bash yarn add @vercel/otel ``` Create `src/instrumentation.ts` at the root of your `src/` directory: ```typescript 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: **npm:** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" npm run dev ``` **pnpm:** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" pnpm dev ``` **yarn:** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" yarn dev ``` ## 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 Install the Node SDK: **npm:** ```bash npm install @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http \ @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/semantic-conventions ``` **pnpm:** ```bash pnpm add @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http \ @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/semantic-conventions ``` **yarn:** ```bash yarn add @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http \ @opentelemetry/resources @opentelemetry/sdk-trace-node @opentelemetry/semantic-conventions ``` Create `src/instrumentation.ts` with a runtime guard so the Node SDK is never imported into the Edge runtime: ```typescript export async function register() { if (process.env.NEXT_RUNTIME === "nodejs") { await import("./instrumentation.node"); } } ``` Create `src/instrumentation.node.ts`: ```typescript 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 Install the browser packages: **npm:** ```bash npm install @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \ @opentelemetry/context-zone @opentelemetry/instrumentation \ @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-fetch ``` **pnpm:** ```bash pnpm add @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \ @opentelemetry/context-zone @opentelemetry/instrumentation \ @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-fetch ``` **yarn:** ```bash yarn add @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \ @opentelemetry/context-zone @opentelemetry/instrumentation \ @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-fetch ``` The browser can't POST directly to `http://localhost:4318` due to CORS. Add a same-origin proxy at `src/app/api/otel/route.ts`: ```typescript 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`: ```typescript "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`: ```typescript import OtelProvider from "./otel-provider"; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {children} ); } ``` `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 Start the app and generate traffic: **npm:** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" npm run dev ``` **pnpm:** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" pnpm dev ``` **yarn:** ```bash export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" yarn dev ``` Open [http://localhost:3000](http://localhost:3000) and click around. Then query the Kopai CLI: **npm:** ```bash # List recent traces npx @kopai/cli traces search --json # Inspect a specific trace (copy a traceId from above) npx @kopai/cli traces get --json ``` **pnpm:** ```bash # List recent traces pnpm dlx @kopai/cli traces search --json # Inspect a specific trace (copy a traceId from above) pnpm dlx @kopai/cli traces get --json ``` **yarn:** ```bash # List recent traces yarn dlx @kopai/cli traces search --json # Inspect a specific trace (copy a traceId from above) yarn dlx @kopai/cli traces get --json ``` With 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 Change the environment variables to point at the cloud endpoint with your API key: ```bash 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 Two complete runnable examples, one per approach: - **[Next.js + @vercel/otel](https://github.com/kopai-app/kopai-integration-examples/tree/main/next-js/vercel-otel)** — minimal server-side setup - **[Next.js + Manual OTel SDK](https://github.com/kopai-app/kopai-integration-examples/tree/main/next-js/manual-sdk)** — server + browser + distributed tracing