ADR-009: Cookieless analytics via GoatCounter

Status

Accepted (2026-08-29)

Context

The site shipped site-url and Open Graph metadata on 2026-08-27 (GH #47), leaving the analytics half of that issue open on an explicit owner decision: whether to measure usage at all, and if so with what.

The launch post on LinkedIn forced the question. It drew 90,201 impressions, 65,264 members reached, 516 saves and 729 click-throughs to dsp-workshop.nl, none of which were measurable here. Two consequences made that worth fixing:

  1. The grow-by-deepening policy has no input. Choosing the next unit of work by deepening existing pages assumes some knowledge of which pages are read. Across 34 topics, 12 basics chapters and 30 embedded pages, that was pure guess.
  2. The long tail is invisible. A teaching resource earns its readers over years through search and through other people linking it. Referrer data is the only way to see that happening, and it is the interesting question about a site like this one: not the launch spike, but whether anything comes after.

The site is on GitHub Pages (ADR-004), so there are no server logs to read. A client-side beacon is the only mechanism available.

The constraint that ruled the choice is that this is a gift, not a funnel. It is CC BY-SA 4.0 and asks nothing of its readers. Instrumenting it with anything that tracks people would contradict what it is.

Decision

Add GoatCounter as a one-tag beacon: analytics.html, included through _quarto.yml’s format: html: include-in-header, alongside the existing include-after-body: fix-svg.html.

Dashboard: https://dsp-workshop.goatcounter.com.

Why this one:

  • No cookies, and the visitor IP is not retained. It is used transiently for a country lookup and a rotating-salt session hash, then discarded. No consent banner, and no personal data at rest. (Verify against GoatCounter’s own privacy page before restating this anywhere reader-facing; it is their documented behaviour, not something this repo can check.)
  • Free for non-commercial use, which this site is.
  • Open source and self-hostable (single Go binary plus SQLite). If the hosted service ever becomes unacceptable, moving is a one-URL change to the data-goatcounter attribute in analytics.html. That escape hatch is the reason a hosted third party was acceptable at all.

Rejected alternatives:

  • Plausible: equivalent privacy posture, EUR 9/month hosted. Nothing here justifies a running cost.
  • Umami: free but self-hosted, needing a database and somewhere to run it. Real infrastructure for a site whose whole build story is “no local tooling”.
  • Cloudflare Web Analytics: free and cookieless, but the data lands with a large US company. Acceptable on the merits, less aligned with what this is.
  • Google Analytics: not considered. Cookies, consent banner, and the exact posture this site should not have.
  • Server-side counting: unavailable on GitHub Pages.

Consequences

  • Positive: per-path read counts, so the deepening policy has evidence instead of intuition. Referrers, so an adopting course or an inbound link is visible.
  • Positive: no consent banner, no mailing list, no personal data held.
  • Positive: reversible in one commit. Deleting analytics.html and its include-in-header line removes the whole mechanism.
  • Negative: the numbers undercount, probably substantially. count.js is in the common blocklists and this audience runs blockers. Treat it as a trend instrument, never as a census, and never quote an absolute from it as a fact about readership.
  • Negative: a third-party request on every page, which the site did not previously make. Mitigated by async (never blocks rendering) and by the self-host escape hatch above.
  • Neutral: the script skips localhost, so the no-local-preview posture of ADR-004 means almost no risk of polluting the data from development anyway.