@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.
Files changed (47) hide show
  1. package/dist/cjs/index.d.ts +29 -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 +74 -0
  5. package/dist/cjs/interaction-tracking.d.ts.map +1 -0
  6. package/dist/cjs/interaction-tracking.js +292 -0
  7. package/dist/cjs/interaction-tracking.js.map +1 -0
  8. package/dist/cjs/section-tracking.d.ts +101 -0
  9. package/dist/cjs/section-tracking.d.ts.map +1 -0
  10. package/dist/cjs/section-tracking.js +455 -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 +29 -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 +74 -0
  20. package/dist/esm/interaction-tracking.d.ts.map +1 -0
  21. package/dist/esm/interaction-tracking.js +288 -0
  22. package/dist/esm/interaction-tracking.js.map +1 -0
  23. package/dist/esm/section-tracking.d.ts +101 -0
  24. package/dist/esm/section-tracking.d.ts.map +1 -0
  25. package/dist/esm/section-tracking.js +451 -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 +29 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.global.dev.js +821 -3
  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 +149 -4
  38. package/dist/index.mjs +116 -4
  39. package/dist/interaction-tracking.d.ts +74 -0
  40. package/dist/interaction-tracking.d.ts.map +1 -0
  41. package/dist/interaction-tracking.js +292 -0
  42. package/dist/section-tracking.d.ts +101 -0
  43. package/dist/section-tracking.d.ts.map +1 -0
  44. package/dist/section-tracking.js +455 -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.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({ events });
4299
- if (typeof navigator !== "undefined" && "sendBeacon" in navigator) {
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 = [];