DEV Community

ConstantineHayes8524
ConstantineHayes8524

Posted on

Hosted Metrics Query API in 2026: How to Build Dashboard Cards

TL;DR: Put a thin backend query contract between dashboard cards and a hosted metrics API, but choose dimensions for incident reconstruction first. For a property-management agent loop, record latency and cost by operation, portfolio, and outcome. Keep the unique run ID in correlated logs or traces.

Choice First useful card Incident reconstruction Operations
Backend adapter over hosted metrics Fast Good for narrowing a window Low
Browser-to-provider queries Fastest demo Weak boundary Low at first
Self-hosted metrics stack Slower Good, with storage control High

Recommendation: start with the backend adapter. Self-hosting is the runner-up when residency, retention, or measured query economics justify owning more machinery. Browser queries lose because a card should not hold provider credentials or query syntax.

Should a hosted metrics query API feed React dashboard cards directly?

A property manager does not need a generic p95 card. They need to reconstruct why a maintenance-request agent became slow or expensive during a known window. Was the delay in classification, lease lookup, vendor matching, or message drafting? Did retries inflate cost after a failed operation? For a startup admin panel, the React dashboard should call a Node.js backend that owns the time-series query and tenant check. It should not call the hosted API directly.

That changes the schema. Measure each agent operation, not only the full request. Capture its timestamp, operation, duration, outcome, stable model class, portfolio, token counts, and computed cost in the smallest unit used by your accounting model. Metrics get bounded dimensions. A unique run ID belongs in the correlated trace or log because making every run a metric label creates an ever-growing series set.

Metrics find the suspicious ten minutes. Traces and logs explain one run.

Criterion one: preserve the evidence

Start with the questions on-call will ask, then work backward. Two views cover the first pass: latency by operation and outcome, plus cost by operation and portfolio. Return explicit bucket boundaries to the client. Otherwise a chart can imply continuity where no samples exist.

Preserve absence too. Zero and missing are different. Zero is a measurement; missing means the query produced no point for that bucket. Converting both to 0 hides ingestion gaps and can turn a telemetry outage into a reassuring flat line.

I benchmark this boundary with three numbers: response bytes, backend query duration, and rendered point count. Those are not universal targets. Record a baseline with production-shaped cardinality, then fail the performance test when a change exceeds the dashboard budget your team chose.

A clean adapter accepts a narrow request and returns boring JSON. The React layer should not know a provider query language, authentication scheme, rollup function, or billing vocabulary. The backend can cap time ranges, normalize gaps, enforce tenant scope, and measure query duration in one place. This is the second criterion: minimize the glue that reaches the dashboard. A small startup may be tempted to skip the Node.js boundary because direct access produces the first screenshot sooner. The drawback arrives during the first permission change or storage migration, when browser code, credentials, and query behavior must all change together.

Do not accept raw query strings. Enumerate measures and grouping keys server-side. This makes authorization review tractable and stops each caller from coupling itself to storage syntax.

Pricing needs the same skepticism. Hosted observability billing may separate ingestion from indexed or retained data. Datadog's public pricing page is one concrete example with separate log ingestion and indexed-log charges. That is evidence to inspect the meter, not a product recommendation. Benchmark write volume, active series, retention, and read frequency against every candidate's current terms.

Implement the smallest useful contract

This TypeScript keeps storage behind one interface. The allowlists matter more than the interface name.

type Measure = "latency_ms" | "cost_microunits";
type GroupBy = "operation" | "outcome" | "portfolio";
type Query = {
  measure: Measure;
  from: string;
  to: string;
  stepSeconds: 60 | 300 | 900;
  groupBy: GroupBy[];
  portfolio: string;
};
type Point = { at: string; value: number | null };
type Series = { labels: Record<string, string>; points: Point[] };

interface MetricsStore {
  query(input: Query): Promise<Series[]>;
}

export async function queryCard(
  store: MetricsStore,
  input: Query,
  tenantPortfolios: ReadonlySet<string>,
): Promise<{ series: Series[]; queriedAt: string }> {
  if (!tenantPortfolios.has(input.portfolio)) throw new Error("Forbidden portfolio");
  const from = Date.parse(input.from);
  const to = Date.parse(input.to);
  if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) {
    throw new Error("Invalid time range");
  }
  if (to - from > 24 * 60 * 60 * 1000) throw new Error("Range exceeds 24 hours");
  return { series: await store.query(input), queriedAt: new Date().toISOString() };
}
Enter fullscreen mode Exit fullscreen mode

The concrete ceiling here is 24 hours. It is an example policy, not a universal limit. Make it explicit and test it. Incident views can request narrow windows; monthly reports should use a pre-aggregated path. The trade-off is less freedom for an operator in exchange for bounded query work and a contract that is easy to benchmark.

Test an empty bucket, delayed point, overlapping tenant identifier, rejected grouping key, and partial provider response. Then load-test production-shaped fan-out. Five cards, four storage queries per card, twelve viewers, and two refreshes per minute produce 480 storage queries per minute.

const queriesPerMinute = 5 * 4 * 12 * 2;
if (queriesPerMinute !== 480) throw new Error("Benchmark fixture changed");
Enter fullscreen mode Exit fullscreen mode

Small arithmetic. Large consequence. Cache aligned buckets in the backend and cancel stale browser requests when the operator changes the time window.

When is the runner-up better?

Choose self-hosting when control is a requirement: mandated data location, custom retention, disconnected operation, or query behavior a hosted API cannot supply. It can also win when a measured workload and current terms justify the operational burden. Count upgrades, backups, capacity headroom, and on-call ownership. Server cost alone is incomplete.

The backend-adapter approach has a real limitation: it is not suitable when analysts need unrestricted exploratory queries or when the extra network hop violates a measured latency budget. In that case, use a dedicated analysis path or a controlled self-hosted interface. Do not weaken the card endpoint until it becomes a general query proxy.

A hosted adapter is attractive when the team needs cards quickly and does not want to own ingestion and storage. Portability still requires tests. Run the same fixture against each adapter and compare bucket boundaries, missing-point behavior, percentile semantics, filtering, response size, and query duration.

Choose the path that reconstructs a real agent incident with the least unowned glue under a measured operational budget. The card is only the surface. The evidence chain is the product.

Sources

Top comments (0)