← Blog
PRIVACY5 min read

The analytics decision that started with reading someone else's source code

Sep 14, 2026DekimuAI-generated

Self-hosted analytics settled its own data-handling question the hard way: not by trusting the vendor's FAQ, which promises it collects no personal data, but by reading its source. The code shows a hashed, salted session identifier, a salt that rotates monthly by default, and a retention system nobody had built yet — none of which the FAQ mentions.

What actually gets stored, checked against the code

The choice was Umami, self-hosted, over Google Analytics, Vercel Analytics, and Plausible. Google was out on a standing rule before any other argument applied. Vercel Analytics would have added a product dependency on a vendor for a signal that can be held in-house. Plausible is a genuine peer, but its standard deployment adds ClickHouse to infrastructure that doesn't run it. Umami is MIT-licensed, self-hosts against a plain Postgres already known how to back up, and is cookieless by default.

"Cookieless by default" is a vendor claim, so it wasn't taken as one. The actual send route builds a session id as a keyed hash of the website id, IP, user agent, and a rotating salt — no cookie, nothing written to local storage or session storage. That's what removes the deployment from ePrivacy consent rules, and it's also why no consent banner will appear on any Dekimu landing that runs it. The session id is still a pseudonymous identifier under GDPR, not an anonymous one, which is why it needed its own entry in the company's Records of Processing Activities before a single line of deployment code was written, resting on a legitimate-interest balancing test rather than consent.

Four things the FAQ didn't mention

Writing the deployment plan against the actual 3.3.1 source, not the docs, surfaced four corrections in one afternoon. The session-replay toggle wasn't the database column a first draft assumed — it's a different pair of fields entirely, and the original check would have errored instead of asserting anything. The schema turned out to have no foreign keys at all, enforced only in the application layer, which means a retention job that misses a table doesn't crash — it prints a plausible-looking success and quietly keeps that table's data forever. A verification test that was going to take two real days of waiting collapsed into a one-second query once it became clear the tracker accepts a timestamp in its own payload. And a draft privacy-policy paragraph promised that Do Not Track and Global Privacy Control would both stop the tracker, when the bare install script honors neither without two extra attributes and a callback.

A vendor's FAQ describes what a product is supposed to do. Reading the source describes what it actually does.

Building the retention job Umami doesn't ship

There's no built-in expiry, so a thirteen-month retention window is a job to own, not a setting to tick. Because nothing in the schema cascades, the safe list is an explicit allowlist of the nine tables that actually hold visitor data, each pruned on its own timestamp — not a sweep of every table with a timestamp column, which would delete the install's own user accounts and settings along with the visitor rows. Two tables were missing from the first draft of that list, including one that stores a caller-supplied identifier with no foreign key pointing at it anywhere; left out, it would have kept that identifier forever while every other table looked correctly pruned. The job runs as a system-level timer rather than a user one, because a user timer failing silently for two weeks is exactly how a similar backup job on other infrastructure already went unnoticed.

What's built, and what's still parked

Session replay and heatmaps ship in this version of Umami, off by default, one checkbox away from being turned on inside the admin panel. That checkbox stays untouched — recording a visitor's on-page behavior is a different processing activity from counting a page view, needs its own compliance record, and was never part of what got approved.

The decision is made, the compliance record is written, and the deployment — compose file, reverse proxy config, the retention timer, seven checks that assert real behavior rather than a config file's presence — is fully specified and committed. What isn't done yet is running it anywhere: the plan to host it on the company's existing CI server was rejected on purpose, because that server is deliberately closed to public traffic and putting a public tracking endpoint on it would undo that; running it on Vercel would trade a self-hosted database for a new managed one, the exact dependency self-hosting was chosen to avoid. A dedicated small server was the answer, and buying one is a purchase decision that hasn't been made. Nine landing pages are lined up to receive the tracker once a server exists, with one brand deliberately excluded pending a separate ruling. Six months after it does ship, if no product decision has actually changed because of a number it produced, the plan is to delete it — analytics nobody acts on is a processing activity with no purpose.

PRIVACY

This post was drafted by an AI system from Dekimu's public engineering record and published with automated checks, without per-post human editing.

← Back to blog
The analytics decision that started with reading someone else's source code — Dekimu Blog