saasco-sdk 0.1.30 → 0.1.32

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/index.esm.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { __awaiter } from 'tslib';
2
- import { v4 } from '@lukeed/uuid';
3
2
  import { isValid, parse } from 'psl';
3
+ import { v4 } from '@lukeed/uuid';
4
4
  import { z } from 'zod';
5
5
 
6
- var version = "0.1.30";
6
+ var version = "0.1.32";
7
7
 
8
8
  const timezones = {
9
9
  'Asia/Barnaul': 'RU',
@@ -462,6 +462,68 @@ function getBrowserContext() {
462
462
  const $utmCampaignId = params.get('utm_campaign_id');
463
463
  const $utmCreativeFormat = params.get('utm_creative_format');
464
464
  const $utmMarketingTactic = params.get('utm_marketing_tactic');
465
+ let $utmAdSource = params.get('utm_ad_source');
466
+ let $utmAdId = params.get('utm_ad_id');
467
+ // If the adId is not set directly attempt to get it from each ad network
468
+ // Also set the ad source to the ad network if it's not set
469
+ if (!$utmAdId) {
470
+ const fbAdId = params.get('fbadid');
471
+ if (fbAdId) {
472
+ $utmAdId = fbAdId;
473
+ if (!$utmAdSource) {
474
+ $utmAdSource = 'facebook';
475
+ }
476
+ }
477
+ const googleAdId = params.get('gadid');
478
+ if (googleAdId) {
479
+ $utmAdId = googleAdId;
480
+ if (!$utmAdSource) {
481
+ $utmAdSource = 'google';
482
+ }
483
+ }
484
+ const xAdId = params.get('xadid');
485
+ if (xAdId) {
486
+ $utmAdId = xAdId;
487
+ if (!$utmAdSource) {
488
+ $utmAdSource = 'x';
489
+ }
490
+ }
491
+ const pinterestAdId = params.get('padid');
492
+ if (pinterestAdId) {
493
+ $utmAdId = pinterestAdId;
494
+ if (!$utmAdSource) {
495
+ $utmAdSource = 'pinterest';
496
+ }
497
+ }
498
+ const tiktokAdId = params.get('ttadid');
499
+ if (tiktokAdId) {
500
+ $utmAdId = tiktokAdId;
501
+ if (!$utmAdSource) {
502
+ $utmAdSource = 'tiktok';
503
+ }
504
+ }
505
+ const snapchatAdId = params.get('scadid');
506
+ if (snapchatAdId) {
507
+ $utmAdId = snapchatAdId;
508
+ if (!$utmAdSource) {
509
+ $utmAdSource = 'snapchat';
510
+ }
511
+ }
512
+ const redditAdId = params.get('radid');
513
+ if (redditAdId) {
514
+ $utmAdId = redditAdId;
515
+ if (!$utmAdSource) {
516
+ $utmAdSource = 'reddit';
517
+ }
518
+ }
519
+ const linkedinAdId = params.get('liadid');
520
+ if (linkedinAdId) {
521
+ $utmAdId = linkedinAdId;
522
+ if (!$utmAdSource) {
523
+ $utmAdSource = 'linkedin';
524
+ }
525
+ }
526
+ }
465
527
  return {
466
528
  $locale,
467
529
  $location,
@@ -482,10 +544,391 @@ function getBrowserContext() {
482
544
  $utmSourcePlatform,
483
545
  $utmCampaignId,
484
546
  $utmCreativeFormat,
485
- $utmMarketingTactic
547
+ $utmMarketingTactic,
548
+ $utmAdSource,
549
+ $utmAdId
486
550
  };
487
551
  }
488
552
 
553
+ const isBrowser$1 = typeof window !== 'undefined';
554
+ const isServer$1 = !isBrowser$1;
555
+ /**
556
+ * Create a Facebook Pixel integration instance
557
+ */
558
+ function createFacebookPixelIntegration(config) {
559
+ let isPixelReady = false;
560
+ return {
561
+ name: 'facebook-pixel',
562
+ environments: ['client'],
563
+ init: () => __awaiter(this, void 0, void 0, function* () {
564
+ if (isServer$1) {
565
+ throw new Error('Facebook Pixel cannot be initialized on server');
566
+ }
567
+ // Check if Facebook Pixel is already loaded
568
+ if (window.fbq || window._fbq) {
569
+ console.warn('Facebook Pixel is already initialized');
570
+ isPixelReady = true;
571
+ return;
572
+ }
573
+ // Initialize Facebook Pixel
574
+ return new Promise((resolve, reject) => {
575
+ try {
576
+ (function (f, b, e, v, n, t, s) {
577
+ if (f.fbq) return;
578
+ n = f.fbq = function () {
579
+ n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
580
+ };
581
+ if (!f._fbq) f._fbq = n;
582
+ n.push = n;
583
+ n.loaded = !0;
584
+ n.version = '2.0';
585
+ n.queue = [];
586
+ t = b.createElement(e);
587
+ t.async = !0;
588
+ t.src = v;
589
+ // Add load event listener
590
+ t.onload = () => {
591
+ isPixelReady = true;
592
+ resolve();
593
+ };
594
+ t.onerror = () => {
595
+ reject(new Error('Failed to load Facebook Pixel script'));
596
+ };
597
+ s = b.getElementsByTagName(e)[0];
598
+ s.parentNode.insertBefore(t, s);
599
+ })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
600
+ // Initialize pixel (but don't track PageView automatically)
601
+ window.fbq('init', config.pixelId);
602
+ // If script loads synchronously, mark as ready
603
+ if (window.fbq && typeof window.fbq === 'function') {
604
+ isPixelReady = true;
605
+ resolve();
606
+ }
607
+ } catch (error) {
608
+ reject(error);
609
+ }
610
+ });
611
+ }),
612
+ track: (eventName, properties, context) => {
613
+ if (!isPixelReady || !window.fbq) return;
614
+ const fbEventName = getFacebookEventName(eventName, config.eventMapping);
615
+ // Standard Facebook events that support parameters
616
+ const eventsWithParams = ['Purchase', 'StartTrial', 'Subscribe'];
617
+ if (eventsWithParams.includes(fbEventName) && properties) {
618
+ // Extract standard Facebook parameters
619
+ const fbParams = {};
620
+ if (properties['value'] !== undefined) fbParams['value'] = properties['value'];
621
+ if (properties['currency'] !== undefined) fbParams['currency'] = properties['currency'];
622
+ if (properties['predicted_ltv'] !== undefined) fbParams['predicted_ltv'] = properties['predicted_ltv'];
623
+ window.fbq('track', fbEventName, fbParams);
624
+ } else {
625
+ window.fbq('track', fbEventName);
626
+ }
627
+ },
628
+ identify: (userId, properties, context) => {
629
+ if (!isPixelReady || !window.fbq) return;
630
+ // Facebook Pixel doesn't have explicit identify method
631
+ // We can use advanced matching or track as CompleteRegistration
632
+ if (userId && properties) {
633
+ // Track as registration/identification event
634
+ window.fbq('track', 'CompleteRegistration', properties);
635
+ }
636
+ }
637
+ };
638
+ }
639
+ function getFacebookEventName(eventName, eventMapping) {
640
+ if (!eventMapping) return eventName;
641
+ return eventMapping[eventName] || eventName;
642
+ }
643
+ // Legacy functions for backward compatibility (deprecated)
644
+ function initializeFacebookPixel(pixelId) {
645
+ var _a;
646
+ console.warn('initializeFacebookPixel is deprecated. Use createFacebookPixelIntegration instead.');
647
+ const integration = createFacebookPixelIntegration({
648
+ pixelId
649
+ });
650
+ (_a = integration.init) === null || _a === void 0 ? void 0 : _a.call(integration);
651
+ }
652
+ function trackFacebookEvent(eventName, properties, eventMapping) {
653
+ console.warn('trackFacebookEvent is deprecated. Use IntegrationManager instead.');
654
+ if (isServer$1 || !window.fbq) return;
655
+ const fbEventName = getFacebookEventName(eventName, eventMapping);
656
+ const eventsWithParams = ['Purchase', 'StartTrial', 'Subscribe'];
657
+ if (eventsWithParams.includes(fbEventName) && properties) {
658
+ const fbParams = {};
659
+ if (properties['value'] !== undefined) fbParams['value'] = properties['value'];
660
+ if (properties['currency'] !== undefined) fbParams['currency'] = properties['currency'];
661
+ if (properties['predicted_ltv'] !== undefined) fbParams['predicted_ltv'] = properties['predicted_ltv'];
662
+ window.fbq('track', fbEventName, fbParams);
663
+ } else {
664
+ window.fbq('track', fbEventName);
665
+ }
666
+ }
667
+
668
+ /* eslint-disable @typescript-eslint/no-explicit-any */
669
+ class AnalyticsLogger {
670
+ constructor(config) {
671
+ this.config = config;
672
+ }
673
+ /**
674
+ * Log debug information
675
+ */
676
+ log(...args) {
677
+ if (!this.config.debug) return;
678
+ const icon = '◍';
679
+ const message = `${icon} ${this.config.label}`;
680
+ console.info(`%c ${message}`, `background: #eee; color: #000; padding-right: 6px; border-radius: 2px;`, ...args);
681
+ }
682
+ /**
683
+ * Log warning information
684
+ */
685
+ warn(...args) {
686
+ if (!this.config.debug) return;
687
+ const icon = '◍';
688
+ const message = `${icon} ${this.config.label}`;
689
+ console.warn(`%c ${message}`, `background: #ffa500; color: #000; padding-right: 6px; border-radius: 2px;`, ...args);
690
+ }
691
+ /**
692
+ * Log error information
693
+ */
694
+ error(...args) {
695
+ const icon = '◍';
696
+ const message = `${icon} ${this.config.label} Error`;
697
+ console.error(`%c ${message}`, 'background: red; color: white; padding-right: 6px; border-radius: 2px;', ...args);
698
+ return {
699
+ success: false,
700
+ message: args.join(' ')
701
+ };
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Generate a collision-resistant UUID
707
+ * Uses lukeed's UUID v4 implementation for consistent, fast UUID generation
708
+ */
709
+ function uuid() {
710
+ return v4();
711
+ }
712
+
713
+ /*
714
+ Minimal analytics integration manager (v0)
715
+ */
716
+ class Logger {
717
+ constructor(label = 'Analytics', debug = false) {
718
+ this.label = label;
719
+ this.debug = debug;
720
+ }
721
+ log(...args) {
722
+ if (this.debug) console.log(`[${this.label}]`, ...args);
723
+ }
724
+ warn(...args) {
725
+ console.warn(`[${this.label}]`, ...args);
726
+ }
727
+ error(...args) {
728
+ console.error(`[${this.label}]`, ...args);
729
+ }
730
+ }
731
+ class IntegrationManager {
732
+ constructor(config = {}) {
733
+ var _a, _b, _c, _d;
734
+ this.context = {};
735
+ this.integrations = new Map();
736
+ this.globalQueue = [];
737
+ this.config = {
738
+ debug: (_a = config.debug) !== null && _a !== void 0 ? _a : false,
739
+ maxQueueSize: (_b = config.maxQueueSize) !== null && _b !== void 0 ? _b : 200,
740
+ maxIntegrationWaitTime: (_c = config.maxIntegrationWaitTime) !== null && _c !== void 0 ? _c : 10000,
741
+ flushInterval: (_d = config.flushInterval) !== null && _d !== void 0 ? _d : 5000
742
+ };
743
+ this.logger = new AnalyticsLogger({
744
+ label: 'Saasco Integrations Debug',
745
+ debug: this.config.debug
746
+ });
747
+ this.initTime = Date.now();
748
+ this.currentEnvironment = typeof window !== 'undefined' ? 'client' : 'server';
749
+ this.setupPeriodicFlushing();
750
+ this.setupUnloadHandler();
751
+ }
752
+ /**
753
+ * Shallow-merge context to keep it simple + predictable in v0
754
+ */
755
+ setContext(next) {
756
+ this.context = Object.assign(Object.assign({}, this.context), next);
757
+ // Only log if we are setting something
758
+ if (Object.keys(this.context).length > 0) this.logger.log('context', this.context);
759
+ }
760
+ /**
761
+ * Register and init an integration. When init resolves, we mark it ready and
762
+ * immediately flush any queued events in FIFO order to *all* ready integrations.
763
+ */
764
+ registerIntegration(integration) {
765
+ return __awaiter(this, void 0, void 0, function* () {
766
+ if (this.integrations.has(integration.name)) {
767
+ this.logger.warn(`integration already registered: ${integration.name}`);
768
+ return;
769
+ }
770
+ // Check environment compatibility
771
+ const isCompatible = integration.environments.includes(this.currentEnvironment);
772
+ if (!isCompatible) {
773
+ this.logger.warn(`skipping ${integration.name}: requires ${integration.environments.join(' or ')} but running in ${this.currentEnvironment}`);
774
+ return;
775
+ }
776
+ this.logger.log(`registering ${integration.name} (supports: ${integration.environments.join(', ')}, current: ${this.currentEnvironment})`);
777
+ const state = {
778
+ integration,
779
+ status: 'idle'
780
+ };
781
+ this.integrations.set(integration.name, state);
782
+ if (integration.init) {
783
+ try {
784
+ state.status = 'loading';
785
+ this.logger.log(`init ${integration.name}`);
786
+ yield integration.init();
787
+ state.status = 'ready';
788
+ this.logger.log(`ready ${integration.name}`);
789
+ this.flush();
790
+ } catch (e) {
791
+ state.status = 'error';
792
+ this.logger.error(`failed to init ${integration.name}`, e);
793
+ }
794
+ } else {
795
+ state.status = 'ready';
796
+ this.logger.log(`ready (no init) ${integration.name}`);
797
+ this.flush();
798
+ }
799
+ });
800
+ }
801
+ identify(userId, traits) {
802
+ // update context so subsequent track() carries new userId
803
+ this.setContext({
804
+ userId
805
+ });
806
+ this.send({
807
+ type: 'identify',
808
+ name: undefined,
809
+ properties: traits
810
+ });
811
+ }
812
+ track(name, properties) {
813
+ this.send({
814
+ type: 'track',
815
+ name,
816
+ properties
817
+ });
818
+ }
819
+ /**
820
+ * Core send path: if at least one integration is ready -> deliver immediately
821
+ * Else enqueue (bounded FIFO)
822
+ */
823
+ send(partial) {
824
+ const envelope = {
825
+ id: uuid(),
826
+ type: partial.type,
827
+ timestamp: Date.now(),
828
+ name: partial.name,
829
+ properties: partial.properties,
830
+ context: Object.assign({}, this.context)
831
+ };
832
+ // If we're not ready, enqueue
833
+ if (!this.isReady()) {
834
+ if (this.globalQueue.length >= this.config.maxQueueSize) {
835
+ // drop oldest
836
+ this.globalQueue.shift();
837
+ this.logger.warn('queue full → dropped oldest');
838
+ }
839
+ this.globalQueue.push(envelope);
840
+ this.logger.log(`queued (${this.globalQueue.length})`, envelope);
841
+ return;
842
+ }
843
+ this.deliver(envelope);
844
+ }
845
+ deliver(evt) {
846
+ for (const {
847
+ integration,
848
+ status
849
+ } of this.integrations.values()) {
850
+ if (status !== 'ready') continue;
851
+ try {
852
+ if (evt.type === 'track' && integration.track && evt.name) {
853
+ integration.track(evt.name, evt.properties, evt.context);
854
+ } else if (evt.type === 'identify' && integration.identify) {
855
+ integration.identify(evt.context.distinctId, evt.properties, evt.context);
856
+ }
857
+ } catch (e) {
858
+ this.logger.error(`deliver error in ${integration.name}`, e);
859
+ }
860
+ }
861
+ }
862
+ /**
863
+ * Flush queued events FIFO once at least one integration is ready.
864
+ */
865
+ flush() {
866
+ // nothing to flush
867
+ if (this.globalQueue.length === 0) return;
868
+ // If we're not ready don't flush
869
+ if (!this.isReady()) return;
870
+ this.logger.log(`flushing ${this.globalQueue.length} queued events`);
871
+ const toSend = this.globalQueue;
872
+ this.globalQueue = [];
873
+ // deliver all events
874
+ toSend.forEach(evt => this.deliver(evt));
875
+ }
876
+ readyCount() {
877
+ let n = 0;
878
+ for (const s of this.integrations.values()) if (s.status === 'ready') n++;
879
+ return n;
880
+ }
881
+ isReady() {
882
+ // Return if integrations are not ready, or if we haven't waited the max integration wait time
883
+ if (this.readyCount() < this.integrations.size && Date.now() - this.initTime < this.config.maxIntegrationWaitTime) return false;
884
+ return true;
885
+ }
886
+ /**
887
+ * Setup periodic flushing if enabled
888
+ */
889
+ setupPeriodicFlushing() {
890
+ if (this.config.flushInterval <= 0) return;
891
+ this.flushTimer = setInterval(() => {
892
+ if (this.globalQueue.length > 0) {
893
+ this.logger.log('periodic flush triggered');
894
+ this.flush();
895
+ }
896
+ }, this.config.flushInterval);
897
+ this.logger.log(`periodic flushing enabled: ${this.config.flushInterval}ms`);
898
+ }
899
+ /**
900
+ * Setup page unload handler for client environment
901
+ */
902
+ setupUnloadHandler() {
903
+ if (this.currentEnvironment !== 'client' || typeof window === 'undefined') {
904
+ return;
905
+ }
906
+ this.unloadHandler = () => {
907
+ this.logger.log('page unloading, flushing remaining events');
908
+ this.flush();
909
+ };
910
+ // Use both beforeunload and pagehide for better coverage
911
+ window.addEventListener('beforeunload', this.unloadHandler);
912
+ window.addEventListener('pagehide', this.unloadHandler);
913
+ this.logger.log('page unload handlers registered');
914
+ }
915
+ /** Debug helpers */
916
+ getStats() {
917
+ return {
918
+ currentEnvironment: this.currentEnvironment,
919
+ integrations: [...this.integrations.values()].map(s => ({
920
+ name: s.integration.name,
921
+ status: s.status,
922
+ environments: s.integration.environments
923
+ })),
924
+ queueLength: this.globalQueue.length,
925
+ readyCount: this.readyCount(),
926
+ periodicFlushEnabled: this.config.flushInterval > 0,
927
+ flushInterval: this.config.flushInterval
928
+ };
929
+ }
930
+ }
931
+
489
932
  const isBrowser = typeof window !== 'undefined';
490
933
  const isServer = !isBrowser;
491
934
  const PREF = 'saasco-sdk';
@@ -620,8 +1063,9 @@ function setSessionId({
620
1063
  } = {
621
1064
  reset: false
622
1065
  }) {
623
- const sessionId = reset ? v4() : getSessionId() || v4();
1066
+ const sessionId = reset ? uuid() : getSessionId() || uuid();
624
1067
  storeData(`session-id`, sessionId, SESSION_DURATION);
1068
+ return sessionId;
625
1069
  }
626
1070
  function getAnonymousId() {
627
1071
  return retrieveData(`anonymous-id`);
@@ -631,7 +1075,7 @@ function setAnonymousId({
631
1075
  } = {
632
1076
  reset: false
633
1077
  }) {
634
- const anonymousId = reset ? v4() : getAnonymousId() || v4();
1078
+ const anonymousId = reset ? uuid() : getAnonymousId() || uuid();
635
1079
  storeData(`anonymous-id`, anonymousId, USER_DURATION);
636
1080
  return anonymousId;
637
1081
  }
@@ -649,7 +1093,7 @@ function setSuperContext(superContext) {
649
1093
  }
650
1094
  function updateSuperContext(browserContext) {
651
1095
  const existingSuperContext = getSuperContext();
652
- const defaultSuperContextKeys = ['$utmSource', '$utmMedium', '$utmCampaign', '$utmTerm', '$utmContent', '$utmId', '$utmSourcePlatform', '$utmCampaignId', '$utmCreativeFormat', '$utmMarketingTactic'];
1096
+ const defaultSuperContextKeys = ['$utmSource', '$utmMedium', '$utmCampaign', '$utmTerm', '$utmContent', '$utmId', '$utmSourcePlatform', '$utmCampaignId', '$utmCreativeFormat', '$utmMarketingTactic', '$utmAdSource', '$utmAdId'];
653
1097
  const superContext = defaultSuperContextKeys.reduce((acc, key) => {
654
1098
  const value = Object.assign({}, browserContext)[key];
655
1099
  if (value) acc[key] = value;
@@ -678,13 +1122,19 @@ class Saasco {
678
1122
  * @param config.debug Whether to log debug information. Default is false.
679
1123
  * @param config.trackUrlParams Whether to track URL parameters. Default is true.
680
1124
  * @param config.trackHashChanges Whether to track hash changes. Default is true.
1125
+ * @param config.integrations Configuration for third-party integrations like Facebook Pixel.
681
1126
  */
682
1127
  constructor(config) {
683
1128
  this.config = config;
684
1129
  this.lastPageViewPath = '';
685
1130
  this.isInitialized = false;
1131
+ // Initialize logger
1132
+ this.logger = new AnalyticsLogger({
1133
+ debug: !!this.config.debug,
1134
+ label: 'Saasco Debug'
1135
+ });
686
1136
  if (!config.projectId) {
687
- this.error("Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
1137
+ this.logger.error("Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
688
1138
  return;
689
1139
  }
690
1140
  // default enabled to true
@@ -695,10 +1145,17 @@ class Saasco {
695
1145
  trackQueryParams: true,
696
1146
  trackHash: true
697
1147
  }, this.config.autoPageTracking);
1148
+ // Initialize integration manager
1149
+ this.integrationManager = new IntegrationManager({
1150
+ debug: this.config.debug,
1151
+ maxQueueSize: 1000
1152
+ });
1153
+ // Set initial context
1154
+ this.integrationManager.setContext({});
698
1155
  }
699
1156
  init() {
700
1157
  if (this.isInitialized) {
701
- this.log('Saasco is already initialized. Please check your code to ensure that init() is not being called multiple times.');
1158
+ this.logger.log('Saasco is already initialized. Please check your code to ensure that init() is not being called multiple times.');
702
1159
  return;
703
1160
  }
704
1161
  // Export Saasco to the window object for easy access and debugging
@@ -707,23 +1164,46 @@ class Saasco {
707
1164
  }
708
1165
  // Migrate existing localStorage data to cookies for cross-subdomain support
709
1166
  migrateFromLocalStorage();
710
- this.log('Saasco initialized', this.config);
711
- if (this.config.debug) this.log('Debug mode active. This will log all events to the console.');
712
- if (!this.config.enabled) this.log('Analytics is disabled. No requests will be sent to the server.');
1167
+ this.logger.log('Saasco initialized', this.config);
1168
+ if (this.config.debug) this.logger.log('Debug mode active. This will log all events to the console.');
1169
+ if (!this.config.enabled) this.logger.log('Analytics is disabled. No requests will be sent to the server.');
1170
+ // Initialize integrations asynchronously
1171
+ this.initIntegrations().catch(error => {
1172
+ this.logger.log('Error initializing integrations:', error);
1173
+ });
713
1174
  this.initAutoPageTracking();
714
1175
  this.isInitialized = true;
715
1176
  }
716
1177
  disableDebug() {
717
- this.log('Debug mode deactivated.');
1178
+ this.logger.log('Debug mode deactivated.');
718
1179
  this.config.debug = false;
719
1180
  }
720
1181
  enableDebug() {
721
1182
  this.config.debug = true;
722
- this.log('Debug mode activated.');
1183
+ this.logger.log('Debug mode activated.');
1184
+ }
1185
+ /**
1186
+ * Initialize third-party integrations
1187
+ */
1188
+ initIntegrations() {
1189
+ return __awaiter(this, void 0, void 0, function* () {
1190
+ if (!this.config.integrations || this.config.integrations.length === 0) return;
1191
+ for (const integrationConfig of this.config.integrations) {
1192
+ try {
1193
+ if (integrationConfig.type === 'facebook-pixel') {
1194
+ this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
1195
+ const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
1196
+ yield this.integrationManager.registerIntegration(fbIntegration);
1197
+ }
1198
+ } catch (error) {
1199
+ this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
1200
+ }
1201
+ }
1202
+ });
723
1203
  }
724
1204
  track(actionOrPayload, propertiesOrNothing, contextOrNothing) {
725
1205
  if (!this.config.projectId) {
726
- const response = this.error("Unable to track event. Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
1206
+ const response = this.logger.error("Unable to track event. Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
727
1207
  return Promise.resolve(response);
728
1208
  }
729
1209
  setSessionId();
@@ -732,12 +1212,12 @@ class Saasco {
732
1212
  const hasPayload = typeof actionOrPayload === 'object';
733
1213
  // Must do payload on the server
734
1214
  if (isServer && hasAction) {
735
- const response = this.error('When calling track from the server you must pass a payload object with a userId in order to track events. For example: track({ event: "Song Played", userId: "user_123", properties: { song: "Song Name" } })');
1215
+ const response = this.logger.error('When calling track from the server you must pass a payload object with a userId in order to track events. For example: track({ event: "Song Played", userId: "user_123", properties: { song: "Song Name" } })');
736
1216
  return Promise.resolve(response);
737
1217
  }
738
1218
  // On server events we require a userId or an anonymousId
739
1219
  if (isServer && hasPayload && !actionOrPayload.userId && !actionOrPayload.anonymousId) {
740
- const response = this.error('When calling track from the server you must pass a payload object with either a userId or an anonymousId in order to track events. For example:\n\ntrack({ event: "Song Played", userId: "user_123", properties: { song: "Song Name" } }). \n\nIf providing an anonymousId this must be provided from the client side otherwise we will not have a user to connect the event to.');
1220
+ const response = this.logger.error('When calling track from the server you must pass a payload object with either a userId or an anonymousId in order to track events. For example:\n\ntrack({ event: "Song Played", userId: "user_123", properties: { song: "Song Name" } }). \n\nIf providing an anonymousId this must be provided from the client side otherwise we will not have a user to connect the event to.');
741
1221
  return Promise.resolve(response);
742
1222
  }
743
1223
  const action = hasAction ? actionOrPayload : actionOrPayload.event;
@@ -757,7 +1237,7 @@ class Saasco {
757
1237
  properties: properties || {}
758
1238
  });
759
1239
  const data = {
760
- id: v4(),
1240
+ id: uuid(),
761
1241
  timestamp: new Date().toISOString(),
762
1242
  action,
763
1243
  version,
@@ -769,6 +1249,9 @@ class Saasco {
769
1249
  source: isBrowser ? 'client' : 'server',
770
1250
  context: JSON.stringify(context || {})
771
1251
  };
1252
+ // Send event to integrations
1253
+ this.integrationManager.track(action, properties);
1254
+ // Send event to API
772
1255
  return this.doRequest('events', data);
773
1256
  }
774
1257
  /**
@@ -800,20 +1283,20 @@ class Saasco {
800
1283
  }
801
1284
  identify(distinctIdOrProperties, propertiesOrContext, contextOrNothing) {
802
1285
  if (!this.config.projectId) {
803
- const response = this.error("Unable to identify user. Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
1286
+ const response = this.logger.error("Unable to identify user. Project ID is required but has not been provided. If you are using an env variable make sure it's set correctly.");
804
1287
  return Promise.resolve(response);
805
1288
  }
806
1289
  const hasId = typeof distinctIdOrProperties === 'string' || typeof distinctIdOrProperties === 'number' || distinctIdOrProperties === null;
807
- const distinctId = hasId ? distinctIdOrProperties === null || distinctIdOrProperties === void 0 ? void 0 : distinctIdOrProperties.toString() : `soft_${v4()}`;
1290
+ const distinctId = hasId ? distinctIdOrProperties === null || distinctIdOrProperties === void 0 ? void 0 : distinctIdOrProperties.toString() : `soft_${uuid()}`;
808
1291
  const properties = hasId ? propertiesOrContext : distinctIdOrProperties;
809
1292
  const context = hasId ? contextOrNothing : propertiesOrContext;
810
1293
  // When the user gets identified as null this will reset the users session and anonymous id
811
1294
  // This should only be called when distinctId is null and there is an existing userId.
812
1295
  // This means the user has logged out and we should reset the session and anonymous IDs
813
1296
  const userIdChangedToNull = distinctId === null && !!getUserId();
814
- if (userIdChangedToNull) this.log('User logged out');
1297
+ if (userIdChangedToNull) this.logger.log('User logged out');
815
1298
  const reset = userIdChangedToNull;
816
- setSessionId({
1299
+ const sessionId = setSessionId({
817
1300
  reset
818
1301
  });
819
1302
  const anonymousId = setAnonymousId({
@@ -821,13 +1304,21 @@ class Saasco {
821
1304
  });
822
1305
  // set the distinct Id to the userId
823
1306
  setUserId(distinctId);
1307
+ // Update integration manager context and send identify (IntegrationManager will filter by environment)
1308
+ this.integrationManager.setContext({
1309
+ distinctId,
1310
+ anonymousId,
1311
+ sessionId
1312
+ });
1313
+ this.integrationManager.identify(distinctId, properties);
824
1314
  // No distinctId provided so we don't track the user
825
1315
  if (!distinctId) return Promise.resolve({
826
1316
  success: true,
827
1317
  message: 'No distinctId provided'
828
1318
  });
1319
+ // Send identify to API
829
1320
  const data = {
830
- id: v4(),
1321
+ id: uuid(),
831
1322
  timestamp: new Date().toISOString(),
832
1323
  projectId: this.config.projectId,
833
1324
  distinctId,
@@ -861,9 +1352,9 @@ class Saasco {
861
1352
  const base = this.config.proxy || 'https://www.saasco.com/api/';
862
1353
  const url = `${base}${path}`;
863
1354
  if (data.action === 'Page View') {
864
- this.log('Page View', window.location.href, data);
1355
+ this.logger.log('Page View', window.location.href, data);
865
1356
  } else {
866
- this.log(data.action || path, data);
1357
+ this.logger.log(data.action || path, data);
867
1358
  }
868
1359
  // If analytics is disabled, don't send the request
869
1360
  if (this.config.enabled === false) return {
@@ -887,7 +1378,7 @@ class Saasco {
887
1378
  message: responseBody.message
888
1379
  };
889
1380
  } catch (error) {
890
- this.error(`\nError Message: "${error.message}"`, `\nPath: "/${path}"`, `\nRequest Data: ${JSON.stringify(data, null, 2)}`);
1381
+ this.logger.error(`\nError Message: "${error.message}"`, `\nPath: "/${path}"`, `\nRequest Data: ${JSON.stringify(data, null, 2)}`);
891
1382
  return {
892
1383
  success: false,
893
1384
  message: error.message
@@ -895,29 +1386,6 @@ class Saasco {
895
1386
  }
896
1387
  });
897
1388
  }
898
- /**
899
- * @param args Arguments to be logged
900
- */
901
- log(...args) {
902
- if (!this.config.debug) return;
903
- const message = '◍ Saasco Debug';
904
- console.info(
905
- // Message highlighted for easy finding
906
- `%c ${message}`, 'background: #eee; color: #000; padding: 2px 4px; border-radius: 2px;', ...args);
907
- }
908
- /**
909
- * @param args Arguments to be logged
910
- */
911
- error(...args) {
912
- const message = '◍ Saasco Error';
913
- console.error(
914
- // Message highlighted for easy finding
915
- `%c ${message}`, 'background: red; color: white; padding: 2px 4px; border-radius: 2px;', ...args);
916
- return {
917
- success: false,
918
- message: args.join(' ')
919
- };
920
- }
921
1389
  /**
922
1390
  * If autoPageTracking is enabled, this will automatically track page views
923
1391
  * It listens to url changes to track new pages every time the url changes
@@ -931,11 +1399,11 @@ class Saasco {
931
1399
  if (isServer) return console.warn('Saasco auto page tracking is only available in the browser');
932
1400
  // Prevent intitializing auto page tracking more than once
933
1401
  if (window.saascoAutoPageTrackingActive) {
934
- this.log('Auto Page Tracking already enabled');
1402
+ this.logger.log('Auto Page Tracking already enabled');
935
1403
  return;
936
1404
  }
937
1405
  window.saascoAutoPageTrackingActive = true;
938
- this.log('Auto Page Tracking enabled');
1406
+ this.logger.log('Auto Page Tracking enabled');
939
1407
  // Track initial page load
940
1408
  this.page();
941
1409
  // Listen for hash changes if hash tracking is enabled
@@ -981,7 +1449,39 @@ const browserContextSchema = z.object({
981
1449
  $utmSourcePlatform: z.string().nullable(),
982
1450
  $utmCampaignId: z.string().nullable(),
983
1451
  $utmCreativeFormat: z.string().nullable(),
984
- $utmMarketingTactic: z.string().nullable()
1452
+ $utmMarketingTactic: z.string().nullable(),
1453
+ $utmAdSource: z.string().nullable(),
1454
+ $utmAdId: z.string().nullable()
985
1455
  });
986
1456
 
987
- export { Saasco, browserContextSchema, getBrowserContext, timezones };
1457
+ /**
1458
+ * Example integration that works on both client and server
1459
+ * Simply logs events to console
1460
+ */
1461
+ function createConsoleIntegration(config = {}) {
1462
+ const prefix = config.prefix || '[Analytics]';
1463
+ return {
1464
+ name: 'console',
1465
+ environments: ['client', 'server'],
1466
+ // This integration works on both client and server
1467
+ init: () => {
1468
+ console.log(`${prefix} Console integration initialized`);
1469
+ },
1470
+ track: (eventName, properties, context) => {
1471
+ console.log(`${prefix} Track:`, {
1472
+ event: eventName,
1473
+ properties,
1474
+ context
1475
+ });
1476
+ },
1477
+ identify: (userId, properties, context) => {
1478
+ console.log(`${prefix} Identify:`, {
1479
+ userId,
1480
+ properties,
1481
+ context
1482
+ });
1483
+ }
1484
+ };
1485
+ }
1486
+
1487
+ export { IntegrationManager, Logger, Saasco, browserContextSchema, createConsoleIntegration, createFacebookPixelIntegration, getBrowserContext, initializeFacebookPixel, timezones, trackFacebookEvent };