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