@grainql/analytics-web 2.5.3 → 2.6.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 (79) hide show
  1. package/README.md +3 -1
  2. package/dist/activity.js +1 -1
  3. package/dist/cjs/activity.js +1 -1
  4. package/dist/cjs/activity.js.map +1 -1
  5. package/dist/cjs/consent.js +4 -4
  6. package/dist/cjs/consent.js.map +1 -1
  7. package/dist/cjs/heartbeat.d.ts.map +1 -1
  8. package/dist/cjs/heartbeat.js +0 -6
  9. package/dist/cjs/heartbeat.js.map +1 -1
  10. package/dist/cjs/heatmap-tracking.d.ts +90 -0
  11. package/dist/cjs/heatmap-tracking.d.ts.map +1 -0
  12. package/dist/cjs/heatmap-tracking.js +465 -0
  13. package/dist/cjs/heatmap-tracking.js.map +1 -0
  14. package/dist/cjs/index.d.ts +6 -0
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  18. package/dist/cjs/interaction-tracking.js +9 -18
  19. package/dist/cjs/interaction-tracking.js.map +1 -1
  20. package/dist/cjs/page-tracking.d.ts.map +1 -1
  21. package/dist/cjs/page-tracking.js +0 -9
  22. package/dist/cjs/page-tracking.js.map +1 -1
  23. package/dist/cjs/section-tracking.d.ts.map +1 -1
  24. package/dist/cjs/section-tracking.js +1 -7
  25. package/dist/cjs/section-tracking.js.map +1 -1
  26. package/dist/cjs/types/heatmap-tracking.d.ts +41 -0
  27. package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -0
  28. package/dist/cjs/types/heatmap-tracking.js +6 -0
  29. package/dist/cjs/types/heatmap-tracking.js.map +1 -0
  30. package/dist/consent.js +4 -4
  31. package/dist/esm/activity.js +1 -1
  32. package/dist/esm/activity.js.map +1 -1
  33. package/dist/esm/consent.js +4 -4
  34. package/dist/esm/consent.js.map +1 -1
  35. package/dist/esm/heartbeat.d.ts.map +1 -1
  36. package/dist/esm/heartbeat.js +0 -6
  37. package/dist/esm/heartbeat.js.map +1 -1
  38. package/dist/esm/heatmap-tracking.d.ts +90 -0
  39. package/dist/esm/heatmap-tracking.d.ts.map +1 -0
  40. package/dist/esm/heatmap-tracking.js +461 -0
  41. package/dist/esm/heatmap-tracking.js.map +1 -0
  42. package/dist/esm/index.d.ts +6 -0
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  46. package/dist/esm/interaction-tracking.js +9 -18
  47. package/dist/esm/interaction-tracking.js.map +1 -1
  48. package/dist/esm/page-tracking.d.ts.map +1 -1
  49. package/dist/esm/page-tracking.js +0 -9
  50. package/dist/esm/page-tracking.js.map +1 -1
  51. package/dist/esm/section-tracking.d.ts.map +1 -1
  52. package/dist/esm/section-tracking.js +1 -7
  53. package/dist/esm/section-tracking.js.map +1 -1
  54. package/dist/esm/types/heatmap-tracking.d.ts +41 -0
  55. package/dist/esm/types/heatmap-tracking.d.ts.map +1 -0
  56. package/dist/esm/types/heatmap-tracking.js +5 -0
  57. package/dist/esm/types/heatmap-tracking.js.map +1 -0
  58. package/dist/heartbeat.d.ts.map +1 -1
  59. package/dist/heartbeat.js +0 -6
  60. package/dist/heatmap-tracking.d.ts +90 -0
  61. package/dist/heatmap-tracking.d.ts.map +1 -0
  62. package/dist/heatmap-tracking.js +465 -0
  63. package/dist/index.d.ts +6 -0
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.global.dev.js +534 -80
  66. package/dist/index.global.dev.js.map +4 -4
  67. package/dist/index.global.js +2 -2
  68. package/dist/index.global.js.map +4 -4
  69. package/dist/index.js +98 -39
  70. package/dist/index.mjs +99 -40
  71. package/dist/interaction-tracking.d.ts.map +1 -1
  72. package/dist/interaction-tracking.js +9 -18
  73. package/dist/page-tracking.d.ts.map +1 -1
  74. package/dist/page-tracking.js +0 -9
  75. package/dist/section-tracking.d.ts.map +1 -1
  76. package/dist/section-tracking.js +1 -7
  77. package/dist/types/heatmap-tracking.d.ts +41 -0
  78. package/dist/types/heatmap-tracking.d.ts.map +1 -0
  79. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ class GrainAnalytics {
75
75
  // Auto-tracking properties
76
76
  this.interactionTrackingManager = null;
77
77
  this.sectionTrackingManager = null;
78
+ this.heatmapTrackingManager = null;
78
79
  // Session tracking
79
80
  this.sessionStartTime = Date.now();
80
81
  this.sessionEventCount = 0;
@@ -104,6 +105,8 @@ class GrainAnalytics {
104
105
  heartbeatInactiveInterval: 300000, // 5 minutes
105
106
  enableAutoPageView: true,
106
107
  stripQueryParams: true,
108
+ // Heatmap Tracking defaults
109
+ enableHeatmapTracking: true,
107
110
  ...config,
108
111
  tenantId: config.tenantId,
109
112
  };
@@ -132,6 +135,10 @@ class GrainAnalytics {
132
135
  this.initializeAutomaticTracking();
133
136
  // Track session start
134
137
  this.trackSessionStart();
138
+ // Initialize heatmap tracking if enabled
139
+ if (this.config.enableHeatmapTracking) {
140
+ this.initializeHeatmapTracking();
141
+ }
135
142
  }
136
143
  // Set up consent change listener to flush waiting events and handle consent upgrade
137
144
  this.consentManager.addListener((state) => {
@@ -381,6 +388,9 @@ class GrainAnalytics {
381
388
  * Log formatted error gracefully
382
389
  */
383
390
  logError(formattedError) {
391
+ // Only log errors in debug mode to reduce noise in production
392
+ if (!this.config.debug)
393
+ return;
384
394
  const { code, message, digest, timestamp, context } = formattedError;
385
395
  const errorOutput = {
386
396
  '🚨 Grain Analytics Error': {
@@ -398,10 +408,7 @@ class GrainAnalytics {
398
408
  }
399
409
  };
400
410
  console.error('🚨 Grain Analytics Error:', errorOutput);
401
- // Also log in a more compact format for debugging
402
- if (this.config.debug) {
403
- console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
404
- }
411
+ console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
405
412
  }
406
413
  /**
407
414
  * Safely execute a function with error handling
@@ -417,10 +424,46 @@ class GrainAnalytics {
417
424
  }
418
425
  }
419
426
  formatEvent(event) {
427
+ const properties = event.properties || {};
428
+ // Auto-enrich events with session-level attribution properties
429
+ // This ensures UTM parameters and attribution data are available on ALL events, not just page_view
430
+ if (!this.config.disableAutoProperties && typeof window !== 'undefined') {
431
+ const hasConsent = this.consentManager.hasConsent('analytics');
432
+ // Only enrich if not a system event (they handle their own properties)
433
+ const isSystemEvent = event.eventName.startsWith('_grain_');
434
+ if (!isSystemEvent && hasConsent) {
435
+ // Get session UTM parameters
436
+ const sessionUTMs = (0, attribution_1.getSessionUTMParameters)();
437
+ if (sessionUTMs) {
438
+ if (sessionUTMs.utm_source)
439
+ properties.utm_source = sessionUTMs.utm_source;
440
+ if (sessionUTMs.utm_medium)
441
+ properties.utm_medium = sessionUTMs.utm_medium;
442
+ if (sessionUTMs.utm_campaign)
443
+ properties.utm_campaign = sessionUTMs.utm_campaign;
444
+ if (sessionUTMs.utm_term)
445
+ properties.utm_term = sessionUTMs.utm_term;
446
+ if (sessionUTMs.utm_content)
447
+ properties.utm_content = sessionUTMs.utm_content;
448
+ }
449
+ // Get first-touch attribution
450
+ const firstTouch = (0, attribution_1.getFirstTouchAttribution)(this.config.tenantId);
451
+ if (firstTouch) {
452
+ properties.first_touch_source = firstTouch.source;
453
+ properties.first_touch_medium = firstTouch.medium;
454
+ properties.first_touch_campaign = firstTouch.campaign;
455
+ properties.first_touch_referrer_category = firstTouch.referrer_category;
456
+ }
457
+ // Add session ID if not already present
458
+ if (!properties.session_id) {
459
+ properties.session_id = this.getSessionId();
460
+ }
461
+ }
462
+ }
420
463
  return {
421
464
  eventName: event.eventName,
422
465
  userId: event.userId || this.getEffectiveUserIdInternal(),
423
- properties: event.properties || {},
466
+ properties,
424
467
  };
425
468
  }
426
469
  async getAuthHeaders() {
@@ -473,7 +516,6 @@ class GrainAnalytics {
473
516
  try {
474
517
  const headers = await this.getAuthHeaders();
475
518
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
476
- this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);
477
519
  const response = await fetch(url, {
478
520
  method: 'POST',
479
521
  headers,
@@ -546,7 +588,6 @@ class GrainAnalytics {
546
588
  body,
547
589
  keepalive: true,
548
590
  });
549
- this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
550
591
  }
551
592
  catch (error) {
552
593
  // Log error gracefully for beacon failures (page unload scenarios)
@@ -618,7 +659,6 @@ class GrainAnalytics {
618
659
  inactiveInterval: this.config.heartbeatInactiveInterval,
619
660
  debug: this.config.debug,
620
661
  });
621
- this.log('Heartbeat tracking initialized');
622
662
  }
623
663
  catch (error) {
624
664
  this.log('Failed to initialize heartbeat tracking:', error);
@@ -631,7 +671,6 @@ class GrainAnalytics {
631
671
  debug: this.config.debug,
632
672
  tenantId: this.config.tenantId,
633
673
  });
634
- this.log('Auto page view tracking initialized');
635
674
  }
636
675
  catch (error) {
637
676
  this.log('Failed to initialize page view tracking:', error);
@@ -640,12 +679,41 @@ class GrainAnalytics {
640
679
  // Initialize auto-tracking when config is available
641
680
  this.initializeAutoTracking();
642
681
  }
682
+ /**
683
+ * Initialize heatmap tracking
684
+ */
685
+ initializeHeatmapTracking() {
686
+ if (typeof window === 'undefined')
687
+ return;
688
+ try {
689
+ this.log('Initializing heatmap tracking');
690
+ Promise.resolve().then(() => __importStar(require('./heatmap-tracking'))).then(({ HeatmapTrackingManager }) => {
691
+ try {
692
+ this.heatmapTrackingManager = new HeatmapTrackingManager(this, {
693
+ scrollDebounceDelay: 100,
694
+ batchDelay: 2000,
695
+ maxBatchSize: 20,
696
+ debug: this.config.debug,
697
+ });
698
+ this.log('Heatmap tracking initialized');
699
+ }
700
+ catch (error) {
701
+ this.log('Failed to initialize heatmap tracking:', error);
702
+ }
703
+ }).catch((error) => {
704
+ this.log('Failed to load heatmap tracking module:', error);
705
+ });
706
+ }
707
+ catch (error) {
708
+ this.log('Failed to initialize heatmap tracking:', error);
709
+ }
710
+ }
643
711
  /**
644
712
  * Initialize auto-tracking (interactions and sections)
645
713
  */
646
714
  async initializeAutoTracking() {
647
715
  try {
648
- this.log('Initializing auto-tracking...');
716
+ this.log('Initializing auto-tracking');
649
717
  // Fetch remote config to get auto-tracking configuration
650
718
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
651
719
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
@@ -658,25 +726,20 @@ class GrainAnalytics {
658
726
  };
659
727
  const headers = await this.getAuthHeaders();
660
728
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
661
- this.log('Fetching auto-tracking config from:', url);
662
729
  const response = await fetch(url, {
663
730
  method: 'POST',
664
731
  headers,
665
732
  body: JSON.stringify(request),
666
733
  });
667
734
  if (!response.ok) {
668
- this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
735
+ this.log('Failed to fetch auto-tracking config:', response.status);
669
736
  return;
670
737
  }
671
738
  const configResponse = await response.json();
672
- this.log('Received config response:', configResponse);
673
739
  if (configResponse.autoTrackingConfig) {
674
- this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
740
+ this.log('Auto-tracking config loaded');
675
741
  this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
676
742
  }
677
- else {
678
- this.log('No auto-tracking config in response');
679
- }
680
743
  }
681
744
  catch (error) {
682
745
  this.log('Failed to initialize auto-tracking:', error);
@@ -687,10 +750,10 @@ class GrainAnalytics {
687
750
  * Setup auto-tracking managers
688
751
  */
689
752
  setupAutoTrackingManagers(config) {
690
- this.log('Setting up auto-tracking managers...', config);
753
+ this.log('Setting up auto-tracking managers');
691
754
  // Lazy load the managers to avoid bundling them if not needed
692
755
  if (config.interactions && config.interactions.length > 0) {
693
- this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
756
+ this.log('Loading interaction tracking:', config.interactions.length, 'interactions');
694
757
  Promise.resolve().then(() => __importStar(require('./interaction-tracking'))).then(({ InteractionTrackingManager }) => {
695
758
  try {
696
759
  this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
@@ -698,20 +761,17 @@ class GrainAnalytics {
698
761
  enableMutationObserver: true,
699
762
  mutationDebounceDelay: 500,
700
763
  });
701
- this.log('Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
764
+ this.log('Interaction tracking initialized');
702
765
  }
703
766
  catch (error) {
704
- this.log('Failed to initialize interaction tracking:', error);
767
+ this.log('Failed to initialize interaction tracking:', error);
705
768
  }
706
769
  }).catch((error) => {
707
- this.log('Failed to load interaction tracking module:', error);
770
+ this.log('Failed to load interaction tracking module:', error);
708
771
  });
709
772
  }
710
- else {
711
- this.log('No interactions configured for auto-tracking');
712
- }
713
773
  if (config.sections && config.sections.length > 0) {
714
- this.log('Loading section tracking module for', config.sections.length, 'sections');
774
+ this.log('Loading section tracking:', config.sections.length, 'sections');
715
775
  Promise.resolve().then(() => __importStar(require('./section-tracking'))).then(({ SectionTrackingManager }) => {
716
776
  try {
717
777
  this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
@@ -722,18 +782,15 @@ class GrainAnalytics {
722
782
  batchDelay: 2000,
723
783
  debug: this.config.debug,
724
784
  });
725
- this.log('Section tracking initialized successfully with', config.sections.length, 'sections');
785
+ this.log('Section tracking initialized');
726
786
  }
727
787
  catch (error) {
728
- this.log('Failed to initialize section tracking:', error);
788
+ this.log('Failed to initialize section tracking:', error);
729
789
  }
730
790
  }).catch((error) => {
731
- this.log('Failed to load section tracking module:', error);
791
+ this.log('Failed to load section tracking module:', error);
732
792
  });
733
793
  }
734
- else {
735
- this.log('No sections configured for auto-tracking');
736
- }
737
794
  }
738
795
  /**
739
796
  * Track session start event
@@ -791,7 +848,7 @@ class GrainAnalytics {
791
848
  properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
792
849
  }
793
850
  this.trackSystemEvent('_grain_session_start', properties);
794
- this.log('Session started:', properties);
851
+ this.log('Session started');
795
852
  }
796
853
  /**
797
854
  * Track session end event
@@ -814,7 +871,7 @@ class GrainAnalytics {
814
871
  properties.page_count = pageCount; // Keep for backward compatibility
815
872
  }
816
873
  this.trackSystemEvent('_grain_session_end', properties);
817
- this.log('Session ended:', properties);
874
+ this.log('Session ended');
818
875
  }
819
876
  /**
820
877
  * Detect browser name
@@ -918,7 +975,7 @@ class GrainAnalytics {
918
975
  // Bypass consent check for necessary system events
919
976
  this.eventQueue.push(event);
920
977
  this.eventCountSinceLastHeartbeat++;
921
- this.log(`Queued system event: ${eventName}`, properties);
978
+ this.log(`Queued system event: ${eventName}`);
922
979
  // Consider flushing
923
980
  if (this.eventQueue.length >= this.config.batchSize) {
924
981
  this.flush().catch((error) => {
@@ -1013,7 +1070,7 @@ class GrainAnalytics {
1013
1070
  this.eventQueue.push(formattedEvent);
1014
1071
  this.eventCountSinceLastHeartbeat++;
1015
1072
  this.sessionEventCount++;
1016
- this.log(`Queued event: ${event.eventName}`, event.properties);
1073
+ this.log(`Queued event: ${event.eventName}`);
1017
1074
  // Check if we should flush immediately
1018
1075
  if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
1019
1076
  await this.flush();
@@ -1239,7 +1296,6 @@ class GrainAnalytics {
1239
1296
  try {
1240
1297
  const headers = await this.getAuthHeaders();
1241
1298
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;
1242
- this.log(`Setting properties for user ${payload.userId} (attempt ${attempt + 1})`);
1243
1299
  const response = await fetch(url, {
1244
1300
  method: 'POST',
1245
1301
  headers,
@@ -1484,7 +1540,6 @@ class GrainAnalytics {
1484
1540
  try {
1485
1541
  const headers = await this.getAuthHeaders();
1486
1542
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
1487
- this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
1488
1543
  const response = await fetch(url, {
1489
1544
  method: 'POST',
1490
1545
  headers,
@@ -1513,7 +1568,7 @@ class GrainAnalytics {
1513
1568
  if (configResponse.configurations) {
1514
1569
  this.updateConfigCache(configResponse, userId);
1515
1570
  }
1516
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
1571
+ this.log('Successfully fetched configurations');
1517
1572
  return configResponse;
1518
1573
  }
1519
1574
  catch (error) {
@@ -1789,6 +1844,10 @@ class GrainAnalytics {
1789
1844
  this.sectionTrackingManager.destroy();
1790
1845
  this.sectionTrackingManager = null;
1791
1846
  }
1847
+ if (this.heatmapTrackingManager) {
1848
+ this.heatmapTrackingManager.destroy();
1849
+ this.heatmapTrackingManager = null;
1850
+ }
1792
1851
  // Send any remaining events (in chunks if necessary)
1793
1852
  if (this.eventQueue.length > 0) {
1794
1853
  const eventsToSend = [...this.eventQueue];
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';
@@ -34,6 +34,7 @@ export class GrainAnalytics {
34
34
  // Auto-tracking properties
35
35
  this.interactionTrackingManager = null;
36
36
  this.sectionTrackingManager = null;
37
+ this.heatmapTrackingManager = null;
37
38
  // Session tracking
38
39
  this.sessionStartTime = Date.now();
39
40
  this.sessionEventCount = 0;
@@ -63,6 +64,8 @@ export class GrainAnalytics {
63
64
  heartbeatInactiveInterval: 300000, // 5 minutes
64
65
  enableAutoPageView: true,
65
66
  stripQueryParams: true,
67
+ // Heatmap Tracking defaults
68
+ enableHeatmapTracking: true,
66
69
  ...config,
67
70
  tenantId: config.tenantId,
68
71
  };
@@ -91,6 +94,10 @@ export class GrainAnalytics {
91
94
  this.initializeAutomaticTracking();
92
95
  // Track session start
93
96
  this.trackSessionStart();
97
+ // Initialize heatmap tracking if enabled
98
+ if (this.config.enableHeatmapTracking) {
99
+ this.initializeHeatmapTracking();
100
+ }
94
101
  }
95
102
  // Set up consent change listener to flush waiting events and handle consent upgrade
96
103
  this.consentManager.addListener((state) => {
@@ -340,6 +347,9 @@ export class GrainAnalytics {
340
347
  * Log formatted error gracefully
341
348
  */
342
349
  logError(formattedError) {
350
+ // Only log errors in debug mode to reduce noise in production
351
+ if (!this.config.debug)
352
+ return;
343
353
  const { code, message, digest, timestamp, context } = formattedError;
344
354
  const errorOutput = {
345
355
  '🚨 Grain Analytics Error': {
@@ -357,10 +367,7 @@ export class GrainAnalytics {
357
367
  }
358
368
  };
359
369
  console.error('🚨 Grain Analytics Error:', errorOutput);
360
- // Also log in a more compact format for debugging
361
- if (this.config.debug) {
362
- console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
363
- }
370
+ console.error(`[Grain Analytics] ${code}: ${message} (${context}) - Events: ${digest.eventCount}, Props: ${digest.totalProperties}, Size: ${digest.totalSize}B`);
364
371
  }
365
372
  /**
366
373
  * Safely execute a function with error handling
@@ -376,10 +383,46 @@ export class GrainAnalytics {
376
383
  }
377
384
  }
378
385
  formatEvent(event) {
386
+ const properties = event.properties || {};
387
+ // Auto-enrich events with session-level attribution properties
388
+ // This ensures UTM parameters and attribution data are available on ALL events, not just page_view
389
+ if (!this.config.disableAutoProperties && typeof window !== 'undefined') {
390
+ const hasConsent = this.consentManager.hasConsent('analytics');
391
+ // Only enrich if not a system event (they handle their own properties)
392
+ const isSystemEvent = event.eventName.startsWith('_grain_');
393
+ if (!isSystemEvent && hasConsent) {
394
+ // Get session UTM parameters
395
+ const sessionUTMs = getSessionUTMParameters();
396
+ if (sessionUTMs) {
397
+ if (sessionUTMs.utm_source)
398
+ properties.utm_source = sessionUTMs.utm_source;
399
+ if (sessionUTMs.utm_medium)
400
+ properties.utm_medium = sessionUTMs.utm_medium;
401
+ if (sessionUTMs.utm_campaign)
402
+ properties.utm_campaign = sessionUTMs.utm_campaign;
403
+ if (sessionUTMs.utm_term)
404
+ properties.utm_term = sessionUTMs.utm_term;
405
+ if (sessionUTMs.utm_content)
406
+ properties.utm_content = sessionUTMs.utm_content;
407
+ }
408
+ // Get first-touch attribution
409
+ const firstTouch = getFirstTouchAttribution(this.config.tenantId);
410
+ if (firstTouch) {
411
+ properties.first_touch_source = firstTouch.source;
412
+ properties.first_touch_medium = firstTouch.medium;
413
+ properties.first_touch_campaign = firstTouch.campaign;
414
+ properties.first_touch_referrer_category = firstTouch.referrer_category;
415
+ }
416
+ // Add session ID if not already present
417
+ if (!properties.session_id) {
418
+ properties.session_id = this.getSessionId();
419
+ }
420
+ }
421
+ }
379
422
  return {
380
423
  eventName: event.eventName,
381
424
  userId: event.userId || this.getEffectiveUserIdInternal(),
382
- properties: event.properties || {},
425
+ properties,
383
426
  };
384
427
  }
385
428
  async getAuthHeaders() {
@@ -432,7 +475,6 @@ export class GrainAnalytics {
432
475
  try {
433
476
  const headers = await this.getAuthHeaders();
434
477
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
435
- this.log(`Sending ${events.length} events to ${url} (attempt ${attempt + 1})`);
436
478
  const response = await fetch(url, {
437
479
  method: 'POST',
438
480
  headers,
@@ -505,7 +547,6 @@ export class GrainAnalytics {
505
547
  body,
506
548
  keepalive: true,
507
549
  });
508
- this.log(`Successfully sent ${events.length} events via fetch (keepalive)`);
509
550
  }
510
551
  catch (error) {
511
552
  // Log error gracefully for beacon failures (page unload scenarios)
@@ -577,7 +618,6 @@ export class GrainAnalytics {
577
618
  inactiveInterval: this.config.heartbeatInactiveInterval,
578
619
  debug: this.config.debug,
579
620
  });
580
- this.log('Heartbeat tracking initialized');
581
621
  }
582
622
  catch (error) {
583
623
  this.log('Failed to initialize heartbeat tracking:', error);
@@ -590,7 +630,6 @@ export class GrainAnalytics {
590
630
  debug: this.config.debug,
591
631
  tenantId: this.config.tenantId,
592
632
  });
593
- this.log('Auto page view tracking initialized');
594
633
  }
595
634
  catch (error) {
596
635
  this.log('Failed to initialize page view tracking:', error);
@@ -599,12 +638,41 @@ export class GrainAnalytics {
599
638
  // Initialize auto-tracking when config is available
600
639
  this.initializeAutoTracking();
601
640
  }
641
+ /**
642
+ * Initialize heatmap tracking
643
+ */
644
+ initializeHeatmapTracking() {
645
+ if (typeof window === 'undefined')
646
+ return;
647
+ try {
648
+ this.log('Initializing heatmap tracking');
649
+ import('./heatmap-tracking').then(({ HeatmapTrackingManager }) => {
650
+ try {
651
+ this.heatmapTrackingManager = new HeatmapTrackingManager(this, {
652
+ scrollDebounceDelay: 100,
653
+ batchDelay: 2000,
654
+ maxBatchSize: 20,
655
+ debug: this.config.debug,
656
+ });
657
+ this.log('Heatmap tracking initialized');
658
+ }
659
+ catch (error) {
660
+ this.log('Failed to initialize heatmap tracking:', error);
661
+ }
662
+ }).catch((error) => {
663
+ this.log('Failed to load heatmap tracking module:', error);
664
+ });
665
+ }
666
+ catch (error) {
667
+ this.log('Failed to initialize heatmap tracking:', error);
668
+ }
669
+ }
602
670
  /**
603
671
  * Initialize auto-tracking (interactions and sections)
604
672
  */
605
673
  async initializeAutoTracking() {
606
674
  try {
607
- this.log('Initializing auto-tracking...');
675
+ this.log('Initializing auto-tracking');
608
676
  // Fetch remote config to get auto-tracking configuration
609
677
  const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
610
678
  const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
@@ -617,25 +685,20 @@ export class GrainAnalytics {
617
685
  };
618
686
  const headers = await this.getAuthHeaders();
619
687
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
620
- this.log('Fetching auto-tracking config from:', url);
621
688
  const response = await fetch(url, {
622
689
  method: 'POST',
623
690
  headers,
624
691
  body: JSON.stringify(request),
625
692
  });
626
693
  if (!response.ok) {
627
- this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
694
+ this.log('Failed to fetch auto-tracking config:', response.status);
628
695
  return;
629
696
  }
630
697
  const configResponse = await response.json();
631
- this.log('Received config response:', configResponse);
632
698
  if (configResponse.autoTrackingConfig) {
633
- this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
699
+ this.log('Auto-tracking config loaded');
634
700
  this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
635
701
  }
636
- else {
637
- this.log('No auto-tracking config in response');
638
- }
639
702
  }
640
703
  catch (error) {
641
704
  this.log('Failed to initialize auto-tracking:', error);
@@ -646,10 +709,10 @@ export class GrainAnalytics {
646
709
  * Setup auto-tracking managers
647
710
  */
648
711
  setupAutoTrackingManagers(config) {
649
- this.log('Setting up auto-tracking managers...', config);
712
+ this.log('Setting up auto-tracking managers');
650
713
  // Lazy load the managers to avoid bundling them if not needed
651
714
  if (config.interactions && config.interactions.length > 0) {
652
- this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
715
+ this.log('Loading interaction tracking:', config.interactions.length, 'interactions');
653
716
  import('./interaction-tracking').then(({ InteractionTrackingManager }) => {
654
717
  try {
655
718
  this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
@@ -657,20 +720,17 @@ export class GrainAnalytics {
657
720
  enableMutationObserver: true,
658
721
  mutationDebounceDelay: 500,
659
722
  });
660
- this.log('Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
723
+ this.log('Interaction tracking initialized');
661
724
  }
662
725
  catch (error) {
663
- this.log('Failed to initialize interaction tracking:', error);
726
+ this.log('Failed to initialize interaction tracking:', error);
664
727
  }
665
728
  }).catch((error) => {
666
- this.log('Failed to load interaction tracking module:', error);
729
+ this.log('Failed to load interaction tracking module:', error);
667
730
  });
668
731
  }
669
- else {
670
- this.log('No interactions configured for auto-tracking');
671
- }
672
732
  if (config.sections && config.sections.length > 0) {
673
- this.log('Loading section tracking module for', config.sections.length, 'sections');
733
+ this.log('Loading section tracking:', config.sections.length, 'sections');
674
734
  import('./section-tracking').then(({ SectionTrackingManager }) => {
675
735
  try {
676
736
  this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
@@ -681,18 +741,15 @@ export class GrainAnalytics {
681
741
  batchDelay: 2000,
682
742
  debug: this.config.debug,
683
743
  });
684
- this.log('Section tracking initialized successfully with', config.sections.length, 'sections');
744
+ this.log('Section tracking initialized');
685
745
  }
686
746
  catch (error) {
687
- this.log('Failed to initialize section tracking:', error);
747
+ this.log('Failed to initialize section tracking:', error);
688
748
  }
689
749
  }).catch((error) => {
690
- this.log('Failed to load section tracking module:', error);
750
+ this.log('Failed to load section tracking module:', error);
691
751
  });
692
752
  }
693
- else {
694
- this.log('No sections configured for auto-tracking');
695
- }
696
753
  }
697
754
  /**
698
755
  * Track session start event
@@ -750,7 +807,7 @@ export class GrainAnalytics {
750
807
  properties.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
751
808
  }
752
809
  this.trackSystemEvent('_grain_session_start', properties);
753
- this.log('Session started:', properties);
810
+ this.log('Session started');
754
811
  }
755
812
  /**
756
813
  * Track session end event
@@ -773,7 +830,7 @@ export class GrainAnalytics {
773
830
  properties.page_count = pageCount; // Keep for backward compatibility
774
831
  }
775
832
  this.trackSystemEvent('_grain_session_end', properties);
776
- this.log('Session ended:', properties);
833
+ this.log('Session ended');
777
834
  }
778
835
  /**
779
836
  * Detect browser name
@@ -877,7 +934,7 @@ export class GrainAnalytics {
877
934
  // Bypass consent check for necessary system events
878
935
  this.eventQueue.push(event);
879
936
  this.eventCountSinceLastHeartbeat++;
880
- this.log(`Queued system event: ${eventName}`, properties);
937
+ this.log(`Queued system event: ${eventName}`);
881
938
  // Consider flushing
882
939
  if (this.eventQueue.length >= this.config.batchSize) {
883
940
  this.flush().catch((error) => {
@@ -972,7 +1029,7 @@ export class GrainAnalytics {
972
1029
  this.eventQueue.push(formattedEvent);
973
1030
  this.eventCountSinceLastHeartbeat++;
974
1031
  this.sessionEventCount++;
975
- this.log(`Queued event: ${event.eventName}`, event.properties);
1032
+ this.log(`Queued event: ${event.eventName}`);
976
1033
  // Check if we should flush immediately
977
1034
  if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
978
1035
  await this.flush();
@@ -1198,7 +1255,6 @@ export class GrainAnalytics {
1198
1255
  try {
1199
1256
  const headers = await this.getAuthHeaders();
1200
1257
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;
1201
- this.log(`Setting properties for user ${payload.userId} (attempt ${attempt + 1})`);
1202
1258
  const response = await fetch(url, {
1203
1259
  method: 'POST',
1204
1260
  headers,
@@ -1443,7 +1499,6 @@ export class GrainAnalytics {
1443
1499
  try {
1444
1500
  const headers = await this.getAuthHeaders();
1445
1501
  const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
1446
- this.log(`Fetching configurations for user ${userId} (attempt ${attempt + 1})`);
1447
1502
  const response = await fetch(url, {
1448
1503
  method: 'POST',
1449
1504
  headers,
@@ -1472,7 +1527,7 @@ export class GrainAnalytics {
1472
1527
  if (configResponse.configurations) {
1473
1528
  this.updateConfigCache(configResponse, userId);
1474
1529
  }
1475
- this.log(`Successfully fetched configurations for user ${userId}:`, configResponse);
1530
+ this.log('Successfully fetched configurations');
1476
1531
  return configResponse;
1477
1532
  }
1478
1533
  catch (error) {
@@ -1748,6 +1803,10 @@ export class GrainAnalytics {
1748
1803
  this.sectionTrackingManager.destroy();
1749
1804
  this.sectionTrackingManager = null;
1750
1805
  }
1806
+ if (this.heatmapTrackingManager) {
1807
+ this.heatmapTrackingManager.destroy();
1808
+ this.heatmapTrackingManager = null;
1809
+ }
1751
1810
  // Send any remaining events (in chunks if necessary)
1752
1811
  if (this.eventQueue.length > 0) {
1753
1812
  const eventsToSend = [...this.eventQueue];
@@ -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,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"}
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;IAiCjC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAiC9B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAyC1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0B7B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAWvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAoB3D;;OAEG;IACH,OAAO,IAAI,IAAI;CA4BhB"}