@grainql/analytics-web 2.5.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 (37) hide show
  1. package/dist/cjs/index.d.ts +1 -0
  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 +4 -1
  5. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  6. package/dist/cjs/interaction-tracking.js +26 -4
  7. package/dist/cjs/interaction-tracking.js.map +1 -1
  8. package/dist/cjs/section-tracking.d.ts +10 -0
  9. package/dist/cjs/section-tracking.d.ts.map +1 -1
  10. package/dist/cjs/section-tracking.js +83 -1
  11. package/dist/cjs/section-tracking.js.map +1 -1
  12. package/dist/esm/index.d.ts +1 -0
  13. package/dist/esm/index.d.ts.map +1 -1
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/interaction-tracking.d.ts +4 -1
  16. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  17. package/dist/esm/interaction-tracking.js +26 -4
  18. package/dist/esm/interaction-tracking.js.map +1 -1
  19. package/dist/esm/section-tracking.d.ts +10 -0
  20. package/dist/esm/section-tracking.d.ts.map +1 -1
  21. package/dist/esm/section-tracking.js +83 -1
  22. package/dist/esm/section-tracking.js.map +1 -1
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.global.dev.js +140 -32
  26. package/dist/index.global.dev.js.map +2 -2
  27. package/dist/index.global.js +2 -2
  28. package/dist/index.global.js.map +3 -3
  29. package/dist/index.js +52 -31
  30. package/dist/index.mjs +52 -31
  31. package/dist/interaction-tracking.d.ts +4 -1
  32. package/dist/interaction-tracking.d.ts.map +1 -1
  33. package/dist/interaction-tracking.js +26 -4
  34. package/dist/section-tracking.d.ts +10 -0
  35. package/dist/section-tracking.d.ts.map +1 -1
  36. package/dist/section-tracking.js +83 -1
  37. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -526,9 +526,12 @@ class GrainAnalytics {
526
526
  try {
527
527
  const headers = await this.getAuthHeaders();
528
528
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
529
- const body = JSON.stringify({ events });
530
- // Try beacon API first (more reliable for page unload)
531
- if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
529
+ // Send events array directly (not wrapped in object) to match API expectation
530
+ const body = JSON.stringify(events);
531
+ // Beacon API doesn't support custom headers, so only use it for unauthenticated requests
532
+ const needsAuth = this.config.authStrategy !== 'NONE';
533
+ // Try beacon API first (more reliable for page unload, but only if no auth needed)
534
+ if (!needsAuth && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
532
535
  const blob = new Blob([body], { type: 'application/json' });
533
536
  const success = navigator.sendBeacon(url, blob);
534
537
  if (success) {
@@ -536,7 +539,7 @@ class GrainAnalytics {
536
539
  return;
537
540
  }
538
541
  }
539
- // Fallback to fetch with keepalive
542
+ // Use fetch with keepalive (supports headers and works during page unload)
540
543
  await fetch(url, {
541
544
  method: 'POST',
542
545
  headers,
@@ -642,6 +645,7 @@ class GrainAnalytics {
642
645
  */
643
646
  async initializeAutoTracking() {
644
647
  try {
648
+ this.log('Initializing auto-tracking...');
645
649
  // Fetch remote config to get auto-tracking configuration
646
650
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
647
651
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
@@ -654,16 +658,24 @@ class GrainAnalytics {
654
658
  };
655
659
  const headers = await this.getAuthHeaders();
656
660
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
661
+ this.log('Fetching auto-tracking config from:', url);
657
662
  const response = await fetch(url, {
658
663
  method: 'POST',
659
664
  headers,
660
665
  body: JSON.stringify(request),
661
666
  });
662
- if (response.ok) {
663
- const configResponse = await response.json();
664
- if (configResponse.autoTrackingConfig) {
665
- this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
666
- }
667
+ if (!response.ok) {
668
+ this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
669
+ return;
670
+ }
671
+ const configResponse = await response.json();
672
+ this.log('Received config response:', configResponse);
673
+ if (configResponse.autoTrackingConfig) {
674
+ this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
675
+ this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
676
+ }
677
+ else {
678
+ this.log('No auto-tracking config in response');
667
679
  }
668
680
  }
669
681
  catch (error) {
@@ -675,27 +687,33 @@ class GrainAnalytics {
675
687
  * Setup auto-tracking managers
676
688
  */
677
689
  setupAutoTrackingManagers(config) {
690
+ this.log('Setting up auto-tracking managers...', config);
678
691
  // Lazy load the managers to avoid bundling them if not needed
679
- Promise.resolve().then(() => __importStar(require('./interaction-tracking'))).then(({ InteractionTrackingManager }) => {
680
- try {
681
- if (config.interactions && config.interactions.length > 0) {
692
+ if (config.interactions && config.interactions.length > 0) {
693
+ this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
694
+ Promise.resolve().then(() => __importStar(require('./interaction-tracking'))).then(({ InteractionTrackingManager }) => {
695
+ try {
682
696
  this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
683
697
  debug: this.config.debug,
684
698
  enableMutationObserver: true,
685
699
  mutationDebounceDelay: 500,
686
700
  });
687
- this.log('Interaction tracking initialized with', config.interactions.length, 'interactions');
701
+ this.log('Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
688
702
  }
689
- }
690
- catch (error) {
691
- this.log('Failed to initialize interaction tracking:', error);
692
- }
693
- }).catch((error) => {
694
- this.log('Failed to load interaction tracking module:', error);
695
- });
696
- Promise.resolve().then(() => __importStar(require('./section-tracking'))).then(({ SectionTrackingManager }) => {
697
- try {
698
- if (config.sections && config.sections.length > 0) {
703
+ catch (error) {
704
+ this.log('❌ Failed to initialize interaction tracking:', error);
705
+ }
706
+ }).catch((error) => {
707
+ this.log('❌ Failed to load interaction tracking module:', error);
708
+ });
709
+ }
710
+ else {
711
+ this.log('No interactions configured for auto-tracking');
712
+ }
713
+ if (config.sections && config.sections.length > 0) {
714
+ this.log('Loading section tracking module for', config.sections.length, 'sections');
715
+ Promise.resolve().then(() => __importStar(require('./section-tracking'))).then(({ SectionTrackingManager }) => {
716
+ try {
699
717
  this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
700
718
  minDwellTime: 1000,
701
719
  scrollVelocityThreshold: 500,
@@ -704,15 +722,18 @@ class GrainAnalytics {
704
722
  batchDelay: 2000,
705
723
  debug: this.config.debug,
706
724
  });
707
- this.log('Section tracking initialized with', config.sections.length, 'sections');
725
+ this.log('Section tracking initialized successfully with', config.sections.length, 'sections');
708
726
  }
709
- }
710
- catch (error) {
711
- this.log('Failed to initialize section tracking:', error);
712
- }
713
- }).catch((error) => {
714
- this.log('Failed to load section tracking module:', error);
715
- });
727
+ catch (error) {
728
+ this.log('❌ Failed to initialize section tracking:', error);
729
+ }
730
+ }).catch((error) => {
731
+ this.log('❌ Failed to load section tracking module:', error);
732
+ });
733
+ }
734
+ else {
735
+ this.log('No sections configured for auto-tracking');
736
+ }
716
737
  }
717
738
  /**
718
739
  * Track session start event
package/dist/index.mjs CHANGED
@@ -485,9 +485,12 @@ export class GrainAnalytics {
485
485
  try {
486
486
  const headers = await this.getAuthHeaders();
487
487
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
488
- const body = JSON.stringify({ events });
489
- // Try beacon API first (more reliable for page unload)
490
- if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
488
+ // Send events array directly (not wrapped in object) to match API expectation
489
+ const body = JSON.stringify(events);
490
+ // Beacon API doesn't support custom headers, so only use it for unauthenticated requests
491
+ const needsAuth = this.config.authStrategy !== 'NONE';
492
+ // Try beacon API first (more reliable for page unload, but only if no auth needed)
493
+ if (!needsAuth && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
491
494
  const blob = new Blob([body], { type: 'application/json' });
492
495
  const success = navigator.sendBeacon(url, blob);
493
496
  if (success) {
@@ -495,7 +498,7 @@ export class GrainAnalytics {
495
498
  return;
496
499
  }
497
500
  }
498
- // Fallback to fetch with keepalive
501
+ // Use fetch with keepalive (supports headers and works during page unload)
499
502
  await fetch(url, {
500
503
  method: 'POST',
501
504
  headers,
@@ -601,6 +604,7 @@ export class GrainAnalytics {
601
604
  */
602
605
  async initializeAutoTracking() {
603
606
  try {
607
+ this.log('Initializing auto-tracking...');
604
608
  // Fetch remote config to get auto-tracking configuration
605
609
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
606
610
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
@@ -613,16 +617,24 @@ export class GrainAnalytics {
613
617
  };
614
618
  const headers = await this.getAuthHeaders();
615
619
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
620
+ this.log('Fetching auto-tracking config from:', url);
616
621
  const response = await fetch(url, {
617
622
  method: 'POST',
618
623
  headers,
619
624
  body: JSON.stringify(request),
620
625
  });
621
- if (response.ok) {
622
- const configResponse = await response.json();
623
- if (configResponse.autoTrackingConfig) {
624
- this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
625
- }
626
+ if (!response.ok) {
627
+ this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
628
+ return;
629
+ }
630
+ const configResponse = await response.json();
631
+ this.log('Received config response:', configResponse);
632
+ if (configResponse.autoTrackingConfig) {
633
+ this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
634
+ this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
635
+ }
636
+ else {
637
+ this.log('No auto-tracking config in response');
626
638
  }
627
639
  }
628
640
  catch (error) {
@@ -634,27 +646,33 @@ export class GrainAnalytics {
634
646
  * Setup auto-tracking managers
635
647
  */
636
648
  setupAutoTrackingManagers(config) {
649
+ this.log('Setting up auto-tracking managers...', config);
637
650
  // Lazy load the managers to avoid bundling them if not needed
638
- import('./interaction-tracking').then(({ InteractionTrackingManager }) => {
639
- try {
640
- if (config.interactions && config.interactions.length > 0) {
651
+ if (config.interactions && config.interactions.length > 0) {
652
+ this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
653
+ import('./interaction-tracking').then(({ InteractionTrackingManager }) => {
654
+ try {
641
655
  this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
642
656
  debug: this.config.debug,
643
657
  enableMutationObserver: true,
644
658
  mutationDebounceDelay: 500,
645
659
  });
646
- this.log('Interaction tracking initialized with', config.interactions.length, 'interactions');
660
+ this.log('Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
647
661
  }
648
- }
649
- catch (error) {
650
- this.log('Failed to initialize interaction tracking:', error);
651
- }
652
- }).catch((error) => {
653
- this.log('Failed to load interaction tracking module:', error);
654
- });
655
- import('./section-tracking').then(({ SectionTrackingManager }) => {
656
- try {
657
- if (config.sections && config.sections.length > 0) {
662
+ catch (error) {
663
+ this.log('❌ Failed to initialize interaction tracking:', error);
664
+ }
665
+ }).catch((error) => {
666
+ this.log('❌ Failed to load interaction tracking module:', error);
667
+ });
668
+ }
669
+ else {
670
+ this.log('No interactions configured for auto-tracking');
671
+ }
672
+ if (config.sections && config.sections.length > 0) {
673
+ this.log('Loading section tracking module for', config.sections.length, 'sections');
674
+ import('./section-tracking').then(({ SectionTrackingManager }) => {
675
+ try {
658
676
  this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
659
677
  minDwellTime: 1000,
660
678
  scrollVelocityThreshold: 500,
@@ -663,15 +681,18 @@ export class GrainAnalytics {
663
681
  batchDelay: 2000,
664
682
  debug: this.config.debug,
665
683
  });
666
- this.log('Section tracking initialized with', config.sections.length, 'sections');
684
+ this.log('Section tracking initialized successfully with', config.sections.length, 'sections');
667
685
  }
668
- }
669
- catch (error) {
670
- this.log('Failed to initialize section tracking:', error);
671
- }
672
- }).catch((error) => {
673
- this.log('Failed to load section tracking module:', error);
674
- });
686
+ catch (error) {
687
+ this.log('❌ Failed to initialize section tracking:', error);
688
+ }
689
+ }).catch((error) => {
690
+ this.log('❌ Failed to load section tracking module:', error);
691
+ });
692
+ }
693
+ else {
694
+ this.log('No sections configured for auto-tracking');
695
+ }
675
696
  }
676
697
  /**
677
698
  * Track session start event
@@ -3,8 +3,11 @@
3
3
  * Automatically attaches click and focus listeners to detected interactive elements
4
4
  */
5
5
  import type { InteractionConfig } from './types/auto-tracking';
6
+ export interface SendEventOptions {
7
+ flush?: boolean;
8
+ }
6
9
  export interface InteractionTracker {
7
- track(eventName: string, properties?: Record<string, unknown>): void | Promise<void>;
10
+ track(eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions): void | Promise<void>;
8
11
  hasConsent(category: 'analytics' | 'marketing' | 'functional'): boolean;
9
12
  log(...args: unknown[]): void;
10
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"interaction-tracking.d.ts","sourceRoot":"","sources":["../src/interaction-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,0BAA0B;IACrC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAA6E;IACtG,OAAO,CAAC,UAAU,CAA0C;IAC5D,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,qBAAqB,CAAuB;gBAGlD,OAAO,EAAE,kBAAkB,EAC3B,YAAY,EAAE,iBAAiB,EAAE,EACjC,MAAM,GAAE,yBAA8B;IA0BxC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAkCjC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAqB9B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAoB9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmC1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0B7B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAoB3D;;OAEG;IACH,OAAO,IAAI,IAAI;CA4BhB"}
1
+ {"version":3,"file":"interaction-tracking.d.ts","sourceRoot":"","sources":["../src/interaction-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,0BAA0B;IACrC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAA6E;IACtG,OAAO,CAAC,UAAU,CAA0C;IAC5D,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,qBAAqB,CAAuB;gBAGlD,OAAO,EAAE,kBAAkB,EAC3B,YAAY,EAAE,iBAAiB,EAAE,EACjC,MAAM,GAAE,yBAA8B;IA0BxC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAkCjC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAuC9B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAoB9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAyC1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0B7B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAoB3D;;OAEG;IACH,OAAO,IAAI,IAAI;CA4BhB"}
@@ -84,7 +84,9 @@ class InteractionTrackingManager {
84
84
  if (!this.tracker.hasConsent('analytics'))
85
85
  return;
86
86
  const element = event.target;
87
- this.tracker.track(interaction.eventName, {
87
+ // Check if this is a navigation link (has href attribute)
88
+ const isNavigationLink = element instanceof HTMLAnchorElement && element.href;
89
+ const eventProperties = {
88
90
  interaction_type: 'click',
89
91
  interaction_label: interaction.label,
90
92
  interaction_description: interaction.description,
@@ -93,8 +95,23 @@ class InteractionTrackingManager {
93
95
  element_text: element.textContent?.trim().substring(0, 100),
94
96
  element_id: element.id || undefined,
95
97
  element_class: element.className || undefined,
98
+ ...(isNavigationLink && { href: element.href }),
96
99
  timestamp: Date.now(),
97
- });
100
+ };
101
+ // If it's a navigation link, flush immediately to ensure event is sent before navigation
102
+ if (isNavigationLink) {
103
+ // Use flush option to send immediately - handle promise if returned
104
+ const result = this.tracker.track(interaction.eventName, eventProperties, { flush: true });
105
+ if (result instanceof Promise) {
106
+ result.catch((error) => {
107
+ // Log error but don't block navigation
108
+ this.log('Failed to track navigation click:', error);
109
+ });
110
+ }
111
+ }
112
+ else {
113
+ this.tracker.track(interaction.eventName, eventProperties);
114
+ }
98
115
  this.log('Tracked click interaction:', interaction.eventName);
99
116
  }
100
117
  /**
@@ -133,9 +150,14 @@ class InteractionTrackingManager {
133
150
  this.xpathCache.delete(xpath);
134
151
  }
135
152
  try {
136
- const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
153
+ // Strip the xpath= prefix if present (from Stagehand selectors)
154
+ let cleanXpath = xpath;
155
+ if (xpath.startsWith('xpath=')) {
156
+ cleanXpath = xpath.substring(6); // Remove 'xpath=' prefix
157
+ }
158
+ const result = document.evaluate(cleanXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
137
159
  const element = result.singleNodeValue;
138
- // Cache the result
160
+ // Cache the result (use original xpath as key)
139
161
  if (element) {
140
162
  this.xpathCache.set(xpath, element);
141
163
  }
@@ -22,6 +22,8 @@ export declare class SectionTrackingManager {
22
22
  private scrollDebounceTimer;
23
23
  private pendingEvents;
24
24
  private batchTimer;
25
+ private sectionTimers;
26
+ private readonly SPLIT_DURATION;
25
27
  constructor(tracker: SectionTracker, sections: SectionConfig[], options?: Partial<SectionTrackingOptions>);
26
28
  /**
27
29
  * Initialize section tracking
@@ -47,6 +49,14 @@ export declare class SectionTrackingManager {
47
49
  * Handle section entry (became visible)
48
50
  */
49
51
  private handleSectionEntry;
52
+ /**
53
+ * Start periodic tracking for a section (sends events every 3 seconds)
54
+ */
55
+ private startPeriodicTracking;
56
+ /**
57
+ * Stop periodic tracking for a section
58
+ */
59
+ private stopPeriodicTracking;
50
60
  /**
51
61
  * Handle section exit (became invisible)
52
62
  */
@@ -1 +1 @@
1
- {"version":3,"file":"section-tracking.d.ts","sourceRoot":"","sources":["../src/section-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAmB,sBAAsB,EAAwB,MAAM,uBAAuB,CAAC;AAE1H,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAWD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,WAAW,CAAS;IAG5B,OAAO,CAAC,aAAa,CAAgD;IACrE,OAAO,CAAC,oBAAoB,CAAqC;IACjE,OAAO,CAAC,UAAU,CAA0C;IAG5D,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,mBAAmB,CAAuB;IAGlD,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,UAAU,CAAuB;gBAGvC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,aAAa,EAAE,EACzB,OAAO,GAAE,OAAO,CAAC,sBAAsB,CAAM;IAgB/C;;OAEG;IACH,OAAO,CAAC,UAAU;IAelB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAqBjC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAkB3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkC1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoCzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA+B1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAgC1B;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI;IAsB/C;;OAEG;IACH,OAAO,IAAI,IAAI;CAgChB"}
1
+ {"version":3,"file":"section-tracking.d.ts","sourceRoot":"","sources":["../src/section-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAmB,sBAAsB,EAAwB,MAAM,uBAAuB,CAAC;AAE1H,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAWD,qBAAa,sBAAsB;IACjC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,OAAO,CAAyB;IACxC,OAAO,CAAC,WAAW,CAAS;IAG5B,OAAO,CAAC,aAAa,CAAgD;IACrE,OAAO,CAAC,oBAAoB,CAAqC;IACjE,OAAO,CAAC,UAAU,CAA0C;IAG5D,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,cAAc,CAAc;IACpC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,mBAAmB,CAAuB;IAGlD,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,UAAU,CAAuB;IAGzC,OAAO,CAAC,aAAa,CAAkC;IACvD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;gBAGrC,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,aAAa,EAAE,EACzB,OAAO,GAAE,OAAO,CAAC,sBAAsB,CAAM;IAgB/C;;OAEG;IACH,OAAO,CAAC,UAAU;IAelB;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAqBjC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAkB3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAkC1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAa1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA2D7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAQ5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAuCzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAe5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAYxB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA+B1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuC1B;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI;IAsB/C;;OAEG;IACH,OAAO,IAAI,IAAI;CAsChB"}
@@ -28,6 +28,9 @@ class SectionTrackingManager {
28
28
  // Event batching
29
29
  this.pendingEvents = [];
30
30
  this.batchTimer = null;
31
+ // Periodic tracking for long-duration views
32
+ this.sectionTimers = new Map(); // sectionName -> timer ID
33
+ this.SPLIT_DURATION = 3000; // 3 seconds
31
34
  this.tracker = tracker;
32
35
  this.sections = sections;
33
36
  this.options = { ...DEFAULT_OPTIONS, ...options };
@@ -159,12 +162,80 @@ class SectionTrackingManager {
159
162
  state.lastScrollPosition = window.scrollY;
160
163
  state.lastScrollTime = Date.now();
161
164
  state.maxVisibleArea = 0;
165
+ // Start periodic tracking timer (3 second intervals)
166
+ this.startPeriodicTracking(state);
167
+ }
168
+ /**
169
+ * Start periodic tracking for a section (sends events every 3 seconds)
170
+ */
171
+ startPeriodicTracking(state) {
172
+ // Clear any existing timer for this section
173
+ this.stopPeriodicTracking(state.config.sectionName);
174
+ const timerId = window.setInterval(() => {
175
+ if (this.isDestroyed || !state.isVisible || state.entryTime === null) {
176
+ this.stopPeriodicTracking(state.config.sectionName);
177
+ return;
178
+ }
179
+ const now = Date.now();
180
+ const duration = now - state.entryTime;
181
+ // Only track if minimum dwell time has passed
182
+ if (duration >= this.options.minDwellTime) {
183
+ // Create partial section view data
184
+ const viewData = {
185
+ sectionName: state.config.sectionName,
186
+ sectionType: state.config.sectionType,
187
+ entryTime: state.entryTime,
188
+ exitTime: now, // Current time as "exit" for this split
189
+ duration,
190
+ viewportWidth: window.innerWidth,
191
+ viewportHeight: window.innerHeight,
192
+ scrollDepth: this.calculateScrollDepth(),
193
+ visibleAreaPercentage: Math.round(state.maxVisibleArea * 100),
194
+ scrollSpeedAtEntry: state.entryScrollSpeed,
195
+ scrollSpeedAtExit: this.scrollVelocity,
196
+ };
197
+ // Track this split immediately (don't queue for batching)
198
+ if (this.shouldTrackSection(viewData)) {
199
+ this.tracker.trackSystemEvent('_grain_section_view', {
200
+ section_name: viewData.sectionName,
201
+ section_type: viewData.sectionType,
202
+ duration_ms: viewData.duration,
203
+ viewport_width: viewData.viewportWidth,
204
+ viewport_height: viewData.viewportHeight,
205
+ scroll_depth_percent: viewData.scrollDepth,
206
+ visible_area_percent: viewData.visibleAreaPercentage,
207
+ scroll_speed_entry: Math.round(viewData.scrollSpeedAtEntry || 0),
208
+ scroll_speed_exit: Math.round(viewData.scrollSpeedAtExit || 0),
209
+ entry_timestamp: viewData.entryTime,
210
+ exit_timestamp: viewData.exitTime,
211
+ is_split: true, // Flag to indicate this is a periodic split, not final exit
212
+ });
213
+ this.log('Tracked periodic section view split:', state.config.sectionName, 'duration:', duration);
214
+ // Reset entry time for next split (but keep tracking)
215
+ state.entryTime = now;
216
+ state.entryScrollSpeed = this.scrollVelocity;
217
+ }
218
+ }
219
+ }, this.SPLIT_DURATION);
220
+ this.sectionTimers.set(state.config.sectionName, timerId);
221
+ }
222
+ /**
223
+ * Stop periodic tracking for a section
224
+ */
225
+ stopPeriodicTracking(sectionName) {
226
+ const timerId = this.sectionTimers.get(sectionName);
227
+ if (timerId !== undefined) {
228
+ clearInterval(timerId);
229
+ this.sectionTimers.delete(sectionName);
230
+ }
162
231
  }
163
232
  /**
164
233
  * Handle section exit (became invisible)
165
234
  */
166
235
  handleSectionExit(state) {
167
236
  this.log('Section exited view:', state.config.sectionName);
237
+ // Stop periodic tracking
238
+ this.stopPeriodicTracking(state.config.sectionName);
168
239
  if (state.entryTime === null)
169
240
  return;
170
241
  state.exitTime = Date.now();
@@ -299,8 +370,14 @@ class SectionTrackingManager {
299
370
  this.xpathCache.delete(xpath);
300
371
  }
301
372
  try {
302
- const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
373
+ // Strip the xpath= prefix if present (from Stagehand selectors)
374
+ let cleanXpath = xpath;
375
+ if (xpath.startsWith('xpath=')) {
376
+ cleanXpath = xpath.substring(6); // Remove 'xpath=' prefix
377
+ }
378
+ const result = document.evaluate(cleanXpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
303
379
  const element = result.singleNodeValue;
380
+ // Cache the result (use original xpath as key)
304
381
  if (element) {
305
382
  this.xpathCache.set(xpath, element);
306
383
  }
@@ -347,6 +424,11 @@ class SectionTrackingManager {
347
424
  return;
348
425
  this.log('Destroying section tracking manager');
349
426
  this.isDestroyed = true;
427
+ // Stop all periodic tracking timers
428
+ this.sectionTimers.forEach((timerId) => {
429
+ clearInterval(timerId);
430
+ });
431
+ this.sectionTimers.clear();
350
432
  // Flush any pending events
351
433
  this.flushPendingEvents();
352
434
  // Clear timers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grainql/analytics-web",
3
- "version": "2.5.0",
3
+ "version": "2.5.3",
4
4
  "description": "Lightweight TypeScript SDK for sending analytics events and managing remote configurations via Grain's REST API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",