@grainql/analytics-web 2.5.0 → 2.5.4

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 +171 -33
  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 +89 -32
  30. package/dist/index.mjs +90 -33
  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
@@ -417,10 +417,46 @@ class GrainAnalytics {
417
417
  }
418
418
  }
419
419
  formatEvent(event) {
420
+ const properties = event.properties || {};
421
+ // Auto-enrich events with session-level attribution properties
422
+ // This ensures UTM parameters and attribution data are available on ALL events, not just page_view
423
+ if (!this.config.disableAutoProperties && typeof window !== 'undefined') {
424
+ const hasConsent = this.consentManager.hasConsent('analytics');
425
+ // Only enrich if not a system event (they handle their own properties)
426
+ const isSystemEvent = event.eventName.startsWith('_grain_');
427
+ if (!isSystemEvent && hasConsent) {
428
+ // Get session UTM parameters
429
+ const sessionUTMs = (0, attribution_1.getSessionUTMParameters)();
430
+ if (sessionUTMs) {
431
+ if (sessionUTMs.utm_source)
432
+ properties.utm_source = sessionUTMs.utm_source;
433
+ if (sessionUTMs.utm_medium)
434
+ properties.utm_medium = sessionUTMs.utm_medium;
435
+ if (sessionUTMs.utm_campaign)
436
+ properties.utm_campaign = sessionUTMs.utm_campaign;
437
+ if (sessionUTMs.utm_term)
438
+ properties.utm_term = sessionUTMs.utm_term;
439
+ if (sessionUTMs.utm_content)
440
+ properties.utm_content = sessionUTMs.utm_content;
441
+ }
442
+ // Get first-touch attribution
443
+ const firstTouch = (0, attribution_1.getFirstTouchAttribution)(this.config.tenantId);
444
+ if (firstTouch) {
445
+ properties.first_touch_source = firstTouch.source;
446
+ properties.first_touch_medium = firstTouch.medium;
447
+ properties.first_touch_campaign = firstTouch.campaign;
448
+ properties.first_touch_referrer_category = firstTouch.referrer_category;
449
+ }
450
+ // Add session ID if not already present
451
+ if (!properties.session_id) {
452
+ properties.session_id = this.getSessionId();
453
+ }
454
+ }
455
+ }
420
456
  return {
421
457
  eventName: event.eventName,
422
458
  userId: event.userId || this.getEffectiveUserIdInternal(),
423
- properties: event.properties || {},
459
+ properties,
424
460
  };
425
461
  }
426
462
  async getAuthHeaders() {
@@ -526,9 +562,12 @@ class GrainAnalytics {
526
562
  try {
527
563
  const headers = await this.getAuthHeaders();
528
564
  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) {
565
+ // Send events array directly (not wrapped in object) to match API expectation
566
+ const body = JSON.stringify(events);
567
+ // Beacon API doesn't support custom headers, so only use it for unauthenticated requests
568
+ const needsAuth = this.config.authStrategy !== 'NONE';
569
+ // Try beacon API first (more reliable for page unload, but only if no auth needed)
570
+ if (!needsAuth && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
532
571
  const blob = new Blob([body], { type: 'application/json' });
533
572
  const success = navigator.sendBeacon(url, blob);
534
573
  if (success) {
@@ -536,7 +575,7 @@ class GrainAnalytics {
536
575
  return;
537
576
  }
538
577
  }
539
- // Fallback to fetch with keepalive
578
+ // Use fetch with keepalive (supports headers and works during page unload)
540
579
  await fetch(url, {
541
580
  method: 'POST',
542
581
  headers,
@@ -642,6 +681,7 @@ class GrainAnalytics {
642
681
  */
643
682
  async initializeAutoTracking() {
644
683
  try {
684
+ this.log('Initializing auto-tracking...');
645
685
  // Fetch remote config to get auto-tracking configuration
646
686
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
647
687
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
@@ -654,16 +694,24 @@ class GrainAnalytics {
654
694
  };
655
695
  const headers = await this.getAuthHeaders();
656
696
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
697
+ this.log('Fetching auto-tracking config from:', url);
657
698
  const response = await fetch(url, {
658
699
  method: 'POST',
659
700
  headers,
660
701
  body: JSON.stringify(request),
661
702
  });
662
- if (response.ok) {
663
- const configResponse = await response.json();
664
- if (configResponse.autoTrackingConfig) {
665
- this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
666
- }
703
+ if (!response.ok) {
704
+ this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
705
+ return;
706
+ }
707
+ const configResponse = await response.json();
708
+ this.log('Received config response:', configResponse);
709
+ if (configResponse.autoTrackingConfig) {
710
+ this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
711
+ this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
712
+ }
713
+ else {
714
+ this.log('No auto-tracking config in response');
667
715
  }
668
716
  }
669
717
  catch (error) {
@@ -675,27 +723,33 @@ class GrainAnalytics {
675
723
  * Setup auto-tracking managers
676
724
  */
677
725
  setupAutoTrackingManagers(config) {
726
+ this.log('Setting up auto-tracking managers...', config);
678
727
  // 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) {
728
+ if (config.interactions && config.interactions.length > 0) {
729
+ this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
730
+ Promise.resolve().then(() => __importStar(require('./interaction-tracking'))).then(({ InteractionTrackingManager }) => {
731
+ try {
682
732
  this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
683
733
  debug: this.config.debug,
684
734
  enableMutationObserver: true,
685
735
  mutationDebounceDelay: 500,
686
736
  });
687
- this.log('Interaction tracking initialized with', config.interactions.length, 'interactions');
737
+ this.log('Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
688
738
  }
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) {
739
+ catch (error) {
740
+ this.log('❌ Failed to initialize interaction tracking:', error);
741
+ }
742
+ }).catch((error) => {
743
+ this.log('❌ Failed to load interaction tracking module:', error);
744
+ });
745
+ }
746
+ else {
747
+ this.log('No interactions configured for auto-tracking');
748
+ }
749
+ if (config.sections && config.sections.length > 0) {
750
+ this.log('Loading section tracking module for', config.sections.length, 'sections');
751
+ Promise.resolve().then(() => __importStar(require('./section-tracking'))).then(({ SectionTrackingManager }) => {
752
+ try {
699
753
  this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
700
754
  minDwellTime: 1000,
701
755
  scrollVelocityThreshold: 500,
@@ -704,15 +758,18 @@ class GrainAnalytics {
704
758
  batchDelay: 2000,
705
759
  debug: this.config.debug,
706
760
  });
707
- this.log('Section tracking initialized with', config.sections.length, 'sections');
761
+ this.log('Section tracking initialized successfully with', config.sections.length, 'sections');
708
762
  }
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
- });
763
+ catch (error) {
764
+ this.log('❌ Failed to initialize section tracking:', error);
765
+ }
766
+ }).catch((error) => {
767
+ this.log('❌ Failed to load section tracking module:', error);
768
+ });
769
+ }
770
+ else {
771
+ this.log('No sections configured for auto-tracking');
772
+ }
716
773
  }
717
774
  /**
718
775
  * Track session start event
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ import { setCookie, getCookie, areCookiesEnabled } from './cookies.js';
7
7
  import { ActivityDetector } from './activity.js';
8
8
  import { HeartbeatManager } from './heartbeat.js';
9
9
  import { PageTrackingManager } from './page-tracking.js';
10
- import { categorizeReferrer, parseUTMParameters, getOrCreateFirstTouchAttribution, getSessionUTMParameters, } from './attribution.js';
10
+ import { categorizeReferrer, parseUTMParameters, getOrCreateFirstTouchAttribution, getSessionUTMParameters, getFirstTouchAttribution, } from './attribution.js';
11
11
  export { categorizeReferrer, parseUTMParameters };
12
12
  // Re-export timezone-country utilities
13
13
  export { getCountry, getCountryCodeFromTimezone, getState } from './countries.js';
@@ -376,10 +376,46 @@ export class GrainAnalytics {
376
376
  }
377
377
  }
378
378
  formatEvent(event) {
379
+ const properties = event.properties || {};
380
+ // Auto-enrich events with session-level attribution properties
381
+ // This ensures UTM parameters and attribution data are available on ALL events, not just page_view
382
+ if (!this.config.disableAutoProperties && typeof window !== 'undefined') {
383
+ const hasConsent = this.consentManager.hasConsent('analytics');
384
+ // Only enrich if not a system event (they handle their own properties)
385
+ const isSystemEvent = event.eventName.startsWith('_grain_');
386
+ if (!isSystemEvent && hasConsent) {
387
+ // Get session UTM parameters
388
+ const sessionUTMs = getSessionUTMParameters();
389
+ if (sessionUTMs) {
390
+ if (sessionUTMs.utm_source)
391
+ properties.utm_source = sessionUTMs.utm_source;
392
+ if (sessionUTMs.utm_medium)
393
+ properties.utm_medium = sessionUTMs.utm_medium;
394
+ if (sessionUTMs.utm_campaign)
395
+ properties.utm_campaign = sessionUTMs.utm_campaign;
396
+ if (sessionUTMs.utm_term)
397
+ properties.utm_term = sessionUTMs.utm_term;
398
+ if (sessionUTMs.utm_content)
399
+ properties.utm_content = sessionUTMs.utm_content;
400
+ }
401
+ // Get first-touch attribution
402
+ const firstTouch = getFirstTouchAttribution(this.config.tenantId);
403
+ if (firstTouch) {
404
+ properties.first_touch_source = firstTouch.source;
405
+ properties.first_touch_medium = firstTouch.medium;
406
+ properties.first_touch_campaign = firstTouch.campaign;
407
+ properties.first_touch_referrer_category = firstTouch.referrer_category;
408
+ }
409
+ // Add session ID if not already present
410
+ if (!properties.session_id) {
411
+ properties.session_id = this.getSessionId();
412
+ }
413
+ }
414
+ }
379
415
  return {
380
416
  eventName: event.eventName,
381
417
  userId: event.userId || this.getEffectiveUserIdInternal(),
382
- properties: event.properties || {},
418
+ properties,
383
419
  };
384
420
  }
385
421
  async getAuthHeaders() {
@@ -485,9 +521,12 @@ export class GrainAnalytics {
485
521
  try {
486
522
  const headers = await this.getAuthHeaders();
487
523
  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) {
524
+ // Send events array directly (not wrapped in object) to match API expectation
525
+ const body = JSON.stringify(events);
526
+ // Beacon API doesn't support custom headers, so only use it for unauthenticated requests
527
+ const needsAuth = this.config.authStrategy !== 'NONE';
528
+ // Try beacon API first (more reliable for page unload, but only if no auth needed)
529
+ if (!needsAuth && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
491
530
  const blob = new Blob([body], { type: 'application/json' });
492
531
  const success = navigator.sendBeacon(url, blob);
493
532
  if (success) {
@@ -495,7 +534,7 @@ export class GrainAnalytics {
495
534
  return;
496
535
  }
497
536
  }
498
- // Fallback to fetch with keepalive
537
+ // Use fetch with keepalive (supports headers and works during page unload)
499
538
  await fetch(url, {
500
539
  method: 'POST',
501
540
  headers,
@@ -601,6 +640,7 @@ export class GrainAnalytics {
601
640
  */
602
641
  async initializeAutoTracking() {
603
642
  try {
643
+ this.log('Initializing auto-tracking...');
604
644
  // Fetch remote config to get auto-tracking configuration
605
645
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
606
646
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
@@ -613,16 +653,24 @@ export class GrainAnalytics {
613
653
  };
614
654
  const headers = await this.getAuthHeaders();
615
655
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
656
+ this.log('Fetching auto-tracking config from:', url);
616
657
  const response = await fetch(url, {
617
658
  method: 'POST',
618
659
  headers,
619
660
  body: JSON.stringify(request),
620
661
  });
621
- if (response.ok) {
622
- const configResponse = await response.json();
623
- if (configResponse.autoTrackingConfig) {
624
- this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
625
- }
662
+ if (!response.ok) {
663
+ this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
664
+ return;
665
+ }
666
+ const configResponse = await response.json();
667
+ this.log('Received config response:', configResponse);
668
+ if (configResponse.autoTrackingConfig) {
669
+ this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
670
+ this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
671
+ }
672
+ else {
673
+ this.log('No auto-tracking config in response');
626
674
  }
627
675
  }
628
676
  catch (error) {
@@ -634,27 +682,33 @@ export class GrainAnalytics {
634
682
  * Setup auto-tracking managers
635
683
  */
636
684
  setupAutoTrackingManagers(config) {
685
+ this.log('Setting up auto-tracking managers...', config);
637
686
  // 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) {
687
+ if (config.interactions && config.interactions.length > 0) {
688
+ this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
689
+ import('./interaction-tracking').then(({ InteractionTrackingManager }) => {
690
+ try {
641
691
  this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
642
692
  debug: this.config.debug,
643
693
  enableMutationObserver: true,
644
694
  mutationDebounceDelay: 500,
645
695
  });
646
- this.log('Interaction tracking initialized with', config.interactions.length, 'interactions');
696
+ this.log('Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
647
697
  }
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) {
698
+ catch (error) {
699
+ this.log('❌ Failed to initialize interaction tracking:', error);
700
+ }
701
+ }).catch((error) => {
702
+ this.log('❌ Failed to load interaction tracking module:', error);
703
+ });
704
+ }
705
+ else {
706
+ this.log('No interactions configured for auto-tracking');
707
+ }
708
+ if (config.sections && config.sections.length > 0) {
709
+ this.log('Loading section tracking module for', config.sections.length, 'sections');
710
+ import('./section-tracking').then(({ SectionTrackingManager }) => {
711
+ try {
658
712
  this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
659
713
  minDwellTime: 1000,
660
714
  scrollVelocityThreshold: 500,
@@ -663,15 +717,18 @@ export class GrainAnalytics {
663
717
  batchDelay: 2000,
664
718
  debug: this.config.debug,
665
719
  });
666
- this.log('Section tracking initialized with', config.sections.length, 'sections');
720
+ this.log('Section tracking initialized successfully with', config.sections.length, 'sections');
667
721
  }
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
- });
722
+ catch (error) {
723
+ this.log('❌ Failed to initialize section tracking:', error);
724
+ }
725
+ }).catch((error) => {
726
+ this.log('❌ Failed to load section tracking module:', error);
727
+ });
728
+ }
729
+ else {
730
+ this.log('No sections configured for auto-tracking');
731
+ }
675
732
  }
676
733
  /**
677
734
  * 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.4",
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",