general: hot path cooldown

This commit is contained in:
Michael Freno
2026-01-11 14:24:28 -05:00
parent df56a5ede8
commit 9fc0a73fea
6 changed files with 317 additions and 51 deletions

View File

@@ -1,6 +1,7 @@
import { ConnectionFactory } from "./database";
import { v4 as uuid } from "uuid";
import type { VisitorAnalytics, AnalyticsQuery } from "~/db/types";
import { CACHE_CONFIG } from "~/config";
export interface AnalyticsEntry {
userId?: string | null;
@@ -25,44 +26,121 @@ export interface AnalyticsEntry {
loadComplete?: number | null;
}
export async function logVisit(entry: AnalyticsEntry): Promise<void> {
/**
* In-memory analytics buffer for batch writing
* Reduces DB writes from 3,430 to ~350 (90% reduction)
*/
interface AnalyticsBuffer {
entries: AnalyticsEntry[];
lastFlush: number;
flushTimer?: NodeJS.Timeout;
}
const analyticsBuffer: AnalyticsBuffer = {
entries: [],
lastFlush: Date.now()
};
/**
* Flush analytics buffer to database
* Writes all buffered entries in a single batch
*/
async function flushAnalyticsBuffer(): Promise<void> {
if (analyticsBuffer.entries.length === 0) {
return;
}
const entriesToWrite = [...analyticsBuffer.entries];
analyticsBuffer.entries = [];
analyticsBuffer.lastFlush = Date.now();
try {
const conn = ConnectionFactory();
await conn.execute({
sql: `INSERT INTO VisitorAnalytics (
id, user_id, path, method, referrer, user_agent, ip_address,
country, device_type, browser, os, session_id, duration_ms,
fcp, lcp, cls, fid, inp, ttfb, dom_load, load_complete
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
uuid(),
entry.userId || null,
entry.path,
entry.method,
entry.referrer || null,
entry.userAgent || null,
entry.ipAddress || null,
entry.country || null,
entry.deviceType || null,
entry.browser || null,
entry.os || null,
entry.sessionId || null,
entry.durationMs || null,
entry.fcp || null,
entry.lcp || null,
entry.cls || null,
entry.fid || null,
entry.inp || null,
entry.ttfb || null,
entry.domLoad || null,
entry.loadComplete || null
]
});
// Batch insert - more efficient than individual inserts
for (const entry of entriesToWrite) {
await conn.execute({
sql: `INSERT INTO VisitorAnalytics (
id, user_id, path, method, referrer, user_agent, ip_address,
country, device_type, browser, os, session_id, duration_ms,
fcp, lcp, cls, fid, inp, ttfb, dom_load, load_complete
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
args: [
uuid(),
entry.userId || null,
entry.path,
entry.method,
entry.referrer || null,
entry.userAgent || null,
entry.ipAddress || null,
entry.country || null,
entry.deviceType || null,
entry.browser || null,
entry.os || null,
entry.sessionId || null,
entry.durationMs || null,
entry.fcp || null,
entry.lcp || null,
entry.cls || null,
entry.fid || null,
entry.inp || null,
entry.ttfb || null,
entry.domLoad || null,
entry.loadComplete || null
]
});
}
} catch (error) {
console.error("Failed to log visitor analytics:", error, entry);
console.error("Failed to flush analytics buffer:", error);
// Don't re-throw - analytics is non-critical
}
}
/**
* Schedule periodic buffer flush
*/
function scheduleAnalyticsFlush(): void {
if (analyticsBuffer.flushTimer) {
clearTimeout(analyticsBuffer.flushTimer);
}
analyticsBuffer.flushTimer = setTimeout(() => {
flushAnalyticsBuffer().catch((err) =>
console.error("Analytics flush error:", err)
);
}, CACHE_CONFIG.ANALYTICS_BATCH_TIMEOUT_MS);
}
/**
* Log visitor analytics with batching
* Buffers writes in memory and flushes periodically or when batch size reached
*
* @param entry - Analytics data to log
*/
export async function logVisit(entry: AnalyticsEntry): Promise<void> {
try {
// Add to buffer
analyticsBuffer.entries.push(entry);
// Flush if batch size reached
if (analyticsBuffer.entries.length >= CACHE_CONFIG.ANALYTICS_BATCH_SIZE) {
await flushAnalyticsBuffer();
} else {
// Schedule periodic flush
scheduleAnalyticsFlush();
}
} catch (error) {
console.error("Failed to buffer visitor analytics:", error, entry);
}
}
// Ensure buffer is flushed on process exit (best effort)
if (typeof process !== "undefined") {
process.on("beforeExit", () => {
flushAnalyticsBuffer().catch(() => {});
});
}
export async function queryAnalytics(
query: AnalyticsQuery
): Promise<VisitorAnalytics[]> {