saasco-sdk 0.1.41 → 0.1.42
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 +291 -4
- package/index.esm.js +290 -5
- 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.42";
|
|
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 {
|
|
@@ -1334,6 +1615,10 @@ class Saasco {
|
|
|
1334
1615
|
this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1335
1616
|
const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
|
|
1336
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);
|
|
1337
1622
|
}
|
|
1338
1623
|
} catch (error) {
|
|
1339
1624
|
this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
|
|
@@ -1599,6 +1884,8 @@ exports.Logger = Logger;
|
|
|
1599
1884
|
exports.Saasco = Saasco;
|
|
1600
1885
|
exports.browserContextSchema = browserContextSchema;
|
|
1601
1886
|
exports.createFacebookPixelIntegration = createFacebookPixelIntegration;
|
|
1887
|
+
exports.createTikTokPixelIntegration = createTikTokPixelIntegration;
|
|
1602
1888
|
exports.getBrowserContext = getBrowserContext;
|
|
1603
1889
|
exports.standardFacebookEvents = standardFacebookEvents;
|
|
1890
|
+
exports.standardTikTokEvents = standardTikTokEvents;
|
|
1604
1891
|
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.42";
|
|
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 {
|
|
@@ -1330,6 +1611,10 @@ class Saasco {
|
|
|
1330
1611
|
this.logger.log('Registering Facebook Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1331
1612
|
const fbIntegration = createFacebookPixelIntegration(integrationConfig.config);
|
|
1332
1613
|
yield this.integrationManager.registerIntegration(fbIntegration);
|
|
1614
|
+
} else if (integrationConfig.type === 'tiktok-pixel') {
|
|
1615
|
+
this.logger.log('Registering TikTok Pixel integration with ID:', integrationConfig.config.pixelId);
|
|
1616
|
+
const tiktokIntegration = createTikTokPixelIntegration(integrationConfig.config);
|
|
1617
|
+
yield this.integrationManager.registerIntegration(tiktokIntegration);
|
|
1333
1618
|
}
|
|
1334
1619
|
} catch (error) {
|
|
1335
1620
|
this.logger.log(`Failed to register ${integrationConfig.type} integration:`, error);
|
|
@@ -1590,4 +1875,4 @@ const browserContextSchema = z.object({
|
|
|
1590
1875
|
$utmAdId: z.string().nullable()
|
|
1591
1876
|
});
|
|
1592
1877
|
|
|
1593
|
-
export { IntegrationManager, Logger, Saasco, browserContextSchema, createFacebookPixelIntegration, getBrowserContext, standardFacebookEvents, timezones };
|
|
1878
|
+
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;
|