Why We Built Our Own Analytics
Google Analytics is the default for most websites, but it comes with a cost: invasive tracking, cookie banners, and sharing your visitors' data with the world's largest advertising company. Simple Analytics and Plausible are great alternatives, but they cost money.
We wanted something different — a privacy-first analytics tool that's free, self-hostable, and doesn't compromise on the data you actually need.
The Architecture
Our analytics stack is surprisingly simple:
// The entire tracking script is ~1KB
(function () {
if (navigator.doNotTrack === "1") return;
var endpoint = serverUrl + "/api/analytics/collect";
function send(payload, useBeacon) {
var body = JSON.stringify(payload);
if (useBeacon && navigator.sendBeacon) {
navigator.sendBeacon(endpoint,
new Blob([body], { type: "text/plain" }));
} else {
fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body,
keepalive: true,
});
}
}
})();No Cookies, No Fingerprinting
Instead of cookies, we use a daily-rotating hash of the visitor's IP + User Agent + date. This gives us unique visitor counting without storing any personally identifiable information:
const visitorId = createHash("sha256")
.update(`${ip}|${userAgent}|${today}|${siteId}`)
.digest("hex")
.slice(0, 16);The hash rotates daily, so we can't track anyone across days. No cookie banners needed.
The Database Layer
We use Neon PostgreSQL in production with a SQLite fallback for local development. The abstraction layer makes this seamless:
const DATABASE_URL = process.env.DATABASE_URL;
const useNeon = !!DATABASE_URL;
async function queryAll<T>(sql: string, params: unknown[]) {
if (useNeon) {
const fn = getSql();
let idx = 0;
const pgSql = sql.replace(/\?/g, () => `$${++idx}`);
return await fn.query(pgSql, params);
}
const db = await getDb();
return db.prepare(sql).all(...params);
}What We Track
- Pageviews and unique visitors (daily hash, not persistent)
- Referrers (where traffic comes from)
- Browsers, OS, and device types (parsed from User-Agent)
- Countries (from Netlify's
x-countryheader) - UTM parameters (campaign tracking)
- Page duration (via
visibilitychangeevents) - Bounce rate (calculated per-session, not per-pageview)
What We Don't Track
- No cookies
- No fingerprinting
- No cross-site tracking
- No personal data storage
- No third-party scripts
- Respects Do Not Track
The Dashboard
The dashboard is built with React and Recharts, showing real-time data with a date range picker. Admin users can manage sites, toggle public visibility for demo access, and export data as CSV.
Lessons Learned
sendBeaconwithapplication/jsontriggers CORS preflight — we usetext/plaininstead and parse JSON on the server regardless of Content-Type.- SQLite's
CURRENT_TIMESTAMPformat differs from JavaScript'stoISOString()— always normalize datetime comparisons. - Bounce rate should be per-session, not per-pageview — a visitor who views 3 pages isn't bouncing even if each page has
is_bounce=1by default.
Try It
Add the tracking script to your site:
<script defer src="https://anit.guru/t.js"
data-site="yourdomain.com"></script>Then log in at anit.guru/tools/analytics to see your data. There's a demo account too — just click "Try the demo" on the login page.