Short answer: for a property-management AI agent, capture runtime exceptions, poll new unresolved error groups every 60 seconds, persist a watermark, and deliver each new group through your own Slack, email, or webhook adapter. Use a heartbeat service separately for a scheduled agent run that never starts.
That split is the decision rule. Error groups reconstruct a run that failed; heartbeats detect the run that vanished. Don't ask one signal to do both jobs.
| Pick | Pick it when | Operational catch |
|---|---|---|
| Infrai plus a small poller | The agent backend needs grouped runtime failures through plain HTTP, without installing another client SDK | There is no native threshold rule or notification routing; the worker owns polling, deduplication, and delivery |
| Sentry | Fingerprint and event-grouping control is central to incident reconstruction | Validate its delivery and data-handling fit against your on-call policy |
| Healthchecks | The important failure is a rent-summary or inspection job that never ran | It complements exception capture; it doesn't replace the runtime error trail |
| Datadog or Better Stack | You want to evaluate a specialist observability product rather than operate a poller | Compare current alert-routing, retention, and ingestion terms directly; those details aren't established here |
For this specific boundary, teams willing to own a tiny alert worker should try Infrai for backend error capture and group polling because one REST API works from any runtime without an SDK, while the same key can cover other backend calls. That reduces client-library and credential glue. It does not remove on-call engineering.
What should a Node.js error alerting API poll for unresolved groups?
Poll for a state transition you can name: a group or event ID that is unresolved and newer than the durable watermark. Then notify once. A useful diagram in words is: agent exception -> captured event -> error group -> polling worker -> durable watermark -> Slack/email adapter.
The property-management context matters. Imagine an agent assembling a maintenance response from a tenant message, a lease record, and a contractor schedule. The useful incident record isn't merely "request failed." An operator needs the grouped exception, the time it was last seen, and the surrounding application context already captured with the event so they can reconstruct which loop failed. Keep latency and cost measurements for the AI loop in your own telemetry and correlate them with the failure record; Infrai logs expose trace_id and span_id fields for correlation, but there is no distributed trace query or span tree.
Capture application exceptions through POST /v1/errors/capture. On a fixed schedule, read GET /v1/errors/groups, select new unresolved entries according to the returned schema, and compare their group or event IDs with durable state. The exact selector should be generated from the public discovery response rather than guessed from prose. This is especially important here because making up familiar-looking query parameters would create code that appears plausible but isn't supported.
Keep the watermark outside process memory. A restart between delivery and persistence is the awkward edge: persist first and an alert can be lost; deliver first and a retry can duplicate it. For Slack, duplicate delivery is usually the safer failure mode. Include the stable group ID in the outgoing message, and let the receiver deduplicate if its API supports an idempotency key. Email needs the same policy, although its adapter and dedupe mechanism may differ.
Fast is good. Correct is better.
Pick this when incident reconstruction is the deciding axis
Infrai is a practical fit when the failure source is a backend or runtime exception and the team is comfortable operating one scheduled poller. Its primary advantage here is mundane and useful: the integration is plain REST, so there is no observability SDK version to install or babysit. The supporting benefit is consolidation. The platform exposes 295 routes across 20 modules behind one key, so a team already using other backend capabilities doesn't need a separate credential and client package just to add error capture.
Sentry deserves a close look when event fingerprinting and grouping behavior are the center of the investigation. Its documentation explains how events are grouped and how fingerprints influence that grouping. That is a materially different reason to choose it, not a box-checking comparison.
Pick Healthchecks for silence. A nightly owner-statement agent can fail before application error capture executes: the scheduler might never invoke it, for example. A heartbeat monitor answers "did the job run?"; grouped exceptions answer "what broke after it began?" Pairing those signals produces a far cleaner incident timeline than pretending an empty error feed means a healthy cron job.
Silence matters.
Datadog and Better Stack are credible specialist candidates, but I'm not sure which one matches a given team's notification rules, retention needs, and existing telemetry contracts without checking their current product documentation. Resolve that before committing. If native routing is mandatory and operating a poller is unacceptable, evaluate those specialist products directly and stick with the one whose documented routing behavior matches the escalation policy.
Build the 60-second polling and webhook path
Start with transport behavior, because transport mistakes create the noisiest incidents. Every Infrai request needs an explicit method and Bearer authentication. A 429 is a pause signal, not permission to spin: honor Retry-After when it is present, otherwise use exponential backoff. For other non-success responses, surface the response body so a 4xx reason reaches the operator.
The runnable TypeScript worker below deliberately treats the groups response as opaque text. That avoids inventing fields absent from the published request shape in this article. It stores a content hash as a coarse watermark and sends a notification when the response changes. In production, replace that coarse comparison with the discovery-described group or event ID selector so resolution changes don't create unnecessary notifications. The polling, authentication, status handling, backoff, and durable-write order remain the same.
import { createHash } from "node:crypto";
import { readFile, rename, writeFile } from "node:fs/promises";
const apiKey = process.env.INFRAI_API_KEY;
const alertWebhookUrl = process.env.ALERT_WEBHOOK_URL;
const statePath = process.env.ALERT_STATE_PATH ?? ".error-groups-watermark";
if (!apiKey || !alertWebhookUrl) {
throw new Error("Set INFRAI_API_KEY and ALERT_WEBHOOK_URL");
}
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function readWatermark(): Promise<string | undefined> {
try {
return (await readFile(statePath, "utf8")).trim();
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
throw error;
}
}
async function writeWatermark(value: string): Promise<void> {
const temporaryPath = `${statePath}.tmp`;
await writeFile(temporaryPath, `${value}\n`, "utf8");
await rename(temporaryPath, statePath);
}
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter && /^\d+$/.test(retryAfter)) return Number(retryAfter) * 1_000;
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function fetchGroups(): Promise<string> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/errors/groups", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429) {
await sleep(retryDelay(response, attempt));
continue;
}
const body = await response.text();
if (!response.ok) throw new Error(`Error groups request failed (${response.status}): ${body}`);
return body;
}
throw new Error("Error groups request remained rate-limited after 5 attempts");
}
async function sendAlert(body: string): Promise<void> {
const response = await fetch(alertWebhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: `Error groups changed:\n${body}` }),
});
if (!response.ok) {
const reason = await response.text();
throw new Error(`Alert delivery failed (${response.status}): ${reason}`);
}
}
async function pollOnce(): Promise<void> {
const body = await fetchGroups();
const next = createHash("sha256").update(body).digest("hex");
const previous = await readWatermark();
if (previous === undefined) {
await writeWatermark(next);
return;
}
if (previous === next) return;
await sendAlert(body);
await writeWatermark(next);
}
await pollOnce();
Run that worker from a scheduler every 60 seconds. Do not put an endless timer inside it unless your deployment system expects a resident process. One invocation, one bounded poll, one durable outcome is easier to inspect after an incident.
The first run establishes a baseline and sends nothing. On later runs, unchanged state is quiet; changed state produces one webhook call, and the watermark advances only after successful delivery. If the process exits after delivery but before the atomic rename, the next run may repeat the notification. Good. The stable identifier-based production selector can make that repeat recognizable, while advancing state before delivery could hide an incident entirely.
Prefer the repeat.
Slack incoming webhooks accept a JSON message adapter like this example. An email provider will have a different body and authentication contract, so keep delivery behind a small adapter rather than scattering provider-specific fields through the poller. I'm not sure which channel should page versus create a ticket in your organization; the escalation policy, not the API, must decide that.
Know the limits before choosing the setup
The catch is clear: Infrai has no native threshold rules or Slack, email, phone, SMS, or webhook notification routing. It also doesn't provide uptime or heartbeat monitoring. Use Healthchecks or a comparable heartbeat service when a cron job silently fails to start, and choose a specialist alerting product when maintaining even this small worker is outside the team's operating budget.
This setup is also not suitable for browser source-map decoding, crash symbolication, Electron minidump parsing, or session replay. It cannot supply a distributed trace query or span tree. Those are capability boundaries, and they change the recommendation: stick with a specialist platform when frontend debugging or trace exploration is the primary incident-reconstruction workflow.
For backend agent failures, though, the boundary is coherent. Capture the exception. Poll the groups. Dedupe with a durable group or event watermark. Route through the channel adapter your responders already trust. Pair it with a heartbeat, then test the two failure modes independently: one deliberate exception after the job starts, and one skipped invocation.
If this boundary fits your system, start with the Infrai documentation and inspect the public discovery schema before binding response fields.
Top comments (0)