saasco-sdk 0.1.41 → 0.1.43
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 +317 -12
- package/index.esm.js +316 -13
- package/package.json +1 -1
- package/src/lib/analytics.d.ts +2 -1
- package/src/lib/integrations/index.d.ts +1 -0
- package/src/lib/integrations/tiktok-pixel.d.ts +59 -0
package/index.cjs.js
CHANGED
|
@@ -8,7 +8,7 @@ var jsSha256 = require('js-sha256');
|
|
|
8
8
|
var uuid$1 = require('@lukeed/uuid');
|
|
9
9
|
var zod = require('zod');
|
|
10
10
|
|
|
11
|
-
var version = "0.1.
|
|
11
|
+
var version = "0.1.43";
|
|
12
12
|
|
|
13
13
|
const timezones = {
|
|
14
14
|
'Asia/Barnaul': 'RU',
|
|
@@ -555,8 +555,8 @@ function getBrowserContext() {
|
|
|
555
555
|
};
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
const isBrowser$
|
|
559
|
-
const isServer$
|
|
558
|
+
const isBrowser$2 = typeof window !== 'undefined';
|
|
559
|
+
const isServer$2 = !isBrowser$2;
|
|
560
560
|
const standardFacebookEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Donate', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
|
|
561
561
|
/**
|
|
562
562
|
* Create a Facebook Pixel integration instance
|
|
@@ -567,7 +567,7 @@ function createFacebookPixelIntegration(config) {
|
|
|
567
567
|
name: 'facebook-pixel',
|
|
568
568
|
environments: ['client'],
|
|
569
569
|
init: () => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
570
|
-
if (isServer$
|
|
570
|
+
if (isServer$2) {
|
|
571
571
|
// On server, Facebook Pixel is a no-op but doesn't throw
|
|
572
572
|
// This allows the integration to be registered but remain inactive
|
|
573
573
|
return;
|
|
@@ -646,6 +646,10 @@ function createFacebookPixelIntegration(config) {
|
|
|
646
646
|
const amValues = coerceMetaConversionsAmValues(Object.assign(Object.assign({}, properties || {}), userId ? {
|
|
647
647
|
external_id: userId
|
|
648
648
|
} : {}));
|
|
649
|
+
// Advanced Matching
|
|
650
|
+
// Add event to identify user and hash email field
|
|
651
|
+
// FB pixel handles hashing automatically
|
|
652
|
+
// https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
|
|
649
653
|
const am = {};
|
|
650
654
|
if (amValues.external_id) am['external_id'] = amValues.external_id;
|
|
651
655
|
if (amValues.em) am['em'] = jsSha256.sha256(amValues.em);
|
|
@@ -674,6 +678,18 @@ function getFacebookEventName(eventName, eventMapping) {
|
|
|
674
678
|
if (!eventMapping) return eventName;
|
|
675
679
|
return eventMapping[eventName] || eventName;
|
|
676
680
|
}
|
|
681
|
+
/**
|
|
682
|
+
* Get Advanced Matching values from properties
|
|
683
|
+
*
|
|
684
|
+
* Documentation for Pixel
|
|
685
|
+
* https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
|
|
686
|
+
*
|
|
687
|
+
* Documentation for Conversions API
|
|
688
|
+
* https://developers.secure.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
|
|
689
|
+
*
|
|
690
|
+
* @param properties
|
|
691
|
+
* @returns
|
|
692
|
+
*/
|
|
677
693
|
function coerceMetaConversionsAmValues(properties) {
|
|
678
694
|
const coerce = [{
|
|
679
695
|
key: 'em',
|
|
@@ -781,6 +797,214 @@ function coerceMetaConversionsAmValues(properties) {
|
|
|
781
797
|
}
|
|
782
798
|
}];
|
|
783
799
|
const result = {};
|
|
800
|
+
for (const {
|
|
801
|
+
key,
|
|
802
|
+
coerceFrom,
|
|
803
|
+
transform
|
|
804
|
+
} of coerce) {
|
|
805
|
+
const foundValue = findFirstProperty$1(properties, coerceFrom);
|
|
806
|
+
if (foundValue !== null && transform) {
|
|
807
|
+
const transformedValue = transform(foundValue);
|
|
808
|
+
if (transformedValue) {
|
|
809
|
+
result[key] = transformedValue;
|
|
810
|
+
}
|
|
811
|
+
} else if (foundValue !== null && typeof foundValue === 'string') {
|
|
812
|
+
result[key] = foundValue;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
function findFirstProperty$1(properties, keys) {
|
|
818
|
+
for (const key of keys) {
|
|
819
|
+
if (properties[key] !== undefined) {
|
|
820
|
+
return properties[key];
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* TikTok Pixel Integration
|
|
828
|
+
*
|
|
829
|
+
* This integration provides TikTok Pixel tracking capabilities including:
|
|
830
|
+
* - Event tracking for standard and custom events
|
|
831
|
+
* - Advanced matching with hashed user identification data
|
|
832
|
+
* - Automatic script loading and initialization
|
|
833
|
+
*
|
|
834
|
+
* Key Resources:
|
|
835
|
+
* - Advanced Matching: https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
|
|
836
|
+
* - Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
837
|
+
*/
|
|
838
|
+
const isBrowser$1 = typeof window !== 'undefined';
|
|
839
|
+
const isServer$1 = !isBrowser$1;
|
|
840
|
+
/**
|
|
841
|
+
* Standard TikTok events supported by the pixel
|
|
842
|
+
*
|
|
843
|
+
* Note: This list reflects TikTok's official standard events. Events like 'CompletePayment'
|
|
844
|
+
* are not standard events and should be tracked as custom events if needed.
|
|
845
|
+
*
|
|
846
|
+
* Documentation: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
847
|
+
*/
|
|
848
|
+
const standardTikTokEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'ApplicationApproval', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Download', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
|
|
849
|
+
/**
|
|
850
|
+
* Create a TikTok Pixel integration instance
|
|
851
|
+
*/
|
|
852
|
+
function createTikTokPixelIntegration(config) {
|
|
853
|
+
let isPixelReady = false;
|
|
854
|
+
return {
|
|
855
|
+
name: 'tiktok-pixel',
|
|
856
|
+
environments: ['client'],
|
|
857
|
+
init: () => tslib.__awaiter(this, void 0, void 0, function* () {
|
|
858
|
+
if (isServer$1) {
|
|
859
|
+
// On server, TikTok Pixel is a no-op but doesn't throw
|
|
860
|
+
// This allows the integration to be registered but remain inactive
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
// Check if TikTok Pixel is already loaded
|
|
864
|
+
if (window.ttq) {
|
|
865
|
+
console.warn('TikTok Pixel is already initialized');
|
|
866
|
+
isPixelReady = true;
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
// Initialize TikTok Pixel
|
|
870
|
+
return new Promise((resolve, reject) => {
|
|
871
|
+
try {
|
|
872
|
+
(function (w, d, t) {
|
|
873
|
+
w.TiktokAnalyticsObject = t;
|
|
874
|
+
const ttq = w[t] = w[t] || [];
|
|
875
|
+
ttq.methods = ['page', 'track', 'identify', 'instances', 'debug', 'on', 'off', 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie', 'holdConsent', 'revokeConsent', 'grantConsent'];
|
|
876
|
+
ttq.setAndDefer = function (t, e) {
|
|
877
|
+
t[e] = function () {
|
|
878
|
+
t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
|
|
879
|
+
};
|
|
880
|
+
};
|
|
881
|
+
for (let i = 0; i < ttq.methods.length; i++) ttq.setAndDefer(ttq, ttq.methods[i]);
|
|
882
|
+
ttq.instance = function (t) {
|
|
883
|
+
const e = ttq._i[t] || [];
|
|
884
|
+
for (let n = 0; n < ttq.methods.length; n++) ttq.setAndDefer(e, ttq.methods[n]);
|
|
885
|
+
return e;
|
|
886
|
+
};
|
|
887
|
+
ttq.load = function (e, n) {
|
|
888
|
+
const r = 'https://analytics.tiktok.com/i18n/pixel/events.js';
|
|
889
|
+
const o = n && n.partner;
|
|
890
|
+
ttq._i = ttq._i || {};
|
|
891
|
+
ttq._i[e] = [];
|
|
892
|
+
ttq._i[e]._u = r;
|
|
893
|
+
ttq._t = ttq._t || {};
|
|
894
|
+
ttq._t[e] = +new Date();
|
|
895
|
+
ttq._o = ttq._o || {};
|
|
896
|
+
ttq._o[e] = n || {};
|
|
897
|
+
const script = document.createElement('script');
|
|
898
|
+
script.type = 'text/javascript';
|
|
899
|
+
script.async = true;
|
|
900
|
+
script.src = r + '?sdkid=' + e + '&lib=' + t;
|
|
901
|
+
// Add load event listener
|
|
902
|
+
script.onload = () => {
|
|
903
|
+
isPixelReady = true;
|
|
904
|
+
resolve();
|
|
905
|
+
};
|
|
906
|
+
script.onerror = () => {
|
|
907
|
+
reject(new Error('Failed to load TikTok Pixel script'));
|
|
908
|
+
};
|
|
909
|
+
const firstScript = document.getElementsByTagName('script')[0];
|
|
910
|
+
firstScript.parentNode.insertBefore(script, firstScript);
|
|
911
|
+
};
|
|
912
|
+
// Load the pixel with the provided ID
|
|
913
|
+
ttq.load(config.pixelId, config.testMode ? {
|
|
914
|
+
test_mode: true
|
|
915
|
+
} : undefined);
|
|
916
|
+
ttq.page();
|
|
917
|
+
// If script loads synchronously, mark as ready
|
|
918
|
+
if (window.ttq && typeof window.ttq === 'function') {
|
|
919
|
+
isPixelReady = true;
|
|
920
|
+
resolve();
|
|
921
|
+
}
|
|
922
|
+
})(window, document, 'ttq');
|
|
923
|
+
} catch (error) {
|
|
924
|
+
reject(error);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
}),
|
|
928
|
+
track: (eventName, properties, context) => {
|
|
929
|
+
if (!isPixelReady || !window.ttq) return;
|
|
930
|
+
const ttEventName = getTikTokEventName(eventName, config.eventMapping);
|
|
931
|
+
// Handle PageView separately as it uses ttq.page()
|
|
932
|
+
if (ttEventName === 'PageView') {
|
|
933
|
+
window.ttq.page();
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
// Map parameters based on specific event requirements
|
|
937
|
+
const ttParams = mapTikTokEventParameters(ttEventName, properties);
|
|
938
|
+
if (ttParams && Object.keys(ttParams).length > 0) {
|
|
939
|
+
window.ttq.track(ttEventName, ttParams);
|
|
940
|
+
} else {
|
|
941
|
+
window.ttq.track(ttEventName);
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
identify: (userId, properties, context) => {
|
|
945
|
+
try {
|
|
946
|
+
if (typeof window !== 'undefined' && window.ttq) {
|
|
947
|
+
const identifyData = coerceTikTokIdentifyValues(Object.assign(Object.assign({}, properties || {}), userId ? {
|
|
948
|
+
external_id: userId
|
|
949
|
+
} : {}));
|
|
950
|
+
if (Object.keys(identifyData).length > 0) {
|
|
951
|
+
window.ttq.identify(identifyData);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} catch (error) {
|
|
955
|
+
console.error('Error identifying user in TikTok Pixel:', error);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function getTikTokEventName(eventName, eventMapping) {
|
|
961
|
+
// Convert default saasco Page View event by default
|
|
962
|
+
if (eventName === 'Page View') return 'PageView';
|
|
963
|
+
if (!eventMapping) return eventName;
|
|
964
|
+
return eventMapping[eventName] || eventName;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Get identification values from properties for TikTok Pixel
|
|
968
|
+
*
|
|
969
|
+
* Advanced Matching Documentation:
|
|
970
|
+
* https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
|
|
971
|
+
*
|
|
972
|
+
* Standard Events Documentation:
|
|
973
|
+
* https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
974
|
+
*
|
|
975
|
+
* @param properties
|
|
976
|
+
* @returns
|
|
977
|
+
*/
|
|
978
|
+
function coerceTikTokIdentifyValues(properties) {
|
|
979
|
+
const coerce = [{
|
|
980
|
+
key: 'email',
|
|
981
|
+
coerceFrom: ['email', 'user_email', 'userEmail', 'email_address', 'emailAddress', 'e_mail', 'E_Mail', 'mail', 'Mail', 'contact_email', 'contactEmail', 'primary_email', 'primaryEmail'],
|
|
982
|
+
transform: value => {
|
|
983
|
+
if (typeof value !== 'string') return undefined;
|
|
984
|
+
const normalized = value.trim().toLowerCase();
|
|
985
|
+
// TikTok requires hashed email
|
|
986
|
+
return normalized ? jsSha256.sha256(normalized) : undefined;
|
|
987
|
+
}
|
|
988
|
+
}, {
|
|
989
|
+
key: 'phone_number',
|
|
990
|
+
coerceFrom: ['phone', 'phone_number', 'phoneNumber', 'mobile', 'mobile_number', 'mobileNumber', 'contact_number', 'contactNumber', 'tel', 'telephone'],
|
|
991
|
+
transform: value => {
|
|
992
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined;
|
|
993
|
+
const phoneStr = String(value).replace(/\D/g, '');
|
|
994
|
+
// TikTok requires hashed phone number
|
|
995
|
+
return phoneStr ? jsSha256.sha256(phoneStr) : undefined;
|
|
996
|
+
}
|
|
997
|
+
}, {
|
|
998
|
+
key: 'external_id',
|
|
999
|
+
coerceFrom: ['external_id', 'externalId', 'user_id', 'userId', 'id', 'distinctId', 'distinct_id', 'customer_id', 'customerId', 'member_id', 'memberId'],
|
|
1000
|
+
transform: value => {
|
|
1001
|
+
if (value == null) return undefined;
|
|
1002
|
+
const stringValue = String(value);
|
|
1003
|
+
// TikTok requires hashed external_id
|
|
1004
|
+
return stringValue ? jsSha256.sha256(stringValue) : undefined;
|
|
1005
|
+
}
|
|
1006
|
+
}];
|
|
1007
|
+
const result = {};
|
|
784
1008
|
for (const {
|
|
785
1009
|
key,
|
|
786
1010
|
coerceFrom,
|
|
@@ -806,6 +1030,63 @@ function findFirstProperty(properties, keys) {
|
|
|
806
1030
|
}
|
|
807
1031
|
return null;
|
|
808
1032
|
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Maps event properties to TikTok-specific parameters based on event type
|
|
1035
|
+
*
|
|
1036
|
+
* Parameter Documentation: https://ads.tiktok.com/help/article/about-parameters
|
|
1037
|
+
* Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
1038
|
+
*/
|
|
1039
|
+
function mapTikTokEventParameters(eventName, properties) {
|
|
1040
|
+
if (!properties) return null;
|
|
1041
|
+
const params = {};
|
|
1042
|
+
// Helper to safely add parameters, skipping objects/null/undefined
|
|
1043
|
+
const addParam = (key, value) => {
|
|
1044
|
+
if (value != null && typeof value !== 'object') {
|
|
1045
|
+
params[key] = value;
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
switch (eventName) {
|
|
1049
|
+
case 'AddToCart':
|
|
1050
|
+
case 'Purchase':
|
|
1051
|
+
case 'ViewContent':
|
|
1052
|
+
addParam('content_type', properties['content_type']);
|
|
1053
|
+
addParam('content_ids', properties['content_ids']);
|
|
1054
|
+
addParam('quantity', properties['quantity']);
|
|
1055
|
+
addParam('description', properties['description']);
|
|
1056
|
+
addParam('currency', properties['currency']);
|
|
1057
|
+
addParam('value', properties['value']);
|
|
1058
|
+
addParam('content_name', properties['content_name']);
|
|
1059
|
+
addParam('content_category', properties['content_category']);
|
|
1060
|
+
break;
|
|
1061
|
+
case 'StartTrial':
|
|
1062
|
+
addParam('content_ids', properties['content_ids']);
|
|
1063
|
+
addParam('currency', properties['currency']);
|
|
1064
|
+
addParam('value', properties['value']);
|
|
1065
|
+
break;
|
|
1066
|
+
case 'Search':
|
|
1067
|
+
const searchString = properties['search_string'] || properties['query'] || properties['search_query'];
|
|
1068
|
+
addParam('search_string', searchString);
|
|
1069
|
+
break;
|
|
1070
|
+
case 'InitiateCheckout':
|
|
1071
|
+
case 'AddPaymentInfo':
|
|
1072
|
+
addParam('value', properties['value']);
|
|
1073
|
+
addParam('currency', properties['currency']);
|
|
1074
|
+
addParam('content_ids', properties['content_ids']);
|
|
1075
|
+
addParam('quantity', properties['quantity']);
|
|
1076
|
+
break;
|
|
1077
|
+
case 'CompleteRegistration':
|
|
1078
|
+
case 'Subscribe':
|
|
1079
|
+
addParam('value', properties['value']);
|
|
1080
|
+
addParam('currency', properties['currency']);
|
|
1081
|
+
break;
|
|
1082
|
+
default:
|
|
1083
|
+
// Pass through common parameters for any other events
|
|
1084
|
+
addParam('value', properties['value']);
|
|
1085
|
+
addParam('currency', properties['currency']);
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
1089
|
+
}
|
|
809
1090
|
|
|
810
1091
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
811
1092
|
class AnalyticsLogger {
|
|
@@ -1310,7 +1591,7 @@ class Saasco {
|
|
|
1310
1591
|
migrateFromLocalStorage();
|
|
1311
1592
|
this.logger.log('Saasco initialized', this.config);
|
|
1312
1593
|
if (this.config.debug) this.logger.log('Debug mode active. This will log all events to the console.');
|
|
1313
|
-
if (!this.config.enabled) this.logger.log(
|
|
1594
|
+
if (!this.config.enabled) this.logger.log(`Analytics is disabled. No requests will be sent to the server and no integrations will be initialized. ${this.config.debug ? 'Debug mode is active and will still log information.' : ''}`);
|
|
1314
1595
|
this.initAutoPageTracking();
|
|
1315
1596
|
this.isInitialized = true;
|
|
1316
1597
|
}
|
|
@@ -1328,12 +1609,20 @@ class Saasco {
|
|
|
1328
1609
|
initIntegrations() {
|
|
1329
1610
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
1330
1611
|
if (!this.config.integrations || this.config.integrations.length === 0) return;
|
|
1612
|
+
if (!this.config.enabled) {
|
|
1613
|
+
this.logger.log('Analytics is disabled. Skipping integration initialization.');
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1331
1616
|
for (const integrationConfig of this.config.integrations) {
|
|
1332
1617
|
try {
|
|
1333
1618
|
if (integrationConfig.type === 'facebook-pixel') {
|
|
1334
1619
|
this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1335
1620
|
const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
|
|
1336
1621
|
yield this.integrationManager.registerIntegration(fbIntegration);
|
|
1622
|
+
} else if (integrationConfig.type === 'tiktok-pixel') {
|
|
1623
|
+
this.logger.log('Registering TikTok Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1624
|
+
const tiktokIntegration = createTikTokPixelIntegration(integrationConfig.config);
|
|
1625
|
+
yield this.integrationManager.registerIntegration(tiktokIntegration);
|
|
1337
1626
|
}
|
|
1338
1627
|
} catch (error) {
|
|
1339
1628
|
this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
|
|
@@ -1389,7 +1678,18 @@ class Saasco {
|
|
|
1389
1678
|
source: isBrowser ? 'client' : 'server',
|
|
1390
1679
|
context: JSON.stringify(context || {})
|
|
1391
1680
|
};
|
|
1392
|
-
|
|
1681
|
+
if (data.action === 'Page View') {
|
|
1682
|
+
this.logger.log('Page View', window.location.href, data);
|
|
1683
|
+
} else {
|
|
1684
|
+
this.logger.log('track', data);
|
|
1685
|
+
}
|
|
1686
|
+
if (!this.config.enabled) {
|
|
1687
|
+
return Promise.resolve({
|
|
1688
|
+
success: true,
|
|
1689
|
+
message: 'Analytics is disabled'
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
// Only send track to integrations if analytics is enabled
|
|
1393
1693
|
this.integrationManager.track(action, properties);
|
|
1394
1694
|
// Send event to API
|
|
1395
1695
|
return this.doRequest('events', data);
|
|
@@ -1450,7 +1750,6 @@ class Saasco {
|
|
|
1450
1750
|
anonymousId,
|
|
1451
1751
|
sessionId
|
|
1452
1752
|
});
|
|
1453
|
-
this.integrationManager.identify(distinctId, properties);
|
|
1454
1753
|
// No distinctId provided so we don't track the user
|
|
1455
1754
|
if (!distinctId) return Promise.resolve({
|
|
1456
1755
|
success: true,
|
|
@@ -1467,6 +1766,15 @@ class Saasco {
|
|
|
1467
1766
|
payload: properties || {},
|
|
1468
1767
|
context: context || {}
|
|
1469
1768
|
};
|
|
1769
|
+
this.logger.log('identify', data);
|
|
1770
|
+
// Only send identify to integrations if analytics is enabled
|
|
1771
|
+
if (!this.config.enabled) {
|
|
1772
|
+
return Promise.resolve({
|
|
1773
|
+
success: true,
|
|
1774
|
+
message: 'Analytics is disabled'
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
this.integrationManager.identify(distinctId, properties);
|
|
1470
1778
|
return this.doRequest('identify', data);
|
|
1471
1779
|
}
|
|
1472
1780
|
/**
|
|
@@ -1491,11 +1799,6 @@ class Saasco {
|
|
|
1491
1799
|
return tslib.__awaiter(this, void 0, void 0, function* () {
|
|
1492
1800
|
const base = this.config.proxy || 'https://www.saasco.com/api/';
|
|
1493
1801
|
const url = `${base}${path}`;
|
|
1494
|
-
if (data.action === 'Page View') {
|
|
1495
|
-
this.logger.log('Page View', window.location.href, data);
|
|
1496
|
-
} else {
|
|
1497
|
-
this.logger.log(data.action || path, data);
|
|
1498
|
-
}
|
|
1499
1802
|
// If analytics is disabled, don't send the request
|
|
1500
1803
|
if (this.config.enabled === false) return {
|
|
1501
1804
|
success: false,
|
|
@@ -1599,6 +1902,8 @@ exports.Logger = Logger;
|
|
|
1599
1902
|
exports.Saasco = Saasco;
|
|
1600
1903
|
exports.browserContextSchema = browserContextSchema;
|
|
1601
1904
|
exports.createFacebookPixelIntegration = createFacebookPixelIntegration;
|
|
1905
|
+
exports.createTikTokPixelIntegration = createTikTokPixelIntegration;
|
|
1602
1906
|
exports.getBrowserContext = getBrowserContext;
|
|
1603
1907
|
exports.standardFacebookEvents = standardFacebookEvents;
|
|
1908
|
+
exports.standardTikTokEvents = standardTikTokEvents;
|
|
1604
1909
|
exports.timezones = timezones;
|
package/index.esm.js
CHANGED
|
@@ -4,7 +4,7 @@ import { sha256 } from 'js-sha256';
|
|
|
4
4
|
import { v4 } from '@lukeed/uuid';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
-
var version = "0.1.
|
|
7
|
+
var version = "0.1.43";
|
|
8
8
|
|
|
9
9
|
const timezones = {
|
|
10
10
|
'Asia/Barnaul': 'RU',
|
|
@@ -551,8 +551,8 @@ function getBrowserContext() {
|
|
|
551
551
|
};
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
-
const isBrowser$
|
|
555
|
-
const isServer$
|
|
554
|
+
const isBrowser$2 = typeof window !== 'undefined';
|
|
555
|
+
const isServer$2 = !isBrowser$2;
|
|
556
556
|
const standardFacebookEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Donate', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
|
|
557
557
|
/**
|
|
558
558
|
* Create a Facebook Pixel integration instance
|
|
@@ -563,7 +563,7 @@ function createFacebookPixelIntegration(config) {
|
|
|
563
563
|
name: 'facebook-pixel',
|
|
564
564
|
environments: ['client'],
|
|
565
565
|
init: () => __awaiter(this, void 0, void 0, function* () {
|
|
566
|
-
if (isServer$
|
|
566
|
+
if (isServer$2) {
|
|
567
567
|
// On server, Facebook Pixel is a no-op but doesn't throw
|
|
568
568
|
// This allows the integration to be registered but remain inactive
|
|
569
569
|
return;
|
|
@@ -642,6 +642,10 @@ function createFacebookPixelIntegration(config) {
|
|
|
642
642
|
const amValues = coerceMetaConversionsAmValues(Object.assign(Object.assign({}, properties || {}), userId ? {
|
|
643
643
|
external_id: userId
|
|
644
644
|
} : {}));
|
|
645
|
+
// Advanced Matching
|
|
646
|
+
// Add event to identify user and hash email field
|
|
647
|
+
// FB pixel handles hashing automatically
|
|
648
|
+
// https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
|
|
645
649
|
const am = {};
|
|
646
650
|
if (amValues.external_id) am['external_id'] = amValues.external_id;
|
|
647
651
|
if (amValues.em) am['em'] = sha256(amValues.em);
|
|
@@ -670,6 +674,18 @@ function getFacebookEventName(eventName, eventMapping) {
|
|
|
670
674
|
if (!eventMapping) return eventName;
|
|
671
675
|
return eventMapping[eventName] || eventName;
|
|
672
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Get Advanced Matching values from properties
|
|
679
|
+
*
|
|
680
|
+
* Documentation for Pixel
|
|
681
|
+
* https://developers.facebook.com/docs/meta-pixel/advanced/advanced-matching/
|
|
682
|
+
*
|
|
683
|
+
* Documentation for Conversions API
|
|
684
|
+
* https://developers.secure.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters
|
|
685
|
+
*
|
|
686
|
+
* @param properties
|
|
687
|
+
* @returns
|
|
688
|
+
*/
|
|
673
689
|
function coerceMetaConversionsAmValues(properties) {
|
|
674
690
|
const coerce = [{
|
|
675
691
|
key: 'em',
|
|
@@ -777,6 +793,214 @@ function coerceMetaConversionsAmValues(properties) {
|
|
|
777
793
|
}
|
|
778
794
|
}];
|
|
779
795
|
const result = {};
|
|
796
|
+
for (const {
|
|
797
|
+
key,
|
|
798
|
+
coerceFrom,
|
|
799
|
+
transform
|
|
800
|
+
} of coerce) {
|
|
801
|
+
const foundValue = findFirstProperty$1(properties, coerceFrom);
|
|
802
|
+
if (foundValue !== null && transform) {
|
|
803
|
+
const transformedValue = transform(foundValue);
|
|
804
|
+
if (transformedValue) {
|
|
805
|
+
result[key] = transformedValue;
|
|
806
|
+
}
|
|
807
|
+
} else if (foundValue !== null && typeof foundValue === 'string') {
|
|
808
|
+
result[key] = foundValue;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return result;
|
|
812
|
+
}
|
|
813
|
+
function findFirstProperty$1(properties, keys) {
|
|
814
|
+
for (const key of keys) {
|
|
815
|
+
if (properties[key] !== undefined) {
|
|
816
|
+
return properties[key];
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* TikTok Pixel Integration
|
|
824
|
+
*
|
|
825
|
+
* This integration provides TikTok Pixel tracking capabilities including:
|
|
826
|
+
* - Event tracking for standard and custom events
|
|
827
|
+
* - Advanced matching with hashed user identification data
|
|
828
|
+
* - Automatic script loading and initialization
|
|
829
|
+
*
|
|
830
|
+
* Key Resources:
|
|
831
|
+
* - Advanced Matching: https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
|
|
832
|
+
* - Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
833
|
+
*/
|
|
834
|
+
const isBrowser$1 = typeof window !== 'undefined';
|
|
835
|
+
const isServer$1 = !isBrowser$1;
|
|
836
|
+
/**
|
|
837
|
+
* Standard TikTok events supported by the pixel
|
|
838
|
+
*
|
|
839
|
+
* Note: This list reflects TikTok's official standard events. Events like 'CompletePayment'
|
|
840
|
+
* are not standard events and should be tracked as custom events if needed.
|
|
841
|
+
*
|
|
842
|
+
* Documentation: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
843
|
+
*/
|
|
844
|
+
const standardTikTokEvents = ['AddPaymentInfo', 'AddToCart', 'AddToWishlist', 'ApplicationApproval', 'CompleteRegistration', 'Contact', 'CustomizeProduct', 'Download', 'FindLocation', 'InitiateCheckout', 'Lead', 'Purchase', 'Schedule', 'Search', 'StartTrial', 'SubmitApplication', 'Subscribe', 'ViewContent', 'PageView'];
|
|
845
|
+
/**
|
|
846
|
+
* Create a TikTok Pixel integration instance
|
|
847
|
+
*/
|
|
848
|
+
function createTikTokPixelIntegration(config) {
|
|
849
|
+
let isPixelReady = false;
|
|
850
|
+
return {
|
|
851
|
+
name: 'tiktok-pixel',
|
|
852
|
+
environments: ['client'],
|
|
853
|
+
init: () => __awaiter(this, void 0, void 0, function* () {
|
|
854
|
+
if (isServer$1) {
|
|
855
|
+
// On server, TikTok Pixel is a no-op but doesn't throw
|
|
856
|
+
// This allows the integration to be registered but remain inactive
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
// Check if TikTok Pixel is already loaded
|
|
860
|
+
if (window.ttq) {
|
|
861
|
+
console.warn('TikTok Pixel is already initialized');
|
|
862
|
+
isPixelReady = true;
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
// Initialize TikTok Pixel
|
|
866
|
+
return new Promise((resolve, reject) => {
|
|
867
|
+
try {
|
|
868
|
+
(function (w, d, t) {
|
|
869
|
+
w.TiktokAnalyticsObject = t;
|
|
870
|
+
const ttq = w[t] = w[t] || [];
|
|
871
|
+
ttq.methods = ['page', 'track', 'identify', 'instances', 'debug', 'on', 'off', 'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie', 'holdConsent', 'revokeConsent', 'grantConsent'];
|
|
872
|
+
ttq.setAndDefer = function (t, e) {
|
|
873
|
+
t[e] = function () {
|
|
874
|
+
t.push([e].concat(Array.prototype.slice.call(arguments, 0)));
|
|
875
|
+
};
|
|
876
|
+
};
|
|
877
|
+
for (let i = 0; i < ttq.methods.length; i++) ttq.setAndDefer(ttq, ttq.methods[i]);
|
|
878
|
+
ttq.instance = function (t) {
|
|
879
|
+
const e = ttq._i[t] || [];
|
|
880
|
+
for (let n = 0; n < ttq.methods.length; n++) ttq.setAndDefer(e, ttq.methods[n]);
|
|
881
|
+
return e;
|
|
882
|
+
};
|
|
883
|
+
ttq.load = function (e, n) {
|
|
884
|
+
const r = 'https://analytics.tiktok.com/i18n/pixel/events.js';
|
|
885
|
+
const o = n && n.partner;
|
|
886
|
+
ttq._i = ttq._i || {};
|
|
887
|
+
ttq._i[e] = [];
|
|
888
|
+
ttq._i[e]._u = r;
|
|
889
|
+
ttq._t = ttq._t || {};
|
|
890
|
+
ttq._t[e] = +new Date();
|
|
891
|
+
ttq._o = ttq._o || {};
|
|
892
|
+
ttq._o[e] = n || {};
|
|
893
|
+
const script = document.createElement('script');
|
|
894
|
+
script.type = 'text/javascript';
|
|
895
|
+
script.async = true;
|
|
896
|
+
script.src = r + '?sdkid=' + e + '&lib=' + t;
|
|
897
|
+
// Add load event listener
|
|
898
|
+
script.onload = () => {
|
|
899
|
+
isPixelReady = true;
|
|
900
|
+
resolve();
|
|
901
|
+
};
|
|
902
|
+
script.onerror = () => {
|
|
903
|
+
reject(new Error('Failed to load TikTok Pixel script'));
|
|
904
|
+
};
|
|
905
|
+
const firstScript = document.getElementsByTagName('script')[0];
|
|
906
|
+
firstScript.parentNode.insertBefore(script, firstScript);
|
|
907
|
+
};
|
|
908
|
+
// Load the pixel with the provided ID
|
|
909
|
+
ttq.load(config.pixelId, config.testMode ? {
|
|
910
|
+
test_mode: true
|
|
911
|
+
} : undefined);
|
|
912
|
+
ttq.page();
|
|
913
|
+
// If script loads synchronously, mark as ready
|
|
914
|
+
if (window.ttq && typeof window.ttq === 'function') {
|
|
915
|
+
isPixelReady = true;
|
|
916
|
+
resolve();
|
|
917
|
+
}
|
|
918
|
+
})(window, document, 'ttq');
|
|
919
|
+
} catch (error) {
|
|
920
|
+
reject(error);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
}),
|
|
924
|
+
track: (eventName, properties, context) => {
|
|
925
|
+
if (!isPixelReady || !window.ttq) return;
|
|
926
|
+
const ttEventName = getTikTokEventName(eventName, config.eventMapping);
|
|
927
|
+
// Handle PageView separately as it uses ttq.page()
|
|
928
|
+
if (ttEventName === 'PageView') {
|
|
929
|
+
window.ttq.page();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
// Map parameters based on specific event requirements
|
|
933
|
+
const ttParams = mapTikTokEventParameters(ttEventName, properties);
|
|
934
|
+
if (ttParams && Object.keys(ttParams).length > 0) {
|
|
935
|
+
window.ttq.track(ttEventName, ttParams);
|
|
936
|
+
} else {
|
|
937
|
+
window.ttq.track(ttEventName);
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
identify: (userId, properties, context) => {
|
|
941
|
+
try {
|
|
942
|
+
if (typeof window !== 'undefined' && window.ttq) {
|
|
943
|
+
const identifyData = coerceTikTokIdentifyValues(Object.assign(Object.assign({}, properties || {}), userId ? {
|
|
944
|
+
external_id: userId
|
|
945
|
+
} : {}));
|
|
946
|
+
if (Object.keys(identifyData).length > 0) {
|
|
947
|
+
window.ttq.identify(identifyData);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} catch (error) {
|
|
951
|
+
console.error('Error identifying user in TikTok Pixel:', error);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
function getTikTokEventName(eventName, eventMapping) {
|
|
957
|
+
// Convert default saasco Page View event by default
|
|
958
|
+
if (eventName === 'Page View') return 'PageView';
|
|
959
|
+
if (!eventMapping) return eventName;
|
|
960
|
+
return eventMapping[eventName] || eventName;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Get identification values from properties for TikTok Pixel
|
|
964
|
+
*
|
|
965
|
+
* Advanced Matching Documentation:
|
|
966
|
+
* https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
|
|
967
|
+
*
|
|
968
|
+
* Standard Events Documentation:
|
|
969
|
+
* https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
970
|
+
*
|
|
971
|
+
* @param properties
|
|
972
|
+
* @returns
|
|
973
|
+
*/
|
|
974
|
+
function coerceTikTokIdentifyValues(properties) {
|
|
975
|
+
const coerce = [{
|
|
976
|
+
key: 'email',
|
|
977
|
+
coerceFrom: ['email', 'user_email', 'userEmail', 'email_address', 'emailAddress', 'e_mail', 'E_Mail', 'mail', 'Mail', 'contact_email', 'contactEmail', 'primary_email', 'primaryEmail'],
|
|
978
|
+
transform: value => {
|
|
979
|
+
if (typeof value !== 'string') return undefined;
|
|
980
|
+
const normalized = value.trim().toLowerCase();
|
|
981
|
+
// TikTok requires hashed email
|
|
982
|
+
return normalized ? sha256(normalized) : undefined;
|
|
983
|
+
}
|
|
984
|
+
}, {
|
|
985
|
+
key: 'phone_number',
|
|
986
|
+
coerceFrom: ['phone', 'phone_number', 'phoneNumber', 'mobile', 'mobile_number', 'mobileNumber', 'contact_number', 'contactNumber', 'tel', 'telephone'],
|
|
987
|
+
transform: value => {
|
|
988
|
+
if (typeof value !== 'string' && typeof value !== 'number') return undefined;
|
|
989
|
+
const phoneStr = String(value).replace(/\D/g, '');
|
|
990
|
+
// TikTok requires hashed phone number
|
|
991
|
+
return phoneStr ? sha256(phoneStr) : undefined;
|
|
992
|
+
}
|
|
993
|
+
}, {
|
|
994
|
+
key: 'external_id',
|
|
995
|
+
coerceFrom: ['external_id', 'externalId', 'user_id', 'userId', 'id', 'distinctId', 'distinct_id', 'customer_id', 'customerId', 'member_id', 'memberId'],
|
|
996
|
+
transform: value => {
|
|
997
|
+
if (value == null) return undefined;
|
|
998
|
+
const stringValue = String(value);
|
|
999
|
+
// TikTok requires hashed external_id
|
|
1000
|
+
return stringValue ? sha256(stringValue) : undefined;
|
|
1001
|
+
}
|
|
1002
|
+
}];
|
|
1003
|
+
const result = {};
|
|
780
1004
|
for (const {
|
|
781
1005
|
key,
|
|
782
1006
|
coerceFrom,
|
|
@@ -802,6 +1026,63 @@ function findFirstProperty(properties, keys) {
|
|
|
802
1026
|
}
|
|
803
1027
|
return null;
|
|
804
1028
|
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Maps event properties to TikTok-specific parameters based on event type
|
|
1031
|
+
*
|
|
1032
|
+
* Parameter Documentation: https://ads.tiktok.com/help/article/about-parameters
|
|
1033
|
+
* Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
1034
|
+
*/
|
|
1035
|
+
function mapTikTokEventParameters(eventName, properties) {
|
|
1036
|
+
if (!properties) return null;
|
|
1037
|
+
const params = {};
|
|
1038
|
+
// Helper to safely add parameters, skipping objects/null/undefined
|
|
1039
|
+
const addParam = (key, value) => {
|
|
1040
|
+
if (value != null && typeof value !== 'object') {
|
|
1041
|
+
params[key] = value;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
switch (eventName) {
|
|
1045
|
+
case 'AddToCart':
|
|
1046
|
+
case 'Purchase':
|
|
1047
|
+
case 'ViewContent':
|
|
1048
|
+
addParam('content_type', properties['content_type']);
|
|
1049
|
+
addParam('content_ids', properties['content_ids']);
|
|
1050
|
+
addParam('quantity', properties['quantity']);
|
|
1051
|
+
addParam('description', properties['description']);
|
|
1052
|
+
addParam('currency', properties['currency']);
|
|
1053
|
+
addParam('value', properties['value']);
|
|
1054
|
+
addParam('content_name', properties['content_name']);
|
|
1055
|
+
addParam('content_category', properties['content_category']);
|
|
1056
|
+
break;
|
|
1057
|
+
case 'StartTrial':
|
|
1058
|
+
addParam('content_ids', properties['content_ids']);
|
|
1059
|
+
addParam('currency', properties['currency']);
|
|
1060
|
+
addParam('value', properties['value']);
|
|
1061
|
+
break;
|
|
1062
|
+
case 'Search':
|
|
1063
|
+
const searchString = properties['search_string'] || properties['query'] || properties['search_query'];
|
|
1064
|
+
addParam('search_string', searchString);
|
|
1065
|
+
break;
|
|
1066
|
+
case 'InitiateCheckout':
|
|
1067
|
+
case 'AddPaymentInfo':
|
|
1068
|
+
addParam('value', properties['value']);
|
|
1069
|
+
addParam('currency', properties['currency']);
|
|
1070
|
+
addParam('content_ids', properties['content_ids']);
|
|
1071
|
+
addParam('quantity', properties['quantity']);
|
|
1072
|
+
break;
|
|
1073
|
+
case 'CompleteRegistration':
|
|
1074
|
+
case 'Subscribe':
|
|
1075
|
+
addParam('value', properties['value']);
|
|
1076
|
+
addParam('currency', properties['currency']);
|
|
1077
|
+
break;
|
|
1078
|
+
default:
|
|
1079
|
+
// Pass through common parameters for any other events
|
|
1080
|
+
addParam('value', properties['value']);
|
|
1081
|
+
addParam('currency', properties['currency']);
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
1085
|
+
}
|
|
805
1086
|
|
|
806
1087
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
807
1088
|
class AnalyticsLogger {
|
|
@@ -1306,7 +1587,7 @@ class Saasco {
|
|
|
1306
1587
|
migrateFromLocalStorage();
|
|
1307
1588
|
this.logger.log('Saasco initialized', this.config);
|
|
1308
1589
|
if (this.config.debug) this.logger.log('Debug mode active. This will log all events to the console.');
|
|
1309
|
-
if (!this.config.enabled) this.logger.log(
|
|
1590
|
+
if (!this.config.enabled) this.logger.log(`Analytics is disabled. No requests will be sent to the server and no integrations will be initialized. ${this.config.debug ? 'Debug mode is active and will still log information.' : ''}`);
|
|
1310
1591
|
this.initAutoPageTracking();
|
|
1311
1592
|
this.isInitialized = true;
|
|
1312
1593
|
}
|
|
@@ -1324,12 +1605,20 @@ class Saasco {
|
|
|
1324
1605
|
initIntegrations() {
|
|
1325
1606
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1326
1607
|
if (!this.config.integrations || this.config.integrations.length === 0) return;
|
|
1608
|
+
if (!this.config.enabled) {
|
|
1609
|
+
this.logger.log('Analytics is disabled. Skipping integration initialization.');
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1327
1612
|
for (const integrationConfig of this.config.integrations) {
|
|
1328
1613
|
try {
|
|
1329
1614
|
if (integrationConfig.type === 'facebook-pixel') {
|
|
1330
1615
|
this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1331
1616
|
const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
|
|
1332
1617
|
yield this.integrationManager.registerIntegration(fbIntegration);
|
|
1618
|
+
} else if (integrationConfig.type === 'tiktok-pixel') {
|
|
1619
|
+
this.logger.log('Registering TikTok Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1620
|
+
const tiktokIntegration = createTikTokPixelIntegration(integrationConfig.config);
|
|
1621
|
+
yield this.integrationManager.registerIntegration(tiktokIntegration);
|
|
1333
1622
|
}
|
|
1334
1623
|
} catch (error) {
|
|
1335
1624
|
this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
|
|
@@ -1385,7 +1674,18 @@ class Saasco {
|
|
|
1385
1674
|
source: isBrowser ? 'client' : 'server',
|
|
1386
1675
|
context: JSON.stringify(context || {})
|
|
1387
1676
|
};
|
|
1388
|
-
|
|
1677
|
+
if (data.action === 'Page View') {
|
|
1678
|
+
this.logger.log('Page View', window.location.href, data);
|
|
1679
|
+
} else {
|
|
1680
|
+
this.logger.log('track', data);
|
|
1681
|
+
}
|
|
1682
|
+
if (!this.config.enabled) {
|
|
1683
|
+
return Promise.resolve({
|
|
1684
|
+
success: true,
|
|
1685
|
+
message: 'Analytics is disabled'
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
// Only send track to integrations if analytics is enabled
|
|
1389
1689
|
this.integrationManager.track(action, properties);
|
|
1390
1690
|
// Send event to API
|
|
1391
1691
|
return this.doRequest('events', data);
|
|
@@ -1446,7 +1746,6 @@ class Saasco {
|
|
|
1446
1746
|
anonymousId,
|
|
1447
1747
|
sessionId
|
|
1448
1748
|
});
|
|
1449
|
-
this.integrationManager.identify(distinctId, properties);
|
|
1450
1749
|
// No distinctId provided so we don't track the user
|
|
1451
1750
|
if (!distinctId) return Promise.resolve({
|
|
1452
1751
|
success: true,
|
|
@@ -1463,6 +1762,15 @@ class Saasco {
|
|
|
1463
1762
|
payload: properties || {},
|
|
1464
1763
|
context: context || {}
|
|
1465
1764
|
};
|
|
1765
|
+
this.logger.log('identify', data);
|
|
1766
|
+
// Only send identify to integrations if analytics is enabled
|
|
1767
|
+
if (!this.config.enabled) {
|
|
1768
|
+
return Promise.resolve({
|
|
1769
|
+
success: true,
|
|
1770
|
+
message: 'Analytics is disabled'
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
this.integrationManager.identify(distinctId, properties);
|
|
1466
1774
|
return this.doRequest('identify', data);
|
|
1467
1775
|
}
|
|
1468
1776
|
/**
|
|
@@ -1487,11 +1795,6 @@ class Saasco {
|
|
|
1487
1795
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1488
1796
|
const base = this.config.proxy || 'https://www.saasco.com/api/';
|
|
1489
1797
|
const url = `${base}${path}`;
|
|
1490
|
-
if (data.action === 'Page View') {
|
|
1491
|
-
this.logger.log('Page View', window.location.href, data);
|
|
1492
|
-
} else {
|
|
1493
|
-
this.logger.log(data.action || path, data);
|
|
1494
|
-
}
|
|
1495
1798
|
// If analytics is disabled, don't send the request
|
|
1496
1799
|
if (this.config.enabled === false) return {
|
|
1497
1800
|
success: false,
|
|
@@ -1590,4 +1893,4 @@ const browserContextSchema = z.object({
|
|
|
1590
1893
|
$utmAdId: z.string().nullable()
|
|
1591
1894
|
});
|
|
1592
1895
|
|
|
1593
|
-
export { IntegrationManager, Logger, Saasco, browserContextSchema, createFacebookPixelIntegration, getBrowserContext, standardFacebookEvents, timezones };
|
|
1896
|
+
export { IntegrationManager, Logger, Saasco, browserContextSchema, createFacebookPixelIntegration, createTikTokPixelIntegration, getBrowserContext, standardFacebookEvents, standardTikTokEvents, timezones };
|
package/package.json
CHANGED
package/src/lib/analytics.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FacebookPixelIntegrationConfig } from './integrations/facebook-pixel';
|
|
2
|
+
import { TikTokPixelIntegrationConfig } from './integrations/tiktok-pixel';
|
|
2
3
|
declare global {
|
|
3
4
|
interface Window {
|
|
4
5
|
saascoAutoPageTrackingActive?: boolean;
|
|
@@ -29,7 +30,7 @@ export type IntegrationConfigBase = {
|
|
|
29
30
|
type: string;
|
|
30
31
|
config: Record<string, any>;
|
|
31
32
|
};
|
|
32
|
-
export type IntegrationsConfig = FacebookPixelIntegrationConfig[];
|
|
33
|
+
export type IntegrationsConfig = (FacebookPixelIntegrationConfig | TikTokPixelIntegrationConfig)[];
|
|
33
34
|
export declare class Saasco {
|
|
34
35
|
private config;
|
|
35
36
|
private lastPageViewPath;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TikTok Pixel Integration
|
|
3
|
+
*
|
|
4
|
+
* This integration provides TikTok Pixel tracking capabilities including:
|
|
5
|
+
* - Event tracking for standard and custom events
|
|
6
|
+
* - Advanced matching with hashed user identification data
|
|
7
|
+
* - Automatic script loading and initialization
|
|
8
|
+
*
|
|
9
|
+
* Key Resources:
|
|
10
|
+
* - Advanced Matching: https://business-api.tiktok.com/portal/docs?rid=5ipocbxyw8v&id=1739585700402178
|
|
11
|
+
* - Standard Events: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
12
|
+
*/
|
|
13
|
+
import { IntegrationConfigBase } from '../analytics';
|
|
14
|
+
import { Integration } from './integration-manager';
|
|
15
|
+
declare global {
|
|
16
|
+
interface Window {
|
|
17
|
+
ttq?: Ttq;
|
|
18
|
+
TiktokAnalyticsObject?: string;
|
|
19
|
+
}
|
|
20
|
+
interface Ttq {
|
|
21
|
+
(...args: any[]): void;
|
|
22
|
+
methods?: string[];
|
|
23
|
+
_i?: Record<string, any>;
|
|
24
|
+
_t?: Record<string, number>;
|
|
25
|
+
_o?: Record<string, any>;
|
|
26
|
+
load?: (pixelId: string, options?: any) => void;
|
|
27
|
+
page?: () => void;
|
|
28
|
+
track?: (eventName: string, properties?: Record<string, unknown>) => void;
|
|
29
|
+
identify?: (properties?: Record<string, unknown>) => void;
|
|
30
|
+
setAndDefer?: (obj: any, method: string) => void;
|
|
31
|
+
instance?: (pixelId: string) => any;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export type TikTokEventMapping = {
|
|
35
|
+
[key: string]: StandardTikTokEvent;
|
|
36
|
+
};
|
|
37
|
+
export type TikTokPixelIntegrationConfig = IntegrationConfigBase & {
|
|
38
|
+
type: 'tiktok-pixel';
|
|
39
|
+
config: TikTokPixelConfig;
|
|
40
|
+
};
|
|
41
|
+
export type TikTokPixelConfig = {
|
|
42
|
+
pixelId: string;
|
|
43
|
+
eventMapping?: TikTokEventMapping;
|
|
44
|
+
testMode?: boolean;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Standard TikTok events supported by the pixel
|
|
48
|
+
*
|
|
49
|
+
* Note: This list reflects TikTok's official standard events. Events like 'CompletePayment'
|
|
50
|
+
* are not standard events and should be tracked as custom events if needed.
|
|
51
|
+
*
|
|
52
|
+
* Documentation: https://business-api.tiktok.com/portal/docs?id=1771101186666498
|
|
53
|
+
*/
|
|
54
|
+
export declare const standardTikTokEvents: readonly ["AddPaymentInfo", "AddToCart", "AddToWishlist", "ApplicationApproval", "CompleteRegistration", "Contact", "CustomizeProduct", "Download", "FindLocation", "InitiateCheckout", "Lead", "Purchase", "Schedule", "Search", "StartTrial", "SubmitApplication", "Subscribe", "ViewContent", "PageView"];
|
|
55
|
+
export type StandardTikTokEvent = (typeof standardTikTokEvents)[number];
|
|
56
|
+
/**
|
|
57
|
+
* Create a TikTok Pixel integration instance
|
|
58
|
+
*/
|
|
59
|
+
export declare function createTikTokPixelIntegration(config: TikTokPixelConfig): Integration;
|