How it works
Data flow
daily cron (in your repo) │ ▼ fetcher ── GitHub Traffic API ──► last 14 days of {date, count, uniques} │ ▼ accumulator (PURE) ── upsert by date key, dedup, never sum overlapping days │ ▼ renderer (PURE) ── total + options ──► SVG string │ ▼ store + git commit ── totals.json + badge.svg on the `traffic-data` branchThe 14-day overlap problem
The GitHub Traffic API returns the last 14 days of data, each as
{ timestamp, count, uniques }. If the Action ran daily and simply added the
new count total to a running number, it would re-count the 13 overlapping
days on every run — turning a small repository’s “views” badge into a
wildly inflated number within a couple of weeks.
The accumulator solves this by storing a date → { count, uniques } map
and upserting by date key on every run. When the API reports a fresh
value for a day already in the map, the entry is overwritten, never
summed. The displayed total is the sum of the map’s values, plus the
optional base offset.
This makes the pipeline idempotent: running it twice on the same input changes nothing. There are explicit unit tests proving this property.
Persisted shape
totals.json looks like this:
{ "schema": 1, "updatedAt": "2026-06-02T03:00:00Z", "views": { "2026-05-20": { "count": 12, "uniques": 4 }, "2026-05-21": { "count": 8, "uniques": 3 } }, "clones": { "2026-05-20": { "count": 2, "uniques": 1 } }}It is plain JSON committed to a branch in your own repository, so you can inspect, edit, or audit it at any time.
The dedicated traffic-data branch
The Action commits the badge and totals.json to a dedicated branch
(default name traffic-data). On first run, the branch is created as an
orphan — its history is independent of main. No badge updates ever
land on your main branch.
The branch contains only:
totals.json— the persisted date-keyed mapbadge.svg(or whatever you setoutputto) — the rendered badge
Embedding the badge in a README uses raw.githubusercontent.com:
Pure vs impure modules
The project keeps a strict purity boundary so all the branching logic is unit-testable without network or filesystem mocks bleeding into the core:
- Pure (exhaustively tested):
options— parse and validate inputsaccumulator— merge fresh data into the persisted map (the dedup invariant lives here)renderer— produce the SVG string from a total and options
- Impure (thin, injected I/O):
fetcher— calls the GitHub API; takesfetchas an argumentstore— reads and writestotals.json; takesreadFile/writeFileindex— orchestrates everything; takes all of the above as arguments
If logic ever creeps into an impure module, it gets moved into a pure one.