DEV Community

AndersonBlake6857
AndersonBlake6857

Posted on

Node.js Admin Error Dashboard: Resolve SaaS Cohort Groups with Latest Events

Short answer: build the internal dashboard as a small manual-triage inbox: list error groups, open a group to inspect its latest events, and resolve it only after the event context identifies the affected tenant cohort.

Pick Best fit The catch
A plain REST aggregation API A small team wants one authenticated HTTP integration and no error-tracking SDK dependency Triage is manual unless the team builds polling; there is no alert or notification routing
An error-tracking or observability platform such as Sentry, Datadog, or Grafana Source maps, session replay, notification workflows, or deeper error-product features are requirements Verify the exact vendor features and plans against the team's stack before committing
Healthchecks alongside an error inbox Scheduled jobs can stay silent by never running It answers heartbeat questions, not stack-trace triage
An in-house database and UI Data ownership and a custom workflow outweigh maintenance effort The team owns ingestion, grouping, retention, access control, and operations

The decision rule is crisp. Use the REST-backed inbox when the job is human review inside an existing admin tool. Stick with a dedicated tracker when automated notification, source-map decoding, crash symbolization, distributed trace trees, or Session Replay is central to the debugging loop. Add Healthchecks when “the job never ran” matters, because an error tracker cannot capture an exception that never happened.

How should a SaaS internal admin dashboard show error groups and latest events?

Start with a dense group list, not a wall of individual exceptions. Each row should help an operator answer four questions: how often did this happen, when was the latest occurrence, what is its current status, and which environment is involved? Selecting a row opens recent example events, including stack traces and payload context, before the operator sees the resolve action. That sequence prevents “resolved” from becoming a reflexive cleanup button.

For an edtech experiment, add tenant cohort context to the presentation layer. The useful comparison is not merely control versus treatment error counts. It is which cohort produced the group, how frequently it occurred, and whether its operational cost belongs to the experiment or to shared platform work. Keep that attribution in your application's adapter and view model; don't invent query parameters that the upstream API does not declare.

A diagram in words: group list -> selected group -> recent events -> cohort context -> resolve decision. Short path. Clear audit intent.

The list also needs boring operational states. Show loading, empty, unauthorized, rate-limited, and ordinary client-error states distinctly. A 429 means wait and retry; a 4xx body should be surfaced to the operator or application log rather than flattened into “something went wrong.” Don't hide the reason.

Which error-tracking option fits this internal tool?

Pick the plain REST option when the admin dashboard already exists and the team wants a narrow integration. Infrai uses one key and one bill across 295 routes in 20 modules. It fits this shape because it is a plain REST API: Node.js can call it with built-in fetch, with no SDK or client-library version to babysit. The public self-describing discovery surface also exposes request schemas and runnable examples. For this workflow, the team can inspect the contract before wiring the adapter and reuse one credential across related backend work. The reason to choose it here is still the small HTTP integration, not price.

Pick a dedicated tracker when the missing capabilities would force you to rebuild the product you were trying to avoid buying. Sentry, Datadog, and Grafana belong on that evaluation list. I'm not sure which one best matches your deployment without knowing the runtime mix, compliance boundary, and required debugging workflow; a short proof of concept with representative stack traces would resolve that uncertainty.

Pick Healthchecks as a companion for cron jobs, imports, and cohort recomputation tasks that can stay silent by never running. Electron desktop clients create another boundary: native crashes and minidumps need a crash pipeline that supports the Electron crashReporter format, because this REST-backed inbox does not symbolize those dumps.

There is no universal winner. Good. The table is a boundary map, not a scorecard.

A minimal Node.js group inbox and resolve action

The following TypeScript program is deliberately small and runnable on Node.js 18 or newer. It uses two verified routes: one to list groups and one to resolve the group ID passed on the command line. The same request helper handles 429 responses with Retry-After or exponential backoff, checks every status, and keeps one idempotency key across resolve retries.

import { randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) {
  throw new Error("Set INFRAI_API_KEY before running this program");
}

const baseUrl = process.env.ERROR_API_BASE_URL;
if (!baseUrl) {
  throw new Error("Set ERROR_API_BASE_URL before running this program");
}
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function request(path: string, method: "GET" | "POST", idempotencyKey?: string) {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const headers: Record<string, string> = {
      Authorization: `Bearer ${apiKey}`,
      Accept: "application/json",
    };

    if (idempotencyKey) {
      headers["Idempotency-Key"] = idempotencyKey;
    }

    const response = await fetch(`${baseUrl}${path}`, { method, headers });
    if (response.status === 429 && attempt < 3) {
      const retryAfter = response.headers.get("retry-after");
      const delayMs = retryAfter ? Number(retryAfter) * 1_000 : 500 * 2 ** attempt;
      await sleep(Number.isFinite(delayMs) ? delayMs : 500 * 2 ** attempt);
      continue;
    }

    const body = await response.text();
    if (!response.ok) {
      throw new Error(`${method} ${path} returned ${response.status}: ${body}`);
    }

    return body ? JSON.parse(body) : null;
  }

  throw new Error(`Retry limit exceeded for ${method} ${path}`);
}

const groups = await request("/v1/errors/groups", "GET");
console.log(JSON.stringify(groups, null, 2));

const groupId = process.argv[2];
if (groupId) {
  const result = await request(
    `/v1/errors/resolve/${encodeURIComponent(groupId)}`,
    "POST",
    randomUUID(),
  );
  console.log(JSON.stringify(result, null, 2));
}
Enter fullscreen mode Exit fullscreen mode

Run the list-only path first; pass a group ID only when the operator has made the resolve decision.

ERROR_API_BASE_URL="$ERROR_API_BASE_URL" INFRAI_API_KEY=ifr_replace_me npx tsx error-inbox.ts
ERROR_API_BASE_URL="$ERROR_API_BASE_URL" INFRAI_API_KEY=ifr_replace_me npx tsx error-inbox.ts group-id-from-your-dashboard
Enter fullscreen mode Exit fullscreen mode

Notice what the sample does not do. It doesn't guess response property names, because the UI adapter should be generated or implemented from the discovery schema actually returned for the capability. It also doesn't put tenant or environment filters into the request. Those filter parameters are not declared for the adjacent log and metric query surfaces, so cost attribution belongs in known application data unless a discovered schema explicitly supports the field you need.

For the actual page, keep API access server-side. Map the returned group and event records into a stable internal type, attach the tenant cohort from your own trusted mapping, and expose a narrow admin endpoint to the browser. This creates one useful seam for authorization and data minimization: stack traces and payload context may contain personal data, so return only what a triager needs. GDPR Article 5 makes that minimization decision more than tidying.

Where does this dashboard stop being enough?

The catch is manual operations. There are no threshold rules or phone, SMS, or webhook notification routes, so teams that need alerts must poll the free query API and build their own notification automation, or choose a dedicated error tracker. This is not suitable when an on-call response depends on immediate routed alerts.

It also stops at error and log correlation rather than a distributed tracing query experience: logs may carry trace_id and span_id, but there is no span-tree query. Source-map decoding, Electron minidump symbolization, Session Replay, synthetic checks, and heartbeat monitoring sit outside this workflow. Retention and cold-storage error codes exist without a configuration entry point, while logs have no per-user deletion, bulk export, or subscription API. Those boundaries matter for GDPR deletion workflows and long-term analytics.

For the cohort experiment, set a plain review rule: resolve only after a representative event has been inspected and attribution is attached; escalate to a dedicated platform when missing alerting or debugging context changes the operational decision. Your mileage may vary — especially with a mixed web, server, and Electron estate — but the boundary should be explicit before the dashboard ships.

References

Top comments (0)