Building Cookie-Free, Privacy-First Web Telemetry That Adblockers Don't Break
Why we ditched Google Analytics and built a lightweight, 100% self-hosted visitor telemetry engine in Next.js: using cryptographic daily rotating salt hashes, offline MaxMind GeoIP lookups, navigator.sendBeacon, and zero third-party cookie banners.
Every web engineer wants to know how their technical articles perform: Which posts resonate with readers? What operating systems and browsers are visiting? How much global traffic arrives each day?
For over a decade, the default answer was to paste the Google Analytics tracking script (gtag.js) into your <head> tag. But in modern web development, particularly for a self-hosted engineering blog, standard cloud tracking scripts present three deal-breaking problems:
- User Privacy & Regulatory Headaches: Google Analytics tracks individuals across different websites, sets persistent third-party cookies, and sends EU citizen IP addresses to US data centers—forcing site owners to display invasive, user-hostile GDPR cookie consent banners.
- Adblocker Blind Spots: Upwards of 45% to 65% of technical readers use content blockers (uBlock Origin, Brave Shields, Pi-hole, AdGuard). Third-party telemetry domains (
google-analytics.com,segment.io) are blocked by default, meaning your dashboard underreports developer traffic by more than half. - Bloat & Third-Party Latency: External analytics scripts add 45 kB+ of JavaScript, execute network handshakes to external domains, and degrade Core Web Vitals and Lighthouse scores.
In this architectural guide, we break down how we built a 100% self-hosted, zero-cookie, GDPR-compliant visitor telemetry engine inside our Next.js blog stack—complete with cryptographic daily rotating visitor salts, offline local GeoIP resolution, and resilient beacon delivery that adblockers do not disrupt.
1. Architectural Blueprint: Sovereign First-Party Telemetry
The core premise of privacy-first telemetry is simple: track page events and aggregate trends, not individual human beings.
[ Reader's Browser ]
│
▼ navigator.sendBeacon('/api/track') (First-party endpoint, zero adblocker match)
[ Next.js Edge API Route (/api/track) ]
│
├─► Extract Request Headers (x-forwarded-for, user-agent, referer)
│
├─► Cryptographic Daily Salt Hash:
│ visitorHash = SHA256(IP + UserAgent + TodayDateString + SecretKey).slice(0, 16)
│ * Anonymized, non-reversible, and resets automatically at midnight UTC!
│
├─► Offline Local GeoIP Engine (geoip-lite / MaxMind binary):
│ IP ──► Country: "United Kingdom", Code: "GB", Flag: "🇬🇧", City: "London"
│ * Zero external HTTP queries; sub-millisecond local lookup.
│
▼
[ Self-Hosted Analytics Datastore (content/analytics.json / SQLite) ]
│
▼
[ Admin Telemetry Studio Dashboard ]
2. Cryptographic Anonymization: The Daily Rotating Salt
To calculate Unique Visitors without storing plain-text IP addresses or planting persistent tracking cookies on the reader's device, we use a cryptographic Daily Rotating Salt Hash.
How It Works
We compute a SHA-256 HMAC combining:
- The visitor's IP address
- The browser User-Agent string
- The current calendar date string (e.g.
2026-09-26) - A private server-side secret key
import crypto from 'crypto';
/**
* Generates an anonymous, non-reversible visitor hash valid only for the current day.
*/
export function generateDailyVisitorHash(
ip: string,
userAgent: string,
dateString: string
): string {
const serverSecret = process.env.ADMIN_PASSWORD || 'default-telemetry-salt';
// Combine factors
const payload = `${ip}:${userAgent}:${dateString}:${serverSecret}`;
// Return truncated 16-character hex hash
return crypto
.createHash('sha256')
.update(payload)
.digest('hex')
.slice(0, 16);
}
Why This Is Completely GDPR-Compliant
- Non-Reversible: You cannot mathematically reverse the 16-character SHA-256 hash to deduce the visitor's real IP address or identity.
- Daily Expiration: Because
dateStringchanges at midnight, the exact same visitor reading an article on Monday and Tuesday will have completely different hashes. It is impossible to build long-term behavioral tracking profiles across days or across external websites. - Zero Cookies Required: Because no state is written to the visitor's browser (
localStorage,sessionStorage, orSet-Cookie), no cookie banners or consent popups are legally required under GDPR, ePrivacy Directive, or CCPA.
3. Offline Local GeoIP Resolution
To understand where your readers are located geographically without sending their IP to third-party APIs (like ipinfo.io or ipapi.co), use local MaxMind binary databases via geoip-lite.
import geoip from 'geoip-lite';
export interface GeoLocation {
country: string;
countryCode: string;
city: string;
flag: string;
}
export function resolveGeoLocation(ip: string): GeoLocation {
// Strip IPv6 encapsulation and private LAN addresses
const cleanIp = ip.replace(/^.*:/, '').trim();
// Handle local loopback / private subnets (RFC 1918)
if (
cleanIp === '127.0.0.1' ||
cleanIp.startsWith('10.') ||
cleanIp.startsWith('192.168.') ||
cleanIp.startsWith('172.16.')
) {
return {
country: 'Local Network',
countryCode: 'LAN',
city: 'Homelab',
flag: '🏠',
};
}
const lookup = geoip.lookup(cleanIp);
if (!lookup) {
return {
country: 'Unknown',
countryCode: 'XX',
city: 'Unknown',
flag: '🌐',
};
}
// Convert 2-letter ISO country code to Emoji Flag
const flag = lookup.country
.toUpperCase()
.replace(/./g, (char) =>
String.fromCodePoint(char.charCodeAt(0) + 127397)
);
return {
country: lookup.country, // e.g. "GB"
countryCode: lookup.country,
city: lookup.city || 'Unknown',
flag,
};
}
Because the database resides locally in system memory, resolving an IP address takes under 0.05 milliseconds, adds zero network latency, and never leaks your visitors' IP addresses to external telemetry clouds.
4. Client-Side Dispatch: Why sendBeacon Beats Standard Fetch
In modern web applications, readers frequently close the browser tab or click a link to navigate away before a standard fetch() completes. Traditional asynchronous requests are cancelled by the browser when the page unloads.
The W3C Beacon API (navigator.sendBeacon) was specifically designed for telemetry. It cues the browser to transmit the payload asynchronously over HTTP POST in the background, guaranteeing delivery even after the tab closes.
// src/components/AnalyticsTracker.tsx
'use client';
import { useEffect, useRef } from 'react';
import { usePathname } from 'next/navigation';
export default function AnalyticsTracker() {
const pathname = usePathname();
const lastTrackedPath = useRef<string | null>(null);
useEffect(() => {
// Avoid double-counting and ignore private admin studio routes
if (pathname === lastTrackedPath.current || pathname.startsWith('/admin')) {
return;
}
lastTrackedPath.current = pathname;
const payload = JSON.stringify({
path: pathname,
referrer: document.referrer || '',
screen: `${window.innerWidth}x${window.innerHeight}`,
});
// 1. Prefer navigator.sendBeacon for reliable delivery
if (navigator.sendBeacon) {
const blob = new Blob([payload], { type: 'application/json' });
navigator.sendBeacon('/api/track', blob);
} else {
// 2. Fallback to fetch with keepalive: true
fetch('/api/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true,
}).catch(() => {});
}
}, [pathname]);
return null;
}
Why Adblockers Don't Intercept This
Adblockers operate using regular expression rules (EasyList, Peter Lowe's List) matching known tracking domains (google-analytics.com, doubleclick.net, hotjar.com) or specific script filenames (analytics.js, gtag.js).
Because our tracking endpoint:
- Is hosted on the same first-party domain (
/api/track), - Uses standard application JSON payloads, and
- Is built into the native Next.js bundle without external script tags,
it is completely immune to adblocker false positives while respecting the user's actual privacy.
5. Architectural Comparison
| Dimension | Google Analytics 4 | SaaS Privacy Tools (Plausible/Fathom) | Self-Hosted First-Party Telemetry | | :--- | :--- | :--- | :--- | | Hosting & Location | Google Cloud (US) | Third-Party SaaS Cloud | Your Private Proxmox Node | | Tracking Cookies | Yes (Persistent client ID) | No | None (Zero cookies) | | GDPR Banner Needed? | ✅ Yes (Mandatory) | ❌ No | ❌ No (Exempt) | | Adblocker Drop Rate | 45% – 65% blocked | 15% – 30% blocked | 0% blocked (First-party endpoint) | | Client JS Payload | ~48 kB minified | ~2.5 kB | < 0.8 kB native React component | | Monthly Cost | Free (at the expense of user data) | $9 – $29 / month | $0.00 (Self-hosted) | | Offline GeoIP | ❌ External cloud only | ❌ External cloud only | ✅ Local in-memory MaxMind (< 0.1ms) |
Conclusion
Building your own telemetry engine is one of the most rewarding additions to a self-hosted platform. It demonstrates respect for your audience's digital autonomy, eliminates annoying cookie consent banners, and gives you accurate, dependable visitor analytics without a single byte of telemetry leaving your servers.
Support Independent Technical Content
If this article helped you build, debug, or host your applications, consider supporting the blog on Ko-fi. Every cup of coffee helps keep this site ad-free and 100% self-hosted!