@grainql/analytics-web 2.4.0 → 2.5.3
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 +29 -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 +74 -0
- package/dist/cjs/interaction-tracking.d.ts.map +1 -0
- package/dist/cjs/interaction-tracking.js +292 -0
- package/dist/cjs/interaction-tracking.js.map +1 -0
- package/dist/cjs/section-tracking.d.ts +101 -0
- package/dist/cjs/section-tracking.d.ts.map +1 -0
- package/dist/cjs/section-tracking.js +455 -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 +29 -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 +74 -0
- package/dist/esm/interaction-tracking.d.ts.map +1 -0
- package/dist/esm/interaction-tracking.js +288 -0
- package/dist/esm/interaction-tracking.js.map +1 -0
- package/dist/esm/section-tracking.d.ts +101 -0
- package/dist/esm/section-tracking.d.ts.map +1 -0
- package/dist/esm/section-tracking.js +451 -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 +29 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +821 -3
- 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 +149 -4
- package/dist/index.mjs +116 -4
- package/dist/interaction-tracking.d.ts +74 -0
- package/dist/interaction-tracking.d.ts.map +1 -0
- package/dist/interaction-tracking.js +292 -0
- package/dist/section-tracking.d.ts +101 -0
- package/dist/section-tracking.d.ts.map +1 -0
- package/dist/section-tracking.js +455 -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.3 | 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,714 @@ 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
|
+
const isNavigationLink = element instanceof HTMLAnchorElement && element.href;
|
|
107
|
+
const eventProperties = {
|
|
108
|
+
interaction_type: "click",
|
|
109
|
+
interaction_label: interaction.label,
|
|
110
|
+
interaction_description: interaction.description,
|
|
111
|
+
interaction_priority: interaction.priority,
|
|
112
|
+
element_tag: element.tagName?.toLowerCase(),
|
|
113
|
+
element_text: element.textContent?.trim().substring(0, 100),
|
|
114
|
+
element_id: element.id || void 0,
|
|
115
|
+
element_class: element.className || void 0,
|
|
116
|
+
...isNavigationLink && { href: element.href },
|
|
117
|
+
timestamp: Date.now()
|
|
118
|
+
};
|
|
119
|
+
if (isNavigationLink) {
|
|
120
|
+
const result = this.tracker.track(interaction.eventName, eventProperties, { flush: true });
|
|
121
|
+
if (result instanceof Promise) {
|
|
122
|
+
result.catch((error) => {
|
|
123
|
+
this.log("Failed to track navigation click:", error);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
this.tracker.track(interaction.eventName, eventProperties);
|
|
128
|
+
}
|
|
129
|
+
this.log("Tracked click interaction:", interaction.eventName);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Handle focus event on interaction (for form fields)
|
|
133
|
+
*/
|
|
134
|
+
handleInteractionFocus(interaction, event) {
|
|
135
|
+
if (this.isDestroyed)
|
|
136
|
+
return;
|
|
137
|
+
if (!this.tracker.hasConsent("analytics"))
|
|
138
|
+
return;
|
|
139
|
+
const element = event.target;
|
|
140
|
+
this.tracker.track(interaction.eventName, {
|
|
141
|
+
interaction_type: "focus",
|
|
142
|
+
interaction_label: interaction.label,
|
|
143
|
+
interaction_description: interaction.description,
|
|
144
|
+
interaction_priority: interaction.priority,
|
|
145
|
+
element_tag: element.tagName?.toLowerCase(),
|
|
146
|
+
element_id: element.id || void 0,
|
|
147
|
+
element_class: element.className || void 0,
|
|
148
|
+
timestamp: Date.now()
|
|
149
|
+
});
|
|
150
|
+
this.log("Tracked focus interaction:", interaction.eventName);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Find element by XPath selector
|
|
154
|
+
*/
|
|
155
|
+
findElementByXPath(xpath) {
|
|
156
|
+
if (this.xpathCache.has(xpath)) {
|
|
157
|
+
const cached = this.xpathCache.get(xpath);
|
|
158
|
+
if (cached && document.contains(cached)) {
|
|
159
|
+
return cached;
|
|
160
|
+
}
|
|
161
|
+
this.xpathCache.delete(xpath);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
let cleanXpath = xpath;
|
|
165
|
+
if (xpath.startsWith("xpath=")) {
|
|
166
|
+
cleanXpath = xpath.substring(6);
|
|
167
|
+
}
|
|
168
|
+
const result = document.evaluate(
|
|
169
|
+
cleanXpath,
|
|
170
|
+
document,
|
|
171
|
+
null,
|
|
172
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
173
|
+
null
|
|
174
|
+
);
|
|
175
|
+
const element = result.singleNodeValue;
|
|
176
|
+
if (element) {
|
|
177
|
+
this.xpathCache.set(xpath, element);
|
|
178
|
+
}
|
|
179
|
+
return element;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.log("Error evaluating XPath:", xpath, error);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Setup mutation observer to handle dynamic content
|
|
187
|
+
*/
|
|
188
|
+
setupMutationObserver() {
|
|
189
|
+
if (typeof MutationObserver === "undefined") {
|
|
190
|
+
this.log("MutationObserver not supported");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
194
|
+
if (this.mutationDebounceTimer !== null) {
|
|
195
|
+
clearTimeout(this.mutationDebounceTimer);
|
|
196
|
+
}
|
|
197
|
+
this.mutationDebounceTimer = window.setTimeout(() => {
|
|
198
|
+
this.handleMutations(mutations);
|
|
199
|
+
this.mutationDebounceTimer = null;
|
|
200
|
+
}, this.config.mutationDebounceDelay);
|
|
201
|
+
});
|
|
202
|
+
this.mutationObserver.observe(document.body, {
|
|
203
|
+
childList: true,
|
|
204
|
+
subtree: true
|
|
205
|
+
});
|
|
206
|
+
this.log("Mutation observer setup");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Handle DOM mutations
|
|
210
|
+
*/
|
|
211
|
+
handleMutations(mutations) {
|
|
212
|
+
if (this.isDestroyed)
|
|
213
|
+
return;
|
|
214
|
+
this.xpathCache.clear();
|
|
215
|
+
const removedElements = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const mutation of mutations) {
|
|
217
|
+
mutation.removedNodes.forEach((node) => {
|
|
218
|
+
if (node instanceof Element) {
|
|
219
|
+
removedElements.add(node);
|
|
220
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
221
|
+
if (node.contains(element)) {
|
|
222
|
+
removedElements.add(element);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
removedElements.forEach((element) => {
|
|
229
|
+
this.detachListeners(element);
|
|
230
|
+
});
|
|
231
|
+
this.attachAllListeners();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Detach listeners from an element
|
|
235
|
+
*/
|
|
236
|
+
detachListeners(element) {
|
|
237
|
+
const handlers = this.attachedListeners.get(element);
|
|
238
|
+
if (!handlers)
|
|
239
|
+
return;
|
|
240
|
+
handlers.forEach(({ event, handler }) => {
|
|
241
|
+
element.removeEventListener(event, handler);
|
|
242
|
+
});
|
|
243
|
+
this.attachedListeners.delete(element);
|
|
244
|
+
this.log("Detached listeners from element");
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Log debug messages
|
|
248
|
+
*/
|
|
249
|
+
log(...args) {
|
|
250
|
+
if (this.config.debug) {
|
|
251
|
+
console.log("[InteractionTracking]", ...args);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Update interactions configuration
|
|
256
|
+
*/
|
|
257
|
+
updateInteractions(interactions) {
|
|
258
|
+
if (this.isDestroyed)
|
|
259
|
+
return;
|
|
260
|
+
this.log("Updating interactions configuration");
|
|
261
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
262
|
+
this.detachListeners(element);
|
|
263
|
+
});
|
|
264
|
+
this.xpathCache.clear();
|
|
265
|
+
this.interactions = interactions;
|
|
266
|
+
this.attachAllListeners();
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Cleanup and destroy
|
|
270
|
+
*/
|
|
271
|
+
destroy() {
|
|
272
|
+
if (this.isDestroyed)
|
|
273
|
+
return;
|
|
274
|
+
this.log("Destroying interaction tracking manager");
|
|
275
|
+
this.isDestroyed = true;
|
|
276
|
+
if (this.mutationDebounceTimer !== null) {
|
|
277
|
+
clearTimeout(this.mutationDebounceTimer);
|
|
278
|
+
this.mutationDebounceTimer = null;
|
|
279
|
+
}
|
|
280
|
+
if (this.mutationObserver) {
|
|
281
|
+
this.mutationObserver.disconnect();
|
|
282
|
+
this.mutationObserver = null;
|
|
283
|
+
}
|
|
284
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
285
|
+
this.detachListeners(element);
|
|
286
|
+
});
|
|
287
|
+
this.attachedListeners.clear();
|
|
288
|
+
this.xpathCache.clear();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// src/section-tracking.ts
|
|
295
|
+
var section_tracking_exports = {};
|
|
296
|
+
__export(section_tracking_exports, {
|
|
297
|
+
SectionTrackingManager: () => SectionTrackingManager
|
|
298
|
+
});
|
|
299
|
+
var DEFAULT_OPTIONS, SectionTrackingManager;
|
|
300
|
+
var init_section_tracking = __esm({
|
|
301
|
+
"src/section-tracking.ts"() {
|
|
302
|
+
"use strict";
|
|
303
|
+
DEFAULT_OPTIONS = {
|
|
304
|
+
minDwellTime: 1e3,
|
|
305
|
+
// 1 second minimum
|
|
306
|
+
scrollVelocityThreshold: 500,
|
|
307
|
+
// 500px/s
|
|
308
|
+
intersectionThreshold: 0.1,
|
|
309
|
+
// 10% visible
|
|
310
|
+
debounceDelay: 100,
|
|
311
|
+
batchDelay: 2e3,
|
|
312
|
+
// 2 seconds
|
|
313
|
+
debug: false
|
|
314
|
+
};
|
|
315
|
+
SectionTrackingManager = class {
|
|
316
|
+
// 3 seconds
|
|
317
|
+
constructor(tracker, sections, options = {}) {
|
|
318
|
+
this.isDestroyed = false;
|
|
319
|
+
// Tracking state
|
|
320
|
+
this.sectionStates = /* @__PURE__ */ new Map();
|
|
321
|
+
this.intersectionObserver = null;
|
|
322
|
+
this.xpathCache = /* @__PURE__ */ new Map();
|
|
323
|
+
// Scroll tracking
|
|
324
|
+
this.lastScrollPosition = 0;
|
|
325
|
+
this.lastScrollTime = Date.now();
|
|
326
|
+
this.scrollVelocity = 0;
|
|
327
|
+
this.scrollDebounceTimer = null;
|
|
328
|
+
// Event batching
|
|
329
|
+
this.pendingEvents = [];
|
|
330
|
+
this.batchTimer = null;
|
|
331
|
+
// Periodic tracking for long-duration views
|
|
332
|
+
this.sectionTimers = /* @__PURE__ */ new Map();
|
|
333
|
+
// sectionName -> timer ID
|
|
334
|
+
this.SPLIT_DURATION = 3e3;
|
|
335
|
+
this.tracker = tracker;
|
|
336
|
+
this.sections = sections;
|
|
337
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
338
|
+
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
|
339
|
+
if (document.readyState === "loading") {
|
|
340
|
+
document.addEventListener("DOMContentLoaded", () => this.initialize());
|
|
341
|
+
} else {
|
|
342
|
+
setTimeout(() => this.initialize(), 0);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Initialize section tracking
|
|
348
|
+
*/
|
|
349
|
+
initialize() {
|
|
350
|
+
if (this.isDestroyed)
|
|
351
|
+
return;
|
|
352
|
+
this.log("Initializing section tracking for", this.sections.length, "sections");
|
|
353
|
+
this.setupIntersectionObserver();
|
|
354
|
+
this.setupScrollListener();
|
|
355
|
+
this.initializeSections();
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Setup IntersectionObserver for section visibility
|
|
359
|
+
*/
|
|
360
|
+
setupIntersectionObserver() {
|
|
361
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
362
|
+
this.log("IntersectionObserver not supported");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
this.intersectionObserver = new IntersectionObserver(
|
|
366
|
+
(entries) => {
|
|
367
|
+
entries.forEach((entry) => {
|
|
368
|
+
this.handleIntersection(entry);
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
threshold: [0, 0.1, 0.25, 0.5, 0.75, 1],
|
|
373
|
+
rootMargin: "0px"
|
|
374
|
+
}
|
|
375
|
+
);
|
|
376
|
+
this.log("IntersectionObserver created");
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Setup scroll listener for velocity calculation
|
|
380
|
+
*/
|
|
381
|
+
setupScrollListener() {
|
|
382
|
+
if (typeof window === "undefined")
|
|
383
|
+
return;
|
|
384
|
+
const scrollHandler = () => {
|
|
385
|
+
if (this.scrollDebounceTimer !== null) {
|
|
386
|
+
clearTimeout(this.scrollDebounceTimer);
|
|
387
|
+
}
|
|
388
|
+
this.scrollDebounceTimer = window.setTimeout(() => {
|
|
389
|
+
this.updateScrollVelocity();
|
|
390
|
+
this.scrollDebounceTimer = null;
|
|
391
|
+
}, this.options.debounceDelay);
|
|
392
|
+
};
|
|
393
|
+
window.addEventListener("scroll", scrollHandler, { passive: true });
|
|
394
|
+
this.log("Scroll listener attached");
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Initialize sections and start observing
|
|
398
|
+
*/
|
|
399
|
+
initializeSections() {
|
|
400
|
+
for (const section of this.sections) {
|
|
401
|
+
const element = this.findElementByXPath(section.selector);
|
|
402
|
+
if (!element) {
|
|
403
|
+
this.log("Section element not found:", section.sectionName, "selector:", section.selector);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const state = {
|
|
407
|
+
element,
|
|
408
|
+
config: section,
|
|
409
|
+
entryTime: null,
|
|
410
|
+
exitTime: null,
|
|
411
|
+
isVisible: false,
|
|
412
|
+
lastScrollPosition: window.scrollY,
|
|
413
|
+
lastScrollTime: Date.now(),
|
|
414
|
+
entryScrollSpeed: 0,
|
|
415
|
+
exitScrollSpeed: 0,
|
|
416
|
+
maxVisibleArea: 0
|
|
417
|
+
};
|
|
418
|
+
this.sectionStates.set(section.sectionName, state);
|
|
419
|
+
if (this.intersectionObserver) {
|
|
420
|
+
this.intersectionObserver.observe(element);
|
|
421
|
+
}
|
|
422
|
+
this.log("Section initialized and observed:", section.sectionName);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Handle intersection observer entry
|
|
427
|
+
*/
|
|
428
|
+
handleIntersection(entry) {
|
|
429
|
+
if (this.isDestroyed)
|
|
430
|
+
return;
|
|
431
|
+
const state = Array.from(this.sectionStates.values()).find(
|
|
432
|
+
(s) => s.element === entry.target
|
|
433
|
+
);
|
|
434
|
+
if (!state)
|
|
435
|
+
return;
|
|
436
|
+
const isVisible = entry.isIntersecting && entry.intersectionRatio >= this.options.intersectionThreshold;
|
|
437
|
+
const visibleArea = entry.intersectionRatio;
|
|
438
|
+
if (visibleArea > state.maxVisibleArea) {
|
|
439
|
+
state.maxVisibleArea = visibleArea;
|
|
440
|
+
}
|
|
441
|
+
if (isVisible && !state.isVisible) {
|
|
442
|
+
this.handleSectionEntry(state);
|
|
443
|
+
} else if (!isVisible && state.isVisible) {
|
|
444
|
+
this.handleSectionExit(state);
|
|
445
|
+
}
|
|
446
|
+
state.isVisible = isVisible;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Handle section entry (became visible)
|
|
450
|
+
*/
|
|
451
|
+
handleSectionEntry(state) {
|
|
452
|
+
this.log("Section entered view:", state.config.sectionName);
|
|
453
|
+
state.entryTime = Date.now();
|
|
454
|
+
state.entryScrollSpeed = this.scrollVelocity;
|
|
455
|
+
state.lastScrollPosition = window.scrollY;
|
|
456
|
+
state.lastScrollTime = Date.now();
|
|
457
|
+
state.maxVisibleArea = 0;
|
|
458
|
+
this.startPeriodicTracking(state);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Start periodic tracking for a section (sends events every 3 seconds)
|
|
462
|
+
*/
|
|
463
|
+
startPeriodicTracking(state) {
|
|
464
|
+
this.stopPeriodicTracking(state.config.sectionName);
|
|
465
|
+
const timerId = window.setInterval(() => {
|
|
466
|
+
if (this.isDestroyed || !state.isVisible || state.entryTime === null) {
|
|
467
|
+
this.stopPeriodicTracking(state.config.sectionName);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
const duration = now - state.entryTime;
|
|
472
|
+
if (duration >= this.options.minDwellTime) {
|
|
473
|
+
const viewData = {
|
|
474
|
+
sectionName: state.config.sectionName,
|
|
475
|
+
sectionType: state.config.sectionType,
|
|
476
|
+
entryTime: state.entryTime,
|
|
477
|
+
exitTime: now,
|
|
478
|
+
// Current time as "exit" for this split
|
|
479
|
+
duration,
|
|
480
|
+
viewportWidth: window.innerWidth,
|
|
481
|
+
viewportHeight: window.innerHeight,
|
|
482
|
+
scrollDepth: this.calculateScrollDepth(),
|
|
483
|
+
visibleAreaPercentage: Math.round(state.maxVisibleArea * 100),
|
|
484
|
+
scrollSpeedAtEntry: state.entryScrollSpeed,
|
|
485
|
+
scrollSpeedAtExit: this.scrollVelocity
|
|
486
|
+
};
|
|
487
|
+
if (this.shouldTrackSection(viewData)) {
|
|
488
|
+
this.tracker.trackSystemEvent("_grain_section_view", {
|
|
489
|
+
section_name: viewData.sectionName,
|
|
490
|
+
section_type: viewData.sectionType,
|
|
491
|
+
duration_ms: viewData.duration,
|
|
492
|
+
viewport_width: viewData.viewportWidth,
|
|
493
|
+
viewport_height: viewData.viewportHeight,
|
|
494
|
+
scroll_depth_percent: viewData.scrollDepth,
|
|
495
|
+
visible_area_percent: viewData.visibleAreaPercentage,
|
|
496
|
+
scroll_speed_entry: Math.round(viewData.scrollSpeedAtEntry || 0),
|
|
497
|
+
scroll_speed_exit: Math.round(viewData.scrollSpeedAtExit || 0),
|
|
498
|
+
entry_timestamp: viewData.entryTime,
|
|
499
|
+
exit_timestamp: viewData.exitTime,
|
|
500
|
+
is_split: true
|
|
501
|
+
// Flag to indicate this is a periodic split, not final exit
|
|
502
|
+
});
|
|
503
|
+
this.log("Tracked periodic section view split:", state.config.sectionName, "duration:", duration);
|
|
504
|
+
state.entryTime = now;
|
|
505
|
+
state.entryScrollSpeed = this.scrollVelocity;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}, this.SPLIT_DURATION);
|
|
509
|
+
this.sectionTimers.set(state.config.sectionName, timerId);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Stop periodic tracking for a section
|
|
513
|
+
*/
|
|
514
|
+
stopPeriodicTracking(sectionName) {
|
|
515
|
+
const timerId = this.sectionTimers.get(sectionName);
|
|
516
|
+
if (timerId !== void 0) {
|
|
517
|
+
clearInterval(timerId);
|
|
518
|
+
this.sectionTimers.delete(sectionName);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Handle section exit (became invisible)
|
|
523
|
+
*/
|
|
524
|
+
handleSectionExit(state) {
|
|
525
|
+
this.log("Section exited view:", state.config.sectionName);
|
|
526
|
+
this.stopPeriodicTracking(state.config.sectionName);
|
|
527
|
+
if (state.entryTime === null)
|
|
528
|
+
return;
|
|
529
|
+
state.exitTime = Date.now();
|
|
530
|
+
state.exitScrollSpeed = this.scrollVelocity;
|
|
531
|
+
const duration = state.exitTime - state.entryTime;
|
|
532
|
+
const viewData = {
|
|
533
|
+
sectionName: state.config.sectionName,
|
|
534
|
+
sectionType: state.config.sectionType,
|
|
535
|
+
entryTime: state.entryTime,
|
|
536
|
+
exitTime: state.exitTime,
|
|
537
|
+
duration,
|
|
538
|
+
viewportWidth: window.innerWidth,
|
|
539
|
+
viewportHeight: window.innerHeight,
|
|
540
|
+
scrollDepth: this.calculateScrollDepth(),
|
|
541
|
+
visibleAreaPercentage: Math.round(state.maxVisibleArea * 100),
|
|
542
|
+
scrollSpeedAtEntry: state.entryScrollSpeed,
|
|
543
|
+
scrollSpeedAtExit: state.exitScrollSpeed
|
|
544
|
+
};
|
|
545
|
+
if (this.shouldTrackSection(viewData)) {
|
|
546
|
+
this.queueSectionView(viewData);
|
|
547
|
+
} else {
|
|
548
|
+
this.log("Section view filtered out:", state.config.sectionName, "duration:", duration);
|
|
549
|
+
}
|
|
550
|
+
state.entryTime = null;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Update scroll velocity
|
|
554
|
+
*/
|
|
555
|
+
updateScrollVelocity() {
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
const currentPosition = window.scrollY;
|
|
558
|
+
const timeDelta = now - this.lastScrollTime;
|
|
559
|
+
const positionDelta = Math.abs(currentPosition - this.lastScrollPosition);
|
|
560
|
+
if (timeDelta > 0) {
|
|
561
|
+
this.scrollVelocity = positionDelta / timeDelta * 1e3;
|
|
562
|
+
}
|
|
563
|
+
this.lastScrollPosition = currentPosition;
|
|
564
|
+
this.lastScrollTime = now;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Calculate current scroll depth as percentage
|
|
568
|
+
*/
|
|
569
|
+
calculateScrollDepth() {
|
|
570
|
+
if (typeof window === "undefined" || typeof document === "undefined")
|
|
571
|
+
return 0;
|
|
572
|
+
const windowHeight = window.innerHeight;
|
|
573
|
+
const documentHeight = Math.max(
|
|
574
|
+
document.body.scrollHeight,
|
|
575
|
+
document.body.offsetHeight,
|
|
576
|
+
document.documentElement.clientHeight,
|
|
577
|
+
document.documentElement.scrollHeight,
|
|
578
|
+
document.documentElement.offsetHeight
|
|
579
|
+
);
|
|
580
|
+
const scrollTop = window.scrollY;
|
|
581
|
+
const scrollableHeight = documentHeight - windowHeight;
|
|
582
|
+
if (scrollableHeight <= 0)
|
|
583
|
+
return 100;
|
|
584
|
+
return Math.round(scrollTop / scrollableHeight * 100);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Determine if section view should be tracked (sanitization)
|
|
588
|
+
*/
|
|
589
|
+
shouldTrackSection(viewData) {
|
|
590
|
+
if (viewData.duration < this.options.minDwellTime) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
const avgScrollSpeed = (viewData.scrollSpeedAtEntry + viewData.scrollSpeedAtExit) / 2;
|
|
594
|
+
if (avgScrollSpeed > this.options.scrollVelocityThreshold * 2) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
if (viewData.visibleAreaPercentage < 10) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Queue section view for batching
|
|
604
|
+
*/
|
|
605
|
+
queueSectionView(viewData) {
|
|
606
|
+
this.pendingEvents.push(viewData);
|
|
607
|
+
this.log("Queued section view:", viewData.sectionName, "duration:", viewData.duration);
|
|
608
|
+
if (this.batchTimer === null) {
|
|
609
|
+
this.batchTimer = window.setTimeout(() => {
|
|
610
|
+
this.flushPendingEvents();
|
|
611
|
+
}, this.options.batchDelay);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Flush pending section view events
|
|
616
|
+
*/
|
|
617
|
+
flushPendingEvents() {
|
|
618
|
+
if (this.isDestroyed || this.pendingEvents.length === 0)
|
|
619
|
+
return;
|
|
620
|
+
if (!this.tracker.hasConsent("analytics")) {
|
|
621
|
+
this.pendingEvents = [];
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
this.log("Flushing", this.pendingEvents.length, "section view events");
|
|
625
|
+
for (const viewData of this.pendingEvents) {
|
|
626
|
+
this.tracker.trackSystemEvent("_grain_section_view", {
|
|
627
|
+
section_name: viewData.sectionName,
|
|
628
|
+
section_type: viewData.sectionType,
|
|
629
|
+
duration_ms: viewData.duration,
|
|
630
|
+
viewport_width: viewData.viewportWidth,
|
|
631
|
+
viewport_height: viewData.viewportHeight,
|
|
632
|
+
scroll_depth_percent: viewData.scrollDepth,
|
|
633
|
+
visible_area_percent: viewData.visibleAreaPercentage,
|
|
634
|
+
scroll_speed_entry: Math.round(viewData.scrollSpeedAtEntry || 0),
|
|
635
|
+
scroll_speed_exit: Math.round(viewData.scrollSpeedAtExit || 0),
|
|
636
|
+
entry_timestamp: viewData.entryTime,
|
|
637
|
+
exit_timestamp: viewData.exitTime
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
this.pendingEvents = [];
|
|
641
|
+
this.batchTimer = null;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Find element by XPath selector
|
|
645
|
+
*/
|
|
646
|
+
findElementByXPath(xpath) {
|
|
647
|
+
if (this.xpathCache.has(xpath)) {
|
|
648
|
+
const cached = this.xpathCache.get(xpath);
|
|
649
|
+
if (cached && document.contains(cached)) {
|
|
650
|
+
return cached;
|
|
651
|
+
}
|
|
652
|
+
this.xpathCache.delete(xpath);
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
let cleanXpath = xpath;
|
|
656
|
+
if (xpath.startsWith("xpath=")) {
|
|
657
|
+
cleanXpath = xpath.substring(6);
|
|
658
|
+
}
|
|
659
|
+
const result = document.evaluate(
|
|
660
|
+
cleanXpath,
|
|
661
|
+
document,
|
|
662
|
+
null,
|
|
663
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
664
|
+
null
|
|
665
|
+
);
|
|
666
|
+
const element = result.singleNodeValue;
|
|
667
|
+
if (element) {
|
|
668
|
+
this.xpathCache.set(xpath, element);
|
|
669
|
+
}
|
|
670
|
+
return element;
|
|
671
|
+
} catch (error) {
|
|
672
|
+
this.log("Error evaluating XPath:", xpath, error);
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Log debug messages
|
|
678
|
+
*/
|
|
679
|
+
log(...args) {
|
|
680
|
+
if (this.options.debug) {
|
|
681
|
+
console.log("[SectionTracking]", ...args);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Update sections configuration
|
|
686
|
+
*/
|
|
687
|
+
updateSections(sections) {
|
|
688
|
+
if (this.isDestroyed)
|
|
689
|
+
return;
|
|
690
|
+
this.log("Updating sections configuration");
|
|
691
|
+
if (this.intersectionObserver) {
|
|
692
|
+
this.intersectionObserver.disconnect();
|
|
693
|
+
}
|
|
694
|
+
this.sectionStates.clear();
|
|
695
|
+
this.xpathCache.clear();
|
|
696
|
+
this.sections = sections;
|
|
697
|
+
this.setupIntersectionObserver();
|
|
698
|
+
this.initializeSections();
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Cleanup and destroy
|
|
702
|
+
*/
|
|
703
|
+
destroy() {
|
|
704
|
+
if (this.isDestroyed)
|
|
705
|
+
return;
|
|
706
|
+
this.log("Destroying section tracking manager");
|
|
707
|
+
this.isDestroyed = true;
|
|
708
|
+
this.sectionTimers.forEach((timerId) => {
|
|
709
|
+
clearInterval(timerId);
|
|
710
|
+
});
|
|
711
|
+
this.sectionTimers.clear();
|
|
712
|
+
this.flushPendingEvents();
|
|
713
|
+
if (this.scrollDebounceTimer !== null) {
|
|
714
|
+
clearTimeout(this.scrollDebounceTimer);
|
|
715
|
+
this.scrollDebounceTimer = null;
|
|
716
|
+
}
|
|
717
|
+
if (this.batchTimer !== null) {
|
|
718
|
+
clearTimeout(this.batchTimer);
|
|
719
|
+
this.batchTimer = null;
|
|
720
|
+
}
|
|
721
|
+
if (this.intersectionObserver) {
|
|
722
|
+
this.intersectionObserver.disconnect();
|
|
723
|
+
this.intersectionObserver = null;
|
|
724
|
+
}
|
|
725
|
+
this.sectionStates.clear();
|
|
726
|
+
this.xpathCache.clear();
|
|
727
|
+
this.pendingEvents = [];
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
22
733
|
// src/index.ts
|
|
23
734
|
var src_exports = {};
|
|
24
735
|
__export(src_exports, {
|
|
@@ -3878,6 +4589,9 @@ var Grain = (() => {
|
|
|
3878
4589
|
this.pageTrackingManager = null;
|
|
3879
4590
|
this.ephemeralSessionId = null;
|
|
3880
4591
|
this.eventCountSinceLastHeartbeat = 0;
|
|
4592
|
+
// Auto-tracking properties
|
|
4593
|
+
this.interactionTrackingManager = null;
|
|
4594
|
+
this.sectionTrackingManager = null;
|
|
3881
4595
|
// Session tracking
|
|
3882
4596
|
this.sessionStartTime = Date.now();
|
|
3883
4597
|
this.sessionEventCount = 0;
|
|
@@ -4295,8 +5009,9 @@ var Grain = (() => {
|
|
|
4295
5009
|
try {
|
|
4296
5010
|
const headers = await this.getAuthHeaders();
|
|
4297
5011
|
const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
|
|
4298
|
-
const body = JSON.stringify(
|
|
4299
|
-
|
|
5012
|
+
const body = JSON.stringify(events);
|
|
5013
|
+
const needsAuth = this.config.authStrategy !== "NONE";
|
|
5014
|
+
if (!needsAuth && typeof navigator !== "undefined" && "sendBeacon" in navigator) {
|
|
4300
5015
|
const blob = new Blob([body], { type: "application/json" });
|
|
4301
5016
|
const success = navigator.sendBeacon(url, blob);
|
|
4302
5017
|
if (success) {
|
|
@@ -4396,6 +5111,101 @@ var Grain = (() => {
|
|
|
4396
5111
|
this.log("Failed to initialize page view tracking:", error);
|
|
4397
5112
|
}
|
|
4398
5113
|
}
|
|
5114
|
+
this.initializeAutoTracking();
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
5117
|
+
* Initialize auto-tracking (interactions and sections)
|
|
5118
|
+
*/
|
|
5119
|
+
async initializeAutoTracking() {
|
|
5120
|
+
try {
|
|
5121
|
+
this.log("Initializing auto-tracking...");
|
|
5122
|
+
const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
|
|
5123
|
+
const currentUrl = typeof window !== "undefined" ? window.location.href : "";
|
|
5124
|
+
const request = {
|
|
5125
|
+
userId,
|
|
5126
|
+
immediateKeys: [],
|
|
5127
|
+
properties: {},
|
|
5128
|
+
currentUrl
|
|
5129
|
+
// Add current URL to request
|
|
5130
|
+
};
|
|
5131
|
+
const headers = await this.getAuthHeaders();
|
|
5132
|
+
const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
|
|
5133
|
+
this.log("Fetching auto-tracking config from:", url);
|
|
5134
|
+
const response = await fetch(url, {
|
|
5135
|
+
method: "POST",
|
|
5136
|
+
headers,
|
|
5137
|
+
body: JSON.stringify(request)
|
|
5138
|
+
});
|
|
5139
|
+
if (!response.ok) {
|
|
5140
|
+
this.log("Failed to fetch auto-tracking config:", response.status, response.statusText);
|
|
5141
|
+
return;
|
|
5142
|
+
}
|
|
5143
|
+
const configResponse = await response.json();
|
|
5144
|
+
this.log("Received config response:", configResponse);
|
|
5145
|
+
if (configResponse.autoTrackingConfig) {
|
|
5146
|
+
this.log("Auto-tracking config found:", configResponse.autoTrackingConfig);
|
|
5147
|
+
this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
|
|
5148
|
+
} else {
|
|
5149
|
+
this.log("No auto-tracking config in response");
|
|
5150
|
+
}
|
|
5151
|
+
} catch (error) {
|
|
5152
|
+
this.log("Failed to initialize auto-tracking:", error);
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
/**
|
|
5156
|
+
* Setup auto-tracking managers
|
|
5157
|
+
*/
|
|
5158
|
+
setupAutoTrackingManagers(config) {
|
|
5159
|
+
this.log("Setting up auto-tracking managers...", config);
|
|
5160
|
+
if (config.interactions && config.interactions.length > 0) {
|
|
5161
|
+
this.log("Loading interaction tracking module for", config.interactions.length, "interactions");
|
|
5162
|
+
Promise.resolve().then(() => (init_interaction_tracking(), interaction_tracking_exports)).then(({ InteractionTrackingManager: InteractionTrackingManager2 }) => {
|
|
5163
|
+
try {
|
|
5164
|
+
this.interactionTrackingManager = new InteractionTrackingManager2(
|
|
5165
|
+
this,
|
|
5166
|
+
config.interactions,
|
|
5167
|
+
{
|
|
5168
|
+
debug: this.config.debug,
|
|
5169
|
+
enableMutationObserver: true,
|
|
5170
|
+
mutationDebounceDelay: 500
|
|
5171
|
+
}
|
|
5172
|
+
);
|
|
5173
|
+
this.log("\u2705 Interaction tracking initialized successfully with", config.interactions.length, "interactions");
|
|
5174
|
+
} catch (error) {
|
|
5175
|
+
this.log("\u274C Failed to initialize interaction tracking:", error);
|
|
5176
|
+
}
|
|
5177
|
+
}).catch((error) => {
|
|
5178
|
+
this.log("\u274C Failed to load interaction tracking module:", error);
|
|
5179
|
+
});
|
|
5180
|
+
} else {
|
|
5181
|
+
this.log("No interactions configured for auto-tracking");
|
|
5182
|
+
}
|
|
5183
|
+
if (config.sections && config.sections.length > 0) {
|
|
5184
|
+
this.log("Loading section tracking module for", config.sections.length, "sections");
|
|
5185
|
+
Promise.resolve().then(() => (init_section_tracking(), section_tracking_exports)).then(({ SectionTrackingManager: SectionTrackingManager2 }) => {
|
|
5186
|
+
try {
|
|
5187
|
+
this.sectionTrackingManager = new SectionTrackingManager2(
|
|
5188
|
+
this,
|
|
5189
|
+
config.sections,
|
|
5190
|
+
{
|
|
5191
|
+
minDwellTime: 1e3,
|
|
5192
|
+
scrollVelocityThreshold: 500,
|
|
5193
|
+
intersectionThreshold: 0.1,
|
|
5194
|
+
debounceDelay: 100,
|
|
5195
|
+
batchDelay: 2e3,
|
|
5196
|
+
debug: this.config.debug
|
|
5197
|
+
}
|
|
5198
|
+
);
|
|
5199
|
+
this.log("\u2705 Section tracking initialized successfully with", config.sections.length, "sections");
|
|
5200
|
+
} catch (error) {
|
|
5201
|
+
this.log("\u274C Failed to initialize section tracking:", error);
|
|
5202
|
+
}
|
|
5203
|
+
}).catch((error) => {
|
|
5204
|
+
this.log("\u274C Failed to load section tracking module:", error);
|
|
5205
|
+
});
|
|
5206
|
+
} else {
|
|
5207
|
+
this.log("No sections configured for auto-tracking");
|
|
5208
|
+
}
|
|
4399
5209
|
}
|
|
4400
5210
|
/**
|
|
4401
5211
|
* Track session start event
|
|
@@ -5355,6 +6165,14 @@ var Grain = (() => {
|
|
|
5355
6165
|
this.activityDetector.destroy();
|
|
5356
6166
|
this.activityDetector = null;
|
|
5357
6167
|
}
|
|
6168
|
+
if (this.interactionTrackingManager) {
|
|
6169
|
+
this.interactionTrackingManager.destroy();
|
|
6170
|
+
this.interactionTrackingManager = null;
|
|
6171
|
+
}
|
|
6172
|
+
if (this.sectionTrackingManager) {
|
|
6173
|
+
this.sectionTrackingManager.destroy();
|
|
6174
|
+
this.sectionTrackingManager = null;
|
|
6175
|
+
}
|
|
5358
6176
|
if (this.eventQueue.length > 0) {
|
|
5359
6177
|
const eventsToSend = [...this.eventQueue];
|
|
5360
6178
|
this.eventQueue = [];
|