saasco-sdk 0.1.32 → 0.1.34
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 +194 -59
- package/index.esm.js +195 -59
- package/package.json +1 -1
- package/src/lib/analytics.d.ts +2 -1
- package/src/lib/integrations/facebook-pixel.d.ts +2 -2
- package/src/lib/integrations/index.d.ts +0 -1
- package/src/lib/integrations/integration-manager.d.ts +1 -1
- package/src/lib/integrations/console-integration.d.ts +0 -9
package/index.cjs.js
CHANGED
|
@@ -7,7 +7,7 @@ var psl = require('psl');
|
|
|
7
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.34";
|
|
11
11
|
|
|
12
12
|
const timezones = {
|
|
13
13
|
'Asia/Barnaul': 'RU',
|
|
@@ -566,7 +566,9 @@ function createFacebookPixelIntegration(config) {
|
|
|
566
566
|
environments: ['client'],
|
|
567
567
|
init: () => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
568
568
|
if (isServer$1) {
|
|
569
|
-
|
|
569
|
+
// On server, Facebook Pixel is a no-op but doesn't throw
|
|
570
|
+
// This allows the integration to be registered but remain inactive
|
|
571
|
+
return;
|
|
570
572
|
}
|
|
571
573
|
// Check if Facebook Pixel is already loaded
|
|
572
574
|
if (window.fbq || window._fbq) {
|
|
@@ -624,23 +626,45 @@ function createFacebookPixelIntegration(config) {
|
|
|
624
626
|
if (properties['value'] !== undefined) fbParams['value'] = properties['value'];
|
|
625
627
|
if (properties['currency'] !== undefined) fbParams['currency'] = properties['currency'];
|
|
626
628
|
if (properties['predicted_ltv'] !== undefined) fbParams['predicted_ltv'] = properties['predicted_ltv'];
|
|
627
|
-
window.fbq('track', fbEventName, fbParams);
|
|
629
|
+
window.fbq('track', fbEventName, Object.assign(Object.assign({}, properties), fbParams));
|
|
628
630
|
} else {
|
|
629
631
|
window.fbq('track', fbEventName);
|
|
630
632
|
}
|
|
631
633
|
},
|
|
632
|
-
identify: (userId, properties, context) => {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
634
|
+
identify: (userId, properties, context) => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
635
|
+
try {
|
|
636
|
+
if (typeof window !== 'undefined' && window.fbq) {
|
|
637
|
+
const amValues = coerceMetaConversionsAmValues(Object.assign(Object.assign({}, properties || {}), userId ? {
|
|
638
|
+
external_id: userId
|
|
639
|
+
} : {}));
|
|
640
|
+
const am = {};
|
|
641
|
+
if (amValues.external_id) am['external_id'] = amValues.external_id;
|
|
642
|
+
if (amValues.em) am['em'] = yield sha256Hex(amValues.em);
|
|
643
|
+
if (amValues.fn) am['fn'] = amValues.fn;
|
|
644
|
+
if (amValues.ln) am['ln'] = amValues.ln;
|
|
645
|
+
if (amValues.ph) am['ph'] = amValues.ph;
|
|
646
|
+
if (amValues.ge) am['ge'] = amValues.ge;
|
|
647
|
+
if (amValues.db) am['db'] = amValues.db;
|
|
648
|
+
if (amValues.ct) am['ct'] = amValues.ct;
|
|
649
|
+
if (amValues.st) am['st'] = amValues.st;
|
|
650
|
+
if (amValues.zp) am['zp'] = amValues.zp;
|
|
651
|
+
if (amValues.country) am['country'] = amValues.country;
|
|
652
|
+
if (Object.keys(am).length > 0) {
|
|
653
|
+
window.fbq('init', config.pixelId, am);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
} catch (error) {
|
|
657
|
+
console.error('Error identifying user in Meta Pixel:', error);
|
|
639
658
|
}
|
|
640
|
-
}
|
|
659
|
+
})
|
|
641
660
|
};
|
|
642
661
|
}
|
|
643
662
|
function getFacebookEventName(eventName, eventMapping) {
|
|
663
|
+
// Convert default saasco page view event by default
|
|
664
|
+
if (eventName === 'Page View') {
|
|
665
|
+
const mappedEvent = 'ViewContent';
|
|
666
|
+
return mappedEvent;
|
|
667
|
+
}
|
|
644
668
|
if (!eventMapping) return eventName;
|
|
645
669
|
return eventMapping[eventName] || eventName;
|
|
646
670
|
}
|
|
@@ -668,6 +692,146 @@ function trackFacebookEvent(eventName, properties, eventMapping) {
|
|
|
668
692
|
window.fbq('track', fbEventName);
|
|
669
693
|
}
|
|
670
694
|
}
|
|
695
|
+
function coerceMetaConversionsAmValues(properties) {
|
|
696
|
+
const coerce = [{
|
|
697
|
+
key: 'em',
|
|
698
|
+
coerceFrom: ['email', 'user_email', 'userEmail', 'email_address', 'emailAddress', 'e_mail', 'E_Mail', 'mail', 'Mail', 'contact_email', 'contactEmail', 'primary_email', 'primaryEmail'],
|
|
699
|
+
transform: value => {
|
|
700
|
+
if (typeof value !== 'string') return undefined;
|
|
701
|
+
const normalized = value.trim().toLowerCase();
|
|
702
|
+
return normalized || undefined;
|
|
703
|
+
}
|
|
704
|
+
}, {
|
|
705
|
+
key: 'fn',
|
|
706
|
+
coerceFrom: ['first_name', 'firstName', 'firstname', 'given_name', 'givenName', 'user_first_name', 'userFirstName', 'name_first', 'nameFirst', 'f_name', 'fName'],
|
|
707
|
+
transform: value => {
|
|
708
|
+
if (typeof value !== 'string') return undefined;
|
|
709
|
+
const normalized = value.trim().toLowerCase();
|
|
710
|
+
return normalized || undefined;
|
|
711
|
+
}
|
|
712
|
+
}, {
|
|
713
|
+
key: 'ln',
|
|
714
|
+
coerceFrom: ['last_name', 'lastName', 'lastname', 'surname', 'user_last_name', 'userLastName', 'name_last', 'nameLast', 'l_name', 'lName'],
|
|
715
|
+
transform: value => {
|
|
716
|
+
if (typeof value !== 'string') return undefined;
|
|
717
|
+
const normalized = value.trim().toLowerCase();
|
|
718
|
+
return normalized || undefined;
|
|
719
|
+
}
|
|
720
|
+
}, {
|
|
721
|
+
key: 'ph',
|
|
722
|
+
coerceFrom: ['phone', 'phone_number', 'phoneNumber', 'mobile', 'mobile_number', 'mobileNumber', 'contact_number', 'contactNumber', 'tel', 'telephone'],
|
|
723
|
+
transform: value => {
|
|
724
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined;
|
|
725
|
+
const phoneStr = String(value).replace(/\D/g, '');
|
|
726
|
+
return phoneStr || undefined;
|
|
727
|
+
}
|
|
728
|
+
}, {
|
|
729
|
+
key: 'external_id',
|
|
730
|
+
coerceFrom: ['external_id', 'externalId', 'user_id', 'userId', 'id', 'distinctId', 'distinct_id', 'customer_id', 'customerId', 'member_id', 'memberId', 'loyalty_id', 'loyaltyId'],
|
|
731
|
+
transform: value => {
|
|
732
|
+
if (value == null) return undefined;
|
|
733
|
+
return String(value) || undefined;
|
|
734
|
+
}
|
|
735
|
+
}, {
|
|
736
|
+
key: 'ge',
|
|
737
|
+
coerceFrom: ['gender', 'user_gender', 'userGender', 'sex', 'user_sex', 'userSex'],
|
|
738
|
+
transform: value => {
|
|
739
|
+
if (typeof value !== 'string') return undefined;
|
|
740
|
+
const normalized = value.trim().toLowerCase();
|
|
741
|
+
if (normalized === 'female' || normalized === 'f') return 'f';
|
|
742
|
+
if (normalized === 'male' || normalized === 'm') return 'm';
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
}, {
|
|
746
|
+
key: 'db',
|
|
747
|
+
coerceFrom: ['birthday', 'birth_date', 'birthDate', 'date_of_birth', 'dateOfBirth', 'dob', 'Dob', 'birth_day', 'birthDay'],
|
|
748
|
+
transform: value => {
|
|
749
|
+
if (!value) return undefined;
|
|
750
|
+
let dateStr;
|
|
751
|
+
if (value instanceof Date) {
|
|
752
|
+
dateStr = value.toISOString().split('T')[0];
|
|
753
|
+
} else {
|
|
754
|
+
dateStr = String(value);
|
|
755
|
+
}
|
|
756
|
+
const dateMatch = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
757
|
+
if (dateMatch) {
|
|
758
|
+
const [, year, month, day] = dateMatch;
|
|
759
|
+
return `${year}${month}${day}`;
|
|
760
|
+
}
|
|
761
|
+
const numericMatch = dateStr.replace(/\D/g, '');
|
|
762
|
+
if (numericMatch.length === 8) {
|
|
763
|
+
return numericMatch;
|
|
764
|
+
}
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
}, {
|
|
768
|
+
key: 'ct',
|
|
769
|
+
coerceFrom: ['city', 'user_city', 'userCity', 'location_city', 'locationCity', 'address_city', 'addressCity'],
|
|
770
|
+
transform: value => {
|
|
771
|
+
if (typeof value !== 'string') return undefined;
|
|
772
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, '');
|
|
773
|
+
return normalized || undefined;
|
|
774
|
+
}
|
|
775
|
+
}, {
|
|
776
|
+
key: 'st',
|
|
777
|
+
coerceFrom: ['state', 'user_state', 'userState', 'province', 'user_province', 'userProvince', 'region', 'user_region', 'userRegion', 'location_state', 'locationState', 'address_state', 'addressState'],
|
|
778
|
+
transform: value => {
|
|
779
|
+
if (typeof value !== 'string') return undefined;
|
|
780
|
+
const normalized = value.trim().toLowerCase();
|
|
781
|
+
if (normalized.length === 2) return normalized;
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
}, {
|
|
785
|
+
key: 'zp',
|
|
786
|
+
coerceFrom: ['zip', 'zipcode', 'zip_code', 'postal_code', 'postalCode', 'postcode', 'user_zip', 'userZip', 'address_zip', 'addressZip'],
|
|
787
|
+
transform: value => {
|
|
788
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined;
|
|
789
|
+
return String(value).trim() || undefined;
|
|
790
|
+
}
|
|
791
|
+
}, {
|
|
792
|
+
key: 'country',
|
|
793
|
+
coerceFrom: ['country', 'country_code', 'countryCode', 'user_country', 'userCountry', 'location_country', 'locationCountry', 'address_country', 'addressCountry'],
|
|
794
|
+
transform: value => {
|
|
795
|
+
if (typeof value !== 'string') return undefined;
|
|
796
|
+
const normalized = value.trim().toLowerCase();
|
|
797
|
+
if (normalized.length === 2) return normalized;
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
}];
|
|
801
|
+
const result = {};
|
|
802
|
+
for (const {
|
|
803
|
+
key,
|
|
804
|
+
coerceFrom,
|
|
805
|
+
transform
|
|
806
|
+
} of coerce) {
|
|
807
|
+
const foundValue = findFirstProperty(properties, coerceFrom);
|
|
808
|
+
if (foundValue !== null && transform) {
|
|
809
|
+
const transformedValue = transform(foundValue);
|
|
810
|
+
if (transformedValue) {
|
|
811
|
+
result[key] = transformedValue;
|
|
812
|
+
}
|
|
813
|
+
} else if (foundValue !== null && typeof foundValue === 'string') {
|
|
814
|
+
result[key] = foundValue;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return result;
|
|
818
|
+
}
|
|
819
|
+
function findFirstProperty(properties, keys) {
|
|
820
|
+
for (const key of keys) {
|
|
821
|
+
if (properties[key] !== undefined) {
|
|
822
|
+
return properties[key];
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
const toHex = buf => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
828
|
+
function sha256Hex(input) {
|
|
829
|
+
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
830
|
+
const enc = new TextEncoder().encode(input);
|
|
831
|
+
const digest = yield crypto.subtle.digest('SHA-256', enc);
|
|
832
|
+
return toHex(digest);
|
|
833
|
+
});
|
|
834
|
+
}
|
|
671
835
|
|
|
672
836
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
673
837
|
class AnalyticsLogger {
|
|
@@ -847,21 +1011,23 @@ class IntegrationManager {
|
|
|
847
1011
|
this.deliver(envelope);
|
|
848
1012
|
}
|
|
849
1013
|
deliver(evt) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1014
|
+
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
1015
|
+
for (const {
|
|
1016
|
+
integration,
|
|
1017
|
+
status
|
|
1018
|
+
} of this.integrations.values()) {
|
|
1019
|
+
if (status !== 'ready') continue;
|
|
1020
|
+
try {
|
|
1021
|
+
if (evt.type === 'track' && integration.track && evt.name) {
|
|
1022
|
+
yield integration.track(evt.name, evt.properties, evt.context);
|
|
1023
|
+
} else if (evt.type === 'identify' && integration.identify) {
|
|
1024
|
+
yield integration.identify(evt.context.distinctId, evt.properties, evt.context);
|
|
1025
|
+
}
|
|
1026
|
+
} catch (e) {
|
|
1027
|
+
this.logger.error(`deliver error in ${integration.name} (${evt.type})`, e);
|
|
860
1028
|
}
|
|
861
|
-
} catch (e) {
|
|
862
|
-
this.logger.error(`deliver error in ${integration.name}`, e);
|
|
863
1029
|
}
|
|
864
|
-
}
|
|
1030
|
+
});
|
|
865
1031
|
}
|
|
866
1032
|
/**
|
|
867
1033
|
* Flush queued events FIFO once at least one integration is ready.
|
|
@@ -1156,6 +1322,10 @@ class Saasco {
|
|
|
1156
1322
|
});
|
|
1157
1323
|
// Set initial context
|
|
1158
1324
|
this.integrationManager.setContext({});
|
|
1325
|
+
// Initialize integrations immediately (works on both client and server)
|
|
1326
|
+
this.initIntegrations().catch(error => {
|
|
1327
|
+
this.logger.log('Error initializing integrations:', error);
|
|
1328
|
+
});
|
|
1159
1329
|
}
|
|
1160
1330
|
init() {
|
|
1161
1331
|
if (this.isInitialized) {
|
|
@@ -1171,10 +1341,6 @@ class Saasco {
|
|
|
1171
1341
|
this.logger.log('Saasco initialized', this.config);
|
|
1172
1342
|
if (this.config.debug) this.logger.log('Debug mode active. This will log all events to the console.');
|
|
1173
1343
|
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
|
-
});
|
|
1178
1344
|
this.initAutoPageTracking();
|
|
1179
1345
|
this.isInitialized = true;
|
|
1180
1346
|
}
|
|
@@ -1458,41 +1624,10 @@ const browserContextSchema = zod.z.object({
|
|
|
1458
1624
|
$utmAdId: zod.z.string().nullable()
|
|
1459
1625
|
});
|
|
1460
1626
|
|
|
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
1627
|
exports.IntegrationManager = IntegrationManager;
|
|
1492
1628
|
exports.Logger = Logger;
|
|
1493
1629
|
exports.Saasco = Saasco;
|
|
1494
1630
|
exports.browserContextSchema = browserContextSchema;
|
|
1495
|
-
exports.createConsoleIntegration = createConsoleIntegration;
|
|
1496
1631
|
exports.createFacebookPixelIntegration = createFacebookPixelIntegration;
|
|
1497
1632
|
exports.getBrowserContext = getBrowserContext;
|
|
1498
1633
|
exports.initializeFacebookPixel = initializeFacebookPixel;
|
package/index.esm.js
CHANGED
|
@@ -3,7 +3,7 @@ import { isValid, parse } from 'psl';
|
|
|
3
3
|
import { v4 } from '@lukeed/uuid';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
|
-
var version = "0.1.
|
|
6
|
+
var version = "0.1.34";
|
|
7
7
|
|
|
8
8
|
const timezones = {
|
|
9
9
|
'Asia/Barnaul': 'RU',
|
|
@@ -562,7 +562,9 @@ function createFacebookPixelIntegration(config) {
|
|
|
562
562
|
environments: ['client'],
|
|
563
563
|
init: () => __awaiter(this, void 0, void 0, function* () {
|
|
564
564
|
if (isServer$1) {
|
|
565
|
-
|
|
565
|
+
// On server, Facebook Pixel is a no-op but doesn't throw
|
|
566
|
+
// This allows the integration to be registered but remain inactive
|
|
567
|
+
return;
|
|
566
568
|
}
|
|
567
569
|
// Check if Facebook Pixel is already loaded
|
|
568
570
|
if (window.fbq || window._fbq) {
|
|
@@ -620,23 +622,45 @@ function createFacebookPixelIntegration(config) {
|
|
|
620
622
|
if (properties['value'] !== undefined) fbParams['value'] = properties['value'];
|
|
621
623
|
if (properties['currency'] !== undefined) fbParams['currency'] = properties['currency'];
|
|
622
624
|
if (properties['predicted_ltv'] !== undefined) fbParams['predicted_ltv'] = properties['predicted_ltv'];
|
|
623
|
-
window.fbq('track', fbEventName, fbParams);
|
|
625
|
+
window.fbq('track', fbEventName, Object.assign(Object.assign({}, properties), fbParams));
|
|
624
626
|
} else {
|
|
625
627
|
window.fbq('track', fbEventName);
|
|
626
628
|
}
|
|
627
629
|
},
|
|
628
|
-
identify: (userId, properties, context) => {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
630
|
+
identify: (userId, properties, context) => __awaiter(this, void 0, void 0, function* () {
|
|
631
|
+
try {
|
|
632
|
+
if (typeof window !== 'undefined' && window.fbq) {
|
|
633
|
+
const amValues = coerceMetaConversionsAmValues(Object.assign(Object.assign({}, properties || {}), userId ? {
|
|
634
|
+
external_id: userId
|
|
635
|
+
} : {}));
|
|
636
|
+
const am = {};
|
|
637
|
+
if (amValues.external_id) am['external_id'] = amValues.external_id;
|
|
638
|
+
if (amValues.em) am['em'] = yield sha256Hex(amValues.em);
|
|
639
|
+
if (amValues.fn) am['fn'] = amValues.fn;
|
|
640
|
+
if (amValues.ln) am['ln'] = amValues.ln;
|
|
641
|
+
if (amValues.ph) am['ph'] = amValues.ph;
|
|
642
|
+
if (amValues.ge) am['ge'] = amValues.ge;
|
|
643
|
+
if (amValues.db) am['db'] = amValues.db;
|
|
644
|
+
if (amValues.ct) am['ct'] = amValues.ct;
|
|
645
|
+
if (amValues.st) am['st'] = amValues.st;
|
|
646
|
+
if (amValues.zp) am['zp'] = amValues.zp;
|
|
647
|
+
if (amValues.country) am['country'] = amValues.country;
|
|
648
|
+
if (Object.keys(am).length > 0) {
|
|
649
|
+
window.fbq('init', config.pixelId, am);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
console.error('Error identifying user in Meta Pixel:', error);
|
|
635
654
|
}
|
|
636
|
-
}
|
|
655
|
+
})
|
|
637
656
|
};
|
|
638
657
|
}
|
|
639
658
|
function getFacebookEventName(eventName, eventMapping) {
|
|
659
|
+
// Convert default saasco page view event by default
|
|
660
|
+
if (eventName === 'Page View') {
|
|
661
|
+
const mappedEvent = 'ViewContent';
|
|
662
|
+
return mappedEvent;
|
|
663
|
+
}
|
|
640
664
|
if (!eventMapping) return eventName;
|
|
641
665
|
return eventMapping[eventName] || eventName;
|
|
642
666
|
}
|
|
@@ -664,6 +688,146 @@ function trackFacebookEvent(eventName, properties, eventMapping) {
|
|
|
664
688
|
window.fbq('track', fbEventName);
|
|
665
689
|
}
|
|
666
690
|
}
|
|
691
|
+
function coerceMetaConversionsAmValues(properties) {
|
|
692
|
+
const coerce = [{
|
|
693
|
+
key: 'em',
|
|
694
|
+
coerceFrom: ['email', 'user_email', 'userEmail', 'email_address', 'emailAddress', 'e_mail', 'E_Mail', 'mail', 'Mail', 'contact_email', 'contactEmail', 'primary_email', 'primaryEmail'],
|
|
695
|
+
transform: value => {
|
|
696
|
+
if (typeof value !== 'string') return undefined;
|
|
697
|
+
const normalized = value.trim().toLowerCase();
|
|
698
|
+
return normalized || undefined;
|
|
699
|
+
}
|
|
700
|
+
}, {
|
|
701
|
+
key: 'fn',
|
|
702
|
+
coerceFrom: ['first_name', 'firstName', 'firstname', 'given_name', 'givenName', 'user_first_name', 'userFirstName', 'name_first', 'nameFirst', 'f_name', 'fName'],
|
|
703
|
+
transform: value => {
|
|
704
|
+
if (typeof value !== 'string') return undefined;
|
|
705
|
+
const normalized = value.trim().toLowerCase();
|
|
706
|
+
return normalized || undefined;
|
|
707
|
+
}
|
|
708
|
+
}, {
|
|
709
|
+
key: 'ln',
|
|
710
|
+
coerceFrom: ['last_name', 'lastName', 'lastname', 'surname', 'user_last_name', 'userLastName', 'name_last', 'nameLast', 'l_name', 'lName'],
|
|
711
|
+
transform: value => {
|
|
712
|
+
if (typeof value !== 'string') return undefined;
|
|
713
|
+
const normalized = value.trim().toLowerCase();
|
|
714
|
+
return normalized || undefined;
|
|
715
|
+
}
|
|
716
|
+
}, {
|
|
717
|
+
key: 'ph',
|
|
718
|
+
coerceFrom: ['phone', 'phone_number', 'phoneNumber', 'mobile', 'mobile_number', 'mobileNumber', 'contact_number', 'contactNumber', 'tel', 'telephone'],
|
|
719
|
+
transform: value => {
|
|
720
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined;
|
|
721
|
+
const phoneStr = String(value).replace(/\D/g, '');
|
|
722
|
+
return phoneStr || undefined;
|
|
723
|
+
}
|
|
724
|
+
}, {
|
|
725
|
+
key: 'external_id',
|
|
726
|
+
coerceFrom: ['external_id', 'externalId', 'user_id', 'userId', 'id', 'distinctId', 'distinct_id', 'customer_id', 'customerId', 'member_id', 'memberId', 'loyalty_id', 'loyaltyId'],
|
|
727
|
+
transform: value => {
|
|
728
|
+
if (value == null) return undefined;
|
|
729
|
+
return String(value) || undefined;
|
|
730
|
+
}
|
|
731
|
+
}, {
|
|
732
|
+
key: 'ge',
|
|
733
|
+
coerceFrom: ['gender', 'user_gender', 'userGender', 'sex', 'user_sex', 'userSex'],
|
|
734
|
+
transform: value => {
|
|
735
|
+
if (typeof value !== 'string') return undefined;
|
|
736
|
+
const normalized = value.trim().toLowerCase();
|
|
737
|
+
if (normalized === 'female' || normalized === 'f') return 'f';
|
|
738
|
+
if (normalized === 'male' || normalized === 'm') return 'm';
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
}, {
|
|
742
|
+
key: 'db',
|
|
743
|
+
coerceFrom: ['birthday', 'birth_date', 'birthDate', 'date_of_birth', 'dateOfBirth', 'dob', 'Dob', 'birth_day', 'birthDay'],
|
|
744
|
+
transform: value => {
|
|
745
|
+
if (!value) return undefined;
|
|
746
|
+
let dateStr;
|
|
747
|
+
if (value instanceof Date) {
|
|
748
|
+
dateStr = value.toISOString().split('T')[0];
|
|
749
|
+
} else {
|
|
750
|
+
dateStr = String(value);
|
|
751
|
+
}
|
|
752
|
+
const dateMatch = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
753
|
+
if (dateMatch) {
|
|
754
|
+
const [, year, month, day] = dateMatch;
|
|
755
|
+
return `${year}${month}${day}`;
|
|
756
|
+
}
|
|
757
|
+
const numericMatch = dateStr.replace(/\D/g, '');
|
|
758
|
+
if (numericMatch.length === 8) {
|
|
759
|
+
return numericMatch;
|
|
760
|
+
}
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
}, {
|
|
764
|
+
key: 'ct',
|
|
765
|
+
coerceFrom: ['city', 'user_city', 'userCity', 'location_city', 'locationCity', 'address_city', 'addressCity'],
|
|
766
|
+
transform: value => {
|
|
767
|
+
if (typeof value !== 'string') return undefined;
|
|
768
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, '');
|
|
769
|
+
return normalized || undefined;
|
|
770
|
+
}
|
|
771
|
+
}, {
|
|
772
|
+
key: 'st',
|
|
773
|
+
coerceFrom: ['state', 'user_state', 'userState', 'province', 'user_province', 'userProvince', 'region', 'user_region', 'userRegion', 'location_state', 'locationState', 'address_state', 'addressState'],
|
|
774
|
+
transform: value => {
|
|
775
|
+
if (typeof value !== 'string') return undefined;
|
|
776
|
+
const normalized = value.trim().toLowerCase();
|
|
777
|
+
if (normalized.length === 2) return normalized;
|
|
778
|
+
return undefined;
|
|
779
|
+
}
|
|
780
|
+
}, {
|
|
781
|
+
key: 'zp',
|
|
782
|
+
coerceFrom: ['zip', 'zipcode', 'zip_code', 'postal_code', 'postalCode', 'postcode', 'user_zip', 'userZip', 'address_zip', 'addressZip'],
|
|
783
|
+
transform: value => {
|
|
784
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined;
|
|
785
|
+
return String(value).trim() || undefined;
|
|
786
|
+
}
|
|
787
|
+
}, {
|
|
788
|
+
key: 'country',
|
|
789
|
+
coerceFrom: ['country', 'country_code', 'countryCode', 'user_country', 'userCountry', 'location_country', 'locationCountry', 'address_country', 'addressCountry'],
|
|
790
|
+
transform: value => {
|
|
791
|
+
if (typeof value !== 'string') return undefined;
|
|
792
|
+
const normalized = value.trim().toLowerCase();
|
|
793
|
+
if (normalized.length === 2) return normalized;
|
|
794
|
+
return undefined;
|
|
795
|
+
}
|
|
796
|
+
}];
|
|
797
|
+
const result = {};
|
|
798
|
+
for (const {
|
|
799
|
+
key,
|
|
800
|
+
coerceFrom,
|
|
801
|
+
transform
|
|
802
|
+
} of coerce) {
|
|
803
|
+
const foundValue = findFirstProperty(properties, coerceFrom);
|
|
804
|
+
if (foundValue !== null && transform) {
|
|
805
|
+
const transformedValue = transform(foundValue);
|
|
806
|
+
if (transformedValue) {
|
|
807
|
+
result[key] = transformedValue;
|
|
808
|
+
}
|
|
809
|
+
} else if (foundValue !== null && typeof foundValue === 'string') {
|
|
810
|
+
result[key] = foundValue;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
function findFirstProperty(properties, keys) {
|
|
816
|
+
for (const key of keys) {
|
|
817
|
+
if (properties[key] !== undefined) {
|
|
818
|
+
return properties[key];
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
const toHex = buf => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('');
|
|
824
|
+
function sha256Hex(input) {
|
|
825
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
826
|
+
const enc = new TextEncoder().encode(input);
|
|
827
|
+
const digest = yield crypto.subtle.digest('SHA-256', enc);
|
|
828
|
+
return toHex(digest);
|
|
829
|
+
});
|
|
830
|
+
}
|
|
667
831
|
|
|
668
832
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
669
833
|
class AnalyticsLogger {
|
|
@@ -843,21 +1007,23 @@ class IntegrationManager {
|
|
|
843
1007
|
this.deliver(envelope);
|
|
844
1008
|
}
|
|
845
1009
|
deliver(evt) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1010
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1011
|
+
for (const {
|
|
1012
|
+
integration,
|
|
1013
|
+
status
|
|
1014
|
+
} of this.integrations.values()) {
|
|
1015
|
+
if (status !== 'ready') continue;
|
|
1016
|
+
try {
|
|
1017
|
+
if (evt.type === 'track' && integration.track && evt.name) {
|
|
1018
|
+
yield integration.track(evt.name, evt.properties, evt.context);
|
|
1019
|
+
} else if (evt.type === 'identify' && integration.identify) {
|
|
1020
|
+
yield integration.identify(evt.context.distinctId, evt.properties, evt.context);
|
|
1021
|
+
}
|
|
1022
|
+
} catch (e) {
|
|
1023
|
+
this.logger.error(`deliver error in ${integration.name} (${evt.type})`, e);
|
|
856
1024
|
}
|
|
857
|
-
} catch (e) {
|
|
858
|
-
this.logger.error(`deliver error in ${integration.name}`, e);
|
|
859
1025
|
}
|
|
860
|
-
}
|
|
1026
|
+
});
|
|
861
1027
|
}
|
|
862
1028
|
/**
|
|
863
1029
|
* Flush queued events FIFO once at least one integration is ready.
|
|
@@ -1152,6 +1318,10 @@ class Saasco {
|
|
|
1152
1318
|
});
|
|
1153
1319
|
// Set initial context
|
|
1154
1320
|
this.integrationManager.setContext({});
|
|
1321
|
+
// Initialize integrations immediately (works on both client and server)
|
|
1322
|
+
this.initIntegrations().catch(error => {
|
|
1323
|
+
this.logger.log('Error initializing integrations:', error);
|
|
1324
|
+
});
|
|
1155
1325
|
}
|
|
1156
1326
|
init() {
|
|
1157
1327
|
if (this.isInitialized) {
|
|
@@ -1167,10 +1337,6 @@ class Saasco {
|
|
|
1167
1337
|
this.logger.log('Saasco initialized', this.config);
|
|
1168
1338
|
if (this.config.debug) this.logger.log('Debug mode active. This will log all events to the console.');
|
|
1169
1339
|
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
|
-
});
|
|
1174
1340
|
this.initAutoPageTracking();
|
|
1175
1341
|
this.isInitialized = true;
|
|
1176
1342
|
}
|
|
@@ -1454,34 +1620,4 @@ const browserContextSchema = z.object({
|
|
|
1454
1620
|
$utmAdId: z.string().nullable()
|
|
1455
1621
|
});
|
|
1456
1622
|
|
|
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 };
|
|
1623
|
+
export { IntegrationManager, Logger, Saasco, browserContextSchema, createFacebookPixelIntegration, getBrowserContext, initializeFacebookPixel, timezones, trackFacebookEvent };
|
package/package.json
CHANGED
package/src/lib/analytics.d.ts
CHANGED
|
@@ -25,8 +25,9 @@ type DoRequestResponse = {
|
|
|
25
25
|
success: boolean;
|
|
26
26
|
message: string;
|
|
27
27
|
};
|
|
28
|
-
export type
|
|
28
|
+
export type IntegrationConfigBase = {
|
|
29
29
|
type: string;
|
|
30
|
+
config: Record<string, any>;
|
|
30
31
|
};
|
|
31
32
|
export type IntegrationsConfig = FacebookPixelIntegrationConfig[];
|
|
32
33
|
export declare class Saasco {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IntegrationConfigBase } from '../analytics';
|
|
2
2
|
import { Integration } from './integration-manager';
|
|
3
3
|
declare global {
|
|
4
4
|
interface Window {
|
|
@@ -9,7 +9,7 @@ declare global {
|
|
|
9
9
|
export type FacebookEventMapping = {
|
|
10
10
|
[key: string]: StandardFacebookEvent;
|
|
11
11
|
};
|
|
12
|
-
export type FacebookPixelIntegrationConfig =
|
|
12
|
+
export type FacebookPixelIntegrationConfig = IntegrationConfigBase & {
|
|
13
13
|
type: 'facebook-pixel';
|
|
14
14
|
config: FacebookPixelConfig;
|
|
15
15
|
};
|
|
@@ -34,7 +34,7 @@ export type Integration = {
|
|
|
34
34
|
/**
|
|
35
35
|
* The function to identify a user
|
|
36
36
|
*/
|
|
37
|
-
identify?: (userId?: string | null, properties?: Record<string, unknown>, context?: AnalyticsContext) => void
|
|
37
|
+
identify?: (userId?: string | null, properties?: Record<string, unknown>, context?: AnalyticsContext) => void | Promise<void>;
|
|
38
38
|
};
|
|
39
39
|
export type IntegrationStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
40
40
|
export type IntegrationState = {
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Integration } from './integration-manager';
|
|
2
|
-
export type ConsoleIntegrationConfig = {
|
|
3
|
-
prefix?: string;
|
|
4
|
-
};
|
|
5
|
-
/**
|
|
6
|
-
* Example integration that works on both client and server
|
|
7
|
-
* Simply logs events to console
|
|
8
|
-
*/
|
|
9
|
-
export declare function createConsoleIntegration(config?: ConsoleIntegrationConfig): Integration;
|