@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.
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interaction-tracking.d.ts +4 -1
- package/dist/cjs/interaction-tracking.d.ts.map +1 -1
- package/dist/cjs/interaction-tracking.js +26 -4
- package/dist/cjs/interaction-tracking.js.map +1 -1
- package/dist/cjs/section-tracking.d.ts +10 -0
- package/dist/cjs/section-tracking.d.ts.map +1 -1
- package/dist/cjs/section-tracking.js +83 -1
- package/dist/cjs/section-tracking.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interaction-tracking.d.ts +4 -1
- package/dist/esm/interaction-tracking.d.ts.map +1 -1
- package/dist/esm/interaction-tracking.js +26 -4
- package/dist/esm/interaction-tracking.js.map +1 -1
- package/dist/esm/section-tracking.d.ts +10 -0
- package/dist/esm/section-tracking.d.ts.map +1 -1
- package/dist/esm/section-tracking.js +83 -1
- package/dist/esm/section-tracking.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +140 -32
- package/dist/index.global.dev.js.map +2 -2
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +3 -3
- package/dist/index.js +52 -31
- package/dist/index.mjs +52 -31
- package/dist/interaction-tracking.d.ts +4 -1
- package/dist/interaction-tracking.d.ts.map +1 -1
- package/dist/interaction-tracking.js +26 -4
- package/dist/section-tracking.d.ts +10 -0
- package/dist/section-tracking.d.ts.map +1 -1
- package/dist/section-tracking.js +83 -1
- 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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
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;
|
|
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"}
|
package/dist/section-tracking.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|