Cloud region: This page uses US Cloud hosts. For an EU Cloud project, use https://eu.i.posthog.com instead of https://us.i.posthog.com (ingestion host), and use https://eu.posthog.com instead of https://app.posthog.com (app host for links).
Already using posthog-node on the server? Route handlers, server actions, and server components can create spans with its own span API instead of OpenTelemetry, with no extra packages – see the Node.js guide (opens in a new tab). Either way, spans are server-side only: there is no browser span API.
@opentelemetry/exporter-trace-otlp-proto is the OTLP HTTP/protobuf trace exporter. The similarly named -otlp-http package sends HTTP/JSON and -otlp-grpc sends gRPC, so pick -proto to match this guide.
Note: This step is only needed on Next.js 13.2–14.x. For Next.js 15 and later, instrumentation.ts is enabled by default and the experimental.instrumentationHook option is deprecated — remove it from your config if it’s set.
On Next.js 14 and earlier, add the following to your next.config.js (or next.config.mjs) to enable the instrumentation hook:
import { resourceFromAttributes } from '@opentelemetry/resources'
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
// Create the provider outside register() so it can be exported and flushed in route handlers
export const tracerProvider = new NodeTracerProvider({
resource: resourceFromAttributes({
[ATTR_SERVICE_NAME]: 'my-nextjs-app',
}),
spanProcessors: [
new BatchSpanProcessor(
new OTLPTraceExporter({
url: 'https://us.i.posthog.com/i/v1/traces',
headers: {
Authorization: 'Bearer <ph_project_token>',
},
})
),
],
})
export function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
tracerProvider.register()
}
}
Note: The tracerProvider is created outside of register() so it can be exported and used to flush spans in route handlers. This pattern is necessary because Route Handlers complete execution before batched spans have a chance to be sent to the collector. By exporting the provider, we can manually flush spans at the end of each request.
Note:@opentelemetry/sdk-trace-node only works in the Node.js runtime. If parts of your app use the edge runtime (opens in a new tab) (e.g. middleware), the Next.js OpenTelemetry guide (opens in a new tab) recommends moving Node-only setup into a separate file that register() dynamically imports behind the NEXT_RUNTIME === 'nodejs' check.
Alternatively, you can pass the token as a query parameter:
typescript
new OTLPTraceExporter({ url: 'https://us.i.posthog.com/i/v1/traces?token=<ph_project_token>',})
// Ensure spans are flushed before the serverless function freezes
after(async () => {
await tracerProvider.forceFlush()
})
return Response.json({ success: true })
}
Important: Without calling forceFlush(), your spans may not be sent. When deploying to a serverless platform like Vercel, Route Handlers complete execution before the OpenTelemetry batch processor has a chance to export spans, and the function freezes before the batch is flushed. The after() function from next/server runs code after the response is sent, ensuring spans are flushed before the serverless function freezes.
Note:after() is stable in Next.js 15.1+ (available as unstable_after in 15.0). On Next.js 14 and earlier, it doesn’t exist — flush before returning instead: