saasco-sdk 0.1.31 → 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 +488 -48
- package/index.esm.js +483 -49
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
- package/src/lib/analytics.d.ts +13 -8
- package/src/lib/integrations/console-integration.d.ts +9 -0
- package/src/lib/integrations/facebook-pixel.d.ts +26 -0
- package/src/lib/integrations/index.d.ts +3 -0
- package/src/lib/integrations/integration-manager.d.ts +125 -0
- package/src/lib/utils/index.d.ts +2 -0
- package/src/lib/utils/logger.d.ts +23 -0
- package/src/lib/utils/uuid.d.ts +5 -0
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.
|
|
10
|
+
var version = "0.1.32";
|
|
11
11
|
|
|
12
12
|
const timezones = {
|
|
13
13
|
'Asia/Barnaul': 'RU',
|
|
@@ -554,6 +554,385 @@ function getBrowserContext() {
|
|
|
554
554
|
};
|
|
555
555
|
}
|
|
556
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
|
+
}
|
|
641
|
+
};
|
|
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
|
+
}
|
|
935
|
+
|
|
557
936
|
const isBrowser = typeof window !== 'undefined';
|
|
558
937
|
const isServer = !isBrowser;
|
|
559
938
|
const PREF = 'saasco-sdk';
|
|
@@ -688,8 +1067,9 @@ function setSessionId({
|
|
|
688
1067
|
} = {
|
|
689
1068
|
reset: false
|
|
690
1069
|
}) {
|
|
691
|
-
const sessionId = reset ? uuid
|
|
1070
|
+
const sessionId = reset ? uuid() : getSessionId() || uuid();
|
|
692
1071
|
storeData(`session-id`, sessionId, SESSION_DURATION);
|
|
1072
|
+
return sessionId;
|
|
693
1073
|
}
|
|
694
1074
|
function getAnonymousId() {
|
|
695
1075
|
return retrieveData(`anonymous-id`);
|
|
@@ -699,7 +1079,7 @@ function setAnonymousId({
|
|
|
699
1079
|
} = {
|
|
700
1080
|
reset: false
|
|
701
1081
|
}) {
|
|
702
|
-
const anonymousId = reset ? uuid
|
|
1082
|
+
const anonymousId = reset ? uuid() : getAnonymousId() || uuid();
|
|
703
1083
|
storeData(`anonymous-id`, anonymousId, USER_DURATION);
|
|
704
1084
|
return anonymousId;
|
|
705
1085
|
}
|
|
@@ -746,13 +1126,19 @@ class Saasco {
|
|
|
746
1126
|
* @param config.debug Whether to log debug information. Default is false.
|
|
747
1127
|
* @param config.trackUrlParams Whether to track URL parameters. Default is true.
|
|
748
1128
|
* @param config.trackHashChanges Whether to track hash changes. Default is true.
|
|
1129
|
+
* @param config.integrations Configuration for third-party integrations like Facebook Pixel.
|
|
749
1130
|
*/
|
|
750
1131
|
constructor(config) {
|
|
751
1132
|
this.config = config;
|
|
752
1133
|
this.lastPageViewPath = '';
|
|
753
1134
|
this.isInitialized = false;
|
|
1135
|
+
// Initialize logger
|
|
1136
|
+
this.logger = new AnalyticsLogger({
|
|
1137
|
+
debug: !!this.config.debug,
|
|
1138
|
+
label: 'Saasco Debug'
|
|
1139
|
+
});
|
|
754
1140
|
if (!config.projectId) {
|
|
755
|
-
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.");
|
|
756
1142
|
return;
|
|
757
1143
|
}
|
|
758
1144
|
// default enabled to true
|
|
@@ -763,10 +1149,17 @@ class Saasco {
|
|
|
763
1149
|
trackQueryParams: true,
|
|
764
1150
|
trackHash: true
|
|
765
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({});
|
|
766
1159
|
}
|
|
767
1160
|
init() {
|
|
768
1161
|
if (this.isInitialized) {
|
|
769
|
-
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.');
|
|
770
1163
|
return;
|
|
771
1164
|
}
|
|
772
1165
|
// Export Saasco to the window object for easy access and debugging
|
|
@@ -775,23 +1168,46 @@ class Saasco {
|
|
|
775
1168
|
}
|
|
776
1169
|
// Migrate existing localStorage data to cookies for cross-subdomain support
|
|
777
1170
|
migrateFromLocalStorage();
|
|
778
|
-
this.log('Saasco initialized', this.config);
|
|
779
|
-
if (this.config.debug) this.log('Debug mode active. This will log all events to the console.');
|
|
780
|
-
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
|
+
});
|
|
781
1178
|
this.initAutoPageTracking();
|
|
782
1179
|
this.isInitialized = true;
|
|
783
1180
|
}
|
|
784
1181
|
disableDebug() {
|
|
785
|
-
this.log('Debug mode deactivated.');
|
|
1182
|
+
this.logger.log('Debug mode deactivated.');
|
|
786
1183
|
this.config.debug = false;
|
|
787
1184
|
}
|
|
788
1185
|
enableDebug() {
|
|
789
1186
|
this.config.debug = true;
|
|
790
|
-
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
|
+
});
|
|
791
1207
|
}
|
|
792
1208
|
track(actionOrPayload, propertiesOrNothing, contextOrNothing) {
|
|
793
1209
|
if (!this.config.projectId) {
|
|
794
|
-
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.");
|
|
795
1211
|
return Promise.resolve(response);
|
|
796
1212
|
}
|
|
797
1213
|
setSessionId();
|
|
@@ -800,12 +1216,12 @@ class Saasco {
|
|
|
800
1216
|
const hasPayload = typeof actionOrPayload === 'object';
|
|
801
1217
|
// Must do payload on the server
|
|
802
1218
|
if (isServer && hasAction) {
|
|
803
|
-
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" } })');
|
|
804
1220
|
return Promise.resolve(response);
|
|
805
1221
|
}
|
|
806
1222
|
// On server events we require a userId or an anonymousId
|
|
807
1223
|
if (isServer && hasPayload && !actionOrPayload.userId && !actionOrPayload.anonymousId) {
|
|
808
|
-
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.');
|
|
809
1225
|
return Promise.resolve(response);
|
|
810
1226
|
}
|
|
811
1227
|
const action = hasAction ? actionOrPayload : actionOrPayload.event;
|
|
@@ -825,7 +1241,7 @@ class Saasco {
|
|
|
825
1241
|
properties: properties || {}
|
|
826
1242
|
});
|
|
827
1243
|
const data = {
|
|
828
|
-
id: uuid
|
|
1244
|
+
id: uuid(),
|
|
829
1245
|
timestamp: new Date().toISOString(),
|
|
830
1246
|
action,
|
|
831
1247
|
version,
|
|
@@ -837,6 +1253,9 @@ class Saasco {
|
|
|
837
1253
|
source: isBrowser ? 'client' : 'server',
|
|
838
1254
|
context: JSON.stringify(context || {})
|
|
839
1255
|
};
|
|
1256
|
+
// Send event to integrations
|
|
1257
|
+
this.integrationManager.track(action, properties);
|
|
1258
|
+
// Send event to API
|
|
840
1259
|
return this.doRequest('events', data);
|
|
841
1260
|
}
|
|
842
1261
|
/**
|
|
@@ -868,20 +1287,20 @@ class Saasco {
|
|
|
868
1287
|
}
|
|
869
1288
|
identify(distinctIdOrProperties, propertiesOrContext, contextOrNothing) {
|
|
870
1289
|
if (!this.config.projectId) {
|
|
871
|
-
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.");
|
|
872
1291
|
return Promise.resolve(response);
|
|
873
1292
|
}
|
|
874
1293
|
const hasId = typeof distinctIdOrProperties === 'string' || typeof distinctIdOrProperties === 'number' || distinctIdOrProperties === null;
|
|
875
|
-
const distinctId = hasId ? distinctIdOrProperties === null || distinctIdOrProperties === void 0 ? void 0 : distinctIdOrProperties.toString() : `soft_${uuid
|
|
1294
|
+
const distinctId = hasId ? distinctIdOrProperties === null || distinctIdOrProperties === void 0 ? void 0 : distinctIdOrProperties.toString() : `soft_${uuid()}`;
|
|
876
1295
|
const properties = hasId ? propertiesOrContext : distinctIdOrProperties;
|
|
877
1296
|
const context = hasId ? contextOrNothing : propertiesOrContext;
|
|
878
1297
|
// When the user gets identified as null this will reset the users session and anonymous id
|
|
879
1298
|
// This should only be called when distinctId is null and there is an existing userId.
|
|
880
1299
|
// This means the user has logged out and we should reset the session and anonymous IDs
|
|
881
1300
|
const userIdChangedToNull = distinctId === null && !!getUserId();
|
|
882
|
-
if (userIdChangedToNull) this.log('User logged out');
|
|
1301
|
+
if (userIdChangedToNull) this.logger.log('User logged out');
|
|
883
1302
|
const reset = userIdChangedToNull;
|
|
884
|
-
setSessionId({
|
|
1303
|
+
const sessionId = setSessionId({
|
|
885
1304
|
reset
|
|
886
1305
|
});
|
|
887
1306
|
const anonymousId = setAnonymousId({
|
|
@@ -889,13 +1308,21 @@ class Saasco {
|
|
|
889
1308
|
});
|
|
890
1309
|
// set the distinct Id to the userId
|
|
891
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);
|
|
892
1318
|
// No distinctId provided so we don't track the user
|
|
893
1319
|
if (!distinctId) return Promise.resolve({
|
|
894
1320
|
success: true,
|
|
895
1321
|
message: 'No distinctId provided'
|
|
896
1322
|
});
|
|
1323
|
+
// Send identify to API
|
|
897
1324
|
const data = {
|
|
898
|
-
id: uuid
|
|
1325
|
+
id: uuid(),
|
|
899
1326
|
timestamp: new Date().toISOString(),
|
|
900
1327
|
projectId: this.config.projectId,
|
|
901
1328
|
distinctId,
|
|
@@ -929,9 +1356,9 @@ class Saasco {
|
|
|
929
1356
|
const base = this.config.proxy || 'https://www.saasco.com/api/';
|
|
930
1357
|
const url = `${base}${path}`;
|
|
931
1358
|
if (data.action === 'Page View') {
|
|
932
|
-
this.log('Page View', window.location.href, data);
|
|
1359
|
+
this.logger.log('Page View', window.location.href, data);
|
|
933
1360
|
} else {
|
|
934
|
-
this.log(data.action || path, data);
|
|
1361
|
+
this.logger.log(data.action || path, data);
|
|
935
1362
|
}
|
|
936
1363
|
// If analytics is disabled, don't send the request
|
|
937
1364
|
if (this.config.enabled === false) return {
|
|
@@ -955,7 +1382,7 @@ class Saasco {
|
|
|
955
1382
|
message: responseBody.message
|
|
956
1383
|
};
|
|
957
1384
|
} catch (error) {
|
|
958
|
-
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)}`);
|
|
959
1386
|
return {
|
|
960
1387
|
success: false,
|
|
961
1388
|
message: error.message
|
|
@@ -963,29 +1390,6 @@ class Saasco {
|
|
|
963
1390
|
}
|
|
964
1391
|
});
|
|
965
1392
|
}
|
|
966
|
-
/**
|
|
967
|
-
* @param args Arguments to be logged
|
|
968
|
-
*/
|
|
969
|
-
log(...args) {
|
|
970
|
-
if (!this.config.debug) return;
|
|
971
|
-
const message = '◍ Saasco Debug';
|
|
972
|
-
console.info(
|
|
973
|
-
// Message highlighted for easy finding
|
|
974
|
-
`%c ${message}`, 'background: #eee; color: #000; padding: 2px 4px; border-radius: 2px;', ...args);
|
|
975
|
-
}
|
|
976
|
-
/**
|
|
977
|
-
* @param args Arguments to be logged
|
|
978
|
-
*/
|
|
979
|
-
error(...args) {
|
|
980
|
-
const message = '◍ Saasco Error';
|
|
981
|
-
console.error(
|
|
982
|
-
// Message highlighted for easy finding
|
|
983
|
-
`%c ${message}`, 'background: red; color: white; padding: 2px 4px; border-radius: 2px;', ...args);
|
|
984
|
-
return {
|
|
985
|
-
success: false,
|
|
986
|
-
message: args.join(' ')
|
|
987
|
-
};
|
|
988
|
-
}
|
|
989
1393
|
/**
|
|
990
1394
|
* If autoPageTracking is enabled, this will automatically track page views
|
|
991
1395
|
* It listens to url changes to track new pages every time the url changes
|
|
@@ -999,11 +1403,11 @@ class Saasco {
|
|
|
999
1403
|
if (isServer) return console.warn('Saasco auto page tracking is only available in the browser');
|
|
1000
1404
|
// Prevent intitializing auto page tracking more than once
|
|
1001
1405
|
if (window.saascoAutoPageTrackingActive) {
|
|
1002
|
-
this.log('Auto Page Tracking already enabled');
|
|
1406
|
+
this.logger.log('Auto Page Tracking already enabled');
|
|
1003
1407
|
return;
|
|
1004
1408
|
}
|
|
1005
1409
|
window.saascoAutoPageTrackingActive = true;
|
|
1006
|
-
this.log('Auto Page Tracking enabled');
|
|
1410
|
+
this.logger.log('Auto Page Tracking enabled');
|
|
1007
1411
|
// Track initial page load
|
|
1008
1412
|
this.page();
|
|
1009
1413
|
// Listen for hash changes if hash tracking is enabled
|
|
@@ -1054,7 +1458,43 @@ const browserContextSchema = zod.z.object({
|
|
|
1054
1458
|
$utmAdId: zod.z.string().nullable()
|
|
1055
1459
|
});
|
|
1056
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;
|
|
1057
1493
|
exports.Saasco = Saasco;
|
|
1058
1494
|
exports.browserContextSchema = browserContextSchema;
|
|
1495
|
+
exports.createConsoleIntegration = createConsoleIntegration;
|
|
1496
|
+
exports.createFacebookPixelIntegration = createFacebookPixelIntegration;
|
|
1059
1497
|
exports.getBrowserContext = getBrowserContext;
|
|
1498
|
+
exports.initializeFacebookPixel = initializeFacebookPixel;
|
|
1060
1499
|
exports.timezones = timezones;
|
|
1500
|
+
exports.trackFacebookEvent = trackFacebookEvent;
|