@grainql/analytics-web 2.4.0 → 2.5.3

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 (47) hide show
  1. package/dist/cjs/index.d.ts +29 -1
  2. package/dist/cjs/index.d.ts.map +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/interaction-tracking.d.ts +74 -0
  5. package/dist/cjs/interaction-tracking.d.ts.map +1 -0
  6. package/dist/cjs/interaction-tracking.js +292 -0
  7. package/dist/cjs/interaction-tracking.js.map +1 -0
  8. package/dist/cjs/section-tracking.d.ts +101 -0
  9. package/dist/cjs/section-tracking.d.ts.map +1 -0
  10. package/dist/cjs/section-tracking.js +455 -0
  11. package/dist/cjs/section-tracking.js.map +1 -0
  12. package/dist/cjs/types/auto-tracking.d.ts +55 -0
  13. package/dist/cjs/types/auto-tracking.d.ts.map +1 -0
  14. package/dist/cjs/types/auto-tracking.js +6 -0
  15. package/dist/cjs/types/auto-tracking.js.map +1 -0
  16. package/dist/esm/index.d.ts +29 -1
  17. package/dist/esm/index.d.ts.map +1 -1
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/interaction-tracking.d.ts +74 -0
  20. package/dist/esm/interaction-tracking.d.ts.map +1 -0
  21. package/dist/esm/interaction-tracking.js +288 -0
  22. package/dist/esm/interaction-tracking.js.map +1 -0
  23. package/dist/esm/section-tracking.d.ts +101 -0
  24. package/dist/esm/section-tracking.d.ts.map +1 -0
  25. package/dist/esm/section-tracking.js +451 -0
  26. package/dist/esm/section-tracking.js.map +1 -0
  27. package/dist/esm/types/auto-tracking.d.ts +55 -0
  28. package/dist/esm/types/auto-tracking.d.ts.map +1 -0
  29. package/dist/esm/types/auto-tracking.js +5 -0
  30. package/dist/esm/types/auto-tracking.js.map +1 -0
  31. package/dist/index.d.ts +29 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.global.dev.js +821 -3
  34. package/dist/index.global.dev.js.map +4 -4
  35. package/dist/index.global.js +2 -2
  36. package/dist/index.global.js.map +4 -4
  37. package/dist/index.js +149 -4
  38. package/dist/index.mjs +116 -4
  39. package/dist/interaction-tracking.d.ts +74 -0
  40. package/dist/interaction-tracking.d.ts.map +1 -0
  41. package/dist/interaction-tracking.js +292 -0
  42. package/dist/section-tracking.d.ts +101 -0
  43. package/dist/section-tracking.d.ts.map +1 -0
  44. package/dist/section-tracking.js +455 -0
  45. package/dist/types/auto-tracking.d.ts +55 -0
  46. package/dist/types/auto-tracking.d.ts.map +1 -0
  47. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,6 +3,39 @@
3
3
  * Grain Analytics Web SDK
4
4
  * A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API
5
5
  */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
6
39
  Object.defineProperty(exports, "__esModule", { value: true });
7
40
  exports.GrainAnalytics = exports.getState = exports.getCountryCodeFromTimezone = exports.getCountry = exports.parseUTMParameters = exports.categorizeReferrer = void 0;
8
41
  exports.createGrainAnalytics = createGrainAnalytics;
@@ -39,6 +72,9 @@ class GrainAnalytics {
39
72
  this.pageTrackingManager = null;
40
73
  this.ephemeralSessionId = null;
41
74
  this.eventCountSinceLastHeartbeat = 0;
75
+ // Auto-tracking properties
76
+ this.interactionTrackingManager = null;
77
+ this.sectionTrackingManager = null;
42
78
  // Session tracking
43
79
  this.sessionStartTime = Date.now();
44
80
  this.sessionEventCount = 0;
@@ -490,9 +526,12 @@ class GrainAnalytics {
490
526
  try {
491
527
  const headers = await this.getAuthHeaders();
492
528
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
493
- const body = JSON.stringify({ events });
494
- // Try beacon API first (more reliable for page unload)
495
- if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
529
+ // Send events array directly (not wrapped in object) to match API expectation
530
+ const body = JSON.stringify(events);
531
+ // Beacon API doesn't support custom headers, so only use it for unauthenticated requests
532
+ const needsAuth = this.config.authStrategy !== 'NONE';
533
+ // Try beacon API first (more reliable for page unload, but only if no auth needed)
534
+ if (!needsAuth && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
496
535
  const blob = new Blob([body], { type: 'application/json' });
497
536
  const success = navigator.sendBeacon(url, blob);
498
537
  if (success) {
@@ -500,7 +539,7 @@ class GrainAnalytics {
500
539
  return;
501
540
  }
502
541
  }
503
- // Fallback to fetch with keepalive
542
+ // Use fetch with keepalive (supports headers and works during page unload)
504
543
  await fetch(url, {
505
544
  method: 'POST',
506
545
  headers,
@@ -598,6 +637,103 @@ class GrainAnalytics {
598
637
  this.log('Failed to initialize page view tracking:', error);
599
638
  }
600
639
  }
640
+ // Initialize auto-tracking when config is available
641
+ this.initializeAutoTracking();
642
+ }
643
+ /**
644
+ * Initialize auto-tracking (interactions and sections)
645
+ */
646
+ async initializeAutoTracking() {
647
+ try {
648
+ this.log('Initializing auto-tracking...');
649
+ // Fetch remote config to get auto-tracking configuration
650
+ const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
651
+ const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
652
+ // Fetch config with currentUrl
653
+ const request = {
654
+ userId,
655
+ immediateKeys: [],
656
+ properties: {},
657
+ currentUrl, // Add current URL to request
658
+ };
659
+ const headers = await this.getAuthHeaders();
660
+ const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
661
+ this.log('Fetching auto-tracking config from:', url);
662
+ const response = await fetch(url, {
663
+ method: 'POST',
664
+ headers,
665
+ body: JSON.stringify(request),
666
+ });
667
+ if (!response.ok) {
668
+ this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
669
+ return;
670
+ }
671
+ const configResponse = await response.json();
672
+ this.log('Received config response:', configResponse);
673
+ if (configResponse.autoTrackingConfig) {
674
+ this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
675
+ this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
676
+ }
677
+ else {
678
+ this.log('No auto-tracking config in response');
679
+ }
680
+ }
681
+ catch (error) {
682
+ this.log('Failed to initialize auto-tracking:', error);
683
+ // Fail silently - auto-tracking is optional
684
+ }
685
+ }
686
+ /**
687
+ * Setup auto-tracking managers
688
+ */
689
+ setupAutoTrackingManagers(config) {
690
+ this.log('Setting up auto-tracking managers...', config);
691
+ // Lazy load the managers to avoid bundling them if not needed
692
+ if (config.interactions && config.interactions.length > 0) {
693
+ this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
694
+ Promise.resolve().then(() => __importStar(require('./interaction-tracking'))).then(({ InteractionTrackingManager }) => {
695
+ try {
696
+ this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
697
+ debug: this.config.debug,
698
+ enableMutationObserver: true,
699
+ mutationDebounceDelay: 500,
700
+ });
701
+ this.log('✅ Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
702
+ }
703
+ catch (error) {
704
+ this.log('❌ Failed to initialize interaction tracking:', error);
705
+ }
706
+ }).catch((error) => {
707
+ this.log('❌ Failed to load interaction tracking module:', error);
708
+ });
709
+ }
710
+ else {
711
+ this.log('No interactions configured for auto-tracking');
712
+ }
713
+ if (config.sections && config.sections.length > 0) {
714
+ this.log('Loading section tracking module for', config.sections.length, 'sections');
715
+ Promise.resolve().then(() => __importStar(require('./section-tracking'))).then(({ SectionTrackingManager }) => {
716
+ try {
717
+ this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
718
+ minDwellTime: 1000,
719
+ scrollVelocityThreshold: 500,
720
+ intersectionThreshold: 0.1,
721
+ debounceDelay: 100,
722
+ batchDelay: 2000,
723
+ debug: this.config.debug,
724
+ });
725
+ this.log('✅ Section tracking initialized successfully with', config.sections.length, 'sections');
726
+ }
727
+ catch (error) {
728
+ this.log('❌ Failed to initialize section tracking:', error);
729
+ }
730
+ }).catch((error) => {
731
+ this.log('❌ Failed to load section tracking module:', error);
732
+ });
733
+ }
734
+ else {
735
+ this.log('No sections configured for auto-tracking');
736
+ }
601
737
  }
602
738
  /**
603
739
  * Track session start event
@@ -1644,6 +1780,15 @@ class GrainAnalytics {
1644
1780
  this.activityDetector.destroy();
1645
1781
  this.activityDetector = null;
1646
1782
  }
1783
+ // Destroy auto-tracking managers
1784
+ if (this.interactionTrackingManager) {
1785
+ this.interactionTrackingManager.destroy();
1786
+ this.interactionTrackingManager = null;
1787
+ }
1788
+ if (this.sectionTrackingManager) {
1789
+ this.sectionTrackingManager.destroy();
1790
+ this.sectionTrackingManager = null;
1791
+ }
1647
1792
  // Send any remaining events (in chunks if necessary)
1648
1793
  if (this.eventQueue.length > 0) {
1649
1794
  const eventsToSend = [...this.eventQueue];
package/dist/index.mjs CHANGED
@@ -31,6 +31,9 @@ export class GrainAnalytics {
31
31
  this.pageTrackingManager = null;
32
32
  this.ephemeralSessionId = null;
33
33
  this.eventCountSinceLastHeartbeat = 0;
34
+ // Auto-tracking properties
35
+ this.interactionTrackingManager = null;
36
+ this.sectionTrackingManager = null;
34
37
  // Session tracking
35
38
  this.sessionStartTime = Date.now();
36
39
  this.sessionEventCount = 0;
@@ -482,9 +485,12 @@ export class GrainAnalytics {
482
485
  try {
483
486
  const headers = await this.getAuthHeaders();
484
487
  const url = `${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;
485
- const body = JSON.stringify({ events });
486
- // Try beacon API first (more reliable for page unload)
487
- if (typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
488
+ // Send events array directly (not wrapped in object) to match API expectation
489
+ const body = JSON.stringify(events);
490
+ // Beacon API doesn't support custom headers, so only use it for unauthenticated requests
491
+ const needsAuth = this.config.authStrategy !== 'NONE';
492
+ // Try beacon API first (more reliable for page unload, but only if no auth needed)
493
+ if (!needsAuth && typeof navigator !== 'undefined' && 'sendBeacon' in navigator) {
488
494
  const blob = new Blob([body], { type: 'application/json' });
489
495
  const success = navigator.sendBeacon(url, blob);
490
496
  if (success) {
@@ -492,7 +498,7 @@ export class GrainAnalytics {
492
498
  return;
493
499
  }
494
500
  }
495
- // Fallback to fetch with keepalive
501
+ // Use fetch with keepalive (supports headers and works during page unload)
496
502
  await fetch(url, {
497
503
  method: 'POST',
498
504
  headers,
@@ -590,6 +596,103 @@ export class GrainAnalytics {
590
596
  this.log('Failed to initialize page view tracking:', error);
591
597
  }
592
598
  }
599
+ // Initialize auto-tracking when config is available
600
+ this.initializeAutoTracking();
601
+ }
602
+ /**
603
+ * Initialize auto-tracking (interactions and sections)
604
+ */
605
+ async initializeAutoTracking() {
606
+ try {
607
+ this.log('Initializing auto-tracking...');
608
+ // Fetch remote config to get auto-tracking configuration
609
+ const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
610
+ const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
611
+ // Fetch config with currentUrl
612
+ const request = {
613
+ userId,
614
+ immediateKeys: [],
615
+ properties: {},
616
+ currentUrl, // Add current URL to request
617
+ };
618
+ const headers = await this.getAuthHeaders();
619
+ const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
620
+ this.log('Fetching auto-tracking config from:', url);
621
+ const response = await fetch(url, {
622
+ method: 'POST',
623
+ headers,
624
+ body: JSON.stringify(request),
625
+ });
626
+ if (!response.ok) {
627
+ this.log('Failed to fetch auto-tracking config:', response.status, response.statusText);
628
+ return;
629
+ }
630
+ const configResponse = await response.json();
631
+ this.log('Received config response:', configResponse);
632
+ if (configResponse.autoTrackingConfig) {
633
+ this.log('Auto-tracking config found:', configResponse.autoTrackingConfig);
634
+ this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
635
+ }
636
+ else {
637
+ this.log('No auto-tracking config in response');
638
+ }
639
+ }
640
+ catch (error) {
641
+ this.log('Failed to initialize auto-tracking:', error);
642
+ // Fail silently - auto-tracking is optional
643
+ }
644
+ }
645
+ /**
646
+ * Setup auto-tracking managers
647
+ */
648
+ setupAutoTrackingManagers(config) {
649
+ this.log('Setting up auto-tracking managers...', config);
650
+ // Lazy load the managers to avoid bundling them if not needed
651
+ if (config.interactions && config.interactions.length > 0) {
652
+ this.log('Loading interaction tracking module for', config.interactions.length, 'interactions');
653
+ import('./interaction-tracking').then(({ InteractionTrackingManager }) => {
654
+ try {
655
+ this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
656
+ debug: this.config.debug,
657
+ enableMutationObserver: true,
658
+ mutationDebounceDelay: 500,
659
+ });
660
+ this.log('✅ Interaction tracking initialized successfully with', config.interactions.length, 'interactions');
661
+ }
662
+ catch (error) {
663
+ this.log('❌ Failed to initialize interaction tracking:', error);
664
+ }
665
+ }).catch((error) => {
666
+ this.log('❌ Failed to load interaction tracking module:', error);
667
+ });
668
+ }
669
+ else {
670
+ this.log('No interactions configured for auto-tracking');
671
+ }
672
+ if (config.sections && config.sections.length > 0) {
673
+ this.log('Loading section tracking module for', config.sections.length, 'sections');
674
+ import('./section-tracking').then(({ SectionTrackingManager }) => {
675
+ try {
676
+ this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
677
+ minDwellTime: 1000,
678
+ scrollVelocityThreshold: 500,
679
+ intersectionThreshold: 0.1,
680
+ debounceDelay: 100,
681
+ batchDelay: 2000,
682
+ debug: this.config.debug,
683
+ });
684
+ this.log('✅ Section tracking initialized successfully with', config.sections.length, 'sections');
685
+ }
686
+ catch (error) {
687
+ this.log('❌ Failed to initialize section tracking:', error);
688
+ }
689
+ }).catch((error) => {
690
+ this.log('❌ Failed to load section tracking module:', error);
691
+ });
692
+ }
693
+ else {
694
+ this.log('No sections configured for auto-tracking');
695
+ }
593
696
  }
594
697
  /**
595
698
  * Track session start event
@@ -1636,6 +1739,15 @@ export class GrainAnalytics {
1636
1739
  this.activityDetector.destroy();
1637
1740
  this.activityDetector = null;
1638
1741
  }
1742
+ // Destroy auto-tracking managers
1743
+ if (this.interactionTrackingManager) {
1744
+ this.interactionTrackingManager.destroy();
1745
+ this.interactionTrackingManager = null;
1746
+ }
1747
+ if (this.sectionTrackingManager) {
1748
+ this.sectionTrackingManager.destroy();
1749
+ this.sectionTrackingManager = null;
1750
+ }
1639
1751
  // Send any remaining events (in chunks if necessary)
1640
1752
  if (this.eventQueue.length > 0) {
1641
1753
  const eventsToSend = [...this.eventQueue];
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Interaction Tracking Manager for Grain Analytics
3
+ * Automatically attaches click and focus listeners to detected interactive elements
4
+ */
5
+ import type { InteractionConfig } from './types/auto-tracking';
6
+ export interface SendEventOptions {
7
+ flush?: boolean;
8
+ }
9
+ export interface InteractionTracker {
10
+ track(eventName: string, properties?: Record<string, unknown>, options?: SendEventOptions): void | Promise<void>;
11
+ hasConsent(category: 'analytics' | 'marketing' | 'functional'): boolean;
12
+ log(...args: unknown[]): void;
13
+ }
14
+ export interface InteractionTrackingConfig {
15
+ debug?: boolean;
16
+ enableMutationObserver?: boolean;
17
+ mutationDebounceDelay?: number;
18
+ }
19
+ export declare class InteractionTrackingManager {
20
+ private tracker;
21
+ private interactions;
22
+ private config;
23
+ private isDestroyed;
24
+ private attachedListeners;
25
+ private xpathCache;
26
+ private mutationObserver;
27
+ private mutationDebounceTimer;
28
+ constructor(tracker: InteractionTracker, interactions: InteractionConfig[], config?: InteractionTrackingConfig);
29
+ /**
30
+ * Attach listeners to all configured interactions
31
+ */
32
+ private attachAllListeners;
33
+ /**
34
+ * Attach listener to a specific interaction
35
+ */
36
+ private attachInteractionListener;
37
+ /**
38
+ * Handle click event on interaction
39
+ */
40
+ private handleInteractionClick;
41
+ /**
42
+ * Handle focus event on interaction (for form fields)
43
+ */
44
+ private handleInteractionFocus;
45
+ /**
46
+ * Find element by XPath selector
47
+ */
48
+ private findElementByXPath;
49
+ /**
50
+ * Setup mutation observer to handle dynamic content
51
+ */
52
+ private setupMutationObserver;
53
+ /**
54
+ * Handle DOM mutations
55
+ */
56
+ private handleMutations;
57
+ /**
58
+ * Detach listeners from an element
59
+ */
60
+ private detachListeners;
61
+ /**
62
+ * Log debug messages
63
+ */
64
+ private log;
65
+ /**
66
+ * Update interactions configuration
67
+ */
68
+ updateInteractions(interactions: InteractionConfig[]): void;
69
+ /**
70
+ * Cleanup and destroy
71
+ */
72
+ destroy(): void;
73
+ }
74
+ //# sourceMappingURL=interaction-tracking.d.ts.map
@@ -0,0 +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"}