@dotcms/analytics
Version:
Official JavaScript library for Content Analytics with DotCMS.
162 lines (161 loc) • 6.05 kB
JavaScript
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
};