@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.
Files changed (128) hide show
  1. package/dist/attribution.d.ts +47 -0
  2. package/dist/attribution.d.ts.map +1 -0
  3. package/dist/attribution.js +228 -0
  4. package/dist/cjs/attribution.d.ts +47 -0
  5. package/dist/cjs/attribution.d.ts.map +1 -0
  6. package/dist/cjs/attribution.js +228 -0
  7. package/dist/cjs/attribution.js.map +1 -0
  8. package/dist/cjs/heartbeat.d.ts +1 -0
  9. package/dist/cjs/heartbeat.d.ts.map +1 -1
  10. package/dist/cjs/heartbeat.js +1 -1
  11. package/dist/cjs/heartbeat.js.map +1 -1
  12. package/dist/cjs/index.d.ts +25 -0
  13. package/dist/cjs/index.d.ts.map +1 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/page-tracking.d.ts +25 -0
  16. package/dist/cjs/page-tracking.d.ts.map +1 -1
  17. package/dist/cjs/page-tracking.js +158 -9
  18. package/dist/cjs/page-tracking.js.map +1 -1
  19. package/dist/esm/attribution.d.ts +47 -0
  20. package/dist/esm/attribution.d.ts.map +1 -0
  21. package/dist/esm/attribution.js +218 -0
  22. package/dist/esm/attribution.js.map +1 -0
  23. package/dist/esm/heartbeat.d.ts +1 -0
  24. package/dist/esm/heartbeat.d.ts.map +1 -1
  25. package/dist/esm/heartbeat.js +1 -1
  26. package/dist/esm/heartbeat.js.map +1 -1
  27. package/dist/esm/index.d.ts +25 -0
  28. package/dist/esm/index.d.ts.map +1 -1
  29. package/dist/esm/index.js.map +1 -1
  30. package/dist/esm/page-tracking.d.ts +25 -0
  31. package/dist/esm/page-tracking.d.ts.map +1 -1
  32. package/dist/esm/page-tracking.js +158 -9
  33. package/dist/esm/page-tracking.js.map +1 -1
  34. package/dist/heartbeat.d.ts +1 -0
  35. package/dist/heartbeat.d.ts.map +1 -1
  36. package/dist/heartbeat.js +1 -1
  37. package/dist/index.d.ts +25 -0
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.global.dev.js +464 -11
  40. package/dist/index.global.dev.js.map +3 -3
  41. package/dist/index.global.js +2 -2
  42. package/dist/index.global.js.map +4 -4
  43. package/dist/index.js +157 -1
  44. package/dist/index.mjs +155 -0
  45. package/dist/page-tracking.d.ts +25 -0
  46. package/dist/page-tracking.d.ts.map +1 -1
  47. package/dist/page-tracking.js +158 -9
  48. package/dist/react/index.d.ts +44 -519
  49. package/dist/react/index.d.ts.map +1 -1
  50. package/dist/react/index.js +54 -1517
  51. package/dist/react/index.mjs +42 -1514
  52. package/package.json +1 -1
  53. package/dist/react/activity.d.ts +0 -59
  54. package/dist/react/activity.d.ts.map +0 -1
  55. package/dist/react/activity.js +0 -130
  56. package/dist/react/activity.mjs +0 -126
  57. package/dist/react/consent.d.ts +0 -72
  58. package/dist/react/consent.d.ts.map +0 -1
  59. package/dist/react/consent.js +0 -195
  60. package/dist/react/consent.mjs +0 -191
  61. package/dist/react/cookies.d.ts +0 -28
  62. package/dist/react/cookies.d.ts.map +0 -1
  63. package/dist/react/cookies.js +0 -94
  64. package/dist/react/cookies.mjs +0 -88
  65. package/dist/react/heartbeat.d.ts +0 -47
  66. package/dist/react/heartbeat.d.ts.map +0 -1
  67. package/dist/react/heartbeat.js +0 -119
  68. package/dist/react/heartbeat.mjs +0 -115
  69. package/dist/react/page-tracking.d.ts +0 -60
  70. package/dist/react/page-tracking.d.ts.map +0 -1
  71. package/dist/react/page-tracking.js +0 -179
  72. package/dist/react/page-tracking.mjs +0 -175
  73. package/dist/react/react/index.d.ts +0 -47
  74. package/dist/react/react/index.d.ts.map +0 -1
  75. package/dist/react/react/index.js +0 -58
  76. package/dist/react/react/index.mjs +0 -44
  77. /package/dist/react/{react/GrainProvider.d.ts → GrainProvider.d.ts} +0 -0
  78. /package/dist/react/{react/GrainProvider.d.ts.map → GrainProvider.d.ts.map} +0 -0
  79. /package/dist/react/{react/GrainProvider.js → GrainProvider.js} +0 -0
  80. /package/dist/react/{react/GrainProvider.mjs → GrainProvider.mjs} +0 -0
  81. /package/dist/react/{react/components → components}/ConsentBanner.d.ts +0 -0
  82. /package/dist/react/{react/components → components}/ConsentBanner.d.ts.map +0 -0
  83. /package/dist/react/{react/components → components}/ConsentBanner.js +0 -0
  84. /package/dist/react/{react/components → components}/ConsentBanner.mjs +0 -0
  85. /package/dist/react/{react/components → components}/CookieNotice.d.ts +0 -0
  86. /package/dist/react/{react/components → components}/CookieNotice.d.ts.map +0 -0
  87. /package/dist/react/{react/components → components}/CookieNotice.js +0 -0
  88. /package/dist/react/{react/components → components}/CookieNotice.mjs +0 -0
  89. /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.d.ts +0 -0
  90. /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.d.ts.map +0 -0
  91. /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.js +0 -0
  92. /package/dist/react/{react/components → components}/PrivacyPreferenceCenter.mjs +0 -0
  93. /package/dist/react/{react/context.d.ts → context.d.ts} +0 -0
  94. /package/dist/react/{react/context.d.ts.map → context.d.ts.map} +0 -0
  95. /package/dist/react/{react/context.js → context.js} +0 -0
  96. /package/dist/react/{react/context.mjs → context.mjs} +0 -0
  97. /package/dist/react/{react/hooks → hooks}/useAllConfigs.d.ts +0 -0
  98. /package/dist/react/{react/hooks → hooks}/useAllConfigs.d.ts.map +0 -0
  99. /package/dist/react/{react/hooks → hooks}/useAllConfigs.js +0 -0
  100. /package/dist/react/{react/hooks → hooks}/useAllConfigs.mjs +0 -0
  101. /package/dist/react/{react/hooks → hooks}/useConfig.d.ts +0 -0
  102. /package/dist/react/{react/hooks → hooks}/useConfig.d.ts.map +0 -0
  103. /package/dist/react/{react/hooks → hooks}/useConfig.js +0 -0
  104. /package/dist/react/{react/hooks → hooks}/useConfig.mjs +0 -0
  105. /package/dist/react/{react/hooks → hooks}/useConsent.d.ts +0 -0
  106. /package/dist/react/{react/hooks → hooks}/useConsent.d.ts.map +0 -0
  107. /package/dist/react/{react/hooks → hooks}/useConsent.js +0 -0
  108. /package/dist/react/{react/hooks → hooks}/useConsent.mjs +0 -0
  109. /package/dist/react/{react/hooks → hooks}/useDataDeletion.d.ts +0 -0
  110. /package/dist/react/{react/hooks → hooks}/useDataDeletion.d.ts.map +0 -0
  111. /package/dist/react/{react/hooks → hooks}/useDataDeletion.js +0 -0
  112. /package/dist/react/{react/hooks → hooks}/useDataDeletion.mjs +0 -0
  113. /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.d.ts +0 -0
  114. /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.d.ts.map +0 -0
  115. /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.js +0 -0
  116. /package/dist/react/{react/hooks → hooks}/useGrainAnalytics.mjs +0 -0
  117. /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.d.ts +0 -0
  118. /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.d.ts.map +0 -0
  119. /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.js +0 -0
  120. /package/dist/react/{react/hooks → hooks}/usePrivacyPreferences.mjs +0 -0
  121. /package/dist/react/{react/hooks → hooks}/useTrack.d.ts +0 -0
  122. /package/dist/react/{react/hooks → hooks}/useTrack.d.ts.map +0 -0
  123. /package/dist/react/{react/hooks → hooks}/useTrack.js +0 -0
  124. /package/dist/react/{react/hooks → hooks}/useTrack.mjs +0 -0
  125. /package/dist/react/{react/types.d.ts → types.d.ts} +0 -0
  126. /package/dist/react/{react/types.d.ts.map → types.d.ts.map} +0 -0
  127. /package/dist/react/{react/types.js → types.js} +0 -0
  128. /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) {
@@ -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;AAEH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;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;CACjC;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;gBAE5D,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;IAmCxB;;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;IAgCnE;;OAEG;IACH,OAAO,IAAI,IAAI;CAyBhB"}
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"}