cx-nexredirect/lib/hits.ts

79 lines
2.5 KiB
TypeScript
Raw Normal View History

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();
}