A Node.js SaaS app needs one boring guarantee from its metrics dashboard API: if a scheduled property import stops producing usable results, the on-call engineer hears about it before tenants do. The least complex option is a custom success counter plus a last-success age gauge, emitted by the worker and evaluated outside that worker.
TL;DR: choose the signal path that survives an application rollback. Keep product events separate from operational metrics, bound every label, and alert on the age of the last completed import rather than on errors alone.
| Choice | Detects silent stalls | Rollback exposure | Setup surface | Best fit |
|---|---|---|---|---|
| Success counter only | Indirectly | Low | Small | Stable, frequent schedules |
| Last-success age gauge plus counter | Yes | Low | Small | Default for scheduled imports |
| Product-event pipeline as monitor | Sometimes | Higher | Medium | Cohort analysis, not primary paging |
Recommendation: use the age gauge plus the counter. The runner-up is the counter alone when every import runs frequently enough that a rate window cannot hide a missed schedule. This choice is deliberately dull. It is also easy to remove during rollback and easy to test without a dashboard.
What should a Node.js SaaS app metrics dashboard API detect?
An error counter records an attempted operation that failed. A scheduler that never fires attempts nothing. A worker that loses its lease, waits forever on an upstream response, or exits before registering the job can leave the error count perfectly flat. Green charts can describe a dead pipeline.
Property data makes the distinction concrete. Suppose each management company expects a nightly unit-status import. The operational question is not “How many exceptions occurred?” It is “How old is the newest completed, usable result for this schedule?” That is a liveness question.
Track at least two signals: completed imports as a counter and seconds since the most recent successful completion as a gauge. The counter supports rates and longer-term capacity analysis. The gauge maps directly to the paging condition. Error counts and duration distributions still matter, but neither proves fresh output exists.
Do not put property_id, file name, lease ID, or raw error text in metric labels. Those values can grow without a known limit, which turns a tiny instrumentation patch into a storage and query problem. Use bounded dimensions such as import_kind, region, and result. Prometheus naming guidance also recommends a common application prefix, base units, and names whose sum or average remains logically meaningful.
That sounds fussy. It saves cleanup later.
No metric makes this free. The limitation of the age-gauge approach is that every schedule needs a trustworthy identity and freshness budget; it is a poor fit for ad hoc jobs with no promised cadence. In that case, a queue-depth signal or an explicit job deadline may describe the contract better. The trade-off is extra schedule metadata in exchange for a rule an operator can read at 03:00.
Rollback safety is a data-contract problem
A safe rollback does not require the old release to understand a new dashboard. It requires the telemetry contract to remain readable while old and new workers overlap. During a rolling deployment, both versions may emit for the same schedule. Counters must tolerate duplicate processes, while the last-success timestamp must move only after the imported result is committed and available to readers.
The ordering matters. Emit success before the database commit and a crash can produce a fresh-looking signal for data that never became visible. Emit it after commit and the monitor reflects the user-facing outcome. Keep the instrumentation non-blocking from the job's point of view: failure to publish telemetry should be visible in logs, but it should not undo a valid property import.
I would make the release decision with three checks:
- Can the previous worker version run while the new metric exists?
- Can a duplicate execution increase the counter without falsely advancing last success?
- Can the alert rule remain valid if the new release is rolled back immediately?
The third check kills clever schemas. Picture two releases overlapping for ten minutes: version A knows units and leases; version B adds a new result label; then the deployment rolls back while one slow import is still committing. If the alert depends on the new label, silence may mean “healthy,” “old worker,” or “query no longer matches.” Those are three different states hidden behind one empty graph. Add signals first, observe them across both releases, then switch the alert in a later change. A renamed label, a changed unit, or a status value bundled into the alert deployment makes recovery harder to reason about. Small diffs win.
Logs play a supporting role. RFC 5424 defines ordered severity levels from Emergency through Debug, but a severity label is not a liveness detector. Log the schedule identifier, bounded import kind, result, and duration as structured fields. Use an error severity for a failed attempt. Use the metric age for absence.
A minimal TypeScript boundary
The worker should depend on a tiny interface, not on a dashboard SDK spread through business logic. This keeps time-to-first-call short and makes rollback behavior testable with an in-memory recorder.
type ImportLabels = {
importKind: "units" | "leases";
region: "eu" | "us";
};
interface ImportSignals {
completed(labels: ImportLabels, durationSeconds: number): void;
failed(labels: ImportLabels, reason: "fetch" | "parse" | "commit"): void;
lastSuccess(labels: ImportLabels, unixSeconds: number): void;
}
async function runScheduledImport(
labels: ImportLabels,
signals: ImportSignals,
importAndCommit: () => Promise<void>,
now: () => number = Date.now,
): Promise<void> {
const startedAt = now();
try {
await importAndCommit();
const completedAt = now();
signals.completed(labels, (completedAt - startedAt) / 1_000);
signals.lastSuccess(labels, Math.floor(completedAt / 1_000));
} catch (error) {
signals.failed(labels, classifyFailure(error));
throw error;
}
}
function classifyFailure(error: unknown): "fetch" | "parse" | "commit" {
if (error instanceof SyntaxError) return "parse";
return "commit";
}
The example has no network endpoint and no dashboard configuration. Good. The adapter can expose these calls through the metrics system already used by the service. The business boundary stays stable if that adapter changes.
There is one intentional constraint: classifyFailure returns a closed set. Passing error.message would create an unbounded dimension and leak implementation detail into queries. In production, classification should follow typed errors from the fetch, parse, and commit boundaries; the short fallback above only demonstrates the metric contract.
Test the outcome, not the chart. Use a fake clock and recorder to verify that a successful commit emits one completion and one timestamp, a rejected commit emits failure without advancing the timestamp, and telemetry exceptions do not change the persistence result. Then exercise the alert with missing samples and a stale timestamp. Missing data and old data are different states, even if both eventually page.
The two criteria worth benchmarking
First, benchmark detection delay against the schedule, not HTTP request latency. For a nightly import, a five-minute evaluation interval is noise; for a job expected every minute, it is material. Pick a freshness threshold from the schedule interval, normal completion variance, and the time operators need to act before stale property data affects downstream work. No universal number is honest here. I would benchmark two releases, one forced rollback, one missing run, and one delayed commit before accepting the rule; that is a small test matrix, but it exposes the ambiguous empty-series case quickly.
Second, measure integration drag. Count the configuration files, runtime dependencies, required environment variables, and lines crossing into domain code. I care more about those numbers than a polished default screen. Every extra coupling point expands the rollback checklist.
Also verify where telemetry is processed and retained if the service operates in EU and US regions. Region placement, access control, retention, and deletion are deployment requirements, not logo comparisons. Confirm them against the current contract and architecture of whichever system is under evaluation. They change.
For a self-serve comparison of PostHog, Grafana Cloud, and Better Stack, run the same acceptance test instead of scoring screenshots: emit the bounded counter and timestamp through a disposable adapter, remove one scheduled run, overlap two worker versions, then roll back. Record time-to-first-call, configuration count, alert behavior on absent data, and the steps needed to delete the adapter. This is not a ranking, and those product names are only candidates from the search question. Current deployment regions and service boundaries must be verified in their own documentation before selection.
The dashboard itself needs only a small operational view: current last-success age by bounded schedule group, completion rate, error counters by closed reason, and a latency distribution. Product analytics can answer different questions, such as how imported inventory changes tenant behavior. Do not make that event pipeline the only pager path. Its batching and transformation stages add failure modes between the worker and the alert.
Cheap ingestion is not the primary decision rule. A low bill cannot rescue an alert that lies during rollback.
When the runner-up is enough
A completion counter alone is reasonable when schedules are frequent, execution is uniform, and the query engine can reliably alert on an absent or zero rate. It has less state at the emitter and one fewer metric to maintain.
It becomes awkward for sparse schedules. Query windows must be wide enough to include expected runs, calendar behavior complicates evaluation, and “no series exists” may behave differently from “the series has value zero.” A last-success timestamp makes the rule legible: current time minus timestamp exceeds the agreed freshness budget.
Choose product events as the secondary path when analysts need tenant funnels, cohorts, or import-volume segmentation. Choose logs for diagnosis and audit context. Neither is a substitute for the low-cardinality operational signal that pages on silent absence.
The final decision is plain: prefer the smallest metrics boundary that remains truthful through commit, overlapping releases, and rollback. Alert on stale successful output. Everything else is context.
Further reading
- Prometheus, “Metric and label naming”: https://prometheus.io/docs/practices/naming/
- IETF RFC 5424, “The Syslog Protocol”: https://datatracker.ietf.org/doc/html/rfc5424
Top comments (0)