@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
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Type definitions for Heatmap Tracking
3
+ */
4
+ export interface HeatmapClickData {
5
+ pageUrl: string;
6
+ xpath: string;
7
+ viewportX: number;
8
+ viewportY: number;
9
+ pageX: number;
10
+ pageY: number;
11
+ elementTag: string;
12
+ elementText?: string;
13
+ timestamp: number;
14
+ }
15
+ export interface HeatmapScrollData {
16
+ pageUrl: string;
17
+ viewportSection: number;
18
+ scrollDepthPx: number;
19
+ durationMs: number;
20
+ entryTimestamp: number;
21
+ exitTimestamp: number;
22
+ pageHeight: number;
23
+ viewportHeight: number;
24
+ }
25
+ export interface HeatmapTrackingOptions {
26
+ scrollDebounceDelay: number;
27
+ batchDelay: number;
28
+ maxBatchSize: number;
29
+ debug?: boolean;
30
+ }
31
+ export interface HeatmapScrollState {
32
+ viewportSection: number;
33
+ entryTime: number;
34
+ scrollDepthPx: number;
35
+ }
36
+ export interface HeatmapTrackingState {
37
+ currentScrollState: HeatmapScrollState | null;
38
+ pendingClicks: HeatmapClickData[];
39
+ pendingScrolls: HeatmapScrollData[];
40
+ }
41
+ //# sourceMappingURL=heatmap-tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatmap-tracking.d.ts","sourceRoot":"","sources":["../../../src/types/heatmap-tracking.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,kBAAkB,EAAE,kBAAkB,GAAG,IAAI,CAAC;IAC9C,aAAa,EAAE,gBAAgB,EAAE,CAAC;IAClC,cAAc,EAAE,iBAAiB,EAAE,CAAC;CACrC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Type definitions for Heatmap Tracking
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=heatmap-tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatmap-tracking.js","sourceRoot":"","sources":["../../../src/types/heatmap-tracking.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;IACvB,cAAc,IAAI,MAAM,GAAG,IAAI,CAAC;IAChC,+BAA+B,IAAI,MAAM,CAAC;IAC1C,iCAAiC,IAAI,IAAI,CAAC;CAC3C;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,wBAAwB,CAAS;gBAGvC,OAAO,EAAE,gBAAgB,EACzB,gBAAgB,EAAE,gBAAgB,EAClC,MAAM,EAAE,eAAe;IAezB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAkB7B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAyB7B;;OAEG;IACH,OAAO,CAAC,aAAa;IA2CrB;;OAEG;IACH,OAAO,IAAI,IAAI;CAchB"}
1
+ {"version":3,"file":"heartbeat.d.ts","sourceRoot":"","sources":["../src/heartbeat.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;IACvB,cAAc,IAAI,MAAM,GAAG,IAAI,CAAC;IAChC,+BAA+B,IAAI,MAAM,CAAC;IAC1C,iCAAiC,IAAI,IAAI,CAAC;CAC3C;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAAS;IAClC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,wBAAwB,CAAS;gBAGvC,OAAO,EAAE,gBAAgB,EACzB,gBAAgB,EAAE,gBAAgB,EAClC,MAAM,EAAE,eAAe;IAezB;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAkB7B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAyB7B;;OAEG;IACH,OAAO,CAAC,aAAa;IAuCrB;;OAEG;IACH,OAAO,IAAI,IAAI;CAUhB"}
package/dist/heartbeat.js CHANGED
@@ -96,9 +96,6 @@ class HeartbeatManager {
96
96
  // Track the heartbeat event
97
97
  this.tracker.trackSystemEvent('_grain_heartbeat', properties);
98
98
  this.lastHeartbeatTime = now;
99
- if (this.config.debug) {
100
- console.log('[Heartbeat] Sent heartbeat:', properties);
101
- }
102
99
  }
103
100
  /**
104
101
  * Destroy the heartbeat manager
@@ -111,9 +108,6 @@ class HeartbeatManager {
111
108
  this.heartbeatTimer = null;
112
109
  }
113
110
  this.isDestroyed = true;
114
- if (this.config.debug) {
115
- console.log('[Heartbeat] Destroyed');
116
- }
117
111
  }
118
112
  }
119
113
  exports.HeartbeatManager = HeartbeatManager;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Heatmap Tracking Manager for Grain Analytics
3
+ * Tracks click interactions and scroll depth across all pages
4
+ */
5
+ import type { HeatmapTrackingOptions } from './types/heatmap-tracking';
6
+ export interface SendEventOptions {
7
+ flush?: boolean;
8
+ }
9
+ export interface HeatmapTracker {
10
+ trackSystemEvent(eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions): void | Promise<void>;
11
+ hasConsent(category: 'analytics' | 'marketing' | 'functional'): boolean;
12
+ log(...args: unknown[]): void;
13
+ }
14
+ export declare class HeatmapTrackingManager {
15
+ private tracker;
16
+ private options;
17
+ private isDestroyed;
18
+ private currentScrollState;
19
+ private pendingClicks;
20
+ private pendingScrolls;
21
+ private scrollDebounceTimer;
22
+ private batchTimer;
23
+ private scrollTrackingTimer;
24
+ private periodicScrollTimer;
25
+ private lastScrollPosition;
26
+ private lastScrollTime;
27
+ private readonly SPLIT_DURATION;
28
+ constructor(tracker: HeatmapTracker, options?: Partial<HeatmapTrackingOptions>);
29
+ /**
30
+ * Initialize heatmap tracking
31
+ */
32
+ private initialize;
33
+ /**
34
+ * Setup click event tracking
35
+ */
36
+ private setupClickTracking;
37
+ /**
38
+ * Setup scroll event tracking
39
+ */
40
+ private setupScrollTracking;
41
+ /**
42
+ * Start periodic scroll state tracking
43
+ */
44
+ private startScrollTracking;
45
+ /**
46
+ * Start periodic scroll tracking (sends events every 3 seconds)
47
+ */
48
+ private startPeriodicScrollTracking;
49
+ /**
50
+ * Setup page unload handler to beacon remaining data
51
+ */
52
+ private setupUnloadHandler;
53
+ /**
54
+ * Handle click event
55
+ */
56
+ private handleClick;
57
+ /**
58
+ * Handle scroll event
59
+ */
60
+ private handleScroll;
61
+ /**
62
+ * Update current scroll state
63
+ */
64
+ private updateScrollState;
65
+ /**
66
+ * Generate XPath for an element
67
+ */
68
+ private generateXPath;
69
+ /**
70
+ * Consider flushing batched events
71
+ */
72
+ private considerBatchFlush;
73
+ /**
74
+ * Flush pending events
75
+ */
76
+ private flushPendingEvents;
77
+ /**
78
+ * Flush pending events with beacon (for page unload)
79
+ */
80
+ private flushPendingEventsWithBeacon;
81
+ /**
82
+ * Log debug message
83
+ */
84
+ private log;
85
+ /**
86
+ * Destroy the tracking manager
87
+ */
88
+ destroy(): void;
89
+ }
90
+ //# sourceMappingURL=heatmap-tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heatmap-tracking.d.ts","sourceRoot":"","sources":["../src/heatmap-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAGV,sBAAsB,EAEvB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5H,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AASD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,WAAW,CAAS;IAG5B,OAAO,CAAC,kBAAkB,CAAmC;IAC7D,OAAO,CAAC,aAAa,CAA0B;IAC/C,OAAO,CAAC,cAAc,CAA2B;IAGjD,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,mBAAmB,CAAuB;IAGlD,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;gBAGrC,OAAO,EAAE,cAAc,EACvB,OAAO,GAAE,OAAO,CAAC,sBAAsB,CAAM;IAe/C;;OAEG;IACH,OAAO,CAAC,UAAU;IAkBlB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAa1B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAiB3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAgB3B;;OAEG;IACH,OAAO,CAAC,2BAA2B;IA8CnC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkC1B;;OAEG;IACH,OAAO,CAAC,WAAW;IAwDnB;;OAEG;IACH,OAAO,CAAC,YAAY;IAKpB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAiDzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAiCrB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkB1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAqD1B;;OAEG;IACH,OAAO,CAAC,4BAA4B;IA6CpC;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,OAAO,IAAI,IAAI;CA2BhB"}
@@ -0,0 +1,465 @@
1
+ "use strict";
2
+ /**
3
+ * Heatmap Tracking Manager for Grain Analytics
4
+ * Tracks click interactions and scroll depth across all pages
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.HeatmapTrackingManager = void 0;
8
+ const DEFAULT_OPTIONS = {
9
+ scrollDebounceDelay: 100,
10
+ batchDelay: 2000,
11
+ maxBatchSize: 20,
12
+ debug: false,
13
+ };
14
+ class HeatmapTrackingManager {
15
+ constructor(tracker, options = {}) {
16
+ this.isDestroyed = false;
17
+ // Tracking state
18
+ this.currentScrollState = null;
19
+ this.pendingClicks = [];
20
+ this.pendingScrolls = [];
21
+ // Timers
22
+ this.scrollDebounceTimer = null;
23
+ this.batchTimer = null;
24
+ this.scrollTrackingTimer = null;
25
+ this.periodicScrollTimer = null;
26
+ // Scroll tracking
27
+ this.lastScrollPosition = 0;
28
+ this.lastScrollTime = Date.now();
29
+ this.SPLIT_DURATION = 3000; // 3 seconds - same as section tracking
30
+ this.tracker = tracker;
31
+ this.options = { ...DEFAULT_OPTIONS, ...options };
32
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
33
+ // Initialize after DOM is ready
34
+ if (document.readyState === 'loading') {
35
+ document.addEventListener('DOMContentLoaded', () => this.initialize());
36
+ }
37
+ else {
38
+ setTimeout(() => this.initialize(), 0);
39
+ }
40
+ }
41
+ }
42
+ /**
43
+ * Initialize heatmap tracking
44
+ */
45
+ initialize() {
46
+ if (this.isDestroyed)
47
+ return;
48
+ this.log('Initializing heatmap tracking');
49
+ // Setup click tracking
50
+ this.setupClickTracking();
51
+ // Setup scroll tracking
52
+ this.setupScrollTracking();
53
+ // Start periodic scroll state tracking
54
+ this.startScrollTracking();
55
+ // Setup page unload handler for beaconing
56
+ this.setupUnloadHandler();
57
+ }
58
+ /**
59
+ * Setup click event tracking
60
+ */
61
+ setupClickTracking() {
62
+ if (typeof document === 'undefined')
63
+ return;
64
+ const clickHandler = (event) => {
65
+ if (this.isDestroyed)
66
+ return;
67
+ if (!this.tracker.hasConsent('analytics'))
68
+ return;
69
+ this.handleClick(event);
70
+ };
71
+ document.addEventListener('click', clickHandler, { passive: true, capture: true });
72
+ }
73
+ /**
74
+ * Setup scroll event tracking
75
+ */
76
+ setupScrollTracking() {
77
+ if (typeof window === 'undefined')
78
+ return;
79
+ const scrollHandler = () => {
80
+ if (this.scrollDebounceTimer !== null) {
81
+ clearTimeout(this.scrollDebounceTimer);
82
+ }
83
+ this.scrollDebounceTimer = window.setTimeout(() => {
84
+ this.handleScroll();
85
+ this.scrollDebounceTimer = null;
86
+ }, this.options.scrollDebounceDelay);
87
+ };
88
+ window.addEventListener('scroll', scrollHandler, { passive: true });
89
+ }
90
+ /**
91
+ * Start periodic scroll state tracking
92
+ */
93
+ startScrollTracking() {
94
+ if (typeof window === 'undefined')
95
+ return;
96
+ // Track initial scroll position
97
+ this.updateScrollState();
98
+ // Update scroll state every 500ms (for detecting section changes)
99
+ this.scrollTrackingTimer = window.setInterval(() => {
100
+ if (this.isDestroyed)
101
+ return;
102
+ this.updateScrollState();
103
+ }, 500);
104
+ // Start periodic 3-second scroll duration tracking
105
+ this.startPeriodicScrollTracking();
106
+ }
107
+ /**
108
+ * Start periodic scroll tracking (sends events every 3 seconds)
109
+ */
110
+ startPeriodicScrollTracking() {
111
+ if (typeof window === 'undefined')
112
+ return;
113
+ this.periodicScrollTimer = window.setInterval(() => {
114
+ if (this.isDestroyed || !this.currentScrollState)
115
+ return;
116
+ if (!this.tracker.hasConsent('analytics'))
117
+ return;
118
+ const currentTime = Date.now();
119
+ const duration = currentTime - this.currentScrollState.entryTime;
120
+ // Only send if meaningful duration (> 1 second)
121
+ if (duration > 1000) {
122
+ const scrollY = window.scrollY || window.pageYOffset;
123
+ const viewportHeight = window.innerHeight;
124
+ const pageHeight = document.documentElement.scrollHeight;
125
+ const scrollData = {
126
+ pageUrl: window.location.href,
127
+ viewportSection: this.currentScrollState.viewportSection,
128
+ scrollDepthPx: scrollY,
129
+ durationMs: duration,
130
+ entryTimestamp: this.currentScrollState.entryTime,
131
+ exitTimestamp: currentTime,
132
+ pageHeight,
133
+ viewportHeight,
134
+ };
135
+ // Send immediately using beacon to ensure delivery
136
+ this.tracker.trackSystemEvent('_grain_heatmap_scroll', {
137
+ page_url: scrollData.pageUrl,
138
+ viewport_section: scrollData.viewportSection,
139
+ scroll_depth_px: scrollData.scrollDepthPx,
140
+ duration_ms: scrollData.durationMs,
141
+ entry_timestamp: scrollData.entryTimestamp,
142
+ exit_timestamp: scrollData.exitTimestamp,
143
+ page_height: scrollData.pageHeight,
144
+ viewport_height: scrollData.viewportHeight,
145
+ is_split: true, // Flag to indicate periodic tracking, not final exit
146
+ }, { flush: true });
147
+ // Reset entry time for next period
148
+ this.currentScrollState.entryTime = currentTime;
149
+ }
150
+ }, this.SPLIT_DURATION);
151
+ }
152
+ /**
153
+ * Setup page unload handler to beacon remaining data
154
+ */
155
+ setupUnloadHandler() {
156
+ if (typeof window === 'undefined')
157
+ return;
158
+ const unloadHandler = () => {
159
+ // Finalize current scroll state
160
+ if (this.currentScrollState) {
161
+ const currentTime = Date.now();
162
+ const duration = currentTime - this.currentScrollState.entryTime;
163
+ if (duration > 100) {
164
+ const scrollData = {
165
+ pageUrl: window.location.href,
166
+ viewportSection: this.currentScrollState.viewportSection,
167
+ scrollDepthPx: this.currentScrollState.scrollDepthPx,
168
+ durationMs: duration,
169
+ entryTimestamp: this.currentScrollState.entryTime,
170
+ exitTimestamp: currentTime,
171
+ pageHeight: document.documentElement.scrollHeight,
172
+ viewportHeight: window.innerHeight,
173
+ };
174
+ this.pendingScrolls.push(scrollData);
175
+ }
176
+ }
177
+ // Flush all pending events with beacon
178
+ this.flushPendingEventsWithBeacon();
179
+ };
180
+ // Use both events for better compatibility
181
+ window.addEventListener('beforeunload', unloadHandler);
182
+ window.addEventListener('pagehide', unloadHandler);
183
+ }
184
+ /**
185
+ * Handle click event
186
+ */
187
+ handleClick(event) {
188
+ if (!this.tracker.hasConsent('analytics'))
189
+ return;
190
+ const element = event.target;
191
+ if (!element)
192
+ return;
193
+ const pageUrl = window.location.href;
194
+ const xpath = this.generateXPath(element);
195
+ // Get viewport coordinates
196
+ const viewportX = Math.round(event.clientX);
197
+ const viewportY = Math.round(event.clientY);
198
+ // Get page coordinates (including scroll offset)
199
+ const pageX = Math.round(event.pageX);
200
+ const pageY = Math.round(event.pageY);
201
+ const elementTag = element.tagName?.toLowerCase() || 'unknown';
202
+ const elementText = element.textContent?.trim().substring(0, 100);
203
+ const clickData = {
204
+ pageUrl,
205
+ xpath,
206
+ viewportX,
207
+ viewportY,
208
+ pageX,
209
+ pageY,
210
+ elementTag,
211
+ elementText: elementText || undefined,
212
+ timestamp: Date.now(),
213
+ };
214
+ // Check if this is a navigation link
215
+ const isNavigationLink = element instanceof HTMLAnchorElement && element.href;
216
+ // Send immediately with beacon for navigation links to ensure delivery
217
+ if (isNavigationLink) {
218
+ this.tracker.trackSystemEvent('_grain_heatmap_click', {
219
+ page_url: clickData.pageUrl,
220
+ xpath: clickData.xpath,
221
+ viewport_x: clickData.viewportX,
222
+ viewport_y: clickData.viewportY,
223
+ page_x: clickData.pageX,
224
+ page_y: clickData.pageY,
225
+ element_tag: clickData.elementTag,
226
+ element_text: clickData.elementText,
227
+ timestamp: clickData.timestamp,
228
+ }, { flush: true });
229
+ }
230
+ else {
231
+ this.pendingClicks.push(clickData);
232
+ // Check if we should flush
233
+ this.considerBatchFlush();
234
+ }
235
+ }
236
+ /**
237
+ * Handle scroll event
238
+ */
239
+ handleScroll() {
240
+ if (!this.tracker.hasConsent('analytics'))
241
+ return;
242
+ this.updateScrollState();
243
+ }
244
+ /**
245
+ * Update current scroll state
246
+ */
247
+ updateScrollState() {
248
+ if (typeof window === 'undefined')
249
+ return;
250
+ if (!this.tracker.hasConsent('analytics'))
251
+ return;
252
+ const currentTime = Date.now();
253
+ const scrollY = window.scrollY || window.pageYOffset;
254
+ const viewportHeight = window.innerHeight;
255
+ const pageHeight = document.documentElement.scrollHeight;
256
+ // Calculate which viewport section we're in
257
+ const viewportSection = Math.floor(scrollY / viewportHeight);
258
+ // If we're in a new section, record the previous one
259
+ if (this.currentScrollState && this.currentScrollState.viewportSection !== viewportSection) {
260
+ const duration = currentTime - this.currentScrollState.entryTime;
261
+ // Only record if duration is meaningful (> 100ms)
262
+ if (duration > 100) {
263
+ const scrollData = {
264
+ pageUrl: window.location.href,
265
+ viewportSection: this.currentScrollState.viewportSection,
266
+ scrollDepthPx: this.currentScrollState.scrollDepthPx,
267
+ durationMs: duration,
268
+ entryTimestamp: this.currentScrollState.entryTime,
269
+ exitTimestamp: currentTime,
270
+ pageHeight,
271
+ viewportHeight,
272
+ };
273
+ this.pendingScrolls.push(scrollData);
274
+ }
275
+ }
276
+ // Update current state
277
+ if (!this.currentScrollState || this.currentScrollState.viewportSection !== viewportSection) {
278
+ this.currentScrollState = {
279
+ viewportSection,
280
+ entryTime: currentTime,
281
+ scrollDepthPx: scrollY,
282
+ };
283
+ }
284
+ this.lastScrollPosition = scrollY;
285
+ this.lastScrollTime = currentTime;
286
+ // Check if we should flush
287
+ this.considerBatchFlush();
288
+ }
289
+ /**
290
+ * Generate XPath for an element
291
+ */
292
+ generateXPath(element) {
293
+ if (!element)
294
+ return '';
295
+ // If element has an ID, use that for simpler XPath
296
+ if (element.id) {
297
+ return `//*[@id="${element.id}"]`;
298
+ }
299
+ const paths = [];
300
+ let currentElement = element;
301
+ while (currentElement && currentElement.nodeType === Node.ELEMENT_NODE) {
302
+ let index = 0;
303
+ let sibling = currentElement;
304
+ // Count preceding siblings of the same tag
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
+ // Flush if we've hit the batch size
324
+ if (totalEvents >= this.options.maxBatchSize) {
325
+ this.flushPendingEvents();
326
+ return;
327
+ }
328
+ // Otherwise, schedule a batch flush
329
+ if (this.batchTimer === null && totalEvents > 0) {
330
+ this.batchTimer = window.setTimeout(() => {
331
+ this.flushPendingEvents();
332
+ this.batchTimer = null;
333
+ }, this.options.batchDelay);
334
+ }
335
+ }
336
+ /**
337
+ * Flush pending events
338
+ */
339
+ flushPendingEvents() {
340
+ if (this.isDestroyed)
341
+ return;
342
+ if (!this.tracker.hasConsent('analytics')) {
343
+ // Clear pending events if consent is revoked
344
+ this.pendingClicks = [];
345
+ this.pendingScrolls = [];
346
+ return;
347
+ }
348
+ // Send click events
349
+ if (this.pendingClicks.length > 0) {
350
+ for (const clickData of this.pendingClicks) {
351
+ this.tracker.trackSystemEvent('_grain_heatmap_click', {
352
+ page_url: clickData.pageUrl,
353
+ xpath: clickData.xpath,
354
+ viewport_x: clickData.viewportX,
355
+ viewport_y: clickData.viewportY,
356
+ page_x: clickData.pageX,
357
+ page_y: clickData.pageY,
358
+ element_tag: clickData.elementTag,
359
+ element_text: clickData.elementText,
360
+ timestamp: clickData.timestamp,
361
+ });
362
+ }
363
+ this.pendingClicks = [];
364
+ }
365
+ // Send scroll events
366
+ if (this.pendingScrolls.length > 0) {
367
+ for (const scrollData of this.pendingScrolls) {
368
+ this.tracker.trackSystemEvent('_grain_heatmap_scroll', {
369
+ page_url: scrollData.pageUrl,
370
+ viewport_section: scrollData.viewportSection,
371
+ scroll_depth_px: scrollData.scrollDepthPx,
372
+ duration_ms: scrollData.durationMs,
373
+ entry_timestamp: scrollData.entryTimestamp,
374
+ exit_timestamp: scrollData.exitTimestamp,
375
+ page_height: scrollData.pageHeight,
376
+ viewport_height: scrollData.viewportHeight,
377
+ });
378
+ }
379
+ this.pendingScrolls = [];
380
+ }
381
+ // Clear batch timer
382
+ if (this.batchTimer !== null) {
383
+ clearTimeout(this.batchTimer);
384
+ this.batchTimer = null;
385
+ }
386
+ }
387
+ /**
388
+ * Flush pending events with beacon (for page unload)
389
+ */
390
+ flushPendingEventsWithBeacon() {
391
+ if (!this.tracker.hasConsent('analytics')) {
392
+ this.pendingClicks = [];
393
+ this.pendingScrolls = [];
394
+ return;
395
+ }
396
+ // Send click events with beacon
397
+ if (this.pendingClicks.length > 0) {
398
+ for (const clickData of this.pendingClicks) {
399
+ this.tracker.trackSystemEvent('_grain_heatmap_click', {
400
+ page_url: clickData.pageUrl,
401
+ xpath: clickData.xpath,
402
+ viewport_x: clickData.viewportX,
403
+ viewport_y: clickData.viewportY,
404
+ page_x: clickData.pageX,
405
+ page_y: clickData.pageY,
406
+ element_tag: clickData.elementTag,
407
+ element_text: clickData.elementText,
408
+ timestamp: clickData.timestamp,
409
+ }, { flush: true });
410
+ }
411
+ this.pendingClicks = [];
412
+ }
413
+ // Send scroll events with beacon
414
+ if (this.pendingScrolls.length > 0) {
415
+ for (const scrollData of this.pendingScrolls) {
416
+ this.tracker.trackSystemEvent('_grain_heatmap_scroll', {
417
+ page_url: scrollData.pageUrl,
418
+ viewport_section: scrollData.viewportSection,
419
+ scroll_depth_px: scrollData.scrollDepthPx,
420
+ duration_ms: scrollData.durationMs,
421
+ entry_timestamp: scrollData.entryTimestamp,
422
+ exit_timestamp: scrollData.exitTimestamp,
423
+ page_height: scrollData.pageHeight,
424
+ viewport_height: scrollData.viewportHeight,
425
+ }, { flush: true });
426
+ }
427
+ this.pendingScrolls = [];
428
+ }
429
+ }
430
+ /**
431
+ * Log debug message
432
+ */
433
+ log(...args) {
434
+ if (this.options.debug) {
435
+ this.tracker.log('[Heatmap Tracking]', ...args);
436
+ }
437
+ }
438
+ /**
439
+ * Destroy the tracking manager
440
+ */
441
+ destroy() {
442
+ this.isDestroyed = true;
443
+ // Clear timers
444
+ if (this.scrollDebounceTimer !== null) {
445
+ clearTimeout(this.scrollDebounceTimer);
446
+ this.scrollDebounceTimer = null;
447
+ }
448
+ if (this.batchTimer !== null) {
449
+ clearTimeout(this.batchTimer);
450
+ this.batchTimer = null;
451
+ }
452
+ if (this.scrollTrackingTimer !== null) {
453
+ clearInterval(this.scrollTrackingTimer);
454
+ this.scrollTrackingTimer = null;
455
+ }
456
+ if (this.periodicScrollTimer !== null) {
457
+ clearInterval(this.periodicScrollTimer);
458
+ this.periodicScrollTimer = null;
459
+ }
460
+ // Flush any remaining events
461
+ this.flushPendingEvents();
462
+ }
463
+ }
464
+ exports.HeatmapTrackingManager = HeatmapTrackingManager;
465
+ //# sourceMappingURL=heatmap-tracking.js.map
package/dist/index.d.ts CHANGED
@@ -56,6 +56,7 @@ export interface GrainConfig {
56
56
  heartbeatInactiveInterval?: number;
57
57
  enableAutoPageView?: boolean;
58
58
  stripQueryParams?: boolean;
59
+ enableHeatmapTracking?: boolean;
59
60
  }
60
61
  export interface SendEventOptions {
61
62
  flush?: boolean;
@@ -237,6 +238,7 @@ export declare class GrainAnalytics implements HeartbeatTracker, PageTracker {
237
238
  private eventCountSinceLastHeartbeat;
238
239
  private interactionTrackingManager;
239
240
  private sectionTrackingManager;
241
+ private heatmapTrackingManager;
240
242
  private sessionStartTime;
241
243
  private sessionEventCount;
242
244
  constructor(config: GrainConfig);
@@ -311,6 +313,10 @@ export declare class GrainAnalytics implements HeartbeatTracker, PageTracker {
311
313
  * Initialize automatic tracking (heartbeat and page views)
312
314
  */
313
315
  private initializeAutomaticTracking;
316
+ /**
317
+ * Initialize heatmap tracking
318
+ */
319
+ private initializeHeatmapTracking;
314
320
  /**
315
321
  * Initialize auto-tracking (interactions and sections)
316
322
  */