Adopt @vercel/otel — the Phase-B TracerProvider owner
Until now pramaan-app ran no OTel SDK: it only
minted and forwarded a W3C traceparent header (the
pramaan-docs ADR-104 Phase-A bridge in src/proxy.ts). The
Vercel half of every request joined the backend trace through
trace_id in the pino logs — but was never rendered as
spans. This Story turns on Phase-B: it adds the one
src/instrumentation.ts that stands up a real
TracerProvider via registerOTel(), so Next.js
server spans now export with AWS X-Ray-format ids and land in the same
trace as the Lambda.
Why a single instrumentation.ts
pramaan-docs ADR-104 freezes a provider-ownership rule:
exactly one place per surface owns the SDK. Next.js auto-loads the
register() export from src/instrumentation.ts
once at server startup, so that file is the natural — and the
only — home for the provider. ADR-124 ratifies
@vercel/otel as the target. The rule the reviewer should
enforce: no registerOTel, no
TracerProvider construction, no exporter setup anywhere
else (no riders in proxy.ts / gateway.ts).
Phase-A carrying traceparent with no provider was correct
and is left untouched.
What register() sets up
export function register() {
if (isTraceBridgeDisabled()) return; // PRAMAAN_OTEL_DISABLED=1 → no-op
registerOTel({
serviceName: "pramaan-app", // frozen runtime identity
idGenerator: new AWSXRayIdGenerator(), // X-Ray-format trace ids
propagators: ["tracecontext", "baggage"], // W3C carrier (+ pinned default)
traceSampler: "parentbased_traceidratio", // ratio via OTEL_TRACES_SAMPLER_ARG
});
}
| Option | Value | Why |
|---|---|---|
serviceName | pramaan-app | Frozen service.name per ADR-124 §D5 — identifies the deployed runtime that emitted the span. |
idGenerator | AWSXRayIdGenerator | The acceptance criterion. X-Ray ids encode an 8-hex epoch prefix; the X-Ray/CloudWatch drain rejects plain random OTel ids. This makes app spans first-class X-Ray traces. |
propagators | tracecontext + baggage | W3C trace-context is THE end-to-end carrier (ADR-104 carrier table); baggage is the pinned @vercel/otel default. firm_id is never put in baggage — root-span attribute only (ADR-124 guardrail). This file sets no baggage at all. |
traceSampler | parentbased_traceidratio | If proxy.ts minted a traceparent with the 01 sampled flag, that decision is honored; otherwise new roots sample at OTEL_TRACES_SAMPLER_ARG (canonical 0.1 = 10%). Keeps app + Lambda sampling consistent. |
How a request now traces, end to end
- proxy.ts (Phase-A, unchanged) still mints/forwards
traceparent+x-pramaan-correlation-idon the edge and the outbound helper. - instrumentation.ts (Phase-B, new) means the Next.js server work between those points is now a real, exported span — root id in X-Ray format, child of (or root for) the same
trace_id. - Lambda extracts the
traceparentand its server span joins the same trace. Result: one trace in X-Ray / CloudWatch Transaction Search spanning Vercel → API Gateway → Lambda, instead of "logs-only" on the Vercel half.
Kill switch
register() reuses the existing
isTraceBridgeDisabled() helper (src/lib/trace.ts):
PRAMAAN_OTEL_DISABLED=1 makes registration a
no-op — no provider stood up, no spans exported — with
no code redeploy (ADR-104 env matrix). It shares the same switch that
already gates Phase-A traceparent minting, so one env var
turns the whole tracing path off.
semconv check
service.nameset viaserviceName— current, non-deprecated key.- The deprecated bare
deployment.environment(→deployment.environment.nameper ADR-124) is not written here; environment/resource attributes stay env-driven (OTEL_RESOURCE_ATTRIBUTES) per the ADR-104 matrix, so no deprecated key is emitted. - No manual spans, no raw tracer calls — a single
registerOTel()provider setup (no span-per-function).
Verification green
pnpm typecheck— clean.pnpm lint— clean (1 pre-existingno-img-elementwarning inavatar.tsx, unrelated).pnpm build— pass; emits.next/server/instrumentation.js, confirming Next loaded theregister()hook.pnpm test— 532 passed (94 files).- Deps added:
@vercel/otel@2.1.3,@opentelemetry/id-generator-aws-xray@2.1.0,@opentelemetry/api@1.9.1.
Out of scope (later EPIC-1400 tickets)
The signing-gateway exporter wiring (App Runner collector, bearer token,
OIDC), the per-environment OTEL_EXPORTER_OTLP_* Vercel env
vars, correlation_id as a root-span attribute, and the live
staging drain smoke (forcing a sampled traceparent and
confirming the trace renders in Transaction Search) land in their own
Phase-2 tickets. This Story is provider adoption only.
Files changed
3 file(s), +248 / -16 lines
| Path | Role | + | - |
|---|---|---|---|
| package.json | Config | +3 | -0 |
| pnpm-lock.yaml | Config | +198 | -16 |
| src/instrumentation.ts | Source | +47 | -0 |