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