A CTO can spend months choosing a dashboard and still have no defensible answer to a simple question: how often did each service reach production? I would buy the reporting interface and build the small, version-controlled data contract behind it. A custom interface is the wrong first investment because the expensive mistakes usually sit in event definitions, ownership, and access to source data.
Define one production event before drawing a chart
Start with a question that a release lead can check against a deployment log: “How many successful production deployments did each service complete last week?” It is narrow enough to expose disagreements before they become dashboard conventions. A successful GitHub Actions workflow is not necessarily a production deployment, because a workflow may stop after tests or publish to a staging environment. Likewise, counting commits on a release branch can inflate the answer when several commits ship together.
Write the event definition in the reporting repository. One event should have a stable source ID, service name, production environment, completion timestamp in UTC, commit SHA, and a final successful status. Assign one owner to decide whether a rollback counts as another deployment and document that decision beside the SQL. Without that owner, two teams can produce different weekly totals from the same source records and both reasonably call them correct.
GitHub’s Deployments API and deployment statuses can supply the first source if those records reflect the actual release path. If they do not, capture events from the release system instead; a convenient API is not a reason to report a fictional process. GitHub’s published maximum of 100 records per page on the relevant REST list endpoints means an extractor must follow pagination rather than assume its first response is complete. Use the 2022-11-28 GitHub REST API version header, give its token only the repository permissions it needs, and keep that token out of the reporting database.
Do not join Jira Cloud REST API v3 issues into this first metric merely to make the page look richer. Issue-to-commit links are often incomplete or many-to-many, so the resulting “lead time” would be hard to reconcile. Add that measure only after the team can state which issue transition starts its clock, which deployment stops it, and how it handles work without an issue. The first deliverable is a definition another engineer can reproduce, not a collection of tiles.
A local vertical slice should work before anyone approves a platform
Run a disposable slice on a machine with Docker installed. The commands below create a PostgreSQL 16 database with one sample production event and launch Metabase on the same Docker network. They are a local demonstration, not production configuration: the password is deliberately temporary, the database has no mounted volume, and the Metabase image is unpinned.
docker network create reports-demo
docker run --name reports-pg --network reports-demo -e POSTGRES_PASSWORD=localonly -d postgres:16
until docker exec reports-pg pg_isready -U postgres -q; do sleep 1; done
docker exec reports-pg psql -U postgres -v ON_ERROR_STOP=1 -c \
"CREATE TABLE deployments (id text PRIMARY KEY, service text NOT NULL,
environment text NOT NULL, finished_at timestamptz NOT NULL,
succeeded boolean NOT NULL);"
docker exec reports-pg psql -U postgres -v ON_ERROR_STOP=1 -c \
"INSERT INTO deployments VALUES ('d-1','api','production',now(),true);"
docker run --name reports-ui --network reports-demo -p 127.0.0.1:3000:3000 -d metabase/metabase:latest
Open Metabase at localhost:3000 and add a PostgreSQL connection using host reports-pg, database and user postgres, and the temporary password. Create a question that filters environment to production and succeeded to true, groups finished_at by week, and counts rows by service. The seeded database yields 1 successful deployment for api; that result checks the path from stored event to visible answer, not the accuracy of any live release data.
Next, replace the sample insert with a small scheduled extractor. Fetch deployment records and their latest statuses, map them to the agreed event fields, and upsert by source ID so a retry cannot double-count a release. Keep a watermark for efficient fetching, but revisit a recent window on every run because a status can arrive after its deployment record. As a starting setting to tune against actual release cadence, refresh every 15 minutes; display the last successful extraction time beside the chart so an old answer cannot masquerade as a current one.
Before exposing this to the wider organization, move credentials to a secret manager, pin container images, persist both databases, and back them up. Use a read-only reporting account for Metabase and a separate write account for ingestion. Metabase’s application database also needs durable storage: otherwise losing the container can lose saved questions and permissions even if the deployment rows survive. A production setup is working only when a second engineer can rebuild it and verify the same count.
Metabase beats a custom interface until its limits are observable
The explicit choice is self-hosted Metabase versus a React and Apache ECharts application backed by a bespoke API. Metabase wins the first release when the audience needs shared, filterable tables and charts over a curated PostgreSQL model: it removes interface work while leaving metric definitions in SQL that engineers can review. A custom application wins when reporting must sit inside an existing product, follow unusual interactions, or enforce access rules that the chosen Metabase deployment cannot express cleanly. That advantage costs engineering time and ongoing responsibility for authentication, queries, accessibility, exports, and upgrades.
For a budget conversation, use 3 engineer-days to get a controlled Metabase pilot in front of reviewers and 15–25 engineer-days for a comparable bespoke first release as planning assumptions, not vendor prices or promises. Replace those estimates after a short spike against your own identity provider and deployment process. Self-hosting is not free: someone still owns patches, backups, and permission reviews. Conversely, “we already use React” does not make a reporting application cheap, because reusable components do not settle what a deployment means.
I would not build the custom interface first, because it commits the team to maintaining presentation code before anyone has tested whether the underlying numbers deserve distribution. I would also avoid buying an all-in-one engineering-metrics product solely for its default scorecard, because its definitions may not match the organization’s release events and exceptions. Buying such a product becomes defensible when a trial proves it can ingest those events, expose its calculation rules, and meet the organization’s access requirements with less maintenance than the smaller setup.
I would treat Modern Dashboard Reporting for Software Development Teams as a destination rather than a starting requirement, because the first purchasing decision should turn on a reconciled answer, not the breadth of the presentation. That order also makes a later migration less painful: when definitions and event IDs live outside the interface, the team can replace Metabase without rewriting its account of what happened.
Reconciliation is the acceptance test for reporting
Pick a recent week and ask a release lead for the production deployments they can verify independently. Compare that list, ID by ID, with the PostgreSQL rows and the Metabase result. Investigate omissions before adjusting a chart: missing pagination, staging mislabeled as production, duplicate webhook deliveries, and late status changes each require a different fix. Record exceptions and their resolution in the repository so a future change to the extractor does not reintroduce them.
Add an automated check that rejects blank service names and duplicate source IDs, and another that alerts when extraction stops succeeding. dbt Core can hold SQL model tests if the team already runs it; a small scheduled SQL check is sufficient if it does not. Export OpenTelemetry metrics or use an existing Prometheus scrape to observe extractor failures and data age, but keep those operational signals separate from delivery metrics. An extractor’s uptime cannot establish that its deployment count is right; only comparison with an independent release record can do that.
Control access at the data source, not only in a dashboard collection. A read-only PostgreSQL role limits accidental edits, while a curated view can omit repository details that the audience does not need. If teams must see different rows, test that requirement with representative accounts before choosing a product tier or writing custom authorization. OAuth 2.0 or OpenID Connect can authenticate a viewer, but authentication alone does not decide which deployment records that viewer may inspect.
IT Visualization and Reporting for Software Teams can describe the broader reporting ambition; I would hold that ambition behind the reconciliation test, because adding sources multiplies the number of plausible but conflicting answers. Once the weekly deployment count matches the release record, add the next question as another documented contract. If it cannot be reconciled, stop adding charts and fix the source path.
The first decision is who can verify a production release
Tomorrow, ask one release owner to identify a week of production deployments and grant a scoped way to retrieve their source records. Have an engineer write the event definition and reconcile that week before scheduling a vendor demonstration or commissioning an interface. If the records cannot be obtained or trusted, that is the decision to resolve first; neither a purchase nor a polished chart will repair it.


