Skip to main content
GreenSlope

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