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