Own your project's growth data, in a repository you control.
What you get:
- 📈 Traffic history beyond 14 days. Views, clones, referrers, and popular pages, kept forever.
- ⭐ Real star history. See when your stars arrived and what caused the spike.
- 📦 Downloads. GitHub release assets, PyPI, and npm.
- 🤝 Community activity. Issues opened vs closed, PRs merged, first-time contributors, time to merge.
- 🧮 41 governed metrics you or your agent can query. Defined once in MetricFlow, tested, and served through Dex.
- 🦆 A DuckDB file for the lowest level of detail. Every row lives in
warehouse/analytics.duckdb, and the raw history is plain CSV in a branch you own.
Metrics are defined once (dbt + MetricFlow), tested, and queryable by you or your coding agent through Dex.
It runs free on GitHub Actions. No paid warehouse, no SaaS, no LLM API.
GitHub's Insights → Traffic page has real limits:
- It forgets everything after 14 days.
- It shows only the top 10 referrers.
- It has no history for stars, downloads, or contributors.
This template fixes that and answers the questions maintainers actually ask. Each question maps to a ready-made recipe (see the question cookbook):
| Question | Recipe |
|---|---|
| Are views and clones growing week over week? | growth, traffic-trend |
| When did my stars arrive, and what caused the spike? | star-history, event-impact |
| Where does traffic come from, and how is that changing? | referrers, referrer-trend, referrer-change |
| What do visitors read? | paths, path-change |
| How many PyPI / npm / release downloads did we get? | package-downloads, release-downloads |
| Is the community healthy? Issues opened vs closed, PRs merged, first-time contributors, time to merge | community |
| How do my projects compare? | compare |
| What happened this week? | weekly-report |
| Are the numbers fresh and complete enough to trust? | status |
Run any recipe with uv run --frozen oss-analytics question <recipe>.
This repository tracks Dex, an open-source project for analytics engineering agents. Everything below is real output from the warehouse you build in Try it in two minutes, captured on 2026-10-22. Periods are end-exclusive.
status reports 120 observed traffic days for exmergo/dex, from 2026-06-23 to 2026-10-20, with no missing day: 11,462 views and 23,457 clones. GitHub's own Traffic page would show the last 14 of them.
question traffic-trend --repository exmergo/dex --days 91 --grain week, the last eight complete weeks:
week views clones
2026-08-24 621 1,022
2026-08-31 1,369 2,688
2026-09-07 324 1,132
2026-09-14 129 472
2026-09-21 119 513
2026-09-28 316 1,155
2026-10-05 976 1,113
2026-10-12 1,761 5,351
question star-history --repository exmergo/dex --days 91 --grain week. Stars crept from 22 to 25 between mid-August and late September, then:
week stars_gained forks_created stars_from_history
2026-09-21 0 0 25
2026-09-28 0 0 25
2026-10-05 36 1 61
2026-10-12 27 12 88
Was it a release? question event-impact compares the 7 days before and after Dex v1.12.6 (2026-10-06). Views went from 343 to 1,523 (+344%) and clones from 1,147 to 1,895 (+65%). The recipe does not credit the release: it names the six other releases inside those windows and labels the result "association only".
Where did the visitors come from? question referrer-change compares GitHub's rolling 14-day top 10 on two exact capture days:
referrer 2026-09-22 2026-10-22
exmergo.com 15 588
github.com 83 77
linkedin.com 4 13
reddit.com 15 2
hn.algolia.com 1 (not in top 10)
In the 14 days before 2026-10-22, the project's own website sent 588 views, up from 15. A referrer that drops out of the top 10 is reported as unreported, never as zero.
question community --repository exmergo/dex --days 28 (2026-09-24 to 2026-10-22):
issues opened issues closed PRs opened PRs merged median time to merge
175 95 118 106 11.6 hours
question package-downloads --repository exmergo/dex --days 28, PyPI downloads of exmergo-dex-core per complete week: 894, then 1,611, then 1,651.
question weekly-report returns traffic growth, stars, community, and downloads in one envelope. Its release-downloads section for Dex reads:
This repository has no release assets captured; GitHub counts downloads only for uploaded assets, not source archives.
Every answer is a JSON envelope with the exact period, metric names, coverage, freshness, source revision, and code digest. When the data cannot support an answer, it says so, and missing data is unavailable, never zero.
Prerequisites
- Git
- uv 0.13.1+
- macOS or Linux (Windows via WSL)
1. Clone and bootstrap
git clone https://github.com/exmergo/github-repo-analytics.git
cd github-repo-analytics
bash scripts/bootstrap.sh2. Collect, build, and check the data
uv run --frozen oss-analytics sync # import and collect raw data
uv run --frozen oss-analytics build # build the tested models
uv run --frozen oss-analytics baseline # record the accepted baseline
uv run --frozen oss-analytics check # verify against the baselineTip
Want star history? GitHub only serves star dates to authenticated requests. Run this before sync (any token works, no scopes needed):
export GITHUB_TOKEN=$(gh auth token)Without a token, star-history reports itself unavailable.
3. Ask a question
uv run --frozen oss-analytics question weekly-report --repository exmergo/dexYou get the numbers in Real answers, updated to the latest published day. The example imports Dex's public traffic history and collects its forks, issues, releases, and PyPI downloads live. The second source is this repository itself, so --repository exmergo/github-repo-analytics shows the template tracking its own launch.
Production shipping is also available as an optional deployment source: count successful production deployments and measure median merge-to-deployment hours for shipped PRs. GitHub Deployments is supported; workflow-run and release inference are not yet supported.
4. Query the governed metrics
Recipes cover the common questions. For everything else, query the semantic layer: 41 metrics defined once in the model YAML, compiled by MetricFlow, and run through Dex. This is the path for agents. Metric names are discovered rather than guessed, every metric is tested, and MetricFlow handles the joins and time grains, so the answer is the same number every time.
Discover what exists. Each metric carries its meaning in its description (condensed from the JSON envelope):
uv run --frozen dex explore semantic list --localviews simple Total GitHub page views. May include automated activity.
stars_gained simple Stars from current stargazers by the day they starred. ...
stars_from_history cumulative Running total of current stargazers at the end of each period ...
views_wow_growth derived Relative change in views versus the previous week; query at week grain ...
... 41 metrics
Read a dimension's values before filtering on it:
uv run --frozen dex explore semantic values repository__name --metric views --local
# exmergo/dex, exmergo/github-repo-analyticsQuery several metrics at once, by month, for complete months:
uv run --frozen dex explore semantic query views,clones,stars_gained,prs_merged,pypi_downloads \
--group-by metric_time --grain month \
--where "{{ Dimension('repository__name') }} = 'exmergo/dex'" \
--where "{{ TimeDimension('metric_time', 'day') }} >= '2026-07-01' and {{ TimeDimension('metric_time', 'day') }} < '2026-10-01'" \
--order-by metric_time --localmonth views clones stars_gained prs_merged pypi_downloads
2026-07 3,167 4,645 15 77 5,070
2026-08 3,274 5,013 9 83 4,857
2026-09 1,728 4,772 1 54 2,587
Derived metrics work the same way. Here, week-over-week growth:
uv run --frozen dex explore semantic query views,views_wow_growth,stars_gained \
--group-by metric_time --grain week \
--where "{{ Dimension('repository__name') }} = 'exmergo/dex'" \
--where "{{ TimeDimension('metric_time', 'day') }} >= '2026-09-21' and {{ TimeDimension('metric_time', 'day') }} < '2026-10-19'" \
--order-by metric_time --localweek views views_wow_growth stars_gained
2026-09-21 119 -7.8% 0
2026-09-28 316 +165.5% 0
2026-10-05 976 +208.9% 36
2026-10-12 1,761 +80.4% 27
Each answer is a JSON envelope (data.columns, data.cells, warnings, and whether the result was truncated). Dex returns at most 50 rows per query, so pick a coarser grain rather than reading a truncated result. Filters use MetricFlow's Dimension and TimeDimension syntax. AGENTS.md has the full rules for reading each metric.
5. Go to the lowest level of detail in DuckDB
build writes everything to warehouse/analytics.duckdb:
| Schema | What is in it |
|---|---|
raw |
Each source as collected, for example traffic, stargazer_days, fork_days, issues, discovery (referrers and paths), observations, releases, release_assets, package_downloads, optional deployments and deployment_changes |
main.stg_* |
Typed staging views over raw |
main.fct_*, main.dim_* |
The tested marts the metrics read: fct_traffic (one row per repository and day), fct_star_history, fct_fork_history, fct_issues, fct_referrer, fct_path, fct_package_downloads, fct_release_downloads, fct_observations, optional fct_deployments and fct_deployment_lead_times, dim_event, dim_repository |
Open it with any DuckDB client. With the Python package that bootstrap.sh already installed:
uv run --frozen python -c "import duckdb; print(duckdb.connect('warehouse/analytics.duckdb', read_only=True).sql('''
select strftime(date, '%Y-%m') as month, sum(views_total) as views, sum(clones_total) as clones
from fct_traffic where repository = 'exmergo/dex' group by 1 order by 1'''))"┌─────────┬────────┬────────┐
│ month │ views │ clones │
│ varchar │ int128 │ int128 │
├─────────┼────────┼────────┤
│ 2026-06 │ 228 │ 97 │
│ 2026-07 │ 3167 │ 4645 │
│ 2026-08 │ 3274 │ 5013 │
│ 2026-09 │ 1728 │ 4772 │
│ 2026-10 │ 3065 │ 8930 │
└─────────┴────────┴────────┘
Or use the DuckDB CLI (duckdb -readonly warehouse/analytics.duckdb), or uv run --frozen dex explore query "<SELECT ...>", which reads the same file through Dex's read-only, PII-aware guard.
Open it read-only: build replaces the file from the raw data each time. Ad hoc SQL is for exploring. For numbers you report, use the recipes or the governed metrics, which already handle the traps. For example, summing views_unique across days counts the same person again every day.
No lock-in: the raw history lives in this repository's github-repo-stats branch as plain files:
- per project,
ghrs-data/views_clones_aggregate.csvand the daily top-10 referrer and path snapshots; signals/*.csvfor stars, forks, and issues, plus optionalsignals/deployments.jsonfor shipping evidence;metadata/*.jsoncaptures.
Any tool can read them, even if you stop using this template.
-
Create a repository from this template. It opens a setup checklist issue for you.
-
List your projects in
sources.yml:repositories: - repository: your-org/your-project mode: collect packages: # optional; omit if you publish none pypi: [your-project]
See Track package downloads for npm and more options.
-
Give the workflow access:
- Add a
GHRS_TOKENsecret: a fine-grained token with Administration: read on tracked repos and Contents: read/write on this repo (how to create it). - Set the
ANALYTICS_ENABLEDrepository variable totrue(details).
- Add a
The full walkthrough is in the setup guide.
What happens next: a daily workflow collects traffic, referrers, paths, stars, forks, issues, releases, and downloads into a versioned github-repo-stats branch. It then rebuilds the models, checks them, and publishes only a validated revision. If a scheduled run fails, it opens an "Analytics collection is failing" issue, and raw data already collected is kept.
GitHub only keeps traffic for 14 days, so traffic history starts the day you turn collection on (star, fork, and issue history is backfilled). Set it up before your next launch, not after.
One analytics repository can track many projects.
Important
Aggregates are as visible as your analytics repository. Collected data lives in its github-repo-stats branch, so a public analytics repository publishes traffic counts, referrers, and popular paths that normally need push access to see. Make it private to keep them private, and to track private repositories. No logins, names, emails, or issue text are ever stored. See SECURITY.md.
Point any coding agent at AGENTS.md and ask in plain language. Asked "did the v1.12.6 release drive traffic?", an agent following it:
- checks
status, and sees the data is fresh with no missing days; - picks the
event-impactrecipe rather than writing SQL; - answers with the numbers above: views +344% in the 7 days after, clones +65%. It also says this is an association, and that six other releases fall inside the same windows.
When no recipe fits, it discovers the 41 governed metrics in the model YAML through Dex, so it never guesses a metric name.
uv run --frozen oss-analytics status --json # is the data ready?
uv run --frozen oss-analytics questions # list available recipes
uv run --frozen dex explore semantic list --local # list governed metrics
uv run --frozen dex explore semantic query views,views_wow_growth,stars_gained \
--group-by repository__name,metric_time --grain week --localflowchart LR
GitHub[GitHub traffic + public API] --> Raw[Versioned raw data branch]
Registries[PyPI / npm stats] --> Raw
Raw --> DuckDB[Local DuckDB]
DuckDB --> dbt[Tested dbt models]
dbt --> MF[MetricFlow definitions]
MF --> Dex[Dex governed queries]
Dex --> Answers[Maintainers and agents]
Dex --> ETM[Explore / Transform / Maintain]
ETM --> dbt
Everything that defines a number lives in Git: sources.yml, the SQL models, and models/semantic.yml. The DuckDB file is rebuilt from pinned raw commits.
Every model and metric change went through Dex (lifecycle guide):
| Step | What it does |
|---|---|
| Explore | Inspect the warehouse before writing any SQL. |
| Transform | Make changes through a reviewable plan, a dev build, and row-level verification. |
| Maintain | Check schema, volume, grain, and metric definitions against the committed baseline. |
The audit trail holds the raw command output for each of these:
- Every change is proven before it lands. Models change through reviewable plans. Before anything was written, a refactor of
fct_legacywas shown to return exactly the same rows: 33 before, 33 after (step 09). - The semantic layer only grows through checked plans. One plan added 27 metric definitions, left 21 untouched, and was parsed by dbt before it was stored, so it could not land broken (step 13).
- Metric meanings cannot change silently. The daily run compares every definition with the committed baseline, and a changed metric is held until someone reviews it (step 24).
- New data is checked before it is published. When a second project joined, the run held publication, Dex showed exactly what changed, and the reviewed result went out the same day (steps 35–37).
- Upgrades are maintained like any other change. Moving to Dex 1.15.2 meant checking against the previous baseline, tracing every finding to real new data, and only then accepting a new baseline (steps 40–49).
- Clones are not installs, and views include bots. Daily unique counts are exposed as visitor-days and cloner-days, because one person counts again on each day and repository.
- Star history dates current stargazers. People who later unstarred are invisible, so past totals are lower bounds.
stars_observedis the true current total. - Downloads are not users. They exist only for uploaded release assets and configured packages, and include CI.
- Release downloads are tested on synthetic data only. Neither example project uploads release assets, so these metrics have not yet run against live data. Open an issue if your numbers look wrong.
- Referrers are a rolling top 10. Snapshots are never summed. A source missing from the top 10 is unreported, not zero.
- First-time contributor PRs are accurate from the start of daily collection. Backfilled history undercounts.
- Metric contract: every metric and its exact meaning
- Question cookbook: all recipes with examples
- Setup guide: tokens, collection, packages, campaign context
- Architecture: how the pieces fit together and where data comes from
- Dex lifecycle: explore, transform, maintain in practice
- Operations guide: daily runs, freshness, and recovery from failures
bash scripts/bootstrap.sh # set up the environment
uv run --frozen pytest -q # run the test suite
uv run --frozen ruff check . && uv run --frozen ruff format --check . # lint and formatThe tests use two synthetic repositories and run real MetricFlow queries through Dex. They need no network or credentials. To opt into the public example, run pytest tests/test_live.py --live.
Please read CONTRIBUTING.md and the code of conduct.