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