counting readers without following them

September 7, 2026 meta analyticsprivacysvelte

I wanted to know if anyone actually reads this blog. Not who they are, just whether the posts I spend evenings on get seen, which pages people land on, and whether they stay or bounce. The normal answer is to paste in a script from some analytics company and let them figure it out, but the entire point of this project is that nothing leaves my server. So I built it myself.

The rules I set before writing any code: no third parties, nothing collected before the visitor says yes, and nothing stored that could identify a person. What gets stored is a random visitor id (a UUID the browser makes up itself, nothing derived from you), the path you visited with query strings stripped, and the referrer only if you came from another site. No IP addresses in the analytics table, no user agents, no fingerprinting.

Consent is a small banner with a real choice, and the choice is remembered. Say no (or say nothing) and the analytics code simply never starts. Say yes and the browser mints its own id and starts reporting pageviews. There is also a toggle in the footer to change your mind later, which deletes the cookie.

The analytics tab in admin, pageviews and visitors over the last days

The part I like most is how "time on page" works. Instead of tracking when you leave (which browsers make unreliable anyway), the page sends a tiny heartbeat every 15 seconds, but only while the tab is actually visible:

function startHeartbeat() {
    stopHeartbeat();
    heartbeatTimer = setInterval(() => {
        if (document.visibilityState === 'visible') sendEvent('heartbeat');
    }, HEARTBEAT_INTERVAL_MS);
}

So a post left open in a background tab all day counts for nothing, and four heartbeats mean someone genuinely looked at the page for a minute. Reading time measured in a way that cannot lie much in either direction.

The collector endpoint on the blog service answers 204 no matter what. Bad payload, rate limited, database down, it says 204. There is nothing useful an attacker can learn from probing it, and a broken analytics pipeline should never break the actual page for a reader. Events older than 90 days get swept out automatically, because analytics from last season is trivia, not information.

The mistake worth admitting: the first version of the consent banner asked for consent without linking to the privacy policy, so it asked you to agree to something it never explained. The code review caught it. The banner now links to the policy, and the policy got a section explaining exactly what is collected (the list above) and what is not. There was also a missing Secure flag on the visitor cookie that went out with the same fix.

The admin panel got an analytics tab where I can finally see the numbers: pageviews, unique visitors, top pages, referrers, and how long people stay. It turns out some of you actually do read the long posts. That was genuinely nice to learn, and I learned it without knowing a single thing about who you are.

0 comments

Log in to comment.

Log in

No account?