@grainql/analytics-web 2.5.4 → 2.6.0

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.
Files changed (79) hide show
  1. package/README.md +3 -1
  2. package/dist/activity.js +1 -1
  3. package/dist/cjs/activity.js +1 -1
  4. package/dist/cjs/activity.js.map +1 -1
  5. package/dist/cjs/consent.js +4 -4
  6. package/dist/cjs/consent.js.map +1 -1
  7. package/dist/cjs/heartbeat.d.ts.map +1 -1
  8. package/dist/cjs/heartbeat.js +0 -6
  9. package/dist/cjs/heartbeat.js.map +1 -1
  10. package/dist/cjs/heatmap-tracking.d.ts +90 -0
  11. package/dist/cjs/heatmap-tracking.d.ts.map +1 -0
  12. package/dist/cjs/heatmap-tracking.js +465 -0
  13. package/dist/cjs/heatmap-tracking.js.map +1 -0
  14. package/dist/cjs/index.d.ts +6 -0
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  18. package/dist/cjs/interaction-tracking.js +9 -18
  19. package/dist/cjs/interaction-tracking.js.map +1 -1
  20. package/dist/cjs/page-tracking.d.ts.map +1 -1
  21. package/dist/cjs/page-tracking.js +0 -9
  22. package/dist/cjs/page-tracking.js.map +1 -1
  23. package/dist/cjs/section-tracking.d.ts.map +1 -1
  24. package/dist/cjs/section-tracking.js +1 -7
  25. package/dist/cjs/section-tracking.js.map +1 -1
  26. package/dist/cjs/types/heatmap-tracking.d.ts +41 -0
  27. package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -0
  28. package/dist/cjs/types/heatmap-tracking.js +6 -0
  29. package/dist/cjs/types/heatmap-tracking.js.map +1 -0
  30. package/dist/consent.js +4 -4
  31. package/dist/esm/activity.js +1 -1
  32. package/dist/esm/activity.js.map +1 -1
  33. package/dist/esm/consent.js +4 -4
  34. package/dist/esm/consent.js.map +1 -1
  35. package/dist/esm/heartbeat.d.ts.map +1 -1
  36. package/dist/esm/heartbeat.js +0 -6
  37. package/dist/esm/heartbeat.js.map +1 -1
  38. package/dist/esm/heatmap-tracking.d.ts +90 -0
  39. package/dist/esm/heatmap-tracking.d.ts.map +1 -0
  40. package/dist/esm/heatmap-tracking.js +461 -0
  41. package/dist/esm/heatmap-tracking.js.map +1 -0
  42. package/dist/esm/index.d.ts +6 -0
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  46. package/dist/esm/interaction-tracking.js +9 -18
  47. package/dist/esm/interaction-tracking.js.map +1 -1
  48. package/dist/esm/page-tracking.d.ts.map +1 -1
  49. package/dist/esm/page-tracking.js +0 -9
  50. package/dist/esm/page-tracking.js.map +1 -1
  51. package/dist/esm/section-tracking.d.ts.map +1 -1
  52. package/dist/esm/section-tracking.js +1 -7
  53. package/dist/esm/section-tracking.js.map +1 -1
  54. package/dist/esm/types/heatmap-tracking.d.ts +41 -0
  55. package/dist/esm/types/heatmap-tracking.d.ts.map +1 -0
  56. package/dist/esm/types/heatmap-tracking.js +5 -0
  57. package/dist/esm/types/heatmap-tracking.js.map +1 -0
  58. package/dist/heartbeat.d.ts.map +1 -1
  59. package/dist/heartbeat.js +0 -6
  60. package/dist/heatmap-tracking.d.ts +90 -0
  61. package/dist/heatmap-tracking.d.ts.map +1 -0
  62. package/dist/heatmap-tracking.js +465 -0
  63. package/dist/index.d.ts +6 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.global.dev.js +503 -79
  66. package/dist/index.global.dev.js.map +4 -4
  67. package/dist/index.global.js +2 -2
  68. package/dist/index.global.js.map +4 -4
  69. package/dist/index.js +61 -38
  70. package/dist/index.mjs +61 -38
  71. package/dist/interaction-tracking.d.ts.map +1 -1
  72. package/dist/interaction-tracking.js +9 -18
  73. package/dist/page-tracking.d.ts.map +1 -1
  74. package/dist/page-tracking.js +0 -9
  75. package/dist/section-tracking.d.ts.map +1 -1
  76. package/dist/section-tracking.js +1 -7
  77. package/dist/types/heatmap-tracking.d.ts +41 -0
  78. package/dist/types/heatmap-tracking.d.ts.map +1 -0
  79. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v2.5.4 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v2.6.0 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -22,6 +22,438 @@ var Grain = (() => {
22
22
  };
23
23
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
24
 
25
+ // src/heatmap-tracking.ts
26
+ var heatmap_tracking_exports = {};
27
+ __export(heatmap_tracking_exports, {
28
+ HeatmapTrackingManager: () => HeatmapTrackingManager
29
+ });
30
+ var DEFAULT_OPTIONS, HeatmapTrackingManager;
31
+ var init_heatmap_tracking = __esm({
32
+ "src/heatmap-tracking.ts"() {
33
+ "use strict";
34
+ DEFAULT_OPTIONS = {
35
+ scrollDebounceDelay: 100,
36
+ batchDelay: 2e3,
37
+ maxBatchSize: 20,
38
+ debug: false
39
+ };
40
+ HeatmapTrackingManager = class {
41
+ // 3 seconds - same as section tracking
42
+ constructor(tracker, options = {}) {
43
+ this.isDestroyed = false;
44
+ // Tracking state
45
+ this.currentScrollState = null;
46
+ this.pendingClicks = [];
47
+ this.pendingScrolls = [];
48
+ // Timers
49
+ this.scrollDebounceTimer = null;
50
+ this.batchTimer = null;
51
+ this.scrollTrackingTimer = null;
52
+ this.periodicScrollTimer = null;
53
+ // Scroll tracking
54
+ this.lastScrollPosition = 0;
55
+ this.lastScrollTime = Date.now();
56
+ this.SPLIT_DURATION = 3e3;
57
+ this.tracker = tracker;
58
+ this.options = { ...DEFAULT_OPTIONS, ...options };
59
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
60
+ if (document.readyState === "loading") {
61
+ document.addEventListener("DOMContentLoaded", () => this.initialize());
62
+ } else {
63
+ setTimeout(() => this.initialize(), 0);
64
+ }
65
+ }
66
+ }
67
+ /**
68
+ * Initialize heatmap tracking
69
+ */
70
+ initialize() {
71
+ if (this.isDestroyed)
72
+ return;
73
+ this.log("Initializing heatmap tracking");
74
+ this.setupClickTracking();
75
+ this.setupScrollTracking();
76
+ this.startScrollTracking();
77
+ this.setupUnloadHandler();
78
+ }
79
+ /**
80
+ * Setup click event tracking
81
+ */
82
+ setupClickTracking() {
83
+ if (typeof document === "undefined")
84
+ return;
85
+ const clickHandler = (event) => {
86
+ if (this.isDestroyed)
87
+ return;
88
+ if (!this.tracker.hasConsent("analytics"))
89
+ return;
90
+ this.handleClick(event);
91
+ };
92
+ document.addEventListener("click", clickHandler, { passive: true, capture: true });
93
+ }
94
+ /**
95
+ * Setup scroll event tracking
96
+ */
97
+ setupScrollTracking() {
98
+ if (typeof window === "undefined")
99
+ return;
100
+ const scrollHandler = () => {
101
+ if (this.scrollDebounceTimer !== null) {
102
+ clearTimeout(this.scrollDebounceTimer);
103
+ }
104
+ this.scrollDebounceTimer = window.setTimeout(() => {
105
+ this.handleScroll();
106
+ this.scrollDebounceTimer = null;
107
+ }, this.options.scrollDebounceDelay);
108
+ };
109
+ window.addEventListener("scroll", scrollHandler, { passive: true });
110
+ }
111
+ /**
112
+ * Start periodic scroll state tracking
113
+ */
114
+ startScrollTracking() {
115
+ if (typeof window === "undefined")
116
+ return;
117
+ this.updateScrollState();
118
+ this.scrollTrackingTimer = window.setInterval(() => {
119
+ if (this.isDestroyed)
120
+ return;
121
+ this.updateScrollState();
122
+ }, 500);
123
+ this.startPeriodicScrollTracking();
124
+ }
125
+ /**
126
+ * Start periodic scroll tracking (sends events every 3 seconds)
127
+ */
128
+ startPeriodicScrollTracking() {
129
+ if (typeof window === "undefined")
130
+ return;
131
+ this.periodicScrollTimer = window.setInterval(() => {
132
+ if (this.isDestroyed || !this.currentScrollState)
133
+ return;
134
+ if (!this.tracker.hasConsent("analytics"))
135
+ return;
136
+ const currentTime = Date.now();
137
+ const duration = currentTime - this.currentScrollState.entryTime;
138
+ if (duration > 1e3) {
139
+ const scrollY = window.scrollY || window.pageYOffset;
140
+ const viewportHeight = window.innerHeight;
141
+ const pageHeight = document.documentElement.scrollHeight;
142
+ const scrollData = {
143
+ pageUrl: window.location.href,
144
+ viewportSection: this.currentScrollState.viewportSection,
145
+ scrollDepthPx: scrollY,
146
+ durationMs: duration,
147
+ entryTimestamp: this.currentScrollState.entryTime,
148
+ exitTimestamp: currentTime,
149
+ pageHeight,
150
+ viewportHeight
151
+ };
152
+ this.tracker.trackSystemEvent("_grain_heatmap_scroll", {
153
+ page_url: scrollData.pageUrl,
154
+ viewport_section: scrollData.viewportSection,
155
+ scroll_depth_px: scrollData.scrollDepthPx,
156
+ duration_ms: scrollData.durationMs,
157
+ entry_timestamp: scrollData.entryTimestamp,
158
+ exit_timestamp: scrollData.exitTimestamp,
159
+ page_height: scrollData.pageHeight,
160
+ viewport_height: scrollData.viewportHeight,
161
+ is_split: true
162
+ // Flag to indicate periodic tracking, not final exit
163
+ }, { flush: true });
164
+ this.currentScrollState.entryTime = currentTime;
165
+ }
166
+ }, this.SPLIT_DURATION);
167
+ }
168
+ /**
169
+ * Setup page unload handler to beacon remaining data
170
+ */
171
+ setupUnloadHandler() {
172
+ if (typeof window === "undefined")
173
+ return;
174
+ const unloadHandler = () => {
175
+ if (this.currentScrollState) {
176
+ const currentTime = Date.now();
177
+ const duration = currentTime - this.currentScrollState.entryTime;
178
+ if (duration > 100) {
179
+ const scrollData = {
180
+ pageUrl: window.location.href,
181
+ viewportSection: this.currentScrollState.viewportSection,
182
+ scrollDepthPx: this.currentScrollState.scrollDepthPx,
183
+ durationMs: duration,
184
+ entryTimestamp: this.currentScrollState.entryTime,
185
+ exitTimestamp: currentTime,
186
+ pageHeight: document.documentElement.scrollHeight,
187
+ viewportHeight: window.innerHeight
188
+ };
189
+ this.pendingScrolls.push(scrollData);
190
+ }
191
+ }
192
+ this.flushPendingEventsWithBeacon();
193
+ };
194
+ window.addEventListener("beforeunload", unloadHandler);
195
+ window.addEventListener("pagehide", unloadHandler);
196
+ }
197
+ /**
198
+ * Handle click event
199
+ */
200
+ handleClick(event) {
201
+ if (!this.tracker.hasConsent("analytics"))
202
+ return;
203
+ const element = event.target;
204
+ if (!element)
205
+ return;
206
+ const pageUrl = window.location.href;
207
+ const xpath = this.generateXPath(element);
208
+ const viewportX = Math.round(event.clientX);
209
+ const viewportY = Math.round(event.clientY);
210
+ const pageX = Math.round(event.pageX);
211
+ const pageY = Math.round(event.pageY);
212
+ const elementTag = element.tagName?.toLowerCase() || "unknown";
213
+ const elementText = element.textContent?.trim().substring(0, 100);
214
+ const clickData = {
215
+ pageUrl,
216
+ xpath,
217
+ viewportX,
218
+ viewportY,
219
+ pageX,
220
+ pageY,
221
+ elementTag,
222
+ elementText: elementText || void 0,
223
+ timestamp: Date.now()
224
+ };
225
+ const isNavigationLink = element instanceof HTMLAnchorElement && element.href;
226
+ if (isNavigationLink) {
227
+ this.tracker.trackSystemEvent("_grain_heatmap_click", {
228
+ page_url: clickData.pageUrl,
229
+ xpath: clickData.xpath,
230
+ viewport_x: clickData.viewportX,
231
+ viewport_y: clickData.viewportY,
232
+ page_x: clickData.pageX,
233
+ page_y: clickData.pageY,
234
+ element_tag: clickData.elementTag,
235
+ element_text: clickData.elementText,
236
+ timestamp: clickData.timestamp
237
+ }, { flush: true });
238
+ } else {
239
+ this.pendingClicks.push(clickData);
240
+ this.considerBatchFlush();
241
+ }
242
+ }
243
+ /**
244
+ * Handle scroll event
245
+ */
246
+ handleScroll() {
247
+ if (!this.tracker.hasConsent("analytics"))
248
+ return;
249
+ this.updateScrollState();
250
+ }
251
+ /**
252
+ * Update current scroll state
253
+ */
254
+ updateScrollState() {
255
+ if (typeof window === "undefined")
256
+ return;
257
+ if (!this.tracker.hasConsent("analytics"))
258
+ return;
259
+ const currentTime = Date.now();
260
+ const scrollY = window.scrollY || window.pageYOffset;
261
+ const viewportHeight = window.innerHeight;
262
+ const pageHeight = document.documentElement.scrollHeight;
263
+ const viewportSection = Math.floor(scrollY / viewportHeight);
264
+ if (this.currentScrollState && this.currentScrollState.viewportSection !== viewportSection) {
265
+ const duration = currentTime - this.currentScrollState.entryTime;
266
+ if (duration > 100) {
267
+ const scrollData = {
268
+ pageUrl: window.location.href,
269
+ viewportSection: this.currentScrollState.viewportSection,
270
+ scrollDepthPx: this.currentScrollState.scrollDepthPx,
271
+ durationMs: duration,
272
+ entryTimestamp: this.currentScrollState.entryTime,
273
+ exitTimestamp: currentTime,
274
+ pageHeight,
275
+ viewportHeight
276
+ };
277
+ this.pendingScrolls.push(scrollData);
278
+ }
279
+ }
280
+ if (!this.currentScrollState || this.currentScrollState.viewportSection !== viewportSection) {
281
+ this.currentScrollState = {
282
+ viewportSection,
283
+ entryTime: currentTime,
284
+ scrollDepthPx: scrollY
285
+ };
286
+ }
287
+ this.lastScrollPosition = scrollY;
288
+ this.lastScrollTime = currentTime;
289
+ this.considerBatchFlush();
290
+ }
291
+ /**
292
+ * Generate XPath for an element
293
+ */
294
+ generateXPath(element) {
295
+ if (!element)
296
+ return "";
297
+ if (element.id) {
298
+ return `//*[@id="${element.id}"]`;
299
+ }
300
+ const paths = [];
301
+ let currentElement = element;
302
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
303
+ let index = 0;
304
+ let sibling = currentElement;
305
+ while (sibling) {
306
+ sibling = sibling.previousElementSibling;
307
+ if (sibling && sibling.nodeName === currentElement.nodeName) {
308
+ index++;
309
+ }
310
+ }
311
+ const tagName = currentElement.nodeName.toLowerCase();
312
+ const pathIndex = index > 0 ? `[${index + 1}]` : "";
313
+ paths.unshift(`${tagName}${pathIndex}`);
314
+ currentElement = currentElement.parentElement;
315
+ }
316
+ return paths.length ? `/${paths.join("/")}` : "";
317
+ }
318
+ /**
319
+ * Consider flushing batched events
320
+ */
321
+ considerBatchFlush() {
322
+ const totalEvents = this.pendingClicks.length + this.pendingScrolls.length;
323
+ if (totalEvents >= this.options.maxBatchSize) {
324
+ this.flushPendingEvents();
325
+ return;
326
+ }
327
+ if (this.batchTimer === null && totalEvents > 0) {
328
+ this.batchTimer = window.setTimeout(() => {
329
+ this.flushPendingEvents();
330
+ this.batchTimer = null;
331
+ }, this.options.batchDelay);
332
+ }
333
+ }
334
+ /**
335
+ * Flush pending events
336
+ */
337
+ flushPendingEvents() {
338
+ if (this.isDestroyed)
339
+ return;
340
+ if (!this.tracker.hasConsent("analytics")) {
341
+ this.pendingClicks = [];
342
+ this.pendingScrolls = [];
343
+ return;
344
+ }
345
+ if (this.pendingClicks.length > 0) {
346
+ for (const clickData of this.pendingClicks) {
347
+ this.tracker.trackSystemEvent("_grain_heatmap_click", {
348
+ page_url: clickData.pageUrl,
349
+ xpath: clickData.xpath,
350
+ viewport_x: clickData.viewportX,
351
+ viewport_y: clickData.viewportY,
352
+ page_x: clickData.pageX,
353
+ page_y: clickData.pageY,
354
+ element_tag: clickData.elementTag,
355
+ element_text: clickData.elementText,
356
+ timestamp: clickData.timestamp
357
+ });
358
+ }
359
+ this.pendingClicks = [];
360
+ }
361
+ if (this.pendingScrolls.length > 0) {
362
+ for (const scrollData of this.pendingScrolls) {
363
+ this.tracker.trackSystemEvent("_grain_heatmap_scroll", {
364
+ page_url: scrollData.pageUrl,
365
+ viewport_section: scrollData.viewportSection,
366
+ scroll_depth_px: scrollData.scrollDepthPx,
367
+ duration_ms: scrollData.durationMs,
368
+ entry_timestamp: scrollData.entryTimestamp,
369
+ exit_timestamp: scrollData.exitTimestamp,
370
+ page_height: scrollData.pageHeight,
371
+ viewport_height: scrollData.viewportHeight
372
+ });
373
+ }
374
+ this.pendingScrolls = [];
375
+ }
376
+ if (this.batchTimer !== null) {
377
+ clearTimeout(this.batchTimer);
378
+ this.batchTimer = null;
379
+ }
380
+ }
381
+ /**
382
+ * Flush pending events with beacon (for page unload)
383
+ */
384
+ flushPendingEventsWithBeacon() {
385
+ if (!this.tracker.hasConsent("analytics")) {
386
+ this.pendingClicks = [];
387
+ this.pendingScrolls = [];
388
+ return;
389
+ }
390
+ if (this.pendingClicks.length > 0) {
391
+ for (const clickData of this.pendingClicks) {
392
+ this.tracker.trackSystemEvent("_grain_heatmap_click", {
393
+ page_url: clickData.pageUrl,
394
+ xpath: clickData.xpath,
395
+ viewport_x: clickData.viewportX,
396
+ viewport_y: clickData.viewportY,
397
+ page_x: clickData.pageX,
398
+ page_y: clickData.pageY,
399
+ element_tag: clickData.elementTag,
400
+ element_text: clickData.elementText,
401
+ timestamp: clickData.timestamp
402
+ }, { flush: true });
403
+ }
404
+ this.pendingClicks = [];
405
+ }
406
+ if (this.pendingScrolls.length > 0) {
407
+ for (const scrollData of this.pendingScrolls) {
408
+ this.tracker.trackSystemEvent("_grain_heatmap_scroll", {
409
+ page_url: scrollData.pageUrl,
410
+ viewport_section: scrollData.viewportSection,
411
+ scroll_depth_px: scrollData.scrollDepthPx,
412
+ duration_ms: scrollData.durationMs,
413
+ entry_timestamp: scrollData.entryTimestamp,
414
+ exit_timestamp: scrollData.exitTimestamp,
415
+ page_height: scrollData.pageHeight,
416
+ viewport_height: scrollData.viewportHeight
417
+ }, { flush: true });
418
+ }
419
+ this.pendingScrolls = [];
420
+ }
421
+ }
422
+ /**
423
+ * Log debug message
424
+ */
425
+ log(...args) {
426
+ if (this.options.debug) {
427
+ this.tracker.log("[Heatmap Tracking]", ...args);
428
+ }
429
+ }
430
+ /**
431
+ * Destroy the tracking manager
432
+ */
433
+ destroy() {
434
+ this.isDestroyed = true;
435
+ if (this.scrollDebounceTimer !== null) {
436
+ clearTimeout(this.scrollDebounceTimer);
437
+ this.scrollDebounceTimer = null;
438
+ }
439
+ if (this.batchTimer !== null) {
440
+ clearTimeout(this.batchTimer);
441
+ this.batchTimer = null;
442
+ }
443
+ if (this.scrollTrackingTimer !== null) {
444
+ clearInterval(this.scrollTrackingTimer);
445
+ this.scrollTrackingTimer = null;
446
+ }
447
+ if (this.periodicScrollTimer !== null) {
448
+ clearInterval(this.periodicScrollTimer);
449
+ this.periodicScrollTimer = null;
450
+ }
451
+ this.flushPendingEvents();
452
+ }
453
+ };
454
+ }
455
+ });
456
+
25
457
  // src/interaction-tracking.ts
26
458
  var interaction_tracking_exports = {};
27
459
  __export(interaction_tracking_exports, {
@@ -62,7 +494,7 @@ var Grain = (() => {
62
494
  attachAllListeners() {
63
495
  if (this.isDestroyed)
64
496
  return;
65
- this.log("Attaching interaction listeners for", this.interactions.length, "interactions");
497
+ this.log("Attaching interaction listeners");
66
498
  for (const interaction of this.interactions) {
67
499
  this.attachInteractionListener(interaction);
68
500
  }
@@ -92,7 +524,6 @@ var Grain = (() => {
92
524
  handlers.push({ event: "focus", handler: focusHandler });
93
525
  }
94
526
  this.attachedListeners.set(element, handlers);
95
- this.log("Attached listeners to element for:", interaction.eventName);
96
527
  }
97
528
  /**
98
529
  * Handle click event on interaction
@@ -116,17 +547,12 @@ var Grain = (() => {
116
547
  ...isNavigationLink && { href: element.href },
117
548
  timestamp: Date.now()
118
549
  };
119
- if (isNavigationLink) {
120
- const result = this.tracker.track(interaction.eventName, eventProperties, { flush: true });
121
- if (result instanceof Promise) {
122
- result.catch((error) => {
123
- this.log("Failed to track navigation click:", error);
124
- });
125
- }
126
- } else {
127
- this.tracker.track(interaction.eventName, eventProperties);
550
+ const result = this.tracker.track(interaction.eventName, eventProperties, { flush: true });
551
+ if (result instanceof Promise) {
552
+ result.catch((error) => {
553
+ this.log("Failed to track click:", error);
554
+ });
128
555
  }
129
- this.log("Tracked click interaction:", interaction.eventName);
130
556
  }
131
557
  /**
132
558
  * Handle focus event on interaction (for form fields)
@@ -147,7 +573,6 @@ var Grain = (() => {
147
573
  element_class: element.className || void 0,
148
574
  timestamp: Date.now()
149
575
  });
150
- this.log("Tracked focus interaction:", interaction.eventName);
151
576
  }
152
577
  /**
153
578
  * Find element by XPath selector
@@ -241,7 +666,6 @@ var Grain = (() => {
241
666
  element.removeEventListener(event, handler);
242
667
  });
243
668
  this.attachedListeners.delete(element);
244
- this.log("Detached listeners from element");
245
669
  }
246
670
  /**
247
671
  * Log debug messages
@@ -296,11 +720,11 @@ var Grain = (() => {
296
720
  __export(section_tracking_exports, {
297
721
  SectionTrackingManager: () => SectionTrackingManager
298
722
  });
299
- var DEFAULT_OPTIONS, SectionTrackingManager;
723
+ var DEFAULT_OPTIONS2, SectionTrackingManager;
300
724
  var init_section_tracking = __esm({
301
725
  "src/section-tracking.ts"() {
302
726
  "use strict";
303
- DEFAULT_OPTIONS = {
727
+ DEFAULT_OPTIONS2 = {
304
728
  minDwellTime: 1e3,
305
729
  // 1 second minimum
306
730
  scrollVelocityThreshold: 500,
@@ -334,7 +758,7 @@ var Grain = (() => {
334
758
  this.SPLIT_DURATION = 3e3;
335
759
  this.tracker = tracker;
336
760
  this.sections = sections;
337
- this.options = { ...DEFAULT_OPTIONS, ...options };
761
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
338
762
  if (typeof window !== "undefined" && typeof document !== "undefined") {
339
763
  if (document.readyState === "loading") {
340
764
  document.addEventListener("DOMContentLoaded", () => this.initialize());
@@ -349,7 +773,7 @@ var Grain = (() => {
349
773
  initialize() {
350
774
  if (this.isDestroyed)
351
775
  return;
352
- this.log("Initializing section tracking for", this.sections.length, "sections");
776
+ this.log("Initializing section tracking");
353
777
  this.setupIntersectionObserver();
354
778
  this.setupScrollListener();
355
779
  this.initializeSections();
@@ -419,7 +843,6 @@ var Grain = (() => {
419
843
  if (this.intersectionObserver) {
420
844
  this.intersectionObserver.observe(element);
421
845
  }
422
- this.log("Section initialized and observed:", section.sectionName);
423
846
  }
424
847
  }
425
848
  /**
@@ -449,7 +872,6 @@ var Grain = (() => {
449
872
  * Handle section entry (became visible)
450
873
  */
451
874
  handleSectionEntry(state) {
452
- this.log("Section entered view:", state.config.sectionName);
453
875
  state.entryTime = Date.now();
454
876
  state.entryScrollSpeed = this.scrollVelocity;
455
877
  state.lastScrollPosition = window.scrollY;
@@ -500,7 +922,6 @@ var Grain = (() => {
500
922
  is_split: true
501
923
  // Flag to indicate this is a periodic split, not final exit
502
924
  });
503
- this.log("Tracked periodic section view split:", state.config.sectionName, "duration:", duration);
504
925
  state.entryTime = now;
505
926
  state.entryScrollSpeed = this.scrollVelocity;
506
927
  }
@@ -522,7 +943,6 @@ var Grain = (() => {
522
943
  * Handle section exit (became invisible)
523
944
  */
524
945
  handleSectionExit(state) {
525
- this.log("Section exited view:", state.config.sectionName);
526
946
  this.stopPeriodicTracking(state.config.sectionName);
527
947
  if (state.entryTime === null)
528
948
  return;
@@ -604,7 +1024,6 @@ var Grain = (() => {
604
1024
  */
605
1025
  queueSectionView(viewData) {
606
1026
  this.pendingEvents.push(viewData);
607
- this.log("Queued section view:", viewData.sectionName, "duration:", viewData.duration);
608
1027
  if (this.batchTimer === null) {
609
1028
  this.batchTimer = window.setTimeout(() => {
610
1029
  this.flushPendingEvents();
@@ -621,7 +1040,6 @@ var Grain = (() => {
621
1040
  this.pendingEvents = [];
622
1041
  return;
623
1042
  }
624
- this.log("Flushing", this.pendingEvents.length, "section view events");
625
1043
  for (const viewData of this.pendingEvents) {
626
1044
  this.tracker.trackSystemEvent("_grain_section_view", {
627
1045
  section_name: viewData.sectionName,
@@ -782,7 +1200,6 @@ var Grain = (() => {
782
1200
  this.saveConsentState();
783
1201
  }
784
1202
  } catch (error) {
785
- console.error("[Grain Consent] Failed to load consent state:", error);
786
1203
  }
787
1204
  }
788
1205
  /**
@@ -794,7 +1211,6 @@ var Grain = (() => {
794
1211
  try {
795
1212
  localStorage.setItem(this.storageKey, JSON.stringify(this.consentState));
796
1213
  } catch (error) {
797
- console.error("[Grain Consent] Failed to save consent state:", error);
798
1214
  }
799
1215
  }
800
1216
  /**
@@ -895,7 +1311,6 @@ var Grain = (() => {
895
1311
  try {
896
1312
  listener(this.consentState);
897
1313
  } catch (error) {
898
- console.error("[Grain Consent] Listener error:", error);
899
1314
  }
900
1315
  });
901
1316
  }
@@ -909,7 +1324,6 @@ var Grain = (() => {
909
1324
  localStorage.removeItem(this.storageKey);
910
1325
  this.consentState = null;
911
1326
  } catch (error) {
912
- console.error("[Grain Consent] Failed to clear consent:", error);
913
1327
  }
914
1328
  }
915
1329
  };
@@ -1089,7 +1503,6 @@ var Grain = (() => {
1089
1503
  try {
1090
1504
  listener();
1091
1505
  } catch (error) {
1092
- console.error("[Activity Detector] Listener error:", error);
1093
1506
  }
1094
1507
  }
1095
1508
  }
@@ -1190,9 +1603,6 @@ var Grain = (() => {
1190
1603
  }
1191
1604
  this.tracker.trackSystemEvent("_grain_heartbeat", properties);
1192
1605
  this.lastHeartbeatTime = now;
1193
- if (this.config.debug) {
1194
- console.log("[Heartbeat] Sent heartbeat:", properties);
1195
- }
1196
1606
  }
1197
1607
  /**
1198
1608
  * Destroy the heartbeat manager
@@ -1205,9 +1615,6 @@ var Grain = (() => {
1205
1615
  this.heartbeatTimer = null;
1206
1616
  }
1207
1617
  this.isDestroyed = true;
1208
- if (this.config.debug) {
1209
- console.log("[Heartbeat] Destroyed");
1210
- }
1211
1618
  }
1212
1619
  };
1213
1620
 
@@ -4408,9 +4815,6 @@ var Grain = (() => {
4408
4815
  properties.viewport = `${window.innerWidth}x${window.innerHeight}`;
4409
4816
  }
4410
4817
  this.tracker.trackSystemEvent("page_view", properties);
4411
- if (this.config.debug) {
4412
- console.log("[Page Tracking] Tracked page view:", properties);
4413
- }
4414
4818
  }
4415
4819
  /**
4416
4820
  * Extract domain from URL
@@ -4533,9 +4937,6 @@ var Grain = (() => {
4533
4937
  }
4534
4938
  }
4535
4939
  this.tracker.trackSystemEvent("page_view", baseProperties);
4536
- if (this.config.debug) {
4537
- console.log("[Page Tracking] Manually tracked page:", baseProperties);
4538
- }
4539
4940
  }
4540
4941
  /**
4541
4942
  * Get page view count for current session
@@ -4562,9 +4963,6 @@ var Grain = (() => {
4562
4963
  window.removeEventListener("hashchange", this.handleHashChange);
4563
4964
  }
4564
4965
  this.isDestroyed = true;
4565
- if (this.config.debug) {
4566
- console.log("[Page Tracking] Destroyed");
4567
- }
4568
4966
  }
4569
4967
  };
4570
4968
 
@@ -4592,6 +4990,7 @@ var Grain = (() => {
4592
4990
  // Auto-tracking properties
4593
4991
  this.interactionTrackingManager = null;
4594
4992
  this.sectionTrackingManager = null;
4993
+ this.heatmapTrackingManager = null;
4595
4994
  // Session tracking
4596
4995
  this.sessionStartTime = Date.now();
4597
4996
  this.sessionEventCount = 0;
@@ -4627,6 +5026,8 @@ var Grain = (() => {
4627
5026
  // 5 minutes
4628
5027
  enableAutoPageView: true,
4629
5028
  stripQueryParams: true,
5029
+ // Heatmap Tracking defaults
5030
+ enableHeatmapTracking: true,
4630
5031
  ...config,
4631
5032
  tenantId: config.tenantId
4632
5033
  };
@@ -4649,6 +5050,9 @@ var Grain = (() => {
4649
5050
  if (typeof window !== "undefined") {
4650
5051
  this.initializeAutomaticTracking();
4651
5052
  this.trackSessionStart();
5053
+ if (this.config.enableHeatmapTracking) {
5054
+ this.initializeHeatmapTracking();
5055
+ }
4652
5056
  }
4653
5057
  this.consentManager.addListener((state) => {
4654
5058
  if (state.granted) {
@@ -4872,6 +5276,8 @@ var Grain = (() => {
4872
5276
  * Log formatted error gracefully
4873
5277
  */
4874
5278
  logError(formattedError) {
5279
+ if (!this.config.debug)
5280
+ return;
4875
5281
  const { code, message, digest, timestamp, context } = formattedError;
4876
5282
  const errorOutput = {
4877
5283
  "\u{1F6A8} Grain Analytics Error": {
@@ -4889,9 +5295,7 @@ var Grain = (() => {
4889
5295
  }
4890
5296
  };
4891
5297
  console.error("\u{1F6A8} Grain Analytics Error:", errorOutput);
4892
- if (this.config.debug) {
4893
- console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
4894
- }
5298
+ console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
4895
5299
  }
4896
5300
  /**
4897
5301
  * Safely execute a function with error handling
@@ -4990,7 +5394,6 @@ var Grain = (() => {
4990
5394
  try {
4991
5395
  const headers = await this.getAuthHeaders();
4992
5396
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
4993
- this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);
4994
5397
  const response = await fetch(url, {
4995
5398
  method: "POST",
4996
5399
  headers,
@@ -5055,7 +5458,6 @@ var Grain = (() => {
5055
5458
  body,
5056
5459
  keepalive: true
5057
5460
  });
5058
- this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
5059
5461
  } catch (error) {
5060
5462
  const formattedError = this.formatError(error, "sendEventsWithBeacon", events);
5061
5463
  this.logError(formattedError);
@@ -5121,7 +5523,6 @@ var Grain = (() => {
5121
5523
  debug: this.config.debug
5122
5524
  }
5123
5525
  );
5124
- this.log("Heartbeat tracking initialized");
5125
5526
  } catch (error) {
5126
5527
  this.log("Failed to initialize heartbeat tracking:", error);
5127
5528
  }
@@ -5136,19 +5537,48 @@ var Grain = (() => {
5136
5537
  tenantId: this.config.tenantId
5137
5538
  }
5138
5539
  );
5139
- this.log("Auto page view tracking initialized");
5140
5540
  } catch (error) {
5141
5541
  this.log("Failed to initialize page view tracking:", error);
5142
5542
  }
5143
5543
  }
5144
5544
  this.initializeAutoTracking();
5145
5545
  }
5546
+ /**
5547
+ * Initialize heatmap tracking
5548
+ */
5549
+ initializeHeatmapTracking() {
5550
+ if (typeof window === "undefined")
5551
+ return;
5552
+ try {
5553
+ this.log("Initializing heatmap tracking");
5554
+ Promise.resolve().then(() => (init_heatmap_tracking(), heatmap_tracking_exports)).then(({ HeatmapTrackingManager: HeatmapTrackingManager2 }) => {
5555
+ try {
5556
+ this.heatmapTrackingManager = new HeatmapTrackingManager2(
5557
+ this,
5558
+ {
5559
+ scrollDebounceDelay: 100,
5560
+ batchDelay: 2e3,
5561
+ maxBatchSize: 20,
5562
+ debug: this.config.debug
5563
+ }
5564
+ );
5565
+ this.log("Heatmap tracking initialized");
5566
+ } catch (error) {
5567
+ this.log("Failed to initialize heatmap tracking:", error);
5568
+ }
5569
+ }).catch((error) => {
5570
+ this.log("Failed to load heatmap tracking module:", error);
5571
+ });
5572
+ } catch (error) {
5573
+ this.log("Failed to initialize heatmap tracking:", error);
5574
+ }
5575
+ }
5146
5576
  /**
5147
5577
  * Initialize auto-tracking (interactions and sections)
5148
5578
  */
5149
5579
  async initializeAutoTracking() {
5150
5580
  try {
5151
- this.log("Initializing auto-tracking...");
5581
+ this.log("Initializing auto-tracking");
5152
5582
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
5153
5583
  const currentUrl = typeof window !== "undefined" ? window.location.href : "";
5154
5584
  const request = {
@@ -5160,23 +5590,19 @@ var Grain = (() => {
5160
5590
  };
5161
5591
  const headers = await this.getAuthHeaders();
5162
5592
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
5163
- this.log("Fetching auto-tracking config from:", url);
5164
5593
  const response = await fetch(url, {
5165
5594
  method: "POST",
5166
5595
  headers,
5167
5596
  body: JSON.stringify(request)
5168
5597
  });
5169
5598
  if (!response.ok) {
5170
- this.log("Failed to fetch auto-tracking config:", response.status, response.statusText);
5599
+ this.log("Failed to fetch auto-tracking config:", response.status);
5171
5600
  return;
5172
5601
  }
5173
5602
  const configResponse = await response.json();
5174
- this.log("Received config response:", configResponse);
5175
5603
  if (configResponse.autoTrackingConfig) {
5176
- this.log("Auto-tracking config found:", configResponse.autoTrackingConfig);
5604
+ this.log("Auto-tracking config loaded");
5177
5605
  this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
5178
- } else {
5179
- this.log("No auto-tracking config in response");
5180
5606
  }
5181
5607
  } catch (error) {
5182
5608
  this.log("Failed to initialize auto-tracking:", error);
@@ -5186,9 +5612,9 @@ var Grain = (() => {
5186
5612
  * Setup auto-tracking managers
5187
5613
  */
5188
5614
  setupAutoTrackingManagers(config) {
5189
- this.log("Setting up auto-tracking managers...", config);
5615
+ this.log("Setting up auto-tracking managers");
5190
5616
  if (config.interactions && config.interactions.length > 0) {
5191
- this.log("Loading interaction tracking module for", config.interactions.length, "interactions");
5617
+ this.log("Loading interaction tracking:", config.interactions.length, "interactions");
5192
5618
  Promise.resolve().then(() => (init_interaction_tracking(), interaction_tracking_exports)).then(({ InteractionTrackingManager: InteractionTrackingManager2 }) => {
5193
5619
  try {
5194
5620
  this.interactionTrackingManager = new InteractionTrackingManager2(
@@ -5200,18 +5626,16 @@ var Grain = (() => {
5200
5626
  mutationDebounceDelay: 500
5201
5627
  }
5202
5628
  );
5203
- this.log("\u2705 Interaction tracking initialized successfully with", config.interactions.length, "interactions");
5629
+ this.log("Interaction tracking initialized");
5204
5630
  } catch (error) {
5205
- this.log("\u274C Failed to initialize interaction tracking:", error);
5631
+ this.log("Failed to initialize interaction tracking:", error);
5206
5632
  }
5207
5633
  }).catch((error) => {
5208
- this.log("\u274C Failed to load interaction tracking module:", error);
5634
+ this.log("Failed to load interaction tracking module:", error);
5209
5635
  });
5210
- } else {
5211
- this.log("No interactions configured for auto-tracking");
5212
5636
  }
5213
5637
  if (config.sections && config.sections.length > 0) {
5214
- this.log("Loading section tracking module for", config.sections.length, "sections");
5638
+ this.log("Loading section tracking:", config.sections.length, "sections");
5215
5639
  Promise.resolve().then(() => (init_section_tracking(), section_tracking_exports)).then(({ SectionTrackingManager: SectionTrackingManager2 }) => {
5216
5640
  try {
5217
5641
  this.sectionTrackingManager = new SectionTrackingManager2(
@@ -5226,15 +5650,13 @@ var Grain = (() => {
5226
5650
  debug: this.config.debug
5227
5651
  }
5228
5652
  );
5229
- this.log("\u2705 Section tracking initialized successfully with", config.sections.length, "sections");
5653
+ this.log("Section tracking initialized");
5230
5654
  } catch (error) {
5231
- this.log("\u274C Failed to initialize section tracking:", error);
5655
+ this.log("Failed to initialize section tracking:", error);
5232
5656
  }
5233
5657
  }).catch((error) => {
5234
- this.log("\u274C Failed to load section tracking module:", error);
5658
+ this.log("Failed to load section tracking module:", error);
5235
5659
  });
5236
- } else {
5237
- this.log("No sections configured for auto-tracking");
5238
5660
  }
5239
5661
  }
5240
5662
  /**
@@ -5290,7 +5712,7 @@ var Grain = (() => {
5290
5712
  properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
5291
5713
  }
5292
5714
  this.trackSystemEvent("_grain_session_start", properties);
5293
- this.log("Session started:", properties);
5715
+ this.log("Session started");
5294
5716
  }
5295
5717
  /**
5296
5718
  * Track session end event
@@ -5315,7 +5737,7 @@ var Grain = (() => {
5315
5737
  properties.page_count = pageCount;
5316
5738
  }
5317
5739
  this.trackSystemEvent("_grain_session_end", properties);
5318
- this.log("Session ended:", properties);
5740
+ this.log("Session ended");
5319
5741
  }
5320
5742
  /**
5321
5743
  * Detect browser name
@@ -5412,7 +5834,7 @@ var Grain = (() => {
5412
5834
  };
5413
5835
  this.eventQueue.push(event);
5414
5836
  this.eventCountSinceLastHeartbeat++;
5415
- this.log(`Queued system event: ${eventName}`, properties);
5837
+ this.log(`Queued system event: ${eventName}`);
5416
5838
  if (this.eventQueue.length >= this.config.batchSize) {
5417
5839
  this.flush().catch((error) => {
5418
5840
  const formattedError = this.formatError(error, "flush system event");
@@ -5502,7 +5924,7 @@ var Grain = (() => {
5502
5924
  this.eventQueue.push(formattedEvent);
5503
5925
  this.eventCountSinceLastHeartbeat++;
5504
5926
  this.sessionEventCount++;
5505
- this.log(`Queued event: ${event.eventName}`, event.properties);
5927
+ this.log(`Queued event: ${event.eventName}`);
5506
5928
  if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
5507
5929
  await this.flush();
5508
5930
  }
@@ -5701,7 +6123,6 @@ var Grain = (() => {
5701
6123
  try {
5702
6124
  const headers = await this.getAuthHeaders();
5703
6125
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;
5704
- this.log(`Setting properties for user ${payload.userId} (attempt ${attempt + 1})`);
5705
6126
  const response = await fetch(url, {
5706
6127
  method: "POST",
5707
6128
  headers,
@@ -5927,7 +6348,6 @@ var Grain = (() => {
5927
6348
  try {
5928
6349
  const headers = await this.getAuthHeaders();
5929
6350
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
5930
- this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
5931
6351
  const response = await fetch(url, {
5932
6352
  method: "POST",
5933
6353
  headers,
@@ -5954,7 +6374,7 @@ var Grain = (() => {
5954
6374
  if (configResponse.configurations) {
5955
6375
  this.updateConfigCache(configResponse, userId);
5956
6376
  }
5957
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
6377
+ this.log("Successfully fetched configurations");
5958
6378
  return configResponse;
5959
6379
  } catch (error) {
5960
6380
  lastError = error;
@@ -6203,6 +6623,10 @@ var Grain = (() => {
6203
6623
  this.sectionTrackingManager.destroy();
6204
6624
  this.sectionTrackingManager = null;
6205
6625
  }
6626
+ if (this.heatmapTrackingManager) {
6627
+ this.heatmapTrackingManager.destroy();
6628
+ this.heatmapTrackingManager = null;
6629
+ }
6206
6630
  if (this.eventQueue.length > 0) {
6207
6631
  const eventsToSend = [...this.eventQueue];
6208
6632
  this.eventQueue = [];