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.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.
|
|
6
|
+
var version = "0.1.32";
|
|
7
7
|
|
|
8
8
|
const timezones = {
|
|
9
9
|
'Asia/Barnaul': 'RU',
|
|
@@ -550,6 +550,385 @@ function getBrowserContext() {
|
|
|
550
550
|
};
|
|
551
551
|
}
|
|
552
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
|
+
|
|
553
932
|
const isBrowser = typeof window !== 'undefined';
|
|
554
933
|
const isServer = !isBrowser;
|
|
555
934
|
const PREF = 'saasco-sdk';
|
|
@@ -684,8 +1063,9 @@ function setSessionId({
|
|
|
684
1063
|
} = {
|
|
685
1064
|
reset: false
|
|
686
1065
|
}) {
|
|
687
|
-
const sessionId = reset ?
|
|
1066
|
+
const sessionId = reset ? uuid() : getSessionId() || uuid();
|
|
688
1067
|
storeData(`session-id`, sessionId, SESSION_DURATION);
|
|
1068
|
+
return sessionId;
|
|
689
1069
|
}
|
|
690
1070
|
function getAnonymousId() {
|
|
691
1071
|
return retrieveData(`anonymous-id`);
|
|
@@ -695,7 +1075,7 @@ function setAnonymousId({
|
|
|
695
1075
|
} = {
|
|
696
1076
|
reset: false
|
|
697
1077
|
}) {
|
|
698
|
-
const anonymousId = reset ?
|
|
1078
|
+
const anonymousId = reset ? uuid() : getAnonymousId() || uuid();
|
|
699
1079
|
storeData(`anonymous-id`, anonymousId, USER_DURATION);
|
|
700
1080
|
return anonymousId;
|
|
701
1081
|
}
|
|
@@ -742,13 +1122,19 @@ class Saasco {
|
|
|
742
1122
|
* @param config.debug Whether to log debug information. Default is false.
|
|
743
1123
|
* @param config.trackUrlParams Whether to track URL parameters. Default is true.
|
|
744
1124
|
* @param config.trackHashChanges Whether to track hash changes. Default is true.
|
|
1125
|
+
* @param config.integrations Configuration for third-party integrations like Facebook Pixel.
|
|
745
1126
|
*/
|
|
746
1127
|
constructor(config) {
|
|
747
1128
|
this.config = config;
|
|
748
1129
|
this.lastPageViewPath = '';
|
|
749
1130
|
this.isInitialized = false;
|
|
1131
|
+
// Initialize logger
|
|
1132
|
+
this.logger = new AnalyticsLogger({
|
|
1133
|
+
debug: !!this.config.debug,
|
|
1134
|
+
label: 'Saasco Debug'
|
|
1135
|
+
});
|
|
750
1136
|
if (!config.projectId) {
|
|
751
|
-
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.");
|
|
752
1138
|
return;
|
|
753
1139
|
}
|
|
754
1140
|
// default enabled to true
|
|
@@ -759,10 +1145,17 @@ class Saasco {
|
|
|
759
1145
|
trackQueryParams: true,
|
|
760
1146
|
trackHash: true
|
|
761
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({});
|
|
762
1155
|
}
|
|
763
1156
|
init() {
|
|
764
1157
|
if (this.isInitialized) {
|
|
765
|
-
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.');
|
|
766
1159
|
return;
|
|
767
1160
|
}
|
|
768
1161
|
// Export Saasco to the window object for easy access and debugging
|
|
@@ -771,23 +1164,46 @@ class Saasco {
|
|
|
771
1164
|
}
|
|
772
1165
|
// Migrate existing localStorage data to cookies for cross-subdomain support
|
|
773
1166
|
migrateFromLocalStorage();
|
|
774
|
-
this.log('Saasco initialized', this.config);
|
|
775
|
-
if (this.config.debug) this.log('Debug mode active. This will log all events to the console.');
|
|
776
|
-
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
|
+
});
|
|
777
1174
|
this.initAutoPageTracking();
|
|
778
1175
|
this.isInitialized = true;
|
|
779
1176
|
}
|
|
780
1177
|
disableDebug() {
|
|
781
|
-
this.log('Debug mode deactivated.');
|
|
1178
|
+
this.logger.log('Debug mode deactivated.');
|
|
782
1179
|
this.config.debug = false;
|
|
783
1180
|
}
|
|
784
1181
|
enableDebug() {
|
|
785
1182
|
this.config.debug = true;
|
|
786
|
-
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
|
+
});
|
|
787
1203
|
}
|
|
788
1204
|
track(actionOrPayload, propertiesOrNothing, contextOrNothing) {
|
|
789
1205
|
if (!this.config.projectId) {
|
|
790
|
-
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.");
|
|
791
1207
|
return Promise.resolve(response);
|
|
792
1208
|
}
|
|
793
1209
|
setSessionId();
|
|
@@ -796,12 +1212,12 @@ class Saasco {
|
|
|
796
1212
|
const hasPayload = typeof actionOrPayload === 'object';
|
|
797
1213
|
// Must do payload on the server
|
|
798
1214
|
if (isServer && hasAction) {
|
|
799
|
-
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" } })');
|
|
800
1216
|
return Promise.resolve(response);
|
|
801
1217
|
}
|
|
802
1218
|
// On server events we require a userId or an anonymousId
|
|
803
1219
|
if (isServer && hasPayload && !actionOrPayload.userId && !actionOrPayload.anonymousId) {
|
|
804
|
-
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.');
|
|
805
1221
|
return Promise.resolve(response);
|
|
806
1222
|
}
|
|
807
1223
|
const action = hasAction ? actionOrPayload : actionOrPayload.event;
|
|
@@ -821,7 +1237,7 @@ class Saasco {
|
|
|
821
1237
|
properties: properties || {}
|
|
822
1238
|
});
|
|
823
1239
|
const data = {
|
|
824
|
-
id:
|
|
1240
|
+
id: uuid(),
|
|
825
1241
|
timestamp: new Date().toISOString(),
|
|
826
1242
|
action,
|
|
827
1243
|
version,
|
|
@@ -833,6 +1249,9 @@ class Saasco {
|
|
|
833
1249
|
source: isBrowser ? 'client' : 'server',
|
|
834
1250
|
context: JSON.stringify(context || {})
|
|
835
1251
|
};
|
|
1252
|
+
// Send event to integrations
|
|
1253
|
+
this.integrationManager.track(action, properties);
|
|
1254
|
+
// Send event to API
|
|
836
1255
|
return this.doRequest('events', data);
|
|
837
1256
|
}
|
|
838
1257
|
/**
|
|
@@ -864,20 +1283,20 @@ class Saasco {
|
|
|
864
1283
|
}
|
|
865
1284
|
identify(distinctIdOrProperties, propertiesOrContext, contextOrNothing) {
|
|
866
1285
|
if (!this.config.projectId) {
|
|
867
|
-
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.");
|
|
868
1287
|
return Promise.resolve(response);
|
|
869
1288
|
}
|
|
870
1289
|
const hasId = typeof distinctIdOrProperties === 'string' || typeof distinctIdOrProperties === 'number' || distinctIdOrProperties === null;
|
|
871
|
-
const distinctId = hasId ? distinctIdOrProperties === null || distinctIdOrProperties === void 0 ? void 0 : distinctIdOrProperties.toString() : `soft_${
|
|
1290
|
+
const distinctId = hasId ? distinctIdOrProperties === null || distinctIdOrProperties === void 0 ? void 0 : distinctIdOrProperties.toString() : `soft_${uuid()}`;
|
|
872
1291
|
const properties = hasId ? propertiesOrContext : distinctIdOrProperties;
|
|
873
1292
|
const context = hasId ? contextOrNothing : propertiesOrContext;
|
|
874
1293
|
// When the user gets identified as null this will reset the users session and anonymous id
|
|
875
1294
|
// This should only be called when distinctId is null and there is an existing userId.
|
|
876
1295
|
// This means the user has logged out and we should reset the session and anonymous IDs
|
|
877
1296
|
const userIdChangedToNull = distinctId === null && !!getUserId();
|
|
878
|
-
if (userIdChangedToNull) this.log('User logged out');
|
|
1297
|
+
if (userIdChangedToNull) this.logger.log('User logged out');
|
|
879
1298
|
const reset = userIdChangedToNull;
|
|
880
|
-
setSessionId({
|
|
1299
|
+
const sessionId = setSessionId({
|
|
881
1300
|
reset
|
|
882
1301
|
});
|
|
883
1302
|
const anonymousId = setAnonymousId({
|
|
@@ -885,13 +1304,21 @@ class Saasco {
|
|
|
885
1304
|
});
|
|
886
1305
|
// set the distinct Id to the userId
|
|
887
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);
|
|
888
1314
|
// No distinctId provided so we don't track the user
|
|
889
1315
|
if (!distinctId) return Promise.resolve({
|
|
890
1316
|
success: true,
|
|
891
1317
|
message: 'No distinctId provided'
|
|
892
1318
|
});
|
|
1319
|
+
// Send identify to API
|
|
893
1320
|
const data = {
|
|
894
|
-
id:
|
|
1321
|
+
id: uuid(),
|
|
895
1322
|
timestamp: new Date().toISOString(),
|
|
896
1323
|
projectId: this.config.projectId,
|
|
897
1324
|
distinctId,
|
|
@@ -925,9 +1352,9 @@ class Saasco {
|
|
|
925
1352
|
const base = this.config.proxy || 'https://www.saasco.com/api/';
|
|
926
1353
|
const url = `${base}${path}`;
|
|
927
1354
|
if (data.action === 'Page View') {
|
|
928
|
-
this.log('Page View', window.location.href, data);
|
|
1355
|
+
this.logger.log('Page View', window.location.href, data);
|
|
929
1356
|
} else {
|
|
930
|
-
this.log(data.action || path, data);
|
|
1357
|
+
this.logger.log(data.action || path, data);
|
|
931
1358
|
}
|
|
932
1359
|
// If analytics is disabled, don't send the request
|
|
933
1360
|
if (this.config.enabled === false) return {
|
|
@@ -951,7 +1378,7 @@ class Saasco {
|
|
|
951
1378
|
message: responseBody.message
|
|
952
1379
|
};
|
|
953
1380
|
} catch (error) {
|
|
954
|
-
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)}`);
|
|
955
1382
|
return {
|
|
956
1383
|
success: false,
|
|
957
1384
|
message: error.message
|
|
@@ -959,29 +1386,6 @@ class Saasco {
|
|
|
959
1386
|
}
|
|
960
1387
|
});
|
|
961
1388
|
}
|
|
962
|
-
/**
|
|
963
|
-
* @param args Arguments to be logged
|
|
964
|
-
*/
|
|
965
|
-
log(...args) {
|
|
966
|
-
if (!this.config.debug) return;
|
|
967
|
-
const message = '◍ Saasco Debug';
|
|
968
|
-
console.info(
|
|
969
|
-
// Message highlighted for easy finding
|
|
970
|
-
`%c ${message}`, 'background: #eee; color: #000; padding: 2px 4px; border-radius: 2px;', ...args);
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* @param args Arguments to be logged
|
|
974
|
-
*/
|
|
975
|
-
error(...args) {
|
|
976
|
-
const message = '◍ Saasco Error';
|
|
977
|
-
console.error(
|
|
978
|
-
// Message highlighted for easy finding
|
|
979
|
-
`%c ${message}`, 'background: red; color: white; padding: 2px 4px; border-radius: 2px;', ...args);
|
|
980
|
-
return {
|
|
981
|
-
success: false,
|
|
982
|
-
message: args.join(' ')
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
1389
|
/**
|
|
986
1390
|
* If autoPageTracking is enabled, this will automatically track page views
|
|
987
1391
|
* It listens to url changes to track new pages every time the url changes
|
|
@@ -995,11 +1399,11 @@ class Saasco {
|
|
|
995
1399
|
if (isServer) return console.warn('Saasco auto page tracking is only available in the browser');
|
|
996
1400
|
// Prevent intitializing auto page tracking more than once
|
|
997
1401
|
if (window.saascoAutoPageTrackingActive) {
|
|
998
|
-
this.log('Auto Page Tracking already enabled');
|
|
1402
|
+
this.logger.log('Auto Page Tracking already enabled');
|
|
999
1403
|
return;
|
|
1000
1404
|
}
|
|
1001
1405
|
window.saascoAutoPageTrackingActive = true;
|
|
1002
|
-
this.log('Auto Page Tracking enabled');
|
|
1406
|
+
this.logger.log('Auto Page Tracking enabled');
|
|
1003
1407
|
// Track initial page load
|
|
1004
1408
|
this.page();
|
|
1005
1409
|
// Listen for hash changes if hash tracking is enabled
|
|
@@ -1050,4 +1454,34 @@ const browserContextSchema = z.object({
|
|
|
1050
1454
|
$utmAdId: z.string().nullable()
|
|
1051
1455
|
});
|
|
1052
1456
|
|
|
1053
|
-
|
|
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 };
|