Next.js setup
This page picks up where the Next.js quickstart stops. Read that first.
Edge runtime
The quickstart uses @opentelemetry/sdk-node, which runs on Node only.
Next.js edge routes (export const runtime = "edge") run on a stripped
Web runtime — sdk-node won't load there.
For edge routes, initialise a fetch-based exporter from within
instrumentation.ts:
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./instrumentation-node")
} else if (process.env.NEXT_RUNTIME === "edge") {
await import("./instrumentation-edge")
}
}// instrumentation-edge.ts
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import {
BasicTracerProvider,
SimpleSpanProcessor
} from "@opentelemetry/sdk-trace-base"
import { resourceFromAttributes } from "@opentelemetry/resources"
const provider = new BasicTracerProvider({
resource: resourceFromAttributes({
"service.name": "web",
"service.version": process.env.NEXT_PUBLIC_APP_VERSION ?? "0.0.0",
"greenslope.release.id": process.env.VERCEL_GIT_COMMIT_SHA ?? "local"
}),
spanProcessors: [
new SimpleSpanProcessor(
new OTLPTraceExporter({
url: "https://ingest.greenslope.io/v1/otel/v1/traces",
headers: { "x-greenslope-key": process.env.GREENSLOPE_INGEST_KEY ?? "" }
})
)
]
})
provider.register()SimpleSpanProcessor is the right choice for edge because the runtime
doesn't have reliable timers for batching.
Middleware
Middleware also runs on the edge runtime. The edge init above covers it. If you only use middleware and no edge API routes, you can skip the Node-side init entirely — but most apps have both.
Server actions
Server actions run in the Node runtime and are covered by the
Node-side instrumentation. They show up as spans named
POST /app/action with the action's name as an attribute.
Manual spans
Auto-instrumentation covers HTTP in and HTTP out. For custom work inside a route, wrap it in a manual span:
import { trace } from "@opentelemetry/api"
const tracer = trace.getTracer("checkout")
export async function POST(req: Request) {
return tracer.startActiveSpan("checkout.process", async (span) => {
try {
const result = await process(await req.json())
span.setStatus({ code: 1 /* OK */ })
return Response.json(result)
} catch (err) {
span.recordException(err as Error)
span.setStatus({ code: 2 /* ERROR */ })
throw err
} finally {
span.end()
}
})
}Vercel deployments
If you're on Vercel, the Vercel integration is a faster path than the manual exporter above — it wires the OTel collector at the platform level and saves you the exporter config.
Use manual instrumentation when you have services outside Vercel that need to share release IDs, or when you want explicit control over the exporter batch behaviour.
Related