@dotcms/analytics 1.2.3 → 1.2.4-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,26 @@
1
1
  import { Analytics as l } from "analytics";
2
- import { dotAnalyticsClickPlugin as d } from "./plugin/click/dot-analytics.click.plugin.js";
3
- import { dotAnalyticsEnricherPlugin as y } from "./plugin/enricher/dot-analytics.enricher.plugin.js";
2
+ import { getUVEState as d } from "../../uve/src/lib/core/core.utils.js";
3
+ import "../../uve/src/internal/constants.js";
4
+ import { dotAnalyticsClickPlugin as y } from "./plugin/click/dot-analytics.click.plugin.js";
5
+ import { dotAnalyticsEnricherPlugin as m } from "./plugin/enricher/dot-analytics.enricher.plugin.js";
4
6
  import { dotAnalyticsIdentityPlugin as u } from "./plugin/identity/dot-analytics.identity.plugin.js";
5
- import { dotAnalyticsImpressionPlugin as m } from "./plugin/impression/dot-analytics.impression.plugin.js";
6
- import { dotAnalytics as A } from "./plugin/main/dot-analytics.plugin.js";
7
- import { ANALYTICS_WINDOWS_ACTIVE_KEY as a, ANALYTICS_WINDOWS_CLEANUP_KEY as p, DotCMSPredefinedEventType as f } from "./shared/constants/dot-analytics.constants.js";
8
- import { validateAnalyticsConfig as C, getEnhancedTrackingPlugins as w } from "./shared/utils/dot-analytics.utils.js";
9
- import { cleanupActivityTracking as g } from "./plugin/identity/dot-analytics.identity.activity-tracker.js";
10
- const T = (t) => {
11
- const e = C(t);
12
- if (e)
7
+ import { dotAnalyticsImpressionPlugin as A } from "./plugin/impression/dot-analytics.impression.plugin.js";
8
+ import { dotAnalytics as p } from "./plugin/main/dot-analytics.plugin.js";
9
+ import { ANALYTICS_WINDOWS_ACTIVE_KEY as e, ANALYTICS_WINDOWS_CLEANUP_KEY as f, DotCMSPredefinedEventType as w } from "./shared/constants/dot-analytics.constants.js";
10
+ import { validateAnalyticsConfig as C, isBrowser as g, getEnhancedTrackingPlugins as E } from "./shared/utils/dot-analytics.utils.js";
11
+ import { cleanupActivityTracking as S } from "./plugin/identity/dot-analytics.identity.activity-tracker.js";
12
+ const O = (t) => {
13
+ const r = C(t);
14
+ if (r)
13
15
  return console.error(
14
- `DotCMS Analytics [Core]: Missing ${e.join(" and ")} in configuration`
15
- ), typeof window < "u" && (window[a] = !1), null;
16
- const s = w(
16
+ `DotCMS Analytics [Core]: Missing ${r.join(" and ")} in configuration`
17
+ ), typeof window < "u" && (window[e] = !1), null;
18
+ if (g() && d())
19
+ return console.warn("DotCMS Analytics [Core]: Analytics disabled inside UVE editor"), window[e] = !1, null;
20
+ const s = E(
17
21
  t,
18
- m,
19
- d
22
+ A,
23
+ y
20
24
  ), i = l({
21
25
  app: "dotAnalytics",
22
26
  debug: t.debug,
@@ -25,13 +29,13 @@ const T = (t) => {
25
29
  // Inject identity context
26
30
  ...s,
27
31
  //Track content impressions & clicks (conditionally loaded)
28
- y(),
32
+ m(),
29
33
  // Enrich and clean payload with page, device, utm data and custom data
30
- A(t)
34
+ p(t)
31
35
  // Send events to server
32
36
  ]
33
- }), r = () => g();
34
- return typeof window < "u" && (window.addEventListener("beforeunload", r), window[p] = r, window[a] = !0, window.dispatchEvent(new CustomEvent("dotcms:analytics:ready"))), {
37
+ }), a = () => S();
38
+ return typeof window < "u" && (window.addEventListener("beforeunload", a), window[f] = a, window[e] = !0, window.dispatchEvent(new CustomEvent("dotcms:analytics:ready"))), {
35
39
  /**
36
40
  * Track a page view.
37
41
  * Session activity is automatically updated by the identity plugin.
@@ -74,10 +78,10 @@ const T = (t) => {
74
78
  name: n,
75
79
  ...Object.keys(o).length > 0 && { custom: o }
76
80
  };
77
- i.track(f.CONVERSION, c);
81
+ i.track(w.CONVERSION, c);
78
82
  }
79
83
  };
80
84
  };
81
85
  export {
82
- T as initializeContentAnalytics
86
+ O as initializeContentAnalytics
83
87
  };
@@ -1,11 +1,9 @@
1
- import { getUVEState as m } from "../../../../uve/src/lib/core/core.utils.js";
2
- import "../../../../uve/src/internal/constants.js";
3
- import { getViewportMetrics as d, isElementMeetingVisibilityThreshold as u } from "./dot-analytics.impression.utils.js";
4
- import { DEFAULT_IMPRESSION_CONFIG as l, IMPRESSION_EVENT_TYPE as g } from "../../shared/constants/dot-analytics.constants.js";
5
- import { createPluginLogger as p, isBrowser as a, INITIAL_SCAN_DELAY_MS as f, findContentlets as b, extractContentletIdentifier as c, createContentletObserver as v, extractContentletData as I } from "../../shared/utils/dot-analytics.utils.js";
6
- class E {
1
+ import { getViewportMetrics as m, isElementMeetingVisibilityThreshold as d } from "./dot-analytics.impression.utils.js";
2
+ import { DEFAULT_IMPRESSION_CONFIG as l, IMPRESSION_EVENT_TYPE as u } from "../../shared/constants/dot-analytics.constants.js";
3
+ import { createPluginLogger as g, isBrowser as a, INITIAL_SCAN_DELAY_MS as p, findContentlets as b, extractContentletIdentifier as c, createContentletObserver as f, extractContentletData as v } from "../../shared/utils/dot-analytics.utils.js";
4
+ class T {
7
5
  constructor(e) {
8
- this.observer = null, this.mutationObserver = null, this.elementImpressionStates = /* @__PURE__ */ new Map(), this.sessionTrackedImpressions = /* @__PURE__ */ new Set(), this.currentPagePath = "", this.subscribers = /* @__PURE__ */ new Set(), this.logger = p("Impression", e), this.impressionConfig = this.resolveImpressionConfig(e.impressions);
6
+ this.observer = null, this.mutationObserver = null, this.elementImpressionStates = /* @__PURE__ */ new Map(), this.sessionTrackedImpressions = /* @__PURE__ */ new Set(), this.currentPagePath = "", this.subscribers = /* @__PURE__ */ new Set(), this.logger = g("Impression", e), this.impressionConfig = this.resolveImpressionConfig(e.impressions);
9
7
  }
10
8
  /**
11
9
  * Subscribe to impression events
@@ -38,15 +36,9 @@ class E {
38
36
  }
39
37
  /** Initializes tracking: sets up observers, finds contentlets, handles visibility/navigation */
40
38
  initialize() {
41
- if (a()) {
42
- if (m()) {
43
- this.logger.warn("Impression tracking disabled in editor mode");
44
- return;
45
- }
46
- this.initializeIntersectionObserver(), typeof window < "u" && setTimeout(() => {
47
- this.logger.debug("Running initial scan after timeout..."), this.findAndObserveContentletElements();
48
- }, f), this.initializeDynamicContentDetector(), this.initializePageVisibilityHandler(), this.initializePageNavigationHandler(), this.logger.info("Impression tracking initialized with config:", this.impressionConfig);
49
- }
39
+ a() && (this.initializeIntersectionObserver(), typeof window < "u" && setTimeout(() => {
40
+ this.logger.debug("Running initial scan after timeout..."), this.findAndObserveContentletElements();
41
+ }, p), this.initializeDynamicContentDetector(), this.initializePageVisibilityHandler(), this.initializePageNavigationHandler(), this.logger.info("Impression tracking initialized with config:", this.impressionConfig));
50
42
  }
51
43
  /** Sets up IntersectionObserver with configured visibility threshold */
52
44
  initializeIntersectionObserver() {
@@ -92,7 +84,7 @@ class E {
92
84
  }
93
85
  /** Watches for new contentlets added to DOM (debounced for performance) */
94
86
  initializeDynamicContentDetector() {
95
- a() && (this.mutationObserver = v(() => {
87
+ a() && (this.mutationObserver = f(() => {
96
88
  this.findAndObserveContentletElements();
97
89
  }), this.logger.info("MutationObserver enabled for dynamic content detection"));
98
90
  }
@@ -150,7 +142,7 @@ class E {
150
142
  trackAndSendImpression(e, i) {
151
143
  const t = this.elementImpressionStates.get(e);
152
144
  if (!t) return;
153
- const s = t.visibleSince ? Date.now() - t.visibleSince : 0, n = I(i), r = d(i), o = parseInt(i.dataset.dotDomIndex || "-1", 10), h = {
145
+ const s = t.visibleSince ? Date.now() - t.visibleSince : 0, n = v(i), r = m(i), o = parseInt(i.dataset.dotDomIndex || "-1", 10), h = {
154
146
  content: {
155
147
  identifier: n.identifier,
156
148
  inode: n.inode,
@@ -162,7 +154,7 @@ class E {
162
154
  dom_index: o
163
155
  }
164
156
  };
165
- this.notifySubscribers(g, h), this.markImpressionAsTracked(e), t.timer = null, t.visibleSince = null, t.tracked = !0, this.observer && this.observer.unobserve(i), this.logger.info(
157
+ this.notifySubscribers(u, h), this.markImpressionAsTracked(e), t.timer = null, t.visibleSince = null, t.tracked = !0, this.observer && this.observer.unobserve(i), this.logger.info(
166
158
  `Fired impression for ${e} (dwell: ${s}ms) - element unobserved`,
167
159
  n
168
160
  );
@@ -177,7 +169,7 @@ class E {
177
169
  }
178
170
  /** Post-dwell check: verifies element still meets visibility threshold */
179
171
  isElementStillVisible(e) {
180
- return document.visibilityState !== "visible" ? !1 : u(
172
+ return document.visibilityState !== "visible" ? !1 : d(
181
173
  e,
182
174
  this.impressionConfig.visibilityThreshold
183
175
  );
@@ -198,5 +190,5 @@ class E {
198
190
  }
199
191
  }
200
192
  export {
201
- E as DotCMSImpressionTracker
193
+ T as DotCMSImpressionTracker
202
194
  };
@@ -156,3 +156,22 @@ export declare const ANALYTICS_WINDOWS_CLEANUP_KEY = "__dotAnalyticsCleanup__";
156
156
  * @see core-web/libs/sdk/react/src/lib/next/components/Contentlet/Contentlet.tsx
157
157
  */
158
158
  export declare const CONTENTLET_CLASS = "dotcms-contentlet";
159
+ /**
160
+ * Queue persistence configuration constants
161
+ * Used for storing events in sessionStorage for traditional page navigations
162
+ */
163
+ /**
164
+ * Session storage key for persistent tab ID
165
+ * This ID remains constant across page navigations within the same browser tab
166
+ */
167
+ export declare const TAB_ID_STORAGE_KEY = "dot_analytics_tab_id";
168
+ /**
169
+ * Prefix for queue storage key in sessionStorage
170
+ * Full key format: dot_analytics_queue_{tabId}
171
+ */
172
+ export declare const QUEUE_STORAGE_KEY_PREFIX = "dot_analytics_queue";
173
+ /**
174
+ * Maximum age in milliseconds for persisted events
175
+ * Events older than this will be discarded (24 hours)
176
+ */
177
+ export declare const MAX_EVENT_AGE_MS: number;
@@ -1,20 +1,20 @@
1
- const I = "/api/v1/analytics/content/event", c = {
1
+ const c = "/api/v1/analytics/content/event", o = {
2
2
  PAGEVIEW: "pageview",
3
3
  CONTENT_IMPRESSION: "content_impression",
4
4
  CONTENT_CLICK: "content_click",
5
5
  CONVERSION: "conversion"
6
- }, o = [
6
+ }, s = [
7
7
  "utm_source",
8
8
  "utm_medium",
9
9
  "utm_campaign",
10
10
  "utm_term",
11
11
  "utm_content"
12
- ], s = 30, e = "dot_analytics_session_id", N = "dot_analytics_user_id", _ = 15, E = 5e3, A = ["click"], L = {
12
+ ], I = 30, e = "dot_analytics_session_id", N = "dot_analytics_user_id", _ = 15, E = 5e3, A = ["click"], O = {
13
13
  eventBatchSize: _,
14
14
  // Max events per batch - auto-sends when reached
15
15
  flushInterval: E
16
16
  // Time between flushes - sends whatever is queued
17
- }, O = [
17
+ }, L = [
18
18
  "title",
19
19
  "url",
20
20
  "path",
@@ -28,28 +28,31 @@ const I = "/api/v1/analytics/content/event", c = {
28
28
  dwellMs: n,
29
29
  maxNodes: T,
30
30
  throttleMs: S
31
- }, U = "content_impression", D = "content_click", M = 300, a = "a, button", R = "__dotAnalyticsActive__", l = "__dotAnalyticsCleanup__", r = "dotcms-contentlet";
31
+ }, U = "content_impression", a = "content_click", D = 300, M = "a, button", R = "__dotAnalyticsActive__", l = "__dotAnalyticsCleanup__", F = "dotcms-contentlet", P = "dot_analytics_tab_id", d = "dot_analytics_queue", r = 1440 * 60 * 1e3;
32
32
  export {
33
33
  A as ACTIVITY_EVENTS,
34
- I as ANALYTICS_ENDPOINT,
35
- O as ANALYTICS_JS_DEFAULT_PROPERTIES,
34
+ c as ANALYTICS_ENDPOINT,
35
+ L as ANALYTICS_JS_DEFAULT_PROPERTIES,
36
36
  R as ANALYTICS_WINDOWS_ACTIVE_KEY,
37
37
  l as ANALYTICS_WINDOWS_CLEANUP_KEY,
38
- a as CLICKABLE_ELEMENTS_SELECTOR,
39
- D as CLICK_EVENT_TYPE,
40
- r as CONTENTLET_CLASS,
41
- M as DEFAULT_CLICK_THROTTLE_MS,
38
+ M as CLICKABLE_ELEMENTS_SELECTOR,
39
+ a as CLICK_EVENT_TYPE,
40
+ F as CONTENTLET_CLASS,
41
+ D as DEFAULT_CLICK_THROTTLE_MS,
42
42
  C as DEFAULT_IMPRESSION_CONFIG,
43
43
  n as DEFAULT_IMPRESSION_DWELL_MS,
44
44
  T as DEFAULT_IMPRESSION_MAX_NODES,
45
45
  i as DEFAULT_IMPRESSION_MUTATION_OBSERVER_DEBOUNCE_MS,
46
46
  S as DEFAULT_IMPRESSION_THROTTLE_MS,
47
47
  t as DEFAULT_IMPRESSION_VISIBILITY_THRESHOLD,
48
- L as DEFAULT_QUEUE_CONFIG,
49
- s as DEFAULT_SESSION_TIMEOUT_MINUTES,
50
- c as DotCMSPredefinedEventType,
51
- o as EXPECTED_UTM_KEYS,
48
+ O as DEFAULT_QUEUE_CONFIG,
49
+ I as DEFAULT_SESSION_TIMEOUT_MINUTES,
50
+ o as DotCMSPredefinedEventType,
51
+ s as EXPECTED_UTM_KEYS,
52
52
  U as IMPRESSION_EVENT_TYPE,
53
+ r as MAX_EVENT_AGE_MS,
54
+ d as QUEUE_STORAGE_KEY_PREFIX,
53
55
  e as SESSION_STORAGE_KEY,
56
+ P as TAB_ID_STORAGE_KEY,
54
57
  N as USER_ID_KEY
55
58
  };
@@ -6,4 +6,4 @@ import { DotCMSAnalyticsConfig, DotCMSAnalyticsRequestBody } from '../models';
6
6
  * @param keepalive - Use keepalive mode for page unload scenarios (default: false)
7
7
  * @returns A promise that resolves when the request is complete
8
8
  */
9
- export declare const sendAnalyticsEvent: (payload: DotCMSAnalyticsRequestBody, config: DotCMSAnalyticsConfig, keepalive?: boolean) => Promise<void>;
9
+ export declare const sendAnalyticsEvent: (payload: DotCMSAnalyticsRequestBody, config: DotCMSAnalyticsConfig, keepalive?: boolean) => Promise<boolean>;
@@ -1,34 +1,36 @@
1
- import { ANALYTICS_ENDPOINT as f } from "../constants/dot-analytics.constants.js";
2
- import { createPluginLogger as T } from "../utils/dot-analytics.utils.js";
3
- const h = async (n, a, i = !1) => {
4
- const r = T("HTTP", a), c = `${a.server}${f}`, g = JSON.stringify(n);
5
- r.info(`Sending ${n.events.length} event(s)${i ? " (keepalive)" : ""}`, {
1
+ import { ANALYTICS_ENDPOINT as p } from "../constants/dot-analytics.constants.js";
2
+ import { createPluginLogger as l } from "../utils/dot-analytics.utils.js";
3
+ const m = async (n, i, c = !1) => {
4
+ const e = l("HTTP", i), f = `${i.server}${p}`, g = JSON.stringify(n);
5
+ e.info(`Sending ${n.events.length} event(s)${c ? " (keepalive)" : ""}`, {
6
6
  payload: n
7
7
  });
8
8
  try {
9
- const e = {
9
+ const r = {
10
10
  method: "POST",
11
11
  headers: { "Content-Type": "application/json" },
12
12
  body: g
13
13
  };
14
- if (i) {
15
- e.keepalive = !0, e.credentials = "omit", fetch(c, e);
16
- return;
17
- }
18
- const t = await fetch(c, e);
14
+ if (c)
15
+ return r.keepalive = !0, r.credentials = "omit", fetch(f, r).catch((o) => {
16
+ e.error("Keepalive request failed (browser may have ignored it):", o);
17
+ }), !0;
18
+ const t = await fetch(f, r);
19
19
  if (!t.ok) {
20
- const p = t.statusText || "Unknown Error", o = `HTTP ${t.status}: ${p}`;
20
+ const o = t.statusText || "Unknown Error", a = `HTTP ${t.status}: ${o}`;
21
21
  try {
22
22
  const s = await t.json();
23
- s.message ? r.warn(`${s.message} (${o})`) : r.warn(`${o} - No error message in response`);
23
+ s.message ? e.warn(`${s.message} (${a})`) : e.warn(`${a} - No error message in response`);
24
24
  } catch (s) {
25
- r.warn(`${o} - Failed to parse error response:`, s);
25
+ e.warn(`${a} - Failed to parse error response:`, s);
26
26
  }
27
+ return !1;
27
28
  }
28
- } catch (e) {
29
- r.error("Error sending event:", e);
29
+ return !0;
30
+ } catch (r) {
31
+ return e.error("Error sending event:", r), !1;
30
32
  }
31
33
  };
32
34
  export {
33
- h as sendAnalyticsEvent
35
+ m as sendAnalyticsEvent
34
36
  };
@@ -167,3 +167,15 @@ export type DotCMSCustomEvent = DotCMSEventBase<DotCMSCustomEventType, DotCMSCus
167
167
  * Used primarily for type documentation and validation.
168
168
  */
169
169
  export type DotCMSEvent = DotCMSPageViewEvent | DotCMSContentImpressionEvent | DotCMSContentClickEvent | DotCMSConversionEvent | DotCMSCustomEvent;
170
+ /**
171
+ * Structure for persisted queue in sessionStorage.
172
+ * Used to preserve events across traditional page navigations.
173
+ */
174
+ export interface PersistedQueue {
175
+ /** Unique identifier for this browser tab */
176
+ tabId: string;
177
+ /** Timestamp when the queue was last persisted */
178
+ timestamp: number;
179
+ /** Array of events waiting to be sent */
180
+ events: DotCMSEvent[];
181
+ }
@@ -23,6 +23,10 @@ export declare const createAnalyticsQueue: (config: DotCMSAnalyticsConfig) => {
23
23
  /**
24
24
  * Clean up queue resources
25
25
  * Flushes remaining events and cleans up listeners
26
+ *
27
+ * IMPORTANT: Does NOT clear sessionStorage
28
+ * - Storage is cleared only after sendBatch succeeds or in initialize()
29
+ * - This allows events to persist across traditional page navigations
26
30
  */
27
31
  cleanup: () => void;
28
32
  };
@@ -1,49 +1,125 @@
1
- import m from "@analytics/queue-utils";
2
- import v from "@analytics/router-utils";
3
- import { DEFAULT_QUEUE_CONFIG as y } from "../constants/dot-analytics.constants.js";
4
- import { sendAnalyticsEvent as b } from "../http/dot-analytics.http.js";
5
- import { createPluginLogger as w } from "../utils/dot-analytics.utils.js";
6
- const L = (u) => {
7
- const i = w("Queue", u);
8
- let t = null, n = null, o = !1, d = !1, f = typeof window < "u" ? window.location.pathname : "";
9
- const a = {
10
- ...y,
11
- ...typeof u.queue == "object" ? u.queue : {}
12
- }, g = (e, s) => {
13
- if (!n) return;
14
- i.debug(`Sending batch of ${e.length} event(s)`, {
1
+ import q from "@analytics/queue-utils";
2
+ import F from "@analytics/router-utils";
3
+ 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";
4
+ import { sendAnalyticsEvent as w } from "../http/dot-analytics.http.js";
5
+ import { createPluginLogger as T, safeSessionStorage as d, generateSecureId as U, getAnalyticsContext as D } from "../utils/dot-analytics.utils.js";
6
+ const R = (l) => {
7
+ const n = T("Queue", l);
8
+ let i = null, c = null, f = !1, m = !1, v = typeof window < "u" ? window.location.pathname : "", o = "", a = [];
9
+ const y = {
10
+ ...x,
11
+ ...typeof l.queue == "object" ? l.queue : {}
12
+ }, g = () => `${P}_${o}`, E = (e) => {
13
+ const t = e;
14
+ if (!t || typeof t != "object" || Array.isArray(t))
15
+ return null;
16
+ if (typeof t.tabId != "string" || typeof t.timestamp != "number" || !Number.isFinite(t.timestamp) || !Array.isArray(t.events))
17
+ return n.warn("Invalid persisted queue: structural mismatch"), null;
18
+ const s = t.events.filter(
19
+ (r) => r && typeof r == "object" && "event_type" in r
20
+ );
21
+ return {
22
+ tabId: t.tabId,
23
+ timestamp: t.timestamp,
24
+ events: s
25
+ };
26
+ }, A = () => {
27
+ try {
28
+ const e = g(), t = d.getItem(e);
29
+ if (!t)
30
+ return n.debug("No persisted queue found"), null;
31
+ const s = JSON.parse(t), r = E(s);
32
+ if (!r)
33
+ return d.removeItem(e), null;
34
+ const u = Date.now() - r.timestamp;
35
+ return u > z ? (n.warn(
36
+ `Persisted events too old (${Math.round(u / 1e3 / 60 / 60)}h), discarding`
37
+ ), d.removeItem(e), null) : (n.info(
38
+ `Loaded ${r.events.length} persisted event(s) from storage (age: ${Math.round(u / 1e3)}s)`
39
+ ), r);
40
+ } catch (e) {
41
+ return n.error("Failed to load persisted queue", e), d.removeItem(g()), null;
42
+ }
43
+ }, h = () => {
44
+ if (a.length !== 0)
45
+ try {
46
+ const e = g(), t = {
47
+ tabId: o,
48
+ timestamp: Date.now(),
49
+ events: a
50
+ };
51
+ d.setItem(e, JSON.stringify(t)), n.debug(`Persisted ${a.length} event(s) to storage`);
52
+ } catch (e) {
53
+ e instanceof Error && e.name === "QuotaExceededError" ? n.warn("sessionStorage quota exceeded, continuing without persistence") : n.error("Failed to persist queue", e);
54
+ }
55
+ }, b = () => {
56
+ try {
57
+ const e = g();
58
+ d.removeItem(e), n.debug("Persisted queue cleared from storage");
59
+ } catch (e) {
60
+ n.error("Failed to clear persisted queue", e);
61
+ }
62
+ }, _ = async (e, t = !0) => {
63
+ if (e.length === 0)
64
+ return !0;
65
+ n.info(`Sending ${e.length} persisted event(s) immediately`);
66
+ const r = { context: D(l), events: e };
67
+ return w(r, l, t);
68
+ }, $ = (e, t) => {
69
+ if (!c) return;
70
+ n.debug(`Sending batch of ${e.length} event(s)`, {
15
71
  events: e,
16
- keepalive: o
17
- }), b({ context: n, events: e }, u, o);
18
- }, l = () => {
19
- !t || t.size() === 0 || !n || (i.info(`Flushing ${t.size()} events (page hidden/unload)`), o = !0, t.flush(!0));
20
- }, c = () => {
21
- if (i.debug("handleVisibilityChange", document.visibilityState), document.visibilityState === "hidden") {
22
- if (d) {
23
- i.debug("Skipping flush (SPA navigation detected)");
72
+ keepalive: f
73
+ }), w({ context: c, events: e }, l, f), a = a.filter(
74
+ (r) => !e.some((u) => u === r)
75
+ ), f || (a.length === 0 ? b() : h());
76
+ }, p = () => {
77
+ !i || i.size() === 0 || !c || (n.info(`Flushing ${i.size()} events (page hidden/unload)`), f = !0, i.flush(!0));
78
+ }, S = () => {
79
+ if (n.debug("handleVisibilityChange", document.visibilityState), document.visibilityState === "hidden") {
80
+ if (m) {
81
+ n.debug("Skipping flush (SPA navigation detected), persisting to storage"), h();
24
82
  return;
25
83
  }
26
- l();
27
- } else document.visibilityState === "visible" && (d = !1);
84
+ p();
85
+ } else document.visibilityState === "visible" && (m = !1);
28
86
  };
29
87
  return {
30
88
  /**
31
89
  * Initialize the queue with smart batching
32
90
  */
33
91
  initialize: () => {
34
- t = m(
35
- (e, s) => {
36
- g(e);
92
+ if (typeof window < "u") {
93
+ const t = d.getItem(I);
94
+ if (t)
95
+ o = t, n.debug(`Reusing Tab ID: ${o}`);
96
+ else {
97
+ typeof crypto < "u" && typeof crypto.randomUUID == "function" ? o = crypto.randomUUID() : (o = U("tab"), n.debug("crypto.randomUUID not available, using fallback generator"));
98
+ try {
99
+ d.setItem(I, o);
100
+ } catch {
101
+ }
102
+ n.debug(`Generated new Tab ID: ${o}`);
103
+ }
104
+ }
105
+ const e = A();
106
+ e && e.events.length > 0 && _(e.events, !1).then((t) => {
107
+ t ? b() : n.warn(
108
+ "Failed to send persisted events, keeping in storage for next retry"
109
+ );
110
+ }), i = q(
111
+ (t, s) => {
112
+ $(t);
37
113
  },
38
114
  {
39
- max: a.eventBatchSize,
40
- interval: a.flushInterval,
115
+ max: y.eventBatchSize,
116
+ interval: y.flushInterval,
41
117
  throttle: !1
42
118
  // Always false - enables both batch size and interval triggers
43
119
  }
44
- ), typeof window < "u" && typeof document < "u" && (document.addEventListener("visibilitychange", c), window.addEventListener("pagehide", l), v((e) => {
45
- d = !0, f = e, i.debug(`SPA navigation detected (${f})`), setTimeout(() => {
46
- d = !1;
120
+ ), typeof window < "u" && typeof document < "u" && (document.addEventListener("visibilitychange", S), window.addEventListener("pagehide", p), F((t) => {
121
+ m = !0, v = t, n.debug(`SPA navigation detected (${v})`), setTimeout(() => {
122
+ m = !1;
47
123
  }, 100);
48
124
  }));
49
125
  },
@@ -53,28 +129,33 @@ const L = (u) => {
53
129
  * - Sends immediately when eventBatchSize reached (with throttle: false)
54
130
  * - Sends pending events every flushInterval
55
131
  */
56
- enqueue: (e, s) => {
57
- if (n = s, !t) return;
58
- const r = t.size() + 1, p = a.eventBatchSize, h = r >= p;
59
- i.debug(
60
- `Event added. Queue size: ${r}/${p}${h ? " (full, sending...)" : ""}`,
132
+ enqueue: (e, t) => {
133
+ if (c = t, !i) return;
134
+ a.push(e);
135
+ const s = i.size() + 1, r = y.eventBatchSize, u = s >= r;
136
+ n.debug(
137
+ `Event added. Queue size: ${s}/${r}${u ? " (full, sending...)" : ""}`,
61
138
  { eventType: e.event_type, event: e }
62
- ), t.push(e);
139
+ ), i.push(e), h();
63
140
  },
64
141
  /**
65
142
  * Get queue size for debugging
66
143
  * Returns the number of events in smartQueue
67
144
  */
68
- size: () => t?.size() ?? 0,
145
+ size: () => i?.size() ?? 0,
69
146
  /**
70
147
  * Clean up queue resources
71
148
  * Flushes remaining events and cleans up listeners
149
+ *
150
+ * IMPORTANT: Does NOT clear sessionStorage
151
+ * - Storage is cleared only after sendBatch succeeds or in initialize()
152
+ * - This allows events to persist across traditional page navigations
72
153
  */
73
154
  cleanup: () => {
74
- l(), typeof window < "u" && typeof document < "u" && (document.removeEventListener("visibilitychange", c), window.removeEventListener("pagehide", l)), t = null, n = null, o = !1;
155
+ p(), typeof window < "u" && typeof document < "u" && (document.removeEventListener("visibilitychange", S), window.removeEventListener("pagehide", p)), i = null, c = null, f = !1, a = [];
75
156
  }
76
157
  };
77
158
  };
78
159
  export {
79
- L as createAnalyticsQueue
160
+ R as createAnalyticsQueue
80
161
  };
@@ -50,6 +50,7 @@ export declare const generateSecureId: (prefix: string) => string;
50
50
  export declare const safeSessionStorage: {
51
51
  getItem: (key: string) => string | null;
52
52
  setItem: (key: string, value: string) => void;
53
+ removeItem: (key: string) => void;
53
54
  };
54
55
  /**
55
56
  * Gets or generates a user ID from localStorage.