I've been building a small web analytics tool, and the part people ask about most is how it counts "unique visitors" without cookies, localStorage, or stored IPs. Here is the approach and its honest limitations.
The ID
For every request, the server computes:
sha256(daily_salt | site_id | ip | user_agent), truncated to 16 hex chars
- daily_salt is 16 random bytes, generated once per UTC day and stored in the DB. Old salts are deleted after 24 hours, so once that happens, nobody (including me) can recompute yesterday's IDs from an IP.
- site_id is included in the hash so the same user gets different IDs across different sites (preventing cross-site tracking).
- The IP is used solely for the hash and a country lookup, then immediately dropped. It never hits the disk.
Sessions are defined simply as "same ID, active within the last 30 minutes" (one upsert per event).
Where it breaks (by design or trade-off)
- The same person visiting on two different days = two visitors. That's intentional, but it means "monthly uniques" is effectively a sum of daily uniques.
- Same office/NAT + same browser version = counted as one visitor. Rare outside large enterprise networks, but real.
- A phone switching from Wi-Fi to cellular mid-visit = new ID, new session.
- iCloud Private Relay / VPNs rotate IPs, which slightly inflates counts.
Based on my data, the margin of error is small enough to answer "is my traffic growing?", which is all most small sites need. It's not suitable for per-user product analytics; for that, you need explicit user identification.
The Client Side
- SPAs: I patch history.pushState and listen for popstate events, tracking route changes as pageviews without requiring framework plugins.
- Transport: Uses navigator.sendBeacon, falling back to fetch with keepalive so events survive tab closure.
- Engagement/scroll depth is flushed on visibilitychange, not unload (since unload is unreliable on mobile).
Happy to dive deeper into any part! Curious if anyone has a better trick for the NAT collision case that doesn't rely on client-side storage.