All posts
EngineeringAug 3, 2026·5 min read

Cookieless analytics, and why there's no consent banner

By Engineering

Almost every analytics tool answers "how many people visited?" the same way: drop an identifier on the device, then count the distinct ones. That identifier is what turns a page view into personal data, and it's why you end up with a consent banner in front of your own product.

We wanted the numbers without the identifier. So Marsad never stores anything on the visitor's device — no cookie, no localStorage entry, no device id.

A visitor id that expires every day

When a page view arrives, the ingest endpoint derives a visitor id by hashing four things together with a secret salt that never leaves our server: the current day, the project id, the request IP, and the user-agent string.

createHmac("sha256", ANALYTICS_SALT)
  .update(`${day}|${projectId}|${ip}|${ua}`)
  .digest("hex")
  .slice(0, 16);

The raw IP is used for the hash and then dropped — it is never written to the event. Because the day is part of the input, the same person browsing tomorrow hashes to a completely different id, so ids can't be correlated across days. And because the salt is secret and the output is truncated, the hash can't be walked backwards to a person.

That's the whole mechanism. It's the same approach Plausible and Fathom take, and it has a real consequence worth being upfront about.

What this costs us

A rotating id means unique visitors are a daily figure. We cannot tell you that someone who came on Monday came back on Thursday, because by design we no longer have anything that links the two. Cross-day retention cohorts and multi-session funnels are off the table.

In exchange you get numbers you can publish without a legal review, and a dashboard nobody has to click through a banner to reach. For most teams shipping a product, "how much traffic, from where, to which pages, and did it convert today" is the actual question.

What we collect

  • The path — never the query string, so tokens and email addresses in URLs can't leak into analytics.
  • The referrer and page title.
  • UTM parameters (source, medium, campaign, term, content), grouped into campaigns.
  • Coarse buckets derived server-side from the user-agent: device type, browser, OS.
  • Country, resolved at the edge. No city, no coordinates.

It's on by default in the browser and needs no extra code — the SDK records a view on load and on every route change. If you'd rather not have it, init({ webAnalytics: false }) turns the whole thing off.

Start watching your apps in minutes

Drop in the SDK and watch your first events arrive. Free plan, no credit card.

Start free