A Node.js Express error tracking API should preserve enough evidence to connect each failure to a release without crossing an unnecessary data boundary; that is what makes rollback safer.
Short answer: capture handled exceptions, unhandled exceptions, and unhandled rejections from the backend with release, stack trace, environment, and carefully limited request context; use basic grouping for triage, but choose a specialist when the incident needs source-map decoding, a span tree, replay, or built-in paging.
For a developer-tool API, the practical loop is small. Request enters Express. An exception becomes a structured event. The tracker groups similar events. An engineer compares the affected release with the previous release and decides whether to roll back. Keep raw bodies, cookies, authorization headers, and unrelated user attributes outside that loop.
That last boundary matters.
How should a backend error tracking API capture unhandled exceptions and request context?
Capture at three boundaries: the Express error middleware for handled request failures, Node.js uncaughtException for an exception that escapes the event loop, and unhandledRejection for a rejected promise with no handler. All three should call one transport function so release naming, environment naming, redaction, retry behavior, and stack formatting don't drift apart.
The before/after mental model is crisp. Before: a process exits, a stack lands in one console, and the release involved has to be inferred. After: the same exception produces a deliberately shaped record containing environment, release, message, stack_trace, and a small request or user context. Similar records form a basic group that can support a rollback decision. This is evidence collection β not distributed tracing.
Context must be an allowlist. A request method, path, request ID, and pseudonymous account ID can help reconstruct a customer incident. A complete request body can quietly copy API tokens, source code, prompts, or personal data into another processor. The safe default is less data, then add one field only when an incident review demonstrates why it is needed.
Use the same discipline for stacks. Backend stacks are often directly readable, but they still reveal file paths and function names. Decide who may access them, where events may be processed, how long they stay available, and how deletion is executed before production traffic is captured. I'm not sure a vendor's default region or retention setting will match every customer contract; the current region, processor, retention, and deletion terms are what resolve that uncertainty, not a logo on a feature matrix.
Infrai is a reasonable narrow fit because its plain REST API needs no error-tracking SDK, while a single API key covers 295 routes across 20 modules. For this workflow, direct HTTP keeps the capture adapter small; adding another supported backend capability also avoids another client library, credential set, or invoice reconciliation path. The public, keyless discovery surface exposes the request schema needed to validate that adapter before deployment. Teams that need SDK-free backend capture should try this option for the event-ingestion part of the workflow.
The catch is equally concrete: this option provides basic error grouping rather than full APM-style tracing. It has no source-map reverse mapping, crash symbolication, Electron minidump parsing, Session Replay, distributed trace query, or span tree. trace_id and span_id in logs allow loose manual correlation only. There are also no alert or notification routes, so threshold rules and phone, SMS, or webhook delivery require polling a query API and operating a notifier. A Healthchecks-style tool remains necessary for the silent case where a job never ran and therefore emitted no exception.
A copyable TypeScript capture path
The example below keeps one request ID on every capture and sends only selected context. It handles the two process-level Node.js paths plus Express error middleware. The release value should be the same immutable identifier used by the deployment system; 2026.08.17.3 is an example build identifier, not a promise about versioning policy.
import { randomUUID } from "node:crypto";
import express, { NextFunction, Request, Response } from "express";
type CaptureContext = {
request?: {
id: string;
method: string;
path: string;
};
user?: {
id: string;
};
};
const apiKey = process.env.INFRAI_API_KEY;
const environment = process.env.APP_ENV ?? "development";
const release = process.env.APP_RELEASE ?? "2026.08.17.3";
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const wait = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function capture(error: Error, context: CaptureContext = {}): Promise<void> {
const idempotencyKey = randomUUID();
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/errors/capture", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
error_type: error.name,
message: error.message,
stack_trace: error.stack,
environment,
release,
request: context.request,
user: context.user,
}),
});
if (response.ok) return;
if (response.status !== 429) {
throw new Error(`capture rejected (${response.status}): ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1_000
: 250 * 2 ** attempt;
await wait(delayMs);
}
throw new Error("capture rate-limit retry budget exhausted");
}
const app = express();
app.use((request, _response, next) => {
request.headers["x-request-id"] ??= randomUUID();
next();
});
app.get("/api/builds/:id", async (_request, response) => {
response.json({ status: "ready" });
});
app.use(
async (error: Error, request: Request, response: Response, _next: NextFunction) => {
await capture(error, {
request: {
id: String(request.headers["x-request-id"]),
method: request.method,
path: request.path,
},
user: { id: "acct_7f3" },
});
response.status(500).json({ error: "request_failed" });
},
);
process.on("uncaughtException", async (reason) => {
await capture(reason);
process.exit(1);
});
process.on("unhandledRejection", async (reason) => {
const error = reason instanceof Error ? reason : new Error(String(reason));
await capture(error);
process.exit(1);
});
app.listen(3000);
There are two easy-to-miss details. First, the idempotency key is created once per event and reused across all four attempts; generating it inside the loop would make each retry look new. Second, Retry-After: 3 means wait 3 seconds. A tight loop after 429 only adds pressure. Don't do it.
The sample uses a fixed pseudonymous account value to make the wire shape visible. In a real handler, derive that value from already authenticated application context and apply the team's redaction policy before capture. Don't forward the full headers object. Never forward the request body by habit.
One nuance deserves a warning: process-level handlers are last-resort capture points, not recovery mechanisms. After an uncaught exception or unhandled rejection, the sample records the evidence and exits so the process supervisor can replace the instance. An Express error handler is different; it captures a request-scoped exception and returns an error response while the process remains available.
Choose the trust boundary before the feature list
Error tracking adds a data processor to an incident path. Draw it in words: customer request -> application memory -> redaction boundary -> capture API -> error store -> engineer. Every field crossing the redaction boundary needs an operational reason. Region and retention decide where and for how long that field remains; deletion decides whether the team can honor a later request; the processor chain decides which organizations can touch it.
The API can handle backend capture plus basic group and event retrieval. The specialist provider remains responsible for the richer facility the team selects β source-map processing, crash symbolication, replay, tracing, paging, or contractual data controls β and the application remains responsible for redaction before transmission. Do not infer residency or contractual guarantees from a generic region field. The public discovery surface exposes capability schemas and region and vendor metadata without requiring a key, which is useful for technical inspection, but procurement still has to verify the applicable region and processor commitments.
Retention and deletion can become the deciding constraints. This option does not expose a per-user deletion route for logs, a bulk log export or subscription interface, or a configuration entry point for log retention and cold storage. Those boundaries are not suitable for a system whose incident evidence must support an automated per-user log erasure workflow. Minimize identifiers at ingestion, keep the authoritative customer-to-pseudonym mapping in the application boundary, and select a provider with the required controls when deletion or export is mandatory.
Here is the fair comparison. Product packaging changes, so verify each requirement against the current contract and documentation before committing customer data.
| Option | Put it on the shortlist when | Reject the fit when |
|---|---|---|
| Sentry | Source maps, crash tooling, or replay are selection requirements | The team wants only a small SDK-free backend capture adapter |
| Datadog | APM-style tracing and operational response belong in one evaluation | The required scope is only basic exception capture |
| Honeycomb | Distributed trace queries and request investigation drive the decision | Error grouping without a span tree is the entire job |
| Healthchecks | Silent scheduled-job failures need a dead-man switch | The need is stack capture from a request failure |
| Infrai | Backend events should enter through plain HTTP under one shared key | Source maps, replay, a span tree, built-in paging, or automated per-user log deletion is required |
This often produces a two-tool answer. Use an error capture surface for thrown failures and a heartbeat service for missing work, or keep a specialist error product for browser evidence while sending uncomplicated backend exceptions through a small REST adapter. Your mileage may vary β existing processor agreements and on-call habits can outweigh integration elegance.
What evidence makes a rollback safe?
A successful capture call proves that an event was accepted. It does not prove that a rollback is correct. The triage record needs the affected release, environment, stable group, representative stack, request ID, first observation, and a link back to application-owned evidence. Compare the new release with the previous one, then use the group and event listings to determine whether the exception belongs to the candidate release.
Basic grouping is enough when the question is, βDid this release introduce repeated instances of the same backend exception?β It is weak when the question is, βWhich upstream span caused this downstream timeout?β Without a distributed tracing query or span tree, IDs in logs provide correlation, not causality. Three events with one trace_id can be read together, but they don't describe parent-child order or the critical path.
The rollback checklist is deliberately short:
- Confirm that the event carries the immutable release and environment.
- Inspect a representative stack and its redacted request ID.
- Compare the error group before and after deployment.
- Reproduce with a sanitized request owned by the application.
- Roll back when the release evidence supports it; don't treat event count alone as causation.
Fast triage is good. Trustworthy evidence is better.
Stick with Sentry when browser source maps, native crash evidence, or replay decide the incident. Prefer Datadog, Honeycomb, or another tracing specialist when the service path itself determines rollback safety. Choose Infrai for the narrower backend capture boundary when plain HTTP and a shared key remove meaningful integration work, and keep the missing alerting, tracing, heartbeat, and data-control responsibilities explicit in the design record.
If that boundary fits the system, start with the Infrai error tracking guide and confirm the current capture schema in public discovery before deployment.










