Visits counted without cookies

Each page view is counted on the site's own server with its path, a link's tag, the referring host, a screen class and a pseudonym that lasts a day; no cookie, no address, no third party.

Status
Accepted 3 October 2026, revised 7 November 2026 · the guide's work counted beside, in numbers alone, and what it could not answer by topic
The record
0014-visits-counted-without-cookies.md

Context

The site is part of a job search: links sent with applications should say whether a company looked, and the pages, which ones were read. ADR 0007 promised that analytics, when they came, would be first-party and cookieless, with nothing to consent to. The service scales to zero (ADR 0003), so anything held in its memory is lost at every cold start.

Decision

  • A beacon of the site's own. Each page sends navigator.sendBeacon to POST /api/visits, with the path, the tag of a tagged link (?r=acme), the host of the referring page and the screen as narrow, medium or wide, as query parameters the API validates like any other. The referrer and the tag go with the landing page only; the tag then leaves the address bar, by a replacing navigation, so a copied link does not carry it on.
  • Nothing for whoever says no, or is not a reader. The browser sends nothing under Global Privacy Control or Do Not Track, and the server stores nothing for a request carrying Sec-GPC: 1 or DNT: 1, nor for a user agent that says it is a program. Any page can send a beacon here, so one that a browser says came from another site's page (Sec-Fetch-Site other than same-origin) is not counted either.
  • A pseudonym that lasts a day. Unique visitors are counted by an HMAC-SHA256 of network address and user agent under the day's salt, its first 64 bits; neither input is stored. A salt in memory would be new at every cold start and count one visitor as several, so the day's salt is kept in the database for its day: the first visit of a later day makes that day's and deletes the earlier ones. After that, nobody can say whose a pseudonym was, or match it with another day's. Where the salt comes from is a Strategy (SaltSource): the database in production, a fixed one in tests.
  • Little, and not for long. A row holds the minute, path, tag, referring host, screen class and pseudonym; the database checks each against the API's patterns. Rows are deleted after 400 days.
  • Said where it is promised. The footer's "No cookies, no third-party trackers" links to a privacy page that lists what a visit leaves, what it does not, how to say no and for how long.
  • The guide's work, counted beside (revised 2026-10-25). What the site's guide does is counted by day, in numbers alone, in a table of its own: conversations, questions, actions by tool, tours started and finished, refusals by reason, sentences held back, tokens read from the cache, and how long the first sentence took, in bands. A row is a day, a counter named by the code and a number: no question, no answer, no path, no pseudonym. The server counts what it answers; tours play in the browser, which says when one starts and ends (POST /api/guide/events, the tour's id alone). The same no holds: nothing is counted for a request with Sec-GPC: 1 or DNT: 1, nor for one from another site's page. Only the owner's admin reads it.
  • What the guide could not answer, by topic (revised 2026-11-07). When an answer is refused as not in the record, or as ungrounded, each topic its question named is counted beside: a term of the lexicon the fit check and the assistant share (a technology or practice the site knows by name), or a hiring topic of guide/topics.txt (the start date, the contract, work authorisation and the rest), or other when it named none. A counter's name comes from those closed lists alone, so the question's own words never reach the database. It tells the owner what the site should say next (G13), which a count of refusals alone could not.

Consequences

  • ADR 0007 holds: no cookie, no storage but the theme, no third party, nothing to consent to.
  • A topic counted may be a word the visitor typed, "Terraform", but only as one of the site's own list: a question about anything the lists do not name, a person or a company among them, leaves other and nothing more.
  • A visitor counts once a day per address and browser: a phone and a laptop are two visitors, and so is one browser that changes network. Pages read after the landing one are tied to its tag through the day's pseudonym.
  • The day's salt stays in the database until the first visit of a later day. After a quiet spell that can be days; while it lasts, someone with the database and a visitor's address and user agent could recompute that visitor's pseudonym.
  • Every counted view writes to the database, which on Neon wakes a suspended compute: a few seconds of compute a visit, within the free plan for a personal site's traffic. Rate-limited, like every endpoint that reaches it (ADR 0012).
  • Google Cloud's own request logs keep addresses for 30 days; the site's count does not depend on them, and the privacy page says so.