@grainql/analytics-web 2.4.0 → 2.5.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/index.d.ts +28 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interaction-tracking.d.ts +71 -0
- package/dist/cjs/interaction-tracking.d.ts.map +1 -0
- package/dist/cjs/interaction-tracking.js +270 -0
- package/dist/cjs/interaction-tracking.js.map +1 -0
- package/dist/cjs/section-tracking.d.ts +91 -0
- package/dist/cjs/section-tracking.d.ts.map +1 -0
- package/dist/cjs/section-tracking.js +373 -0
- package/dist/cjs/section-tracking.js.map +1 -0
- package/dist/cjs/types/auto-tracking.d.ts +55 -0
- package/dist/cjs/types/auto-tracking.d.ts.map +1 -0
- package/dist/cjs/types/auto-tracking.js +6 -0
- package/dist/cjs/types/auto-tracking.js.map +1 -0
- package/dist/esm/index.d.ts +28 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interaction-tracking.d.ts +71 -0
- package/dist/esm/interaction-tracking.d.ts.map +1 -0
- package/dist/esm/interaction-tracking.js +266 -0
- package/dist/esm/interaction-tracking.js.map +1 -0
- package/dist/esm/section-tracking.d.ts +91 -0
- package/dist/esm/section-tracking.d.ts.map +1 -0
- package/dist/esm/section-tracking.js +369 -0
- package/dist/esm/section-tracking.js.map +1 -0
- package/dist/esm/types/auto-tracking.d.ts +55 -0
- package/dist/esm/types/auto-tracking.d.ts.map +1 -0
- package/dist/esm/types/auto-tracking.js +5 -0
- package/dist/esm/types/auto-tracking.js.map +1 -0
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +711 -1
- 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 +124 -0
- package/dist/index.mjs +91 -0
- package/dist/interaction-tracking.d.ts +71 -0
- package/dist/interaction-tracking.d.ts.map +1 -0
- package/dist/interaction-tracking.js +270 -0
- package/dist/section-tracking.d.ts +91 -0
- package/dist/section-tracking.d.ts.map +1 -0
- package/dist/section-tracking.js +373 -0
- package/dist/types/auto-tracking.d.ts +55 -0
- package/dist/types/auto-tracking.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.global.dev.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
/* Grain Analytics Web SDK v2.
|
|
1
|
+
/* Grain Analytics Web SDK v2.5.0 | MIT License | Development Build */
|
|
2
2
|
"use strict";
|
|
3
3
|
var Grain = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -19,6 +22,622 @@ var Grain = (() => {
|
|
|
19
22
|
};
|
|
20
23
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
24
|
|
|
25
|
+
// src/interaction-tracking.ts
|
|
26
|
+
var interaction_tracking_exports = {};
|
|
27
|
+
__export(interaction_tracking_exports, {
|
|
28
|
+
InteractionTrackingManager: () => InteractionTrackingManager
|
|
29
|
+
});
|
|
30
|
+
var InteractionTrackingManager;
|
|
31
|
+
var init_interaction_tracking = __esm({
|
|
32
|
+
"src/interaction-tracking.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
InteractionTrackingManager = class {
|
|
35
|
+
constructor(tracker, interactions, config = {}) {
|
|
36
|
+
this.isDestroyed = false;
|
|
37
|
+
this.attachedListeners = /* @__PURE__ */ new Map();
|
|
38
|
+
this.xpathCache = /* @__PURE__ */ new Map();
|
|
39
|
+
this.mutationObserver = null;
|
|
40
|
+
this.mutationDebounceTimer = null;
|
|
41
|
+
this.tracker = tracker;
|
|
42
|
+
this.interactions = interactions;
|
|
43
|
+
this.config = {
|
|
44
|
+
debug: config.debug ?? false,
|
|
45
|
+
enableMutationObserver: config.enableMutationObserver ?? true,
|
|
46
|
+
mutationDebounceDelay: config.mutationDebounceDelay ?? 500
|
|
47
|
+
};
|
|
48
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
49
|
+
if (document.readyState === "loading") {
|
|
50
|
+
document.addEventListener("DOMContentLoaded", () => this.attachAllListeners());
|
|
51
|
+
} else {
|
|
52
|
+
setTimeout(() => this.attachAllListeners(), 0);
|
|
53
|
+
}
|
|
54
|
+
if (this.config.enableMutationObserver) {
|
|
55
|
+
this.setupMutationObserver();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Attach listeners to all configured interactions
|
|
61
|
+
*/
|
|
62
|
+
attachAllListeners() {
|
|
63
|
+
if (this.isDestroyed)
|
|
64
|
+
return;
|
|
65
|
+
this.log("Attaching interaction listeners for", this.interactions.length, "interactions");
|
|
66
|
+
for (const interaction of this.interactions) {
|
|
67
|
+
this.attachInteractionListener(interaction);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Attach listener to a specific interaction
|
|
72
|
+
*/
|
|
73
|
+
attachInteractionListener(interaction) {
|
|
74
|
+
if (this.isDestroyed)
|
|
75
|
+
return;
|
|
76
|
+
const element = this.findElementByXPath(interaction.selector);
|
|
77
|
+
if (!element) {
|
|
78
|
+
this.log("Element not found for interaction:", interaction.eventName, "selector:", interaction.selector);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (this.attachedListeners.has(element)) {
|
|
82
|
+
this.log("Listeners already attached for element:", element);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const handlers = [];
|
|
86
|
+
const clickHandler = (event) => this.handleInteractionClick(interaction, event);
|
|
87
|
+
element.addEventListener("click", clickHandler, { passive: true });
|
|
88
|
+
handlers.push({ event: "click", handler: clickHandler });
|
|
89
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
90
|
+
const focusHandler = (event) => this.handleInteractionFocus(interaction, event);
|
|
91
|
+
element.addEventListener("focus", focusHandler, { passive: true });
|
|
92
|
+
handlers.push({ event: "focus", handler: focusHandler });
|
|
93
|
+
}
|
|
94
|
+
this.attachedListeners.set(element, handlers);
|
|
95
|
+
this.log("Attached listeners to element for:", interaction.eventName);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Handle click event on interaction
|
|
99
|
+
*/
|
|
100
|
+
handleInteractionClick(interaction, event) {
|
|
101
|
+
if (this.isDestroyed)
|
|
102
|
+
return;
|
|
103
|
+
if (!this.tracker.hasConsent("analytics"))
|
|
104
|
+
return;
|
|
105
|
+
const element = event.target;
|
|
106
|
+
this.tracker.track(interaction.eventName, {
|
|
107
|
+
interaction_type: "click",
|
|
108
|
+
interaction_label: interaction.label,
|
|
109
|
+
interaction_description: interaction.description,
|
|
110
|
+
interaction_priority: interaction.priority,
|
|
111
|
+
element_tag: element.tagName?.toLowerCase(),
|
|
112
|
+
element_text: element.textContent?.trim().substring(0, 100),
|
|
113
|
+
element_id: element.id || void 0,
|
|
114
|
+
element_class: element.className || void 0,
|
|
115
|
+
timestamp: Date.now()
|
|
116
|
+
});
|
|
117
|
+
this.log("Tracked click interaction:", interaction.eventName);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Handle focus event on interaction (for form fields)
|
|
121
|
+
*/
|
|
122
|
+
handleInteractionFocus(interaction, event) {
|
|
123
|
+
if (this.isDestroyed)
|
|
124
|
+
return;
|
|
125
|
+
if (!this.tracker.hasConsent("analytics"))
|
|
126
|
+
return;
|
|
127
|
+
const element = event.target;
|
|
128
|
+
this.tracker.track(interaction.eventName, {
|
|
129
|
+
interaction_type: "focus",
|
|
130
|
+
interaction_label: interaction.label,
|
|
131
|
+
interaction_description: interaction.description,
|
|
132
|
+
interaction_priority: interaction.priority,
|
|
133
|
+
element_tag: element.tagName?.toLowerCase(),
|
|
134
|
+
element_id: element.id || void 0,
|
|
135
|
+
element_class: element.className || void 0,
|
|
136
|
+
timestamp: Date.now()
|
|
137
|
+
});
|
|
138
|
+
this.log("Tracked focus interaction:", interaction.eventName);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find element by XPath selector
|
|
142
|
+
*/
|
|
143
|
+
findElementByXPath(xpath) {
|
|
144
|
+
if (this.xpathCache.has(xpath)) {
|
|
145
|
+
const cached = this.xpathCache.get(xpath);
|
|
146
|
+
if (cached && document.contains(cached)) {
|
|
147
|
+
return cached;
|
|
148
|
+
}
|
|
149
|
+
this.xpathCache.delete(xpath);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const result = document.evaluate(
|
|
153
|
+
xpath,
|
|
154
|
+
document,
|
|
155
|
+
null,
|
|
156
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
157
|
+
null
|
|
158
|
+
);
|
|
159
|
+
const element = result.singleNodeValue;
|
|
160
|
+
if (element) {
|
|
161
|
+
this.xpathCache.set(xpath, element);
|
|
162
|
+
}
|
|
163
|
+
return element;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.log("Error evaluating XPath:", xpath, error);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Setup mutation observer to handle dynamic content
|
|
171
|
+
*/
|
|
172
|
+
setupMutationObserver() {
|
|
173
|
+
if (typeof MutationObserver === "undefined") {
|
|
174
|
+
this.log("MutationObserver not supported");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
178
|
+
if (this.mutationDebounceTimer !== null) {
|
|
179
|
+
clearTimeout(this.mutationDebounceTimer);
|
|
180
|
+
}
|
|
181
|
+
this.mutationDebounceTimer = window.setTimeout(() => {
|
|
182
|
+
this.handleMutations(mutations);
|
|
183
|
+
this.mutationDebounceTimer = null;
|
|
184
|
+
}, this.config.mutationDebounceDelay);
|
|
185
|
+
});
|
|
186
|
+
this.mutationObserver.observe(document.body, {
|
|
187
|
+
childList: true,
|
|
188
|
+
subtree: true
|
|
189
|
+
});
|
|
190
|
+
this.log("Mutation observer setup");
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Handle DOM mutations
|
|
194
|
+
*/
|
|
195
|
+
handleMutations(mutations) {
|
|
196
|
+
if (this.isDestroyed)
|
|
197
|
+
return;
|
|
198
|
+
this.xpathCache.clear();
|
|
199
|
+
const removedElements = /* @__PURE__ */ new Set();
|
|
200
|
+
for (const mutation of mutations) {
|
|
201
|
+
mutation.removedNodes.forEach((node) => {
|
|
202
|
+
if (node instanceof Element) {
|
|
203
|
+
removedElements.add(node);
|
|
204
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
205
|
+
if (node.contains(element)) {
|
|
206
|
+
removedElements.add(element);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
removedElements.forEach((element) => {
|
|
213
|
+
this.detachListeners(element);
|
|
214
|
+
});
|
|
215
|
+
this.attachAllListeners();
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Detach listeners from an element
|
|
219
|
+
*/
|
|
220
|
+
detachListeners(element) {
|
|
221
|
+
const handlers = this.attachedListeners.get(element);
|
|
222
|
+
if (!handlers)
|
|
223
|
+
return;
|
|
224
|
+
handlers.forEach(({ event, handler }) => {
|
|
225
|
+
element.removeEventListener(event, handler);
|
|
226
|
+
});
|
|
227
|
+
this.attachedListeners.delete(element);
|
|
228
|
+
this.log("Detached listeners from element");
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Log debug messages
|
|
232
|
+
*/
|
|
233
|
+
log(...args) {
|
|
234
|
+
if (this.config.debug) {
|
|
235
|
+
console.log("[InteractionTracking]", ...args);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Update interactions configuration
|
|
240
|
+
*/
|
|
241
|
+
updateInteractions(interactions) {
|
|
242
|
+
if (this.isDestroyed)
|
|
243
|
+
return;
|
|
244
|
+
this.log("Updating interactions configuration");
|
|
245
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
246
|
+
this.detachListeners(element);
|
|
247
|
+
});
|
|
248
|
+
this.xpathCache.clear();
|
|
249
|
+
this.interactions = interactions;
|
|
250
|
+
this.attachAllListeners();
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Cleanup and destroy
|
|
254
|
+
*/
|
|
255
|
+
destroy() {
|
|
256
|
+
if (this.isDestroyed)
|
|
257
|
+
return;
|
|
258
|
+
this.log("Destroying interaction tracking manager");
|
|
259
|
+
this.isDestroyed = true;
|
|
260
|
+
if (this.mutationDebounceTimer !== null) {
|
|
261
|
+
clearTimeout(this.mutationDebounceTimer);
|
|
262
|
+
this.mutationDebounceTimer = null;
|
|
263
|
+
}
|
|
264
|
+
if (this.mutationObserver) {
|
|
265
|
+
this.mutationObserver.disconnect();
|
|
266
|
+
this.mutationObserver = null;
|
|
267
|
+
}
|
|
268
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
269
|
+
this.detachListeners(element);
|
|
270
|
+
});
|
|
271
|
+
this.attachedListeners.clear();
|
|
272
|
+
this.xpathCache.clear();
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// src/section-tracking.ts
|
|
279
|
+
var section_tracking_exports = {};
|
|
280
|
+
__export(section_tracking_exports, {
|
|
281
|
+
SectionTrackingManager: () => SectionTrackingManager
|
|
282
|
+
});
|
|
283
|
+
var DEFAULT_OPTIONS, SectionTrackingManager;
|
|
284
|
+
var init_section_tracking = __esm({
|
|
285
|
+
"src/section-tracking.ts"() {
|
|
286
|
+
"use strict";
|
|
287
|
+
DEFAULT_OPTIONS = {
|
|
288
|
+
minDwellTime: 1e3,
|
|
289
|
+
// 1 second minimum
|
|
290
|
+
scrollVelocityThreshold: 500,
|
|
291
|
+
// 500px/s
|
|
292
|
+
intersectionThreshold: 0.1,
|
|
293
|
+
// 10% visible
|
|
294
|
+
debounceDelay: 100,
|
|
295
|
+
batchDelay: 2e3,
|
|
296
|
+
// 2 seconds
|
|
297
|
+
debug: false
|
|
298
|
+
};
|
|
299
|
+
SectionTrackingManager = class {
|
|
300
|
+
constructor(tracker, sections, options = {}) {
|
|
301
|
+
this.isDestroyed = false;
|
|
302
|
+
// Tracking state
|
|
303
|
+
this.sectionStates = /* @__PURE__ */ new Map();
|
|
304
|
+
this.intersectionObserver = null;
|
|
305
|
+
this.xpathCache = /* @__PURE__ */ new Map();
|
|
306
|
+
// Scroll tracking
|
|
307
|
+
this.lastScrollPosition = 0;
|
|
308
|
+
this.lastScrollTime = Date.now();
|
|
309
|
+
this.scrollVelocity = 0;
|
|
310
|
+
this.scrollDebounceTimer = null;
|
|
311
|
+
// Event batching
|
|
312
|
+
this.pendingEvents = [];
|
|
313
|
+
this.batchTimer = null;
|
|
314
|
+
this.tracker = tracker;
|
|
315
|
+
this.sections = sections;
|
|
316
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
317
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
318
|
+
if (document.readyState === "loading") {
|
|
319
|
+
document.addEventListener("DOMContentLoaded", () => this.initialize());
|
|
320
|
+
} else {
|
|
321
|
+
setTimeout(() => this.initialize(), 0);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Initialize section tracking
|
|
327
|
+
*/
|
|
328
|
+
initialize() {
|
|
329
|
+
if (this.isDestroyed)
|
|
330
|
+
return;
|
|
331
|
+
this.log("Initializing section tracking for", this.sections.length, "sections");
|
|
332
|
+
this.setupIntersectionObserver();
|
|
333
|
+
this.setupScrollListener();
|
|
334
|
+
this.initializeSections();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Setup IntersectionObserver for section visibility
|
|
338
|
+
*/
|
|
339
|
+
setupIntersectionObserver() {
|
|
340
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
341
|
+
this.log("IntersectionObserver not supported");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
this.intersectionObserver = new IntersectionObserver(
|
|
345
|
+
(entries) => {
|
|
346
|
+
entries.forEach((entry) => {
|
|
347
|
+
this.handleIntersection(entry);
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
threshold: [0, 0.1, 0.25, 0.5, 0.75, 1],
|
|
352
|
+
rootMargin: "0px"
|
|
353
|
+
}
|
|
354
|
+
);
|
|
355
|
+
this.log("IntersectionObserver created");
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Setup scroll listener for velocity calculation
|
|
359
|
+
*/
|
|
360
|
+
setupScrollListener() {
|
|
361
|
+
if (typeof window === "undefined")
|
|
362
|
+
return;
|
|
363
|
+
const scrollHandler = () => {
|
|
364
|
+
if (this.scrollDebounceTimer !== null) {
|
|
365
|
+
clearTimeout(this.scrollDebounceTimer);
|
|
366
|
+
}
|
|
367
|
+
this.scrollDebounceTimer = window.setTimeout(() => {
|
|
368
|
+
this.updateScrollVelocity();
|
|
369
|
+
this.scrollDebounceTimer = null;
|
|
370
|
+
}, this.options.debounceDelay);
|
|
371
|
+
};
|
|
372
|
+
window.addEventListener("scroll", scrollHandler, { passive: true });
|
|
373
|
+
this.log("Scroll listener attached");
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Initialize sections and start observing
|
|
377
|
+
*/
|
|
378
|
+
initializeSections() {
|
|
379
|
+
for (const section of this.sections) {
|
|
380
|
+
const element = this.findElementByXPath(section.selector);
|
|
381
|
+
if (!element) {
|
|
382
|
+
this.log("Section element not found:", section.sectionName, "selector:", section.selector);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const state = {
|
|
386
|
+
element,
|
|
387
|
+
config: section,
|
|
388
|
+
entryTime: null,
|
|
389
|
+
exitTime: null,
|
|
390
|
+
isVisible: false,
|
|
391
|
+
lastScrollPosition: window.scrollY,
|
|
392
|
+
lastScrollTime: Date.now(),
|
|
393
|
+
entryScrollSpeed: 0,
|
|
394
|
+
exitScrollSpeed: 0,
|
|
395
|
+
maxVisibleArea: 0
|
|
396
|
+
};
|
|
397
|
+
this.sectionStates.set(section.sectionName, state);
|
|
398
|
+
if (this.intersectionObserver) {
|
|
399
|
+
this.intersectionObserver.observe(element);
|
|
400
|
+
}
|
|
401
|
+
this.log("Section initialized and observed:", section.sectionName);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Handle intersection observer entry
|
|
406
|
+
*/
|
|
407
|
+
handleIntersection(entry) {
|
|
408
|
+
if (this.isDestroyed)
|
|
409
|
+
return;
|
|
410
|
+
const state = Array.from(this.sectionStates.values()).find(
|
|
411
|
+
(s) => s.element === entry.target
|
|
412
|
+
);
|
|
413
|
+
if (!state)
|
|
414
|
+
return;
|
|
415
|
+
const isVisible = entry.isIntersecting && entry.intersectionRatio >= this.options.intersectionThreshold;
|
|
416
|
+
const visibleArea = entry.intersectionRatio;
|
|
417
|
+
if (visibleArea > state.maxVisibleArea) {
|
|
418
|
+
state.maxVisibleArea = visibleArea;
|
|
419
|
+
}
|
|
420
|
+
if (isVisible && !state.isVisible) {
|
|
421
|
+
this.handleSectionEntry(state);
|
|
422
|
+
} else if (!isVisible && state.isVisible) {
|
|
423
|
+
this.handleSectionExit(state);
|
|
424
|
+
}
|
|
425
|
+
state.isVisible = isVisible;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Handle section entry (became visible)
|
|
429
|
+
*/
|
|
430
|
+
handleSectionEntry(state) {
|
|
431
|
+
this.log("Section entered view:", state.config.sectionName);
|
|
432
|
+
state.entryTime = Date.now();
|
|
433
|
+
state.entryScrollSpeed = this.scrollVelocity;
|
|
434
|
+
state.lastScrollPosition = window.scrollY;
|
|
435
|
+
state.lastScrollTime = Date.now();
|
|
436
|
+
state.maxVisibleArea = 0;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Handle section exit (became invisible)
|
|
440
|
+
*/
|
|
441
|
+
handleSectionExit(state) {
|
|
442
|
+
this.log("Section exited view:", state.config.sectionName);
|
|
443
|
+
if (state.entryTime === null)
|
|
444
|
+
return;
|
|
445
|
+
state.exitTime = Date.now();
|
|
446
|
+
state.exitScrollSpeed = this.scrollVelocity;
|
|
447
|
+
const duration = state.exitTime - state.entryTime;
|
|
448
|
+
const viewData = {
|
|
449
|
+
sectionName: state.config.sectionName,
|
|
450
|
+
sectionType: state.config.sectionType,
|
|
451
|
+
entryTime: state.entryTime,
|
|
452
|
+
exitTime: state.exitTime,
|
|
453
|
+
duration,
|
|
454
|
+
viewportWidth: window.innerWidth,
|
|
455
|
+
viewportHeight: window.innerHeight,
|
|
456
|
+
scrollDepth: this.calculateScrollDepth(),
|
|
457
|
+
visibleAreaPercentage: Math.round(state.maxVisibleArea * 100),
|
|
458
|
+
scrollSpeedAtEntry: state.entryScrollSpeed,
|
|
459
|
+
scrollSpeedAtExit: state.exitScrollSpeed
|
|
460
|
+
};
|
|
461
|
+
if (this.shouldTrackSection(viewData)) {
|
|
462
|
+
this.queueSectionView(viewData);
|
|
463
|
+
} else {
|
|
464
|
+
this.log("Section view filtered out:", state.config.sectionName, "duration:", duration);
|
|
465
|
+
}
|
|
466
|
+
state.entryTime = null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Update scroll velocity
|
|
470
|
+
*/
|
|
471
|
+
updateScrollVelocity() {
|
|
472
|
+
const now = Date.now();
|
|
473
|
+
const currentPosition = window.scrollY;
|
|
474
|
+
const timeDelta = now - this.lastScrollTime;
|
|
475
|
+
const positionDelta = Math.abs(currentPosition - this.lastScrollPosition);
|
|
476
|
+
if (timeDelta > 0) {
|
|
477
|
+
this.scrollVelocity = positionDelta / timeDelta * 1e3;
|
|
478
|
+
}
|
|
479
|
+
this.lastScrollPosition = currentPosition;
|
|
480
|
+
this.lastScrollTime = now;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Calculate current scroll depth as percentage
|
|
484
|
+
*/
|
|
485
|
+
calculateScrollDepth() {
|
|
486
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
487
|
+
return 0;
|
|
488
|
+
const windowHeight = window.innerHeight;
|
|
489
|
+
const documentHeight = Math.max(
|
|
490
|
+
document.body.scrollHeight,
|
|
491
|
+
document.body.offsetHeight,
|
|
492
|
+
document.documentElement.clientHeight,
|
|
493
|
+
document.documentElement.scrollHeight,
|
|
494
|
+
document.documentElement.offsetHeight
|
|
495
|
+
);
|
|
496
|
+
const scrollTop = window.scrollY;
|
|
497
|
+
const scrollableHeight = documentHeight - windowHeight;
|
|
498
|
+
if (scrollableHeight <= 0)
|
|
499
|
+
return 100;
|
|
500
|
+
return Math.round(scrollTop / scrollableHeight * 100);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Determine if section view should be tracked (sanitization)
|
|
504
|
+
*/
|
|
505
|
+
shouldTrackSection(viewData) {
|
|
506
|
+
if (viewData.duration < this.options.minDwellTime) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
const avgScrollSpeed = (viewData.scrollSpeedAtEntry + viewData.scrollSpeedAtExit) / 2;
|
|
510
|
+
if (avgScrollSpeed > this.options.scrollVelocityThreshold * 2) {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
if (viewData.visibleAreaPercentage < 10) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Queue section view for batching
|
|
520
|
+
*/
|
|
521
|
+
queueSectionView(viewData) {
|
|
522
|
+
this.pendingEvents.push(viewData);
|
|
523
|
+
this.log("Queued section view:", viewData.sectionName, "duration:", viewData.duration);
|
|
524
|
+
if (this.batchTimer === null) {
|
|
525
|
+
this.batchTimer = window.setTimeout(() => {
|
|
526
|
+
this.flushPendingEvents();
|
|
527
|
+
}, this.options.batchDelay);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Flush pending section view events
|
|
532
|
+
*/
|
|
533
|
+
flushPendingEvents() {
|
|
534
|
+
if (this.isDestroyed || this.pendingEvents.length === 0)
|
|
535
|
+
return;
|
|
536
|
+
if (!this.tracker.hasConsent("analytics")) {
|
|
537
|
+
this.pendingEvents = [];
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.log("Flushing", this.pendingEvents.length, "section view events");
|
|
541
|
+
for (const viewData of this.pendingEvents) {
|
|
542
|
+
this.tracker.trackSystemEvent("_grain_section_view", {
|
|
543
|
+
section_name: viewData.sectionName,
|
|
544
|
+
section_type: viewData.sectionType,
|
|
545
|
+
duration_ms: viewData.duration,
|
|
546
|
+
viewport_width: viewData.viewportWidth,
|
|
547
|
+
viewport_height: viewData.viewportHeight,
|
|
548
|
+
scroll_depth_percent: viewData.scrollDepth,
|
|
549
|
+
visible_area_percent: viewData.visibleAreaPercentage,
|
|
550
|
+
scroll_speed_entry: Math.round(viewData.scrollSpeedAtEntry || 0),
|
|
551
|
+
scroll_speed_exit: Math.round(viewData.scrollSpeedAtExit || 0),
|
|
552
|
+
entry_timestamp: viewData.entryTime,
|
|
553
|
+
exit_timestamp: viewData.exitTime
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
this.pendingEvents = [];
|
|
557
|
+
this.batchTimer = null;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Find element by XPath selector
|
|
561
|
+
*/
|
|
562
|
+
findElementByXPath(xpath) {
|
|
563
|
+
if (this.xpathCache.has(xpath)) {
|
|
564
|
+
const cached = this.xpathCache.get(xpath);
|
|
565
|
+
if (cached && document.contains(cached)) {
|
|
566
|
+
return cached;
|
|
567
|
+
}
|
|
568
|
+
this.xpathCache.delete(xpath);
|
|
569
|
+
}
|
|
570
|
+
try {
|
|
571
|
+
const result = document.evaluate(
|
|
572
|
+
xpath,
|
|
573
|
+
document,
|
|
574
|
+
null,
|
|
575
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
576
|
+
null
|
|
577
|
+
);
|
|
578
|
+
const element = result.singleNodeValue;
|
|
579
|
+
if (element) {
|
|
580
|
+
this.xpathCache.set(xpath, element);
|
|
581
|
+
}
|
|
582
|
+
return element;
|
|
583
|
+
} catch (error) {
|
|
584
|
+
this.log("Error evaluating XPath:", xpath, error);
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Log debug messages
|
|
590
|
+
*/
|
|
591
|
+
log(...args) {
|
|
592
|
+
if (this.options.debug) {
|
|
593
|
+
console.log("[SectionTracking]", ...args);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Update sections configuration
|
|
598
|
+
*/
|
|
599
|
+
updateSections(sections) {
|
|
600
|
+
if (this.isDestroyed)
|
|
601
|
+
return;
|
|
602
|
+
this.log("Updating sections configuration");
|
|
603
|
+
if (this.intersectionObserver) {
|
|
604
|
+
this.intersectionObserver.disconnect();
|
|
605
|
+
}
|
|
606
|
+
this.sectionStates.clear();
|
|
607
|
+
this.xpathCache.clear();
|
|
608
|
+
this.sections = sections;
|
|
609
|
+
this.setupIntersectionObserver();
|
|
610
|
+
this.initializeSections();
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Cleanup and destroy
|
|
614
|
+
*/
|
|
615
|
+
destroy() {
|
|
616
|
+
if (this.isDestroyed)
|
|
617
|
+
return;
|
|
618
|
+
this.log("Destroying section tracking manager");
|
|
619
|
+
this.isDestroyed = true;
|
|
620
|
+
this.flushPendingEvents();
|
|
621
|
+
if (this.scrollDebounceTimer !== null) {
|
|
622
|
+
clearTimeout(this.scrollDebounceTimer);
|
|
623
|
+
this.scrollDebounceTimer = null;
|
|
624
|
+
}
|
|
625
|
+
if (this.batchTimer !== null) {
|
|
626
|
+
clearTimeout(this.batchTimer);
|
|
627
|
+
this.batchTimer = null;
|
|
628
|
+
}
|
|
629
|
+
if (this.intersectionObserver) {
|
|
630
|
+
this.intersectionObserver.disconnect();
|
|
631
|
+
this.intersectionObserver = null;
|
|
632
|
+
}
|
|
633
|
+
this.sectionStates.clear();
|
|
634
|
+
this.xpathCache.clear();
|
|
635
|
+
this.pendingEvents = [];
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
22
641
|
// src/index.ts
|
|
23
642
|
var src_exports = {};
|
|
24
643
|
__export(src_exports, {
|
|
@@ -3878,6 +4497,9 @@ var Grain = (() => {
|
|
|
3878
4497
|
this.pageTrackingManager = null;
|
|
3879
4498
|
this.ephemeralSessionId = null;
|
|
3880
4499
|
this.eventCountSinceLastHeartbeat = 0;
|
|
4500
|
+
// Auto-tracking properties
|
|
4501
|
+
this.interactionTrackingManager = null;
|
|
4502
|
+
this.sectionTrackingManager = null;
|
|
3881
4503
|
// Session tracking
|
|
3882
4504
|
this.sessionStartTime = Date.now();
|
|
3883
4505
|
this.sessionEventCount = 0;
|
|
@@ -4396,6 +5018,86 @@ var Grain = (() => {
|
|
|
4396
5018
|
this.log("Failed to initialize page view tracking:", error);
|
|
4397
5019
|
}
|
|
4398
5020
|
}
|
|
5021
|
+
this.initializeAutoTracking();
|
|
5022
|
+
}
|
|
5023
|
+
/**
|
|
5024
|
+
* Initialize auto-tracking (interactions and sections)
|
|
5025
|
+
*/
|
|
5026
|
+
async initializeAutoTracking() {
|
|
5027
|
+
try {
|
|
5028
|
+
const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
|
|
5029
|
+
const currentUrl = typeof window !== "undefined" ? window.location.href : "";
|
|
5030
|
+
const request = {
|
|
5031
|
+
userId,
|
|
5032
|
+
immediateKeys: [],
|
|
5033
|
+
properties: {},
|
|
5034
|
+
currentUrl
|
|
5035
|
+
// Add current URL to request
|
|
5036
|
+
};
|
|
5037
|
+
const headers = await this.getAuthHeaders();
|
|
5038
|
+
const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
|
|
5039
|
+
const response = await fetch(url, {
|
|
5040
|
+
method: "POST",
|
|
5041
|
+
headers,
|
|
5042
|
+
body: JSON.stringify(request)
|
|
5043
|
+
});
|
|
5044
|
+
if (response.ok) {
|
|
5045
|
+
const configResponse = await response.json();
|
|
5046
|
+
if (configResponse.autoTrackingConfig) {
|
|
5047
|
+
this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
|
|
5048
|
+
}
|
|
5049
|
+
}
|
|
5050
|
+
} catch (error) {
|
|
5051
|
+
this.log("Failed to initialize auto-tracking:", error);
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
/**
|
|
5055
|
+
* Setup auto-tracking managers
|
|
5056
|
+
*/
|
|
5057
|
+
setupAutoTrackingManagers(config) {
|
|
5058
|
+
Promise.resolve().then(() => (init_interaction_tracking(), interaction_tracking_exports)).then(({ InteractionTrackingManager: InteractionTrackingManager2 }) => {
|
|
5059
|
+
try {
|
|
5060
|
+
if (config.interactions && config.interactions.length > 0) {
|
|
5061
|
+
this.interactionTrackingManager = new InteractionTrackingManager2(
|
|
5062
|
+
this,
|
|
5063
|
+
config.interactions,
|
|
5064
|
+
{
|
|
5065
|
+
debug: this.config.debug,
|
|
5066
|
+
enableMutationObserver: true,
|
|
5067
|
+
mutationDebounceDelay: 500
|
|
5068
|
+
}
|
|
5069
|
+
);
|
|
5070
|
+
this.log("Interaction tracking initialized with", config.interactions.length, "interactions");
|
|
5071
|
+
}
|
|
5072
|
+
} catch (error) {
|
|
5073
|
+
this.log("Failed to initialize interaction tracking:", error);
|
|
5074
|
+
}
|
|
5075
|
+
}).catch((error) => {
|
|
5076
|
+
this.log("Failed to load interaction tracking module:", error);
|
|
5077
|
+
});
|
|
5078
|
+
Promise.resolve().then(() => (init_section_tracking(), section_tracking_exports)).then(({ SectionTrackingManager: SectionTrackingManager2 }) => {
|
|
5079
|
+
try {
|
|
5080
|
+
if (config.sections && config.sections.length > 0) {
|
|
5081
|
+
this.sectionTrackingManager = new SectionTrackingManager2(
|
|
5082
|
+
this,
|
|
5083
|
+
config.sections,
|
|
5084
|
+
{
|
|
5085
|
+
minDwellTime: 1e3,
|
|
5086
|
+
scrollVelocityThreshold: 500,
|
|
5087
|
+
intersectionThreshold: 0.1,
|
|
5088
|
+
debounceDelay: 100,
|
|
5089
|
+
batchDelay: 2e3,
|
|
5090
|
+
debug: this.config.debug
|
|
5091
|
+
}
|
|
5092
|
+
);
|
|
5093
|
+
this.log("Section tracking initialized with", config.sections.length, "sections");
|
|
5094
|
+
}
|
|
5095
|
+
} catch (error) {
|
|
5096
|
+
this.log("Failed to initialize section tracking:", error);
|
|
5097
|
+
}
|
|
5098
|
+
}).catch((error) => {
|
|
5099
|
+
this.log("Failed to load section tracking module:", error);
|
|
5100
|
+
});
|
|
4399
5101
|
}
|
|
4400
5102
|
/**
|
|
4401
5103
|
* Track session start event
|
|
@@ -5355,6 +6057,14 @@ var Grain = (() => {
|
|
|
5355
6057
|
this.activityDetector.destroy();
|
|
5356
6058
|
this.activityDetector = null;
|
|
5357
6059
|
}
|
|
6060
|
+
if (this.interactionTrackingManager) {
|
|
6061
|
+
this.interactionTrackingManager.destroy();
|
|
6062
|
+
this.interactionTrackingManager = null;
|
|
6063
|
+
}
|
|
6064
|
+
if (this.sectionTrackingManager) {
|
|
6065
|
+
this.sectionTrackingManager.destroy();
|
|
6066
|
+
this.sectionTrackingManager = null;
|
|
6067
|
+
}
|
|
5358
6068
|
if (this.eventQueue.length > 0) {
|
|
5359
6069
|
const eventsToSend = [...this.eventQueue];
|
|
5360
6070
|
this.eventQueue = [];
|