diff --git a/src/app/routes/api/events.$.ts b/src/app/routes/api/events.$.ts index 5fa0689..1ced59f 100644 --- a/src/app/routes/api/events.$.ts +++ b/src/app/routes/api/events.$.ts @@ -39,9 +39,10 @@ export const Route = createFileRoute("/api/events/$")({ serverEvents.on(type, handleEvent); } + // Under the shortest idle timeout observed on proxies/PWA paths. const pingInterval = setInterval(() => { send({ type: "ping", timestamp: Date.now() }); - }, 15000); + }, 8000); cleanup = () => { if (cleanedUp) return; diff --git a/src/lib/telemetry/client/index.ts b/src/lib/telemetry/client/index.ts index 21c05b9..91d08b1 100644 --- a/src/lib/telemetry/client/index.ts +++ b/src/lib/telemetry/client/index.ts @@ -17,19 +17,37 @@ const recentErrors = new Map(); const isEnabled = (): boolean => typeof window !== "undefined"; -const getSessionId = (): string => { +// A session ends after 30 idle minutes, not when the tab closes — installed +// PWAs keep sessionStorage alive for days. +const SESSION_IDLE_MS = 30 * 60 * 1000; +const SESSION_KEY = "flxn-tsid"; + +const readSession = (): { id: string; t: number } | null => { try { - let id = sessionStorage.getItem("flxn-tsid"); - if (!id) { - id = crypto.randomUUID(); - sessionStorage.setItem("flxn-tsid", id); + const raw = sessionStorage.getItem(SESSION_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw); + if (typeof parsed?.id === "string" && typeof parsed?.t === "number") { + return parsed; } - return id; + return null; } catch { - return "anon-session"; + return null; } }; +const touchSession = () => { + try { + const now = Date.now(); + const current = readSession(); + const id = + current && now - current.t <= SESSION_IDLE_MS ? current.id : crypto.randomUUID(); + sessionStorage.setItem(SESSION_KEY, JSON.stringify({ id, t: now })); + } catch {} +}; + +const getSessionId = (): string => readSession()?.id ?? "anon-session"; + const location = (): { path: string; routeId?: string } => ({ path: window.location.pathname, routeId: lastRouteId, @@ -64,6 +82,7 @@ const flush = (useBeacon = false) => { const enqueue = (event: QueuedEvent) => { if (!isEnabled()) return; + touchSession(); queue.push(event); if (queue.length >= FLUSH_AT) { flush();