The chain
One person, six hops, and every hop can lose them
It is a Tuesday. Someone opens the storefront on a laptop. They have no account, no cookie, no name. By Friday they are a row in an audience being synced to an ad destination, and by the following month they are a number in an incrementality readout. Step through every hop they make.
A laptop, a cookie, and no name
Someone opens the storefront. The collector sets dev_31f0a and stamps session sess_9c22. Four page views, one add to cart, no purchase.
A cookie the browser expires in seven days makes this person a stranger again next week.
Notice how much of that is bookkeeping about a person’s continuity. The events are easy. The purchase is easy. The hard part is the claim that Tuesday’s laptop, Wednesday’s phone and Friday’s order belong to one human being.
The squeeze
The identity you rented is being taken back, one layer at a time
Third-party cookies are the layer everyone talks about, and they are the least interesting one. The durable change is that the browser stopped being neutral about how a cookie was written.
- Third-party cookies are no longer a foundation. Cross-site identity you did not collect yourself is not something you can build on any more. That is the end of a rented graph, not the end of measurement.
- A cookie written by a page script is treated as an analytics artefact. Browsers with tracking prevention cap its lifetime at days rather than months. A returning customer with a longer gap looks like a brand new visitor, forever.
- A cookie set by your own server on your own subdomain behaves differently. It is a first-party cookie the browser treats as infrastructure rather than tracking. This is the entire reason the collector moved server-side — not to evade anything, but to stop being classified as something it is not.
- App tracking permissions and ad blockers cut the tail. Mobile ad identifiers largely stopped being available, and a meaningful slice of page-side tags never fire at all. Server-side collection recovers the blocked slice; nothing recovers the opted-out slice, and nothing should.
↳ A real customer comes back every week or two. Drag past 7 and watch the top lane fall apart.
Put a number on it. Take a thousand devices that come back after a gap of ten days. With a script-written cookie, all thousand return as new devices — a thousand strangers, a thousand first sessions, nothing to attach an order to later. With a server-set first-party cookie, the same thousand come back as themselves. Nothing about the analysis changed. The only thing that changed was who wrote the cookie.
The constraint is real and the panic is not. You lose the identity you were renting. You keep the identity you observe on your own property — provided you actually observe it well.
The spine
Every downstream number is a bet that two sessions were the same person
This is the sentence to carry through all six posts. Lifetime value, cost per acquisition, propensity, incrementality — none of them are measurements of customers. They are measurements of profiles, and a profile is whatever your resolution logic decided. Get that decision wrong and the arithmetic downstream is perfect and meaningless.
- Under-merge and one customer looks like four. Laptop, phone, tablet, a guest checkout with a typo. Four profiles, one order each. Lifetime value is a quarter of what it really is, so the bid ceiling is a quarter of what it should be, and you lose auctions you could afford to win.
- Over-merge and you email a stranger their neighbour’s order history. A shared tablet, a kiosk, an office network. One bad link joins two households, and the failure is not a bad metric — it is a privacy incident with a screenshot attached.
- The catastrophic case spreads. A public device bridges two groups, those bridge others, and a runaway group quietly swallows thousands of people. Nothing errors. The audience just gets strange.
- Un-merging has to be an ordinary operation. Which means every link keeps its provenance — the event that produced it, the kinds of key involved, the timestamp — because you cannot undo a merge you cannot explain.
The silent version is the one to fear. Nobody files a ticket saying “identity resolution is under-merging.” They say revenue per customer looks flat, and it looks flat for a year.
The leak
You can build all of this correctly and still lose half of it at the door
Suppose the graph is right. The audience is right. Eighteen hundred profiles, hashed correctly, consented properly, pushed to a destination. The destination recognises nine hundred of them.
Match rate — keys submitted versus keys the destination recognises — is the number that tells you whether the entire pipeline was worth building. Everything upstream is invisible to the business until it survives this one join on someone else’s infrastructure.
- It fails silently and gradually. A change to how you clean up an email before hashing it, a shift in which kind of key you send, an audience skewing toward people who used a work address. The rate slides by a third over two months and no alert fires, because nothing is broken.
- The absolute number is not the alarm. Match rates differ enormously by destination and audience. What matters is a relative drop against that pair’s own baseline.
- Most of the loss happens before the hash. Case, whitespace, phone formatting, plus-tags — hash the raw input and you have silently invented a second person who matches nothing.
Post 5 owns the mechanics. For now, know the name of the number.
Wrong turns
Two reasonable answers that hand away the thing you are trying to own
- “Just use the ad platform’s pixel and audiences.” It works, and it is the fastest path to a retargeting campaign. The cost is structural. You are renting the identity graph — the mapping from your visitors to reachable people lives on infrastructure you cannot query, export or audit. You cannot reach across destinations, because an audience built in one is not portable to another, so every new channel starts from zero. And the measurement is graded by the party selling you the media. That last one is not an accusation; it is just a bad experimental design.
- “Just buy a customer data platform.” Often correct. Buying one does not exempt you from any of this. The vendor gives you the plumbing; you still own the semantics — what counts as a purchase, when a session ends, which keys are trustworthy — and you still own the consent model and the match keys. The vendor cannot decide those for you, and every failure in this series is a failure of those decisions rather than of the plumbing. The point of building a small one is to know what you are buying, and what you are still on the hook for after you buy it.
Governance
Consent is a join key, and deletion is a fan-out
Consent modelled as a filter is consent you can forget to apply. Somebody writes a new audience query, ships it, and it compiles membership without ever touching the consent table — no error, no failing test, just marketing sent to people who said no. Modelled as a join key it is structurally unavoidable: there is no code path that produces membership without joining consent, and the join fails closed.
Deletion is the mirror image. A request that deletes from your warehouse and stops there is not a deletion — the profile still sits in an audience inside three destinations you synced to last Tuesday. Doing it properly means a tombstone that fans out: delete from the warehouse, split the identity component, drop audience membership, and issue removals to every destination this profile was ever sent to. Which requires having kept a per-profile sync ledger from day one. Post 4 owns this, and it is a data-model decision, not a compliance chapter.
What the series builds
Six posts, one stage each, each with a runnable tag on the same open-source repo.
A first-party server-side collector, an event envelope you can trust, and a streaming backbone that keeps one device's events in the order they happened. Five stages, thin but real, running on your laptop.
A profile is a connected component, not a row. Edges carry provenance, merges are derivations you can undo, and one in-store kiosk quietly welds four households into a single customer.
Traits as a query over the event log rather than a document anyone can clobber — then a propensity model, and the one bug that makes it look brilliant offline and do nothing at all in production.
Consent as a join key that audience compilation cannot compile without, brand isolation with a row policy underneath it, and a deletion that reaches every destination you ever synced to.
Send what changed, not what is. Snapshots, diffs, batches that resume, rejects that get dead-lettered — and the one number that says whether the whole pipeline was worth building.
Attribution divides credit; incrementality asks whether it mattered. A deterministic holdout, a power calculation done before the test, and a readout that carries an interval.
This is a teaching-grade reference implementation, not a product. The parts it shows you are the tractable parts: a stream consumer, a component solver, a diff engine, a hash-based holdout. The hard part in any real company is the organisational agreement about what a customer is — whether a household counts as one, whether last quarter’s consent still applies, whether a returned order still makes someone a buyer. Every one of those is a product decision wearing an engineering costume. Build the pipeline so that when the answers change, you can change them in one place.
Explain it back
Reveal a model answer
The fix changed the hash of every address that previously had stray case or whitespace, so the keys you send today are not the keys you sent before. Profiles that were previously two separate people collapsed into one — good — but the sync engine diffs against a snapshot keyed by the old hashes, so it emitted a wave of adds for people the destination already had under a different key, and a wave of removes for keys that no longer exist. The destination sees heavy churn in a list it was optimising against and effectively restarts its learning phase, which is what the lead is feeling as worse performance.
The fix is to treat normalisation policy as a versioned part of the audience contract: rebuild the snapshot under the new policy in one migration, then diff against that, rather than letting a semantic change leak out as ordinary daily churn. The second-order consequence is subtler — every historical match-rate number was computed under the old policy, so your baseline for alerting is now wrong too, and the relative-drop alarm you rely on is comparing against a rate that no longer means the same thing.
The collector, the envelope, and the topics everything else reads from — five stages at their thinnest honest version, so the later posts have somewhere to land.
Clone it and run it →