@grainql/analytics-web 2.2.0 → 2.3.0
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/attribution.d.ts +47 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/attribution.js +228 -0
- package/dist/cjs/attribution.d.ts +47 -0
- package/dist/cjs/attribution.d.ts.map +1 -0
- package/dist/cjs/attribution.js +228 -0
- package/dist/cjs/attribution.js.map +1 -0
- package/dist/cjs/heartbeat.d.ts +1 -0
- package/dist/cjs/heartbeat.d.ts.map +1 -1
- package/dist/cjs/heartbeat.js +1 -1
- package/dist/cjs/heartbeat.js.map +1 -1
- package/dist/cjs/index.d.ts +25 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts +25 -0
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +158 -9
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/esm/attribution.d.ts +47 -0
- package/dist/esm/attribution.d.ts.map +1 -0
- package/dist/esm/attribution.js +218 -0
- package/dist/esm/attribution.js.map +1 -0
- package/dist/esm/heartbeat.d.ts +1 -0
- package/dist/esm/heartbeat.d.ts.map +1 -1
- package/dist/esm/heartbeat.js +1 -1
- package/dist/esm/heartbeat.js.map +1 -1
- package/dist/esm/index.d.ts +25 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-tracking.d.ts +25 -0
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +158 -9
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/heartbeat.d.ts +1 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +1 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +464 -11
- package/dist/index.global.dev.js.map +3 -3
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +157 -1
- package/dist/index.mjs +155 -0
- package/dist/page-tracking.d.ts +25 -0
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +158 -9
- package/dist/react/index.d.ts +44 -519
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +54 -1517
- package/dist/react/index.mjs +42 -1514
- package/package.json +1 -1
- package/dist/react/activity.d.ts +0 -59
- package/dist/react/activity.d.ts.map +0 -1
- package/dist/react/activity.js +0 -130
- package/dist/react/activity.mjs +0 -126
- package/dist/react/consent.d.ts +0 -72
- package/dist/react/consent.d.ts.map +0 -1
- package/dist/react/consent.js +0 -195
- package/dist/react/consent.mjs +0 -191
- package/dist/react/cookies.d.ts +0 -28
- package/dist/react/cookies.d.ts.map +0 -1
- package/dist/react/cookies.js +0 -94
- package/dist/react/cookies.mjs +0 -88
- package/dist/react/heartbeat.d.ts +0 -47
- package/dist/react/heartbeat.d.ts.map +0 -1
- package/dist/react/heartbeat.js +0 -119
- package/dist/react/heartbeat.mjs +0 -115
- package/dist/react/page-tracking.d.ts +0 -60
- package/dist/react/page-tracking.d.ts.map +0 -1
- package/dist/react/page-tracking.js +0 -179
- package/dist/react/page-tracking.mjs +0 -175
- package/dist/react/react/index.d.ts +0 -47
- package/dist/react/react/index.d.ts.map +0 -1
- package/dist/react/react/index.js +0 -58
- package/dist/react/react/index.mjs +0 -44
- /package/dist/react/{react/GrainProvider.d.ts → GrainProvider.d.ts} +0 -0
- /package/dist/react/{react/GrainProvider.d.ts.map → GrainProvider.d.ts.map} +0 -0
- /package/dist/react/{react/GrainProvider.js → GrainProvider.js} +0 -0
- /package/dist/react/{react/GrainProvider.mjs → GrainProvider.mjs} +0 -0
- /package/dist/react/{react/components → components}/ConsentBanner.d.ts +0 -0
- /package/dist/react/{react/components → components}/ConsentBanner.d.ts.map +0 -0
- /package/dist/react/{react/components → components}/ConsentBanner.js +0 -0
- /package/dist/react/{react/components → components}/ConsentBanner.mjs +0 -0
- /package/dist/react/{react/components → components}/CookieNotice.d.ts +0 -0
- /package/dist/react/{react/components → components}/CookieNotice.d.ts.map +0 -0
- /package/dist/react/{react/components → components}/CookieNotice.js +0 -0
- /package/dist/react/{react/components → components}/CookieNotice.mjs +0 -0
- /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.d.ts +0 -0
- /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.d.ts.map +0 -0
- /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.js +0 -0
- /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.mjs +0 -0
- /package/dist/react/{react/context.d.ts → context.d.ts} +0 -0
- /package/dist/react/{react/context.d.ts.map → context.d.ts.map} +0 -0
- /package/dist/react/{react/context.js → context.js} +0 -0
- /package/dist/react/{react/context.mjs → context.mjs} +0 -0
- /package/dist/react/{react/hooks → hooks}/useAllConfigs.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/useAllConfigs.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/useAllConfigs.js +0 -0
- /package/dist/react/{react/hooks → hooks}/useAllConfigs.mjs +0 -0
- /package/dist/react/{react/hooks → hooks}/useConfig.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/useConfig.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/useConfig.js +0 -0
- /package/dist/react/{react/hooks → hooks}/useConfig.mjs +0 -0
- /package/dist/react/{react/hooks → hooks}/useConsent.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/useConsent.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/useConsent.js +0 -0
- /package/dist/react/{react/hooks → hooks}/useConsent.mjs +0 -0
- /package/dist/react/{react/hooks → hooks}/useDataDeletion.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/useDataDeletion.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/useDataDeletion.js +0 -0
- /package/dist/react/{react/hooks → hooks}/useDataDeletion.mjs +0 -0
- /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.js +0 -0
- /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.mjs +0 -0
- /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.js +0 -0
- /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.mjs +0 -0
- /package/dist/react/{react/hooks → hooks}/useTrack.d.ts +0 -0
- /package/dist/react/{react/hooks → hooks}/useTrack.d.ts.map +0 -0
- /package/dist/react/{react/hooks → hooks}/useTrack.js +0 -0
- /package/dist/react/{react/hooks → hooks}/useTrack.mjs +0 -0
- /package/dist/react/{react/types.d.ts → types.d.ts} +0 -0
- /package/dist/react/{react/types.d.ts.map → types.d.ts.map} +0 -0
- /package/dist/react/{react/types.js → types.js} +0 -0
- /package/dist/react/{react/types.mjs → types.mjs} +0 -0
package/dist/index.js
CHANGED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.GrainAnalytics = void 0;
|
|
7
|
+
exports.GrainAnalytics = exports.parseUTMParameters = exports.categorizeReferrer = void 0;
|
|
8
8
|
exports.createGrainAnalytics = createGrainAnalytics;
|
|
9
9
|
const consent_1 = require("./consent");
|
|
10
10
|
const cookies_1 = require("./cookies");
|
|
11
11
|
const activity_1 = require("./activity");
|
|
12
12
|
const heartbeat_1 = require("./heartbeat");
|
|
13
13
|
const page_tracking_1 = require("./page-tracking");
|
|
14
|
+
const attribution_1 = require("./attribution");
|
|
15
|
+
Object.defineProperty(exports, "categorizeReferrer", { enumerable: true, get: function () { return attribution_1.categorizeReferrer; } });
|
|
16
|
+
Object.defineProperty(exports, "parseUTMParameters", { enumerable: true, get: function () { return attribution_1.parseUTMParameters; } });
|
|
14
17
|
class GrainAnalytics {
|
|
15
18
|
constructor(config) {
|
|
16
19
|
this.eventQueue = [];
|
|
@@ -31,6 +34,9 @@ class GrainAnalytics {
|
|
|
31
34
|
this.pageTrackingManager = null;
|
|
32
35
|
this.ephemeralSessionId = null;
|
|
33
36
|
this.eventCountSinceLastHeartbeat = 0;
|
|
37
|
+
// Session tracking
|
|
38
|
+
this.sessionStartTime = Date.now();
|
|
39
|
+
this.sessionEventCount = 0;
|
|
34
40
|
this.config = {
|
|
35
41
|
apiUrl: 'https://api.grainql.com',
|
|
36
42
|
authStrategy: 'NONE',
|
|
@@ -83,6 +89,8 @@ class GrainAnalytics {
|
|
|
83
89
|
// Initialize automatic tracking (browser only)
|
|
84
90
|
if (typeof window !== 'undefined') {
|
|
85
91
|
this.initializeAutomaticTracking();
|
|
92
|
+
// Track session start
|
|
93
|
+
this.trackSessionStart();
|
|
86
94
|
}
|
|
87
95
|
// Set up consent change listener to flush waiting events and handle consent upgrade
|
|
88
96
|
this.consentManager.addListener((state) => {
|
|
@@ -521,6 +529,8 @@ class GrainAnalytics {
|
|
|
521
529
|
if (typeof window === 'undefined')
|
|
522
530
|
return;
|
|
523
531
|
const handleBeforeUnload = () => {
|
|
532
|
+
// Track session end
|
|
533
|
+
this.trackSessionEnd();
|
|
524
534
|
if (this.eventQueue.length > 0) {
|
|
525
535
|
// Use beacon API for reliable delivery during page unload
|
|
526
536
|
const eventsToSend = [...this.eventQueue];
|
|
@@ -575,6 +585,7 @@ class GrainAnalytics {
|
|
|
575
585
|
this.pageTrackingManager = new page_tracking_1.PageTrackingManager(this, {
|
|
576
586
|
stripQueryParams: this.config.stripQueryParams,
|
|
577
587
|
debug: this.config.debug,
|
|
588
|
+
tenantId: this.config.tenantId,
|
|
578
589
|
});
|
|
579
590
|
this.log('Auto page view tracking initialized');
|
|
580
591
|
}
|
|
@@ -583,6 +594,150 @@ class GrainAnalytics {
|
|
|
583
594
|
}
|
|
584
595
|
}
|
|
585
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* Track session start event
|
|
599
|
+
*/
|
|
600
|
+
trackSessionStart() {
|
|
601
|
+
if (typeof window === 'undefined')
|
|
602
|
+
return;
|
|
603
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
604
|
+
const properties = {
|
|
605
|
+
session_id: this.getSessionId(),
|
|
606
|
+
timestamp: this.sessionStartTime,
|
|
607
|
+
};
|
|
608
|
+
if (hasConsent) {
|
|
609
|
+
const referrer = document.referrer || '';
|
|
610
|
+
const currentUrl = window.location.href;
|
|
611
|
+
// Parse UTM parameters
|
|
612
|
+
const utmParams = (0, attribution_1.parseUTMParameters)(currentUrl);
|
|
613
|
+
const sessionUTMs = (0, attribution_1.getSessionUTMParameters)() || utmParams;
|
|
614
|
+
// Get first-touch attribution
|
|
615
|
+
const firstTouch = (0, attribution_1.getOrCreateFirstTouchAttribution)(this.config.tenantId, referrer, currentUrl, sessionUTMs);
|
|
616
|
+
// Landing page
|
|
617
|
+
properties.landing_page = window.location.pathname;
|
|
618
|
+
// Referrer info
|
|
619
|
+
if (referrer) {
|
|
620
|
+
properties.referrer = referrer;
|
|
621
|
+
properties.referrer_domain = new URL(referrer).hostname;
|
|
622
|
+
properties.referrer_category = (0, attribution_1.categorizeReferrer)(referrer, currentUrl);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
properties.referrer_category = 'direct';
|
|
626
|
+
}
|
|
627
|
+
// UTM parameters
|
|
628
|
+
if (sessionUTMs.utm_source)
|
|
629
|
+
properties.utm_source = sessionUTMs.utm_source;
|
|
630
|
+
if (sessionUTMs.utm_medium)
|
|
631
|
+
properties.utm_medium = sessionUTMs.utm_medium;
|
|
632
|
+
if (sessionUTMs.utm_campaign)
|
|
633
|
+
properties.utm_campaign = sessionUTMs.utm_campaign;
|
|
634
|
+
if (sessionUTMs.utm_term)
|
|
635
|
+
properties.utm_term = sessionUTMs.utm_term;
|
|
636
|
+
if (sessionUTMs.utm_content)
|
|
637
|
+
properties.utm_content = sessionUTMs.utm_content;
|
|
638
|
+
// First-touch attribution
|
|
639
|
+
properties.first_touch_source = firstTouch.source;
|
|
640
|
+
properties.first_touch_medium = firstTouch.medium;
|
|
641
|
+
properties.first_touch_campaign = firstTouch.campaign;
|
|
642
|
+
properties.first_touch_referrer_category = firstTouch.referrer_category;
|
|
643
|
+
// Device and browser info
|
|
644
|
+
properties.device = this.getDeviceType();
|
|
645
|
+
properties.screen_resolution = `${screen.width}x${screen.height}`;
|
|
646
|
+
properties.viewport = `${window.innerWidth}x${window.innerHeight}`;
|
|
647
|
+
properties.browser = this.getBrowser();
|
|
648
|
+
properties.os = this.getOS();
|
|
649
|
+
properties.language = navigator.language || '';
|
|
650
|
+
properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
651
|
+
}
|
|
652
|
+
this.trackSystemEvent('_grain_session_start', properties);
|
|
653
|
+
this.log('Session started:', properties);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Track session end event
|
|
657
|
+
*/
|
|
658
|
+
trackSessionEnd() {
|
|
659
|
+
if (typeof window === 'undefined')
|
|
660
|
+
return;
|
|
661
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
662
|
+
const sessionDuration = Date.now() - this.sessionStartTime;
|
|
663
|
+
const properties = {
|
|
664
|
+
session_id: this.getSessionId(),
|
|
665
|
+
session_duration: Math.floor(sessionDuration / 1000), // In seconds for easier querying
|
|
666
|
+
duration: sessionDuration, // Keep for backward compatibility
|
|
667
|
+
event_count: this.sessionEventCount,
|
|
668
|
+
timestamp: Date.now(),
|
|
669
|
+
};
|
|
670
|
+
if (hasConsent && this.pageTrackingManager) {
|
|
671
|
+
const pageCount = this.pageTrackingManager.getPageViewCount();
|
|
672
|
+
properties.pages_per_session = pageCount;
|
|
673
|
+
properties.page_count = pageCount; // Keep for backward compatibility
|
|
674
|
+
}
|
|
675
|
+
this.trackSystemEvent('_grain_session_end', properties);
|
|
676
|
+
this.log('Session ended:', properties);
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Detect browser name
|
|
680
|
+
*/
|
|
681
|
+
getBrowser() {
|
|
682
|
+
if (typeof navigator === 'undefined')
|
|
683
|
+
return 'Unknown';
|
|
684
|
+
const ua = navigator.userAgent;
|
|
685
|
+
if (ua.includes('Firefox/'))
|
|
686
|
+
return 'Firefox';
|
|
687
|
+
if (ua.includes('Edg/'))
|
|
688
|
+
return 'Edge';
|
|
689
|
+
if (ua.includes('Chrome/'))
|
|
690
|
+
return 'Chrome';
|
|
691
|
+
if (ua.includes('Safari/') && !ua.includes('Chrome/'))
|
|
692
|
+
return 'Safari';
|
|
693
|
+
if (ua.includes('Opera/') || ua.includes('OPR/'))
|
|
694
|
+
return 'Opera';
|
|
695
|
+
return 'Unknown';
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Detect operating system
|
|
699
|
+
*/
|
|
700
|
+
getOS() {
|
|
701
|
+
if (typeof navigator === 'undefined')
|
|
702
|
+
return 'Unknown';
|
|
703
|
+
const ua = navigator.userAgent;
|
|
704
|
+
if (ua.includes('Win'))
|
|
705
|
+
return 'Windows';
|
|
706
|
+
if (ua.includes('Mac'))
|
|
707
|
+
return 'macOS';
|
|
708
|
+
if (ua.includes('Linux'))
|
|
709
|
+
return 'Linux';
|
|
710
|
+
if (ua.includes('Android'))
|
|
711
|
+
return 'Android';
|
|
712
|
+
if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad'))
|
|
713
|
+
return 'iOS';
|
|
714
|
+
return 'Unknown';
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Detect device type (Mobile, Tablet, Desktop)
|
|
718
|
+
*/
|
|
719
|
+
getDeviceType() {
|
|
720
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined')
|
|
721
|
+
return 'Unknown';
|
|
722
|
+
const ua = navigator.userAgent;
|
|
723
|
+
const width = window.innerWidth;
|
|
724
|
+
// Check for tablet-specific indicators
|
|
725
|
+
if (ua.includes('iPad') || (ua.includes('Android') && !ua.includes('Mobile'))) {
|
|
726
|
+
return 'Tablet';
|
|
727
|
+
}
|
|
728
|
+
// Check for mobile indicators
|
|
729
|
+
if (ua.includes('Mobile') || ua.includes('iPhone') || ua.includes('Android')) {
|
|
730
|
+
return 'Mobile';
|
|
731
|
+
}
|
|
732
|
+
// Fallback to screen width detection
|
|
733
|
+
if (width < 768) {
|
|
734
|
+
return 'Mobile';
|
|
735
|
+
}
|
|
736
|
+
else if (width >= 768 && width < 1024) {
|
|
737
|
+
return 'Tablet';
|
|
738
|
+
}
|
|
739
|
+
return 'Desktop';
|
|
740
|
+
}
|
|
586
741
|
/**
|
|
587
742
|
* Handle consent granted - upgrade ephemeral session to persistent user
|
|
588
743
|
*/
|
|
@@ -716,6 +871,7 @@ class GrainAnalytics {
|
|
|
716
871
|
}
|
|
717
872
|
this.eventQueue.push(formattedEvent);
|
|
718
873
|
this.eventCountSinceLastHeartbeat++;
|
|
874
|
+
this.sessionEventCount++;
|
|
719
875
|
this.log(`Queued event: ${event.eventName}`, event.properties);
|
|
720
876
|
// Check if we should flush immediately
|
|
721
877
|
if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
|
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,8 @@ 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';
|
|
11
|
+
export { categorizeReferrer, parseUTMParameters };
|
|
10
12
|
export class GrainAnalytics {
|
|
11
13
|
constructor(config) {
|
|
12
14
|
this.eventQueue = [];
|
|
@@ -27,6 +29,9 @@ export class GrainAnalytics {
|
|
|
27
29
|
this.pageTrackingManager = null;
|
|
28
30
|
this.ephemeralSessionId = null;
|
|
29
31
|
this.eventCountSinceLastHeartbeat = 0;
|
|
32
|
+
// Session tracking
|
|
33
|
+
this.sessionStartTime = Date.now();
|
|
34
|
+
this.sessionEventCount = 0;
|
|
30
35
|
this.config = {
|
|
31
36
|
apiUrl: 'https://api.grainql.com',
|
|
32
37
|
authStrategy: 'NONE',
|
|
@@ -79,6 +84,8 @@ export class GrainAnalytics {
|
|
|
79
84
|
// Initialize automatic tracking (browser only)
|
|
80
85
|
if (typeof window !== 'undefined') {
|
|
81
86
|
this.initializeAutomaticTracking();
|
|
87
|
+
// Track session start
|
|
88
|
+
this.trackSessionStart();
|
|
82
89
|
}
|
|
83
90
|
// Set up consent change listener to flush waiting events and handle consent upgrade
|
|
84
91
|
this.consentManager.addListener((state) => {
|
|
@@ -517,6 +524,8 @@ export class GrainAnalytics {
|
|
|
517
524
|
if (typeof window === 'undefined')
|
|
518
525
|
return;
|
|
519
526
|
const handleBeforeUnload = () => {
|
|
527
|
+
// Track session end
|
|
528
|
+
this.trackSessionEnd();
|
|
520
529
|
if (this.eventQueue.length > 0) {
|
|
521
530
|
// Use beacon API for reliable delivery during page unload
|
|
522
531
|
const eventsToSend = [...this.eventQueue];
|
|
@@ -571,6 +580,7 @@ export class GrainAnalytics {
|
|
|
571
580
|
this.pageTrackingManager = new PageTrackingManager(this, {
|
|
572
581
|
stripQueryParams: this.config.stripQueryParams,
|
|
573
582
|
debug: this.config.debug,
|
|
583
|
+
tenantId: this.config.tenantId,
|
|
574
584
|
});
|
|
575
585
|
this.log('Auto page view tracking initialized');
|
|
576
586
|
}
|
|
@@ -579,6 +589,150 @@ export class GrainAnalytics {
|
|
|
579
589
|
}
|
|
580
590
|
}
|
|
581
591
|
}
|
|
592
|
+
/**
|
|
593
|
+
* Track session start event
|
|
594
|
+
*/
|
|
595
|
+
trackSessionStart() {
|
|
596
|
+
if (typeof window === 'undefined')
|
|
597
|
+
return;
|
|
598
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
599
|
+
const properties = {
|
|
600
|
+
session_id: this.getSessionId(),
|
|
601
|
+
timestamp: this.sessionStartTime,
|
|
602
|
+
};
|
|
603
|
+
if (hasConsent) {
|
|
604
|
+
const referrer = document.referrer || '';
|
|
605
|
+
const currentUrl = window.location.href;
|
|
606
|
+
// Parse UTM parameters
|
|
607
|
+
const utmParams = parseUTMParameters(currentUrl);
|
|
608
|
+
const sessionUTMs = getSessionUTMParameters() || utmParams;
|
|
609
|
+
// Get first-touch attribution
|
|
610
|
+
const firstTouch = getOrCreateFirstTouchAttribution(this.config.tenantId, referrer, currentUrl, sessionUTMs);
|
|
611
|
+
// Landing page
|
|
612
|
+
properties.landing_page = window.location.pathname;
|
|
613
|
+
// Referrer info
|
|
614
|
+
if (referrer) {
|
|
615
|
+
properties.referrer = referrer;
|
|
616
|
+
properties.referrer_domain = new URL(referrer).hostname;
|
|
617
|
+
properties.referrer_category = categorizeReferrer(referrer, currentUrl);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
properties.referrer_category = 'direct';
|
|
621
|
+
}
|
|
622
|
+
// UTM parameters
|
|
623
|
+
if (sessionUTMs.utm_source)
|
|
624
|
+
properties.utm_source = sessionUTMs.utm_source;
|
|
625
|
+
if (sessionUTMs.utm_medium)
|
|
626
|
+
properties.utm_medium = sessionUTMs.utm_medium;
|
|
627
|
+
if (sessionUTMs.utm_campaign)
|
|
628
|
+
properties.utm_campaign = sessionUTMs.utm_campaign;
|
|
629
|
+
if (sessionUTMs.utm_term)
|
|
630
|
+
properties.utm_term = sessionUTMs.utm_term;
|
|
631
|
+
if (sessionUTMs.utm_content)
|
|
632
|
+
properties.utm_content = sessionUTMs.utm_content;
|
|
633
|
+
// First-touch attribution
|
|
634
|
+
properties.first_touch_source = firstTouch.source;
|
|
635
|
+
properties.first_touch_medium = firstTouch.medium;
|
|
636
|
+
properties.first_touch_campaign = firstTouch.campaign;
|
|
637
|
+
properties.first_touch_referrer_category = firstTouch.referrer_category;
|
|
638
|
+
// Device and browser info
|
|
639
|
+
properties.device = this.getDeviceType();
|
|
640
|
+
properties.screen_resolution = `${screen.width}x${screen.height}`;
|
|
641
|
+
properties.viewport = `${window.innerWidth}x${window.innerHeight}`;
|
|
642
|
+
properties.browser = this.getBrowser();
|
|
643
|
+
properties.os = this.getOS();
|
|
644
|
+
properties.language = navigator.language || '';
|
|
645
|
+
properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
646
|
+
}
|
|
647
|
+
this.trackSystemEvent('_grain_session_start', properties);
|
|
648
|
+
this.log('Session started:', properties);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Track session end event
|
|
652
|
+
*/
|
|
653
|
+
trackSessionEnd() {
|
|
654
|
+
if (typeof window === 'undefined')
|
|
655
|
+
return;
|
|
656
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
657
|
+
const sessionDuration = Date.now() - this.sessionStartTime;
|
|
658
|
+
const properties = {
|
|
659
|
+
session_id: this.getSessionId(),
|
|
660
|
+
session_duration: Math.floor(sessionDuration / 1000), // In seconds for easier querying
|
|
661
|
+
duration: sessionDuration, // Keep for backward compatibility
|
|
662
|
+
event_count: this.sessionEventCount,
|
|
663
|
+
timestamp: Date.now(),
|
|
664
|
+
};
|
|
665
|
+
if (hasConsent && this.pageTrackingManager) {
|
|
666
|
+
const pageCount = this.pageTrackingManager.getPageViewCount();
|
|
667
|
+
properties.pages_per_session = pageCount;
|
|
668
|
+
properties.page_count = pageCount; // Keep for backward compatibility
|
|
669
|
+
}
|
|
670
|
+
this.trackSystemEvent('_grain_session_end', properties);
|
|
671
|
+
this.log('Session ended:', properties);
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Detect browser name
|
|
675
|
+
*/
|
|
676
|
+
getBrowser() {
|
|
677
|
+
if (typeof navigator === 'undefined')
|
|
678
|
+
return 'Unknown';
|
|
679
|
+
const ua = navigator.userAgent;
|
|
680
|
+
if (ua.includes('Firefox/'))
|
|
681
|
+
return 'Firefox';
|
|
682
|
+
if (ua.includes('Edg/'))
|
|
683
|
+
return 'Edge';
|
|
684
|
+
if (ua.includes('Chrome/'))
|
|
685
|
+
return 'Chrome';
|
|
686
|
+
if (ua.includes('Safari/') && !ua.includes('Chrome/'))
|
|
687
|
+
return 'Safari';
|
|
688
|
+
if (ua.includes('Opera/') || ua.includes('OPR/'))
|
|
689
|
+
return 'Opera';
|
|
690
|
+
return 'Unknown';
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Detect operating system
|
|
694
|
+
*/
|
|
695
|
+
getOS() {
|
|
696
|
+
if (typeof navigator === 'undefined')
|
|
697
|
+
return 'Unknown';
|
|
698
|
+
const ua = navigator.userAgent;
|
|
699
|
+
if (ua.includes('Win'))
|
|
700
|
+
return 'Windows';
|
|
701
|
+
if (ua.includes('Mac'))
|
|
702
|
+
return 'macOS';
|
|
703
|
+
if (ua.includes('Linux'))
|
|
704
|
+
return 'Linux';
|
|
705
|
+
if (ua.includes('Android'))
|
|
706
|
+
return 'Android';
|
|
707
|
+
if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad'))
|
|
708
|
+
return 'iOS';
|
|
709
|
+
return 'Unknown';
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Detect device type (Mobile, Tablet, Desktop)
|
|
713
|
+
*/
|
|
714
|
+
getDeviceType() {
|
|
715
|
+
if (typeof window === 'undefined' || typeof navigator === 'undefined')
|
|
716
|
+
return 'Unknown';
|
|
717
|
+
const ua = navigator.userAgent;
|
|
718
|
+
const width = window.innerWidth;
|
|
719
|
+
// Check for tablet-specific indicators
|
|
720
|
+
if (ua.includes('iPad') || (ua.includes('Android') && !ua.includes('Mobile'))) {
|
|
721
|
+
return 'Tablet';
|
|
722
|
+
}
|
|
723
|
+
// Check for mobile indicators
|
|
724
|
+
if (ua.includes('Mobile') || ua.includes('iPhone') || ua.includes('Android')) {
|
|
725
|
+
return 'Mobile';
|
|
726
|
+
}
|
|
727
|
+
// Fallback to screen width detection
|
|
728
|
+
if (width < 768) {
|
|
729
|
+
return 'Mobile';
|
|
730
|
+
}
|
|
731
|
+
else if (width >= 768 && width < 1024) {
|
|
732
|
+
return 'Tablet';
|
|
733
|
+
}
|
|
734
|
+
return 'Desktop';
|
|
735
|
+
}
|
|
582
736
|
/**
|
|
583
737
|
* Handle consent granted - upgrade ephemeral session to persistent user
|
|
584
738
|
*/
|
|
@@ -712,6 +866,7 @@ export class GrainAnalytics {
|
|
|
712
866
|
}
|
|
713
867
|
this.eventQueue.push(formattedEvent);
|
|
714
868
|
this.eventCountSinceLastHeartbeat++;
|
|
869
|
+
this.sessionEventCount++;
|
|
715
870
|
this.log(`Queued event: ${event.eventName}`, event.properties);
|
|
716
871
|
// Check if we should flush immediately
|
|
717
872
|
if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
|
package/dist/page-tracking.d.ts
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
export interface PageTrackingConfig {
|
|
6
6
|
stripQueryParams: boolean;
|
|
7
7
|
debug?: boolean;
|
|
8
|
+
tenantId: string;
|
|
8
9
|
}
|
|
9
10
|
export interface PageTracker {
|
|
10
11
|
trackSystemEvent(eventName: string, properties: Record<string, unknown>): void;
|
|
11
12
|
hasConsent(category?: string): boolean;
|
|
12
13
|
getEffectiveUserId(): string;
|
|
13
14
|
getEphemeralSessionId(): string;
|
|
15
|
+
getSessionId(): string;
|
|
14
16
|
}
|
|
15
17
|
export declare class PageTrackingManager {
|
|
16
18
|
private config;
|
|
@@ -19,6 +21,9 @@ export declare class PageTrackingManager {
|
|
|
19
21
|
private currentPath;
|
|
20
22
|
private originalPushState;
|
|
21
23
|
private originalReplaceState;
|
|
24
|
+
private previousPage;
|
|
25
|
+
private landingPage;
|
|
26
|
+
private pageViewCount;
|
|
22
27
|
constructor(tracker: PageTracker, config: PageTrackingConfig);
|
|
23
28
|
/**
|
|
24
29
|
* Setup History API listeners (pushState, replaceState, popstate)
|
|
@@ -40,6 +45,22 @@ export declare class PageTrackingManager {
|
|
|
40
45
|
* Track the current page
|
|
41
46
|
*/
|
|
42
47
|
private trackCurrentPage;
|
|
48
|
+
/**
|
|
49
|
+
* Extract domain from URL
|
|
50
|
+
*/
|
|
51
|
+
private extractDomain;
|
|
52
|
+
/**
|
|
53
|
+
* Detect browser name
|
|
54
|
+
*/
|
|
55
|
+
private getBrowser;
|
|
56
|
+
/**
|
|
57
|
+
* Detect operating system
|
|
58
|
+
*/
|
|
59
|
+
private getOS;
|
|
60
|
+
/**
|
|
61
|
+
* Detect device type (Mobile, Tablet, Desktop)
|
|
62
|
+
*/
|
|
63
|
+
private getDeviceType;
|
|
43
64
|
/**
|
|
44
65
|
* Extract path from URL, optionally stripping query parameters
|
|
45
66
|
*/
|
|
@@ -52,6 +73,10 @@ export declare class PageTrackingManager {
|
|
|
52
73
|
* Manually track a page view (for custom navigation)
|
|
53
74
|
*/
|
|
54
75
|
trackPage(page: string, properties?: Record<string, unknown>): void;
|
|
76
|
+
/**
|
|
77
|
+
* Get page view count for current session
|
|
78
|
+
*/
|
|
79
|
+
getPageViewCount(): number;
|
|
55
80
|
/**
|
|
56
81
|
* Destroy the page tracker
|
|
57
82
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAYH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,oBAAoB,CAA4C;IACxE,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAK;gBAEd,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB;IAY5D;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,cAAc,CAGpB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAGtB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA2GxB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACH,OAAO,CAAC,KAAK;IAUb;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;OAEG;IACH,OAAO,CAAC,WAAW;IAmBnB;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAyCnE;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,OAAO,IAAI,IAAI;CAyBhB"}
|