@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.
- 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 +171 -33
- 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 +89 -32
- package/dist/index.mjs +90 -33
- 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
|
@@ -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
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
|
|
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
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
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
|
|
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.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",
|