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 +557 -51
- package/index.esm.js +552 -52
- 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/tracking/types.d.ts +6 -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',
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|