@grainql/analytics-web 2.5.4 → 2.7.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 (99) hide show
  1. package/README.md +3 -1
  2. package/dist/activity.js +1 -1
  3. package/dist/attention-quality.d.ts +128 -0
  4. package/dist/attention-quality.d.ts.map +1 -0
  5. package/dist/attention-quality.js +246 -0
  6. package/dist/cjs/activity.js +1 -1
  7. package/dist/cjs/activity.js.map +1 -1
  8. package/dist/cjs/attention-quality.d.ts +128 -0
  9. package/dist/cjs/attention-quality.d.ts.map +1 -0
  10. package/dist/cjs/attention-quality.js +246 -0
  11. package/dist/cjs/attention-quality.js.map +1 -0
  12. package/dist/cjs/consent.js +4 -4
  13. package/dist/cjs/consent.js.map +1 -1
  14. package/dist/cjs/heartbeat.d.ts.map +1 -1
  15. package/dist/cjs/heartbeat.js +0 -6
  16. package/dist/cjs/heartbeat.js.map +1 -1
  17. package/dist/cjs/heatmap-tracking.d.ts +93 -0
  18. package/dist/cjs/heatmap-tracking.d.ts.map +1 -0
  19. package/dist/cjs/heatmap-tracking.js +499 -0
  20. package/dist/cjs/heatmap-tracking.js.map +1 -0
  21. package/dist/cjs/index.d.ts +11 -0
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  25. package/dist/cjs/interaction-tracking.js +9 -18
  26. package/dist/cjs/interaction-tracking.js.map +1 -1
  27. package/dist/cjs/page-tracking.d.ts.map +1 -1
  28. package/dist/cjs/page-tracking.js +0 -9
  29. package/dist/cjs/page-tracking.js.map +1 -1
  30. package/dist/cjs/section-tracking.d.ts +3 -0
  31. package/dist/cjs/section-tracking.d.ts.map +1 -1
  32. package/dist/cjs/section-tracking.js +30 -7
  33. package/dist/cjs/section-tracking.js.map +1 -1
  34. package/dist/cjs/types/auto-tracking.d.ts +3 -0
  35. package/dist/cjs/types/auto-tracking.d.ts.map +1 -1
  36. package/dist/cjs/types/heatmap-tracking.d.ts +44 -0
  37. package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -0
  38. package/dist/cjs/types/heatmap-tracking.js +6 -0
  39. package/dist/cjs/types/heatmap-tracking.js.map +1 -0
  40. package/dist/consent.js +4 -4
  41. package/dist/esm/activity.js +1 -1
  42. package/dist/esm/activity.js.map +1 -1
  43. package/dist/esm/attention-quality.d.ts +128 -0
  44. package/dist/esm/attention-quality.d.ts.map +1 -0
  45. package/dist/esm/attention-quality.js +242 -0
  46. package/dist/esm/attention-quality.js.map +1 -0
  47. package/dist/esm/consent.js +4 -4
  48. package/dist/esm/consent.js.map +1 -1
  49. package/dist/esm/heartbeat.d.ts.map +1 -1
  50. package/dist/esm/heartbeat.js +0 -6
  51. package/dist/esm/heartbeat.js.map +1 -1
  52. package/dist/esm/heatmap-tracking.d.ts +93 -0
  53. package/dist/esm/heatmap-tracking.d.ts.map +1 -0
  54. package/dist/esm/heatmap-tracking.js +495 -0
  55. package/dist/esm/heatmap-tracking.js.map +1 -0
  56. package/dist/esm/index.d.ts +11 -0
  57. package/dist/esm/index.d.ts.map +1 -1
  58. package/dist/esm/index.js.map +1 -1
  59. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  60. package/dist/esm/interaction-tracking.js +9 -18
  61. package/dist/esm/interaction-tracking.js.map +1 -1
  62. package/dist/esm/page-tracking.d.ts.map +1 -1
  63. package/dist/esm/page-tracking.js +0 -9
  64. package/dist/esm/page-tracking.js.map +1 -1
  65. package/dist/esm/section-tracking.d.ts +3 -0
  66. package/dist/esm/section-tracking.d.ts.map +1 -1
  67. package/dist/esm/section-tracking.js +30 -7
  68. package/dist/esm/section-tracking.js.map +1 -1
  69. package/dist/esm/types/auto-tracking.d.ts +3 -0
  70. package/dist/esm/types/auto-tracking.d.ts.map +1 -1
  71. package/dist/esm/types/heatmap-tracking.d.ts +44 -0
  72. package/dist/esm/types/heatmap-tracking.d.ts.map +1 -0
  73. package/dist/esm/types/heatmap-tracking.js +5 -0
  74. package/dist/esm/types/heatmap-tracking.js.map +1 -0
  75. package/dist/heartbeat.d.ts.map +1 -1
  76. package/dist/heartbeat.js +0 -6
  77. package/dist/heatmap-tracking.d.ts +93 -0
  78. package/dist/heatmap-tracking.d.ts.map +1 -0
  79. package/dist/heatmap-tracking.js +499 -0
  80. package/dist/index.d.ts +11 -0
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.global.dev.js +801 -80
  83. package/dist/index.global.dev.js.map +4 -4
  84. package/dist/index.global.js +2 -2
  85. package/dist/index.global.js.map +4 -4
  86. package/dist/index.js +70 -38
  87. package/dist/index.mjs +70 -38
  88. package/dist/interaction-tracking.d.ts.map +1 -1
  89. package/dist/interaction-tracking.js +9 -18
  90. package/dist/page-tracking.d.ts.map +1 -1
  91. package/dist/page-tracking.js +0 -9
  92. package/dist/section-tracking.d.ts +3 -0
  93. package/dist/section-tracking.d.ts.map +1 -1
  94. package/dist/section-tracking.js +30 -7
  95. package/dist/types/auto-tracking.d.ts +3 -0
  96. package/dist/types/auto-tracking.d.ts.map +1 -1
  97. package/dist/types/heatmap-tracking.d.ts +44 -0
  98. package/dist/types/heatmap-tracking.d.ts.map +1 -0
  99. 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.7.0 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -22,6 +22,696 @@ var Grain = (() => {
22
22
  };
23
23
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
24
 
25
+ // src/attention-quality.ts
26
+ var DEFAULT_OPTIONS, AttentionQualityManager;
27
+ var init_attention_quality = __esm({
28
+ "src/attention-quality.ts"() {
29
+ "use strict";
30
+ DEFAULT_OPTIONS = {
31
+ maxSectionDuration: 9e3,
32
+ // 9 seconds
33
+ minScrollDistance: 100,
34
+ // 100 pixels
35
+ idleThreshold: 3e4
36
+ // 30 seconds
37
+ };
38
+ AttentionQualityManager = class {
39
+ constructor(activityDetector, options = {}) {
40
+ this.isDestroyed = false;
41
+ // Page visibility tracking
42
+ this.isPageVisible = true;
43
+ this.visibilityChangeHandler = null;
44
+ // Section attention state
45
+ this.sectionStates = /* @__PURE__ */ new Map();
46
+ // Policies applied reasons (for debugging/traceability)
47
+ this.lastFilterReason = null;
48
+ this.activityDetector = activityDetector;
49
+ this.options = {
50
+ ...DEFAULT_OPTIONS,
51
+ ...options,
52
+ debug: options.debug ?? false
53
+ };
54
+ this.setupPageVisibilityTracking();
55
+ }
56
+ /**
57
+ * Setup page visibility tracking
58
+ */
59
+ setupPageVisibilityTracking() {
60
+ if (typeof document === "undefined")
61
+ return;
62
+ this.isPageVisible = document.visibilityState === "visible";
63
+ this.visibilityChangeHandler = () => {
64
+ const wasVisible = this.isPageVisible;
65
+ this.isPageVisible = document.visibilityState === "visible";
66
+ if (!this.isPageVisible && wasVisible) {
67
+ this.log("Page hidden - tracking paused");
68
+ } else if (this.isPageVisible && !wasVisible) {
69
+ this.log("Page visible - tracking resumed");
70
+ this.resetAllSections();
71
+ }
72
+ };
73
+ document.addEventListener("visibilitychange", this.visibilityChangeHandler);
74
+ }
75
+ /**
76
+ * Check if tracking should be allowed (global check)
77
+ * Returns true if tracking is allowed, false if it should be paused
78
+ */
79
+ shouldTrack() {
80
+ if (!this.isPageVisible) {
81
+ this.lastFilterReason = "page_hidden";
82
+ return false;
83
+ }
84
+ if (!this.activityDetector.isActive(this.options.idleThreshold)) {
85
+ this.lastFilterReason = "user_idle";
86
+ return false;
87
+ }
88
+ this.lastFilterReason = null;
89
+ return true;
90
+ }
91
+ /**
92
+ * Check if section view tracking should be allowed for a specific section
93
+ * @param sectionName - Section identifier
94
+ * @param currentScrollY - Current scroll position
95
+ * @returns Object with shouldTrack boolean and optional reason
96
+ */
97
+ shouldTrackSection(sectionName, currentScrollY) {
98
+ if (!this.shouldTrack()) {
99
+ return {
100
+ shouldTrack: false,
101
+ reason: this.lastFilterReason || "global_policy"
102
+ };
103
+ }
104
+ let state = this.sectionStates.get(sectionName);
105
+ if (!state) {
106
+ state = {
107
+ sectionName,
108
+ currentDuration: 0,
109
+ lastScrollPosition: currentScrollY,
110
+ lastResetTime: Date.now()
111
+ };
112
+ this.sectionStates.set(sectionName, state);
113
+ }
114
+ const scrollDistance = Math.abs(currentScrollY - state.lastScrollPosition);
115
+ const hasScrolledEnough = scrollDistance >= this.options.minScrollDistance;
116
+ if (hasScrolledEnough) {
117
+ this.log(`Section "${sectionName}": Attention reset due to ${Math.round(scrollDistance)}px scroll`);
118
+ state.currentDuration = 0;
119
+ state.lastScrollPosition = currentScrollY;
120
+ state.lastResetTime = Date.now();
121
+ return {
122
+ shouldTrack: true,
123
+ resetAttention: true
124
+ };
125
+ }
126
+ if (state.currentDuration >= this.options.maxSectionDuration) {
127
+ return {
128
+ shouldTrack: false,
129
+ reason: "max_duration_reached"
130
+ };
131
+ }
132
+ return {
133
+ shouldTrack: true
134
+ };
135
+ }
136
+ /**
137
+ * Update section duration (call this when tracking a section view event)
138
+ * @param sectionName - Section identifier
139
+ * @param durationMs - Duration to add to current attention block
140
+ */
141
+ updateSectionDuration(sectionName, durationMs) {
142
+ const state = this.sectionStates.get(sectionName);
143
+ if (state) {
144
+ state.currentDuration += durationMs;
145
+ if (state.currentDuration >= this.options.maxSectionDuration) {
146
+ this.log(`Section "${sectionName}": Max duration cap reached (${state.currentDuration}ms)`);
147
+ }
148
+ }
149
+ }
150
+ /**
151
+ * Reset attention for a specific section (call when user navigates to different section)
152
+ * @param sectionName - Section identifier
153
+ */
154
+ resetSection(sectionName) {
155
+ const state = this.sectionStates.get(sectionName);
156
+ if (state) {
157
+ this.log(`Section "${sectionName}": Attention reset (section exit)`);
158
+ state.currentDuration = 0;
159
+ state.lastResetTime = Date.now();
160
+ }
161
+ }
162
+ /**
163
+ * Reset all section attention states
164
+ */
165
+ resetAllSections() {
166
+ this.log("Resetting all section attention states");
167
+ for (const state of this.sectionStates.values()) {
168
+ state.currentDuration = 0;
169
+ state.lastResetTime = Date.now();
170
+ }
171
+ }
172
+ /**
173
+ * Get current attention state for a section (for debugging/monitoring)
174
+ */
175
+ getSectionState(sectionName) {
176
+ return this.sectionStates.get(sectionName);
177
+ }
178
+ /**
179
+ * Get reason why last tracking attempt was filtered
180
+ */
181
+ getLastFilterReason() {
182
+ return this.lastFilterReason;
183
+ }
184
+ /**
185
+ * Check if scroll tracking should be allowed
186
+ * Similar to shouldTrack() but also checks scroll-specific conditions
187
+ */
188
+ shouldTrackScroll(previousScrollY, currentScrollY) {
189
+ if (!this.shouldTrack()) {
190
+ return {
191
+ shouldTrack: false,
192
+ reason: this.lastFilterReason || "global_policy"
193
+ };
194
+ }
195
+ const scrollDistance = Math.abs(currentScrollY - previousScrollY);
196
+ if (scrollDistance < 10) {
197
+ return {
198
+ shouldTrack: false,
199
+ reason: "scroll_too_small"
200
+ };
201
+ }
202
+ return {
203
+ shouldTrack: true
204
+ };
205
+ }
206
+ /**
207
+ * Get all active policies as object (for monitoring/debugging)
208
+ */
209
+ getPolicies() {
210
+ return {
211
+ maxSectionDuration: this.options.maxSectionDuration,
212
+ minScrollDistance: this.options.minScrollDistance,
213
+ idleThreshold: this.options.idleThreshold
214
+ };
215
+ }
216
+ /**
217
+ * Get current tracking state (for monitoring/debugging)
218
+ */
219
+ getTrackingState() {
220
+ return {
221
+ isPageVisible: this.isPageVisible,
222
+ isUserActive: this.activityDetector.isActive(this.options.idleThreshold),
223
+ timeSinceLastActivity: this.activityDetector.getTimeSinceLastActivity(),
224
+ activeSections: this.sectionStates.size
225
+ };
226
+ }
227
+ /**
228
+ * Log debug messages
229
+ */
230
+ log(...args) {
231
+ if (this.options.debug) {
232
+ console.log("[AttentionQuality]", ...args);
233
+ }
234
+ }
235
+ /**
236
+ * Destroy and cleanup
237
+ */
238
+ destroy() {
239
+ if (this.isDestroyed)
240
+ return;
241
+ this.isDestroyed = true;
242
+ if (this.visibilityChangeHandler && typeof document !== "undefined") {
243
+ document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
244
+ this.visibilityChangeHandler = null;
245
+ }
246
+ this.sectionStates.clear();
247
+ }
248
+ };
249
+ }
250
+ });
251
+
252
+ // src/heatmap-tracking.ts
253
+ var heatmap_tracking_exports = {};
254
+ __export(heatmap_tracking_exports, {
255
+ HeatmapTrackingManager: () => HeatmapTrackingManager
256
+ });
257
+ var DEFAULT_OPTIONS2, HeatmapTrackingManager;
258
+ var init_heatmap_tracking = __esm({
259
+ "src/heatmap-tracking.ts"() {
260
+ "use strict";
261
+ init_attention_quality();
262
+ DEFAULT_OPTIONS2 = {
263
+ scrollDebounceDelay: 100,
264
+ batchDelay: 2e3,
265
+ maxBatchSize: 20,
266
+ debug: false
267
+ };
268
+ HeatmapTrackingManager = class {
269
+ constructor(tracker, options = {}) {
270
+ this.isDestroyed = false;
271
+ // Tracking state
272
+ this.currentScrollState = null;
273
+ this.pendingClicks = [];
274
+ this.pendingScrolls = [];
275
+ // Timers
276
+ this.scrollDebounceTimer = null;
277
+ this.batchTimer = null;
278
+ this.scrollTrackingTimer = null;
279
+ this.periodicScrollTimer = null;
280
+ // Scroll tracking
281
+ this.lastScrollPosition = 0;
282
+ this.lastScrollTime = Date.now();
283
+ this.SPLIT_DURATION = 3e3;
284
+ this.tracker = tracker;
285
+ this.options = { ...DEFAULT_OPTIONS2, ...options };
286
+ this.attentionQuality = new AttentionQualityManager(
287
+ tracker.getActivityDetector(),
288
+ {
289
+ maxSectionDuration: 9e3,
290
+ // 9 seconds per viewport section
291
+ minScrollDistance: 100,
292
+ // 100 pixels
293
+ idleThreshold: 3e4,
294
+ // 30 seconds
295
+ debug: this.options.debug
296
+ }
297
+ );
298
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
299
+ if (document.readyState === "loading") {
300
+ document.addEventListener("DOMContentLoaded", () => this.initialize());
301
+ } else {
302
+ setTimeout(() => this.initialize(), 0);
303
+ }
304
+ }
305
+ }
306
+ /**
307
+ * Initialize heatmap tracking
308
+ */
309
+ initialize() {
310
+ if (this.isDestroyed)
311
+ return;
312
+ this.log("Initializing heatmap tracking");
313
+ this.setupClickTracking();
314
+ this.setupScrollTracking();
315
+ this.startScrollTracking();
316
+ this.setupUnloadHandler();
317
+ }
318
+ /**
319
+ * Setup click event tracking
320
+ */
321
+ setupClickTracking() {
322
+ if (typeof document === "undefined")
323
+ return;
324
+ const clickHandler = (event) => {
325
+ if (this.isDestroyed)
326
+ return;
327
+ if (!this.tracker.hasConsent("analytics"))
328
+ return;
329
+ this.handleClick(event);
330
+ };
331
+ document.addEventListener("click", clickHandler, { passive: true, capture: true });
332
+ }
333
+ /**
334
+ * Setup scroll event tracking
335
+ */
336
+ setupScrollTracking() {
337
+ if (typeof window === "undefined")
338
+ return;
339
+ const scrollHandler = () => {
340
+ if (this.scrollDebounceTimer !== null) {
341
+ clearTimeout(this.scrollDebounceTimer);
342
+ }
343
+ this.scrollDebounceTimer = window.setTimeout(() => {
344
+ this.handleScroll();
345
+ this.scrollDebounceTimer = null;
346
+ }, this.options.scrollDebounceDelay);
347
+ };
348
+ window.addEventListener("scroll", scrollHandler, { passive: true });
349
+ }
350
+ /**
351
+ * Start periodic scroll state tracking
352
+ */
353
+ startScrollTracking() {
354
+ if (typeof window === "undefined")
355
+ return;
356
+ this.updateScrollState();
357
+ this.scrollTrackingTimer = window.setInterval(() => {
358
+ if (this.isDestroyed)
359
+ return;
360
+ this.updateScrollState();
361
+ }, 500);
362
+ this.startPeriodicScrollTracking();
363
+ }
364
+ /**
365
+ * Start periodic scroll tracking (sends events every 3 seconds)
366
+ */
367
+ startPeriodicScrollTracking() {
368
+ if (typeof window === "undefined")
369
+ return;
370
+ this.periodicScrollTimer = window.setInterval(() => {
371
+ if (this.isDestroyed || !this.currentScrollState)
372
+ return;
373
+ if (!this.tracker.hasConsent("analytics"))
374
+ return;
375
+ if (!this.attentionQuality.shouldTrack()) {
376
+ this.log("Scroll tracking paused:", this.attentionQuality.getLastFilterReason());
377
+ return;
378
+ }
379
+ const currentTime = Date.now();
380
+ const duration = currentTime - this.currentScrollState.entryTime;
381
+ if (duration > 1e3) {
382
+ const scrollY = window.scrollY || window.pageYOffset;
383
+ const viewportHeight = window.innerHeight;
384
+ const pageHeight = document.documentElement.scrollHeight;
385
+ const sectionKey = `viewport_section_${this.currentScrollState.viewportSection}`;
386
+ const attentionCheck = this.attentionQuality.shouldTrackSection(sectionKey, scrollY);
387
+ if (attentionCheck.resetAttention) {
388
+ this.log(`Viewport section ${this.currentScrollState.viewportSection}: Attention reset`);
389
+ this.currentScrollState.entryTime = currentTime;
390
+ return;
391
+ }
392
+ if (!attentionCheck.shouldTrack) {
393
+ this.log(`Viewport section ${this.currentScrollState.viewportSection}: ${attentionCheck.reason}`);
394
+ return;
395
+ }
396
+ const scrollData = {
397
+ pageUrl: window.location.href,
398
+ viewportSection: this.currentScrollState.viewportSection,
399
+ scrollDepthPx: scrollY,
400
+ durationMs: duration,
401
+ entryTimestamp: this.currentScrollState.entryTime,
402
+ exitTimestamp: currentTime,
403
+ pageHeight,
404
+ viewportHeight
405
+ };
406
+ this.tracker.trackSystemEvent("_grain_heatmap_scroll", {
407
+ page_url: scrollData.pageUrl,
408
+ viewport_section: scrollData.viewportSection,
409
+ scroll_depth_px: scrollData.scrollDepthPx,
410
+ duration_ms: scrollData.durationMs,
411
+ entry_timestamp: scrollData.entryTimestamp,
412
+ exit_timestamp: scrollData.exitTimestamp,
413
+ page_height: scrollData.pageHeight,
414
+ viewport_height: scrollData.viewportHeight,
415
+ is_split: true
416
+ // Flag to indicate periodic tracking, not final exit
417
+ }, { flush: true });
418
+ this.attentionQuality.updateSectionDuration(sectionKey, duration);
419
+ this.currentScrollState.entryTime = currentTime;
420
+ }
421
+ }, this.SPLIT_DURATION);
422
+ }
423
+ /**
424
+ * Setup page unload handler to beacon remaining data
425
+ */
426
+ setupUnloadHandler() {
427
+ if (typeof window === "undefined")
428
+ return;
429
+ const unloadHandler = () => {
430
+ if (this.currentScrollState) {
431
+ const currentTime = Date.now();
432
+ const duration = currentTime - this.currentScrollState.entryTime;
433
+ if (duration > 100) {
434
+ const scrollData = {
435
+ pageUrl: window.location.href,
436
+ viewportSection: this.currentScrollState.viewportSection,
437
+ scrollDepthPx: this.currentScrollState.scrollDepthPx,
438
+ durationMs: duration,
439
+ entryTimestamp: this.currentScrollState.entryTime,
440
+ exitTimestamp: currentTime,
441
+ pageHeight: document.documentElement.scrollHeight,
442
+ viewportHeight: window.innerHeight
443
+ };
444
+ this.pendingScrolls.push(scrollData);
445
+ }
446
+ }
447
+ this.flushPendingEventsWithBeacon();
448
+ };
449
+ window.addEventListener("beforeunload", unloadHandler);
450
+ window.addEventListener("pagehide", unloadHandler);
451
+ }
452
+ /**
453
+ * Handle click event
454
+ */
455
+ handleClick(event) {
456
+ if (!this.tracker.hasConsent("analytics"))
457
+ return;
458
+ const element = event.target;
459
+ if (!element)
460
+ return;
461
+ const pageUrl = window.location.href;
462
+ const xpath = this.generateXPath(element);
463
+ const viewportX = Math.round(event.clientX);
464
+ const viewportY = Math.round(event.clientY);
465
+ const pageX = Math.round(event.pageX);
466
+ const pageY = Math.round(event.pageY);
467
+ const elementTag = element.tagName?.toLowerCase() || "unknown";
468
+ const elementText = element.textContent?.trim().substring(0, 100);
469
+ const clickData = {
470
+ pageUrl,
471
+ xpath,
472
+ viewportX,
473
+ viewportY,
474
+ pageX,
475
+ pageY,
476
+ elementTag,
477
+ elementText: elementText || void 0,
478
+ timestamp: Date.now()
479
+ };
480
+ const isNavigationLink = element instanceof HTMLAnchorElement && element.href;
481
+ if (isNavigationLink) {
482
+ this.tracker.trackSystemEvent("_grain_heatmap_click", {
483
+ page_url: clickData.pageUrl,
484
+ xpath: clickData.xpath,
485
+ viewport_x: clickData.viewportX,
486
+ viewport_y: clickData.viewportY,
487
+ page_x: clickData.pageX,
488
+ page_y: clickData.pageY,
489
+ element_tag: clickData.elementTag,
490
+ element_text: clickData.elementText,
491
+ timestamp: clickData.timestamp
492
+ }, { flush: true });
493
+ } else {
494
+ this.pendingClicks.push(clickData);
495
+ this.considerBatchFlush();
496
+ }
497
+ }
498
+ /**
499
+ * Handle scroll event
500
+ */
501
+ handleScroll() {
502
+ if (!this.tracker.hasConsent("analytics"))
503
+ return;
504
+ this.updateScrollState();
505
+ }
506
+ /**
507
+ * Update current scroll state
508
+ */
509
+ updateScrollState() {
510
+ if (typeof window === "undefined")
511
+ return;
512
+ if (!this.tracker.hasConsent("analytics"))
513
+ return;
514
+ const currentTime = Date.now();
515
+ const scrollY = window.scrollY || window.pageYOffset;
516
+ const viewportHeight = window.innerHeight;
517
+ const pageHeight = document.documentElement.scrollHeight;
518
+ const viewportSection = Math.floor(scrollY / viewportHeight);
519
+ if (this.currentScrollState && this.currentScrollState.viewportSection !== viewportSection) {
520
+ const duration = currentTime - this.currentScrollState.entryTime;
521
+ if (duration > 100) {
522
+ const scrollData = {
523
+ pageUrl: window.location.href,
524
+ viewportSection: this.currentScrollState.viewportSection,
525
+ scrollDepthPx: this.currentScrollState.scrollDepthPx,
526
+ durationMs: duration,
527
+ entryTimestamp: this.currentScrollState.entryTime,
528
+ exitTimestamp: currentTime,
529
+ pageHeight,
530
+ viewportHeight
531
+ };
532
+ this.pendingScrolls.push(scrollData);
533
+ }
534
+ const prevSectionKey = `viewport_section_${this.currentScrollState.viewportSection}`;
535
+ this.attentionQuality.resetSection(prevSectionKey);
536
+ }
537
+ if (!this.currentScrollState || this.currentScrollState.viewportSection !== viewportSection) {
538
+ this.currentScrollState = {
539
+ viewportSection,
540
+ entryTime: currentTime,
541
+ scrollDepthPx: scrollY
542
+ };
543
+ }
544
+ this.lastScrollPosition = scrollY;
545
+ this.lastScrollTime = currentTime;
546
+ this.considerBatchFlush();
547
+ }
548
+ /**
549
+ * Generate XPath for an element
550
+ */
551
+ generateXPath(element) {
552
+ if (!element)
553
+ return "";
554
+ if (element.id) {
555
+ return `//*[@id="${element.id}"]`;
556
+ }
557
+ const paths = [];
558
+ let currentElement = element;
559
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
560
+ let index = 0;
561
+ let sibling = currentElement;
562
+ while (sibling) {
563
+ sibling = sibling.previousElementSibling;
564
+ if (sibling && sibling.nodeName === currentElement.nodeName) {
565
+ index++;
566
+ }
567
+ }
568
+ const tagName = currentElement.nodeName.toLowerCase();
569
+ const pathIndex = index > 0 ? `[${index + 1}]` : "";
570
+ paths.unshift(`${tagName}${pathIndex}`);
571
+ currentElement = currentElement.parentElement;
572
+ }
573
+ return paths.length ? `/${paths.join("/")}` : "";
574
+ }
575
+ /**
576
+ * Consider flushing batched events
577
+ */
578
+ considerBatchFlush() {
579
+ const totalEvents = this.pendingClicks.length + this.pendingScrolls.length;
580
+ if (totalEvents >= this.options.maxBatchSize) {
581
+ this.flushPendingEvents();
582
+ return;
583
+ }
584
+ if (this.batchTimer === null && totalEvents > 0) {
585
+ this.batchTimer = window.setTimeout(() => {
586
+ this.flushPendingEvents();
587
+ this.batchTimer = null;
588
+ }, this.options.batchDelay);
589
+ }
590
+ }
591
+ /**
592
+ * Flush pending events
593
+ */
594
+ flushPendingEvents() {
595
+ if (this.isDestroyed)
596
+ return;
597
+ if (!this.tracker.hasConsent("analytics")) {
598
+ this.pendingClicks = [];
599
+ this.pendingScrolls = [];
600
+ return;
601
+ }
602
+ if (this.pendingClicks.length > 0) {
603
+ for (const clickData of this.pendingClicks) {
604
+ this.tracker.trackSystemEvent("_grain_heatmap_click", {
605
+ page_url: clickData.pageUrl,
606
+ xpath: clickData.xpath,
607
+ viewport_x: clickData.viewportX,
608
+ viewport_y: clickData.viewportY,
609
+ page_x: clickData.pageX,
610
+ page_y: clickData.pageY,
611
+ element_tag: clickData.elementTag,
612
+ element_text: clickData.elementText,
613
+ timestamp: clickData.timestamp
614
+ });
615
+ }
616
+ this.pendingClicks = [];
617
+ }
618
+ if (this.pendingScrolls.length > 0) {
619
+ for (const scrollData of this.pendingScrolls) {
620
+ this.tracker.trackSystemEvent("_grain_heatmap_scroll", {
621
+ page_url: scrollData.pageUrl,
622
+ viewport_section: scrollData.viewportSection,
623
+ scroll_depth_px: scrollData.scrollDepthPx,
624
+ duration_ms: scrollData.durationMs,
625
+ entry_timestamp: scrollData.entryTimestamp,
626
+ exit_timestamp: scrollData.exitTimestamp,
627
+ page_height: scrollData.pageHeight,
628
+ viewport_height: scrollData.viewportHeight
629
+ });
630
+ }
631
+ this.pendingScrolls = [];
632
+ }
633
+ if (this.batchTimer !== null) {
634
+ clearTimeout(this.batchTimer);
635
+ this.batchTimer = null;
636
+ }
637
+ }
638
+ /**
639
+ * Flush pending events with beacon (for page unload)
640
+ */
641
+ flushPendingEventsWithBeacon() {
642
+ if (!this.tracker.hasConsent("analytics")) {
643
+ this.pendingClicks = [];
644
+ this.pendingScrolls = [];
645
+ return;
646
+ }
647
+ if (this.pendingClicks.length > 0) {
648
+ for (const clickData of this.pendingClicks) {
649
+ this.tracker.trackSystemEvent("_grain_heatmap_click", {
650
+ page_url: clickData.pageUrl,
651
+ xpath: clickData.xpath,
652
+ viewport_x: clickData.viewportX,
653
+ viewport_y: clickData.viewportY,
654
+ page_x: clickData.pageX,
655
+ page_y: clickData.pageY,
656
+ element_tag: clickData.elementTag,
657
+ element_text: clickData.elementText,
658
+ timestamp: clickData.timestamp
659
+ }, { flush: true });
660
+ }
661
+ this.pendingClicks = [];
662
+ }
663
+ if (this.pendingScrolls.length > 0) {
664
+ for (const scrollData of this.pendingScrolls) {
665
+ this.tracker.trackSystemEvent("_grain_heatmap_scroll", {
666
+ page_url: scrollData.pageUrl,
667
+ viewport_section: scrollData.viewportSection,
668
+ scroll_depth_px: scrollData.scrollDepthPx,
669
+ duration_ms: scrollData.durationMs,
670
+ entry_timestamp: scrollData.entryTimestamp,
671
+ exit_timestamp: scrollData.exitTimestamp,
672
+ page_height: scrollData.pageHeight,
673
+ viewport_height: scrollData.viewportHeight
674
+ }, { flush: true });
675
+ }
676
+ this.pendingScrolls = [];
677
+ }
678
+ }
679
+ /**
680
+ * Log debug message
681
+ */
682
+ log(...args) {
683
+ if (this.options.debug) {
684
+ this.tracker.log("[Heatmap Tracking]", ...args);
685
+ }
686
+ }
687
+ /**
688
+ * Destroy the tracking manager
689
+ */
690
+ destroy() {
691
+ this.isDestroyed = true;
692
+ if (this.scrollDebounceTimer !== null) {
693
+ clearTimeout(this.scrollDebounceTimer);
694
+ this.scrollDebounceTimer = null;
695
+ }
696
+ if (this.batchTimer !== null) {
697
+ clearTimeout(this.batchTimer);
698
+ this.batchTimer = null;
699
+ }
700
+ if (this.scrollTrackingTimer !== null) {
701
+ clearInterval(this.scrollTrackingTimer);
702
+ this.scrollTrackingTimer = null;
703
+ }
704
+ if (this.periodicScrollTimer !== null) {
705
+ clearInterval(this.periodicScrollTimer);
706
+ this.periodicScrollTimer = null;
707
+ }
708
+ this.attentionQuality.destroy();
709
+ this.flushPendingEvents();
710
+ }
711
+ };
712
+ }
713
+ });
714
+
25
715
  // src/interaction-tracking.ts
26
716
  var interaction_tracking_exports = {};
27
717
  __export(interaction_tracking_exports, {
@@ -62,7 +752,7 @@ var Grain = (() => {
62
752
  attachAllListeners() {
63
753
  if (this.isDestroyed)
64
754
  return;
65
- this.log("Attaching interaction listeners for", this.interactions.length, "interactions");
755
+ this.log("Attaching interaction listeners");
66
756
  for (const interaction of this.interactions) {
67
757
  this.attachInteractionListener(interaction);
68
758
  }
@@ -92,7 +782,6 @@ var Grain = (() => {
92
782
  handlers.push({ event: "focus", handler: focusHandler });
93
783
  }
94
784
  this.attachedListeners.set(element, handlers);
95
- this.log("Attached listeners to element for:", interaction.eventName);
96
785
  }
97
786
  /**
98
787
  * Handle click event on interaction
@@ -116,17 +805,12 @@ var Grain = (() => {
116
805
  ...isNavigationLink && { href: element.href },
117
806
  timestamp: Date.now()
118
807
  };
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);
808
+ const result = this.tracker.track(interaction.eventName, eventProperties, { flush: true });
809
+ if (result instanceof Promise) {
810
+ result.catch((error) => {
811
+ this.log("Failed to track click:", error);
812
+ });
128
813
  }
129
- this.log("Tracked click interaction:", interaction.eventName);
130
814
  }
131
815
  /**
132
816
  * Handle focus event on interaction (for form fields)
@@ -147,7 +831,6 @@ var Grain = (() => {
147
831
  element_class: element.className || void 0,
148
832
  timestamp: Date.now()
149
833
  });
150
- this.log("Tracked focus interaction:", interaction.eventName);
151
834
  }
152
835
  /**
153
836
  * Find element by XPath selector
@@ -241,7 +924,6 @@ var Grain = (() => {
241
924
  element.removeEventListener(event, handler);
242
925
  });
243
926
  this.attachedListeners.delete(element);
244
- this.log("Detached listeners from element");
245
927
  }
246
928
  /**
247
929
  * Log debug messages
@@ -296,11 +978,12 @@ var Grain = (() => {
296
978
  __export(section_tracking_exports, {
297
979
  SectionTrackingManager: () => SectionTrackingManager
298
980
  });
299
- var DEFAULT_OPTIONS, SectionTrackingManager;
981
+ var DEFAULT_OPTIONS3, SectionTrackingManager;
300
982
  var init_section_tracking = __esm({
301
983
  "src/section-tracking.ts"() {
302
984
  "use strict";
303
- DEFAULT_OPTIONS = {
985
+ init_attention_quality();
986
+ DEFAULT_OPTIONS3 = {
304
987
  minDwellTime: 1e3,
305
988
  // 1 second minimum
306
989
  scrollVelocityThreshold: 500,
@@ -313,7 +996,6 @@ var Grain = (() => {
313
996
  debug: false
314
997
  };
315
998
  SectionTrackingManager = class {
316
- // 3 seconds
317
999
  constructor(tracker, sections, options = {}) {
318
1000
  this.isDestroyed = false;
319
1001
  // Tracking state
@@ -334,7 +1016,19 @@ var Grain = (() => {
334
1016
  this.SPLIT_DURATION = 3e3;
335
1017
  this.tracker = tracker;
336
1018
  this.sections = sections;
337
- this.options = { ...DEFAULT_OPTIONS, ...options };
1019
+ this.options = { ...DEFAULT_OPTIONS3, ...options };
1020
+ this.attentionQuality = new AttentionQualityManager(
1021
+ tracker.getActivityDetector(),
1022
+ {
1023
+ maxSectionDuration: 9e3,
1024
+ // 9 seconds
1025
+ minScrollDistance: 100,
1026
+ // 100 pixels
1027
+ idleThreshold: 3e4,
1028
+ // 30 seconds
1029
+ debug: this.options.debug
1030
+ }
1031
+ );
338
1032
  if (typeof window !== "undefined" && typeof document !== "undefined") {
339
1033
  if (document.readyState === "loading") {
340
1034
  document.addEventListener("DOMContentLoaded", () => this.initialize());
@@ -349,7 +1043,7 @@ var Grain = (() => {
349
1043
  initialize() {
350
1044
  if (this.isDestroyed)
351
1045
  return;
352
- this.log("Initializing section tracking for", this.sections.length, "sections");
1046
+ this.log("Initializing section tracking");
353
1047
  this.setupIntersectionObserver();
354
1048
  this.setupScrollListener();
355
1049
  this.initializeSections();
@@ -419,7 +1113,6 @@ var Grain = (() => {
419
1113
  if (this.intersectionObserver) {
420
1114
  this.intersectionObserver.observe(element);
421
1115
  }
422
- this.log("Section initialized and observed:", section.sectionName);
423
1116
  }
424
1117
  }
425
1118
  /**
@@ -449,7 +1142,6 @@ var Grain = (() => {
449
1142
  * Handle section entry (became visible)
450
1143
  */
451
1144
  handleSectionEntry(state) {
452
- this.log("Section entered view:", state.config.sectionName);
453
1145
  state.entryTime = Date.now();
454
1146
  state.entryScrollSpeed = this.scrollVelocity;
455
1147
  state.lastScrollPosition = window.scrollY;
@@ -470,6 +1162,21 @@ var Grain = (() => {
470
1162
  const now = Date.now();
471
1163
  const duration = now - state.entryTime;
472
1164
  if (duration >= this.options.minDwellTime) {
1165
+ const currentScrollY = window.scrollY;
1166
+ const attentionCheck = this.attentionQuality.shouldTrackSection(
1167
+ state.config.sectionName,
1168
+ currentScrollY
1169
+ );
1170
+ if (attentionCheck.resetAttention) {
1171
+ this.log(`Section "${state.config.sectionName}": Attention reset, restarting timer`);
1172
+ state.entryTime = now;
1173
+ state.entryScrollSpeed = this.scrollVelocity;
1174
+ return;
1175
+ }
1176
+ if (!attentionCheck.shouldTrack) {
1177
+ this.log(`Section "${state.config.sectionName}": Tracking paused - ${attentionCheck.reason}`);
1178
+ return;
1179
+ }
473
1180
  const viewData = {
474
1181
  sectionName: state.config.sectionName,
475
1182
  sectionType: state.config.sectionType,
@@ -500,7 +1207,7 @@ var Grain = (() => {
500
1207
  is_split: true
501
1208
  // Flag to indicate this is a periodic split, not final exit
502
1209
  });
503
- this.log("Tracked periodic section view split:", state.config.sectionName, "duration:", duration);
1210
+ this.attentionQuality.updateSectionDuration(state.config.sectionName, duration);
504
1211
  state.entryTime = now;
505
1212
  state.entryScrollSpeed = this.scrollVelocity;
506
1213
  }
@@ -522,8 +1229,8 @@ var Grain = (() => {
522
1229
  * Handle section exit (became invisible)
523
1230
  */
524
1231
  handleSectionExit(state) {
525
- this.log("Section exited view:", state.config.sectionName);
526
1232
  this.stopPeriodicTracking(state.config.sectionName);
1233
+ this.attentionQuality.resetSection(state.config.sectionName);
527
1234
  if (state.entryTime === null)
528
1235
  return;
529
1236
  state.exitTime = Date.now();
@@ -604,7 +1311,6 @@ var Grain = (() => {
604
1311
  */
605
1312
  queueSectionView(viewData) {
606
1313
  this.pendingEvents.push(viewData);
607
- this.log("Queued section view:", viewData.sectionName, "duration:", viewData.duration);
608
1314
  if (this.batchTimer === null) {
609
1315
  this.batchTimer = window.setTimeout(() => {
610
1316
  this.flushPendingEvents();
@@ -621,7 +1327,6 @@ var Grain = (() => {
621
1327
  this.pendingEvents = [];
622
1328
  return;
623
1329
  }
624
- this.log("Flushing", this.pendingEvents.length, "section view events");
625
1330
  for (const viewData of this.pendingEvents) {
626
1331
  this.tracker.trackSystemEvent("_grain_section_view", {
627
1332
  section_name: viewData.sectionName,
@@ -722,6 +1427,7 @@ var Grain = (() => {
722
1427
  this.intersectionObserver.disconnect();
723
1428
  this.intersectionObserver = null;
724
1429
  }
1430
+ this.attentionQuality.destroy();
725
1431
  this.sectionStates.clear();
726
1432
  this.xpathCache.clear();
727
1433
  this.pendingEvents = [];
@@ -782,7 +1488,6 @@ var Grain = (() => {
782
1488
  this.saveConsentState();
783
1489
  }
784
1490
  } catch (error) {
785
- console.error("[Grain Consent] Failed to load consent state:", error);
786
1491
  }
787
1492
  }
788
1493
  /**
@@ -794,7 +1499,6 @@ var Grain = (() => {
794
1499
  try {
795
1500
  localStorage.setItem(this.storageKey, JSON.stringify(this.consentState));
796
1501
  } catch (error) {
797
- console.error("[Grain Consent] Failed to save consent state:", error);
798
1502
  }
799
1503
  }
800
1504
  /**
@@ -895,7 +1599,6 @@ var Grain = (() => {
895
1599
  try {
896
1600
  listener(this.consentState);
897
1601
  } catch (error) {
898
- console.error("[Grain Consent] Listener error:", error);
899
1602
  }
900
1603
  });
901
1604
  }
@@ -909,7 +1612,6 @@ var Grain = (() => {
909
1612
  localStorage.removeItem(this.storageKey);
910
1613
  this.consentState = null;
911
1614
  } catch (error) {
912
- console.error("[Grain Consent] Failed to clear consent:", error);
913
1615
  }
914
1616
  }
915
1617
  };
@@ -1089,7 +1791,6 @@ var Grain = (() => {
1089
1791
  try {
1090
1792
  listener();
1091
1793
  } catch (error) {
1092
- console.error("[Activity Detector] Listener error:", error);
1093
1794
  }
1094
1795
  }
1095
1796
  }
@@ -1190,9 +1891,6 @@ var Grain = (() => {
1190
1891
  }
1191
1892
  this.tracker.trackSystemEvent("_grain_heartbeat", properties);
1192
1893
  this.lastHeartbeatTime = now;
1193
- if (this.config.debug) {
1194
- console.log("[Heartbeat] Sent heartbeat:", properties);
1195
- }
1196
1894
  }
1197
1895
  /**
1198
1896
  * Destroy the heartbeat manager
@@ -1205,9 +1903,6 @@ var Grain = (() => {
1205
1903
  this.heartbeatTimer = null;
1206
1904
  }
1207
1905
  this.isDestroyed = true;
1208
- if (this.config.debug) {
1209
- console.log("[Heartbeat] Destroyed");
1210
- }
1211
1906
  }
1212
1907
  };
1213
1908
 
@@ -4408,9 +5103,6 @@ var Grain = (() => {
4408
5103
  properties.viewport = `${window.innerWidth}x${window.innerHeight}`;
4409
5104
  }
4410
5105
  this.tracker.trackSystemEvent("page_view", properties);
4411
- if (this.config.debug) {
4412
- console.log("[Page Tracking] Tracked page view:", properties);
4413
- }
4414
5106
  }
4415
5107
  /**
4416
5108
  * Extract domain from URL
@@ -4533,9 +5225,6 @@ var Grain = (() => {
4533
5225
  }
4534
5226
  }
4535
5227
  this.tracker.trackSystemEvent("page_view", baseProperties);
4536
- if (this.config.debug) {
4537
- console.log("[Page Tracking] Manually tracked page:", baseProperties);
4538
- }
4539
5228
  }
4540
5229
  /**
4541
5230
  * Get page view count for current session
@@ -4562,9 +5251,6 @@ var Grain = (() => {
4562
5251
  window.removeEventListener("hashchange", this.handleHashChange);
4563
5252
  }
4564
5253
  this.isDestroyed = true;
4565
- if (this.config.debug) {
4566
- console.log("[Page Tracking] Destroyed");
4567
- }
4568
5254
  }
4569
5255
  };
4570
5256
 
@@ -4592,6 +5278,7 @@ var Grain = (() => {
4592
5278
  // Auto-tracking properties
4593
5279
  this.interactionTrackingManager = null;
4594
5280
  this.sectionTrackingManager = null;
5281
+ this.heatmapTrackingManager = null;
4595
5282
  // Session tracking
4596
5283
  this.sessionStartTime = Date.now();
4597
5284
  this.sessionEventCount = 0;
@@ -4627,6 +5314,8 @@ var Grain = (() => {
4627
5314
  // 5 minutes
4628
5315
  enableAutoPageView: true,
4629
5316
  stripQueryParams: true,
5317
+ // Heatmap Tracking defaults
5318
+ enableHeatmapTracking: true,
4630
5319
  ...config,
4631
5320
  tenantId: config.tenantId
4632
5321
  };
@@ -4649,6 +5338,9 @@ var Grain = (() => {
4649
5338
  if (typeof window !== "undefined") {
4650
5339
  this.initializeAutomaticTracking();
4651
5340
  this.trackSessionStart();
5341
+ if (this.config.enableHeatmapTracking) {
5342
+ this.initializeHeatmapTracking();
5343
+ }
4652
5344
  }
4653
5345
  this.consentManager.addListener((state) => {
4654
5346
  if (state.granted) {
@@ -4872,6 +5564,8 @@ var Grain = (() => {
4872
5564
  * Log formatted error gracefully
4873
5565
  */
4874
5566
  logError(formattedError) {
5567
+ if (!this.config.debug)
5568
+ return;
4875
5569
  const { code, message, digest, timestamp, context } = formattedError;
4876
5570
  const errorOutput = {
4877
5571
  "\u{1F6A8} Grain Analytics Error": {
@@ -4889,9 +5583,7 @@ var Grain = (() => {
4889
5583
  }
4890
5584
  };
4891
5585
  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
- }
5586
+ console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
4895
5587
  }
4896
5588
  /**
4897
5589
  * Safely execute a function with error handling
@@ -4990,7 +5682,6 @@ var Grain = (() => {
4990
5682
  try {
4991
5683
  const headers = await this.getAuthHeaders();
4992
5684
  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
5685
  const response = await fetch(url, {
4995
5686
  method: "POST",
4996
5687
  headers,
@@ -5055,7 +5746,6 @@ var Grain = (() => {
5055
5746
  body,
5056
5747
  keepalive: true
5057
5748
  });
5058
- this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
5059
5749
  } catch (error) {
5060
5750
  const formattedError = this.formatError(error, "sendEventsWithBeacon", events);
5061
5751
  this.logError(formattedError);
@@ -5121,7 +5811,6 @@ var Grain = (() => {
5121
5811
  debug: this.config.debug
5122
5812
  }
5123
5813
  );
5124
- this.log("Heartbeat tracking initialized");
5125
5814
  } catch (error) {
5126
5815
  this.log("Failed to initialize heartbeat tracking:", error);
5127
5816
  }
@@ -5136,19 +5825,48 @@ var Grain = (() => {
5136
5825
  tenantId: this.config.tenantId
5137
5826
  }
5138
5827
  );
5139
- this.log("Auto page view tracking initialized");
5140
5828
  } catch (error) {
5141
5829
  this.log("Failed to initialize page view tracking:", error);
5142
5830
  }
5143
5831
  }
5144
5832
  this.initializeAutoTracking();
5145
5833
  }
5834
+ /**
5835
+ * Initialize heatmap tracking
5836
+ */
5837
+ initializeHeatmapTracking() {
5838
+ if (typeof window === "undefined")
5839
+ return;
5840
+ try {
5841
+ this.log("Initializing heatmap tracking");
5842
+ Promise.resolve().then(() => (init_heatmap_tracking(), heatmap_tracking_exports)).then(({ HeatmapTrackingManager: HeatmapTrackingManager2 }) => {
5843
+ try {
5844
+ this.heatmapTrackingManager = new HeatmapTrackingManager2(
5845
+ this,
5846
+ {
5847
+ scrollDebounceDelay: 100,
5848
+ batchDelay: 2e3,
5849
+ maxBatchSize: 20,
5850
+ debug: this.config.debug
5851
+ }
5852
+ );
5853
+ this.log("Heatmap tracking initialized");
5854
+ } catch (error) {
5855
+ this.log("Failed to initialize heatmap tracking:", error);
5856
+ }
5857
+ }).catch((error) => {
5858
+ this.log("Failed to load heatmap tracking module:", error);
5859
+ });
5860
+ } catch (error) {
5861
+ this.log("Failed to initialize heatmap tracking:", error);
5862
+ }
5863
+ }
5146
5864
  /**
5147
5865
  * Initialize auto-tracking (interactions and sections)
5148
5866
  */
5149
5867
  async initializeAutoTracking() {
5150
5868
  try {
5151
- this.log("Initializing auto-tracking...");
5869
+ this.log("Initializing auto-tracking");
5152
5870
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
5153
5871
  const currentUrl = typeof window !== "undefined" ? window.location.href : "";
5154
5872
  const request = {
@@ -5160,23 +5878,19 @@ var Grain = (() => {
5160
5878
  };
5161
5879
  const headers = await this.getAuthHeaders();
5162
5880
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
5163
- this.log("Fetching auto-tracking config from:", url);
5164
5881
  const response = await fetch(url, {
5165
5882
  method: "POST",
5166
5883
  headers,
5167
5884
  body: JSON.stringify(request)
5168
5885
  });
5169
5886
  if (!response.ok) {
5170
- this.log("Failed to fetch auto-tracking config:", response.status, response.statusText);
5887
+ this.log("Failed to fetch auto-tracking config:", response.status);
5171
5888
  return;
5172
5889
  }
5173
5890
  const configResponse = await response.json();
5174
- this.log("Received config response:", configResponse);
5175
5891
  if (configResponse.autoTrackingConfig) {
5176
- this.log("Auto-tracking config found:", configResponse.autoTrackingConfig);
5892
+ this.log("Auto-tracking config loaded");
5177
5893
  this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
5178
- } else {
5179
- this.log("No auto-tracking config in response");
5180
5894
  }
5181
5895
  } catch (error) {
5182
5896
  this.log("Failed to initialize auto-tracking:", error);
@@ -5186,9 +5900,9 @@ var Grain = (() => {
5186
5900
  * Setup auto-tracking managers
5187
5901
  */
5188
5902
  setupAutoTrackingManagers(config) {
5189
- this.log("Setting up auto-tracking managers...", config);
5903
+ this.log("Setting up auto-tracking managers");
5190
5904
  if (config.interactions && config.interactions.length > 0) {
5191
- this.log("Loading interaction tracking module for", config.interactions.length, "interactions");
5905
+ this.log("Loading interaction tracking:", config.interactions.length, "interactions");
5192
5906
  Promise.resolve().then(() => (init_interaction_tracking(), interaction_tracking_exports)).then(({ InteractionTrackingManager: InteractionTrackingManager2 }) => {
5193
5907
  try {
5194
5908
  this.interactionTrackingManager = new InteractionTrackingManager2(
@@ -5200,18 +5914,16 @@ var Grain = (() => {
5200
5914
  mutationDebounceDelay: 500
5201
5915
  }
5202
5916
  );
5203
- this.log("\u2705 Interaction tracking initialized successfully with", config.interactions.length, "interactions");
5917
+ this.log("Interaction tracking initialized");
5204
5918
  } catch (error) {
5205
- this.log("\u274C Failed to initialize interaction tracking:", error);
5919
+ this.log("Failed to initialize interaction tracking:", error);
5206
5920
  }
5207
5921
  }).catch((error) => {
5208
- this.log("\u274C Failed to load interaction tracking module:", error);
5922
+ this.log("Failed to load interaction tracking module:", error);
5209
5923
  });
5210
- } else {
5211
- this.log("No interactions configured for auto-tracking");
5212
5924
  }
5213
5925
  if (config.sections && config.sections.length > 0) {
5214
- this.log("Loading section tracking module for", config.sections.length, "sections");
5926
+ this.log("Loading section tracking:", config.sections.length, "sections");
5215
5927
  Promise.resolve().then(() => (init_section_tracking(), section_tracking_exports)).then(({ SectionTrackingManager: SectionTrackingManager2 }) => {
5216
5928
  try {
5217
5929
  this.sectionTrackingManager = new SectionTrackingManager2(
@@ -5226,15 +5938,13 @@ var Grain = (() => {
5226
5938
  debug: this.config.debug
5227
5939
  }
5228
5940
  );
5229
- this.log("\u2705 Section tracking initialized successfully with", config.sections.length, "sections");
5941
+ this.log("Section tracking initialized");
5230
5942
  } catch (error) {
5231
- this.log("\u274C Failed to initialize section tracking:", error);
5943
+ this.log("Failed to initialize section tracking:", error);
5232
5944
  }
5233
5945
  }).catch((error) => {
5234
- this.log("\u274C Failed to load section tracking module:", error);
5946
+ this.log("Failed to load section tracking module:", error);
5235
5947
  });
5236
- } else {
5237
- this.log("No sections configured for auto-tracking");
5238
5948
  }
5239
5949
  }
5240
5950
  /**
@@ -5290,7 +6000,7 @@ var Grain = (() => {
5290
6000
  properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
5291
6001
  }
5292
6002
  this.trackSystemEvent("_grain_session_start", properties);
5293
- this.log("Session started:", properties);
6003
+ this.log("Session started");
5294
6004
  }
5295
6005
  /**
5296
6006
  * Track session end event
@@ -5315,7 +6025,7 @@ var Grain = (() => {
5315
6025
  properties.page_count = pageCount;
5316
6026
  }
5317
6027
  this.trackSystemEvent("_grain_session_end", properties);
5318
- this.log("Session ended:", properties);
6028
+ this.log("Session ended");
5319
6029
  }
5320
6030
  /**
5321
6031
  * Detect browser name
@@ -5412,7 +6122,7 @@ var Grain = (() => {
5412
6122
  };
5413
6123
  this.eventQueue.push(event);
5414
6124
  this.eventCountSinceLastHeartbeat++;
5415
- this.log(`Queued system event: ${eventName}`, properties);
6125
+ this.log(`Queued system event: ${eventName}`);
5416
6126
  if (this.eventQueue.length >= this.config.batchSize) {
5417
6127
  this.flush().catch((error) => {
5418
6128
  const formattedError = this.formatError(error, "flush system event");
@@ -5447,6 +6157,15 @@ var Grain = (() => {
5447
6157
  resetEventCountSinceLastHeartbeat() {
5448
6158
  this.eventCountSinceLastHeartbeat = 0;
5449
6159
  }
6160
+ /**
6161
+ * Get the activity detector (for internal use by tracking managers)
6162
+ */
6163
+ getActivityDetector() {
6164
+ if (!this.activityDetector) {
6165
+ throw new Error("Activity detector not initialized");
6166
+ }
6167
+ return this.activityDetector;
6168
+ }
5450
6169
  /**
5451
6170
  * Get the effective user ID (public method)
5452
6171
  */
@@ -5502,7 +6221,7 @@ var Grain = (() => {
5502
6221
  this.eventQueue.push(formattedEvent);
5503
6222
  this.eventCountSinceLastHeartbeat++;
5504
6223
  this.sessionEventCount++;
5505
- this.log(`Queued event: ${event.eventName}`, event.properties);
6224
+ this.log(`Queued event: ${event.eventName}`);
5506
6225
  if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
5507
6226
  await this.flush();
5508
6227
  }
@@ -5701,7 +6420,6 @@ var Grain = (() => {
5701
6420
  try {
5702
6421
  const headers = await this.getAuthHeaders();
5703
6422
  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
6423
  const response = await fetch(url, {
5706
6424
  method: "POST",
5707
6425
  headers,
@@ -5927,7 +6645,6 @@ var Grain = (() => {
5927
6645
  try {
5928
6646
  const headers = await this.getAuthHeaders();
5929
6647
  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
6648
  const response = await fetch(url, {
5932
6649
  method: "POST",
5933
6650
  headers,
@@ -5954,7 +6671,7 @@ var Grain = (() => {
5954
6671
  if (configResponse.configurations) {
5955
6672
  this.updateConfigCache(configResponse, userId);
5956
6673
  }
5957
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
6674
+ this.log("Successfully fetched configurations");
5958
6675
  return configResponse;
5959
6676
  } catch (error) {
5960
6677
  lastError = error;
@@ -6203,6 +6920,10 @@ var Grain = (() => {
6203
6920
  this.sectionTrackingManager.destroy();
6204
6921
  this.sectionTrackingManager = null;
6205
6922
  }
6923
+ if (this.heatmapTrackingManager) {
6924
+ this.heatmapTrackingManager.destroy();
6925
+ this.heatmapTrackingManager = null;
6926
+ }
6206
6927
  if (this.eventQueue.length > 0) {
6207
6928
  const eventsToSend = [...this.eventQueue];
6208
6929
  this.eventQueue = [];