@clianta/sdk 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +71 -1
- package/dist/clianta.cjs.js +1138 -363
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +1138 -364
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +1138 -363
- package/dist/clianta.umd.js.map +1 -1
- package/dist/clianta.umd.min.js +2 -2
- package/dist/clianta.umd.min.js.map +1 -1
- package/dist/index.d.ts +646 -235
- package/dist/react.cjs.js +1137 -363
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +37 -3
- package/dist/react.esm.js +1137 -363
- package/dist/react.esm.js.map +1 -1
- package/dist/vue.cjs.js +4028 -0
- package/dist/vue.cjs.js.map +1 -0
- package/dist/vue.d.ts +235 -0
- package/dist/vue.esm.js +4021 -0
- package/dist/vue.esm.js.map +1 -0
- package/package.json +16 -3
package/dist/clianta.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
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.
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
});
|
|
772
|
+
this.boundBeforeUnload = () => this.flushSync();
|
|
773
|
+
window.addEventListener('beforeunload', this.boundBeforeUnload);
|
|
732
774
|
// Flush when page becomes hidden
|
|
733
|
-
|
|
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
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
">×</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 -
|
|
2268
|
-
*
|
|
2392
|
+
* Clianta SDK - Event Triggers Manager
|
|
2393
|
+
* Manages event-driven automation and email notifications
|
|
2269
2394
|
*/
|
|
2270
2395
|
/**
|
|
2271
|
-
*
|
|
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
|
|
2274
|
-
constructor(workspaceId,
|
|
2275
|
-
this.
|
|
2276
|
-
this.
|
|
2277
|
-
|
|
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.
|
|
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
|
-
*
|
|
2413
|
+
* Set authentication token
|
|
2309
2414
|
*/
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
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
|
|
2443
|
+
return {
|
|
2444
|
+
success: true,
|
|
2445
|
+
data: data.data || data,
|
|
2446
|
+
status: response.status,
|
|
2447
|
+
};
|
|
2329
2448
|
}
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
-
*
|
|
2467
|
+
* Get a single trigger by ID
|
|
2335
2468
|
*/
|
|
2336
|
-
|
|
2337
|
-
return
|
|
2469
|
+
async getTrigger(triggerId) {
|
|
2470
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
|
|
2338
2471
|
}
|
|
2339
2472
|
/**
|
|
2340
|
-
*
|
|
2473
|
+
* Create a new event trigger
|
|
2341
2474
|
*/
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
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
|
-
*
|
|
2362
|
-
* Handles both sync and async plugin init methods
|
|
2487
|
+
* Update an existing trigger
|
|
2363
2488
|
*/
|
|
2364
|
-
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
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
|
-
*
|
|
2501
|
+
* Delete a trigger
|
|
2390
2502
|
*/
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
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
|
-
|
|
2422
|
-
logger.debug('Event tracked:', eventName, properties);
|
|
2511
|
+
return result;
|
|
2423
2512
|
}
|
|
2424
2513
|
/**
|
|
2425
|
-
*
|
|
2514
|
+
* Activate a trigger
|
|
2426
2515
|
*/
|
|
2427
|
-
|
|
2428
|
-
|
|
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
|
-
*
|
|
2520
|
+
* Deactivate a trigger
|
|
2436
2521
|
*/
|
|
2437
|
-
async
|
|
2438
|
-
|
|
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
|
-
*
|
|
2529
|
+
* Register a local event listener for client-side triggers
|
|
2530
|
+
* This allows immediate client-side reactions to events
|
|
2458
2531
|
*/
|
|
2459
|
-
|
|
2460
|
-
this.
|
|
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
|
-
*
|
|
2540
|
+
* Remove an event listener
|
|
2464
2541
|
*/
|
|
2465
|
-
|
|
2466
|
-
|
|
2542
|
+
off(eventType, callback) {
|
|
2543
|
+
const listeners = this.listeners.get(eventType);
|
|
2544
|
+
if (listeners) {
|
|
2545
|
+
listeners.delete(callback);
|
|
2546
|
+
}
|
|
2467
2547
|
}
|
|
2468
2548
|
/**
|
|
2469
|
-
*
|
|
2549
|
+
* Emit an event (client-side only)
|
|
2550
|
+
* This will trigger any registered local listeners
|
|
2470
2551
|
*/
|
|
2471
|
-
|
|
2472
|
-
logger.
|
|
2473
|
-
|
|
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
|
-
*
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
-
*
|
|
2601
|
+
* Execute actions for a triggered event (client-side preview)
|
|
2602
|
+
* Note: Actual execution happens on the backend
|
|
2483
2603
|
*/
|
|
2484
|
-
|
|
2485
|
-
|
|
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
|
-
*
|
|
2489
|
-
*/
|
|
2490
|
-
|
|
2491
|
-
|
|
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
|
-
*
|
|
2657
|
+
* Execute webhook action
|
|
2495
2658
|
*/
|
|
2496
|
-
|
|
2497
|
-
|
|
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
|
-
*
|
|
2672
|
+
* Execute create task action
|
|
2501
2673
|
*/
|
|
2502
|
-
async
|
|
2503
|
-
|
|
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
|
-
*
|
|
2692
|
+
* Execute update contact action
|
|
2507
2693
|
*/
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
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
|
-
*
|
|
2707
|
+
* Replace variables in a string template
|
|
2708
|
+
* Supports syntax like {{contact.email}}, {{opportunity.value}}
|
|
2517
2709
|
*/
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
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.
|
|
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
|