@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.
- package/README.md +3 -1
- package/dist/activity.js +1 -1
- package/dist/cjs/activity.js +1 -1
- package/dist/cjs/activity.js.map +1 -1
- package/dist/cjs/consent.js +4 -4
- package/dist/cjs/consent.js.map +1 -1
- package/dist/cjs/heartbeat.d.ts.map +1 -1
- package/dist/cjs/heartbeat.js +0 -6
- package/dist/cjs/heartbeat.js.map +1 -1
- package/dist/cjs/heatmap-tracking.d.ts +90 -0
- package/dist/cjs/heatmap-tracking.d.ts.map +1 -0
- package/dist/cjs/heatmap-tracking.js +465 -0
- package/dist/cjs/heatmap-tracking.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interaction-tracking.d.ts.map +1 -1
- package/dist/cjs/interaction-tracking.js +9 -18
- package/dist/cjs/interaction-tracking.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +0 -9
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/cjs/section-tracking.d.ts.map +1 -1
- package/dist/cjs/section-tracking.js +1 -7
- package/dist/cjs/section-tracking.js.map +1 -1
- package/dist/cjs/types/heatmap-tracking.d.ts +41 -0
- package/dist/cjs/types/heatmap-tracking.d.ts.map +1 -0
- package/dist/cjs/types/heatmap-tracking.js +6 -0
- package/dist/cjs/types/heatmap-tracking.js.map +1 -0
- package/dist/consent.js +4 -4
- package/dist/esm/activity.js +1 -1
- package/dist/esm/activity.js.map +1 -1
- package/dist/esm/consent.js +4 -4
- package/dist/esm/consent.js.map +1 -1
- package/dist/esm/heartbeat.d.ts.map +1 -1
- package/dist/esm/heartbeat.js +0 -6
- package/dist/esm/heartbeat.js.map +1 -1
- package/dist/esm/heatmap-tracking.d.ts +90 -0
- package/dist/esm/heatmap-tracking.d.ts.map +1 -0
- package/dist/esm/heatmap-tracking.js +461 -0
- package/dist/esm/heatmap-tracking.js.map +1 -0
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interaction-tracking.d.ts.map +1 -1
- package/dist/esm/interaction-tracking.js +9 -18
- package/dist/esm/interaction-tracking.js.map +1 -1
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +0 -9
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/esm/section-tracking.d.ts.map +1 -1
- package/dist/esm/section-tracking.js +1 -7
- package/dist/esm/section-tracking.js.map +1 -1
- package/dist/esm/types/heatmap-tracking.d.ts +41 -0
- package/dist/esm/types/heatmap-tracking.d.ts.map +1 -0
- package/dist/esm/types/heatmap-tracking.js +5 -0
- package/dist/esm/types/heatmap-tracking.js.map +1 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +0 -6
- package/dist/heatmap-tracking.d.ts +90 -0
- package/dist/heatmap-tracking.d.ts.map +1 -0
- package/dist/heatmap-tracking.js +465 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +534 -80
- package/dist/index.global.dev.js.map +4 -4
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +98 -39
- package/dist/index.mjs +99 -40
- package/dist/interaction-tracking.d.ts.map +1 -1
- package/dist/interaction-tracking.js +9 -18
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +0 -9
- package/dist/section-tracking.d.ts.map +1 -1
- package/dist/section-tracking.js +1 -7
- package/dist/types/heatmap-tracking.d.ts +41 -0
- package/dist/types/heatmap-tracking.d.ts.map +1 -0
- 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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
764
|
+
this.log('Interaction tracking initialized');
|
|
702
765
|
}
|
|
703
766
|
catch (error) {
|
|
704
|
-
this.log('
|
|
767
|
+
this.log('Failed to initialize interaction tracking:', error);
|
|
705
768
|
}
|
|
706
769
|
}).catch((error) => {
|
|
707
|
-
this.log('
|
|
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
|
|
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('
|
|
785
|
+
this.log('Section tracking initialized');
|
|
726
786
|
}
|
|
727
787
|
catch (error) {
|
|
728
|
-
this.log('
|
|
788
|
+
this.log('Failed to initialize section tracking:', error);
|
|
729
789
|
}
|
|
730
790
|
}).catch((error) => {
|
|
731
|
-
this.log('
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
723
|
+
this.log('Interaction tracking initialized');
|
|
661
724
|
}
|
|
662
725
|
catch (error) {
|
|
663
|
-
this.log('
|
|
726
|
+
this.log('Failed to initialize interaction tracking:', error);
|
|
664
727
|
}
|
|
665
728
|
}).catch((error) => {
|
|
666
|
-
this.log('
|
|
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
|
|
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('
|
|
744
|
+
this.log('Section tracking initialized');
|
|
685
745
|
}
|
|
686
746
|
catch (error) {
|
|
687
|
-
this.log('
|
|
747
|
+
this.log('Failed to initialize section tracking:', error);
|
|
688
748
|
}
|
|
689
749
|
}).catch((error) => {
|
|
690
|
-
this.log('
|
|
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
|
|
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
|
|
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}
|
|
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}
|
|
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(
|
|
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;
|
|
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"}
|