# 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 (