@grainql/analytics-web 2.5.3 → 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 +534 -80
  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 +98 -39
  70. package/dist/index.mjs +99 -40
  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.3 | 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
@@ -4906,10 +5310,40 @@ var Grain = (() => {
4906
5310
  }
4907
5311
  }
4908
5312
  formatEvent(event) {
5313
+ const properties = event.properties || {};
5314
+ if (!this.config.disableAutoProperties && typeof window !== "undefined") {
5315
+ const hasConsent = this.consentManager.hasConsent("analytics");
5316
+ const isSystemEvent = event.eventName.startsWith("_grain_");
5317
+ if (!isSystemEvent && hasConsent) {
5318
+ const sessionUTMs = getSessionUTMParameters();
5319
+ if (sessionUTMs) {
5320
+ if (sessionUTMs.utm_source)
5321
+ properties.utm_source = sessionUTMs.utm_source;
5322
+ if (sessionUTMs.utm_medium)
5323
+ properties.utm_medium = sessionUTMs.utm_medium;
5324
+ if (sessionUTMs.utm_campaign)
5325
+ properties.utm_campaign = sessionUTMs.utm_campaign;
5326
+ if (sessionUTMs.utm_term)
5327
+ properties.utm_term = sessionUTMs.utm_term;
5328
+ if (sessionUTMs.utm_content)
5329
+ properties.utm_content = sessionUTMs.utm_content;
5330
+ }
5331
+ const firstTouch = getFirstTouchAttribution(this.config.tenantId);
5332
+ if (firstTouch) {
5333
+ properties.first_touch_source = firstTouch.source;
5334
+ properties.first_touch_medium = firstTouch.medium;
5335
+ properties.first_touch_campaign = firstTouch.campaign;
5336
+ properties.first_touch_referrer_category = firstTouch.referrer_category;
5337
+ }
5338
+ if (!properties.session_id) {
5339
+ properties.session_id = this.getSessionId();
5340
+ }
5341
+ }
5342
+ }
4909
5343
  return {
4910
5344
  eventName: event.eventName,
4911
5345
  userId: event.userId || this.getEffectiveUserIdInternal(),
4912
- properties: event.properties || {}
5346
+ properties
4913
5347
  };
4914
5348
  }
4915
5349
  async getAuthHeaders() {
@@ -4960,7 +5394,6 @@ var Grain = (() => {
4960
5394
  try {
4961
5395
  const headers = await this.getAuthHeaders();
4962
5396
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
4963
- this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);
4964
5397
  const response = await fetch(url, {
4965
5398
  method: "POST",
4966
5399
  headers,
@@ -5025,7 +5458,6 @@ var Grain = (() => {
5025
5458
  body,
5026
5459
  keepalive: true
5027
5460
  });
5028
- this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
5029
5461
  } catch (error) {
5030
5462
  const formattedError = this.formatError(error, "sendEventsWithBeacon", events);
5031
5463
  this.logError(formattedError);
@@ -5091,7 +5523,6 @@ var Grain = (() => {
5091
5523
  debug: this.config.debug
5092
5524
  }
5093
5525
  );
5094
- this.log("Heartbeat tracking initialized");
5095
5526
  } catch (error) {
5096
5527
  this.log("Failed to initialize heartbeat tracking:", error);
5097
5528
  }
@@ -5106,19 +5537,48 @@ var Grain = (() => {
5106
5537
  tenantId: this.config.tenantId
5107
5538
  }
5108
5539
  );
5109
- this.log("Auto page view tracking initialized");
5110
5540
  } catch (error) {
5111
5541
  this.log("Failed to initialize page view tracking:", error);
5112
5542
  }
5113
5543
  }
5114
5544
  this.initializeAutoTracking();
5115
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
+ }
5116
5576
  /**
5117
5577
  * Initialize auto-tracking (interactions and sections)
5118
5578
  */
5119
5579
  async initializeAutoTracking() {
5120
5580
  try {
5121
- this.log("Initializing auto-tracking...");
5581
+ this.log("Initializing auto-tracking");
5122
5582
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
5123
5583
  const currentUrl = typeof window !== "undefined" ? window.location.href : "";
5124
5584
  const request = {
@@ -5130,23 +5590,19 @@ var Grain = (() => {
5130
5590
  };
5131
5591
  const headers = await this.getAuthHeaders();
5132
5592
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
5133
- this.log("Fetching auto-tracking config from:", url);
5134
5593
  const response = await fetch(url, {
5135
5594
  method: "POST",
5136
5595
  headers,
5137
5596
  body: JSON.stringify(request)
5138
5597
  });
5139
5598
  if (!response.ok) {
5140
- this.log("Failed to fetch auto-tracking config:", response.status, response.statusText);
5599
+ this.log("Failed to fetch auto-tracking config:", response.status);
5141
5600
  return;
5142
5601
  }
5143
5602
  const configResponse = await response.json();
5144
- this.log("Received config response:", configResponse);
5145
5603
  if (configResponse.autoTrackingConfig) {
5146
- this.log("Auto-tracking config found:", configResponse.autoTrackingConfig);
5604
+ this.log("Auto-tracking config loaded");
5147
5605
  this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
5148
- } else {
5149
- this.log("No auto-tracking config in response");
5150
5606
  }
5151
5607
  } catch (error) {
5152
5608
  this.log("Failed to initialize auto-tracking:", error);
@@ -5156,9 +5612,9 @@ var Grain = (() => {
5156
5612
  * Setup auto-tracking managers
5157
5613
  */
5158
5614
  setupAutoTrackingManagers(config) {
5159
- this.log("Setting up auto-tracking managers...", config);
5615
+ this.log("Setting up auto-tracking managers");
5160
5616
  if (config.interactions && config.interactions.length > 0) {
5161
- this.log("Loading interaction tracking module for", config.interactions.length, "interactions");
5617
+ this.log("Loading interaction tracking:", config.interactions.length, "interactions");
5162
5618
  Promise.resolve().then(() => (init_interaction_tracking(), interaction_tracking_exports)).then(({ InteractionTrackingManager: InteractionTrackingManager2 }) => {
5163
5619
  try {
5164
5620
  this.interactionTrackingManager = new InteractionTrackingManager2(
@@ -5170,18 +5626,16 @@ var Grain = (() => {
5170
5626
  mutationDebounceDelay: 500
5171
5627
  }
5172
5628
  );
5173
- this.log("\u2705 Interaction tracking initialized successfully with", config.interactions.length, "interactions");
5629
+ this.log("Interaction tracking initialized");
5174
5630
  } catch (error) {
5175
- this.log("\u274C Failed to initialize interaction tracking:", error);
5631
+ this.log("Failed to initialize interaction tracking:", error);
5176
5632
  }
5177
5633
  }).catch((error) => {
5178
- this.log("\u274C Failed to load interaction tracking module:", error);
5634
+ this.log("Failed to load interaction tracking module:", error);
5179
5635
  });
5180
- } else {
5181
- this.log("No interactions configured for auto-tracking");
5182
5636
  }
5183
5637
  if (config.sections && config.sections.length > 0) {
5184
- this.log("Loading section tracking module for", config.sections.length, "sections");
5638
+ this.log("Loading section tracking:", config.sections.length, "sections");
5185
5639
  Promise.resolve().then(() => (init_section_tracking(), section_tracking_exports)).then(({ SectionTrackingManager: SectionTrackingManager2 }) => {
5186
5640
  try {
5187
5641
  this.sectionTrackingManager = new SectionTrackingManager2(
@@ -5196,15 +5650,13 @@ var Grain = (() => {
5196
5650
  debug: this.config.debug
5197
5651
  }
5198
5652
  );
5199
- this.log("\u2705 Section tracking initialized successfully with", config.sections.length, "sections");
5653
+ this.log("Section tracking initialized");
5200
5654
  } catch (error) {
5201
- this.log("\u274C Failed to initialize section tracking:", error);
5655
+ this.log("Failed to initialize section tracking:", error);
5202
5656
  }
5203
5657
  }).catch((error) => {
5204
- this.log("\u274C Failed to load section tracking module:", error);
5658
+ this.log("Failed to load section tracking module:", error);
5205
5659
  });
5206
- } else {
5207
- this.log("No sections configured for auto-tracking");
5208
5660
  }
5209
5661
  }
5210
5662
  /**
@@ -5260,7 +5712,7 @@ var Grain = (() => {
5260
5712
  properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
5261
5713
  }
5262
5714
  this.trackSystemEvent("_grain_session_start", properties);
5263
- this.log("Session started:", properties);
5715
+ this.log("Session started");
5264
5716
  }
5265
5717
  /**
5266
5718
  * Track session end event
@@ -5285,7 +5737,7 @@ var Grain = (() => {
5285
5737
  properties.page_count = pageCount;
5286
5738
  }
5287
5739
  this.trackSystemEvent("_grain_session_end", properties);
5288
- this.log("Session ended:", properties);
5740
+ this.log("Session ended");
5289
5741
  }
5290
5742
  /**
5291
5743
  * Detect browser name
@@ -5382,7 +5834,7 @@ var Grain = (() => {
5382
5834
  };
5383
5835
  this.eventQueue.push(event);
5384
5836
  this.eventCountSinceLastHeartbeat++;
5385
- this.log(`Queued system event: ${eventName}`, properties);
5837
+ this.log(`Queued system event: ${eventName}`);
5386
5838
  if (this.eventQueue.length >= this.config.batchSize) {
5387
5839
  this.flush().catch((error) => {
5388
5840
  const formattedError = this.formatError(error, "flush system event");
@@ -5472,7 +5924,7 @@ var Grain = (() => {
5472
5924
  this.eventQueue.push(formattedEvent);
5473
5925
  this.eventCountSinceLastHeartbeat++;
5474
5926
  this.sessionEventCount++;
5475
- this.log(`Queued event: ${event.eventName}`, event.properties);
5927
+ this.log(`Queued event: ${event.eventName}`);
5476
5928
  if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
5477
5929
  await this.flush();
5478
5930
  }
@@ -5671,7 +6123,6 @@ var Grain = (() => {
5671
6123
  try {
5672
6124
  const headers = await this.getAuthHeaders();
5673
6125
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;
5674
- this.log(`Setting properties for user ${payload.userId} (attempt ${attempt + 1})`);
5675
6126
  const response = await fetch(url, {
5676
6127
  method: "POST",
5677
6128
  headers,
@@ -5897,7 +6348,6 @@ var Grain = (() => {
5897
6348
  try {
5898
6349
  const headers = await this.getAuthHeaders();
5899
6350
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
5900
- this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
5901
6351
  const response = await fetch(url, {
5902
6352
  method: "POST",
5903
6353
  headers,
@@ -5924,7 +6374,7 @@ var Grain = (() => {
5924
6374
  if (configResponse.configurations) {
5925
6375
  this.updateConfigCache(configResponse, userId);
5926
6376
  }
5927
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
6377
+ this.log("Successfully fetched configurations");
5928
6378
  return configResponse;
5929
6379
  } catch (error) {
5930
6380
  lastError = error;
@@ -6173,6 +6623,10 @@ var Grain = (() => {
6173
6623
  this.sectionTrackingManager.destroy();
6174
6624
  this.sectionTrackingManager = null;
6175
6625
  }
6626
+ if (this.heatmapTrackingManager) {
6627
+ this.heatmapTrackingManager.destroy();
6628
+ this.heatmapTrackingManager = null;
6629
+ }
6176
6630
  if (this.eventQueue.length > 0) {
6177
6631
  const eventsToSend = [...this.eventQueue];
6178
6632
  this.eventQueue = [];