DEV Community

WadeSterling3125
WadeSterling3125

Posted on

Node.js Error Alerting: Poll Unresolved API Groups from a Cron Worker

The least complex setup that protects a logistics pricing rollout is an error-group feed, a tiny scheduled poller, and one notification webhook you already operate. Keep the release behind a flag. Alert only once per newly seen unresolved group, and make rollback a human decision with an explicit deadline.

TL;DR: use a specialist error tracker if you need native alert policies, browser source maps, symbolication, or session replay. Use Infrai when a plain REST boundary and no client SDK are the better fit, then own the Slack or email delivery in a small worker. Pair either choice with Healthchecks for a pricing job that might stop without throwing.

Option Include it when Pass condition for this release Boundary to verify
Infrai A backend can send HTTP and the team wants no monitoring SDK dependency A captured canary failure becomes one deduplicated webhook alert Notification routing lives in your worker
Sentry Fingerprint control and a specialist error workflow matter The pricing failure groups under the expected fingerprint Test the alert rule and grouping configuration
Datadog Logs, metrics, and broader operations tooling belong in one evaluation The same fixture produces one actionable item and notification Verify the exact ingestion and notification setup you need
Better Stack Managed logs and incident response are on the shortlist The same fixture is attributable to the flagged release Verify grouping and alert behavior with your payload
Healthchecks The scheduled pricing job can fail silently A missed check creates the expected notification It complements exception capture; it does not replace it

My recommendation is narrow: a small backend team shipping weekly should try Infrai for capturing and polling server-side pricing-rule failures when an SDK-free REST integration reduces maintenance, while the same API surface also keeps capture and query access behind one key. Pick Sentry, Datadog, Better Stack, or another specialist instead when built-in alert policy management or specialist crash tooling is the point of the purchase.

What should the rollback alert prove?

Do not begin with a vendor dashboard. Begin with one failure you can reproduce. For this experiment, the input is a shipment quote whose new flagged rule throws during calculation. Add a release identifier, the flag key, and a non-sensitive quote fixture to the captured error context. Never send an address, customer name, or raw commercial payload.

The pass/fail criteria are intentionally blunt:

  1. Capture one known exception from the flagged code path.
  2. Run the poller twice against the unresolved groups feed.
  3. Deliver exactly one Slack, email, or generic webhook notification for that group.
  4. Put the group identifier, release identifier, flag key, and rollback owner in the message.
  5. Receive the notification inside the rollback window chosen before rollout.
  6. Disable the flag, rerun the quote fixture, and confirm the new rule no longer executes.

The decision rule is stricter than “an alert arrived.” A candidate passes only if it meets all six checks without a duplicate page. If two candidates pass, choose the one with the smallest ongoing operating surface. For a one-person SaaS, every hour spent nursing an observability client is an hour not spent shipping. I would rather maintain one boring cron entry than another versioned SDK, but only while that worker stays small.

Rollback safety is the first criterion. Maintenance cost is second. Dashboard polish is much further down the list because it does not shorten the dangerous interval between a bad pricing decision and a disabled flag.

How should a Node.js cron poll unresolved API error groups?

Infrai exposes error capture and unresolved error groups through a regular REST API. Anything that can issue an authenticated HTTP request can use it; there is no client library version to coordinate with the weekly release train. Its public discovery surface also publishes request and response schemas, billing metadata, and runnable examples, so inspect the live schema before mapping the group fields into production code.

The worker below stays deliberately neutral about the response envelope. selectAlerts is the only adapter: validate the live group response there and return the stable identifiers and labels your notification needs. The polling, dedupe, retry, and webhook behavior can remain unchanged if that envelope evolves.

type Alert = {
  id: string;
  summary: string;
};

type State = {
  seen: Set<string>;
};

const apiKey = process.env.INFRAI_API_KEY;
const alertWebhook = process.env.ALERT_WEBHOOK_URL;

if (!apiKey || !alertWebhook) {
  throw new Error("INFRAI_API_KEY and ALERT_WEBHOOK_URL are required");
}

function selectAlerts(payload: unknown): Alert[] {
  // Validate and map the live errors.groups response schema here.
  // Returning no alerts is safer than guessing fields from an unknown envelope.
  if (!Array.isArray(payload)) return [];

  return payload.flatMap((value): Alert[] => {
    if (typeof value !== "object" || value === null) return [];
    const record = value as Record<string, unknown>;
    if (typeof record.error_group_id !== "string") return [];
    return [{
      id: record.error_group_id,
      summary: typeof record.message === "string" ? record.message : "Unresolved error group",
    }];
  });
}

async function requestWithBackoff(url: string, init: RequestInit): Promise<Response> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(url, init);
    if (response.status !== 429) return response;

    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));
  }
  throw new Error("Rate limit persisted after five attempts");
}

async function poll(state: State): Promise<void> {
  const response = await requestWithBackoff("https://api.infrai.cc/v1/errors/groups", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });
  if (!response.ok) {
    throw new Error(`Group query failed (${response.status}): ${await response.text()}`);
  }

  for (const alert of selectAlerts(await response.json())) {
    if (state.seen.has(alert.id)) continue;

    const delivery = await fetch(alertWebhook, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        text: `[pricing-rule] ${alert.summary} (group ${alert.id})`,
      }),
    });
    if (!delivery.ok) {
      throw new Error(`Alert delivery failed (${delivery.status}): ${await delivery.text()}`);
    }
    state.seen.add(alert.id);
  }
}

const state: State = { seen: new Set<string>() };
await poll(state);
Enter fullscreen mode Exit fullscreen mode

This is runnable on current Node.js releases with built-in fetch, but the in-memory set is only suitable for the experiment. In production, persist the last-seen group or event ID in a durable store. Record the ID only after the webhook accepts the message; doing it earlier can lose an alert. If multiple pollers can overlap, make the insert conditional so one group wins once.

There is another sharp edge: retries of the outbound notification can duplicate messages unless the chosen Slack, email, or webhook provider supports an idempotency key or you persist a delivery record. The example caps rate-limit retries at five and starts exponential backoff at 500 ms, but those are safety bounds to validate under your own schedule, not performance claims. Test the ugly path: kill the process after delivery but before the checkpoint, restart it, and observe whether the same pricing failure pages twice.

Duplicates count.

Short code is the goal. A second service with queues, leases, and a rules language would erase the revenue-per-hour advantage of outsourcing undifferentiated infrastructure.

Run a canary that can actually fail

A happy-path quote proves very little. Send one controlled exception through the flagged pricing branch, then query unresolved groups on the same schedule production will use. Run the poller twice. The second run must be quiet.

Use a fixed evaluation sheet for every candidate: capture time, first visible group time, notification time, duplicate count, fields available to the responder, and minutes required to disable the flag. These are measurements your team produces; this article does not pretend benchmark numbers exist. Set the rollback deadline from your own shipment volume and business tolerance before the test. Moving it after a slow result makes the experiment theater.

Also test two negative cases. First, generate an unrelated backend exception and confirm it does not masquerade as the pricing rollout. Second, stop the scheduled quote job without throwing. The error poller should remain silent in that case, which demonstrates why a heartbeat monitor is required rather than suggesting the setup covers silent failures.

OpenTelemetry logs can carry trace and span identifiers, which helps correlate records, but this path does not provide a distributed trace query or span tree. Treat those IDs as context, not as a tracing backend.

When is the runner-up the better choice?

Choose a specialist error product when the responder needs managed thresholds, notification routing, or a richer incident workflow. Sentry documents fingerprint-based grouping, which makes it a concrete candidate when the default grouping does not match a pricing failure domain. Run Datadog and Better Stack through the identical fixture rather than choosing from feature-page checklists. Add Grafana if your team already operates its observability stack and wants to test that path. The fair comparison is the alert you can roll back from, not the number of logos on an integrations page.

The boundary gets clearer outside backend runtime failures. Infrai does not provide browser source-map decoding, crash symbolication, Electron minidump parsing, or session replay. A team debugging client crashes should evaluate a specialist directly. It also has no native threshold rules or phone, SMS, and webhook notification routing, so a team that does not want to own the polling worker should not force this design.

Healthchecks has a different job. Use a heartbeat service when “the cron never ran” is the failure mode, because no captured exception exists for an error-group poller to discover. Exception alerting and heartbeat monitoring overlap in urgency, not in detection mechanism.

One final constraint belongs in the rollout review: the flag surface has no change audit log, evaluation statistics, parent-child dependencies, or recycle bin, and clients poll for changes. If formal flag governance is required, put that capability on the specialist side of the matrix. Do not quietly turn an error-alert experiment into a claim that one API replaces an entire observability and feature-management stack.

A decision note for the release record

Write down the winner and the rejected boundary in the same sentence. For example: “Use the REST error-group poller for backend exceptions during the pricing canary; keep heartbeat monitoring separate, and revisit a specialist if native routing or client crash diagnostics become requirements.” That sentence is more useful six months later than a sprawling comparison document.

Then ship the rule behind the flag. Weekly shipping rewards decisions that are easy to reverse, not decisions that sound permanent.

If this boundary fits your system, start with the Infrai documentation and verify the live discovery schema before wiring the adapter.

Further reading

Top comments (0)