ADR-0004 — @vercel/otel pinned to 1.x on Next.js¶
Status: Accepted Date: 2026-04-16 Author(s): Platform engineering
Context¶
Browser and SSR traces from ebit-fe need to reach the OTel Collector alongside the NestJS backend spans. @vercel/otel provides the registerOTel() helper that wires the Node.js OTel SDK inside Next.js's instrumentation.ts hook, including automatic fetch span creation and traceparent injection on outbound requests.
At the time of integration, OpenTelemetry JS had two major release lines: 1.x (@opentelemetry/sdk-trace-web@^1.30, exporters/instrumentations at ^0.57) and the emerging 2.x line. @vercel/otel@1.13.0 declares peer dependencies on the 1.x line. Installing any 2.x OTel package alongside it causes peer-dep warnings at install time and, worse, silent SSR trace shadowing at runtime — two competing TracerProvider registrations fight over the global, and the loser's spans are dropped without error.
A second complication: Sentry v9 (@sentry/nextjs@^9.11.0) is also OTel-based. Sentry.init() grabs the global tracer provider on import, even when the DSN is empty. If the Sentry server config is imported before registerOTel(), Sentry's provider wins and @vercel/otel spans are silently lost.
Decision¶
-
Pin all OTel browser and SSR packages to the 1.x/0.57.x line.
ebit-fe/package.jsonlocks@vercel/otelat1.13.0,@opentelemetry/apiat1.9.0,@opentelemetry/sdk-trace-webat^1.30.1, and all instrumentations/exporters at^0.57.2. No2.xpackages are permitted until@vercel/otelofficially supports them. -
Gate the Sentry import on
NEXT_PUBLIC_SENTRY_DSN. Inebit-fe/src/instrumentation.ts:7-12, theawait import('../sentry.server.config')is only executed when a DSN is configured. Otherwise,registerOTel()runs and owns the global provider. This prevents the silent provider conflict in local dev and compose environments where Sentry is not needed. -
Require
propagateContextUrls: [/.*/]inregisterOTel. Atebit-fe/src/instrumentation.ts:18-22, the fetch instrumentation config injectstraceparenton all outbound requests, not just same-origin. Without this, cross-serviceebit-fe → ebit-apispans orphan in Jaeger.
Alternatives considered¶
-
Upgrade to OTel JS 2.x. Rejected:
@vercel/otel@1.xis the current stable release compatible with Next.js 14. A 2.x upgrade requires waiting for Vercel to publish a compatible version. Mixing 1.x and 2.x providers produces silent data loss. -
Drop
@vercel/oteland wire the OTel SDK manually. Rejected:registerOTelhandles the Next.jsinstrumentation.tslifecycle correctly (server-only registration, proper shutdown hooks, fetch monkey-patching that respects Next.js's patchedfetch). Reimplementing this is fragile and couples us to Next.js internals. -
Use Sentry as the sole trace provider (no
@vercel/otel). Rejected: in local dev and compose, there is no Sentry DSN. Traces would only exist in production. The two-provider gate gives us OTel traces in every environment — Sentry in prod (when DSN is set),@vercel/otel→ Collector locally. -
Allow floating version ranges (
^1.13.0). Rejected for@vercel/otelspecifically: a minor bump could pull in a transitive 2.x dep. Exact pinning prevents surprise breakage onpnpm install.
Consequences¶
pnpm add @opentelemetry/anythinginebit-femust be verified against the 1.x peer matrix before merging. A CI check or Renovate grouping rule would prevent accidental 2.x introduction.- The Sentry gate means local dev never sends error events to Sentry. This is acceptable — Sentry is a production concern; local errors surface in console/Jaeger.
- If Vercel ships
@vercel/otel@2.x, all OTel deps inebit-fe/package.jsonmust be upgraded as a single atomic PR. The pinning strategy makes this a deliberate migration, not an accidental drift.
References¶
ebit-fe/src/instrumentation.ts:7-24— Sentry gate +registerOTelwithpropagateContextUrlsebit-fe/package.json:18-26— OTel 1.x dependency versionsebit-fe/package.json:42—@sentry/nextjs@^9.11.0ebit-fe/package.json:47—@vercel/otel@1.13.0