/_astro/posse-analytics-cover.KWWUXmpH.png

POSSE-Flavored Analytics for the Blog

Self-hosted Umami on my own hardware, no JavaScript beacon, server-side bot filters: analytics, POSSE-style.

JACK IVERSBRIEF3 MIN READ

Until yesterday, cto4.ai had no analytics at all. No Google Analytics, no beacon script, no cookies. That was deliberate, in line with both the POSSE philosophy this site runs on and Astro’s ship-less-JavaScript ethos: I publish on my own site, you read it, and nobody gets tracked for the privilege.

Then I killed the newsletter, and the little visibility I had into uptake went with it. (The subscriber count was mostly bots anyway, but still.) Analytics started to look like a good idea, if it could be done the right way.

The Rig

I worked with Fable to identify “the most POSSE-aligned analytics solution.” The requirements fall out of the philosophy: I own the data, readers don’t run third-party scripts, no cookies, no fingerprinting. The answer we landed on is Umami, self-hosted: Umami and Postgres in Docker on the same Mac Mini that runs my Hermes Agent knowledge refinery, reachable through a Cloudflare Tunnel. Once a page view is written, the data lives on my hardware in my basement and nowhere else.

The twist is on the collection side: there is no JavaScript beacon on the blog at all. Instead, a roughly 50-line Cloudflare Pages middleware reports page views server-side as it serves them: GET requests that return HTML with a 200, on the production hostname only, skipping prefetch and prerender loads. There’s nothing for an adblocker to block, because there’s nothing running in your browser. If you’re reading this, you’ve already been counted, and you gave up nothing for it.

Bots, Again

Server-side collection has a cost: a beacon script filters most bots for free, because crawlers don’t execute JavaScript. Count on the server and you count everything — curl included. Having just spent a day shoveling 2,089 bot subscribers out of a newsletter, I wasn’t going to stand up a dashboard full of the same species.

So the middleware applies two baseline filters before reporting anything. First: drop requests with no Accept-Language header. Real browsers always send one; most crawlers can’t be bothered. Second: drop traffic from datacenter networks. Cloudflare hands the middleware the network owner on every request, and a page view from AWS, Azure, Hetzner, or a dozen other cloud providers is not a human on a couch. Bots still get the page; they just stop being counted.

So how well do the filters work? The first full day of data says: partially. Roughly 40% of the sessions that got through were still bots — headless browsers on residential IPs, rotating user agents (four “different browsers” starting in the same second is one crawler, not four readers), walking the back catalog page by page. But here’s the flip side of owning the data: the raw sessions live in my Postgres, so whenever I want a clean read on human traffic, fingerprinting them like that takes minutes. The dashboard number runs hot; the truth is one query away. Trends and campaign attribution stay reliable regardless, because bots don’t carry UTM parameters. And yes, my own visits still count; self-exclusion is on the list.

So Far

I’m pretty happy with the result. A dashboard I own, on hardware I own, telling me things I can mostly trust — and the blog still serves zero tracking code.

Umami analytics Overview page for cto4.ai showing 40 visitors, 78 visits, and 174 views over the last 24 hours, with an hourly bar chart spiking at each of the day's three post publishes.
About 24 hours of data. The spikes are three posts publishing — and readers actually arriving. The rightmost one is this post.
Back to Archive

Related Posts

View All Posts »
I Shot the Email

I Shot the Email

The cto4.ai newsletter is dead — killed in self-defense. The web POSSE rides on.