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