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