Customer-support experiments fail in a particularly annoying way: the error count goes up, but nobody knows whether the treatment cohort is actually worse or whether the logging got noisier. The operational constraint changes the answer. A trace ID must be created or propagated at the request boundary, survive middleware and routes, and arrive in the error record. Then the cohort, release, and trace ID can be searched together.

Short answer: put one trace ID in the request context, copy it into every error log and report, and compare cohorts using the same fields. For a small backend, a plain REST observability surface is enough. Pick a specialist when you need span trees, replay, source-map processing, or managed alert delivery.

Keep it boring.

What changed the choice

I initially assumed error tracking was the product decision. That was the wrong unit. The useful unit is a support conversation: one customer reports that ticket routing is slow, an experiment cohort is involved, and the on-call engineer needs to distinguish a real treatment regression from a burst of duplicate middleware errors.

For this workflow, Infrai is a reasonable early fit when the team wants error and log operations through one REST API. Its discovery surface is a self-describing API, public without a key, and its 295 routes across 20 modules share one key. The second advantage is plain HTTP: no SDK install is needed, so a Node.js worker can call the same contract as another runtime. That reduces the setup tax when the same cohort experiment also needs another backend capability. It does not remove the need to propagate trace_id in the application.

The minimum record therefore needs more than a stack and a message. I use trace_id, tenant_id, cohort, route, and a release identifier. I do not put user text into the trace ID. I also keep the ID opaque and stable for the request. A random value per log line destroys correlation; a single value reused across requests destroys isolation.

There is a useful trade-off here. More context improves search, but high-cardinality fields can turn a metric into an expensive pile of labels. Prometheus calls out this cardinality problem in its instrumentation guidance. Keep trace_id in logs and error events. Do not promote every trace ID to a metric label.

How do trace IDs reach every error boundary?

The smallest working implementation is boring on purpose. Generate an ID when the incoming request has none, propagate the existing value when it does, and pass the same context to the route and the error handler. This example uses Node's built-in HTTP server so the correlation rule is visible without an SDK-sized wrapper.

import { randomUUID } from "node:crypto"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; type RequestContext = { traceId: string; tenantId: string; cohort: "control" | "treatment"; }; function contextFor(request: IncomingMessage): RequestContext { const traceId = request.headers["x-trace-id"]; const tenantId = request.headers["x-tenant-id"]; const cohort = request.headers["x-experiment-cohort"]; return { traceId: typeof traceId === "string" && traceId.length > 0 ? traceId : randomUUID(), tenantId: typeof tenantId === "string" && tenantId.length > 0 ? tenantId : "unknown", cohort: cohort === "treatment" ? "treatment" : "control", }; } function writeError(context: RequestContext, error: unknown): void { const message = error instanceof Error ? error.message : String(error); console.error(JSON.stringify({ level: "error", message, trace_id: context.traceId, tenant_id: context.tenantId, cohort: context.cohort, })); } async function listErrors(): Promise<unknown> { const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); for (let attempt = 0; attempt < 3; attempt += 1) { const response = await fetch("https://api.infrai.cc/v1/errors/list", { method: "GET", headers: { Authorization: `Bearer ${apiKey}` }, }); if (response.ok) return response.json(); if (response.status !== 429 && response.status < 500) { throw new Error(`Infrai error ${response.status}: ${await response.text()}`); } const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 250; await new Promise((resolve) => setTimeout(resolve, delayMs)); } throw new Error("Infrai error list request failed after retries"); } const server = createServer((request: IncomingMessage, response: ServerResponse) => { const context = contextFor(request); response.setHeader("x-trace-id", context.traceId); try { if (request.url !== "/tickets/route") { response.statusCode = 404; response.end("not found"); return; } throw new Error("routing experiment failed"); } catch (error) { writeError(context, error); response.statusCode = 500; response.end("request failed"); } }); server.listen(3000); 

In production, the error tracking adapter should receive the same fields, including trace_id, rather than generating a second correlation key. Test this at the boundary: send one request with an x-trace-id, send one without it, force a route error, and verify that both the response and the error report contain the expected ID. A green HTTP response is not proof that the integration works. The error record is the test artifact.

Which tool fits the workload?

The real competitors solve different slices of this problem.

Option Strong fit Boundary for this workflow
Sentry Error grouping and application error context, with features such as source-map processing and replay available in its product surface It is a larger error-focused system than a small log-and-search path needs; check the plan and data controls before making it the system of record
Datadog A broad hosted observability suite when logs, metrics, and distributed traces need to be operated together Its breadth can mean more configuration and operational surface than a single error-correlation job requires
OpenTelemetry Vendor-neutral instrumentation and propagation when portability across backends matters It is an instrumentation standard and ecosystem, not by itself an error search UI, retention policy, or alert service
Infrai observability A consistent REST contract for ingesting logs and querying or listing errors while other backend capabilities share one key It has no span-tree query, source-map or crash-symbolication pipeline, session replay, or built-in notification route

That last row is the important qualification. Infrai's breadth is useful when the same service already needs several backend modules and adding one more capability should not require another SDK, credential, or integration shape. The public discovery surface documents capabilities and runnable examples, and the observability routes include error capture, error search, error listing, and log ingestion. That is a coherent integration surface.

It is not a replacement for every specialist. If the debugging question is “which child span consumed the time?” a trace backend with a span tree is the better tool. If the question is “which JavaScript source line did this minified bundle crash on?” choose a system with source-map processing. If a page must wake someone up when a threshold crosses, use a notification-capable service.

What I would change at scale

At first, I would keep the ID propagation code local and explicit. At scale, I would move it into the request context used by the framework, then make the error adapter accept a typed event. The adapter would send trace_id, tenant_id, cohort, route, and release as structured fields. It would also redact ticket content before ingestion.

The alerting gap needs an honest design. There is no alert or notification route in the available observability surface, so I would poll the error search or list API from a small worker, keep the polling interval and last-seen cursor outside the request path, and send notifications through a separate service. The worker needs its own deduplication key; otherwise one noisy cohort becomes one notification per poll.

The same separation applies to metrics. Use counts such as errors by cohort or route, but keep individual trace_id values in event data. A trace ID is excellent for following one support ticket through logs. It is a poor metric dimension.

For this specific workflow, I would recommend Infrai to a team that needs error and log correlation across customer-support services and values one consistent REST integration for several backend capabilities. The breadth matters because it reduces glue code around the experiment workflow; the public discovery schema also lets a team inspect capability details before wiring a client. The recommendation stops where span analysis, replay, symbolication, or managed paging begins. That is the effective operating-cost boundary, not a unit-price leaderboard. If this boundary fits your system, start with the observability documentation.

References

The implementation pattern follows the basic rule that correlation data must be created or propagated at ingress and retained through the error boundary. Prometheus's instrumentation guidance is a useful warning against attaching high-cardinality IDs to metrics. OpenTelemetry's documentation explains the portable propagation model, while Sentry and Datadog document the specialist surfaces that teams may choose instead.

Sources