UNPKG

@dotcms/analytics

Version:

Official JavaScript library for Content Analytics with DotCMS.

162 lines (161 loc) 6.05 kB
import q from "@analytics/queue-utils"; import F from "@analytics/router-utils"; import { DEFAULT_QUEUE_CONFIG as x, TAB_ID_STORAGE_KEY as I, MAX_EVENT_AGE_MS as z, QUEUE_STORAGE_KEY_PREFIX as P } from "../constants/dot-analytics.constants.js"; import { sendAnalyticsEvent as w } from "../http/dot-analytics.http.js"; import { createPluginLogger as T, safeSessionStorage as d, generateSecureId as U, getAnalyticsContext as D } from "../utils/dot-analytics.utils.js"; const R = (l) => { const n = T("Queue", l); let i = null, c = null, f = !1, m = !1, v = typeof window < "u" ? window.location.pathname : "", o = "", a = []; const y = { ...x, ...typeof l.queue == "object" ? l.queue : {} }, g = () => `${P}_${o}`, E = (e) => { const t = e; if (!t || typeof t != "object" || Array.isArray(t)) return null; if (typeof t.tabId != "string" || typeof t.timestamp != "number" || !Number.isFinite(t.timestamp) || !Array.isArray(t.events)) return n.warn("Invalid persisted queue: structural mismatch"), null; const s = t.events.filter( (r) => r && typeof r == "object" && "event_type" in r ); return { tabId: t.tabId, timestamp: t.timestamp, events: s }; }, A = () => { try { const e = g(), t = d.getItem(e); if (!t) return n.debug("No persisted queue found"), null; const s = JSON.parse(t), r = E(s); if (!r) return d.removeItem(e), null; const u = Date.now() - r.timestamp; return u > z ? (n.warn( `Persisted events too old (${Math.round(u / 1e3 / 60 / 60)}h), discarding` ), d.removeItem(e), null) : (n.info( `Loaded ${r.events.length} persisted event(s) from storage (age: ${Math.round(u / 1e3)}s)` ), r); } catch (e) { return n.error("Failed to load persisted queue", e), d.removeItem(g()), null; } }, h = () => { if (a.length !== 0) try { const e = g(), t = { tabId: o, timestamp: Date.now(), events: a }; d.setItem(e, JSON.stringify(t)), n.debug(`Persisted ${a.length} event(s) to storage`); } catch (e) { e instanceof Error && e.name === "QuotaExceededError" ? n.warn("sessionStorage quota exceeded, continuing without persistence") : n.error("Failed to persist queue", e); } }, b = () => { try { const e = g(); d.removeItem(e), n.debug("Persisted queue cleared from storage"); } catch (e) { n.error("Failed to clear persisted queue", e); } }, _ = async (e, t = !0) => { if (e.length === 0) return !0; n.info(`Sending ${e.length} persisted event(s) immediately`); const r = { context: D(l), events: e }; return w(r, l, t); }, $ = (e, t) => { if (!c) return; n.debug(`Sending batch of ${e.length} event(s)`, { events: e, keepalive: f }), w({ context: c, events: e }, l, f), a = a.filter( (r) => !e.some((u) => u === r) ), a.length === 0 ? b() : h(); }, p = () => { !i || i.size() === 0 || !c || (n.info(`Flushing ${i.size()} events (page hidden/unload)`), f = !0, i.flush(!0)); }, S = () => { if (n.debug("handleVisibilityChange", document.visibilityState), document.visibilityState === "hidden") { if (m) { n.debug("Skipping flush (SPA navigation detected), persisting to storage"), h(); return; } p(); } else document.visibilityState === "visible" && (m = !1); }; return { /** * Initialize the queue with smart batching */ initialize: () => { if (typeof window < "u") { const t = d.getItem(I); if (t) o = t, n.debug(`Reusing Tab ID: ${o}`); else { typeof crypto < "u" && typeof crypto.randomUUID == "function" ? o = crypto.randomUUID() : (o = U("tab"), n.debug("crypto.randomUUID not available, using fallback generator")); try { d.setItem(I, o); } catch { } n.debug(`Generated new Tab ID: ${o}`); } } const e = A(); e && e.events.length > 0 && _(e.events, !1).then((t) => { t ? b() : n.warn( "Failed to send persisted events, keeping in storage for next retry" ); }), i = q( (t, s) => { $(t); }, { max: y.eventBatchSize, interval: y.flushInterval, throttle: !1 // Always false - enables both batch size and interval triggers } ), typeof window < "u" && typeof document < "u" && (document.addEventListener("visibilitychange", S), window.addEventListener("pagehide", p), F((t) => { m = !0, v = t, n.debug(`SPA navigation detected (${v})`), setTimeout(() => { m = !1; }, 100); })); }, /** * Add event to queue * smartQueue handles all batching logic automatically: * - Sends immediately when eventBatchSize reached (with throttle: false) * - Sends pending events every flushInterval */ enqueue: (e, t) => { if (c = t, !i) return; a.push(e); const s = i.size() + 1, r = y.eventBatchSize, u = s >= r; n.debug( `Event added. Queue size: ${s}/${r}${u ? " (full, sending...)" : ""}`, { eventType: e.event_type, event: e } ), i.push(e), h(); }, /** * Get queue size for debugging * Returns the number of events in smartQueue */ size: () => i?.size() ?? 0, /** * Clean up queue resources * Flushes remaining events and cleans up listeners * * IMPORTANT: Does NOT clear sessionStorage * - Storage is cleared only after sendBatch succeeds or in initialize() * - This allows events to persist across traditional page navigations */ cleanup: () => { p(), typeof window < "u" && typeof document < "u" && (document.removeEventListener("visibilitychange", S), window.removeEventListener("pagehide", p)), i = null, c = null, f = !1, a = []; } }; }; export { R as createAnalyticsQueue };