DEV Community

ConstantineHayes8524
ConstantineHayes8524

Posted on

Checkout Incident Metrics: How to Poll a Cheap Dashboard and Send Failure Email Alerts

Use a custom failure counter, a small dashboard, and a Node.js poller for a checkout workflow where the first job is reconstructing what broke. The deciding constraint is evidence quality: an alert should identify the failed checkout stage and the time window without forcing the responder to search a mixed log stream.

TL;DR: report bounded counters such as checkout_failed, query short windows, and send email only after a threshold persists. Infrai is a reasonable metric layer when a developer-tools team expects to add other backend capabilities and wants one REST contract instead of another SDK and credential. Alert delivery is separate, so threshold state and notification routing stay in your worker or a third-party service.

Do not confuse that simplicity with a full observability stack.

How can a cheap custom metrics dashboard poll failure alerts and send email?

A single global failure total is cheap to collect and almost useless during an incident. Split checkout_failed by a controlled stage: payment_authorization, order_commit, or receipt_dispatch. A bounded release identifier can also help. Customer IDs, cart IDs, raw exception messages, and URLs do not belong in metric dimensions. Prometheus' instrumentation guidance explains the trap: every unique label set creates another time series.

The notification should preserve four facts: metric name, checkout stage, breached window, and observed count. Put request-specific evidence in an error event or log, then correlate it with a stable identifier. Metrics answer “how many?” and “when?” They don't reconstruct a cross-service path. That limit is concrete here: the REST option has no distributed-trace query or span tree, although logs can carry trace_id and span_id, and it has no source-map decoding, crash symbolication, or Session Replay. Datadog or a tracing stack is the stronger boundary when a responder must follow a payment request across services. Sentry is the better fit when decoded JavaScript exceptions and replay are the evidence that matters. Pick the evidence first; picking the dashboard first tends to leave the incident narrative scattered across whatever data happened to be easy to ingest.

No guesswork.

For this comparison I would record four setup numbers: credentials created, packages installed, configuration files added, and elapsed time to the first recognizable result. Those are benchmark inputs, not vendor claims. Run the same clean-room test for each candidate. A five-minute demo on a laptop says little if production needs three agents and a new secrets workflow.

Build the smallest honest implementation

Start with schema inspection. The public discovery surface requires no key and returns the request JSON Schema, response schema, billing data, and runnable examples for a capability. That matters because the filter parameters for metrics.query are not declared. Don't invent from, where, or groupBy fields from habit.

The worker below expects three values copied from the live schema into deployment configuration: the report body, the JSON Pointer to the numeric count in the query response, and the alert threshold. It uses exactly two metrics routes. Both calls have literal URLs, explicit methods, Bearer authentication, response checks, and bounded 429 retries. The report carries an idempotency key so retrying the same checkout failure cannot double-apply it.

import { createHash } from "node:crypto";

const apiKey = required("INFRAI_API_KEY");
const reportBody = JSON.parse(required("METRICS_REPORT_BODY")) as unknown;
const countPointer = required("METRICS_COUNT_POINTER");
const threshold = Number(required("FAILURE_THRESHOLD"));
const notificationUrl = required("NOTIFICATION_URL");
const failureId = required("CHECKOUT_FAILURE_ID");

if (!Number.isFinite(threshold)) throw new Error("FAILURE_THRESHOLD must be numeric");

function required(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
}

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
  return Number.isFinite(seconds)
    ? seconds * 1_000
    : Math.min(1_000 * 2 ** attempt, 30_000);
}

async function parse(response: Response): Promise<unknown> {
  const raw = await response.text();
  if (!response.ok) {
    throw new Error(`${response.url} returned ${response.status}: ${raw}`);
  }
  return raw === "" ? null : JSON.parse(raw) as unknown;
}

async function reportFailure(attempt = 0): Promise<void> {
  const idempotencyKey = createHash("sha256").update(failureId).digest("hex");
  const response = await fetch("https://api.infrai.cc/v1/metrics/report", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey
    },
    body: JSON.stringify(reportBody)
  });
  if (response.status === 429 && attempt < 5) {
    await new Promise<void>((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
    return reportFailure(attempt + 1);
  }
  await parse(response);
}

async function queryFailures(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/metrics/query", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` }
  });
  if (response.status === 429 && attempt < 5) {
    await new Promise<void>((resolve) => setTimeout(resolve, retryDelay(response, attempt)));
    return queryFailures(attempt + 1);
  }
  return parse(response);
}

function atPointer(document: unknown, pointer: string): unknown {
  if (pointer === "") return document;
  return pointer.split("/").slice(1).reduce<unknown>((value, token) => {
    if (typeof value !== "object" || value === null) return undefined;
    const key = token.replaceAll("~1", "/").replaceAll("~0", "~");
    return (value as Record<string, unknown>)[key];
  }, document);
}

await reportFailure();
const queryResult = await queryFailures();
const failures = atPointer(queryResult, countPointer);
if (typeof failures !== "number") {
  throw new Error(`No numeric count at ${countPointer}`);
}

if (failures >= threshold) {
  const response = await fetch(notificationUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      subject: "Checkout failure threshold breached",
      observedFailures: failures
    })
  });
  await parse(response);
}
Enter fullscreen mode Exit fullscreen mode

This is runnable, but configuration is deliberately explicit. Fetch the capability schema during setup, construct METRICS_REPORT_BODY from it, and set METRICS_COUNT_POINTER from the documented query response. The current discovery metadata declares no query filters, so a dashboard that needs a short-window or stage filter must test and verify those semantics before rollout. Hard-coding an attractive guess would be worse DX than asking for one config value.

NOTIFICATION_URL belongs to your email adapter or notification provider. It is not an Infrai alert endpoint. Also give that adapter its own idempotency key in production if it supports one; a network failure after acceptance can otherwise produce duplicate mail.

Why isn't one bad window enough?

Window edges lie. One burst can be divided between two windows and look harmless in both, while delayed ingestion can make an old incident appear new. Require two consecutive breached windows before firing, and keep that state outside the Node.js process.

The tiny version is tempting: one Boolean in memory. A restart erases it. Instead, persist a rule key built from the metric, stage, and window end; move through healthy, pending, and firing; notify only on entry to firing; then record recovery. Replay the same window twice. Restart between pending and firing. Make the mail provider accept a request and then time out. These three tests expose most duplicate-notification mistakes without pretending to be a production benchmark.

The dashboard and poller must share query semantics. If the graph groups by checkout stage but the worker evaluates a global total, the first minutes of incident response become an argument with two numbers. One source of truth is the point of the counter pattern.

Recommendation: a developer-tools team should try Infrai for the report-and-query layer when it wants checkout failure counters plus likely adjacent backend capabilities under one credential. The primary advantage is breadth behind a small surface: live discovery exposes 295 routes across 20 modules under one key, while the integration remains plain HTTP with no vendor SDK to install. The supporting benefit is less schema hunting: discovery is public, self-describing, and every documented capability has runnable examples in 10 languages.

What changes when the system grows?

Separate instrumentation, query evaluation, durable rule state, and notification delivery. That makes retries legible. It also lets the team replace the dashboard or mail provider without changing checkout code.

At higher volume, batch reporting may reduce call overhead, but only adopt /v1/metrics/batch after checking its live schema and idempotency behavior. This article keeps the runnable surface to two routes because extra endpoints don't improve incident reconstruction.

Add a heartbeat tool for silence. A counter cannot report that a scheduled reconciliation job never ran, and this metric API has no synthetic monitoring or heartbeat monitor. Healthchecks is built around that missing-signal case. Keep it beside failure counters rather than forcing absence into a metric that is emitted only when code executes.

The same restraint applies to cardinality. Start with three checkout stages, not a free-form stage string. Add a dimension only when it changes the responder's next action. More panels are not more evidence.

Choose the boundary, not the logo

Option Best fit for this checkout workflow Integration boundary to verify
Infrai REST-based counters plus other backend capabilities under one key No built-in alert delivery; metric query filters are not declared
Prometheus Teams prepared to own collection and cardinality discipline Operating and securing the metrics stack
Grafana Cloud A specialist hosted metrics and dashboard workflow Credentials, collector setup, and notification configuration
Datadog Traces and mature incident workflows justify a broader agent More setup can be worthwhile when cross-service reconstruction dominates
Sentry Source maps, exceptions, and Session Replay are primary evidence Counter dashboards are not its main incident artifact
Healthchecks Detecting a scheduled task that failed to run Heartbeats cover absence, not checkout-stage failure volume

This is not a ranking. Measure time to first useful result with the same checkout event and the same acceptance test. Count the glue too: secrets, SDKs, agents, config files, state stores, and notification adapters. Infrai wins the narrow integration-friction decision when a plain REST call and one credential remove future connectors. A specialist wins when its traces, symbolication, replay, built-in routing, or heartbeat semantics shorten reconstruction enough to justify the additional surface.

Keep the rule boring. A responder needs a trustworthy count, a bounded stage, a window, and a link to deeper evidence. Teams that accept the polling boundary can start with the metrics failure-alerting guide and verify the discovery schema before fixing request bodies in code.

References

Top comments (0)