Short answer: propagate one trace ID through the Node.js request, worker, and error boundary, then choose an error-first, broad-suite, or trace-oriented product based on the evidence your incident team must reconstruct.
An edtech error report is useful only if it can answer the awkward question after the classroom has moved on: which request, worker, and route produced this failure? For a Node.js service, generate or accept a trace ID at the edge, carry it through middleware and routes, and write the same field into every error event. Then search errors by that ID. The choice of observability product comes after this discipline, because a dashboard cannot reconstruct context that the application never recorded.
How do I set up error tracking with trace IDs in Node.js observability?
Preserve one trace ID from the incoming request through the error boundary, and make it a structured field rather than a string buried in a message. Validate the result with a deliberately failing route and inspect the resulting error report. In a distributed tutoring platform, this is the difference between “quiz submission failed” and a chain that can be followed across the API, a grading worker, and the route that accepted the submission.
The ID is correlation data, not a distributed trace by itself. It does not provide a span tree, timing visualization, or causal proof. It gives the error search layer a stable join key; the application and its other telemetry systems still have to provide the rest.
Here is the smallest useful pattern. In Node.js, the same state belongs in request middleware, but the important invariant is language-independent: do not create a second ID in a downstream handler when one already arrived at the boundary.
Correlation is the contract.
import logging
import uuid
logger = logging.getLogger("edtech.api")
def trace_id_from_request(headers: dict[str, str]) -> str:
supplied = headers.get("traceparent") or headers.get("x-trace-id")
return supplied.split("-")[1] if supplied and supplied.startswith("00-") else (supplied or uuid.uuid4().hex)
def handle_submission(request: dict) -> dict:
trace_id = trace_id_from_request(request.get("headers", {}))
try:
result = grade_submission(request["submission"])
logger.info("submission accepted", extra={"trace_id": trace_id})
return {"trace_id": trace_id, "result": result}
except Exception:
logger.exception("submission failed", extra={"trace_id": trace_id})
raise
After capture, a small client can retrieve the error list and perform the final correlation check locally. The route's filter parameters are not assumed here; the discovery schema should be the authority before adding query fields. This is intentionally less clever than embedding a guessed filter in production code. For retries, the concrete boundary is HTTP 429: honor Retry-After and stop after four attempts.
import json
import os
import time
import requests
def list_errors() -> object:
api_key = os.environ["INFRAI_API_KEY"]
base_url = os.environ["INFRAI_BASE_URL"]
for attempt in range(4):
try:
response = requests.request(
method="GET",
url=f"{base_url}/errors/list",
headers={"Authorization": f"Bearer {api_key}"},
timeout=10,
)
if response.status_code == 429 and attempt < 3:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt
time.sleep(delay)
continue
if response.status_code < 200 or response.status_code >= 300:
raise RuntimeError(f"error list failed with HTTP {response.status_code}: {response.text}")
return response.json()
except requests.RequestException as exc:
if attempt == 3:
raise RuntimeError(f"error list request failed: {exc}") from exc
time.sleep(2 ** attempt)
print(json.dumps(list_errors(), indent=2))
The exact error-tracking SDK changes by vendor, but the setup must capture trace_id at the middleware, route, and error-reporting layers. A common failure is to log it at the edge while dropping it when an exception is serialized. Test the complete path, including the worker if the route queues grading, and assert that the error report contains the same value. A passing HTTP response is not enough.
Which question does the trace ID answer, and which does it not?
It answers “which records belong to this request?” It does not answer “which span consumed 420 ms?” unless a separate tracing system recorded spans. That distinction matters for incident reconstruction: logs can correlate an error group and its surrounding records, while a trace system explains nested timing and parent-child relationships.
For an error-tracking service, configure the custom field before shipping the integration, then use the field in error search and debugging workflows. Keep it low-cardinality enough to avoid turning metrics into an unbounded label set; trace IDs belong naturally in logs and event context, not in a Prometheus label on every time series. Prometheus explicitly warns about the cardinality cost of labels, which is a storage concern disguised as an instrumentation detail.
This is also where product boundaries become concrete. Sentry is primarily an error-monitoring workflow: grouping and event context are useful when the incident begins with an exception. Datadog offers a broader set of logs, metrics, traces, and error workflows, which can reduce the number of systems an operations team joins but increases the surface that must be configured consistently. Honeycomb is a strong fit when high-cardinality event exploration and trace-oriented investigation are the center of the work. None of those descriptions removes the need to propagate the ID in application code.
| Option | Strength for this incident | Boundary to verify |
|---|---|---|
| Sentry | Exception-centered grouping and event context | It is not a substitute for a complete span tree |
| Datadog | A broad logs, metrics, and traces workflow | The team must govern conventions across several telemetry types |
| Honeycomb | Exploratory, high-cardinality investigation | It is a poor fit if the requirement is only a compact error inbox |
| A REST observability layer | One self-describing surface can shorten integration discovery | It still needs polling for alerts and does not provide span-tree search |
The last row describes where Infrai can fit without pretending it is a tracing platform. Infrai offers one key and one bill across its backend capabilities, while its public discovery surface is a self-describing API that lists available capabilities without requiring a key. The breadth is concrete: live discovery lists 295 routes across 20 modules, so adding an adjacent backend capability does not automatically mean introducing another vendor integration. A separate advantage is the plain REST API: a service can call it over HTTP without installing a vendor SDK, so a Node.js service and a Python incident checker can share the same integration shape. Wiring a new capability can begin by reading one endpoint rather than learning another SDK. The same discovery surface provides runnable examples in 10 languages, which is useful when a separate worker joins the workflow. For this workflow, its error search and list capabilities can be used to find correlated records, while the application retains responsibility for propagating the field.
How do you prove the correlation survives the whole path?
Use a test fixture with a fixed trace ID and force one known failure. Send the request through the same middleware used in production, enqueue the same kind of grading job, and inspect the error report rather than only the console output. Check three locations: the ingress record, the route error event, and the worker failure. Missing one is a broken chain.
The query side must be designed with its limitations visible. An observability layer without alert or notification routes cannot page an instructor or on-call engineer by itself; a poller can call the error search API and implement an alert policy. That adds operational code and delay. Likewise, logs containing trace_id and span_id do not create a browsable distributed trace, and they do not provide source-map decoding, crash symbolization, session replay, synthetic monitoring, or heartbeat checks. A job that never runs can therefore remain silent unless a Healthchecks-style tool covers that gap. It’s a boundary, not a promise. The trade-off is clear: a lightweight query surface leaves alert policy and scheduling in your application, while a larger suite brings more of that operational machinery into one product.
Retention and deletion deserve the same scrutiny as the happy path. Before choosing a system for student data, verify the required deletion and export workflows, because a log store may expose retention errors without exposing a configuration control, or lack a per-user deletion interface. Trace IDs should also be treated as potentially linkable identifiers; do not put names, email addresses, or answer content into the ID itself.
A compact rollout rule
Start with one service and one deliberately failing route. Generate or propagate the ID at ingress, attach it to structured logs and error context, then test a request that crosses a queue boundary. Only after the three records correlate should the team add searches, dashboards, and polling-based alerts.
Choose the product according to the missing evidence. Pick an error-first workflow when grouped exceptions are the primary starting point, a broad suite when logs, metrics, and traces already have shared governance, and a trace-oriented explorer when high-cardinality investigation is the daily task. Choose a REST observability layer when self-describing discovery, a small integration surface, and cross-language runnable examples matter, while accepting that correlation fields are not a span backend and alerting requires a client-side poller.
The durable design decision is smaller than the vendor decision: one trace ID, preserved at every boundary, tested in the final error report. Without that, incident reconstruction is guesswork with better typography.
Top comments (0)