Back to All Articles
Web Development•
7 min read

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.

S
Siegfried Stehring•
Building Cookie-Free, Privacy-First Web Telemetry That Adblockers Don't Break

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:

  1. 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.
  2. 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.
  3. 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

  1. Non-Reversible: You cannot mathematically reverse the 16-character SHA-256 hash to deduce the visitor's real IP address or identity.
  2. Daily Expiration: Because dateString changes 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.
  3. Zero Cookies Required: Because no state is written to the visitor's browser (localStorage, sessionStorage, or Set-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:

  1. Is hosted on the same first-party domain (/api/track),
  2. Uses standard application JSON payloads, and
  3. 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!

Support on Ko-fi
Tags:#Analytics#Privacy#Next.js#TypeScript#GeoIP#GDPR#Web Dev#Telemetry

Related Engineering Guides

Architecture10 min read

Why Your Next Internal Tool Doesn't Need a Database: The Case for File-Backed & SQLite Architectures

As teams replace expensive SaaS subscriptions with bespoke internal tools, the biggest trap is over-engineering the persistence tier. Why flat files, Git-backed Markdown, and single-file SQLite in WAL mode outperform distributed client-server databases in latency, cost, and operational resilience.

Read Article
Cybersecurity9 min read

Implementing Zero-Dependency RFC 6238 TOTP from Scratch in Modern TypeScript

A complete, zero-dependency architectural guide to building RFC 6238 Time-based One-Time Passwords (TOTP) and RFC 4226 HOTP in TypeScript—handling Base32 decoding, HMAC-SHA1 dynamic truncation, virtualized LXC container clock drift, and single-use offline recovery codes without external cloud auth dependencies.

Read Article
Architecture7 min read

Architecture of a Modern Self-Hosted Tech Blog: From Proxmox to Next.js and Git-Push CI/CD

A comprehensive architectural breakdown of how this blog was engineered from the bare metal up: Proxmox LXC containers, Docker host networking, privacy-first local analytics, and a zero-cloud GitOps CI/CD pipeline.

Read Article