@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.
Files changed (47) hide show
  1. package/dist/cjs/index.d.ts +28 -1
  2. package/dist/cjs/index.d.ts.map +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/interaction-tracking.d.ts +71 -0
  5. package/dist/cjs/interaction-tracking.d.ts.map +1 -0
  6. package/dist/cjs/interaction-tracking.js +270 -0
  7. package/dist/cjs/interaction-tracking.js.map +1 -0
  8. package/dist/cjs/section-tracking.d.ts +91 -0
  9. package/dist/cjs/section-tracking.d.ts.map +1 -0
  10. package/dist/cjs/section-tracking.js +373 -0
  11. package/dist/cjs/section-tracking.js.map +1 -0
  12. package/dist/cjs/types/auto-tracking.d.ts +55 -0
  13. package/dist/cjs/types/auto-tracking.d.ts.map +1 -0
  14. package/dist/cjs/types/auto-tracking.js +6 -0
  15. package/dist/cjs/types/auto-tracking.js.map +1 -0
  16. package/dist/esm/index.d.ts +28 -1
  17. package/dist/esm/index.d.ts.map +1 -1
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/interaction-tracking.d.ts +71 -0
  20. package/dist/esm/interaction-tracking.d.ts.map +1 -0
  21. package/dist/esm/interaction-tracking.js +266 -0
  22. package/dist/esm/interaction-tracking.js.map +1 -0
  23. package/dist/esm/section-tracking.d.ts +91 -0
  24. package/dist/esm/section-tracking.d.ts.map +1 -0
  25. package/dist/esm/section-tracking.js +369 -0
  26. package/dist/esm/section-tracking.js.map +1 -0
  27. package/dist/esm/types/auto-tracking.d.ts +55 -0
  28. package/dist/esm/types/auto-tracking.d.ts.map +1 -0
  29. package/dist/esm/types/auto-tracking.js +5 -0
  30. package/dist/esm/types/auto-tracking.js.map +1 -0
  31. package/dist/index.d.ts +28 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.global.dev.js +711 -1
  34. package/dist/index.global.dev.js.map +4 -4
  35. package/dist/index.global.js +2 -2
  36. package/dist/index.global.js.map +4 -4
  37. package/dist/index.js +124 -0
  38. package/dist/index.mjs +91 -0
  39. package/dist/interaction-tracking.d.ts +71 -0
  40. package/dist/interaction-tracking.d.ts.map +1 -0
  41. package/dist/interaction-tracking.js +270 -0
  42. package/dist/section-tracking.d.ts +91 -0
  43. package/dist/section-tracking.d.ts.map +1 -0
  44. package/dist/section-tracking.js +373 -0
  45. package/dist/types/auto-tracking.d.ts +55 -0
  46. package/dist/types/auto-tracking.d.ts.map +1 -0
  47. package/package.json +1 -1
@@ -1,10 +1,13 @@
1
- /* Grain Analytics Web SDK v2.4.0 | MIT License | Development Build */
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 = [];