@grainql/analytics-web 2.5.4 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/activity.js +1 -1
- package/dist/cjs/activity.js +1 -1
- package/dist/cjs/activity.js.map +1 -1
- package/dist/cjs/consent.js +4 -4
- package/dist/cjs/consent.js.map +1 -1
- package/dist/cjs/heartbeat.d.ts.map +1 -1
- package/dist/cjs/heartbeat.js +0 -6
- package/dist/cjs/heartbeat.js.map +1 -1
- package/dist/cjs/heatmap-tracking.d.ts +90 -0
- package/dist/cjs/heatmap-tracking.d.ts.map +1 -0
- package/dist/cjs/heatmap-tracking.js +465 -0
- package/dist/cjs/heatmap-tracking.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interaction-tracking.d.ts.map +1 -1
- package/dist/cjs/interaction-tracking.js +9 -18
- package/dist/cjs/interaction-tracking.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +0 -9
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/cjs/section-tracking.d.ts.map +1 -1
- package/dist/cjs/section-tracking.js +1 -7
- package/dist/cjs/section-tracking.js.map +1 -1
- package/dist/cjs/types/heatmap-tracking.d.ts +41 -0
- package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -0
- package/dist/cjs/types/heatmap-tracking.js +6 -0
- package/dist/cjs/types/heatmap-tracking.js.map +1 -0
- package/dist/consent.js +4 -4
- package/dist/esm/activity.js +1 -1
- package/dist/esm/activity.js.map +1 -1
- package/dist/esm/consent.js +4 -4
- package/dist/esm/consent.js.map +1 -1
- package/dist/esm/heartbeat.d.ts.map +1 -1
- package/dist/esm/heartbeat.js +0 -6
- package/dist/esm/heartbeat.js.map +1 -1
- package/dist/esm/heatmap-tracking.d.ts +90 -0
- package/dist/esm/heatmap-tracking.d.ts.map +1 -0
- package/dist/esm/heatmap-tracking.js +461 -0
- package/dist/esm/heatmap-tracking.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interaction-tracking.d.ts.map +1 -1
- package/dist/esm/interaction-tracking.js +9 -18
- package/dist/esm/interaction-tracking.js.map +1 -1
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +0 -9
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/esm/section-tracking.d.ts.map +1 -1
- package/dist/esm/section-tracking.js +1 -7
- package/dist/esm/section-tracking.js.map +1 -1
- package/dist/esm/types/heatmap-tracking.d.ts +41 -0
- package/dist/esm/types/heatmap-tracking.d.ts.map +1 -0
- package/dist/esm/types/heatmap-tracking.js +5 -0
- package/dist/esm/types/heatmap-tracking.js.map +1 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +0 -6
- package/dist/heatmap-tracking.d.ts +90 -0
- package/dist/heatmap-tracking.d.ts.map +1 -0
- package/dist/heatmap-tracking.js +465 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +503 -79
- package/dist/index.global.dev.js.map +4 -4
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +61 -38
- package/dist/index.mjs +61 -38
- package/dist/interaction-tracking.d.ts.map +1 -1
- package/dist/interaction-tracking.js +9 -18
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +0 -9
- package/dist/section-tracking.d.ts.map +1 -1
- package/dist/section-tracking.js +1 -7
- package/dist/types/heatmap-tracking.d.ts +41 -0
- package/dist/types/heatmap-tracking.d.ts.map +1 -0
- 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 @@
|
|
|
1
|
+
{"version":3,"file":"heatmap-tracking.js","sourceRoot":"","sources":["../../../src/types/heatmap-tracking.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/dist/heartbeat.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
*/
|