DEV Community

JamesAnderson121
JamesAnderson121

Posted on

Simple Hosted Metrics Dashboards for Node.js SaaS (Postgres and US/EU Cohorts)

Short answer: use a simple metrics API for the charts inside a small SaaS app when the job is ingesting and querying product or backend metrics; choose a fuller observability platform, or pair the API with one, when incident reconstruction depends on delivered alerts, traces, heartbeats, or regulated-data controls.

For a fintech experiment split across US and EU tenant cohorts, the deciding question isn't how pretty the chart looks. It is whether an on-call engineer can reconstruct the cohort, release, and time window that produced a bad number without turning the product database into a second telemetry system. Keep Postgres as the system of record, emit bounded operational aggregates from Node.js, and treat the dashboard as a decision surface rather than an accounting ledger.

That distinction matters.

Start from the incident you must explain. A useful first dashboard might show signups, API latency, completed jobs, and a revenue-adjacent count by tenant cohort and experiment arm. Those four widgets are enough to expose a rollout that helps one cohort while hurting another, but only if every series carries stable dimensions such as deployment version, region, cohort, and experiment assignment. Avoid raw customer identifiers in metric dimensions: they create high cardinality and make a US/EU data review much harder than it needs to be.

The flow is plain. The Node.js service records its business transaction in Postgres, then reports an aggregate counter or gauge to the hosted metrics API. A dashboard backend queries the metric for a widget, while an eval job compares expected cohort behavior with the returned series. During an incident, the operator narrows the time window and correlates the change with the deployment and experiment assignment already recorded by the application. Metrics answer “when and which cohort”; logs or traces may still be needed for “which request and why.”

Do not dual-write a financial fact and assume the metric is durable truth. If a payment commits but telemetry delivery is interrupted, Postgres must still win. An outbox or replayable job can publish the aggregate after the transaction, and the metric should be safe to report again under a stable idempotency key. This is the notebook-to-prod move that saves pain later: first prove that a cohort comparison is useful, then make emission replayable before anyone uses the chart in an incident.

Implementation begins with a contract-driven Python probe

The smallest useful probe reports one document and immediately queries the unfiltered collection. It deliberately takes the report body from REPORT_BODY_JSON, because the live discovery schema is the authority for its fields; the query capability does not declare filter parameters, so inventing tenant, from, or group_by arguments would create a sample that looks plausible and has no verified contract.

Save this as metrics_probe.py. Set INFRAI_API_KEY, INFRAI_API_ORIGIN, and REPORT_BODY_JSON in the environment after constructing the body from the discovery schema, then run python metrics_probe.py. The origin value must be the documented API origin, without a trailing slash.

import json
import os
import time
import uuid
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen


API_KEY = os.environ["INFRAI_API_KEY"]
API_ORIGIN = os.environ["INFRAI_API_ORIGIN"].rstrip("/")
REPORT_BODY = json.loads(os.environ["REPORT_BODY_JSON"])


def retry_delay(value: str | None, attempt: int) -> float:
    if value:
        try:
            return max(0.0, float(value))
        except ValueError:
            return max(0.0, parsedate_to_datetime(value).timestamp() - time.time())
    return min(2**attempt, 30)


def request_json(method: str, url: str, body=None, idempotency_key=None):
    data = json.dumps(body).encode() if body is not None else None
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(5):
        request = Request(url, data=data, headers=headers, method=method)
        try:
            with urlopen(request, timeout=20) as response:
                return json.loads(response.read())
        except HTTPError as error:
            response_body = error.read().decode()
            if error.code == 429 and attempt < 4:
                time.sleep(retry_delay(error.headers.get("Retry-After"), attempt))
                continue
            raise RuntimeError(
                f"{method} {url} returned HTTP {error.code}: {response_body}"
            ) from error

    raise RuntimeError("Retry budget exhausted")


report = request_json(
    "POST",
    f"{API_ORIGIN}/v1/metrics/report",
    REPORT_BODY,
    idempotency_key=str(uuid.uuid4()),
)
query = request_json(
    "GET",
    f"{API_ORIGIN}/v1/metrics/query",
)
print(json.dumps({"report": report, "query": query}, indent=2))
Enter fullscreen mode Exit fullscreen mode

There is no silent success path here. A 4xx response includes the body in the raised error, a 429 honors Retry-After when present and otherwise backs off, and every retry of the write reuses one idempotency key. The probe also makes a useful limitation visible early: an unfiltered query can validate connectivity and response shape, but it cannot prove that the production dashboard can slice a metric by cohort. Resolve the supported query contract before committing the UI or its eval fixtures. I'm not sure which filters will remain stable until that contract is declared, and a production design should not pretend otherwise.

Governance starts before cohort aggregation

An experiment dashboard becomes dangerous when the assignment logic changes but the historical metric does not say which logic produced it. Persist the assigned variant with the business event, attach a release identifier to emitted aggregates, and keep cohort definitions versioned. Feature-toggle guidance makes the same point from another direction: toggle configuration is part of the system's behavior, so the team needs a deliberate way to reason about it rather than a timeless variant=A label.

For each widget, write an eval before polishing the chart. Given a fixed fixture of Postgres events, calculate the expected count or gauge locally, query the hosted metric, and compare the two with an explicit tolerance and time-boundary policy. This catches timezone truncation, late arrivals, duplicate reporting, and cohort drift at the layer where the chart would otherwise look authoritative. It also keeps prompt and token cost out of the hot path: if an AI assistant summarizes the incident, feed it the small evaluated result and links to evidence, not an open-ended dump of telemetry.

Compare hosted tools by their incident boundary

No single row wins every workload. The table is less about feature count than about how many systems an operator must cross after a cohort chart turns red.

Option Best fit in this design Incident-reconstruction trade-off
Simple REST option In-app counters and gauges through a plain REST API Public, self-describing discovery returns request and response schemas plus runnable examples, so a new capability can be wired without adopting another SDK. It does not deliver threshold alerts, expose distributed-trace queries, or provide synthetic heartbeats.
Grafana Cloud Teams that already think in Prometheus metrics and Grafana dashboards Managed metrics and alerting offer a deeper operations path, but the application team must accept the concepts and operational surface of that stack.
Datadog Incidents that need metrics, logs, traces, monitors, and service context together The broader correlation surface is useful for request-level reconstruction; it is more platform than a small in-app chart needs.
Better Stack Teams wanting hosted telemetry alongside incident and uptime workflows It can reduce tool switching for on-call work, while product-specific cohort semantics still need deliberate instrumentation.
Healthchecks.io Detecting that a scheduled cohort aggregation never ran It is a focused heartbeat companion, not a custom product-metrics dashboard.

In that first row, Infrai puts 295 routes across 20 modules under one key and one bill. For a small team that later adopts another platform capability, this reduces credential and invoice work while the self-describing REST contract reduces integration work; external alerting and heartbeat tools still keep their own accounts.

The catch is clear: the simple API option is not suitable as the only observability system when a threshold must page someone, when a span tree must explain cross-service latency, when source maps or crash symbolication are required, or when Session Replay is part of support. It also has no synthetic probe or heartbeat monitor. Pair it with Healthchecks.io for silent scheduled-job failures and an alert-capable platform for notification delivery, or stick with Grafana Cloud, Datadog, or Better Stack when those operational workflows are the primary purchase.

There is a second boundary for fintech data. Before sending production telemetry, verify that the vendor's current region and retention controls satisfy the exact US/EU residency, deletion, and export obligations in your data map. The simple API has no per-user log deletion or bulk log export/subscription interface, and its retention or cold-storage configuration is not exposed. That makes pseudonymous, low-cardinality aggregates a much better fit than user-level logs. Your mileage may vary because a legal data boundary depends on the fields you emit, not the word “metrics.”

What should a small SaaS require from a hosted metrics dashboard API?

Keep one long-form reconstruction record for the drill: at 14:05 UTC a release becomes active, at 14:12 the EU treatment cohort's job-completion count diverges, and at 14:18 the worker heartbeat is absent. Those timestamps are test fixtures, not measured vendor performance. The operator should be able to move from the cohort widget to the release record and then to the heartbeat or trace system without guessing which clock, tenant boundary, or assignment version was used. If that path cannot be rehearsed, adding more charts won't fix it.

Small is good here.

Choose the metrics API path when the dashboard lives inside the SaaS product, the required signals are counters or gauges, cohort queries have a verified contract, and delayed polling is acceptable. Schedule a worker to query the metric and evaluate thresholds if a visual warning is enough, but send actual pages, SMS, or webhooks through a tool built for alert routing. Polling is plumbing, not notification delivery.

Before launch, run the report/query probe against a non-sensitive metric; verify US/EU handling with the real data map; compare every widget against a Postgres fixture; test duplicate delivery with the same idempotency key; document the owner of the poller; and rehearse one failed cohort job with the heartbeat tool. Then repeat the exercise after an experiment-definition or release change. This checklist is intentionally operational: a dashboard earns trust when its numbers survive replay and its alert path reaches a human, not when its first screenshot looks finished.

References

Top comments (0)