How to Create a Testing Dashboard That Works
Most teams wire up a test reporter, glance at the green/red summary, and call it observability. The dashboard they're missing isn't a prettier pass/fail table — it's a time-series view of failure rates, runtime drift, retry ratios, and per-team ownership that turns a CI run into an engineering signal. The difference between a team that ships confidently and one that argues over "is the suite stable?" is almost always a data model, not a test count.
The core problem: JUnit XML gets consumed once per run and discarded. Nothing accumulates. Without persistence, you can't answer "did this test get slower over the last 30 deploys?" or "which team owns the five tests that appear in every incident postmortem?" Those questions require a store, a schema, and a query layer — not just a CI plugin.
By the end of this article you'll have a concrete architecture for a testing dashboard — data pipeline, schema, Grafana panels, and a Python flakiness scorer — that answers the questions your on-call rotation is already asking.
Learn Python, Behave, GitHub Copilot, APIs, and CI/CD by building a real framework you can finish in a weekend.
What a Testing Dashboard Actually Stores (and Why Pass/Fail Isn't Enough)
A testing dashboard is a persistent, queryable record of test execution history wired to a visualization layer. The key word is persistent: every CI run writes structured rows — test name, suite, duration, status, retry count, branch, commit SHA, runner ID — into a store you own. Grafana, Datadog, or a custom React app sits on top. The store is the product; the UI is interchangeable. Teams that skip the store and go straight to a dashboard plugin end up with a pretty view of a single run and no trend data.
In a modern CI architecture this layer sits between your test runner (Pytest, Playwright, JUnit, k6) and your incident tooling (PagerDuty, Slack). It feeds test traces alongside logs and metrics so a failure in staging can be correlated with a deployment event or an infra anomaly — not just a red dot on a status page. PostgreSQL and ClickHouse are the two most common stores; ClickHouse wins on query speed at scale (tens of millions of rows), PostgreSQL wins on operational simplicity for teams under ~5M rows/month.
Building the CI Dashboard: Schema, Ingestion Pipeline, and Grafana Panels
Start with a schema that captures everything a single test execution knows about itself. In PostgreSQL:
CREATE TABLE test_runs (
id BIGSERIAL PRIMARY KEY,
run_id TEXT NOT NULL, -- CI build ID
suite TEXT NOT NULL,
test_name TEXT NOT NULL,
status TEXT NOT NULL, -- passed | failed | skipped | flaky
duration_ms INTEGER NOT NULL,
retry_count SMALLINT DEFAULT 0,
branch TEXT,
commit_sha TEXT,
runner_id TEXT,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ON test_runs (test_name, recorded_at DESC);
CREATE INDEX ON test_runs (suite, status, recorded_at DESC);
The retry_count column is the one most schemas omit and later regret — it's the primary signal for flakiness detection. A test that passes on retry 2 every time is not a passing test; it's a liability. For ingestion, parse JUnit XML out of CI artifacts and POST rows in bulk. This GitHub Actions step runs after your test job:
- name: Ingest test results
if: always()
run: |
python scripts/ingest_junit.py \
--xml-glob "reports/**/*.xml" \
--run-id "${{ github.run_id }}" \
--branch "${{ github.ref_name }}" \
--commit "${{ github.sha }}" \
--db-url "${{ secrets.TESTDB_URL }}"
if: always() is non-negotiable — you need failure data, not just success data. The Python ingester uses junit-xml or plain xml.etree to walk test cases, maps status to your enum, and bulk-inserts with psycopg2.extras.execute_values for throughput. Once data accumulates, the useful Grafana panels become straightforward. A pass/fail trend panel uses a simple time-series query:
SELECT
date_trunc('hour', recorded_at) AS time,
status,
COUNT(*) AS count
FROM test_runs
WHERE recorded_at > NOW() - INTERVAL '7 days'
AND branch = 'main'
GROUP BY 1, 2
ORDER BY 1;
Wire that to a Grafana time-series panel with status as the series field and you have a rolling pass/fail ratio that shows degradation trends before they become incidents. For the flakiness scorer — the panel most teams are missing — run a nightly Python job that computes a flake score per test: (retry_count_sum / total_executions) * failure_rate, then writes results to a flake_scores table Grafana queries for a leaderboard. Teams that wired this dashboard to Loki for log correlation reported triage time dropping from 22 minutes per failure to under 4, because the failing test row links directly to the log stream for that run_id. For a more detailed walkthrough of the CI-specific panels, the step-by-step CI dashboard guide covers Grafana panel JSON and variable templating in depth.
Where Testing Dashboard Builds Break Down in Practice
The most common mistake is scoping the dashboard to a single team's suite and never normalizing test names across repos. When a platform team runs the same integration tests from three different pipelines, you get three disconnected data sets and no cross-team visibility. Fix this at ingestion time: enforce a canonical suite::test_name convention in your ingester, not in the dashboard query. The second mistake is ignoring skipped tests. Skipped counts that grow quietly over months are a leading indicator of test debt — suites that are silently shrinking in coverage while pass rates look healthy.
The third mistake is dashboard-as-destination thinking: the team builds a beautiful Grafana board, shares the link in Slack, and stops there. A dashboard nobody acts on is a vanity metric with extra steps. Wire alerts — a Grafana alert rule or a Datadog monitor — that fires when the 7-day flake rate for a suite crosses a threshold (say, 8%). That's what turns a dashboard into a feedback loop. The metrics that actually drive decisions are the ones attached to an owner and a threshold, not just a panel.
What Most Teams Get Wrong About Testing Dashboards
Myth 1: Pass/fail rate is the primary signal. It's the least actionable number on the board. A suite with a 97% pass rate can still be destroying developer confidence if the same 12 tests flake on every deploy. Duration P95 and retry rate per test are the numbers that predict pipeline pain. Myth 2: A third-party dashboard (Allure, the Cypress Dashboard) replaces a data store. Allure is excellent for per-run HTML reports and is the right call when you need rich step-level detail for a single run — but it doesn't give you cross-run trend queries or custom flakiness scoring. Use Allure for drill-down; use your own store for trend analysis. The Cypress Dashboard has similar limits — strong for parallelization insights, weak for historical trend queries you control.
Myth 3: Building the dashboard is a QE task. The data pipeline that feeds it is infrastructure — it needs the same care as any other internal platform: schema migrations, retention policies, SLOs on ingestion latency. Teams that treat it as a side project end up with a broken pipeline nobody owns and stale data that erodes trust in the dashboard itself. Assign it an owner, version the schema, and set a data-freshness SLO. That's the difference between a dashboard teams trust and one they stop opening.
A testing dashboard earns its place when it shortens the gap between "the suite is broken" and "here's the test, the owner, and the log." Start with the schema above, get one week of data into PostgreSQL, and build the pass/fail trend and flake leaderboard panels first — those two surfaces answer 80% of the questions your team is already asking in Slack. From there, maturing toward quality engineering means wiring those signals to ownership, SLOs, and incident workflows rather than treating the dashboard as the end state.
Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.