# Otel Nextjs Style

> Next.js/Vercel OpenTelemetry style: instrumentation.ts, @vercel/otel bootstrap, native @opentelemetry/api call sites, inline public ingest token, and no raw NodeSDK replacement.

- Skill: `superloglabs/otel-nextjs-style` (Agent Skill)
- Install (CLI): `npx skillmds add superloglabs/otel-nextjs-style`
- Raw SKILL.md: https://api.skillmd.com/api/skills/superloglabs/otel-nextjs-style/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: superloglabs (https://skillmd.com/u/superloglabs)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/superloglabs/otel-nextjs-style

---


# OTel Next.js Style

For Next.js apps, prefer the framework entrypoint.

```ts
// instrumentation.ts
import { registerOTel } from "@vercel/otel";

export function register() {
  registerOTel({
    serviceName: "mugline-web",
  });
}
```

Do not replace this with a custom `NodeSDK` bootstrap unless the repo is not a
normal Next/Vercel app or already has a custom provider that must be extended.

For JavaScript/TypeScript LLM providers, prefer provider instrumentation over
manual child spans. For Anthropic, add OpenInference in the same bootstrap and
keep call sites native. This example uses `@vercel/otel@2.x`; if the installed
types are v1, use `logRecordProcessor` singular instead.

```ts
import Anthropic from "@anthropic-ai/sdk";
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
import { registerOTel } from "@vercel/otel";

const anthropicInstrumentation = new AnthropicInstrumentation({
  traceConfig: {
    hideInputs: true,
    hideOutputs: true,
  },
});

anthropicInstrumentation.manuallyInstrument(Anthropic);

export function register() {
  registerOTel({
    serviceName: "mugline-web",
    instrumentations: [anthropicInstrumentation],
    logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
  });
}
```

## Route Handlers

Use native OTel APIs where auto-instrumentation is blind.

```ts
import { withSpan } from "@superlog/otel-helpers";

const tracer = trace.getTracer("mugline.web");
const meter = metrics.getMeter("mugline.web");
const requests = meter.createCounter("mug.copy.generated");

export async function POST(request: Request) {
  const tenantId = request.headers.get("x-tenant-id") ?? "tenant_demo";
  return await withSpan("mug.copy.generate", async (span) => {
    span.setAttribute("tenant.id", tenantId);
    requests.add(1, { "tenant.id": tenantId, outcome: "success" });
    return Response.json({ ok: true });
  }, { tracer });
}
```

For TypeScript route handlers, use `@superlog/otel-helpers` `withSpan` for
bounded business spans and add `@superlog/otel-helpers` to `package.json` when it
is not already present. This is required when the package can be installed. It
keeps span lifecycle/error handling out of the handler body and avoids a large
indentation diff. Do not expand the whole route into
`tracer.startActiveSpan(...)` plus `try` / `catch` / `finally` unless the helper
cannot be added or the span has a true cross-callback lifecycle.

If a route has an LLM call and OpenInference/provider instrumentation supports
that SDK, do not wrap the provider call. Leave `client.messages.create(...)` /
equivalent in place and put business context on the active product span or
structured log. Do not duplicate provider/model/token attributes in route-level
spans, logs, or metrics when OpenInference already reports them. Do not calculate
LLM cost in route handlers; Superlog derives estimated cost in the UI/query layer
from OpenInference provider/model/token attributes.
For Anthropic in Next.js/ESM, keep the instrumentation instance and
`manuallyInstrument(Anthropic)` call at module scope so it runs once and before
route code.

Match the `@vercel/otel` logs option to the installed package/types:
`@vercel/otel@1.x` uses `logRecordProcessor` singular, while
`@vercel/otel@2.x` uses `logRecordProcessors` plural. For normal
Next.js/Vercel apps, do not guard
`registerOTel(...)` behind `NEXT_RUNTIME`; Next calls `instrumentation.ts` in
the appropriate runtime and `@vercel/otel` handles its own runtime differences.

`console.info` is not OTLP log export. If there is no existing logger bridge,
use `@opentelemetry/api-logs` for production log records. Remove pre-existing
`console.*` calls that duplicate the same structured OTel log event:

```ts
logger.emit({
  severityNumber: SeverityNumber.INFO,
  severityText: "INFO",
  body: "generated mug copy",
  attributes: {
    "tenant.id": tenantId,
    "gen_ai.provider.name": "anthropic",
    "gen_ai.request.model": model,
    "app.gen_ai.use_case": "web.mug_copy",
    outcome: "success",
  },
});
```

## Configuration And Smoke

Use the source-level public Superlog configuration pattern from
`otel-onboarding-style` inside `instrumentation.ts`, then pass those constants
explicitly to `registerOTel`. The public project token is write-only and belongs
with the endpoint in the setup block, like a PostHog project token or Sentry
DSN.

```ts
const SUPERLOG_ENDPOINT = "https://intake.superlog.sh";
const SUPERLOG_PUBLIC_TOKEN = "sl_public_...";

// The token MUST be sent as the `x-api-key` header. Ingest only reads
// `x-api-key` or `Authorization: Bearer <token>`; any other header name 401s.
function superlogHeaders(token: string): Record<string, string> {
  return { "x-api-key": token };
}

registerOTel({
  serviceName: "mugline-web",
  traceExporter: new OTLPTraceExporter({
    url: `${SUPERLOG_ENDPOINT}/v1/traces`,
    headers: superlogHeaders(SUPERLOG_PUBLIC_TOKEN),
  }),
  // …same shape for log + metric exporters
});
```

Smoke checks should use tools already in the repo, e.g. `npm run typecheck` or
`npm run build`, plus a real app request where practical. Do not invent fragile
inline Node scripts that import TypeScript source files directly, and do not
assume `ts-node` exists unless it is already installed.

