I run a directory site. People arrive, look at listings, and some of them click through to an external booking page. Those outbound clicks are the only thing on the site that correlates with money, so they are logged: what was clicked, from which page, on what device, and — if the person happens to be signed in — which account.

Out of 1,372 recorded clicks, six carried an account.

Six. Not six percent. Six clicks, across eight months, belonging to four distinct people.

That number reads as a broken integration. Something in the chain that attaches identity to an event is dropping it, and 99.56% of the time nobody notices because the click still gets recorded and the dashboard still fills in. So I went looking for the break.

The chain was fine

The click handler passes through a hook, the hook calls an edge function, the edge function reads the Authorization header and resolves it to a user. The client library attaches the current session's token automatically. There was no header override anywhere, session persistence was on, and the storage key was pinned so it could not rotate out from under itself.

The requests also pass through a same-origin proxy on the way to the backend, which is the kind of thing that quietly eats headers. I tested it directly: the auth endpoint received the token, parsed it, and rejected it for the correct reason. Headers survive the hop.

Every layer did what it was written to do. Which meant the six were not survivors of a lossy process. They were the entire population of something.

The six were not random

Here is the part that ended the investigation.

I pulled the six clicks and joined them to the accounts that made them, and looked at the gap between when the account was created and when the click happened.

1.7 minutes
1.8 minutes
2.8 minutes
7.4 minutes
11.6 minutes
14.9 minutes

Enter fullscreen mode Exit fullscreen mode

Six clicks. All of them inside a quarter of an hour of the account existing at all.

That is not the signature of a lossy pipeline. A pipeline that drops identity 99.56% of the time drops it uniformly — you would see attributed clicks scattered across the whole timeline, at all sorts of distances from signup. Instead every attributed click sits in one narrow window, and the window has an obvious shape: it is the period during which somebody was still signed in from having just signed up.

Nothing was failing. The tracking captured identity every single time identity was present. It was present for about ten minutes per person, ever.

Nobody chose this

The site is deliberately open. Nothing is gated. You never have to make an account to see anything, and the only reasons to make one are to save a listing or leave a review — both optional, both rare. That was a good decision, made on its own merits, about content access.

But an auth model is not only a content-access decision. It also silently sets the maximum duration over which your analytics can associate two events with the same person.

Nobody wrote that down. There is no config value for it. It emerged from "the site should be open to everyone" and it landed at roughly ten minutes, and it will stay at roughly ten minutes no matter how good the analytics get, because the analytics were never the constraint.

You cannot measure a relationship that outlives your identifier. Whatever your system uses to say "this is the same person as before" — a session, a token, a cookie, a device id — has a lifespan, and every question your analytics can answer must fit inside it. Ask a longer question and you do not get a wrong answer. You get an empty one, which is worse, because empty looks like evidence of absence.

Why it presents as a bug

The reason this costs a day rather than a minute is that the symptom shows up in the wrong place.

The number that looks wrong is in the analytics table. So you debug the analytics: the handler, the function, the header, the proxy. All of that code is fine, which means every check comes back clean, which feels like you are narrowing in on something subtle when in fact you are auditing a layer that has no defect in it at all.

The defect is not in the layer that produced the symptom. It is in a design decision one layer down and several months back, which is working exactly as intended and has no idea it is also an analytics constraint. Instrumentation problems present as instrumentation. Architecture problems also present as instrumentation.

The tell was the distribution, not the magnitude. 0.44% tells you almost nothing — it is consistent with a lossy pipeline, a rare event, or a design ceiling. The shape of when those six occurred distinguishes between them in one query. When a number looks broken, the useful question is rarely "how big is it" and almost always "how is it distributed".

The fix is a design change

More tracking would not have helped, because nothing was going untracked. The change is to stop borrowing identity from the session.

So every tracked click now also carries a first-party identifier generated once in the browser's local storage, and signup copies that identifier onto the new account. Joining the two recovers the whole visit: what somebody clicked before they had an account, and — the part that actually matters — what they clicked days afterwards.

That is a different kind of thing from a session. It is not authentication and it grants nothing. It is one value whose only job is to outlive the login, because the login was never going to live long enough to answer the question.

What it still cannot do

Worth being precise about, because an identifier like this invites more confidence than it earns:

  • It identifies a browser, not a person. Cleared storage, a second device, a private window — each one starts fresh. Somebody who browses on a phone and books on a laptop is two rows and always will be.
  • It undercounts, never over. It can fail to connect two events that belonged to the same person. It cannot merge two people into one. Given a choice about which direction to be wrong in, that is the right one.
  • It only works going forward. Every existing account predates it, so their history stays blank. There is no backfill, because the data was never captured — and inventing a join key retroactively would produce exactly the kind of confident wrong answer this was meant to eliminate.

None of that makes it a weak instrument. It makes it an instrument with a stated range, which is the only kind worth having. The version I replaced also had a range. It just did not mention it.

The general shape

Every system that counts things has a horizon, and the horizon is usually set somewhere else entirely — in an auth model, a retention policy, a cookie lifetime, a log rotation window. None of those are described as analytics decisions. All of them are.

When a metric comes back far lower than it should be, there are three possibilities, and they look identical at a glance: the pipeline is lossy, the event is genuinely rare, or you asked a question longer than your identifier lives. The first is a bug and you should fix it. The second is a finding and you should accept it. The third is neither — it is a boundary, and no amount of work inside the analytics layer will move it.

Check the distribution before you check the code. If every observation you do have clusters inside one window, you have not found a leak. You have found the edge of what your system was ever able to see.