@grainql/analytics-web 3.1.2 → 3.2.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/dist/cjs/heatmap-tracking.d.ts +38 -0
- package/dist/cjs/heatmap-tracking.d.ts.map +1 -1
- package/dist/cjs/heatmap-tracking.js +266 -5
- package/dist/cjs/heatmap-tracking.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +6 -0
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/cjs/types/heatmap-tracking.d.ts +9 -0
- package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -1
- package/dist/esm/heatmap-tracking.d.ts +38 -0
- package/dist/esm/heatmap-tracking.d.ts.map +1 -1
- package/dist/esm/heatmap-tracking.js +233 -5
- package/dist/esm/heatmap-tracking.js.map +1 -1
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +6 -0
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/esm/types/heatmap-tracking.d.ts +9 -0
- package/dist/esm/types/heatmap-tracking.d.ts.map +1 -1
- package/dist/heatmap-tracking.d.ts +38 -0
- package/dist/heatmap-tracking.d.ts.map +1 -1
- package/dist/heatmap-tracking.js +266 -5
- package/dist/index.global.dev.js +5805 -53
- package/dist/index.global.dev.js.map +4 -4
- package/dist/index.global.js +33 -7
- package/dist/index.global.js.map +4 -4
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +6 -0
- package/dist/types/heatmap-tracking.d.ts +9 -0
- package/dist/types/heatmap-tracking.d.ts.map +1 -1
- package/package.json +8 -2
package/dist/heatmap-tracking.js
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
3
3
|
* Heatmap Tracking Manager for Grain Analytics
|
|
4
4
|
* Tracks click interactions and scroll depth across all pages
|
|
5
5
|
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
6
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
40
|
exports.HeatmapTrackingManager = void 0;
|
|
8
41
|
const attention_quality_1 = require("./attention-quality");
|
|
@@ -29,6 +62,9 @@ class HeatmapTrackingManager {
|
|
|
29
62
|
this.lastScrollPosition = 0;
|
|
30
63
|
this.lastScrollTime = Date.now();
|
|
31
64
|
this.SPLIT_DURATION = 3000; // 3 seconds - same as section tracking
|
|
65
|
+
// Snapshot capture state
|
|
66
|
+
this.snapshotCaptured = false;
|
|
67
|
+
this.snapshotEnabled = false;
|
|
32
68
|
this.tracker = tracker;
|
|
33
69
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
34
70
|
// Initialize attention quality manager
|
|
@@ -51,10 +87,12 @@ class HeatmapTrackingManager {
|
|
|
51
87
|
/**
|
|
52
88
|
* Initialize heatmap tracking
|
|
53
89
|
*/
|
|
54
|
-
initialize() {
|
|
90
|
+
async initialize() {
|
|
55
91
|
if (this.isDestroyed)
|
|
56
92
|
return;
|
|
57
93
|
this.log('Initializing heatmap tracking');
|
|
94
|
+
// Check remote config for snapshot capture
|
|
95
|
+
await this.checkSnapshotConfig();
|
|
58
96
|
// Setup click tracking
|
|
59
97
|
this.setupClickTracking();
|
|
60
98
|
// Setup scroll tracking
|
|
@@ -63,6 +101,158 @@ class HeatmapTrackingManager {
|
|
|
63
101
|
this.startScrollTracking();
|
|
64
102
|
// Setup page unload handler for beaconing
|
|
65
103
|
this.setupUnloadHandler();
|
|
104
|
+
// Initialize snapshot capture if enabled
|
|
105
|
+
if (this.snapshotEnabled && !this.snapshotCaptured) {
|
|
106
|
+
this.captureSnapshot();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check remote config for snapshot capture enablement
|
|
111
|
+
*/
|
|
112
|
+
/**
|
|
113
|
+
* Normalize URL by removing query params and hash, and stripping www prefix
|
|
114
|
+
* This ensures heatmap data is aggregated by page, not by URL variations
|
|
115
|
+
* Subdomains (other than www) are preserved: api.example.com != app.example.com
|
|
116
|
+
*/
|
|
117
|
+
normalizeUrl(url) {
|
|
118
|
+
try {
|
|
119
|
+
const urlObj = new URL(url);
|
|
120
|
+
let hostname = urlObj.hostname.toLowerCase();
|
|
121
|
+
// Strip www prefix but keep other subdomains
|
|
122
|
+
if (hostname.startsWith('www.')) {
|
|
123
|
+
hostname = hostname.substring(4);
|
|
124
|
+
}
|
|
125
|
+
// Return protocol + normalized hostname + pathname only
|
|
126
|
+
return `${urlObj.protocol}//${hostname}${urlObj.pathname}`;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// If URL parsing fails, return as-is
|
|
130
|
+
return url;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async checkSnapshotConfig() {
|
|
134
|
+
try {
|
|
135
|
+
const enableSnapshot = await this.tracker.getConfigAsync('enableHeatmapSnapshot');
|
|
136
|
+
this.snapshotEnabled = enableSnapshot === 'true';
|
|
137
|
+
this.log('Heatmap snapshot capture enabled:', this.snapshotEnabled);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
this.log('Failed to check snapshot config, defaulting to disabled:', error);
|
|
141
|
+
this.snapshotEnabled = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Capture DOM snapshot using rrweb-snapshot
|
|
146
|
+
*/
|
|
147
|
+
async captureSnapshot() {
|
|
148
|
+
if (this.snapshotCaptured || !this.snapshotEnabled)
|
|
149
|
+
return;
|
|
150
|
+
try {
|
|
151
|
+
this.log('Capturing DOM snapshot...');
|
|
152
|
+
// Dynamically import rrweb-snapshot (only if enabled)
|
|
153
|
+
// @ts-ignore - rrweb-snapshot is an optional dependency, may not be resolvable during React build
|
|
154
|
+
const rrwebSnapshot = await Promise.resolve().then(() => __importStar(require('rrweb-snapshot')));
|
|
155
|
+
// Capture full DOM snapshot with PII masking
|
|
156
|
+
const snapshot = rrwebSnapshot.snapshot(document, {
|
|
157
|
+
maskAllInputs: true,
|
|
158
|
+
maskTextFn: (text) => {
|
|
159
|
+
// Basic PII masking - mask anything that looks like email or sensitive data
|
|
160
|
+
return text.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '*****');
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
this.snapshotCaptured = true;
|
|
164
|
+
this.log('DOM snapshot captured successfully');
|
|
165
|
+
// Upload snapshot to backend
|
|
166
|
+
await this.uploadSnapshot(snapshot);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this.log('Failed to capture DOM snapshot:', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Upload snapshot to backend
|
|
174
|
+
*/
|
|
175
|
+
async uploadSnapshot(snapshot) {
|
|
176
|
+
try {
|
|
177
|
+
const sessionId = this.tracker.getEffectiveUserId();
|
|
178
|
+
const pageUrl = this.normalizeUrl(window.location.href);
|
|
179
|
+
this.log('Uploading snapshot to backend - sessionId:', sessionId);
|
|
180
|
+
// Note: The actual API call would need the tenantId from the tracker
|
|
181
|
+
// For now, we'll log that the snapshot is ready to be uploaded
|
|
182
|
+
// The tracker would need to expose tenantId for this to work
|
|
183
|
+
const snapshotData = {
|
|
184
|
+
sessionId,
|
|
185
|
+
pageUrl,
|
|
186
|
+
snapshot
|
|
187
|
+
};
|
|
188
|
+
this.log('Snapshot data prepared:', { sessionId, pageUrl, snapshotSize: JSON.stringify(snapshot).length });
|
|
189
|
+
// Get API configuration from tracker
|
|
190
|
+
const apiUrl = await this.getApiUrl();
|
|
191
|
+
const tenantId = await this.getTenantId();
|
|
192
|
+
const headers = await this.getAuthHeaders();
|
|
193
|
+
if (!apiUrl || !tenantId) {
|
|
194
|
+
this.log('Cannot upload snapshot: missing API URL or tenant ID');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Upload to backend
|
|
198
|
+
const response = await fetch(`${apiUrl}/v1/events/${encodeURIComponent(tenantId)}/snapshot`, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: {
|
|
201
|
+
'Content-Type': 'application/json',
|
|
202
|
+
...headers
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
sessionId,
|
|
206
|
+
pageUrl,
|
|
207
|
+
snapshot: JSON.stringify(snapshot),
|
|
208
|
+
timestamp: Date.now()
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new Error(`Snapshot upload failed: ${response.status}`);
|
|
213
|
+
}
|
|
214
|
+
const result = await response.json();
|
|
215
|
+
this.log('Snapshot uploaded successfully:', result);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
this.log('Failed to upload snapshot:', error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get API URL from tracker configuration
|
|
223
|
+
*/
|
|
224
|
+
async getApiUrl() {
|
|
225
|
+
try {
|
|
226
|
+
return this.tracker.config?.apiUrl || 'https://api.grainql.com';
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return 'https://api.grainql.com';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get tenant ID from tracker configuration
|
|
234
|
+
*/
|
|
235
|
+
async getTenantId() {
|
|
236
|
+
try {
|
|
237
|
+
return this.tracker.config?.tenantId;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get auth headers from tracker
|
|
245
|
+
*/
|
|
246
|
+
async getAuthHeaders() {
|
|
247
|
+
try {
|
|
248
|
+
if (typeof this.tracker.getAuthHeaders === 'function') {
|
|
249
|
+
return await this.tracker.getAuthHeaders();
|
|
250
|
+
}
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
66
256
|
}
|
|
67
257
|
/**
|
|
68
258
|
* Setup click event tracking
|
|
@@ -151,7 +341,7 @@ class HeatmapTrackingManager {
|
|
|
151
341
|
return;
|
|
152
342
|
}
|
|
153
343
|
const scrollData = {
|
|
154
|
-
pageUrl: window.location.href,
|
|
344
|
+
pageUrl: this.normalizeUrl(window.location.href),
|
|
155
345
|
viewportSection: this.currentScrollState.viewportSection,
|
|
156
346
|
scrollDepthPx: scrollY,
|
|
157
347
|
durationMs: duration,
|
|
@@ -192,7 +382,7 @@ class HeatmapTrackingManager {
|
|
|
192
382
|
const duration = currentTime - this.currentScrollState.entryTime;
|
|
193
383
|
if (duration > 100) {
|
|
194
384
|
const scrollData = {
|
|
195
|
-
pageUrl: window.location.href,
|
|
385
|
+
pageUrl: this.normalizeUrl(window.location.href),
|
|
196
386
|
viewportSection: this.currentScrollState.viewportSection,
|
|
197
387
|
scrollDepthPx: this.currentScrollState.scrollDepthPx,
|
|
198
388
|
durationMs: duration,
|
|
@@ -220,8 +410,23 @@ class HeatmapTrackingManager {
|
|
|
220
410
|
const element = event.target;
|
|
221
411
|
if (!element)
|
|
222
412
|
return;
|
|
223
|
-
const pageUrl = window.location.href;
|
|
413
|
+
const pageUrl = this.normalizeUrl(window.location.href);
|
|
224
414
|
const xpath = this.generateXPath(element);
|
|
415
|
+
// Generate CSS selector for element-relative positioning
|
|
416
|
+
const selector = this.generateCSSSelector(element);
|
|
417
|
+
// Calculate element-relative coordinates
|
|
418
|
+
let relX;
|
|
419
|
+
let relY;
|
|
420
|
+
try {
|
|
421
|
+
const rect = element.getBoundingClientRect();
|
|
422
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
423
|
+
relX = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
|
|
424
|
+
relY = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
this.log('Failed to calculate relative coordinates:', error);
|
|
429
|
+
}
|
|
225
430
|
// Get viewport coordinates
|
|
226
431
|
const viewportX = Math.round(event.clientX);
|
|
227
432
|
const viewportY = Math.round(event.clientY);
|
|
@@ -233,6 +438,9 @@ class HeatmapTrackingManager {
|
|
|
233
438
|
const clickData = {
|
|
234
439
|
pageUrl,
|
|
235
440
|
xpath,
|
|
441
|
+
selector,
|
|
442
|
+
relX,
|
|
443
|
+
relY,
|
|
236
444
|
viewportX,
|
|
237
445
|
viewportY,
|
|
238
446
|
pageX,
|
|
@@ -248,6 +456,9 @@ class HeatmapTrackingManager {
|
|
|
248
456
|
this.tracker.trackSystemEvent('_grain_heatmap_click', {
|
|
249
457
|
page_url: clickData.pageUrl,
|
|
250
458
|
xpath: clickData.xpath,
|
|
459
|
+
selector: clickData.selector,
|
|
460
|
+
rel_x: clickData.relX,
|
|
461
|
+
rel_y: clickData.relY,
|
|
251
462
|
viewport_x: clickData.viewportX,
|
|
252
463
|
viewport_y: clickData.viewportY,
|
|
253
464
|
page_x: clickData.pageX,
|
|
@@ -291,7 +502,7 @@ class HeatmapTrackingManager {
|
|
|
291
502
|
// Only record if duration is meaningful (> 100ms)
|
|
292
503
|
if (duration > 100) {
|
|
293
504
|
const scrollData = {
|
|
294
|
-
pageUrl: window.location.href,
|
|
505
|
+
pageUrl: this.normalizeUrl(window.location.href),
|
|
295
506
|
viewportSection: this.currentScrollState.viewportSection,
|
|
296
507
|
scrollDepthPx: this.currentScrollState.scrollDepthPx,
|
|
297
508
|
durationMs: duration,
|
|
@@ -319,6 +530,50 @@ class HeatmapTrackingManager {
|
|
|
319
530
|
// Check if we should flush
|
|
320
531
|
this.considerBatchFlush();
|
|
321
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Generate CSS selector for an element (for element-relative positioning)
|
|
535
|
+
*/
|
|
536
|
+
generateCSSSelector(element) {
|
|
537
|
+
if (!element)
|
|
538
|
+
return '';
|
|
539
|
+
// Prefer ID if available
|
|
540
|
+
if (element.id) {
|
|
541
|
+
return `#${element.id}`;
|
|
542
|
+
}
|
|
543
|
+
// Try data attributes
|
|
544
|
+
const dataAttrs = Array.from(element.attributes).filter(attr => attr.name.startsWith('data-'));
|
|
545
|
+
if (dataAttrs.length > 0) {
|
|
546
|
+
const attr = dataAttrs[0];
|
|
547
|
+
return `${element.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`;
|
|
548
|
+
}
|
|
549
|
+
// Try class names (pick first meaningful class)
|
|
550
|
+
if (element.className && typeof element.className === 'string') {
|
|
551
|
+
const classes = element.className.split(' ').filter(c => c && !c.match(/^(active|hover|focus)/));
|
|
552
|
+
if (classes.length > 0) {
|
|
553
|
+
return `${element.tagName.toLowerCase()}.${classes[0]}`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Fall back to nth-child path
|
|
557
|
+
const path = [];
|
|
558
|
+
let current = element;
|
|
559
|
+
while (current && current !== document.body) {
|
|
560
|
+
let selector = current.tagName.toLowerCase();
|
|
561
|
+
if (current.parentElement) {
|
|
562
|
+
const siblings = Array.from(current.parentElement.children);
|
|
563
|
+
const sameTagSiblings = siblings.filter(s => s.tagName === current.tagName);
|
|
564
|
+
if (sameTagSiblings.length > 1) {
|
|
565
|
+
const index = sameTagSiblings.indexOf(current) + 1;
|
|
566
|
+
selector += `:nth-child(${index})`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
path.unshift(selector);
|
|
570
|
+
current = current.parentElement;
|
|
571
|
+
// Limit path depth to avoid overly long selectors
|
|
572
|
+
if (path.length >= 5)
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
return path.join(' > ');
|
|
576
|
+
}
|
|
322
577
|
/**
|
|
323
578
|
* Generate XPath for an element
|
|
324
579
|
*/
|
|
@@ -380,6 +635,9 @@ class HeatmapTrackingManager {
|
|
|
380
635
|
this.tracker.trackSystemEvent('_grain_heatmap_click', {
|
|
381
636
|
page_url: clickData.pageUrl,
|
|
382
637
|
xpath: clickData.xpath,
|
|
638
|
+
selector: clickData.selector,
|
|
639
|
+
rel_x: clickData.relX,
|
|
640
|
+
rel_y: clickData.relY,
|
|
383
641
|
viewport_x: clickData.viewportX,
|
|
384
642
|
viewport_y: clickData.viewportY,
|
|
385
643
|
page_x: clickData.pageX,
|
|
@@ -425,6 +683,9 @@ class HeatmapTrackingManager {
|
|
|
425
683
|
this.tracker.trackSystemEvent('_grain_heatmap_click', {
|
|
426
684
|
page_url: clickData.pageUrl,
|
|
427
685
|
xpath: clickData.xpath,
|
|
686
|
+
selector: clickData.selector,
|
|
687
|
+
rel_x: clickData.relX,
|
|
688
|
+
rel_y: clickData.relY,
|
|
428
689
|
viewport_x: clickData.viewportX,
|
|
429
690
|
viewport_y: clickData.viewportY,
|
|
430
691
|
page_x: clickData.pageX,
|