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 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.41";
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$1 = typeof window !== 'undefined';
559
- const isServer$1 = !isBrowser$1;
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$1) {
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('Analytics is disabled. No requests will be sent to the server.');
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
- // Send event to integrations
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.41";
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$1 = typeof window !== 'undefined';
555
- const isServer$1 = !isBrowser$1;
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$1) {
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('Analytics is disabled. No requests will be sent to the server.');
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
- // Send event to integrations
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saasco-sdk",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "dependencies": {
5
5
  "tslib": "^2.3.0",
6
6
  "@lukeed/uuid": "^2.0.1",
@@ -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;
@@ -1,2 +1,3 @@
1
1
  export * from './integration-manager';
2
2
  export * from './facebook-pixel';
3
+ export * from './tiktok-pixel';
@@ -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;