@clianta/sdk 1.2.0 → 1.4.0

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/dist/react.cjs.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Clianta SDK v1.2.0
2
+ * Clianta SDK v1.4.0
3
3
  * (c) 2026 Clianta
4
4
  * Released under the MIT License.
5
5
  */
@@ -13,7 +13,7 @@ var react = require('react');
13
13
  * @see SDK_VERSION in core/config.ts
14
14
  */
15
15
  /** SDK Version */
16
- const SDK_VERSION = '1.2.0';
16
+ const SDK_VERSION = '1.4.0';
17
17
  /** Default API endpoint based on environment */
18
18
  const getDefaultApiEndpoint = () => {
19
19
  if (typeof window === 'undefined')
@@ -33,13 +33,13 @@ const DEFAULT_PLUGINS = [
33
33
  'engagement',
34
34
  'downloads',
35
35
  'exitIntent',
36
- 'popupForms',
37
36
  ];
38
37
  /** Default configuration values */
39
38
  const DEFAULT_CONFIG = {
40
39
  projectId: '',
41
40
  apiEndpoint: getDefaultApiEndpoint(),
42
41
  authToken: '',
42
+ apiKey: '',
43
43
  debug: false,
44
44
  autoPageView: true,
45
45
  plugins: DEFAULT_PLUGINS,
@@ -187,12 +187,39 @@ class Transport {
187
187
  return this.send(url, payload);
188
188
  }
189
189
  /**
190
- * Send identify request
190
+ * Send identify request.
191
+ * Returns contactId from the server response so the Tracker can store it.
191
192
  */
192
193
  async sendIdentify(data) {
193
194
  const url = `${this.config.apiEndpoint}/api/public/track/identify`;
194
- const payload = JSON.stringify(data);
195
- return this.send(url, payload);
195
+ try {
196
+ const response = await this.fetchWithTimeout(url, {
197
+ method: 'POST',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify(data),
200
+ keepalive: true,
201
+ });
202
+ const body = await response.json().catch(() => ({}));
203
+ if (response.ok) {
204
+ logger.debug('Identify successful, contactId:', body.contactId);
205
+ return {
206
+ success: true,
207
+ status: response.status,
208
+ contactId: body.contactId ?? undefined,
209
+ };
210
+ }
211
+ if (response.status >= 500) {
212
+ logger.warn(`Identify server error (${response.status})`);
213
+ }
214
+ else {
215
+ logger.error(`Identify failed with status ${response.status}:`, body.message);
216
+ }
217
+ return { success: false, status: response.status };
218
+ }
219
+ catch (error) {
220
+ logger.error('Identify request failed:', error);
221
+ return { success: false, error: error };
222
+ }
196
223
  }
197
224
  /**
198
225
  * Send events synchronously (for page unload)
@@ -591,6 +618,10 @@ class EventQueue {
591
618
  this.isFlushing = false;
592
619
  /** Rate limiting: timestamps of recent events */
593
620
  this.eventTimestamps = [];
621
+ /** Unload handler references for cleanup */
622
+ this.boundBeforeUnload = null;
623
+ this.boundVisibilityChange = null;
624
+ this.boundPageHide = null;
594
625
  this.transport = transport;
595
626
  this.config = {
596
627
  batchSize: config.batchSize ?? 10,
@@ -705,13 +736,25 @@ class EventQueue {
705
736
  this.persistQueue([]);
706
737
  }
707
738
  /**
708
- * Stop the flush timer
739
+ * Stop the flush timer and cleanup handlers
709
740
  */
710
741
  destroy() {
711
742
  if (this.flushTimer) {
712
743
  clearInterval(this.flushTimer);
713
744
  this.flushTimer = null;
714
745
  }
746
+ // Remove unload handlers
747
+ if (typeof window !== 'undefined') {
748
+ if (this.boundBeforeUnload) {
749
+ window.removeEventListener('beforeunload', this.boundBeforeUnload);
750
+ }
751
+ if (this.boundVisibilityChange) {
752
+ window.removeEventListener('visibilitychange', this.boundVisibilityChange);
753
+ }
754
+ if (this.boundPageHide) {
755
+ window.removeEventListener('pagehide', this.boundPageHide);
756
+ }
757
+ }
715
758
  }
716
759
  /**
717
760
  * Start auto-flush timer
@@ -731,19 +774,18 @@ class EventQueue {
731
774
  if (typeof window === 'undefined')
732
775
  return;
733
776
  // Flush on page unload
734
- window.addEventListener('beforeunload', () => {
735
- this.flushSync();
736
- });
777
+ this.boundBeforeUnload = () => this.flushSync();
778
+ window.addEventListener('beforeunload', this.boundBeforeUnload);
737
779
  // Flush when page becomes hidden
738
- window.addEventListener('visibilitychange', () => {
780
+ this.boundVisibilityChange = () => {
739
781
  if (document.visibilityState === 'hidden') {
740
782
  this.flushSync();
741
783
  }
742
- });
784
+ };
785
+ window.addEventListener('visibilitychange', this.boundVisibilityChange);
743
786
  // Flush on page hide (iOS Safari)
744
- window.addEventListener('pagehide', () => {
745
- this.flushSync();
746
- });
787
+ this.boundPageHide = () => this.flushSync();
788
+ window.addEventListener('pagehide', this.boundPageHide);
747
789
  }
748
790
  /**
749
791
  * Persist queue to localStorage
@@ -886,6 +928,10 @@ class ScrollPlugin extends BasePlugin {
886
928
  this.pageLoadTime = 0;
887
929
  this.scrollTimeout = null;
888
930
  this.boundHandler = null;
931
+ /** SPA navigation support */
932
+ this.originalPushState = null;
933
+ this.originalReplaceState = null;
934
+ this.popstateHandler = null;
889
935
  }
890
936
  init(tracker) {
891
937
  super.init(tracker);
@@ -893,6 +939,8 @@ class ScrollPlugin extends BasePlugin {
893
939
  if (typeof window !== 'undefined') {
894
940
  this.boundHandler = this.handleScroll.bind(this);
895
941
  window.addEventListener('scroll', this.boundHandler, { passive: true });
942
+ // Setup SPA navigation reset
943
+ this.setupNavigationReset();
896
944
  }
897
945
  }
898
946
  destroy() {
@@ -902,8 +950,53 @@ class ScrollPlugin extends BasePlugin {
902
950
  if (this.scrollTimeout) {
903
951
  clearTimeout(this.scrollTimeout);
904
952
  }
953
+ // Restore original history methods
954
+ if (this.originalPushState) {
955
+ history.pushState = this.originalPushState;
956
+ this.originalPushState = null;
957
+ }
958
+ if (this.originalReplaceState) {
959
+ history.replaceState = this.originalReplaceState;
960
+ this.originalReplaceState = null;
961
+ }
962
+ // Remove popstate listener
963
+ if (this.popstateHandler && typeof window !== 'undefined') {
964
+ window.removeEventListener('popstate', this.popstateHandler);
965
+ this.popstateHandler = null;
966
+ }
905
967
  super.destroy();
906
968
  }
969
+ /**
970
+ * Reset scroll tracking for SPA navigation
971
+ */
972
+ resetForNavigation() {
973
+ this.milestonesReached.clear();
974
+ this.maxScrollDepth = 0;
975
+ this.pageLoadTime = Date.now();
976
+ }
977
+ /**
978
+ * Setup History API interception for SPA navigation
979
+ */
980
+ setupNavigationReset() {
981
+ if (typeof window === 'undefined')
982
+ return;
983
+ // Store originals for cleanup
984
+ this.originalPushState = history.pushState;
985
+ this.originalReplaceState = history.replaceState;
986
+ // Intercept pushState and replaceState
987
+ const self = this;
988
+ history.pushState = function (...args) {
989
+ self.originalPushState.apply(history, args);
990
+ self.resetForNavigation();
991
+ };
992
+ history.replaceState = function (...args) {
993
+ self.originalReplaceState.apply(history, args);
994
+ self.resetForNavigation();
995
+ };
996
+ // Handle back/forward navigation
997
+ this.popstateHandler = () => this.resetForNavigation();
998
+ window.addEventListener('popstate', this.popstateHandler);
999
+ }
907
1000
  handleScroll() {
908
1001
  // Debounce scroll tracking
909
1002
  if (this.scrollTimeout) {
@@ -956,6 +1049,7 @@ class FormsPlugin extends BasePlugin {
956
1049
  this.trackedForms = new WeakSet();
957
1050
  this.formInteractions = new Set();
958
1051
  this.observer = null;
1052
+ this.listeners = [];
959
1053
  }
960
1054
  init(tracker) {
961
1055
  super.init(tracker);
@@ -974,8 +1068,20 @@ class FormsPlugin extends BasePlugin {
974
1068
  this.observer.disconnect();
975
1069
  this.observer = null;
976
1070
  }
1071
+ // Remove all tracked event listeners
1072
+ for (const { element, event, handler } of this.listeners) {
1073
+ element.removeEventListener(event, handler);
1074
+ }
1075
+ this.listeners = [];
977
1076
  super.destroy();
978
1077
  }
1078
+ /**
1079
+ * Track event listener for cleanup
1080
+ */
1081
+ addListener(element, event, handler) {
1082
+ element.addEventListener(event, handler);
1083
+ this.listeners.push({ element, event, handler });
1084
+ }
979
1085
  trackAllForms() {
980
1086
  document.querySelectorAll('form').forEach((form) => {
981
1087
  this.setupFormTracking(form);
@@ -1001,7 +1107,7 @@ class FormsPlugin extends BasePlugin {
1001
1107
  if (!field.name || field.type === 'submit' || field.type === 'button')
1002
1108
  return;
1003
1109
  ['focus', 'blur', 'change'].forEach((eventType) => {
1004
- field.addEventListener(eventType, () => {
1110
+ const handler = () => {
1005
1111
  const key = `${formId}-${field.name}-${eventType}`;
1006
1112
  if (!this.formInteractions.has(key)) {
1007
1113
  this.formInteractions.add(key);
@@ -1012,12 +1118,13 @@ class FormsPlugin extends BasePlugin {
1012
1118
  interactionType: eventType,
1013
1119
  });
1014
1120
  }
1015
- });
1121
+ };
1122
+ this.addListener(field, eventType, handler);
1016
1123
  });
1017
1124
  }
1018
1125
  });
1019
1126
  // Track form submission
1020
- form.addEventListener('submit', () => {
1127
+ const submitHandler = () => {
1021
1128
  this.track('form_submit', 'Form Submitted', {
1022
1129
  formId,
1023
1130
  action: form.action,
@@ -1025,7 +1132,8 @@ class FormsPlugin extends BasePlugin {
1025
1132
  });
1026
1133
  // Auto-identify if email field found
1027
1134
  this.autoIdentify(form);
1028
- });
1135
+ };
1136
+ this.addListener(form, 'submit', submitHandler);
1029
1137
  }
1030
1138
  autoIdentify(form) {
1031
1139
  const emailField = form.querySelector('input[type="email"], input[name*="email"]');
@@ -1109,6 +1217,7 @@ class EngagementPlugin extends BasePlugin {
1109
1217
  this.engagementTimeout = null;
1110
1218
  this.boundMarkEngaged = null;
1111
1219
  this.boundTrackTimeOnPage = null;
1220
+ this.boundVisibilityHandler = null;
1112
1221
  }
1113
1222
  init(tracker) {
1114
1223
  super.init(tracker);
@@ -1119,12 +1228,7 @@ class EngagementPlugin extends BasePlugin {
1119
1228
  // Setup engagement detection
1120
1229
  this.boundMarkEngaged = this.markEngaged.bind(this);
1121
1230
  this.boundTrackTimeOnPage = this.trackTimeOnPage.bind(this);
1122
- ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1123
- document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1124
- });
1125
- // Track time on page before unload
1126
- window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1127
- window.addEventListener('visibilitychange', () => {
1231
+ this.boundVisibilityHandler = () => {
1128
1232
  if (document.visibilityState === 'hidden') {
1129
1233
  this.trackTimeOnPage();
1130
1234
  }
@@ -1132,7 +1236,13 @@ class EngagementPlugin extends BasePlugin {
1132
1236
  // Reset engagement timer when page becomes visible again
1133
1237
  this.engagementStartTime = Date.now();
1134
1238
  }
1239
+ };
1240
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1241
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1135
1242
  });
1243
+ // Track time on page before unload
1244
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1245
+ document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1136
1246
  }
1137
1247
  destroy() {
1138
1248
  if (this.boundMarkEngaged && typeof document !== 'undefined') {
@@ -1143,6 +1253,9 @@ class EngagementPlugin extends BasePlugin {
1143
1253
  if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1144
1254
  window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1145
1255
  }
1256
+ if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1257
+ document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1258
+ }
1146
1259
  if (this.engagementTimeout) {
1147
1260
  clearTimeout(this.engagementTimeout);
1148
1261
  }
@@ -1187,20 +1300,69 @@ class DownloadsPlugin extends BasePlugin {
1187
1300
  this.name = 'downloads';
1188
1301
  this.trackedDownloads = new Set();
1189
1302
  this.boundHandler = null;
1303
+ /** SPA navigation support */
1304
+ this.originalPushState = null;
1305
+ this.originalReplaceState = null;
1306
+ this.popstateHandler = null;
1190
1307
  }
1191
1308
  init(tracker) {
1192
1309
  super.init(tracker);
1193
1310
  if (typeof document !== 'undefined') {
1194
1311
  this.boundHandler = this.handleClick.bind(this);
1195
1312
  document.addEventListener('click', this.boundHandler, true);
1313
+ // Setup SPA navigation reset
1314
+ this.setupNavigationReset();
1196
1315
  }
1197
1316
  }
1198
1317
  destroy() {
1199
1318
  if (this.boundHandler && typeof document !== 'undefined') {
1200
1319
  document.removeEventListener('click', this.boundHandler, true);
1201
1320
  }
1321
+ // Restore original history methods
1322
+ if (this.originalPushState) {
1323
+ history.pushState = this.originalPushState;
1324
+ this.originalPushState = null;
1325
+ }
1326
+ if (this.originalReplaceState) {
1327
+ history.replaceState = this.originalReplaceState;
1328
+ this.originalReplaceState = null;
1329
+ }
1330
+ // Remove popstate listener
1331
+ if (this.popstateHandler && typeof window !== 'undefined') {
1332
+ window.removeEventListener('popstate', this.popstateHandler);
1333
+ this.popstateHandler = null;
1334
+ }
1202
1335
  super.destroy();
1203
1336
  }
1337
+ /**
1338
+ * Reset download tracking for SPA navigation
1339
+ */
1340
+ resetForNavigation() {
1341
+ this.trackedDownloads.clear();
1342
+ }
1343
+ /**
1344
+ * Setup History API interception for SPA navigation
1345
+ */
1346
+ setupNavigationReset() {
1347
+ if (typeof window === 'undefined')
1348
+ return;
1349
+ // Store originals for cleanup
1350
+ this.originalPushState = history.pushState;
1351
+ this.originalReplaceState = history.replaceState;
1352
+ // Intercept pushState and replaceState
1353
+ const self = this;
1354
+ history.pushState = function (...args) {
1355
+ self.originalPushState.apply(history, args);
1356
+ self.resetForNavigation();
1357
+ };
1358
+ history.replaceState = function (...args) {
1359
+ self.originalReplaceState.apply(history, args);
1360
+ self.resetForNavigation();
1361
+ };
1362
+ // Handle back/forward navigation
1363
+ this.popstateHandler = () => this.resetForNavigation();
1364
+ window.addEventListener('popstate', this.popstateHandler);
1365
+ }
1204
1366
  handleClick(e) {
1205
1367
  const link = e.target.closest('a');
1206
1368
  if (!link || !link.href)
@@ -1326,16 +1488,33 @@ class PerformancePlugin extends BasePlugin {
1326
1488
  constructor() {
1327
1489
  super(...arguments);
1328
1490
  this.name = 'performance';
1491
+ this.boundLoadHandler = null;
1492
+ this.observers = [];
1493
+ this.boundClsVisibilityHandler = null;
1329
1494
  }
1330
1495
  init(tracker) {
1331
1496
  super.init(tracker);
1332
1497
  if (typeof window !== 'undefined') {
1333
1498
  // Track performance after page load
1334
- window.addEventListener('load', () => {
1499
+ this.boundLoadHandler = () => {
1335
1500
  // Delay to ensure all metrics are available
1336
1501
  setTimeout(() => this.trackPerformance(), 100);
1337
- });
1502
+ };
1503
+ window.addEventListener('load', this.boundLoadHandler);
1504
+ }
1505
+ }
1506
+ destroy() {
1507
+ if (this.boundLoadHandler && typeof window !== 'undefined') {
1508
+ window.removeEventListener('load', this.boundLoadHandler);
1338
1509
  }
1510
+ for (const observer of this.observers) {
1511
+ observer.disconnect();
1512
+ }
1513
+ this.observers = [];
1514
+ if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
1515
+ window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
1516
+ }
1517
+ super.destroy();
1339
1518
  }
1340
1519
  trackPerformance() {
1341
1520
  if (typeof performance === 'undefined')
@@ -1393,6 +1572,7 @@ class PerformancePlugin extends BasePlugin {
1393
1572
  }
1394
1573
  });
1395
1574
  lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1575
+ this.observers.push(lcpObserver);
1396
1576
  }
1397
1577
  catch {
1398
1578
  // LCP not supported
@@ -1410,6 +1590,7 @@ class PerformancePlugin extends BasePlugin {
1410
1590
  }
1411
1591
  });
1412
1592
  fidObserver.observe({ type: 'first-input', buffered: true });
1593
+ this.observers.push(fidObserver);
1413
1594
  }
1414
1595
  catch {
1415
1596
  // FID not supported
@@ -1426,15 +1607,17 @@ class PerformancePlugin extends BasePlugin {
1426
1607
  });
1427
1608
  });
1428
1609
  clsObserver.observe({ type: 'layout-shift', buffered: true });
1610
+ this.observers.push(clsObserver);
1429
1611
  // Report CLS after page is hidden
1430
- window.addEventListener('visibilitychange', () => {
1612
+ this.boundClsVisibilityHandler = () => {
1431
1613
  if (document.visibilityState === 'hidden' && clsValue > 0) {
1432
1614
  this.track('performance', 'Web Vital - CLS', {
1433
1615
  metric: 'CLS',
1434
1616
  value: Math.round(clsValue * 1000) / 1000,
1435
1617
  });
1436
1618
  }
1437
- }, { once: true });
1619
+ };
1620
+ window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
1438
1621
  }
1439
1622
  catch {
1440
1623
  // CLS not supported
@@ -1745,7 +1928,7 @@ class PopupFormsPlugin extends BasePlugin {
1745
1928
  label.appendChild(requiredMark);
1746
1929
  }
1747
1930
  fieldWrapper.appendChild(label);
1748
- // Input/Textarea
1931
+ // Input/Textarea/Select
1749
1932
  if (field.type === 'textarea') {
1750
1933
  const textarea = document.createElement('textarea');
1751
1934
  textarea.name = field.name;
@@ -1756,6 +1939,38 @@ class PopupFormsPlugin extends BasePlugin {
1756
1939
  textarea.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px; box-sizing: border-box;';
1757
1940
  fieldWrapper.appendChild(textarea);
1758
1941
  }
1942
+ else if (field.type === 'select') {
1943
+ const select = document.createElement('select');
1944
+ select.name = field.name;
1945
+ if (field.required)
1946
+ select.required = true;
1947
+ select.style.cssText = 'width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box; background: white; cursor: pointer;';
1948
+ // Add placeholder option
1949
+ if (field.placeholder) {
1950
+ const placeholderOption = document.createElement('option');
1951
+ placeholderOption.value = '';
1952
+ placeholderOption.textContent = field.placeholder;
1953
+ placeholderOption.disabled = true;
1954
+ placeholderOption.selected = true;
1955
+ select.appendChild(placeholderOption);
1956
+ }
1957
+ // Add options from field.options array if provided
1958
+ if (field.options && Array.isArray(field.options)) {
1959
+ field.options.forEach((opt) => {
1960
+ const option = document.createElement('option');
1961
+ if (typeof opt === 'string') {
1962
+ option.value = opt;
1963
+ option.textContent = opt;
1964
+ }
1965
+ else {
1966
+ option.value = opt.value;
1967
+ option.textContent = opt.label;
1968
+ }
1969
+ select.appendChild(option);
1970
+ });
1971
+ }
1972
+ fieldWrapper.appendChild(select);
1973
+ }
1759
1974
  else {
1760
1975
  const input = document.createElement('input');
1761
1976
  input.type = field.type;
@@ -1789,96 +2004,6 @@ class PopupFormsPlugin extends BasePlugin {
1789
2004
  formElement.appendChild(submitBtn);
1790
2005
  container.appendChild(formElement);
1791
2006
  }
1792
- buildFormHTML(form) {
1793
- const style = form.style || {};
1794
- const primaryColor = style.primaryColor || '#10B981';
1795
- const textColor = style.textColor || '#18181B';
1796
- let fieldsHTML = form.fields.map(field => {
1797
- const requiredMark = field.required ? '<span style="color: #EF4444;">*</span>' : '';
1798
- if (field.type === 'textarea') {
1799
- return `
1800
- <div style="margin-bottom: 12px;">
1801
- <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1802
- ${field.label} ${requiredMark}
1803
- </label>
1804
- <textarea
1805
- name="${field.name}"
1806
- placeholder="${field.placeholder || ''}"
1807
- ${field.required ? 'required' : ''}
1808
- style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; resize: vertical; min-height: 80px;"
1809
- ></textarea>
1810
- </div>
1811
- `;
1812
- }
1813
- else if (field.type === 'checkbox') {
1814
- return `
1815
- <div style="margin-bottom: 12px;">
1816
- <label style="display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${textColor}; cursor: pointer;">
1817
- <input
1818
- type="checkbox"
1819
- name="${field.name}"
1820
- ${field.required ? 'required' : ''}
1821
- style="width: 16px; height: 16px;"
1822
- />
1823
- ${field.label} ${requiredMark}
1824
- </label>
1825
- </div>
1826
- `;
1827
- }
1828
- else {
1829
- return `
1830
- <div style="margin-bottom: 12px;">
1831
- <label style="display: block; font-size: 14px; font-weight: 500; margin-bottom: 4px; color: ${textColor};">
1832
- ${field.label} ${requiredMark}
1833
- </label>
1834
- <input
1835
- type="${field.type}"
1836
- name="${field.name}"
1837
- placeholder="${field.placeholder || ''}"
1838
- ${field.required ? 'required' : ''}
1839
- style="width: 100%; padding: 8px 12px; border: 1px solid #E4E4E7; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
1840
- />
1841
- </div>
1842
- `;
1843
- }
1844
- }).join('');
1845
- return `
1846
- <button id="clianta-form-close" style="
1847
- position: absolute;
1848
- top: 12px;
1849
- right: 12px;
1850
- background: none;
1851
- border: none;
1852
- font-size: 20px;
1853
- cursor: pointer;
1854
- color: #71717A;
1855
- padding: 4px;
1856
- ">&times;</button>
1857
- <h2 style="font-size: 20px; font-weight: 700; margin-bottom: 8px; color: ${textColor};">
1858
- ${form.headline || 'Stay in touch'}
1859
- </h2>
1860
- <p style="font-size: 14px; color: #71717A; margin-bottom: 16px;">
1861
- ${form.subheadline || 'Get the latest updates'}
1862
- </p>
1863
- <form id="clianta-form-element">
1864
- ${fieldsHTML}
1865
- <button type="submit" style="
1866
- width: 100%;
1867
- padding: 10px 16px;
1868
- background: ${primaryColor};
1869
- color: white;
1870
- border: none;
1871
- border-radius: 6px;
1872
- font-size: 14px;
1873
- font-weight: 500;
1874
- cursor: pointer;
1875
- margin-top: 8px;
1876
- ">
1877
- ${form.submitButtonText || 'Subscribe'}
1878
- </button>
1879
- </form>
1880
- `;
1881
- }
1882
2007
  setupFormEvents(form, overlay, container) {
1883
2008
  // Close button
1884
2009
  const closeBtn = container.querySelector('#clianta-form-close');
@@ -2269,310 +2394,436 @@ class ConsentManager {
2269
2394
  }
2270
2395
 
2271
2396
  /**
2272
- * Clianta SDK - Main Tracker Class
2273
- * @see SDK_VERSION in core/config.ts
2397
+ * Clianta SDK - Event Triggers Manager
2398
+ * Manages event-driven automation and email notifications
2274
2399
  */
2275
2400
  /**
2276
- * Main Clianta Tracker Class
2401
+ * Event Triggers Manager
2402
+ * Handles event-driven automation based on CRM actions
2403
+ *
2404
+ * Similar to:
2405
+ * - Salesforce: Process Builder, Flow Automation
2406
+ * - HubSpot: Workflows, Email Sequences
2407
+ * - Pipedrive: Workflow Automation
2277
2408
  */
2278
- class Tracker {
2279
- constructor(workspaceId, userConfig = {}) {
2280
- this.plugins = [];
2281
- this.isInitialized = false;
2282
- if (!workspaceId) {
2283
- throw new Error('[Clianta] Workspace ID is required');
2284
- }
2409
+ class EventTriggersManager {
2410
+ constructor(apiEndpoint, workspaceId, authToken) {
2411
+ this.triggers = new Map();
2412
+ this.listeners = new Map();
2413
+ this.apiEndpoint = apiEndpoint;
2285
2414
  this.workspaceId = workspaceId;
2286
- this.config = mergeConfig(userConfig);
2287
- // Setup debug mode
2288
- logger.enabled = this.config.debug;
2289
- logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
2290
- // Initialize consent manager
2291
- this.consentManager = new ConsentManager({
2292
- ...this.config.consent,
2293
- onConsentChange: (state, previous) => {
2294
- this.onConsentChange(state, previous);
2295
- },
2296
- });
2297
- // Initialize transport and queue
2298
- this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
2299
- this.queue = new EventQueue(this.transport, {
2300
- batchSize: this.config.batchSize,
2301
- flushInterval: this.config.flushInterval,
2302
- });
2303
- // Get or create visitor and session IDs based on mode
2304
- this.visitorId = this.createVisitorId();
2305
- this.sessionId = this.createSessionId();
2306
- logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
2307
- // Initialize plugins
2308
- this.initPlugins();
2309
- this.isInitialized = true;
2310
- logger.info('SDK initialized successfully');
2415
+ this.authToken = authToken;
2311
2416
  }
2312
2417
  /**
2313
- * Create visitor ID based on storage mode
2418
+ * Set authentication token
2314
2419
  */
2315
- createVisitorId() {
2316
- // Anonymous mode: use temporary ID until consent
2317
- if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
2318
- const key = STORAGE_KEYS.VISITOR_ID + '_anon';
2319
- let anonId = getSessionStorage(key);
2320
- if (!anonId) {
2321
- anonId = 'anon_' + generateUUID();
2322
- setSessionStorage(key, anonId);
2323
- }
2324
- return anonId;
2420
+ setAuthToken(token) {
2421
+ this.authToken = token;
2422
+ }
2423
+ /**
2424
+ * Make authenticated API request
2425
+ */
2426
+ async request(endpoint, options = {}) {
2427
+ const url = `${this.apiEndpoint}${endpoint}`;
2428
+ const headers = {
2429
+ 'Content-Type': 'application/json',
2430
+ ...(options.headers || {}),
2431
+ };
2432
+ if (this.authToken) {
2433
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2325
2434
  }
2326
- // Cookie-less mode: use sessionStorage only
2327
- if (this.config.cookielessMode) {
2328
- let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
2329
- if (!visitorId) {
2330
- visitorId = generateUUID();
2331
- setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
2435
+ try {
2436
+ const response = await fetch(url, {
2437
+ ...options,
2438
+ headers,
2439
+ });
2440
+ const data = await response.json();
2441
+ if (!response.ok) {
2442
+ return {
2443
+ success: false,
2444
+ error: data.message || 'Request failed',
2445
+ status: response.status,
2446
+ };
2332
2447
  }
2333
- return visitorId;
2448
+ return {
2449
+ success: true,
2450
+ data: data.data || data,
2451
+ status: response.status,
2452
+ };
2334
2453
  }
2335
- // Normal mode
2336
- return getOrCreateVisitorId(this.config.useCookies);
2454
+ catch (error) {
2455
+ return {
2456
+ success: false,
2457
+ error: error instanceof Error ? error.message : 'Network error',
2458
+ status: 0,
2459
+ };
2460
+ }
2461
+ }
2462
+ // ============================================
2463
+ // TRIGGER MANAGEMENT
2464
+ // ============================================
2465
+ /**
2466
+ * Get all event triggers
2467
+ */
2468
+ async getTriggers() {
2469
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2337
2470
  }
2338
2471
  /**
2339
- * Create session ID
2472
+ * Get a single trigger by ID
2340
2473
  */
2341
- createSessionId() {
2342
- return getOrCreateSessionId(this.config.sessionTimeout);
2474
+ async getTrigger(triggerId) {
2475
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2343
2476
  }
2344
2477
  /**
2345
- * Handle consent state changes
2478
+ * Create a new event trigger
2346
2479
  */
2347
- onConsentChange(state, previous) {
2348
- logger.debug('Consent changed:', { from: previous, to: state });
2349
- // If analytics consent was just granted
2350
- if (state.analytics && !previous.analytics) {
2351
- // Upgrade from anonymous ID to persistent ID
2352
- if (this.config.consent.anonymousMode) {
2353
- this.visitorId = getOrCreateVisitorId(this.config.useCookies);
2354
- logger.info('Upgraded from anonymous to persistent visitor ID');
2355
- }
2356
- // Flush buffered events
2357
- const buffered = this.consentManager.flushBuffer();
2358
- for (const event of buffered) {
2359
- // Update event with new visitor ID
2360
- event.visitorId = this.visitorId;
2361
- this.queue.push(event);
2362
- }
2480
+ async createTrigger(trigger) {
2481
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2482
+ method: 'POST',
2483
+ body: JSON.stringify(trigger),
2484
+ });
2485
+ // Cache the trigger locally if successful
2486
+ if (result.success && result.data?._id) {
2487
+ this.triggers.set(result.data._id, result.data);
2363
2488
  }
2489
+ return result;
2364
2490
  }
2365
2491
  /**
2366
- * Initialize enabled plugins
2367
- * Handles both sync and async plugin init methods
2492
+ * Update an existing trigger
2368
2493
  */
2369
- initPlugins() {
2370
- const pluginsToLoad = this.config.plugins;
2371
- // Skip pageView plugin if autoPageView is disabled
2372
- const filteredPlugins = this.config.autoPageView
2373
- ? pluginsToLoad
2374
- : pluginsToLoad.filter((p) => p !== 'pageView');
2375
- for (const pluginName of filteredPlugins) {
2376
- try {
2377
- const plugin = getPlugin(pluginName);
2378
- // Handle both sync and async init (fire-and-forget for async)
2379
- const result = plugin.init(this);
2380
- if (result instanceof Promise) {
2381
- result.catch((error) => {
2382
- logger.error(`Async plugin init failed: ${pluginName}`, error);
2383
- });
2384
- }
2385
- this.plugins.push(plugin);
2386
- logger.debug(`Plugin loaded: ${pluginName}`);
2387
- }
2388
- catch (error) {
2389
- logger.error(`Failed to load plugin: ${pluginName}`, error);
2390
- }
2494
+ async updateTrigger(triggerId, updates) {
2495
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2496
+ method: 'PUT',
2497
+ body: JSON.stringify(updates),
2498
+ });
2499
+ // Update cache if successful
2500
+ if (result.success && result.data?._id) {
2501
+ this.triggers.set(result.data._id, result.data);
2391
2502
  }
2503
+ return result;
2392
2504
  }
2393
2505
  /**
2394
- * Track a custom event
2506
+ * Delete a trigger
2395
2507
  */
2396
- track(eventType, eventName, properties = {}) {
2397
- if (!this.isInitialized) {
2398
- logger.warn('SDK not initialized, event dropped');
2399
- return;
2400
- }
2401
- const event = {
2402
- workspaceId: this.workspaceId,
2403
- visitorId: this.visitorId,
2404
- sessionId: this.sessionId,
2405
- eventType: eventType,
2406
- eventName,
2407
- url: typeof window !== 'undefined' ? window.location.href : '',
2408
- referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2409
- properties,
2410
- device: getDeviceInfo(),
2411
- utm: getUTMParams(),
2412
- timestamp: new Date().toISOString(),
2413
- sdkVersion: SDK_VERSION,
2414
- };
2415
- // Check consent before tracking
2416
- if (!this.consentManager.canTrack()) {
2417
- // Buffer event for later if waitForConsent is enabled
2418
- if (this.config.consent.waitForConsent) {
2419
- this.consentManager.bufferEvent(event);
2420
- return;
2421
- }
2422
- // Otherwise drop the event
2423
- logger.debug('Event dropped (no consent):', eventName);
2424
- return;
2508
+ async deleteTrigger(triggerId) {
2509
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2510
+ method: 'DELETE',
2511
+ });
2512
+ // Remove from cache if successful
2513
+ if (result.success) {
2514
+ this.triggers.delete(triggerId);
2425
2515
  }
2426
- this.queue.push(event);
2427
- logger.debug('Event tracked:', eventName, properties);
2516
+ return result;
2428
2517
  }
2429
2518
  /**
2430
- * Track a page view
2519
+ * Activate a trigger
2431
2520
  */
2432
- page(name, properties = {}) {
2433
- const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
2434
- this.track('page_view', pageName, {
2435
- ...properties,
2436
- path: typeof window !== 'undefined' ? window.location.pathname : '',
2437
- });
2521
+ async activateTrigger(triggerId) {
2522
+ return this.updateTrigger(triggerId, { isActive: true });
2438
2523
  }
2439
2524
  /**
2440
- * Identify a visitor
2525
+ * Deactivate a trigger
2441
2526
  */
2442
- async identify(email, traits = {}) {
2443
- if (!email) {
2444
- logger.warn('Email is required for identification');
2445
- return;
2446
- }
2447
- logger.info('Identifying visitor:', email);
2448
- const result = await this.transport.sendIdentify({
2449
- workspaceId: this.workspaceId,
2450
- visitorId: this.visitorId,
2451
- email,
2452
- properties: traits,
2453
- });
2454
- if (result.success) {
2455
- logger.info('Visitor identified successfully');
2456
- }
2457
- else {
2458
- logger.error('Failed to identify visitor:', result.error);
2459
- }
2527
+ async deactivateTrigger(triggerId) {
2528
+ return this.updateTrigger(triggerId, { isActive: false });
2460
2529
  }
2530
+ // ============================================
2531
+ // EVENT HANDLING (CLIENT-SIDE)
2532
+ // ============================================
2461
2533
  /**
2462
- * Update consent state
2534
+ * Register a local event listener for client-side triggers
2535
+ * This allows immediate client-side reactions to events
2463
2536
  */
2464
- consent(state) {
2465
- this.consentManager.update(state);
2537
+ on(eventType, callback) {
2538
+ if (!this.listeners.has(eventType)) {
2539
+ this.listeners.set(eventType, new Set());
2540
+ }
2541
+ this.listeners.get(eventType).add(callback);
2542
+ logger.debug(`Event listener registered: ${eventType}`);
2466
2543
  }
2467
2544
  /**
2468
- * Get current consent state
2545
+ * Remove an event listener
2469
2546
  */
2470
- getConsentState() {
2471
- return this.consentManager.getState();
2547
+ off(eventType, callback) {
2548
+ const listeners = this.listeners.get(eventType);
2549
+ if (listeners) {
2550
+ listeners.delete(callback);
2551
+ }
2472
2552
  }
2473
2553
  /**
2474
- * Toggle debug mode
2554
+ * Emit an event (client-side only)
2555
+ * This will trigger any registered local listeners
2475
2556
  */
2476
- debug(enabled) {
2477
- logger.enabled = enabled;
2478
- logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
2557
+ emit(eventType, data) {
2558
+ logger.debug(`Event emitted: ${eventType}`, data);
2559
+ const listeners = this.listeners.get(eventType);
2560
+ if (listeners) {
2561
+ listeners.forEach(callback => {
2562
+ try {
2563
+ callback(data);
2564
+ }
2565
+ catch (error) {
2566
+ logger.error(`Error in event listener for ${eventType}:`, error);
2567
+ }
2568
+ });
2569
+ }
2479
2570
  }
2480
2571
  /**
2481
- * Get visitor ID
2482
- */
2483
- getVisitorId() {
2484
- return this.visitorId;
2572
+ * Check if conditions are met for a trigger
2573
+ * Supports dynamic field evaluation including custom fields and nested paths
2574
+ */
2575
+ evaluateConditions(conditions, data) {
2576
+ if (!conditions || conditions.length === 0) {
2577
+ return true; // No conditions means always fire
2578
+ }
2579
+ return conditions.every(condition => {
2580
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
2581
+ const fieldValue = condition.field.includes('.')
2582
+ ? this.getNestedValue(data, condition.field)
2583
+ : data[condition.field];
2584
+ const targetValue = condition.value;
2585
+ switch (condition.operator) {
2586
+ case 'equals':
2587
+ return fieldValue === targetValue;
2588
+ case 'not_equals':
2589
+ return fieldValue !== targetValue;
2590
+ case 'contains':
2591
+ return String(fieldValue).includes(String(targetValue));
2592
+ case 'greater_than':
2593
+ return Number(fieldValue) > Number(targetValue);
2594
+ case 'less_than':
2595
+ return Number(fieldValue) < Number(targetValue);
2596
+ case 'in':
2597
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2598
+ case 'not_in':
2599
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2600
+ default:
2601
+ return false;
2602
+ }
2603
+ });
2485
2604
  }
2486
2605
  /**
2487
- * Get session ID
2606
+ * Execute actions for a triggered event (client-side preview)
2607
+ * Note: Actual execution happens on the backend
2488
2608
  */
2489
- getSessionId() {
2490
- return this.sessionId;
2609
+ async executeActions(trigger, data) {
2610
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
2611
+ for (const action of trigger.actions) {
2612
+ try {
2613
+ await this.executeAction(action, data);
2614
+ }
2615
+ catch (error) {
2616
+ logger.error(`Failed to execute action:`, error);
2617
+ }
2618
+ }
2491
2619
  }
2492
2620
  /**
2493
- * Get workspace ID
2494
- */
2495
- getWorkspaceId() {
2496
- return this.workspaceId;
2621
+ * Execute a single action
2622
+ */
2623
+ async executeAction(action, data) {
2624
+ switch (action.type) {
2625
+ case 'send_email':
2626
+ await this.executeSendEmail(action, data);
2627
+ break;
2628
+ case 'webhook':
2629
+ await this.executeWebhook(action, data);
2630
+ break;
2631
+ case 'create_task':
2632
+ await this.executeCreateTask(action, data);
2633
+ break;
2634
+ case 'update_contact':
2635
+ await this.executeUpdateContact(action, data);
2636
+ break;
2637
+ default:
2638
+ logger.warn(`Unknown action type:`, action);
2639
+ }
2640
+ }
2641
+ /**
2642
+ * Execute send email action (via backend API)
2643
+ */
2644
+ async executeSendEmail(action, data) {
2645
+ logger.debug('Sending email:', action);
2646
+ const payload = {
2647
+ to: this.replaceVariables(action.to, data),
2648
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2649
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
2650
+ templateId: action.templateId,
2651
+ cc: action.cc,
2652
+ bcc: action.bcc,
2653
+ from: action.from,
2654
+ delayMinutes: action.delayMinutes,
2655
+ };
2656
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2657
+ method: 'POST',
2658
+ body: JSON.stringify(payload),
2659
+ });
2497
2660
  }
2498
2661
  /**
2499
- * Get current configuration
2662
+ * Execute webhook action
2500
2663
  */
2501
- getConfig() {
2502
- return { ...this.config };
2664
+ async executeWebhook(action, data) {
2665
+ logger.debug('Calling webhook:', action.url);
2666
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2667
+ await fetch(action.url, {
2668
+ method: action.method,
2669
+ headers: {
2670
+ 'Content-Type': 'application/json',
2671
+ ...action.headers,
2672
+ },
2673
+ body,
2674
+ });
2503
2675
  }
2504
2676
  /**
2505
- * Force flush event queue
2677
+ * Execute create task action
2506
2678
  */
2507
- async flush() {
2508
- await this.queue.flush();
2679
+ async executeCreateTask(action, data) {
2680
+ logger.debug('Creating task:', action.title);
2681
+ const dueDate = action.dueDays
2682
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2683
+ : undefined;
2684
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2685
+ method: 'POST',
2686
+ body: JSON.stringify({
2687
+ title: this.replaceVariables(action.title, data),
2688
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
2689
+ priority: action.priority,
2690
+ dueDate,
2691
+ assignedTo: action.assignedTo,
2692
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2693
+ }),
2694
+ });
2509
2695
  }
2510
2696
  /**
2511
- * Reset visitor and session (for logout)
2697
+ * Execute update contact action
2512
2698
  */
2513
- reset() {
2514
- logger.info('Resetting visitor data');
2515
- resetIds(this.config.useCookies);
2516
- this.visitorId = this.createVisitorId();
2517
- this.sessionId = this.createSessionId();
2518
- this.queue.clear();
2699
+ async executeUpdateContact(action, data) {
2700
+ const contactId = data.contactId || data._id;
2701
+ if (!contactId) {
2702
+ logger.warn('Cannot update contact: no contactId in data');
2703
+ return;
2704
+ }
2705
+ logger.debug('Updating contact:', contactId);
2706
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2707
+ method: 'PUT',
2708
+ body: JSON.stringify(action.updates),
2709
+ });
2519
2710
  }
2520
2711
  /**
2521
- * Delete all stored user data (GDPR right-to-erasure)
2712
+ * Replace variables in a string template
2713
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
2522
2714
  */
2523
- deleteData() {
2524
- logger.info('Deleting all user data (GDPR request)');
2525
- // Clear queue
2526
- this.queue.clear();
2527
- // Reset consent
2528
- this.consentManager.reset();
2529
- // Clear all stored IDs
2530
- resetIds(this.config.useCookies);
2531
- // Clear session storage items
2532
- if (typeof sessionStorage !== 'undefined') {
2533
- try {
2534
- sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2535
- sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
2536
- sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
2537
- sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
2538
- }
2539
- catch {
2540
- // Ignore errors
2541
- }
2542
- }
2543
- // Clear localStorage items
2544
- if (typeof localStorage !== 'undefined') {
2545
- try {
2546
- localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
2547
- localStorage.removeItem(STORAGE_KEYS.CONSENT);
2548
- localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
2549
- }
2550
- catch {
2551
- // Ignore errors
2715
+ replaceVariables(template, data) {
2716
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
2717
+ const value = this.getNestedValue(data, path.trim());
2718
+ return value !== undefined ? String(value) : match;
2719
+ });
2720
+ }
2721
+ /**
2722
+ * Get nested value from object using dot notation
2723
+ * Supports dynamic field access including custom fields
2724
+ */
2725
+ getNestedValue(obj, path) {
2726
+ return path.split('.').reduce((current, key) => {
2727
+ return current !== null && current !== undefined && typeof current === 'object'
2728
+ ? current[key]
2729
+ : undefined;
2730
+ }, obj);
2731
+ }
2732
+ /**
2733
+ * Extract all available field paths from a data object
2734
+ * Useful for dynamic field discovery based on platform-specific attributes
2735
+ * @param obj - The data object to extract fields from
2736
+ * @param prefix - Internal use for nested paths
2737
+ * @param maxDepth - Maximum depth to traverse (default: 3)
2738
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
2739
+ */
2740
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
2741
+ if (maxDepth <= 0)
2742
+ return [];
2743
+ const fields = [];
2744
+ for (const key in obj) {
2745
+ if (!obj.hasOwnProperty(key))
2746
+ continue;
2747
+ const value = obj[key];
2748
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
2749
+ fields.push(fieldPath);
2750
+ // Recursively traverse nested objects
2751
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
2752
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
2753
+ fields.push(...nestedFields);
2552
2754
  }
2553
2755
  }
2554
- // Generate new IDs
2555
- this.visitorId = this.createVisitorId();
2556
- this.sessionId = this.createSessionId();
2557
- logger.info('All user data deleted');
2756
+ return fields;
2558
2757
  }
2559
2758
  /**
2560
- * Destroy tracker and cleanup
2759
+ * Get available fields from sample data
2760
+ * Helps with dynamic field detection for platform-specific attributes
2761
+ * @param sampleData - Sample data object to analyze
2762
+ * @returns Array of available field paths
2561
2763
  */
2562
- async destroy() {
2563
- logger.info('Destroying tracker');
2564
- // Flush any remaining events (await to ensure completion)
2565
- await this.queue.flush();
2566
- // Destroy plugins
2567
- for (const plugin of this.plugins) {
2568
- if (plugin.destroy) {
2569
- plugin.destroy();
2570
- }
2571
- }
2572
- this.plugins = [];
2573
- // Destroy queue
2574
- this.queue.destroy();
2575
- this.isInitialized = false;
2764
+ getAvailableFields(sampleData) {
2765
+ return this.extractAvailableFields(sampleData);
2766
+ }
2767
+ // ============================================
2768
+ // HELPER METHODS FOR COMMON PATTERNS
2769
+ // ============================================
2770
+ /**
2771
+ * Create a simple email trigger
2772
+ * Helper method for common use case
2773
+ */
2774
+ async createEmailTrigger(config) {
2775
+ return this.createTrigger({
2776
+ name: config.name,
2777
+ eventType: config.eventType,
2778
+ conditions: config.conditions,
2779
+ actions: [
2780
+ {
2781
+ type: 'send_email',
2782
+ to: config.to,
2783
+ subject: config.subject,
2784
+ body: config.body,
2785
+ },
2786
+ ],
2787
+ isActive: true,
2788
+ });
2789
+ }
2790
+ /**
2791
+ * Create a task creation trigger
2792
+ */
2793
+ async createTaskTrigger(config) {
2794
+ return this.createTrigger({
2795
+ name: config.name,
2796
+ eventType: config.eventType,
2797
+ conditions: config.conditions,
2798
+ actions: [
2799
+ {
2800
+ type: 'create_task',
2801
+ title: config.taskTitle,
2802
+ description: config.taskDescription,
2803
+ priority: config.priority,
2804
+ dueDays: config.dueDays,
2805
+ },
2806
+ ],
2807
+ isActive: true,
2808
+ });
2809
+ }
2810
+ /**
2811
+ * Create a webhook trigger
2812
+ */
2813
+ async createWebhookTrigger(config) {
2814
+ return this.createTrigger({
2815
+ name: config.name,
2816
+ eventType: config.eventType,
2817
+ conditions: config.conditions,
2818
+ actions: [
2819
+ {
2820
+ type: 'webhook',
2821
+ url: config.webhookUrl,
2822
+ method: config.method || 'POST',
2823
+ },
2824
+ ],
2825
+ isActive: true,
2826
+ });
2576
2827
  }
2577
2828
  }
2578
2829
 
@@ -2584,16 +2835,37 @@ class Tracker {
2584
2835
  * CRM API Client for managing contacts and opportunities
2585
2836
  */
2586
2837
  class CRMClient {
2587
- constructor(apiEndpoint, workspaceId, authToken) {
2838
+ constructor(apiEndpoint, workspaceId, authToken, apiKey) {
2588
2839
  this.apiEndpoint = apiEndpoint;
2589
2840
  this.workspaceId = workspaceId;
2590
2841
  this.authToken = authToken;
2842
+ this.apiKey = apiKey;
2843
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
2591
2844
  }
2592
2845
  /**
2593
- * Set authentication token for API requests
2846
+ * Set authentication token for API requests (user JWT)
2594
2847
  */
2595
2848
  setAuthToken(token) {
2596
2849
  this.authToken = token;
2850
+ this.apiKey = undefined;
2851
+ this.triggers.setAuthToken(token);
2852
+ }
2853
+ /**
2854
+ * Set workspace API key for server-to-server requests.
2855
+ * Use this instead of setAuthToken when integrating from an external app.
2856
+ */
2857
+ setApiKey(key) {
2858
+ this.apiKey = key;
2859
+ this.authToken = undefined;
2860
+ }
2861
+ /**
2862
+ * Validate required parameter exists
2863
+ * @throws {Error} if value is null/undefined or empty string
2864
+ */
2865
+ validateRequired(param, value, methodName) {
2866
+ if (value === null || value === undefined || value === '') {
2867
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
2868
+ }
2597
2869
  }
2598
2870
  /**
2599
2871
  * Make authenticated API request
@@ -2604,7 +2876,10 @@ class CRMClient {
2604
2876
  'Content-Type': 'application/json',
2605
2877
  ...(options.headers || {}),
2606
2878
  };
2607
- if (this.authToken) {
2879
+ if (this.apiKey) {
2880
+ headers['X-Api-Key'] = this.apiKey;
2881
+ }
2882
+ else if (this.authToken) {
2608
2883
  headers['Authorization'] = `Bearer ${this.authToken}`;
2609
2884
  }
2610
2885
  try {
@@ -2635,6 +2910,65 @@ class CRMClient {
2635
2910
  }
2636
2911
  }
2637
2912
  // ============================================
2913
+ // INBOUND EVENTS API (API-key authenticated)
2914
+ // ============================================
2915
+ /**
2916
+ * Send an inbound event from an external app (e.g. user signup on client website).
2917
+ * Requires the client to be initialized with an API key via setApiKey() or the constructor.
2918
+ *
2919
+ * The contact is upserted in the CRM and matching workflow automations fire automatically.
2920
+ *
2921
+ * @example
2922
+ * const crm = new CRMClient('https://api.clianta.online', 'WORKSPACE_ID');
2923
+ * crm.setApiKey('mm_live_...');
2924
+ *
2925
+ * await crm.sendEvent({
2926
+ * event: 'user.registered',
2927
+ * contact: { email: 'alice@example.com', firstName: 'Alice' },
2928
+ * data: { plan: 'free', signupSource: 'homepage' },
2929
+ * });
2930
+ */
2931
+ async sendEvent(payload) {
2932
+ const url = `${this.apiEndpoint}/api/public/events`;
2933
+ const headers = { 'Content-Type': 'application/json' };
2934
+ if (this.apiKey) {
2935
+ headers['X-Api-Key'] = this.apiKey;
2936
+ }
2937
+ else if (this.authToken) {
2938
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2939
+ }
2940
+ try {
2941
+ const response = await fetch(url, {
2942
+ method: 'POST',
2943
+ headers,
2944
+ body: JSON.stringify(payload),
2945
+ });
2946
+ const data = await response.json();
2947
+ if (!response.ok) {
2948
+ return {
2949
+ success: false,
2950
+ contactCreated: false,
2951
+ event: payload.event,
2952
+ error: data.error || 'Request failed',
2953
+ };
2954
+ }
2955
+ return {
2956
+ success: data.success,
2957
+ contactCreated: data.contactCreated,
2958
+ contactId: data.contactId,
2959
+ event: data.event,
2960
+ };
2961
+ }
2962
+ catch (error) {
2963
+ return {
2964
+ success: false,
2965
+ contactCreated: false,
2966
+ event: payload.event,
2967
+ error: error instanceof Error ? error.message : 'Network error',
2968
+ };
2969
+ }
2970
+ }
2971
+ // ============================================
2638
2972
  // CONTACTS API
2639
2973
  // ============================================
2640
2974
  /**
@@ -2658,6 +2992,7 @@ class CRMClient {
2658
2992
  * Get a single contact by ID
2659
2993
  */
2660
2994
  async getContact(contactId) {
2995
+ this.validateRequired('contactId', contactId, 'getContact');
2661
2996
  return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2662
2997
  }
2663
2998
  /**
@@ -2673,6 +3008,7 @@ class CRMClient {
2673
3008
  * Update an existing contact
2674
3009
  */
2675
3010
  async updateContact(contactId, updates) {
3011
+ this.validateRequired('contactId', contactId, 'updateContact');
2676
3012
  return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2677
3013
  method: 'PUT',
2678
3014
  body: JSON.stringify(updates),
@@ -2682,6 +3018,7 @@ class CRMClient {
2682
3018
  * Delete a contact
2683
3019
  */
2684
3020
  async deleteContact(contactId) {
3021
+ this.validateRequired('contactId', contactId, 'deleteContact');
2685
3022
  return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2686
3023
  method: 'DELETE',
2687
3024
  });
@@ -3045,6 +3382,442 @@ class CRMClient {
3045
3382
  opportunityId: data.opportunityId,
3046
3383
  });
3047
3384
  }
3385
+ // ============================================
3386
+ // EMAIL TEMPLATES API
3387
+ // ============================================
3388
+ /**
3389
+ * Get all email templates
3390
+ */
3391
+ async getEmailTemplates(params) {
3392
+ const queryParams = new URLSearchParams();
3393
+ if (params?.page)
3394
+ queryParams.set('page', params.page.toString());
3395
+ if (params?.limit)
3396
+ queryParams.set('limit', params.limit.toString());
3397
+ const query = queryParams.toString();
3398
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3399
+ return this.request(endpoint);
3400
+ }
3401
+ /**
3402
+ * Get a single email template by ID
3403
+ */
3404
+ async getEmailTemplate(templateId) {
3405
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3406
+ }
3407
+ /**
3408
+ * Create a new email template
3409
+ */
3410
+ async createEmailTemplate(template) {
3411
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3412
+ method: 'POST',
3413
+ body: JSON.stringify(template),
3414
+ });
3415
+ }
3416
+ /**
3417
+ * Update an email template
3418
+ */
3419
+ async updateEmailTemplate(templateId, updates) {
3420
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3421
+ method: 'PUT',
3422
+ body: JSON.stringify(updates),
3423
+ });
3424
+ }
3425
+ /**
3426
+ * Delete an email template
3427
+ */
3428
+ async deleteEmailTemplate(templateId) {
3429
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3430
+ method: 'DELETE',
3431
+ });
3432
+ }
3433
+ /**
3434
+ * Send an email using a template
3435
+ */
3436
+ async sendEmail(data) {
3437
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3438
+ method: 'POST',
3439
+ body: JSON.stringify(data),
3440
+ });
3441
+ }
3442
+ // ============================================
3443
+ // EVENT TRIGGERS API (delegated to triggers manager)
3444
+ // ============================================
3445
+ /**
3446
+ * Get all event triggers
3447
+ */
3448
+ async getEventTriggers() {
3449
+ return this.triggers.getTriggers();
3450
+ }
3451
+ /**
3452
+ * Create a new event trigger
3453
+ */
3454
+ async createEventTrigger(trigger) {
3455
+ return this.triggers.createTrigger(trigger);
3456
+ }
3457
+ /**
3458
+ * Update an event trigger
3459
+ */
3460
+ async updateEventTrigger(triggerId, updates) {
3461
+ return this.triggers.updateTrigger(triggerId, updates);
3462
+ }
3463
+ /**
3464
+ * Delete an event trigger
3465
+ */
3466
+ async deleteEventTrigger(triggerId) {
3467
+ return this.triggers.deleteTrigger(triggerId);
3468
+ }
3469
+ }
3470
+
3471
+ /**
3472
+ * Clianta SDK - Main Tracker Class
3473
+ * @see SDK_VERSION in core/config.ts
3474
+ */
3475
+ /**
3476
+ * Main Clianta Tracker Class
3477
+ */
3478
+ class Tracker {
3479
+ constructor(workspaceId, userConfig = {}) {
3480
+ this.plugins = [];
3481
+ this.isInitialized = false;
3482
+ /** contactId after a successful identify() call */
3483
+ this.contactId = null;
3484
+ /** Pending identify retry on next flush */
3485
+ this.pendingIdentify = null;
3486
+ if (!workspaceId) {
3487
+ throw new Error('[Clianta] Workspace ID is required');
3488
+ }
3489
+ this.workspaceId = workspaceId;
3490
+ this.config = mergeConfig(userConfig);
3491
+ // Setup debug mode
3492
+ logger.enabled = this.config.debug;
3493
+ logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
3494
+ // Initialize consent manager
3495
+ this.consentManager = new ConsentManager({
3496
+ ...this.config.consent,
3497
+ onConsentChange: (state, previous) => {
3498
+ this.onConsentChange(state, previous);
3499
+ },
3500
+ });
3501
+ // Initialize transport and queue
3502
+ this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
3503
+ this.queue = new EventQueue(this.transport, {
3504
+ batchSize: this.config.batchSize,
3505
+ flushInterval: this.config.flushInterval,
3506
+ });
3507
+ // Get or create visitor and session IDs based on mode
3508
+ this.visitorId = this.createVisitorId();
3509
+ this.sessionId = this.createSessionId();
3510
+ logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
3511
+ // Initialize plugins
3512
+ this.initPlugins();
3513
+ this.isInitialized = true;
3514
+ logger.info('SDK initialized successfully');
3515
+ }
3516
+ /**
3517
+ * Create visitor ID based on storage mode
3518
+ */
3519
+ createVisitorId() {
3520
+ // Anonymous mode: use temporary ID until consent
3521
+ if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
3522
+ const key = STORAGE_KEYS.VISITOR_ID + '_anon';
3523
+ let anonId = getSessionStorage(key);
3524
+ if (!anonId) {
3525
+ anonId = 'anon_' + generateUUID();
3526
+ setSessionStorage(key, anonId);
3527
+ }
3528
+ return anonId;
3529
+ }
3530
+ // Cookie-less mode: use sessionStorage only
3531
+ if (this.config.cookielessMode) {
3532
+ let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
3533
+ if (!visitorId) {
3534
+ visitorId = generateUUID();
3535
+ setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
3536
+ }
3537
+ return visitorId;
3538
+ }
3539
+ // Normal mode
3540
+ return getOrCreateVisitorId(this.config.useCookies);
3541
+ }
3542
+ /**
3543
+ * Create session ID
3544
+ */
3545
+ createSessionId() {
3546
+ return getOrCreateSessionId(this.config.sessionTimeout);
3547
+ }
3548
+ /**
3549
+ * Handle consent state changes
3550
+ */
3551
+ onConsentChange(state, previous) {
3552
+ logger.debug('Consent changed:', { from: previous, to: state });
3553
+ // If analytics consent was just granted
3554
+ if (state.analytics && !previous.analytics) {
3555
+ // Upgrade from anonymous ID to persistent ID
3556
+ if (this.config.consent.anonymousMode) {
3557
+ this.visitorId = getOrCreateVisitorId(this.config.useCookies);
3558
+ logger.info('Upgraded from anonymous to persistent visitor ID');
3559
+ }
3560
+ // Flush buffered events
3561
+ const buffered = this.consentManager.flushBuffer();
3562
+ for (const event of buffered) {
3563
+ // Update event with new visitor ID
3564
+ event.visitorId = this.visitorId;
3565
+ this.queue.push(event);
3566
+ }
3567
+ }
3568
+ }
3569
+ /**
3570
+ * Initialize enabled plugins
3571
+ * Handles both sync and async plugin init methods
3572
+ */
3573
+ initPlugins() {
3574
+ const pluginsToLoad = this.config.plugins;
3575
+ // Skip pageView plugin if autoPageView is disabled
3576
+ const filteredPlugins = this.config.autoPageView
3577
+ ? pluginsToLoad
3578
+ : pluginsToLoad.filter((p) => p !== 'pageView');
3579
+ for (const pluginName of filteredPlugins) {
3580
+ try {
3581
+ const plugin = getPlugin(pluginName);
3582
+ // Handle both sync and async init (fire-and-forget for async)
3583
+ const result = plugin.init(this);
3584
+ if (result instanceof Promise) {
3585
+ result.catch((error) => {
3586
+ logger.error(`Async plugin init failed: ${pluginName}`, error);
3587
+ });
3588
+ }
3589
+ this.plugins.push(plugin);
3590
+ logger.debug(`Plugin loaded: ${pluginName}`);
3591
+ }
3592
+ catch (error) {
3593
+ logger.error(`Failed to load plugin: ${pluginName}`, error);
3594
+ }
3595
+ }
3596
+ }
3597
+ /**
3598
+ * Track a custom event
3599
+ */
3600
+ track(eventType, eventName, properties = {}) {
3601
+ if (!this.isInitialized) {
3602
+ logger.warn('SDK not initialized, event dropped');
3603
+ return;
3604
+ }
3605
+ const event = {
3606
+ workspaceId: this.workspaceId,
3607
+ visitorId: this.visitorId,
3608
+ sessionId: this.sessionId,
3609
+ eventType: eventType,
3610
+ eventName,
3611
+ url: typeof window !== 'undefined' ? window.location.href : '',
3612
+ referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
3613
+ properties: {
3614
+ ...properties,
3615
+ eventId: generateUUID(), // Unique ID for deduplication on retry
3616
+ },
3617
+ device: getDeviceInfo(),
3618
+ ...getUTMParams(),
3619
+ timestamp: new Date().toISOString(),
3620
+ sdkVersion: SDK_VERSION,
3621
+ };
3622
+ // Attach contactId if known (from a prior identify() call)
3623
+ if (this.contactId) {
3624
+ event.contactId = this.contactId;
3625
+ }
3626
+ // Check consent before tracking
3627
+ if (!this.consentManager.canTrack()) {
3628
+ // Buffer event for later if waitForConsent is enabled
3629
+ if (this.config.consent.waitForConsent) {
3630
+ this.consentManager.bufferEvent(event);
3631
+ return;
3632
+ }
3633
+ // Otherwise drop the event
3634
+ logger.debug('Event dropped (no consent):', eventName);
3635
+ return;
3636
+ }
3637
+ this.queue.push(event);
3638
+ logger.debug('Event tracked:', eventName, properties);
3639
+ }
3640
+ /**
3641
+ * Track a page view
3642
+ */
3643
+ page(name, properties = {}) {
3644
+ const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
3645
+ this.track('page_view', pageName, {
3646
+ ...properties,
3647
+ path: typeof window !== 'undefined' ? window.location.pathname : '',
3648
+ });
3649
+ }
3650
+ /**
3651
+ * Identify a visitor.
3652
+ * Links the anonymous visitorId to a CRM contact and returns the contactId.
3653
+ * All subsequent track() calls will include the contactId automatically.
3654
+ */
3655
+ async identify(email, traits = {}) {
3656
+ if (!email) {
3657
+ logger.warn('Email is required for identification');
3658
+ return null;
3659
+ }
3660
+ logger.info('Identifying visitor:', email);
3661
+ const result = await this.transport.sendIdentify({
3662
+ workspaceId: this.workspaceId,
3663
+ visitorId: this.visitorId,
3664
+ email,
3665
+ properties: traits,
3666
+ });
3667
+ if (result.success) {
3668
+ logger.info('Visitor identified successfully, contactId:', result.contactId);
3669
+ // Store contactId so all future track() calls include it
3670
+ this.contactId = result.contactId ?? null;
3671
+ this.pendingIdentify = null;
3672
+ return this.contactId;
3673
+ }
3674
+ else {
3675
+ logger.error('Failed to identify visitor:', result.error);
3676
+ // Store for retry on next flush
3677
+ this.pendingIdentify = { email, traits };
3678
+ return null;
3679
+ }
3680
+ }
3681
+ /**
3682
+ * Send a server-side inbound event via the API key endpoint.
3683
+ * Convenience proxy to CRMClient.sendEvent() — requires apiKey in config.
3684
+ */
3685
+ async sendEvent(payload) {
3686
+ const apiKey = this.config.apiKey;
3687
+ if (!apiKey) {
3688
+ logger.error('sendEvent() requires an apiKey in the SDK config');
3689
+ return { success: false, contactCreated: false, event: payload.event, error: 'No API key configured' };
3690
+ }
3691
+ const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
3692
+ return client.sendEvent(payload);
3693
+ }
3694
+ /**
3695
+ * Retry pending identify call
3696
+ */
3697
+ async retryPendingIdentify() {
3698
+ if (!this.pendingIdentify)
3699
+ return;
3700
+ const { email, traits } = this.pendingIdentify;
3701
+ this.pendingIdentify = null;
3702
+ await this.identify(email, traits);
3703
+ }
3704
+ /**
3705
+ * Update consent state
3706
+ */
3707
+ consent(state) {
3708
+ this.consentManager.update(state);
3709
+ }
3710
+ /**
3711
+ * Get current consent state
3712
+ */
3713
+ getConsentState() {
3714
+ return this.consentManager.getState();
3715
+ }
3716
+ /**
3717
+ * Toggle debug mode
3718
+ */
3719
+ debug(enabled) {
3720
+ logger.enabled = enabled;
3721
+ logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
3722
+ }
3723
+ /**
3724
+ * Get visitor ID
3725
+ */
3726
+ getVisitorId() {
3727
+ return this.visitorId;
3728
+ }
3729
+ /**
3730
+ * Get session ID
3731
+ */
3732
+ getSessionId() {
3733
+ return this.sessionId;
3734
+ }
3735
+ /**
3736
+ * Get workspace ID
3737
+ */
3738
+ getWorkspaceId() {
3739
+ return this.workspaceId;
3740
+ }
3741
+ /**
3742
+ * Get current configuration
3743
+ */
3744
+ getConfig() {
3745
+ return { ...this.config };
3746
+ }
3747
+ /**
3748
+ * Force flush event queue
3749
+ */
3750
+ async flush() {
3751
+ await this.retryPendingIdentify();
3752
+ await this.queue.flush();
3753
+ }
3754
+ /**
3755
+ * Reset visitor and session (for logout)
3756
+ */
3757
+ reset() {
3758
+ logger.info('Resetting visitor data');
3759
+ resetIds(this.config.useCookies);
3760
+ this.visitorId = this.createVisitorId();
3761
+ this.sessionId = this.createSessionId();
3762
+ this.queue.clear();
3763
+ }
3764
+ /**
3765
+ * Delete all stored user data (GDPR right-to-erasure)
3766
+ */
3767
+ deleteData() {
3768
+ logger.info('Deleting all user data (GDPR request)');
3769
+ // Clear queue
3770
+ this.queue.clear();
3771
+ // Reset consent
3772
+ this.consentManager.reset();
3773
+ // Clear all stored IDs
3774
+ resetIds(this.config.useCookies);
3775
+ // Clear session storage items
3776
+ if (typeof sessionStorage !== 'undefined') {
3777
+ try {
3778
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
3779
+ sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
3780
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
3781
+ sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
3782
+ }
3783
+ catch {
3784
+ // Ignore errors
3785
+ }
3786
+ }
3787
+ // Clear localStorage items
3788
+ if (typeof localStorage !== 'undefined') {
3789
+ try {
3790
+ localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
3791
+ localStorage.removeItem(STORAGE_KEYS.CONSENT);
3792
+ localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
3793
+ }
3794
+ catch {
3795
+ // Ignore errors
3796
+ }
3797
+ }
3798
+ // Generate new IDs
3799
+ this.visitorId = this.createVisitorId();
3800
+ this.sessionId = this.createSessionId();
3801
+ logger.info('All user data deleted');
3802
+ }
3803
+ /**
3804
+ * Destroy tracker and cleanup
3805
+ */
3806
+ async destroy() {
3807
+ logger.info('Destroying tracker');
3808
+ // Flush any remaining events (await to ensure completion)
3809
+ await this.queue.flush();
3810
+ // Destroy plugins
3811
+ for (const plugin of this.plugins) {
3812
+ if (plugin.destroy) {
3813
+ plugin.destroy();
3814
+ }
3815
+ }
3816
+ this.plugins = [];
3817
+ // Destroy queue
3818
+ this.queue.destroy();
3819
+ this.isInitialized = false;
3820
+ }
3048
3821
  }
3049
3822
 
3050
3823
  /**
@@ -3099,6 +3872,7 @@ if (typeof window !== 'undefined') {
3099
3872
  Tracker,
3100
3873
  CRMClient,
3101
3874
  ConsentManager,
3875
+ EventTriggersManager,
3102
3876
  };
3103
3877
  }
3104
3878