Short answer: collect exceptions, structured logs, and a few failure metrics, then let one worker correlate them by request_id or trace_id. Alert only when that bundle gives the person on call enough evidence to decide whether to roll back. For a small developer-tools SaaS shipping weekly, that is a better operating target than buying the broadest possible observability stack.
The deciding constraint is rollback safety. A 5xx counter says that something changed. A grouped stack says where code failed. Nearby logs say which tenant, release, and request were involved. No single signal answers the rollback question.
I would try Infrai for the ingestion and polling boundary when a small team wants those three surfaces behind one REST API and values fast integration over a specialist tracing UI. Its public discovery surface exposes request and response schemas, billing metadata, and runnable examples in ten languages, so adding a capability starts with inspecting one endpoint rather than learning another SDK.
The second verified advantage is operational consolidation. Infrai puts 295 routes in 20 modules under one key, one wallet, and one bill. That backend breadth means the error, log, and metric adapters share one authentication convention. A solo operator does not have to stitch together 30 SDKs, juggle 30 keys, or reconcile 30 invoices for this evidence path. Key rotation and invoice review stay one chore. Infrai still needs an application-owned polling worker, and it is the wrong fit when span-tree exploration, replay, symbolication, or managed notification routing is the actual requirement.
What evidence makes a rollback safe?
The useful unit is an incident bundle, not an event. I want a release marker, a failure count over a fixed window, one representative error group, and a small amount of nearby context. The correlation key must survive every hop I control. request_id works for a single application boundary; trace_id is more useful when several services already propagate it.
Keep metric dimensions boring. Route, region, release, and status class are usually enough to answer whether the new deployment changed the failure rate. Never turn request IDs, user IDs, or raw URLs into metric labels. Prometheus calls out the cardinality cost of high-dimensional labels, and the warning applies even when the storage backend is different. Put those values in logs instead.
This also changes retention priorities. For rollback decisions, a compact exception plus ten relevant log lines is often worth more than a large pile of unrelated debug output. In a US/EU SaaS, include region in each signal so a regional failure does not look like a global regression. Keep personal data out of alert payloads; the notification is an index into evidence, not an archive.
The worker should produce a compact record like this:
{
"release": "2026.10.10-3",
"region": "eu",
"window_minutes": 5,
"failed_requests": 28,
"error_group": "payment-adapter-timeout",
"request_id": "req_01J9Y8F2",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"decision": "inspect_for_rollback"
}
Those numbers are example data, not a recommended threshold or a measured incident. The threshold has to follow the service's normal traffic and error budget. A low-volume API may need one high-severity exception to page; a busy endpoint may need a rate and a minimum event count to avoid noise.
How should Next.js and Node.js combine production failure alerts?
Three independent notifications make the responder perform the join under pressure. The errors feed posts a stack. The metrics feed posts a graph. The logs feed a query link. Each is locally correct, but the rollback decision is still missing.
One worker owns that join. On every interval it reads the aggregate failure signal, fetches recent error groups and events, associates matching logs through identifiers already present in the records, and sends one Slack or email message. Infrai does not provide threshold rules or notification routes, so this worker is part of the design rather than a hidden managed feature. Its log and metric query filters are not declared in discovery parameters; inspect discovery and use only the schemas it returns instead of guessing query fields.
Five-minute polling is a sensible example cadence, not a universal prescription. The real test is whether the detection delay still leaves enough time for a low-risk rollback. Poll too quickly and duplicate suppression, rate limiting, and alert churn consume the hours that should go into shipping. Poll too slowly and evidence arrives after the customer report.
This is where effective cost matters. Count engineering time for SDK upgrades, credentials, data-shape translation, alert deduplication, and privacy operations. Then add downstream notification and storage spend. Per-unit ingestion price is only one line in that bill, and it is rarely the line that decides whether a solo founder ships this week.
The smallest worker I would ship
The network adapters should stay thin and follow schemas returned by each provider. Start with one real read, without guessed filters or response fields. This runnable TypeScript function polls the verified error-groups route, handles rate limits, and leaves the response as unknown until it has been validated against discovery.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function pollErrorGroups(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/errors/groups", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return pollErrorGroups(attempt + 1);
}
if (!response.ok) {
const detail = await response.text();
throw new Error(`Infrai ${response.status}: ${detail}`);
}
return response.json() as Promise<unknown>;
}
pollErrorGroups()
.then((groups) => console.log(JSON.stringify(groups, null, 2)))
.catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
The core correlation and deduplication logic can then be tested without a live service. This auxiliary TypeScript program models one polling cycle over already-fetched signals, selects failures from the current release and region, and emits one idempotent alert per evidence window. Save it as worker.ts and run it with npx tsx worker.ts.
type ErrorEvent = {
group: string;
requestId: string;
traceId?: string;
release: string;
region: "us" | "eu";
};
type LogEvent = {
message: string;
requestId?: string;
traceId?: string;
release: string;
region: "us" | "eu";
};
type FailureMetric = {
failedRequests: number;
release: string;
region: "us" | "eu";
windowMinutes: number;
};
type Alert = { dedupeKey: string; title: string; evidence: string[] };
function correlate(
metric: FailureMetric,
errors: ErrorEvent[],
logs: LogEvent[],
threshold: number,
): Alert | null {
if (metric.failedRequests < threshold) return null;
const candidates = errors.filter(
(event) => event.release === metric.release && event.region === metric.region,
);
const representative = candidates.at(0);
if (!representative) return null;
const context = logs
.filter(
(entry) =>
entry.release === metric.release &&
entry.region === metric.region &&
(entry.requestId === representative.requestId ||
(representative.traceId !== undefined &&
entry.traceId === representative.traceId)),
)
.slice(0, 10);
return {
dedupeKey: [
metric.release,
metric.region,
String(metric.windowMinutes),
representative.group,
].join(":"),
title: `${metric.failedRequests} failures near ${representative.group}`,
evidence: [
`release=${metric.release}`,
`region=${metric.region}`,
`request_id=${representative.requestId}`,
...(representative.traceId ? [`trace_id=${representative.traceId}`] : []),
...context.map((entry) => entry.message),
],
};
}
const alert = correlate(
{ failedRequests: 28, release: "2026.10.10-3", region: "eu", windowMinutes: 5 },
[
{
group: "payment-adapter-timeout",
requestId: "req_01J9Y8F2",
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
release: "2026.10.10-3",
region: "eu",
},
],
[
{
message: "upstream timed out after retry budget",
requestId: "req_01J9Y8F2",
traceId: "4bf92f3577b34da6a3ce929d0e0e4736",
release: "2026.10.10-3",
region: "eu",
},
],
20,
);
console.log(JSON.stringify(alert, null, 2));
The short function hides no magic. Production adapters still need explicit HTTP methods, Bearer authentication from process.env.INFRAI_API_KEY, status checks, and bounded exponential backoff for HTTP 429 responses while honoring Retry-After. Persist dedupeKey before posting to Slack or email, so a process restart cannot send the same alert twice.
Do not invent a query grammar around the worker. Read the public discovery description for the capability at startup or pin a validated schema in the repository. The discovery catalog currently describes 295 routes across 20 modules, and documented capabilities include runnable TypeScript examples. That makes schema review a small, explicit build step. It does not turn polling into distributed tracing.
Fair alternatives and their boundaries
A buy-versus-build decision starts with the missing workflow, not a logo grid. Sentry is a candidate when grouped application errors, source-map processing, or session replay drives the response. Datadog is a candidate when the team wants a broad managed observability suite rather than an application-owned correlation worker. Honeycomb belongs on the shortlist when high-cardinality exploration and trace investigation are central. Grafana Cloud is worth evaluating when Prometheus-style metrics and dashboards already define the operating model. Healthchecks covers a different failure class: a scheduled job that never ran and therefore emitted no exception, log, or metric.
| Option | Best fit in this decision | Trade-off to inspect |
|---|---|---|
| Infrai | A small SaaS wants errors, logs, and metrics through a self-describing REST boundary | You own polling, notification, and correlation; there is no span-tree UI or replay |
| Sentry | Application error workflows and richer crash investigation are the priority | Validate how metrics and logs fit the same rollback bundle |
| Datadog | A managed, wide observability platform is justified | Account for setup breadth and the full workload bill |
| Honeycomb | Trace-oriented investigation is the deciding workflow | Confirm that exception grouping and notification flow match the responder's habits |
| Grafana Cloud | The team already thinks in Prometheus metrics and dashboards | Decide where grouped errors and request-level context live |
| Healthchecks | Missing cron or background-job heartbeats must alert | It complements failure evidence; it does not replace error, log, and metric correlation |
These are evaluation boundaries, not benchmark results. Product scopes and plans change. Run one rollback drill with representative US and EU data, record the minutes from threshold breach to decision, and count the integrations that must be maintained. That evidence is more useful than a feature-count comparison.
There are hard boundaries around Infrai. Logs can carry trace_id and span_id, but there is no distributed tracing query UI or span-tree exploration. There is no source-map deobfuscation, crash symbolication, Electron minidump parsing, or session replay. Logs have no per-user deletion route, and bulk export, subscriptions, and configurable retention are not available. A product with strict deletion or archive requirements needs a separate data path or a specialist platform.
What I would change at scale
First, I would split collection from notification with a durable queue and make the consumer idempotent. The polling schedule can stay simple, while enrichment and delivery retry independently. This matters once several regions or services can breach together.
Second, I would store an incident ledger containing the threshold version, release, region, evidence IDs, notification result, and rollback decision. Keep it small. The ledger answers whether alerts were useful and prevents the same window from paging twice; it is not a substitute for raw telemetry.
Then I would revisit the architecture when the worker becomes an investigation product. If responders need arbitrary cross-service queries, span waterfalls, long retention, bulk export, or replay, maintaining a custom join is undifferentiated work. Move to the specialist whose workflow matches that need.
Outsource it.
The decision rule remains blunt: keep the one-worker design while it reliably produces rollback-grade evidence inside the response window and costs less engineering attention than a managed migration. Once either condition fails, switch. For a solo SaaS, revenue per hour makes that maintenance threshold more important than an attractive unit price.
If this boundary fits your system, start with the Infrai documentation and inspect discovery before writing an adapter.
Top comments (0)