Date: 2026-05-08 Status: Proposed
We produce and merge a large volume of changes across fullsend-ai repos but have no visibility into how that volume and velocity change over time. We need a lightweight, self-hosted dashboard that tracks key SDLC metrics without SaaS dependencies.
- Track deployment frequency, PR lead time, PR volume, and issue volume across all fullsend-ai repos
- Show trends over time with historical data back to April 1, 2025
- Publish a public, single-page D3.js dashboard via GitHub Pages
- Run autonomously on a daily cron with zero maintenance
- DORA change failure rate or mean time to recovery
- Per-contributor breakdowns
- Alerts, notifications, or Slack integration
- SaaS dependencies of any kind
| Metric | Definition | Source |
|---|---|---|
| Merge frequency | Merges to default branch per day | GitHub API: merged PRs with base:main |
| Release frequency | Tagged releases per day | GitHub API: releases endpoint |
| PR lead time | Median hours from PR open to merge | GitHub API: PR created_at vs merged_at |
| PR volume | PRs opened, merged, closed per day | GitHub API: PR search by date |
| Issue volume | Issues opened, closed per day | GitHub API: issue search by date |
All metrics are computed per repo and aggregated org-wide.
A single append-only CSV file at data/metrics.csv:
date,repo,prs_opened,prs_merged,prs_closed,issues_opened,issues_closed,releases,pr_lead_time_median_hours
2025-04-01,fullsend,3,2,1,5,3,0,4.5
2025-04-01,experiments,1,1,0,0,0,0,1.2
One row per repo per day. The file grows by N rows per day where N is the number of repos in the org. At ~30 repos and 365 days, that's ~11K rows/year — trivially small.
A shell script that:
- Discovers all repos in the org via
gh api /orgs/fullsend-ai/repos - For each repo, queries the GitHub API for the previous day's metrics:
GET /search/issues?q=repo:{repo}+is:pr+created:{date}for PRs openedGET /search/issues?q=repo:{repo}+is:pr+is:merged+merged:{date}for PRs mergedGET /search/issues?q=repo:{repo}+is:pr+is:closed+closed:{date}for PRs closedGET /search/issues?q=repo:{repo}+is:issue+created:{date}for issues openedGET /search/issues?q=repo:{repo}+is:issue+is:closed+closed:{date}for issues closedGET /repos/{repo}/releasesfiltered by date for releases- PR lead time computed from merged PRs'
created_atandmerged_attimestamps
- Appends rows to
data/metrics.csv - Copies
data/metrics.csvintodocs/for D3 to load
Uses gh api with pagination and jq for JSON processing. No external dependencies beyond gh and jq (both available in GitHub Actions runners).
A one-time script that iterates from 2025-04-01 to today, calling the same collection logic for each day. Produces the initial data/metrics.csv. Respects GitHub API rate limits with small delays between days.
Rate limit considerations:
- GitHub search API allows 30 requests/minute for authenticated users
- Each repo-day requires ~6 search queries
- For 30 repos, one day = ~180 queries = ~6 minutes
- Backfilling ~400 days = ~40 hours at full serial speed
- The backfill script will checkpoint progress so it can resume if interrupted
name: Collect metrics
on:
schedule:
- cron: '0 6 * * *' # 06:00 UTC daily
workflow_dispatch: {} # manual trigger
jobs:
collect:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Collect yesterday's metrics
run: ./scripts/collect.sh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/ docs/
git diff --staged --quiet || git commit -m "metrics: $(date -d yesterday +%Y-%m-%d)"
git pushGITHUB_TOKEN scoped to the org provides read access to all public repos. If private repos need tracking, a GitHub App or PAT with broader scope would be needed.
A single HTML page served by GitHub Pages (from the docs/ directory on main).
Technology: D3.js loaded from CDN. No build step, no bundler, no framework.
Layout:
- Summary cards (top) — current week vs. previous week for each metric, with delta indicators showing percentage change
- Merge + release frequency — D3 line chart, dual y-axes (merges left, releases right), org-wide aggregate with per-repo tooltip on hover
- PR lead time trend — D3 line chart showing median hours over time
- PR volume — D3 stacked area chart: opened vs. merged vs. closed
- Issue volume — D3 area chart: opened vs. closed
- Per-repo breakdown — HTML table below the charts, sortable by any column, showing current-week stats per repo
Data loading: d3.csv("metrics.csv") loads the CSV directly. The collection step copies the data file into docs/ so it's served alongside the HTML.
Styling: Minimal CSS. Respects prefers-color-scheme for dark/light mode. Responsive layout using CSS grid.
Interactivity:
- Time range selector (last 7d / 30d / 90d / all)
- Per-repo filter (dropdown or clickable legend)
- Hover tooltips on chart data points
The dashboard draws horizontal reference lines on relevant charts showing where fullsend-ai falls relative to DORA tiers:
| Metric | Elite | High | Medium | Low |
|---|---|---|---|---|
| Deployment frequency | Multiple per day | Weekly-monthly | Monthly-biannually | < once per 6 months |
| Lead time for changes | < 1 hour | 1 day-1 week | 1 week-1 month | 1-6 months |
fullsend-ai/metrics/
├── .github/workflows/collect.yml # daily cron Action
├── scripts/
│ ├── collect.sh # daily metric collection
│ └── backfill.sh # one-time historical seed
├── data/
│ └── metrics.csv # append-only data
├── docs/
│ ├── index.html # D3 dashboard (single page)
│ ├── style.css # minimal styling
│ └── metrics.csv # copy of data file for Pages
└── README.md
- Shell +
gh+jqover Python: Keeps dependencies minimal. GitHub Actions runners haveghandjqpreinstalled. No virtualenv, no pip, no runtime to manage. - CSV over JSON: D3 loads CSV natively with
d3.csv(). Human-readable in git diffs. Easy to append to. JSON would require read-modify-write. - Single HTML file over static site generator: No build step. One file to maintain. D3 handles all rendering client-side.
docs/directory overgh-pagesbranch: Simpler — no branch management, the dashboard source lives alongside the data.- Daily granularity over hourly: Sufficient for trend analysis. Keeps API usage low. Avoids partial-day noise.
- Org-wide dynamic repo discovery: No hardcoded repo list to maintain. New repos automatically appear in the next day's collection.
- Both merge and release frequency: Reported as separate metrics since merge-to-main and tagged releases represent different deployment signals.
The dashboard has grown beyond the original single-page overview:
| Area | Files | Notes |
|---|---|---|
| Overview | docs/index.html, docs/dashboard.js |
Core SDLC metrics, community, rework, failures |
| Drill-down | docs/details.html |
Per-day detail view (linked from overview charts) |
| Delivered PR types | docs/delivered-pr-types.html |
Conventional-commit mix + fix-filer attribution |
| Quality signals | docs/quality.html, docs/quality.js |
Defect-labeled and revert-event rates |
Navigation: index.html, delivered-pr-types.html, and quality.html share a tab header. details.html is a drill-down page reached from overview charts, not a peer tab.
Delivered PR Types defaults: 90-day range and 28-day smoothing (vs. 30d / none on the overview) because delivery-mix trends are noisy at daily granularity.
Tracks merged PR conventional-commit types and fix-filer attribution for fullsend and agents:
docs/pr-type.csv— daily aggregates per repodocs/pr-type-details.csv— per-PR drill-downscripts/collect-pr-type.sh+scripts/collect-pr-type.py— daily collection (wired incollect.yml)scripts/backfill-pr-type.sh— date-range backfill
The Quality Signals tab derives GitHub-based defect and revert indicators from the PR-type datasets and repository metadata:
docs/quality.csv— daily aggregates per tracked repositoryscripts/collect-quality.sh+scripts/collect-quality.py— daily collection (wired after PR types incollect.yml)scripts/backfill-pr-type.sh— rebuilds quality rows after a PR-type backfill
Defect rows require a defect-labeled linked issue. Revert rows recognize both standalone and typed Conventional Commit revert subjects, plus Git's standard revert marker, while excluding temporary changes reverted inside the same merged PR. These are engineering signals rather than deployment-based DORA metrics. The defect-labeled rate is an explicit-label proxy: it does not establish that a defect was preventable or distinguish a review-escaped bug from a missed requirement. A future preventable-defect metric needs an agreed label taxonomy.
Python exception: Most collectors remain shell + gh + jq. The PR-type
and Quality Signals collectors use Python for cross-API orchestration and
idempotent CSV generation. The PR-type collector needs adaptive search-window
splitting, issue-reference parsing, per-issue author caching, and per-(date,
repo) writes; the Quality Signals collector correlates PR details with issue
labels and commit/PR history, including same-PR revert cleanup. Python is
available on Actions runners without extra setup.