← Blog

· 7 min read

Counting visitors without a cookie banner

Counting visitors without a cookie banner

Open a generated site and read its source. The JavaScript in it is one short inline function that posts the contact form without reloading the page, plus an import map naming the 3D library for the sites that use one. We checked sixty live sites: that is all any of them ships of its own, and not one of them loads a script from another origin. On pages that print an email address, Cloudflare adds one more of its own at the edge, to obfuscate the address — served from your own domain, and it measures nothing.

There is no analytics tag, nothing from a measurement provider, nothing that watches you read.

And the owner can still open Manage and see how many people came yesterday, from where, on what, and to which page. The counting happens on the server, in the same request that served the page.

Why the usual way needs a banner

The industry default is a tag: a script from a third party, which sets an identifier in the visitor's browser and keeps it. That identifier is what lets the tool say "returning visitor" — and it is also what turns page counting into personal data, which under European rules is the thing that needs consent. Hence the banner, which in practice is a box people dismiss without reading, on a site they have been on for four seconds.

The tag has a second problem that nobody advertises: content blockers remove it. The visitors who block trackers are exactly the sort who might be worth counting, and they are missing from the graph. So the banner is the price, and incomplete numbers are what it buys.

What we do instead

A middleware sits on the tenant's page routes and writes one row after the response has already gone out to the browser, so the reader never waits for the measurement. The row holds the path, the source, a device class, a country, a visit id — and instead of an identity, a hash.

The hash is the interesting part. The visitor key is a SHA-256 of four things: the site, the request's IP address, the user agent, and a salt derived from the application key and today's date. The raw IP is never stored. And because the salt changes at midnight, the same person visiting tomorrow produces a completely different hash. Nobody — including us — can join yesterday's visitors to today's.

That is a deliberate trade, and it is the whole reason there is no banner. The counter cannot recognise a person, so it does not need permission to count them.

The four exclusions

The definition of a visit is where these systems are honest or not, and ours needed four exclusions before the numbers meant anything. Each one is a test in the suite.

1. Our own screenshotter

The moment a site is built, the platform opens it in a headless browser to photograph it for the dashboard. There are 169 sites on this installation and every one of them has been photographed. Without the bot-and-headless filter, every site would begin life with a visitor it never had — and would gain another one every time its owner changed something.

This is the exclusion we would have got wrong if we had trusted the obvious source, because the fake traffic is not a crawler from outside. It is us.

2. The editor's own preview

While the owner works, the site renders in an iframe beside the chat. Those are real page loads from a real browser. They are recognised by the combination of a frame request and a referrer on the platform's own host, and they are dropped — otherwise the busiest day in every site's graph would be the day it was built, and the busiest visitor would be its owner.

3. Everything that is not a page

Images, fonts, media, form submissions. This is why the middleware is attached to the page routes specifically instead of to the whole site: a page with ten pictures would otherwise report roughly ten times the traffic it has, and the more careful the design, the more inflated the number.

4. Anything that was not actually served

Errors, redirects, non-HTML responses, and a site the owner has taken offline — an unpublished site answers 503 before the request reaches the counter at all.

What it can tell you

Over the last 7, 30 or 90 days: visitors, visits, page views, pages per visit, average visit length, bounce rate, a visitor-per-day series with the quiet days present as zeros rather than as gaps, and four breakdowns — source, page, device, country.

Two of those deserve a note.

Source is the referring host, with your own site removed from it. A link from your homepage to your prices is not a referral, and a site on a custom domain sees links from its own pages arrive under that domain's name — so both spellings are checked before anything is filed as a source. A utm_source tag, if you use one, wins over the referrer.

Country is honest or absent. It is read from the header the CDN edge attaches, and no geolocation database ships with the platform. If the header is not there, the country reads Unknown rather than a guess. On this installation the column has never had to fall back: every page view recorded so far arrived with a country from the edge, and none of them was a guess.

What it cannot tell you

This is the part the feature page usually leaves out.

  • How long a one-page visit lasted. The duration of a visit is the time between its first and last page view, so a visit with one page view reads zero seconds — and counts as a bounce. The server sees the request for the page; it cannot see the reader. Plausible has the same blind spot and the same definition. Inventing a number would be worse than a plain zero.
  • Anything inside the page. No scroll depth, no clicks, no events, no heatmaps, no funnels. Those need a script, and there isn't one.
  • Whether anyone came back. The salt rotates every night, so a returning visitor is a new one. This is not a limitation we intend to remove: it is the same property that removes the banner.

The cookie we do set, and the one we don't

Being precise: the counter sets no cookie and reads none. But a generated site does set two — a session cookie and a CSRF token, scoped to that site, expiring in two hours.

They exist because the site's contact form is protected against cross-site submission like every other form on this platform, and the token has to live somewhere. That is a strictly necessary cookie in the sense the European rules use the words: required to deliver something the visitor asked for, exempt from consent. It carries no identifier we ever read, and the analytics does not touch it.

Meanwhile this page — the one you are reading — did show you a cookie banner, because dictumly.com loads Google Analytics and that is exactly the kind of counting that needs asking. The difference between our marketing site and the sites we generate is not an oversight. It is the argument.

Where the numbers live

Raw rows, no rollup tables. Every figure in the panel is computed at read time, which means a definition can be corrected later without a rebuild of stored aggregates — and it means nothing exists that the raw data cannot reproduce. A daily job prunes anything older than 180 days.

If you need more than this

Some sites genuinely do. A paid campaign you must attribute per click, a shop funnel, anything where you need to follow one person across sessions — none of that can come from counting on the server, and you should add a proper analytics tool.

You can: a Google Analytics or Tag Manager id goes into one field in Manage, and the tag lands on your site. What comes with it is the banner, the blocked visitors and the obligations, which become yours. That is a reasonable trade for some businesses and a bad one for most small sites, which will look at the numbers roughly monthly and want to know one thing: is anybody coming.

For that question, a page with no script in it answers perfectly well.

Read next