@clianta/sdk 1.2.0 → 1.3.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.3.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.3.0';
12
12
  /** Default API endpoint based on environment */
13
13
  const getDefaultApiEndpoint = () => {
14
14
  if (typeof window === 'undefined')
@@ -28,7 +28,6 @@ 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 = {
@@ -586,6 +585,10 @@ class EventQueue {
586
585
  this.isFlushing = false;
587
586
  /** Rate limiting: timestamps of recent events */
588
587
  this.eventTimestamps = [];
588
+ /** Unload handler references for cleanup */
589
+ this.boundBeforeUnload = null;
590
+ this.boundVisibilityChange = null;
591
+ this.boundPageHide = null;
589
592
  this.transport = transport;
590
593
  this.config = {
591
594
  batchSize: config.batchSize ?? 10,
@@ -700,13 +703,25 @@ class EventQueue {
700
703
  this.persistQueue([]);
701
704
  }
702
705
  /**
703
- * Stop the flush timer
706
+ * Stop the flush timer and cleanup handlers
704
707
  */
705
708
  destroy() {
706
709
  if (this.flushTimer) {
707
710
  clearInterval(this.flushTimer);
708
711
  this.flushTimer = null;
709
712
  }
713
+ // Remove unload handlers
714
+ if (typeof window !== 'undefined') {
715
+ if (this.boundBeforeUnload) {
716
+ window.removeEventListener('beforeunload', this.boundBeforeUnload);
717
+ }
718
+ if (this.boundVisibilityChange) {
719
+ window.removeEventListener('visibilitychange', this.boundVisibilityChange);
720
+ }
721
+ if (this.boundPageHide) {
722
+ window.removeEventListener('pagehide', this.boundPageHide);
723
+ }
724
+ }
710
725
  }
711
726
  /**
712
727
  * Start auto-flush timer
@@ -726,19 +741,18 @@ class EventQueue {
726
741
  if (typeof window === 'undefined')
727
742
  return;
728
743
  // Flush on page unload
729
- window.addEventListener('beforeunload', () => {
730
- this.flushSync();
731
- });
744
+ this.boundBeforeUnload = () => this.flushSync();
745
+ window.addEventListener('beforeunload', this.boundBeforeUnload);
732
746
  // Flush when page becomes hidden
733
- window.addEventListener('visibilitychange', () => {
747
+ this.boundVisibilityChange = () => {
734
748
  if (document.visibilityState === 'hidden') {
735
749
  this.flushSync();
736
750
  }
737
- });
751
+ };
752
+ window.addEventListener('visibilitychange', this.boundVisibilityChange);
738
753
  // Flush on page hide (iOS Safari)
739
- window.addEventListener('pagehide', () => {
740
- this.flushSync();
741
- });
754
+ this.boundPageHide = () => this.flushSync();
755
+ window.addEventListener('pagehide', this.boundPageHide);
742
756
  }
743
757
  /**
744
758
  * Persist queue to localStorage
@@ -881,6 +895,10 @@ class ScrollPlugin extends BasePlugin {
881
895
  this.pageLoadTime = 0;
882
896
  this.scrollTimeout = null;
883
897
  this.boundHandler = null;
898
+ /** SPA navigation support */
899
+ this.originalPushState = null;
900
+ this.originalReplaceState = null;
901
+ this.popstateHandler = null;
884
902
  }
885
903
  init(tracker) {
886
904
  super.init(tracker);
@@ -888,6 +906,8 @@ class ScrollPlugin extends BasePlugin {
888
906
  if (typeof window !== 'undefined') {
889
907
  this.boundHandler = this.handleScroll.bind(this);
890
908
  window.addEventListener('scroll', this.boundHandler, { passive: true });
909
+ // Setup SPA navigation reset
910
+ this.setupNavigationReset();
891
911
  }
892
912
  }
893
913
  destroy() {
@@ -897,8 +917,53 @@ class ScrollPlugin extends BasePlugin {
897
917
  if (this.scrollTimeout) {
898
918
  clearTimeout(this.scrollTimeout);
899
919
  }
920
+ // Restore original history methods
921
+ if (this.originalPushState) {
922
+ history.pushState = this.originalPushState;
923
+ this.originalPushState = null;
924
+ }
925
+ if (this.originalReplaceState) {
926
+ history.replaceState = this.originalReplaceState;
927
+ this.originalReplaceState = null;
928
+ }
929
+ // Remove popstate listener
930
+ if (this.popstateHandler && typeof window !== 'undefined') {
931
+ window.removeEventListener('popstate', this.popstateHandler);
932
+ this.popstateHandler = null;
933
+ }
900
934
  super.destroy();
901
935
  }
936
+ /**
937
+ * Reset scroll tracking for SPA navigation
938
+ */
939
+ resetForNavigation() {
940
+ this.milestonesReached.clear();
941
+ this.maxScrollDepth = 0;
942
+ this.pageLoadTime = Date.now();
943
+ }
944
+ /**
945
+ * Setup History API interception for SPA navigation
946
+ */
947
+ setupNavigationReset() {
948
+ if (typeof window === 'undefined')
949
+ return;
950
+ // Store originals for cleanup
951
+ this.originalPushState = history.pushState;
952
+ this.originalReplaceState = history.replaceState;
953
+ // Intercept pushState and replaceState
954
+ const self = this;
955
+ history.pushState = function (...args) {
956
+ self.originalPushState.apply(history, args);
957
+ self.resetForNavigation();
958
+ };
959
+ history.replaceState = function (...args) {
960
+ self.originalReplaceState.apply(history, args);
961
+ self.resetForNavigation();
962
+ };
963
+ // Handle back/forward navigation
964
+ this.popstateHandler = () => this.resetForNavigation();
965
+ window.addEventListener('popstate', this.popstateHandler);
966
+ }
902
967
  handleScroll() {
903
968
  // Debounce scroll tracking
904
969
  if (this.scrollTimeout) {
@@ -951,6 +1016,7 @@ class FormsPlugin extends BasePlugin {
951
1016
  this.trackedForms = new WeakSet();
952
1017
  this.formInteractions = new Set();
953
1018
  this.observer = null;
1019
+ this.listeners = [];
954
1020
  }
955
1021
  init(tracker) {
956
1022
  super.init(tracker);
@@ -969,8 +1035,20 @@ class FormsPlugin extends BasePlugin {
969
1035
  this.observer.disconnect();
970
1036
  this.observer = null;
971
1037
  }
1038
+ // Remove all tracked event listeners
1039
+ for (const { element, event, handler } of this.listeners) {
1040
+ element.removeEventListener(event, handler);
1041
+ }
1042
+ this.listeners = [];
972
1043
  super.destroy();
973
1044
  }
1045
+ /**
1046
+ * Track event listener for cleanup
1047
+ */
1048
+ addListener(element, event, handler) {
1049
+ element.addEventListener(event, handler);
1050
+ this.listeners.push({ element, event, handler });
1051
+ }
974
1052
  trackAllForms() {
975
1053
  document.querySelectorAll('form').forEach((form) => {
976
1054
  this.setupFormTracking(form);
@@ -996,7 +1074,7 @@ class FormsPlugin extends BasePlugin {
996
1074
  if (!field.name || field.type === 'submit' || field.type === 'button')
997
1075
  return;
998
1076
  ['focus', 'blur', 'change'].forEach((eventType) => {
999
- field.addEventListener(eventType, () => {
1077
+ const handler = () => {
1000
1078
  const key = `${formId}-${field.name}-${eventType}`;
1001
1079
  if (!this.formInteractions.has(key)) {
1002
1080
  this.formInteractions.add(key);
@@ -1007,12 +1085,13 @@ class FormsPlugin extends BasePlugin {
1007
1085
  interactionType: eventType,
1008
1086
  });
1009
1087
  }
1010
- });
1088
+ };
1089
+ this.addListener(field, eventType, handler);
1011
1090
  });
1012
1091
  }
1013
1092
  });
1014
1093
  // Track form submission
1015
- form.addEventListener('submit', () => {
1094
+ const submitHandler = () => {
1016
1095
  this.track('form_submit', 'Form Submitted', {
1017
1096
  formId,
1018
1097
  action: form.action,
@@ -1020,7 +1099,8 @@ class FormsPlugin extends BasePlugin {
1020
1099
  });
1021
1100
  // Auto-identify if email field found
1022
1101
  this.autoIdentify(form);
1023
- });
1102
+ };
1103
+ this.addListener(form, 'submit', submitHandler);
1024
1104
  }
1025
1105
  autoIdentify(form) {
1026
1106
  const emailField = form.querySelector('input[type="email"], input[name*="email"]');
@@ -1104,6 +1184,7 @@ class EngagementPlugin extends BasePlugin {
1104
1184
  this.engagementTimeout = null;
1105
1185
  this.boundMarkEngaged = null;
1106
1186
  this.boundTrackTimeOnPage = null;
1187
+ this.boundVisibilityHandler = null;
1107
1188
  }
1108
1189
  init(tracker) {
1109
1190
  super.init(tracker);
@@ -1114,12 +1195,7 @@ class EngagementPlugin extends BasePlugin {
1114
1195
  // Setup engagement detection
1115
1196
  this.boundMarkEngaged = this.markEngaged.bind(this);
1116
1197
  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', () => {
1198
+ this.boundVisibilityHandler = () => {
1123
1199
  if (document.visibilityState === 'hidden') {
1124
1200
  this.trackTimeOnPage();
1125
1201
  }
@@ -1127,7 +1203,13 @@ class EngagementPlugin extends BasePlugin {
1127
1203
  // Reset engagement timer when page becomes visible again
1128
1204
  this.engagementStartTime = Date.now();
1129
1205
  }
1206
+ };
1207
+ ['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
1208
+ document.addEventListener(event, this.boundMarkEngaged, { passive: true });
1130
1209
  });
1210
+ // Track time on page before unload
1211
+ window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
1212
+ document.addEventListener('visibilitychange', this.boundVisibilityHandler);
1131
1213
  }
1132
1214
  destroy() {
1133
1215
  if (this.boundMarkEngaged && typeof document !== 'undefined') {
@@ -1138,6 +1220,9 @@ class EngagementPlugin extends BasePlugin {
1138
1220
  if (this.boundTrackTimeOnPage && typeof window !== 'undefined') {
1139
1221
  window.removeEventListener('beforeunload', this.boundTrackTimeOnPage);
1140
1222
  }
1223
+ if (this.boundVisibilityHandler && typeof document !== 'undefined') {
1224
+ document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
1225
+ }
1141
1226
  if (this.engagementTimeout) {
1142
1227
  clearTimeout(this.engagementTimeout);
1143
1228
  }
@@ -1182,20 +1267,69 @@ class DownloadsPlugin extends BasePlugin {
1182
1267
  this.name = 'downloads';
1183
1268
  this.trackedDownloads = new Set();
1184
1269
  this.boundHandler = null;
1270
+ /** SPA navigation support */
1271
+ this.originalPushState = null;
1272
+ this.originalReplaceState = null;
1273
+ this.popstateHandler = null;
1185
1274
  }
1186
1275
  init(tracker) {
1187
1276
  super.init(tracker);
1188
1277
  if (typeof document !== 'undefined') {
1189
1278
  this.boundHandler = this.handleClick.bind(this);
1190
1279
  document.addEventListener('click', this.boundHandler, true);
1280
+ // Setup SPA navigation reset
1281
+ this.setupNavigationReset();
1191
1282
  }
1192
1283
  }
1193
1284
  destroy() {
1194
1285
  if (this.boundHandler && typeof document !== 'undefined') {
1195
1286
  document.removeEventListener('click', this.boundHandler, true);
1196
1287
  }
1288
+ // Restore original history methods
1289
+ if (this.originalPushState) {
1290
+ history.pushState = this.originalPushState;
1291
+ this.originalPushState = null;
1292
+ }
1293
+ if (this.originalReplaceState) {
1294
+ history.replaceState = this.originalReplaceState;
1295
+ this.originalReplaceState = null;
1296
+ }
1297
+ // Remove popstate listener
1298
+ if (this.popstateHandler && typeof window !== 'undefined') {
1299
+ window.removeEventListener('popstate', this.popstateHandler);
1300
+ this.popstateHandler = null;
1301
+ }
1197
1302
  super.destroy();
1198
1303
  }
1304
+ /**
1305
+ * Reset download tracking for SPA navigation
1306
+ */
1307
+ resetForNavigation() {
1308
+ this.trackedDownloads.clear();
1309
+ }
1310
+ /**
1311
+ * Setup History API interception for SPA navigation
1312
+ */
1313
+ setupNavigationReset() {
1314
+ if (typeof window === 'undefined')
1315
+ return;
1316
+ // Store originals for cleanup
1317
+ this.originalPushState = history.pushState;
1318
+ this.originalReplaceState = history.replaceState;
1319
+ // Intercept pushState and replaceState
1320
+ const self = this;
1321
+ history.pushState = function (...args) {
1322
+ self.originalPushState.apply(history, args);
1323
+ self.resetForNavigation();
1324
+ };
1325
+ history.replaceState = function (...args) {
1326
+ self.originalReplaceState.apply(history, args);
1327
+ self.resetForNavigation();
1328
+ };
1329
+ // Handle back/forward navigation
1330
+ this.popstateHandler = () => this.resetForNavigation();
1331
+ window.addEventListener('popstate', this.popstateHandler);
1332
+ }
1199
1333
  handleClick(e) {
1200
1334
  const link = e.target.closest('a');
1201
1335
  if (!link || !link.href)
@@ -1321,17 +1455,34 @@ class PerformancePlugin extends BasePlugin {
1321
1455
  constructor() {
1322
1456
  super(...arguments);
1323
1457
  this.name = 'performance';
1458
+ this.boundLoadHandler = null;
1459
+ this.observers = [];
1460
+ this.boundClsVisibilityHandler = null;
1324
1461
  }
1325
1462
  init(tracker) {
1326
1463
  super.init(tracker);
1327
1464
  if (typeof window !== 'undefined') {
1328
1465
  // Track performance after page load
1329
- window.addEventListener('load', () => {
1466
+ this.boundLoadHandler = () => {
1330
1467
  // Delay to ensure all metrics are available
1331
1468
  setTimeout(() => this.trackPerformance(), 100);
1332
- });
1469
+ };
1470
+ window.addEventListener('load', this.boundLoadHandler);
1333
1471
  }
1334
1472
  }
1473
+ destroy() {
1474
+ if (this.boundLoadHandler && typeof window !== 'undefined') {
1475
+ window.removeEventListener('load', this.boundLoadHandler);
1476
+ }
1477
+ for (const observer of this.observers) {
1478
+ observer.disconnect();
1479
+ }
1480
+ this.observers = [];
1481
+ if (this.boundClsVisibilityHandler && typeof window !== 'undefined') {
1482
+ window.removeEventListener('visibilitychange', this.boundClsVisibilityHandler);
1483
+ }
1484
+ super.destroy();
1485
+ }
1335
1486
  trackPerformance() {
1336
1487
  if (typeof performance === 'undefined')
1337
1488
  return;
@@ -1388,6 +1539,7 @@ class PerformancePlugin extends BasePlugin {
1388
1539
  }
1389
1540
  });
1390
1541
  lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });
1542
+ this.observers.push(lcpObserver);
1391
1543
  }
1392
1544
  catch {
1393
1545
  // LCP not supported
@@ -1405,6 +1557,7 @@ class PerformancePlugin extends BasePlugin {
1405
1557
  }
1406
1558
  });
1407
1559
  fidObserver.observe({ type: 'first-input', buffered: true });
1560
+ this.observers.push(fidObserver);
1408
1561
  }
1409
1562
  catch {
1410
1563
  // FID not supported
@@ -1421,15 +1574,17 @@ class PerformancePlugin extends BasePlugin {
1421
1574
  });
1422
1575
  });
1423
1576
  clsObserver.observe({ type: 'layout-shift', buffered: true });
1577
+ this.observers.push(clsObserver);
1424
1578
  // Report CLS after page is hidden
1425
- window.addEventListener('visibilitychange', () => {
1579
+ this.boundClsVisibilityHandler = () => {
1426
1580
  if (document.visibilityState === 'hidden' && clsValue > 0) {
1427
1581
  this.track('performance', 'Web Vital - CLS', {
1428
1582
  metric: 'CLS',
1429
1583
  value: Math.round(clsValue * 1000) / 1000,
1430
1584
  });
1431
1585
  }
1432
- }, { once: true });
1586
+ };
1587
+ window.addEventListener('visibilitychange', this.boundClsVisibilityHandler, { once: true });
1433
1588
  }
1434
1589
  catch {
1435
1590
  // CLS not supported
@@ -1740,7 +1895,7 @@ class PopupFormsPlugin extends BasePlugin {
1740
1895
  label.appendChild(requiredMark);
1741
1896
  }
1742
1897
  fieldWrapper.appendChild(label);
1743
- // Input/Textarea
1898
+ // Input/Textarea/Select
1744
1899
  if (field.type === 'textarea') {
1745
1900
  const textarea = document.createElement('textarea');
1746
1901
  textarea.name = field.name;
@@ -1751,6 +1906,38 @@ class PopupFormsPlugin extends BasePlugin {
1751
1906
  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
1907
  fieldWrapper.appendChild(textarea);
1753
1908
  }
1909
+ else if (field.type === 'select') {
1910
+ const select = document.createElement('select');
1911
+ select.name = field.name;
1912
+ if (field.required)
1913
+ select.required = true;
1914
+ 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;';
1915
+ // Add placeholder option
1916
+ if (field.placeholder) {
1917
+ const placeholderOption = document.createElement('option');
1918
+ placeholderOption.value = '';
1919
+ placeholderOption.textContent = field.placeholder;
1920
+ placeholderOption.disabled = true;
1921
+ placeholderOption.selected = true;
1922
+ select.appendChild(placeholderOption);
1923
+ }
1924
+ // Add options from field.options array if provided
1925
+ if (field.options && Array.isArray(field.options)) {
1926
+ field.options.forEach((opt) => {
1927
+ const option = document.createElement('option');
1928
+ if (typeof opt === 'string') {
1929
+ option.value = opt;
1930
+ option.textContent = opt;
1931
+ }
1932
+ else {
1933
+ option.value = opt.value;
1934
+ option.textContent = opt.label;
1935
+ }
1936
+ select.appendChild(option);
1937
+ });
1938
+ }
1939
+ fieldWrapper.appendChild(select);
1940
+ }
1754
1941
  else {
1755
1942
  const input = document.createElement('input');
1756
1943
  input.type = field.type;
@@ -1784,96 +1971,6 @@ class PopupFormsPlugin extends BasePlugin {
1784
1971
  formElement.appendChild(submitBtn);
1785
1972
  container.appendChild(formElement);
1786
1973
  }
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
1974
  setupFormEvents(form, overlay, container) {
1878
1975
  // Close button
1879
1976
  const closeBtn = container.querySelector('#clianta-form-close');
@@ -2274,6 +2371,8 @@ class Tracker {
2274
2371
  constructor(workspaceId, userConfig = {}) {
2275
2372
  this.plugins = [];
2276
2373
  this.isInitialized = false;
2374
+ /** Pending identify retry on next flush */
2375
+ this.pendingIdentify = null;
2277
2376
  if (!workspaceId) {
2278
2377
  throw new Error('[Clianta] Workspace ID is required');
2279
2378
  }
@@ -2403,7 +2502,7 @@ class Tracker {
2403
2502
  referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
2404
2503
  properties,
2405
2504
  device: getDeviceInfo(),
2406
- utm: getUTMParams(),
2505
+ ...getUTMParams(),
2407
2506
  timestamp: new Date().toISOString(),
2408
2507
  sdkVersion: SDK_VERSION,
2409
2508
  };
@@ -2448,11 +2547,24 @@ class Tracker {
2448
2547
  });
2449
2548
  if (result.success) {
2450
2549
  logger.info('Visitor identified successfully');
2550
+ this.pendingIdentify = null;
2451
2551
  }
2452
2552
  else {
2453
2553
  logger.error('Failed to identify visitor:', result.error);
2554
+ // Store for retry on next flush
2555
+ this.pendingIdentify = { email, traits };
2454
2556
  }
2455
2557
  }
2558
+ /**
2559
+ * Retry pending identify call
2560
+ */
2561
+ async retryPendingIdentify() {
2562
+ if (!this.pendingIdentify)
2563
+ return;
2564
+ const { email, traits } = this.pendingIdentify;
2565
+ this.pendingIdentify = null;
2566
+ await this.identify(email, traits);
2567
+ }
2456
2568
  /**
2457
2569
  * Update consent state
2458
2570
  */
@@ -2500,6 +2612,7 @@ class Tracker {
2500
2612
  * Force flush event queue
2501
2613
  */
2502
2614
  async flush() {
2615
+ await this.retryPendingIdentify();
2503
2616
  await this.queue.flush();
2504
2617
  }
2505
2618
  /**
@@ -2571,6 +2684,440 @@ class Tracker {
2571
2684
  }
2572
2685
  }
2573
2686
 
2687
+ /**
2688
+ * Clianta SDK - Event Triggers Manager
2689
+ * Manages event-driven automation and email notifications
2690
+ */
2691
+ /**
2692
+ * Event Triggers Manager
2693
+ * Handles event-driven automation based on CRM actions
2694
+ *
2695
+ * Similar to:
2696
+ * - Salesforce: Process Builder, Flow Automation
2697
+ * - HubSpot: Workflows, Email Sequences
2698
+ * - Pipedrive: Workflow Automation
2699
+ */
2700
+ class EventTriggersManager {
2701
+ constructor(apiEndpoint, workspaceId, authToken) {
2702
+ this.triggers = new Map();
2703
+ this.listeners = new Map();
2704
+ this.apiEndpoint = apiEndpoint;
2705
+ this.workspaceId = workspaceId;
2706
+ this.authToken = authToken;
2707
+ }
2708
+ /**
2709
+ * Set authentication token
2710
+ */
2711
+ setAuthToken(token) {
2712
+ this.authToken = token;
2713
+ }
2714
+ /**
2715
+ * Make authenticated API request
2716
+ */
2717
+ async request(endpoint, options = {}) {
2718
+ const url = `${this.apiEndpoint}${endpoint}`;
2719
+ const headers = {
2720
+ 'Content-Type': 'application/json',
2721
+ ...(options.headers || {}),
2722
+ };
2723
+ if (this.authToken) {
2724
+ headers['Authorization'] = `Bearer ${this.authToken}`;
2725
+ }
2726
+ try {
2727
+ const response = await fetch(url, {
2728
+ ...options,
2729
+ headers,
2730
+ });
2731
+ const data = await response.json();
2732
+ if (!response.ok) {
2733
+ return {
2734
+ success: false,
2735
+ error: data.message || 'Request failed',
2736
+ status: response.status,
2737
+ };
2738
+ }
2739
+ return {
2740
+ success: true,
2741
+ data: data.data || data,
2742
+ status: response.status,
2743
+ };
2744
+ }
2745
+ catch (error) {
2746
+ return {
2747
+ success: false,
2748
+ error: error instanceof Error ? error.message : 'Network error',
2749
+ status: 0,
2750
+ };
2751
+ }
2752
+ }
2753
+ // ============================================
2754
+ // TRIGGER MANAGEMENT
2755
+ // ============================================
2756
+ /**
2757
+ * Get all event triggers
2758
+ */
2759
+ async getTriggers() {
2760
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
2761
+ }
2762
+ /**
2763
+ * Get a single trigger by ID
2764
+ */
2765
+ async getTrigger(triggerId) {
2766
+ return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
2767
+ }
2768
+ /**
2769
+ * Create a new event trigger
2770
+ */
2771
+ async createTrigger(trigger) {
2772
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
2773
+ method: 'POST',
2774
+ body: JSON.stringify(trigger),
2775
+ });
2776
+ // Cache the trigger locally if successful
2777
+ if (result.success && result.data?._id) {
2778
+ this.triggers.set(result.data._id, result.data);
2779
+ }
2780
+ return result;
2781
+ }
2782
+ /**
2783
+ * Update an existing trigger
2784
+ */
2785
+ async updateTrigger(triggerId, updates) {
2786
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2787
+ method: 'PUT',
2788
+ body: JSON.stringify(updates),
2789
+ });
2790
+ // Update cache if successful
2791
+ if (result.success && result.data?._id) {
2792
+ this.triggers.set(result.data._id, result.data);
2793
+ }
2794
+ return result;
2795
+ }
2796
+ /**
2797
+ * Delete a trigger
2798
+ */
2799
+ async deleteTrigger(triggerId) {
2800
+ const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
2801
+ method: 'DELETE',
2802
+ });
2803
+ // Remove from cache if successful
2804
+ if (result.success) {
2805
+ this.triggers.delete(triggerId);
2806
+ }
2807
+ return result;
2808
+ }
2809
+ /**
2810
+ * Activate a trigger
2811
+ */
2812
+ async activateTrigger(triggerId) {
2813
+ return this.updateTrigger(triggerId, { isActive: true });
2814
+ }
2815
+ /**
2816
+ * Deactivate a trigger
2817
+ */
2818
+ async deactivateTrigger(triggerId) {
2819
+ return this.updateTrigger(triggerId, { isActive: false });
2820
+ }
2821
+ // ============================================
2822
+ // EVENT HANDLING (CLIENT-SIDE)
2823
+ // ============================================
2824
+ /**
2825
+ * Register a local event listener for client-side triggers
2826
+ * This allows immediate client-side reactions to events
2827
+ */
2828
+ on(eventType, callback) {
2829
+ if (!this.listeners.has(eventType)) {
2830
+ this.listeners.set(eventType, new Set());
2831
+ }
2832
+ this.listeners.get(eventType).add(callback);
2833
+ logger.debug(`Event listener registered: ${eventType}`);
2834
+ }
2835
+ /**
2836
+ * Remove an event listener
2837
+ */
2838
+ off(eventType, callback) {
2839
+ const listeners = this.listeners.get(eventType);
2840
+ if (listeners) {
2841
+ listeners.delete(callback);
2842
+ }
2843
+ }
2844
+ /**
2845
+ * Emit an event (client-side only)
2846
+ * This will trigger any registered local listeners
2847
+ */
2848
+ emit(eventType, data) {
2849
+ logger.debug(`Event emitted: ${eventType}`, data);
2850
+ const listeners = this.listeners.get(eventType);
2851
+ if (listeners) {
2852
+ listeners.forEach(callback => {
2853
+ try {
2854
+ callback(data);
2855
+ }
2856
+ catch (error) {
2857
+ logger.error(`Error in event listener for ${eventType}:`, error);
2858
+ }
2859
+ });
2860
+ }
2861
+ }
2862
+ /**
2863
+ * Check if conditions are met for a trigger
2864
+ * Supports dynamic field evaluation including custom fields and nested paths
2865
+ */
2866
+ evaluateConditions(conditions, data) {
2867
+ if (!conditions || conditions.length === 0) {
2868
+ return true; // No conditions means always fire
2869
+ }
2870
+ return conditions.every(condition => {
2871
+ // Support dot notation for nested fields (e.g., 'customFields.industry')
2872
+ const fieldValue = condition.field.includes('.')
2873
+ ? this.getNestedValue(data, condition.field)
2874
+ : data[condition.field];
2875
+ const targetValue = condition.value;
2876
+ switch (condition.operator) {
2877
+ case 'equals':
2878
+ return fieldValue === targetValue;
2879
+ case 'not_equals':
2880
+ return fieldValue !== targetValue;
2881
+ case 'contains':
2882
+ return String(fieldValue).includes(String(targetValue));
2883
+ case 'greater_than':
2884
+ return Number(fieldValue) > Number(targetValue);
2885
+ case 'less_than':
2886
+ return Number(fieldValue) < Number(targetValue);
2887
+ case 'in':
2888
+ return Array.isArray(targetValue) && targetValue.includes(fieldValue);
2889
+ case 'not_in':
2890
+ return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
2891
+ default:
2892
+ return false;
2893
+ }
2894
+ });
2895
+ }
2896
+ /**
2897
+ * Execute actions for a triggered event (client-side preview)
2898
+ * Note: Actual execution happens on the backend
2899
+ */
2900
+ async executeActions(trigger, data) {
2901
+ logger.info(`Executing actions for trigger: ${trigger.name}`);
2902
+ for (const action of trigger.actions) {
2903
+ try {
2904
+ await this.executeAction(action, data);
2905
+ }
2906
+ catch (error) {
2907
+ logger.error(`Failed to execute action:`, error);
2908
+ }
2909
+ }
2910
+ }
2911
+ /**
2912
+ * Execute a single action
2913
+ */
2914
+ async executeAction(action, data) {
2915
+ switch (action.type) {
2916
+ case 'send_email':
2917
+ await this.executeSendEmail(action, data);
2918
+ break;
2919
+ case 'webhook':
2920
+ await this.executeWebhook(action, data);
2921
+ break;
2922
+ case 'create_task':
2923
+ await this.executeCreateTask(action, data);
2924
+ break;
2925
+ case 'update_contact':
2926
+ await this.executeUpdateContact(action, data);
2927
+ break;
2928
+ default:
2929
+ logger.warn(`Unknown action type:`, action);
2930
+ }
2931
+ }
2932
+ /**
2933
+ * Execute send email action (via backend API)
2934
+ */
2935
+ async executeSendEmail(action, data) {
2936
+ logger.debug('Sending email:', action);
2937
+ const payload = {
2938
+ to: this.replaceVariables(action.to, data),
2939
+ subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
2940
+ body: action.body ? this.replaceVariables(action.body, data) : undefined,
2941
+ templateId: action.templateId,
2942
+ cc: action.cc,
2943
+ bcc: action.bcc,
2944
+ from: action.from,
2945
+ delayMinutes: action.delayMinutes,
2946
+ };
2947
+ await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
2948
+ method: 'POST',
2949
+ body: JSON.stringify(payload),
2950
+ });
2951
+ }
2952
+ /**
2953
+ * Execute webhook action
2954
+ */
2955
+ async executeWebhook(action, data) {
2956
+ logger.debug('Calling webhook:', action.url);
2957
+ const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
2958
+ await fetch(action.url, {
2959
+ method: action.method,
2960
+ headers: {
2961
+ 'Content-Type': 'application/json',
2962
+ ...action.headers,
2963
+ },
2964
+ body,
2965
+ });
2966
+ }
2967
+ /**
2968
+ * Execute create task action
2969
+ */
2970
+ async executeCreateTask(action, data) {
2971
+ logger.debug('Creating task:', action.title);
2972
+ const dueDate = action.dueDays
2973
+ ? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
2974
+ : undefined;
2975
+ await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
2976
+ method: 'POST',
2977
+ body: JSON.stringify({
2978
+ title: this.replaceVariables(action.title, data),
2979
+ description: action.description ? this.replaceVariables(action.description, data) : undefined,
2980
+ priority: action.priority,
2981
+ dueDate,
2982
+ assignedTo: action.assignedTo,
2983
+ relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
2984
+ }),
2985
+ });
2986
+ }
2987
+ /**
2988
+ * Execute update contact action
2989
+ */
2990
+ async executeUpdateContact(action, data) {
2991
+ const contactId = data.contactId || data._id;
2992
+ if (!contactId) {
2993
+ logger.warn('Cannot update contact: no contactId in data');
2994
+ return;
2995
+ }
2996
+ logger.debug('Updating contact:', contactId);
2997
+ await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2998
+ method: 'PUT',
2999
+ body: JSON.stringify(action.updates),
3000
+ });
3001
+ }
3002
+ /**
3003
+ * Replace variables in a string template
3004
+ * Supports syntax like {{contact.email}}, {{opportunity.value}}
3005
+ */
3006
+ replaceVariables(template, data) {
3007
+ return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
3008
+ const value = this.getNestedValue(data, path.trim());
3009
+ return value !== undefined ? String(value) : match;
3010
+ });
3011
+ }
3012
+ /**
3013
+ * Get nested value from object using dot notation
3014
+ * Supports dynamic field access including custom fields
3015
+ */
3016
+ getNestedValue(obj, path) {
3017
+ return path.split('.').reduce((current, key) => {
3018
+ return current !== null && current !== undefined && typeof current === 'object'
3019
+ ? current[key]
3020
+ : undefined;
3021
+ }, obj);
3022
+ }
3023
+ /**
3024
+ * Extract all available field paths from a data object
3025
+ * Useful for dynamic field discovery based on platform-specific attributes
3026
+ * @param obj - The data object to extract fields from
3027
+ * @param prefix - Internal use for nested paths
3028
+ * @param maxDepth - Maximum depth to traverse (default: 3)
3029
+ * @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
3030
+ */
3031
+ extractAvailableFields(obj, prefix = '', maxDepth = 3) {
3032
+ if (maxDepth <= 0)
3033
+ return [];
3034
+ const fields = [];
3035
+ for (const key in obj) {
3036
+ if (!obj.hasOwnProperty(key))
3037
+ continue;
3038
+ const value = obj[key];
3039
+ const fieldPath = prefix ? `${prefix}.${key}` : key;
3040
+ fields.push(fieldPath);
3041
+ // Recursively traverse nested objects
3042
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
3043
+ const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
3044
+ fields.push(...nestedFields);
3045
+ }
3046
+ }
3047
+ return fields;
3048
+ }
3049
+ /**
3050
+ * Get available fields from sample data
3051
+ * Helps with dynamic field detection for platform-specific attributes
3052
+ * @param sampleData - Sample data object to analyze
3053
+ * @returns Array of available field paths
3054
+ */
3055
+ getAvailableFields(sampleData) {
3056
+ return this.extractAvailableFields(sampleData);
3057
+ }
3058
+ // ============================================
3059
+ // HELPER METHODS FOR COMMON PATTERNS
3060
+ // ============================================
3061
+ /**
3062
+ * Create a simple email trigger
3063
+ * Helper method for common use case
3064
+ */
3065
+ async createEmailTrigger(config) {
3066
+ return this.createTrigger({
3067
+ name: config.name,
3068
+ eventType: config.eventType,
3069
+ conditions: config.conditions,
3070
+ actions: [
3071
+ {
3072
+ type: 'send_email',
3073
+ to: config.to,
3074
+ subject: config.subject,
3075
+ body: config.body,
3076
+ },
3077
+ ],
3078
+ isActive: true,
3079
+ });
3080
+ }
3081
+ /**
3082
+ * Create a task creation trigger
3083
+ */
3084
+ async createTaskTrigger(config) {
3085
+ return this.createTrigger({
3086
+ name: config.name,
3087
+ eventType: config.eventType,
3088
+ conditions: config.conditions,
3089
+ actions: [
3090
+ {
3091
+ type: 'create_task',
3092
+ title: config.taskTitle,
3093
+ description: config.taskDescription,
3094
+ priority: config.priority,
3095
+ dueDays: config.dueDays,
3096
+ },
3097
+ ],
3098
+ isActive: true,
3099
+ });
3100
+ }
3101
+ /**
3102
+ * Create a webhook trigger
3103
+ */
3104
+ async createWebhookTrigger(config) {
3105
+ return this.createTrigger({
3106
+ name: config.name,
3107
+ eventType: config.eventType,
3108
+ conditions: config.conditions,
3109
+ actions: [
3110
+ {
3111
+ type: 'webhook',
3112
+ url: config.webhookUrl,
3113
+ method: config.method || 'POST',
3114
+ },
3115
+ ],
3116
+ isActive: true,
3117
+ });
3118
+ }
3119
+ }
3120
+
2574
3121
  /**
2575
3122
  * Clianta SDK - CRM API Client
2576
3123
  * @see SDK_VERSION in core/config.ts
@@ -2583,12 +3130,23 @@ class CRMClient {
2583
3130
  this.apiEndpoint = apiEndpoint;
2584
3131
  this.workspaceId = workspaceId;
2585
3132
  this.authToken = authToken;
3133
+ this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
2586
3134
  }
2587
3135
  /**
2588
3136
  * Set authentication token for API requests
2589
3137
  */
2590
3138
  setAuthToken(token) {
2591
3139
  this.authToken = token;
3140
+ this.triggers.setAuthToken(token);
3141
+ }
3142
+ /**
3143
+ * Validate required parameter exists
3144
+ * @throws {Error} if value is null/undefined or empty string
3145
+ */
3146
+ validateRequired(param, value, methodName) {
3147
+ if (value === null || value === undefined || value === '') {
3148
+ throw new Error(`[CRMClient.${methodName}] ${param} is required`);
3149
+ }
2592
3150
  }
2593
3151
  /**
2594
3152
  * Make authenticated API request
@@ -2653,6 +3211,7 @@ class CRMClient {
2653
3211
  * Get a single contact by ID
2654
3212
  */
2655
3213
  async getContact(contactId) {
3214
+ this.validateRequired('contactId', contactId, 'getContact');
2656
3215
  return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
2657
3216
  }
2658
3217
  /**
@@ -2668,6 +3227,7 @@ class CRMClient {
2668
3227
  * Update an existing contact
2669
3228
  */
2670
3229
  async updateContact(contactId, updates) {
3230
+ this.validateRequired('contactId', contactId, 'updateContact');
2671
3231
  return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2672
3232
  method: 'PUT',
2673
3233
  body: JSON.stringify(updates),
@@ -2677,6 +3237,7 @@ class CRMClient {
2677
3237
  * Delete a contact
2678
3238
  */
2679
3239
  async deleteContact(contactId) {
3240
+ this.validateRequired('contactId', contactId, 'deleteContact');
2680
3241
  return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
2681
3242
  method: 'DELETE',
2682
3243
  });
@@ -3040,6 +3601,90 @@ class CRMClient {
3040
3601
  opportunityId: data.opportunityId,
3041
3602
  });
3042
3603
  }
3604
+ // ============================================
3605
+ // EMAIL TEMPLATES API
3606
+ // ============================================
3607
+ /**
3608
+ * Get all email templates
3609
+ */
3610
+ async getEmailTemplates(params) {
3611
+ const queryParams = new URLSearchParams();
3612
+ if (params?.page)
3613
+ queryParams.set('page', params.page.toString());
3614
+ if (params?.limit)
3615
+ queryParams.set('limit', params.limit.toString());
3616
+ const query = queryParams.toString();
3617
+ const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
3618
+ return this.request(endpoint);
3619
+ }
3620
+ /**
3621
+ * Get a single email template by ID
3622
+ */
3623
+ async getEmailTemplate(templateId) {
3624
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
3625
+ }
3626
+ /**
3627
+ * Create a new email template
3628
+ */
3629
+ async createEmailTemplate(template) {
3630
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
3631
+ method: 'POST',
3632
+ body: JSON.stringify(template),
3633
+ });
3634
+ }
3635
+ /**
3636
+ * Update an email template
3637
+ */
3638
+ async updateEmailTemplate(templateId, updates) {
3639
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3640
+ method: 'PUT',
3641
+ body: JSON.stringify(updates),
3642
+ });
3643
+ }
3644
+ /**
3645
+ * Delete an email template
3646
+ */
3647
+ async deleteEmailTemplate(templateId) {
3648
+ return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
3649
+ method: 'DELETE',
3650
+ });
3651
+ }
3652
+ /**
3653
+ * Send an email using a template
3654
+ */
3655
+ async sendEmail(data) {
3656
+ return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
3657
+ method: 'POST',
3658
+ body: JSON.stringify(data),
3659
+ });
3660
+ }
3661
+ // ============================================
3662
+ // EVENT TRIGGERS API (delegated to triggers manager)
3663
+ // ============================================
3664
+ /**
3665
+ * Get all event triggers
3666
+ */
3667
+ async getEventTriggers() {
3668
+ return this.triggers.getTriggers();
3669
+ }
3670
+ /**
3671
+ * Create a new event trigger
3672
+ */
3673
+ async createEventTrigger(trigger) {
3674
+ return this.triggers.createTrigger(trigger);
3675
+ }
3676
+ /**
3677
+ * Update an event trigger
3678
+ */
3679
+ async updateEventTrigger(triggerId, updates) {
3680
+ return this.triggers.updateTrigger(triggerId, updates);
3681
+ }
3682
+ /**
3683
+ * Delete an event trigger
3684
+ */
3685
+ async deleteEventTrigger(triggerId) {
3686
+ return this.triggers.deleteTrigger(triggerId);
3687
+ }
3043
3688
  }
3044
3689
 
3045
3690
  /**
@@ -3094,8 +3739,9 @@ if (typeof window !== 'undefined') {
3094
3739
  Tracker,
3095
3740
  CRMClient,
3096
3741
  ConsentManager,
3742
+ EventTriggersManager,
3097
3743
  };
3098
3744
  }
3099
3745
 
3100
- export { CRMClient, ConsentManager, SDK_VERSION, Tracker, clianta, clianta as default };
3746
+ export { CRMClient, ConsentManager, EventTriggersManager, SDK_VERSION, Tracker, clianta, clianta as default };
3101
3747
  //# sourceMappingURL=clianta.esm.js.map