import { getDb, hashIp } from "./db"; import { lookupCountry } from "./geo"; // Patterns we don't want polluting analytics const BOT_UA = /bot|crawl|spider|slurp|curl|wget|httpclient|python-requests|axios|node-fetch|monitor|uptime|pingdom|datadog|prometheus|scanner|fetch|preview|whatsapp|telegrambot|facebookexternalhit|linkedinbot|twitterbot|discordbot|skypeuripreview|mastodon|matrix-bot|preconnect|dnsperf|sentry|newrelic|gtmetrix|lighthouse|headlesschrome|phantomjs|puppeteer|playwright|chrome-lighthouse/i; const SKIP_PATHS = /^\/(favicon\.ico|robots\.txt|sitemap\.xml|apple-touch-icon[\w-]*\.png|browserconfig\.xml|\.well-known\/|ads\.txt)/i; export function shouldRecord(method: string, path: string | null, userAgent: string | null): boolean { const m = (method || "GET").toUpperCase(); if (m === "HEAD" || m === "OPTIONS") return false; if (path && SKIP_PATHS.test(path)) return false; if (userAgent && BOT_UA.test(userAgent)) return false; return true; } type PendingHit = { domain_id: number; ts: number; ip_hash: string; country: string | null; user_agent: string | null; referer: string | null; path: string | null; }; const buffer: PendingHit[] = []; let flushTimer: NodeJS.Timeout | null = null; function flush() { if (buffer.length === 0) return; const batch = buffer.splice(0, buffer.length); try { const db = getDb(); const stmt = db.prepare( "INSERT INTO hits (domain_id, ts, ip_hash, country, user_agent, referer, path) VALUES (?, ?, ?, ?, ?, ?, ?)" ); db.transaction(() => { for (const h of batch) { stmt.run(h.domain_id, h.ts, h.ip_hash, h.country, h.user_agent, h.referer, h.path); } })(); } catch (e) { console.error("[hits] flush failed", e); } } function scheduleFlush() { if (flushTimer) return; flushTimer = setTimeout(() => { flushTimer = null; flush(); }, 5000); } export async function recordHit(input: { domain_id: number; ip: string; user_agent: string | null; referer: string | null; path: string | null; }) { const country = await lookupCountry(input.ip).catch(() => null); buffer.push({ domain_id: input.domain_id, ts: Date.now(), ip_hash: hashIp(input.ip), country, user_agent: input.user_agent ? input.user_agent.slice(0, 500) : null, referer: input.referer ? input.referer.slice(0, 500) : null, path: input.path ? input.path.slice(0, 500) : null, }); if (buffer.length >= 100) flush(); else scheduleFlush(); } export function flushHitsSync() { flush(); }