The dashboard
The hosted app is at app.metergraph.dev. A local or BYOC deployment serves the same interface at whatever host you configured.
This page is a reference for what each screen shows and how each figure is derived. If a number looks wrong, the derivation is almost always the explanation.
Navigation
Section titled “Navigation”A left sidebar, collapsible. The collapsed state is the only piece of interface
state the app persists, in localStorage under mg.nav.collapsed.
| Item | URL | Shown when |
|---|---|---|
| Get started | #get-started | Only until your first trace has been processed. The URL stays reachable forever |
| Performance | #performance | Always. This is also the landing page |
| Analysis | #analysis | The deployment has the analysis_runs capability |
| Traces | #traces | Always |
| Alerts | #alerts | Always. Carries a red badge with the open-alert count |
| Batch API | #batch-api | Managed deployments only |
| Settings | #settings | Always |
| Keys | #api-keys | Always |
| Admin | #admin | Only for a platform administrator |
| Docs | external | Always |
Routing is by URL fragment, so every screen is linkable and the browser back
button works. An unrecognized fragment lands on Performance. #calls is
rewritten to #traces.
Below the navigation, the account row shows your email, your workspace and your role, and holds Sign out.
The quota meter
Section titled “The quota meter”On a free workspace the sidebar carries a usage meter: the label Free plan, a
percentage, a bar, and {calls counted} / {allowance} calls. The bar turns
amber at 80 percent and red once the allowance is spent, at which point a Book
a call link appears. See
Limits and allowances.
Banners
Section titled “Banners”| Banner | Appears when |
|---|---|
| Capture paused | The monthly allowance is spent. Ingest is returning 402 |
| First trace | A first trace was processed while the tab was open. Links to Traces |
| Viewing as | A platform administrator is in a read-only impersonation session |
| Preview notice | You are on a preview deployment, not the production app |
The environment filter
Section titled “The environment filter”Most pages carry an environment multi-select. It reads the environments seen in
the current window and starts with every environment checked. The button
label is All environments, No environments selected, or N selected.
Two behaviors are worth knowing.
It does not persist. There is no URL parameter, no localStorage entry and
no shared store. Reloading the page resets it to all environments. Changing the
window keeps your selection where the names still exist.
All-selected is not the same as filtering to every name. When every environment is checked the app sends no environment parameter at all, which is what keeps calls that carry no environment label visible. Selecting every name individually would drop them. This was the behavior of an earlier version and is now deliberately avoided.
Performance
Section titled “Performance”#performance, and the landing page. Activity, spend, latency, errors and
replay quality for one window, drillable into a single bucket, a single
workload, and the individual traces underneath.
Controls
Section titled “Controls”| Control | Options | Default |
|---|---|---|
| Window | Today (24 hours, hourly buckets), Last 7 days (six-hour buckets), Last 30 days (daily buckets) | Today |
| Environment | Multi-select | All |
| Group by | Total, Model, Workload | Total |
| Metric | Calls, Errors, Latency | Calls |
| Workloads / Models | Multi-select subset of the ranked series | Empty, meaning everything |
Clicking a bar on the timeline scopes the whole page to that bucket. A dismissible pill names the selected bucket. None of these controls persist across a reload.
The three headline numbers
Section titled “The three headline numbers”| Tile | Window value | What it measures |
|---|---|---|
| Spend | Sum of cost_usd over every call in the window | A lower bound. A call the catalog could not price contributes zero |
| Calls | count(*) over the same rows | Every captured call, errored ones included |
| Error rate | Errored calls divided by all calls | A call counts as errored when its error flag is true, or its status is error, failed, failure, cancelled or canceled |
Under each tile is a comparison against the immediately preceding window of the same length. A change under one percent reads flat and draws no arrow.
Select a bucket and all three switch to that bucket, with the window figure moved
into the note beneath. The Calls note becomes a multiple of typical, where
“typical” is the median of the window’s non-zero buckets, not its mean. It
reads — when there is no non-zero baseline.
Ingest status
Section titled “Ingest status”A pill appears in the header only when something is wrong.
| Text | Meaning |
|---|---|
N batch(es) rejected. Check your API key and payload format | Batches were refused outright |
N batch(es) missing from this window. We are identifying the cause | Unclassified gap |
N batch(es) missing from this window. We are restoring them after an upstream outage | Known upstream incident, nothing for you to do |
No traces received in this window | The workspace has sent traces before, but not here |
no traces received yet | Nothing has ever arrived |
No traces received in the last 12 hours | Delivery may have stopped |
When a last-received time is known the pill also names how long ago the last trace arrived. Healthy ingestion draws no pill at all.
The activity timeline
Section titled “The activity timeline”Call volume, errors and p95 latency on one chart. The trailing bucket is always partial by construction, so it is drawn as In progress rather than dropped. It keeps its real counts and stays in the window totals.
Two rules the chart enforces rather than fudging:
- Latency percentiles come from Postgres
percentile_contover rawlatency_ms, per bucket. They are never averaged. - A percentile cannot be recombined from parts, so selecting a subset while showing Latency draws no p95 and says so, rather than approximating one.
Breaking down by model or workload draws at most four named series plus a folded Other band. Calls and errors sum across a fold. Average latency is weighted by call count. Percentiles are dropped.
Workloads
Section titled “Workloads”One row per workload over the window, or over the selected bucket.
| Column | What it holds |
|---|---|
| Workload | The route name. A route the SDK never labeled reads Unlabeled template · {first 8 hex} and its full template: key is in the tooltip |
| Window shape | A sparkline of that workload’s calls and errors per bucket |
| Spend | Sum of cost_usd, with cost per call beneath it |
| Calls | Call count, with a multiple of typical beneath it when a bucket is selected. Red at 2× or more |
| Latency | p95, with p50 beneath. Red when p95 exceeds 1.5× the window p95 |
| Errors | Error count, red when this workload’s error rate exceeds 1.5× the window rate. The info button names the leading error_type, or unclassified |
| Model | The workload’s top model, with +N when it uses several. The provider prefix is the second line |
| Quality | Grade dots and a letter from the most recent analysis that replayed this workload. Reads not inspected otherwise |
The quality grade comes from replay evidence, not from production traffic. A/B count as held. See Analysis for what the grades mean.
Traces
Section titled “Traces”The ten newest traces in the window, or twenty-five inside a selected bucket. Columns are the same as the Traces page. All traces → opens the full list.
Spend by model
Section titled “Spend by model”| Column | What it holds |
|---|---|
| Model | The raw model identifier as your code sent it |
| Spend | Sum of cost_usd for that model |
| $/answer | Spend divided by call count |
| Calls | Call count |
| Tokens | input_tokens + output_tokens, summed |
When some calls carry no price the table says so directly:
A lower bound.
Ncalls carry no price.
That count is the difference between all calls in the window and those whose
cost_status is priced or reported. To find out which models are behind it,
see Model catalog and pricing.
What failed
Section titled “What failed”A ranked row of chips, one per error_type, with counts. A null type reads
unclassified. The section is absent when nothing failed.
Empty states
Section titled “Empty states”| State | Trigger |
|---|---|
| Onboarding scaffold, with Connect your app | Nothing has ever been received. Each section shows the question it will answer once data lands |
| Quiet window | Zero calls in this window, but the workspace has sent traces before. The tiles read — and the page suggests a wider window |
| No environments selected | You cleared the environment filter |
A quiet window is not the same as a workspace that never delivered, and the page distinguishes them.
Traces
Section titled “Traces”#traces. The individual traces over a window of their own, each opening in
place into its span waterfall.
The window options are Today, Last 7 days, Last 30 days and Last 90 days, defaulting to 7 days. Ninety days is the ceiling because that is the default metadata retention. The list returns at most 100 traces.
This page carries no summary tiles. It is deliberately a list.
| Column | What it holds |
|---|---|
| Trace | The trace name, else its first route, else LLM trace. A Failed chip when the trace errored. Second line: the first 12 characters of the trace ID, and whether content is captured, truncated or not captured |
| Started | Local time, to the second |
| Duration | Wall-clock span of the trace |
| Spans | Number of spans |
| Tokens | Measured tokens across the trace |
| Cost | Sum of cost_usd. A zero renders as —, because the list cannot distinguish a free trace from an unpriced one |
| Model | The trace’s first model, with +N when it used several |
Inside a trace
Section titled “Inside a trace”Expanding a row draws a waterfall of its spans, ordered by nesting with offsets
in milliseconds from the earliest span. Selecting a span opens an inspector with,
in order: Model, Provider, Workload, Function, Cost (reading
Unpriced when there is none), Tokens, Cache (only when non-zero),
Reasoning tokens (only when non-zero), Latency, First token,
Streamed, Status, Endpoint, Request id.
Below that, tool names as chips, then the captured Request and Response,
pretty-printed and copyable. Where content was not captured they read Request not captured and Response not captured. Where the server clipped a field the
inspector says Truncated at the 100 KB field limit.
Analysis
Section titled “Analysis”#analysis, also reachable at #optimize. The most recent fixed-profile
analysis of your production traffic: what it replayed, what held, what it would
save, and what to ship.
Available only where the deployment has the analysis_runs capability.
Starting one
Section titled “Starting one”Start analysis opens a confirmation dialog that states every parameter and lets you change none of them. The profile is fixed on purpose: the window (latest complete UTC days), the evaluation sample size, how many workloads are covered (top N by spend), the objective, the opportunity types, and the candidate models.
A run is blocked with a reason when there is not enough trace data, or when the catalog has no price for a production model the run would need.
While a run is active, Running now shows four stages: Queued, Exporting traffic, Analyzing, Importing report. You can leave the page. The previous analysis stays visible until the new one finishes.
A failed run appears under Recent attempt with a plain-language cause.
The four numbers
Section titled “The four numbers”| Tile | Value | Derivation |
|---|---|---|
| Replays that held | {held}/{total}, with a percentage beneath | Grades A and B, over all four grades, summed across every graded finding. Reads No replays yet when no finding carries grade counts |
| Avg quality change | A signed percentage | The mean of each finding’s quality ratio against its baseline, minus one. Findings with no measured ratio are excluded, never counted as zero |
| Savings | Whole dollars per month | The analysis report’s own projected monthly saving. It is deliberately not the sum of the rows below, and it is a projection from observed traffic rather than a live catalog computation |
| Shipped | A count | Recommendations whose delivery reached “applied” |
Recommendations
Section titled “Recommendations”| Column | What it holds |
|---|---|
| Change | The recommendation title, its kind (Model swap, Search-enabled stack swap, Prompt context reduction, Result caching, Batch-first), and a Shipped or Superseded chip |
| Workload | The route it applies to |
| Evidence | Grade dots and a case count, or the literal Not replayed |
| Quality | held, or a signed percentage against the baseline |
| Latency | held, N× faster, or N× slower |
| Saves | Whole dollars per month for this recommendation alone |
Superseded means a later analysis retested the same workload.
View evidence opens the recommendation, where a model swap shows the full candidate comparison, the pipeline’s stated reason, and the actual replayed question with both models’ verbatim answers side by side.
Tested, no change
Section titled “Tested, no change”Workloads that were analyzed and produced nothing worth shipping, with the reason. This section existing is the point: silence would be indistinguishable from a failed run.
When there is no recommendation
Section titled “When there is no recommendation”The page says which of these happened rather than showing an empty table.
| Outcome | What it means |
|---|---|
| Your current models are already the cheaper choice | Every candidate was analyzed and none beat what you run today at equal or better quality |
| This analysis could not evaluate every candidate | Some workloads did not complete. Run it again |
| Not enough traffic in any single workload | No workload reached the minimum trace count. The page names the largest one and the threshold |
| No analysis yet | Nothing has been run |
Grades
Section titled “Grades”One vocabulary, used by the Performance quality column, the analysis dots, the model comparison legend and the eval definition.
| Grade | Meaning | Counts as held? |
|---|---|---|
| A | Equivalent outcome | Yes |
| B | Same outcome with only non-impacting differences | Yes |
| C | Impacting difference that could change a downstream decision | No |
| F | Unusable result or execution failure | No |
Model comparison
Section titled “Model comparison”Reachable from a recommendation. One row per candidate model against your baseline, with Replays held, Quality against baseline, Cost as a ratio plus the absolute figure, Latency as a ratio plus milliseconds, and the Safe-switch rate as a percentage. A chart view plots safe-switch rate against cost on a log axis, with your current cost marked at 1×.
Alerts
Section titled “Alerts”#alerts. Detector alerts with the evidence each detector measured. Detectors
run every five minutes, which the header states.
No summary tiles. The only count is the sidebar badge, which is the number of open alerts.
| Column | What it holds |
|---|---|
| Alert | The alert title, with {environment} · {detector} beneath |
| Status | Two chips: severity (Critical, Warning, Info) and state (Open, Acknowledged, Resolved) |
| Evidence | What the detector measured, and against what baseline. See below |
| When | Last seen {ago} while live, Resolved {ago} once closed |
| Acknowledge, for an open alert, unless your role is viewer |
Evidence, per detector:
| Detector | Measure | Detail |
|---|---|---|
| Cost drift | N.NN× cost per call | Recent median against baseline median, per call |
| Latency drift | N.NN× slower | Recent median against baseline median, plus the share of calls above the old p95 |
| Unusual or runaway spend | ${amount} in {span} | How many calls fell outside the environment’s cost distribution, and the projected daily figure |
| Anything else | none | The detector’s own explanation |
An environment filter appears only when at least one alert carries an environment. Alerts with no environment are always shown, whatever the filter.
Empty state: No alerts. Cost drift, latency drift and unusual spend are watched on every route, every 5 minutes.
See Set up alerts for what each detector looks for and where alerts are delivered.
Batch API
Section titled “Batch API”#batch-api. Managed deployments only.
Metergraph runs one synthetic, single-request probe through each provider’s Batch API on a fixed schedule and measures how long it actually takes. This is not your traffic, and it is aggregated across every tenant, so it is operational data rather than a measurement of your workloads or a service-level guarantee.
The single control is the direct-fallback deadline: 3m, 5m, 10m, 15m
or 20m, defaulting to 10 minutes. A direct-fallback deadline is the point at
which a caller stops waiting on the batch and sends a normal request instead. It
is the same number you would pass as deadline_seconds to
batch_first().
Per provider:
| Column | What it holds |
|---|---|
| Model | The probed model |
| Completed | Number of probes that finished |
| Failed | Number that did not |
| p50, p90, p95 | Completion time percentiles |
An All models row pools the provider’s probes.
The headline figure is Expected relative cost at this deadline, one of:
N% cheaper than always using the direct API, expectedN% more than always using the direct API, expected. Duplicate cost on a missed deadlineAbout the same as always using the direct API, expectedUnavailable: the catalog has no batch price for a contributing model
Beneath it, the share of completed probes that finished inside the deadline. A completion-time histogram follows, with a low-sample warning under five probes.
Empty state: No completed or failed probes yet. Probes run on a four-hour schedule per provider, check back soon.
Settings
Section titled “Settings”#settings.
Organization. Rename the workspace. Owners only, and read-only on a BYOC deployment, where the footer reads “Workspace settings are managed by your operator.” The permanent workspace slug is shown beneath and cannot be changed.
People. Invite by email with a role of Member or Viewer, defaulting to
Member. The table shows each person’s name, email, role, status and join or
invite date, with Resend for a pending invitation. Roles are owner,
member and viewer.
| Role | Can do |
|---|---|
owner | Everything, including renaming the workspace and inviting people |
member | Everything operational, including creating and disabling ingest keys |
viewer | Read. Cannot acknowledge an alert or manage keys |
Run Metergraph locally. The Docker Compose setup, with a copyable command block. See Run it locally.
Archive. Pages that left the navigation during the interface rebuild and stay routable until each capability re-homes: Overview, Functions, Routes, Models and Datasets.
Ingest keys
Section titled “Ingest keys”#api-keys, labeled Keys in the sidebar. The header chip reads {n} active.
Create a key by naming it. The token is shown exactly once, in a highlighted block with a copy button and a Dismiss, I’ve stored it button. Only its SHA-256 digest is stored, so a lost key must be replaced rather than recovered.
| Column | What it holds |
|---|---|
| Key | The label you gave it, with its masked hint and creation date beneath |
| Status | active or disabled |
| Disable, which is immediate and irreversible |
Every dashboard-created key carries exactly the ingest scope. The read and
agent:read scopes are operator-CLI only, which is why the MCP server needs a
key the dashboard cannot mint. See
Use the MCP server.
Below the table, the same Wrap your client snippets as Get started, with tabs for Python, TypeScript and Agent. The snippet substitutes your real token while a key reveal is open.
Viewers cannot create or disable keys and see “Ask a workspace member to create an ingest key.”
Get started
Section titled “Get started”#get-started, titled See your first trace. It leaves the sidebar once a
trace has been processed, and stays reachable at its URL.
Three steps, polled every five seconds while the tab is visible until a trace arrives.
- Create a key. Collapses to a done row once one exists.
- Wrap your client. The language-tabbed snippet.
- First trace. The status of ingestion:
| State | What it says |
|---|---|
| No key yet | Waiting on an ingest key |
| Key, nothing received | Waiting for your first trace, with a live indicator |
| Received, still processing | Got your data. Processing |
| Received, processing failed | Received, but processing failed. Check your key, or see the docs |
| Received, delayed upstream | Received. Processing delayed by an upstream outage |
| Done | First trace processed |
When the first trace lands, step three is replaced by a card naming the trace, its model, its token count and its cost, with a link into Traces. A trace that arrived but errored says so plainly rather than declaring success.
#admin. Platform administrators only, and not present on a BYOC deployment.
Product health, workspace activation and audited read-only access. It shows other
people’s workspaces, so it is documented here only so you know what it is.
Impersonation is read-only, includes customer-visible prompt and response content, and is recorded in an audit log until the administrator exits or signs out. Every tab sharing the browser session switches with it, and a banner says so on every page.
Archived pages
Section titled “Archived pages”These left the navigation and remain routable.
| Page | URL | What it shows |
|---|---|---|
| Overview | #overview | Trace counts, span counts, error rate and ingestion health, plus provider, function and model breakdowns |
| Functions | #functions | Cost by call site, with a search box |
| Models | #models | Spend by provider and by model |
| Routes | #routes | Cost attributed by route |
| Datasets | #datasets | Coverage dataset construction. See Build a dataset |
Two things the dashboard does not do
Section titled “Two things the dashboard does not do”It does not persist filters. Every window, environment, grouping, metric, subset and deadline resets on reload. The URL fragment carries only the route and any report identifiers, so a link you send someone opens the page, not your view of it.
It does not recompute cost. Every currency figure on Performance and Traces
is a sum of the per-call cost_usd written at ingest. The one figure computed
against the catalog at read time is Batch API’s expected relative cost, which is
why it is the one place that can say the catalog has no batch price for a model.