Skip to content
HN On Hacker News ↗

Tracking unique visitors without cookies — Margin

▲ 10 points • 17 comments • by jamie_davenport • 4mo ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully AI-generated

99 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 5
SEGMENTS · AI 5 of 5
WORD COUNT 1,710
PEAK AI % 99% · §2
Analyzed
Jul 13
backend: pangram/v3.3
Segments scanned
5 windows
avg 342 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,710 words · 5 segments analyzed

Human AI-generated
§1 AI · 99%

Every way analytics tools recognise a returning browser, from cookies and localStorage to fingerprints and salted hashes, exactly what Margin computes, and the trade-offs vendors don't usually print."Unique visitors" is an identity problem wearing a metrics costume. To count people instead of pageviews you have to recognise that two requests came from the same person. For twenty-five years the answer was a cookie: set a random ID once, read it back forever. It works brilliantly, which is exactly why it now comes with a consent banner, an ePrivacy paper trail, and a growing population of browsers and extensions that eat it. Margin sets no cookies, writes nothing to localStorage, and loads no fingerprinting library. It still shows you unique visitors. This post walks through the full menu of identity techniques, from cookies to salted hashes, explains which one Margin picked, and is honest about what it costs, because the trade-offs are real and mostly under-advertised. The menu of identity techniques Every analytics tool answers the same question: is this request from a browser I've seen before? There are only a handful of places an answer can come from, and each sits somewhere on a line between accuracy and privacy. Strategy 1: a cookie The classic. On first visit, mint a random ID and hand it to the browser; the browser dutifully returns it with every request until it expires. // First-party analytics cookie, GA-style let id = readCookie("_uid"); if (!id) { id = crypto.randomUUID(); document.cookie = `_uid=${id}; Max-Age=63072000; SameSite=Lax; Path=/`; } // Two years of stable identity: cross-day uniques, // retention curves, attribution windows. It all just works. Third-party cookies (an ID shared across every site that loads the vendor's script) are effectively dead: blocked by Safari and Firefox for years and heavily restricted in Chrome. First-party cookies like the one above still work technically, but legally they're stored state on the visitor's device, which is exactly what the ePrivacy rules gate behind consent. Hence the banner. And even with consent, Safari's ITP caps script-set cookies at seven days, so your "two years of identity" quietly becomes one week for a large slice of traffic.

§2 AI · 99%

Strategy 2: localStorage The tempting dodge: "the rules say cookies, so we'll keep the ID somewhere else." // Same idea, different drawer let id = localStorage.getItem("uid"); if (!id) { id = crypto.randomUUID(); localStorage.setItem("uid", id); } This buys you nothing. Article 5(3) of the ePrivacy Directive covers "the storing of information, or the gaining of access to information already stored" on the user's device. It never says the word cookie. localStorage, sessionStorage, IndexedDB, cache tricks like ETag respawning: all the same rule, same consent requirement. Swapping the storage API and deleting the banner is a compliance bug, not a strategy. Strategy 3: fingerprinting If you can't store an ID, make the browser itself the ID. Enough quirks, combined, are unique. // Pseudocode. Nothing is stored; the browser's shape is the identifier. const fingerprint = await sha256( [ navigator.userAgent, navigator.language, screen.width, screen.height, Intl.DateTimeFormat().resolvedOptions().timeZone, await canvasQuirks(), // GPU + font rendering differences await audioQuirks(), // DSP floating-point differences ].join("|"), ); This is the darkest corner of the menu. It's durable (you can't clear what was never stored), invisible to the user, and research shows sites reach for it precisely when consent for cookies is refused. Regulators treat gaining access to device information this way as in scope of the same ePrivacy rules, and browsers actively sabotage it: Safari and Firefox deliberately blur the very APIs it reads. Any analytics vendor "solving" cookieless uniques with a fingerprinting library has just rebuilt the problem with worse optics. Strategy 4: derive an ID on the server Everything above stores something on, or reads something from, the visitor's device. The remaining option is to use only what the server was already handed: every HTTP request arrives carrying an IP address and a user agent. Hash them together and the same person produces the same value, at least until their IP or browser changes. Raw, that's just fingerprinting moved server-side: a durable pseudonymous ID. The insight the privacy-first tools converged on is to make the hash expire.

§3 AI · 99%

That's the family Margin belongs to, and it deserves its own section. Strategy 5: give up on identity Worth naming: some tools simply don't count uniques. Pageviews, referrers, and heuristic "sessions" (a gap of thirty minutes starts a new one) need no identity at all. It's the most private option and a perfectly honest one, but "how many people" is usually the first question analytics is asked, so most tools don't stop here. The industry's trick: a hash that expires Every serious cookieless analytics tool converged on the same idea: derive the visitor ID from things the server already sees, and scope it to a single day so it can't become a tracking ID. Plausible computes hash(daily_salt + domain + ip + user_agent). The salt is a random value rotated every 24 hours and old salts are deleted, so yesterday's IDs can't be linked to today's, not by an attacker and not by Plausible themselves. Fathom hashes the IP, user agent, hostname and a site-scoped salt with SHA-256, discarding the IP immediately after. GoatCounter keeps a site + IP + User-Agent → random ID mapping in memory for at most eight hours and only ever persists the random ID. The salted version looks like this: // Pseudocode: the Plausible-style scheme const salt = await saltStore.today(); // random 128+ bits, rotated every 24h, old value deleted forever

const visitorId = sha256(`${salt}${domain}${ip}${userAgent}`); // store visitorId; never store ip or userAgent The common properties:

The same person produces the same ID within a day, so daily uniques are accurate. The inputs (IP, user agent) are never stored; only the derived ID is. The ID expires with the day, so there is nothing durable to build a profile on.

And note the one operational requirement hiding in that snippet: saltStore. Somewhere, a single source of truth has to generate the salt, share it with every server doing the hashing, and reliably destroy it a day later. Keep that in mind. What Margin computes Margin's version is small enough to quote in full.

§4 AI · 99%

It runs in the ingest handler: const day = new Date().toISOString().slice(0, 10); const digest = await crypto.subtle.digest( "SHA-256", new TextEncoder().encode(`${day}:${site}:${ip}:${userAgent}`), ); // first 16 bytes, hex-encoded Same family as the tools above, with one architectural difference that changes the trust model: where it runs. Margin has no third-party event endpoint. The SDK mounts the ingest route on your app, so the browser talks to your domain and the visitor hash is computed on your server, from a request only your infrastructure ever sees. What gets forwarded to Margin is the truncated digest. The raw IP and user agent never leave your stack. browser ── POST /api/margin (your app) │ derive visitor hash from ip + ua └──> Margin API (digest only, org key)

With a hosted script-and-endpoint tool, "we hash and discard your visitors' IPs" is a promise you take on faith, because their servers necessarily see the raw request. In Margin's model there is nothing to take on faith at that hop: the vendor never receives the identifying inputs in the first place. The part we should be honest about You may have noticed Margin's hash input uses the date where Plausible uses a random daily salt. That is a deliberate trade-off, and it has a cost. A random salt exists to make the hash non-recomputable. Without one, anyone who holds the digests and can enumerate plausible inputs can run a membership test: // The question a secret salt makes unanswerable: // "was IP X with browser Y on this site last Tuesday?" const suspect = await visitorHash("2026-07-07", site, candidateIp, candidateUa); const wasThere = storedDigests.has(suspect); Salt-less, day-scoped hashes are recomputable by construction. So why accept that? Remember the saltStore from the Plausible snippet. Plausible can run one because all their hashing happens in one place they control. Margin's handler is stateless code running on your infrastructure, possibly across dozens of serverless instances, and a rotating secret shared across all of them, deleted on schedule, is exactly the kind of operational surface a mounted handler exists to avoid.

§5 AI · 99%

So the honest description of Margin's visitor ID is pseudonymous, not anonymous: it cannot be reversed, but for one day it can be recomputed by someone who already knows what they're looking for. Two things bound the damage. The ID is dead the next day; there is no cross-day linkage to accumulate. And the party best positioned to brute-force it already operates the server that saw the raw request anyway. What day-scoping costs everyone These apply to every tool in this family, ours included:

There are no cross-day uniques. In every day-scoped tool, "weekly visitors" is really: const weekly = days.reduce((sum, day) => sum + day.uniques, 0); // a person who visits every day counts seven times Cookie-based tools genuinely measure something here that cookieless tools structurally cannot.

Shared IPs merge people. An office, a university network, or a mobile carrier behind CGNAT can put thousands of people on one IP; those with the same browser build collapse into one visitor. This undercounts, sometimes substantially, and it undercounts non-uniformly: mobile-heavy audiences are hit hardest.

Identifier churn splits people. A browser update changes the user agent mid-day, a phone hops from Wi-Fi to cellular. Same person, new hash, two "uniques".

No multi-touch attribution, no retention curves, no LTV. If your product decisions need "did the person from last Tuesday come back?", this model cannot answer it, by design.

We think that's the right trade for web analytics: the numbers stay directionally accurate, the failure modes are legible, and the privacy story stops depending on user-hostile machinery. When you already know who they are There is one big exception to those limits: signed-in traffic. If someone has an account, your database already holds a durable identifier for them, and no derivation trick is needed. A hosted analytics script can't see your session. Margin's handler runs inside your app, so it can: // In your route, where your session is visible export async function POST(request: Request) { const session = await auth(request); return createHandler(margin, { $visitor: session ? session.user.id : undefined, })(request); } // Anonymous requests fall back to the day-scoped hash automatically.