Counting without knowing
Counting unique visitors without being able to identify any of them is the entire engineering problem behind trysonex.
Every analytics product starts with a feature list and works backwards to the data it needs. That is how you end up with an identifier that follows somebody across four sites: at some point a feature required it, the feature shipped, and nobody went back to ask whether that feature had been worth what it cost.
trysonex started from the other end. Write down what will never be collected, then find out which questions are still answerable. It is a narrower starting position and it produces a smaller product, and both of those turned out to be the point.
Most of the useful questions survive. Which pages get read. Where people arrive from. Which countries. Whether the trend is up or down and by how much. Not one of those requires knowing who anyone is. A site owner asking whether their new post landed does not need a profile of the person who read it, they need a number that goes up.
The question that does require it is unique visitors, and that single question is the whole engineering problem. Everything else in the product is arithmetic over events. This one asks you to decide whether two requests came from the same person, which is the exact thing you have promised not to be able to determine.
The answer is a rotating salt. Once a day a random value is generated and held in memory. Every incoming request produces a hash of the site, that day's salt, the IP address and the user agent. The hash is stored. The inputs are not. When the day turns, the salt is replaced and the old one is discarded.
The property that gives you is worth being precise about. Within a day, two requests from the same browser on the same network produce the same hash, so they count once. Across days, the same browser produces a completely different hash, and there is no key anywhere that connects the two. Yesterday's visitors are permanently unlinkable to today's. That is not a policy commitment that depends on my future behaviour. It is arithmetic, and it holds even if somebody takes the database.
It costs accuracy, and I would rather be direct about how. Somebody who moves from office wifi to mobile data mid-session counts twice, because their IP changed and the hash changed with it. Somebody on a large corporate network, behind one address, running the same browser version as a colleague, might count once for two people. Neither of these is rare.
I decided that a number which is roughly right and structurally incapable of being abused beats a number which is exactly right because it followed somebody home. That is not a compromise I made reluctantly to satisfy a constraint. The constraint was the product. If the number had to be exact, there was no reason for trysonex to exist, because the exact version already exists and is free.
Being honest about the error turned out to matter commercially, which I did not predict. The documentation says plainly that unique counts drift and explains the mechanism. Several people told me that was the thing that made them trust the rest of the numbers, because everyone else's documentation implies a precision that nobody actually has and they had all quietly noticed.
The operational side was the real surprise. There is almost nothing to store: a hash, a path, a referrer, a country, a timestamp. No profiles, no journeys, no identity graph. The whole thing runs on a rounding error of infrastructure, and it stays cheap as traffic grows because the storage per visit is a constant that never turns into a row per person per session.
It is also the only product I have run that I do not lose any sleep over. There is no database of personal data to secure, to breach, to migrate carefully, or to hand over if somebody with a letterhead asks for it. I cannot produce a list of the people who visited a site, because I do not have one and cannot construct one. The honest answer to that request is that the data does not exist, and it is a much more comfortable answer than a policy.
One implementation detail is worth writing down because it is easy to get subtly wrong. The salt has to live only in memory and it has to be per-process-safe across restarts, which means a deploy in the middle of the day rotates it early and undercounts nobody but overcounts a few. I decided that was acceptable and documented it, rather than persisting the salt somewhere to make deploys clean, because a persisted salt is a stored secret that reconnects two days if it ever leaks.
The other one is the user agent. Including it improves accuracy, because it separates two people behind the same address. It also makes the hash slightly more identifying in principle, since a rare browser string is a narrower bucket. Since nothing is stored except the hash and the salt is gone within the day, I judged that acceptable, but it is a real trade and not one I would want to make silently.
What I would change is the country lookup. It happens at the edge from the IP before anything is hashed, which is correct, but the first version passed the raw address through one more function than it needed to on the way. Nothing was stored and nothing leaked, and it still bothered me enough to restructure it so the address cannot physically reach any code path that writes. Correct by construction beats correct by review, especially when you are the only reviewer.
The constraint that looked like a limitation from the outside is the cheapest, safest and most defensible thing about the product. I did not know that when I started. I picked it because following people around felt bad, which is a worse reason than the one I have now, and it happened to be right.