The wake-up call I didn't ask for
Last week the TanStack folks reported what appears to be a compromise affecting some of their NPM packages (the details are still being sorted out in issue #7383 — read it yourself before drawing conclusions). I won't rehash the postmortem here. What I want to talk about is the gut-punch feeling I had reading it.
I run npm install every day. I've barely thought about which third-party scripts are loading in production. And one of the worst offenders sitting in nearly every site I've ever shipped? Analytics.
So this post is about something I've been chewing on for months but finally moved on: ripping Google Analytics out of three side projects and picking a privacy-focused alternative. Specifically, I'll compare Umami, Plausible, and Fathom — the three I actually evaluated — and walk through the migration steps that worked for me.
Why even migrate?
A few honest reasons, none of them ideological:
-
Script weight. GA4's
gtag.jsis heavy. The privacy-focused tools are typically 1–2 KB. - Cookie banners. No cookies = no consent banner in most jurisdictions. Fewer modals = fewer bounces.
- Vendor trust. After watching a supply chain story unfold in real time, having fewer third-party scripts feels less reckless.
- Self-hosting option. If I can run it on my own infra, I control the script.
If you genuinely need Google's audience features (remarketing, conversion linking to Google Ads), this post probably isn't for you. Stay where you are.
The contenders
Plausible
Open source (AGPL), GDPR/CCPA compliant, cloud or self-hosted. The script is small — the docs claim under 1 KB. Written in Elixir. Cloud plans are subscription-based.
Fathom
Privacy-focused, cloud-only since they pivoted from the original open source v1 ("Fathom Lite," archived) to a commercial closed-source product. I evaluated the commercial product.
Umami
Open source (MIT), self-hosted by default with a hosted cloud option on umami.is. Built on Next.js, runs on PostgreSQL or MySQL. Free if you host it yourself. Easy enough that I had it running in an evening.
Side-by-side
I'll keep this honest — I ran all three on the same site for two weeks before deciding.
| Feature | Plausible | Fathom | Umami |
|---|---|---|---|
| Open source | Yes (AGPL) | No (closed) | Yes (MIT) |
| Self-host | Yes | No | Yes (primary path) |
| Cookies | No | No | No |
| GDPR | Yes | Yes | Yes |
| Cloud option | Paid | Paid | Free tier + paid |
| Script size | ~1 KB | ~2 KB | ~2 KB |
| Funnels / goals | Yes | Yes | Yes (basic) |
The sizes above match what I observed in the network tab, but check each vendor's docs before quoting them anywhere serious.
What the snippets look like
Replacing GA is mostly about swapping a script tag. Here's the before:
<!-- Google Analytics (the thing we're leaving) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXX'); // sends pageview + sets cookies
</script>
And the replacements:
<!-- Plausible (cloud) -->
<script defer data-domain="example.com"
src="https://plausible.io/js/script.js"></script>
<!-- Fathom -->
<script src="https://cdn.usefathom.com/script.js"
data-site="ABCDEFG" defer></script>
<!-- Umami (self-hosted) -->
<script defer src="https://analytics.mydomain.com/script.js"
data-website-id="your-website-id"></script>
That's it. No dataLayer. No consent banner gate. The script loads once, sends a single beacon per pageview, and stops bothering you.
Custom events
The thing I almost forgot when migrating: GA's gtag('event', ...) calls. Here's how I rewrote them for Umami (the APIs are similar across the three, but each has its own conventions):
// Before (GA4)
gtag('event', 'signup_completed', {
plan: 'pro',
source: 'pricing_page'
});
// After (Umami)
// `umami` is attached to window by the loader script
window.umami?.track('signup_completed', {
plan: 'pro',
source: 'pricing_page'
});
Plausible uses window.plausible('signup_completed', { props: { plan: 'pro' } }). Fathom uses fathom.trackEvent('signup_completed'). Don't do a global find-and-replace — the property conventions differ enough that you'll want to read each vendor's docs first.
Self-hosting Umami in five minutes
This is the part that sold me. Here's the docker-compose.yml running on the VPS for one of my side projects:
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://umami:umami@db:5432/umami
DATABASE_TYPE: postgresql
APP_SECRET: change-me-to-a-real-secret # rotate this
depends_on:
db:
condition: service_healthy
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- umami-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U umami"]
volumes:
umami-db:
Run it behind Caddy or Nginx, point a subdomain at it, drop the script tag into your site. You own the data. Nothing leaves your server. The dashboard is genuinely pleasant — the Next.js UI loads fast and shows the things I actually look at.
Migration steps that worked
No magic, just mechanical:
-
Inventory your GA calls. Grep your codebase for
gtag(,dataLayer, and any analytics wrapper functions. Write them down. - Pick your destination. Zero ongoing cost and own your data → self-hosted Umami. Don't want to run Postgres → Plausible Cloud. Want the most polished commercial dashboard → Fathom.
- Run them in parallel for a week. Drop the new script alongside GA. Compare daily pageview counts. You'll see drift — the privacy-focused tools usually report fewer visits because they don't fingerprint, and that's kind of the point.
-
Rewrite custom events. Map each
gtag('event', ...)to the new API. Wrap them in a helper so you can switch again later without grepping. - Remove the GA script and the cookie banner. This is the satisfying part.
My recommendation
Honestly? Here's how I'd choose:
- Side projects, solo devs: Self-hosted Umami. Free, simple, MIT-licensed.
- Small business, no ops appetite: Plausible Cloud. Easiest onboarding, still open source if you ever want to migrate off.
- Polished dashboards for clients: Fathom. The UX feels the most "finished" of the three.
I'm not saying Google Analytics is bad — it's free, it's powerful, and it's still the right answer if you live inside their ad ecosystem. But for the rest of us, three lines of script and a Postgres container will get you 90% of what you actually look at, with one less third-party domain in your Content-Security-Policy.
The TanStack situation reminded me that every script tag is a trust decision. Make fewer trust decisions.
Top comments (0)