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