@clianta/sdk 1.3.0 → 1.5.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 +46 -0
- package/README.md +56 -1
- package/dist/angular.cjs.js +4345 -0
- package/dist/angular.cjs.js.map +1 -0
- package/dist/angular.d.ts +298 -0
- package/dist/angular.esm.js +4341 -0
- package/dist/angular.esm.js.map +1 -0
- package/dist/clianta.cjs.js +1504 -1005
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +1504 -1005
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +1504 -1005
- 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 +1068 -791
- package/dist/react.cjs.js +1517 -1010
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +125 -3
- package/dist/react.esm.js +1518 -1011
- package/dist/react.esm.js.map +1 -1
- package/dist/svelte.cjs.js +4377 -0
- package/dist/svelte.cjs.js.map +1 -0
- package/dist/svelte.d.ts +308 -0
- package/dist/svelte.esm.js +4374 -0
- package/dist/svelte.esm.js.map +1 -0
- package/dist/vue.cjs.js +1504 -1005
- package/dist/vue.cjs.js.map +1 -1
- package/dist/vue.d.ts +125 -3
- package/dist/vue.esm.js +1504 -1005
- package/dist/vue.esm.js.map +1 -1
- package/package.json +21 -2
package/dist/clianta.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
2
|
+
* Clianta SDK v1.5.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')
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
projectId: '',
|
|
41
41
|
apiEndpoint: getDefaultApiEndpoint(),
|
|
42
42
|
authToken: '',
|
|
43
|
+
apiKey: '',
|
|
43
44
|
debug: false,
|
|
44
45
|
autoPageView: true,
|
|
45
46
|
plugins: DEFAULT_PLUGINS,
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
cookieDomain: '',
|
|
56
57
|
useCookies: false,
|
|
57
58
|
cookielessMode: false,
|
|
59
|
+
persistMode: 'session',
|
|
58
60
|
};
|
|
59
61
|
/** Storage keys */
|
|
60
62
|
const STORAGE_KEYS = {
|
|
@@ -187,12 +189,39 @@
|
|
|
187
189
|
return this.send(url, payload);
|
|
188
190
|
}
|
|
189
191
|
/**
|
|
190
|
-
* Send identify request
|
|
192
|
+
* Send identify request.
|
|
193
|
+
* Returns contactId from the server response so the Tracker can store it.
|
|
191
194
|
*/
|
|
192
195
|
async sendIdentify(data) {
|
|
193
196
|
const url = `${this.config.apiEndpoint}/api/public/track/identify`;
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
try {
|
|
198
|
+
const response = await this.fetchWithTimeout(url, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: { 'Content-Type': 'application/json' },
|
|
201
|
+
body: JSON.stringify(data),
|
|
202
|
+
keepalive: true,
|
|
203
|
+
});
|
|
204
|
+
const body = await response.json().catch(() => ({}));
|
|
205
|
+
if (response.ok) {
|
|
206
|
+
logger.debug('Identify successful, contactId:', body.contactId);
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
status: response.status,
|
|
210
|
+
contactId: body.contactId ?? undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (response.status >= 500) {
|
|
214
|
+
logger.warn(`Identify server error (${response.status})`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
logger.error(`Identify failed with status ${response.status}:`, body.message);
|
|
218
|
+
}
|
|
219
|
+
return { success: false, status: response.status };
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
logger.error('Identify request failed:', error);
|
|
223
|
+
return { success: false, error: error };
|
|
224
|
+
}
|
|
196
225
|
}
|
|
197
226
|
/**
|
|
198
227
|
* Send events synchronously (for page unload)
|
|
@@ -221,6 +250,39 @@
|
|
|
221
250
|
return false;
|
|
222
251
|
}
|
|
223
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Fetch data from the tracking API (GET request)
|
|
255
|
+
* Used for read-back APIs (visitor profile, activity, etc.)
|
|
256
|
+
*/
|
|
257
|
+
async fetchData(path, params) {
|
|
258
|
+
const url = new URL(`${this.config.apiEndpoint}${path}`);
|
|
259
|
+
if (params) {
|
|
260
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
261
|
+
if (value !== undefined && value !== null) {
|
|
262
|
+
url.searchParams.set(key, value);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const response = await this.fetchWithTimeout(url.toString(), {
|
|
268
|
+
method: 'GET',
|
|
269
|
+
headers: {
|
|
270
|
+
'Accept': 'application/json',
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
if (response.ok) {
|
|
274
|
+
const body = await response.json();
|
|
275
|
+
logger.debug('Fetch successful:', path);
|
|
276
|
+
return { success: true, data: body.data ?? body, status: response.status };
|
|
277
|
+
}
|
|
278
|
+
logger.error(`Fetch failed with status ${response.status}`);
|
|
279
|
+
return { success: false, status: response.status };
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
logger.error('Fetch request failed:', error);
|
|
283
|
+
return { success: false, error: error };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
224
286
|
/**
|
|
225
287
|
* Internal send with retry logic
|
|
226
288
|
*/
|
|
@@ -385,7 +447,9 @@
|
|
|
385
447
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
386
448
|
expires = '; expires=' + date.toUTCString();
|
|
387
449
|
}
|
|
388
|
-
|
|
450
|
+
// Add Secure flag on HTTPS to prevent cookie leakage over plaintext
|
|
451
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
|
|
452
|
+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax' + secure;
|
|
389
453
|
return value;
|
|
390
454
|
}
|
|
391
455
|
// ============================================
|
|
@@ -549,6 +613,17 @@
|
|
|
549
613
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
550
614
|
}
|
|
551
615
|
// ============================================
|
|
616
|
+
// VALIDATION UTILITIES
|
|
617
|
+
// ============================================
|
|
618
|
+
/**
|
|
619
|
+
* Validate email format
|
|
620
|
+
*/
|
|
621
|
+
function isValidEmail(email) {
|
|
622
|
+
if (typeof email !== 'string' || !email)
|
|
623
|
+
return false;
|
|
624
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
625
|
+
}
|
|
626
|
+
// ============================================
|
|
552
627
|
// DEVICE INFO
|
|
553
628
|
// ============================================
|
|
554
629
|
/**
|
|
@@ -602,6 +677,7 @@
|
|
|
602
677
|
maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
603
678
|
storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
|
|
604
679
|
};
|
|
680
|
+
this.persistMode = config.persistMode || 'session';
|
|
605
681
|
// Restore persisted queue
|
|
606
682
|
this.restoreQueue();
|
|
607
683
|
// Start auto-flush timer
|
|
@@ -707,6 +783,13 @@
|
|
|
707
783
|
clear() {
|
|
708
784
|
this.queue = [];
|
|
709
785
|
this.persistQueue([]);
|
|
786
|
+
// Also clear localStorage if used
|
|
787
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
788
|
+
try {
|
|
789
|
+
localStorage.removeItem(this.config.storageKey);
|
|
790
|
+
}
|
|
791
|
+
catch { /* ignore */ }
|
|
792
|
+
}
|
|
710
793
|
}
|
|
711
794
|
/**
|
|
712
795
|
* Stop the flush timer and cleanup handlers
|
|
@@ -761,22 +844,44 @@
|
|
|
761
844
|
window.addEventListener('pagehide', this.boundPageHide);
|
|
762
845
|
}
|
|
763
846
|
/**
|
|
764
|
-
* Persist queue to
|
|
847
|
+
* Persist queue to storage based on persistMode
|
|
765
848
|
*/
|
|
766
849
|
persistQueue(events) {
|
|
850
|
+
if (this.persistMode === 'none')
|
|
851
|
+
return;
|
|
767
852
|
try {
|
|
768
|
-
|
|
853
|
+
const serialized = JSON.stringify(events);
|
|
854
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
855
|
+
try {
|
|
856
|
+
localStorage.setItem(this.config.storageKey, serialized);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
// localStorage quota exceeded — fallback to sessionStorage
|
|
860
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
865
|
+
}
|
|
769
866
|
}
|
|
770
867
|
catch {
|
|
771
868
|
// Ignore storage errors
|
|
772
869
|
}
|
|
773
870
|
}
|
|
774
871
|
/**
|
|
775
|
-
* Restore queue from
|
|
872
|
+
* Restore queue from storage
|
|
776
873
|
*/
|
|
777
874
|
restoreQueue() {
|
|
778
875
|
try {
|
|
779
|
-
|
|
876
|
+
let stored = null;
|
|
877
|
+
// Check localStorage first (cross-session persistence)
|
|
878
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
879
|
+
stored = localStorage.getItem(this.config.storageKey);
|
|
880
|
+
}
|
|
881
|
+
// Fall back to sessionStorage
|
|
882
|
+
if (!stored) {
|
|
883
|
+
stored = getSessionStorage(this.config.storageKey);
|
|
884
|
+
}
|
|
780
885
|
if (stored) {
|
|
781
886
|
const events = JSON.parse(stored);
|
|
782
887
|
if (Array.isArray(events) && events.length > 0) {
|
|
@@ -844,10 +949,13 @@
|
|
|
844
949
|
history.pushState = function (...args) {
|
|
845
950
|
self.originalPushState.apply(history, args);
|
|
846
951
|
self.trackPageView();
|
|
952
|
+
// Notify other plugins (e.g. ScrollPlugin) about navigation
|
|
953
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
847
954
|
};
|
|
848
955
|
history.replaceState = function (...args) {
|
|
849
956
|
self.originalReplaceState.apply(history, args);
|
|
850
957
|
self.trackPageView();
|
|
958
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
851
959
|
};
|
|
852
960
|
// Handle back/forward navigation
|
|
853
961
|
this.popstateHandler = () => this.trackPageView();
|
|
@@ -901,9 +1009,8 @@
|
|
|
901
1009
|
this.pageLoadTime = 0;
|
|
902
1010
|
this.scrollTimeout = null;
|
|
903
1011
|
this.boundHandler = null;
|
|
904
|
-
/** SPA navigation
|
|
905
|
-
this.
|
|
906
|
-
this.originalReplaceState = null;
|
|
1012
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1013
|
+
this.navigationHandler = null;
|
|
907
1014
|
this.popstateHandler = null;
|
|
908
1015
|
}
|
|
909
1016
|
init(tracker) {
|
|
@@ -912,8 +1019,13 @@
|
|
|
912
1019
|
if (typeof window !== 'undefined') {
|
|
913
1020
|
this.boundHandler = this.handleScroll.bind(this);
|
|
914
1021
|
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
915
|
-
//
|
|
916
|
-
|
|
1022
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1023
|
+
// instead of independently monkey-patching history.pushState
|
|
1024
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1025
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1026
|
+
// Handle back/forward navigation
|
|
1027
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1028
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
917
1029
|
}
|
|
918
1030
|
}
|
|
919
1031
|
destroy() {
|
|
@@ -923,16 +1035,10 @@
|
|
|
923
1035
|
if (this.scrollTimeout) {
|
|
924
1036
|
clearTimeout(this.scrollTimeout);
|
|
925
1037
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
this.originalPushState = null;
|
|
930
|
-
}
|
|
931
|
-
if (this.originalReplaceState) {
|
|
932
|
-
history.replaceState = this.originalReplaceState;
|
|
933
|
-
this.originalReplaceState = null;
|
|
1038
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1039
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1040
|
+
this.navigationHandler = null;
|
|
934
1041
|
}
|
|
935
|
-
// Remove popstate listener
|
|
936
1042
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
937
1043
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
938
1044
|
this.popstateHandler = null;
|
|
@@ -947,29 +1053,6 @@
|
|
|
947
1053
|
this.maxScrollDepth = 0;
|
|
948
1054
|
this.pageLoadTime = Date.now();
|
|
949
1055
|
}
|
|
950
|
-
/**
|
|
951
|
-
* Setup History API interception for SPA navigation
|
|
952
|
-
*/
|
|
953
|
-
setupNavigationReset() {
|
|
954
|
-
if (typeof window === 'undefined')
|
|
955
|
-
return;
|
|
956
|
-
// Store originals for cleanup
|
|
957
|
-
this.originalPushState = history.pushState;
|
|
958
|
-
this.originalReplaceState = history.replaceState;
|
|
959
|
-
// Intercept pushState and replaceState
|
|
960
|
-
const self = this;
|
|
961
|
-
history.pushState = function (...args) {
|
|
962
|
-
self.originalPushState.apply(history, args);
|
|
963
|
-
self.resetForNavigation();
|
|
964
|
-
};
|
|
965
|
-
history.replaceState = function (...args) {
|
|
966
|
-
self.originalReplaceState.apply(history, args);
|
|
967
|
-
self.resetForNavigation();
|
|
968
|
-
};
|
|
969
|
-
// Handle back/forward navigation
|
|
970
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
971
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
972
|
-
}
|
|
973
1056
|
handleScroll() {
|
|
974
1057
|
// Debounce scroll tracking
|
|
975
1058
|
if (this.scrollTimeout) {
|
|
@@ -1169,6 +1252,10 @@
|
|
|
1169
1252
|
elementId: elementInfo.id,
|
|
1170
1253
|
elementClass: elementInfo.className,
|
|
1171
1254
|
href: target.href || undefined,
|
|
1255
|
+
x: Math.round((e.clientX / window.innerWidth) * 100),
|
|
1256
|
+
y: Math.round((e.clientY / window.innerHeight) * 100),
|
|
1257
|
+
viewportWidth: window.innerWidth,
|
|
1258
|
+
viewportHeight: window.innerHeight,
|
|
1172
1259
|
});
|
|
1173
1260
|
}
|
|
1174
1261
|
}
|
|
@@ -1191,6 +1278,9 @@
|
|
|
1191
1278
|
this.boundMarkEngaged = null;
|
|
1192
1279
|
this.boundTrackTimeOnPage = null;
|
|
1193
1280
|
this.boundVisibilityHandler = null;
|
|
1281
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1282
|
+
this.navigationHandler = null;
|
|
1283
|
+
this.popstateHandler = null;
|
|
1194
1284
|
}
|
|
1195
1285
|
init(tracker) {
|
|
1196
1286
|
super.init(tracker);
|
|
@@ -1216,6 +1306,13 @@
|
|
|
1216
1306
|
// Track time on page before unload
|
|
1217
1307
|
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1218
1308
|
document.addEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1309
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1310
|
+
// instead of independently monkey-patching history.pushState
|
|
1311
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1312
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1313
|
+
// Handle back/forward navigation
|
|
1314
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1315
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1219
1316
|
}
|
|
1220
1317
|
destroy() {
|
|
1221
1318
|
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
@@ -1229,11 +1326,28 @@
|
|
|
1229
1326
|
if (this.boundVisibilityHandler && typeof document !== 'undefined') {
|
|
1230
1327
|
document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1231
1328
|
}
|
|
1329
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1330
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1331
|
+
this.navigationHandler = null;
|
|
1332
|
+
}
|
|
1333
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1334
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1335
|
+
this.popstateHandler = null;
|
|
1336
|
+
}
|
|
1232
1337
|
if (this.engagementTimeout) {
|
|
1233
1338
|
clearTimeout(this.engagementTimeout);
|
|
1234
1339
|
}
|
|
1235
1340
|
super.destroy();
|
|
1236
1341
|
}
|
|
1342
|
+
resetForNavigation() {
|
|
1343
|
+
this.pageLoadTime = Date.now();
|
|
1344
|
+
this.engagementStartTime = Date.now();
|
|
1345
|
+
this.isEngaged = false;
|
|
1346
|
+
if (this.engagementTimeout) {
|
|
1347
|
+
clearTimeout(this.engagementTimeout);
|
|
1348
|
+
this.engagementTimeout = null;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1237
1351
|
markEngaged() {
|
|
1238
1352
|
if (!this.isEngaged) {
|
|
1239
1353
|
this.isEngaged = true;
|
|
@@ -1273,9 +1387,8 @@
|
|
|
1273
1387
|
this.name = 'downloads';
|
|
1274
1388
|
this.trackedDownloads = new Set();
|
|
1275
1389
|
this.boundHandler = null;
|
|
1276
|
-
/** SPA navigation
|
|
1277
|
-
this.
|
|
1278
|
-
this.originalReplaceState = null;
|
|
1390
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1391
|
+
this.navigationHandler = null;
|
|
1279
1392
|
this.popstateHandler = null;
|
|
1280
1393
|
}
|
|
1281
1394
|
init(tracker) {
|
|
@@ -1283,24 +1396,25 @@
|
|
|
1283
1396
|
if (typeof document !== 'undefined') {
|
|
1284
1397
|
this.boundHandler = this.handleClick.bind(this);
|
|
1285
1398
|
document.addEventListener('click', this.boundHandler, true);
|
|
1286
|
-
|
|
1287
|
-
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof window !== 'undefined') {
|
|
1401
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1402
|
+
// instead of independently monkey-patching history.pushState
|
|
1403
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1404
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1405
|
+
// Handle back/forward navigation
|
|
1406
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1407
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1288
1408
|
}
|
|
1289
1409
|
}
|
|
1290
1410
|
destroy() {
|
|
1291
1411
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1292
1412
|
document.removeEventListener('click', this.boundHandler, true);
|
|
1293
1413
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
this.originalPushState = null;
|
|
1298
|
-
}
|
|
1299
|
-
if (this.originalReplaceState) {
|
|
1300
|
-
history.replaceState = this.originalReplaceState;
|
|
1301
|
-
this.originalReplaceState = null;
|
|
1414
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1415
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1416
|
+
this.navigationHandler = null;
|
|
1302
1417
|
}
|
|
1303
|
-
// Remove popstate listener
|
|
1304
1418
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1305
1419
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
1306
1420
|
this.popstateHandler = null;
|
|
@@ -1313,29 +1427,6 @@
|
|
|
1313
1427
|
resetForNavigation() {
|
|
1314
1428
|
this.trackedDownloads.clear();
|
|
1315
1429
|
}
|
|
1316
|
-
/**
|
|
1317
|
-
* Setup History API interception for SPA navigation
|
|
1318
|
-
*/
|
|
1319
|
-
setupNavigationReset() {
|
|
1320
|
-
if (typeof window === 'undefined')
|
|
1321
|
-
return;
|
|
1322
|
-
// Store originals for cleanup
|
|
1323
|
-
this.originalPushState = history.pushState;
|
|
1324
|
-
this.originalReplaceState = history.replaceState;
|
|
1325
|
-
// Intercept pushState and replaceState
|
|
1326
|
-
const self = this;
|
|
1327
|
-
history.pushState = function (...args) {
|
|
1328
|
-
self.originalPushState.apply(history, args);
|
|
1329
|
-
self.resetForNavigation();
|
|
1330
|
-
};
|
|
1331
|
-
history.replaceState = function (...args) {
|
|
1332
|
-
self.originalReplaceState.apply(history, args);
|
|
1333
|
-
self.resetForNavigation();
|
|
1334
|
-
};
|
|
1335
|
-
// Handle back/forward navigation
|
|
1336
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
1337
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
1338
|
-
}
|
|
1339
1430
|
handleClick(e) {
|
|
1340
1431
|
const link = e.target.closest('a');
|
|
1341
1432
|
if (!link || !link.href)
|
|
@@ -1371,6 +1462,9 @@
|
|
|
1371
1462
|
this.exitIntentShown = false;
|
|
1372
1463
|
this.pageLoadTime = 0;
|
|
1373
1464
|
this.boundHandler = null;
|
|
1465
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1466
|
+
this.navigationHandler = null;
|
|
1467
|
+
this.popstateHandler = null;
|
|
1374
1468
|
}
|
|
1375
1469
|
init(tracker) {
|
|
1376
1470
|
super.init(tracker);
|
|
@@ -1382,13 +1476,34 @@
|
|
|
1382
1476
|
this.boundHandler = this.handleMouseLeave.bind(this);
|
|
1383
1477
|
document.addEventListener('mouseleave', this.boundHandler);
|
|
1384
1478
|
}
|
|
1479
|
+
if (typeof window !== 'undefined') {
|
|
1480
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1481
|
+
// instead of independently monkey-patching history.pushState
|
|
1482
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1483
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1484
|
+
// Handle back/forward navigation
|
|
1485
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1486
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1487
|
+
}
|
|
1385
1488
|
}
|
|
1386
1489
|
destroy() {
|
|
1387
1490
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1388
1491
|
document.removeEventListener('mouseleave', this.boundHandler);
|
|
1389
1492
|
}
|
|
1493
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1494
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1495
|
+
this.navigationHandler = null;
|
|
1496
|
+
}
|
|
1497
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1498
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1499
|
+
this.popstateHandler = null;
|
|
1500
|
+
}
|
|
1390
1501
|
super.destroy();
|
|
1391
1502
|
}
|
|
1503
|
+
resetForNavigation() {
|
|
1504
|
+
this.exitIntentShown = false;
|
|
1505
|
+
this.pageLoadTime = Date.now();
|
|
1506
|
+
}
|
|
1392
1507
|
handleMouseLeave(e) {
|
|
1393
1508
|
// Only trigger when mouse leaves from the top of the page
|
|
1394
1509
|
if (e.clientY > 0 || this.exitIntentShown)
|
|
@@ -1616,6 +1731,8 @@
|
|
|
1616
1731
|
this.shownForms = new Set();
|
|
1617
1732
|
this.scrollHandler = null;
|
|
1618
1733
|
this.exitHandler = null;
|
|
1734
|
+
this.delayTimers = [];
|
|
1735
|
+
this.clickTriggerListeners = [];
|
|
1619
1736
|
}
|
|
1620
1737
|
async init(tracker) {
|
|
1621
1738
|
super.init(tracker);
|
|
@@ -1630,6 +1747,14 @@
|
|
|
1630
1747
|
}
|
|
1631
1748
|
destroy() {
|
|
1632
1749
|
this.removeTriggers();
|
|
1750
|
+
for (const timer of this.delayTimers) {
|
|
1751
|
+
clearTimeout(timer);
|
|
1752
|
+
}
|
|
1753
|
+
this.delayTimers = [];
|
|
1754
|
+
for (const { element, handler } of this.clickTriggerListeners) {
|
|
1755
|
+
element.removeEventListener('click', handler);
|
|
1756
|
+
}
|
|
1757
|
+
this.clickTriggerListeners = [];
|
|
1633
1758
|
super.destroy();
|
|
1634
1759
|
}
|
|
1635
1760
|
loadShownForms() {
|
|
@@ -1692,7 +1817,7 @@
|
|
|
1692
1817
|
this.forms.forEach(form => {
|
|
1693
1818
|
switch (form.trigger.type) {
|
|
1694
1819
|
case 'delay':
|
|
1695
|
-
setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
|
|
1820
|
+
this.delayTimers.push(setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000));
|
|
1696
1821
|
break;
|
|
1697
1822
|
case 'scroll':
|
|
1698
1823
|
this.setupScrollTrigger(form);
|
|
@@ -1735,7 +1860,9 @@
|
|
|
1735
1860
|
return;
|
|
1736
1861
|
const elements = document.querySelectorAll(form.trigger.selector);
|
|
1737
1862
|
elements.forEach(el => {
|
|
1738
|
-
|
|
1863
|
+
const handler = () => this.showForm(form);
|
|
1864
|
+
el.addEventListener('click', handler);
|
|
1865
|
+
this.clickTriggerListeners.push({ element: el, handler });
|
|
1739
1866
|
});
|
|
1740
1867
|
}
|
|
1741
1868
|
removeTriggers() {
|
|
@@ -2022,7 +2149,7 @@
|
|
|
2022
2149
|
const submitBtn = formElement.querySelector('button[type="submit"]');
|
|
2023
2150
|
if (submitBtn) {
|
|
2024
2151
|
submitBtn.disabled = true;
|
|
2025
|
-
submitBtn.
|
|
2152
|
+
submitBtn.textContent = 'Submitting...';
|
|
2026
2153
|
}
|
|
2027
2154
|
try {
|
|
2028
2155
|
const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
|
|
@@ -2063,11 +2190,24 @@
|
|
|
2063
2190
|
if (data.email) {
|
|
2064
2191
|
this.tracker?.identify(data.email, data);
|
|
2065
2192
|
}
|
|
2066
|
-
// Redirect if configured
|
|
2193
|
+
// Redirect if configured (validate URL to prevent open redirect)
|
|
2067
2194
|
if (form.redirectUrl) {
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2195
|
+
try {
|
|
2196
|
+
const redirect = new URL(form.redirectUrl, window.location.origin);
|
|
2197
|
+
const isSameOrigin = redirect.origin === window.location.origin;
|
|
2198
|
+
const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
|
|
2199
|
+
if (isSameOrigin || isSafeProtocol) {
|
|
2200
|
+
setTimeout(() => {
|
|
2201
|
+
window.location.href = redirect.href;
|
|
2202
|
+
}, 1500);
|
|
2203
|
+
}
|
|
2204
|
+
else {
|
|
2205
|
+
console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
catch {
|
|
2209
|
+
console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
|
|
2210
|
+
}
|
|
2071
2211
|
}
|
|
2072
2212
|
// Close after delay
|
|
2073
2213
|
setTimeout(() => {
|
|
@@ -2082,7 +2222,7 @@
|
|
|
2082
2222
|
console.error('[Clianta] Form submit error:', error);
|
|
2083
2223
|
if (submitBtn) {
|
|
2084
2224
|
submitBtn.disabled = false;
|
|
2085
|
-
submitBtn.
|
|
2225
|
+
submitBtn.textContent = form.submitButtonText || 'Subscribe';
|
|
2086
2226
|
}
|
|
2087
2227
|
}
|
|
2088
2228
|
}
|
|
@@ -2367,355 +2507,478 @@
|
|
|
2367
2507
|
}
|
|
2368
2508
|
|
|
2369
2509
|
/**
|
|
2370
|
-
* Clianta SDK -
|
|
2371
|
-
*
|
|
2510
|
+
* Clianta SDK - Event Triggers Manager
|
|
2511
|
+
* Manages event-driven automation and email notifications
|
|
2372
2512
|
*/
|
|
2373
2513
|
/**
|
|
2374
|
-
*
|
|
2514
|
+
* Event Triggers Manager
|
|
2515
|
+
* Handles event-driven automation based on CRM actions
|
|
2516
|
+
*
|
|
2517
|
+
* Similar to:
|
|
2518
|
+
* - Salesforce: Process Builder, Flow Automation
|
|
2519
|
+
* - HubSpot: Workflows, Email Sequences
|
|
2520
|
+
* - Pipedrive: Workflow Automation
|
|
2375
2521
|
*/
|
|
2376
|
-
class
|
|
2377
|
-
constructor(workspaceId,
|
|
2378
|
-
this.
|
|
2379
|
-
this.
|
|
2380
|
-
|
|
2381
|
-
this.pendingIdentify = null;
|
|
2382
|
-
if (!workspaceId) {
|
|
2383
|
-
throw new Error('[Clianta] Workspace ID is required');
|
|
2384
|
-
}
|
|
2522
|
+
class EventTriggersManager {
|
|
2523
|
+
constructor(apiEndpoint, workspaceId, authToken) {
|
|
2524
|
+
this.triggers = new Map();
|
|
2525
|
+
this.listeners = new Map();
|
|
2526
|
+
this.apiEndpoint = apiEndpoint;
|
|
2385
2527
|
this.workspaceId = workspaceId;
|
|
2386
|
-
this.
|
|
2387
|
-
// Setup debug mode
|
|
2388
|
-
logger.enabled = this.config.debug;
|
|
2389
|
-
logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
|
|
2390
|
-
// Initialize consent manager
|
|
2391
|
-
this.consentManager = new ConsentManager({
|
|
2392
|
-
...this.config.consent,
|
|
2393
|
-
onConsentChange: (state, previous) => {
|
|
2394
|
-
this.onConsentChange(state, previous);
|
|
2395
|
-
},
|
|
2396
|
-
});
|
|
2397
|
-
// Initialize transport and queue
|
|
2398
|
-
this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
|
|
2399
|
-
this.queue = new EventQueue(this.transport, {
|
|
2400
|
-
batchSize: this.config.batchSize,
|
|
2401
|
-
flushInterval: this.config.flushInterval,
|
|
2402
|
-
});
|
|
2403
|
-
// Get or create visitor and session IDs based on mode
|
|
2404
|
-
this.visitorId = this.createVisitorId();
|
|
2405
|
-
this.sessionId = this.createSessionId();
|
|
2406
|
-
logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
|
|
2407
|
-
// Initialize plugins
|
|
2408
|
-
this.initPlugins();
|
|
2409
|
-
this.isInitialized = true;
|
|
2410
|
-
logger.info('SDK initialized successfully');
|
|
2528
|
+
this.authToken = authToken;
|
|
2411
2529
|
}
|
|
2412
2530
|
/**
|
|
2413
|
-
*
|
|
2531
|
+
* Set authentication token
|
|
2414
2532
|
*/
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2533
|
+
setAuthToken(token) {
|
|
2534
|
+
this.authToken = token;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Make authenticated API request
|
|
2538
|
+
*/
|
|
2539
|
+
async request(endpoint, options = {}) {
|
|
2540
|
+
const url = `${this.apiEndpoint}${endpoint}`;
|
|
2541
|
+
const headers = {
|
|
2542
|
+
'Content-Type': 'application/json',
|
|
2543
|
+
...(options.headers || {}),
|
|
2544
|
+
};
|
|
2545
|
+
if (this.authToken) {
|
|
2546
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
2425
2547
|
}
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2548
|
+
try {
|
|
2549
|
+
const response = await fetch(url, {
|
|
2550
|
+
...options,
|
|
2551
|
+
headers,
|
|
2552
|
+
});
|
|
2553
|
+
const data = await response.json();
|
|
2554
|
+
if (!response.ok) {
|
|
2555
|
+
return {
|
|
2556
|
+
success: false,
|
|
2557
|
+
error: data.message || 'Request failed',
|
|
2558
|
+
status: response.status,
|
|
2559
|
+
};
|
|
2432
2560
|
}
|
|
2433
|
-
return
|
|
2561
|
+
return {
|
|
2562
|
+
success: true,
|
|
2563
|
+
data: data.data || data,
|
|
2564
|
+
status: response.status,
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
catch (error) {
|
|
2568
|
+
return {
|
|
2569
|
+
success: false,
|
|
2570
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
2571
|
+
status: 0,
|
|
2572
|
+
};
|
|
2434
2573
|
}
|
|
2435
|
-
// Normal mode
|
|
2436
|
-
return getOrCreateVisitorId(this.config.useCookies);
|
|
2437
2574
|
}
|
|
2575
|
+
// ============================================
|
|
2576
|
+
// TRIGGER MANAGEMENT
|
|
2577
|
+
// ============================================
|
|
2438
2578
|
/**
|
|
2439
|
-
*
|
|
2579
|
+
* Get all event triggers
|
|
2440
2580
|
*/
|
|
2441
|
-
|
|
2442
|
-
return
|
|
2443
|
-
}
|
|
2444
|
-
/**
|
|
2445
|
-
* Handle consent state changes
|
|
2446
|
-
*/
|
|
2447
|
-
onConsentChange(state, previous) {
|
|
2448
|
-
logger.debug('Consent changed:', { from: previous, to: state });
|
|
2449
|
-
// If analytics consent was just granted
|
|
2450
|
-
if (state.analytics && !previous.analytics) {
|
|
2451
|
-
// Upgrade from anonymous ID to persistent ID
|
|
2452
|
-
if (this.config.consent.anonymousMode) {
|
|
2453
|
-
this.visitorId = getOrCreateVisitorId(this.config.useCookies);
|
|
2454
|
-
logger.info('Upgraded from anonymous to persistent visitor ID');
|
|
2455
|
-
}
|
|
2456
|
-
// Flush buffered events
|
|
2457
|
-
const buffered = this.consentManager.flushBuffer();
|
|
2458
|
-
for (const event of buffered) {
|
|
2459
|
-
// Update event with new visitor ID
|
|
2460
|
-
event.visitorId = this.visitorId;
|
|
2461
|
-
this.queue.push(event);
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2581
|
+
async getTriggers() {
|
|
2582
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers`);
|
|
2464
2583
|
}
|
|
2465
2584
|
/**
|
|
2466
|
-
*
|
|
2467
|
-
* Handles both sync and async plugin init methods
|
|
2585
|
+
* Get a single trigger by ID
|
|
2468
2586
|
*/
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
// Skip pageView plugin if autoPageView is disabled
|
|
2472
|
-
const filteredPlugins = this.config.autoPageView
|
|
2473
|
-
? pluginsToLoad
|
|
2474
|
-
: pluginsToLoad.filter((p) => p !== 'pageView');
|
|
2475
|
-
for (const pluginName of filteredPlugins) {
|
|
2476
|
-
try {
|
|
2477
|
-
const plugin = getPlugin(pluginName);
|
|
2478
|
-
// Handle both sync and async init (fire-and-forget for async)
|
|
2479
|
-
const result = plugin.init(this);
|
|
2480
|
-
if (result instanceof Promise) {
|
|
2481
|
-
result.catch((error) => {
|
|
2482
|
-
logger.error(`Async plugin init failed: ${pluginName}`, error);
|
|
2483
|
-
});
|
|
2484
|
-
}
|
|
2485
|
-
this.plugins.push(plugin);
|
|
2486
|
-
logger.debug(`Plugin loaded: ${pluginName}`);
|
|
2487
|
-
}
|
|
2488
|
-
catch (error) {
|
|
2489
|
-
logger.error(`Failed to load plugin: ${pluginName}`, error);
|
|
2490
|
-
}
|
|
2491
|
-
}
|
|
2587
|
+
async getTrigger(triggerId) {
|
|
2588
|
+
return this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`);
|
|
2492
2589
|
}
|
|
2493
2590
|
/**
|
|
2494
|
-
*
|
|
2591
|
+
* Create a new event trigger
|
|
2495
2592
|
*/
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
sessionId: this.sessionId,
|
|
2505
|
-
eventType: eventType,
|
|
2506
|
-
eventName,
|
|
2507
|
-
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
2508
|
-
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
2509
|
-
properties,
|
|
2510
|
-
device: getDeviceInfo(),
|
|
2511
|
-
...getUTMParams(),
|
|
2512
|
-
timestamp: new Date().toISOString(),
|
|
2513
|
-
sdkVersion: SDK_VERSION,
|
|
2514
|
-
};
|
|
2515
|
-
// Check consent before tracking
|
|
2516
|
-
if (!this.consentManager.canTrack()) {
|
|
2517
|
-
// Buffer event for later if waitForConsent is enabled
|
|
2518
|
-
if (this.config.consent.waitForConsent) {
|
|
2519
|
-
this.consentManager.bufferEvent(event);
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
// Otherwise drop the event
|
|
2523
|
-
logger.debug('Event dropped (no consent):', eventName);
|
|
2524
|
-
return;
|
|
2593
|
+
async createTrigger(trigger) {
|
|
2594
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers`, {
|
|
2595
|
+
method: 'POST',
|
|
2596
|
+
body: JSON.stringify(trigger),
|
|
2597
|
+
});
|
|
2598
|
+
// Cache the trigger locally if successful
|
|
2599
|
+
if (result.success && result.data?._id) {
|
|
2600
|
+
this.triggers.set(result.data._id, result.data);
|
|
2525
2601
|
}
|
|
2526
|
-
|
|
2527
|
-
logger.debug('Event tracked:', eventName, properties);
|
|
2602
|
+
return result;
|
|
2528
2603
|
}
|
|
2529
2604
|
/**
|
|
2530
|
-
*
|
|
2605
|
+
* Update an existing trigger
|
|
2531
2606
|
*/
|
|
2532
|
-
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
path: typeof window !== 'undefined' ? window.location.pathname : '',
|
|
2607
|
+
async updateTrigger(triggerId, updates) {
|
|
2608
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
|
|
2609
|
+
method: 'PUT',
|
|
2610
|
+
body: JSON.stringify(updates),
|
|
2537
2611
|
});
|
|
2612
|
+
// Update cache if successful
|
|
2613
|
+
if (result.success && result.data?._id) {
|
|
2614
|
+
this.triggers.set(result.data._id, result.data);
|
|
2615
|
+
}
|
|
2616
|
+
return result;
|
|
2538
2617
|
}
|
|
2539
2618
|
/**
|
|
2540
|
-
*
|
|
2619
|
+
* Delete a trigger
|
|
2541
2620
|
*/
|
|
2542
|
-
async
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
return;
|
|
2546
|
-
}
|
|
2547
|
-
logger.info('Identifying visitor:', email);
|
|
2548
|
-
const result = await this.transport.sendIdentify({
|
|
2549
|
-
workspaceId: this.workspaceId,
|
|
2550
|
-
visitorId: this.visitorId,
|
|
2551
|
-
email,
|
|
2552
|
-
properties: traits,
|
|
2621
|
+
async deleteTrigger(triggerId) {
|
|
2622
|
+
const result = await this.request(`/api/workspaces/${this.workspaceId}/triggers/${triggerId}`, {
|
|
2623
|
+
method: 'DELETE',
|
|
2553
2624
|
});
|
|
2625
|
+
// Remove from cache if successful
|
|
2554
2626
|
if (result.success) {
|
|
2555
|
-
|
|
2556
|
-
this.pendingIdentify = null;
|
|
2557
|
-
}
|
|
2558
|
-
else {
|
|
2559
|
-
logger.error('Failed to identify visitor:', result.error);
|
|
2560
|
-
// Store for retry on next flush
|
|
2561
|
-
this.pendingIdentify = { email, traits };
|
|
2627
|
+
this.triggers.delete(triggerId);
|
|
2562
2628
|
}
|
|
2629
|
+
return result;
|
|
2563
2630
|
}
|
|
2564
2631
|
/**
|
|
2565
|
-
*
|
|
2566
|
-
*/
|
|
2567
|
-
async retryPendingIdentify() {
|
|
2568
|
-
if (!this.pendingIdentify)
|
|
2569
|
-
return;
|
|
2570
|
-
const { email, traits } = this.pendingIdentify;
|
|
2571
|
-
this.pendingIdentify = null;
|
|
2572
|
-
await this.identify(email, traits);
|
|
2573
|
-
}
|
|
2574
|
-
/**
|
|
2575
|
-
* Update consent state
|
|
2576
|
-
*/
|
|
2577
|
-
consent(state) {
|
|
2578
|
-
this.consentManager.update(state);
|
|
2579
|
-
}
|
|
2580
|
-
/**
|
|
2581
|
-
* Get current consent state
|
|
2632
|
+
* Activate a trigger
|
|
2582
2633
|
*/
|
|
2583
|
-
|
|
2584
|
-
return this.
|
|
2634
|
+
async activateTrigger(triggerId) {
|
|
2635
|
+
return this.updateTrigger(triggerId, { isActive: true });
|
|
2585
2636
|
}
|
|
2586
2637
|
/**
|
|
2587
|
-
*
|
|
2638
|
+
* Deactivate a trigger
|
|
2588
2639
|
*/
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
2640
|
+
async deactivateTrigger(triggerId) {
|
|
2641
|
+
return this.updateTrigger(triggerId, { isActive: false });
|
|
2592
2642
|
}
|
|
2643
|
+
// ============================================
|
|
2644
|
+
// EVENT HANDLING (CLIENT-SIDE)
|
|
2645
|
+
// ============================================
|
|
2593
2646
|
/**
|
|
2594
|
-
*
|
|
2647
|
+
* Register a local event listener for client-side triggers
|
|
2648
|
+
* This allows immediate client-side reactions to events
|
|
2595
2649
|
*/
|
|
2596
|
-
|
|
2597
|
-
|
|
2650
|
+
on(eventType, callback) {
|
|
2651
|
+
if (!this.listeners.has(eventType)) {
|
|
2652
|
+
this.listeners.set(eventType, new Set());
|
|
2653
|
+
}
|
|
2654
|
+
this.listeners.get(eventType).add(callback);
|
|
2655
|
+
logger.debug(`Event listener registered: ${eventType}`);
|
|
2598
2656
|
}
|
|
2599
2657
|
/**
|
|
2600
|
-
*
|
|
2658
|
+
* Remove an event listener
|
|
2601
2659
|
*/
|
|
2602
|
-
|
|
2603
|
-
|
|
2660
|
+
off(eventType, callback) {
|
|
2661
|
+
const listeners = this.listeners.get(eventType);
|
|
2662
|
+
if (listeners) {
|
|
2663
|
+
listeners.delete(callback);
|
|
2664
|
+
}
|
|
2604
2665
|
}
|
|
2605
2666
|
/**
|
|
2606
|
-
*
|
|
2667
|
+
* Emit an event (client-side only)
|
|
2668
|
+
* This will trigger any registered local listeners
|
|
2607
2669
|
*/
|
|
2608
|
-
|
|
2609
|
-
|
|
2670
|
+
emit(eventType, data) {
|
|
2671
|
+
logger.debug(`Event emitted: ${eventType}`, data);
|
|
2672
|
+
const listeners = this.listeners.get(eventType);
|
|
2673
|
+
if (listeners) {
|
|
2674
|
+
listeners.forEach(callback => {
|
|
2675
|
+
try {
|
|
2676
|
+
callback(data);
|
|
2677
|
+
}
|
|
2678
|
+
catch (error) {
|
|
2679
|
+
logger.error(`Error in event listener for ${eventType}:`, error);
|
|
2680
|
+
}
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2610
2683
|
}
|
|
2611
2684
|
/**
|
|
2612
|
-
*
|
|
2685
|
+
* Check if conditions are met for a trigger
|
|
2686
|
+
* Supports dynamic field evaluation including custom fields and nested paths
|
|
2613
2687
|
*/
|
|
2614
|
-
|
|
2615
|
-
|
|
2688
|
+
evaluateConditions(conditions, data) {
|
|
2689
|
+
if (!conditions || conditions.length === 0) {
|
|
2690
|
+
return true; // No conditions means always fire
|
|
2691
|
+
}
|
|
2692
|
+
return conditions.every(condition => {
|
|
2693
|
+
// Support dot notation for nested fields (e.g., 'customFields.industry')
|
|
2694
|
+
const fieldValue = condition.field.includes('.')
|
|
2695
|
+
? this.getNestedValue(data, condition.field)
|
|
2696
|
+
: data[condition.field];
|
|
2697
|
+
const targetValue = condition.value;
|
|
2698
|
+
switch (condition.operator) {
|
|
2699
|
+
case 'equals':
|
|
2700
|
+
return fieldValue === targetValue;
|
|
2701
|
+
case 'not_equals':
|
|
2702
|
+
return fieldValue !== targetValue;
|
|
2703
|
+
case 'contains':
|
|
2704
|
+
return String(fieldValue).includes(String(targetValue));
|
|
2705
|
+
case 'greater_than':
|
|
2706
|
+
return Number(fieldValue) > Number(targetValue);
|
|
2707
|
+
case 'less_than':
|
|
2708
|
+
return Number(fieldValue) < Number(targetValue);
|
|
2709
|
+
case 'in':
|
|
2710
|
+
return Array.isArray(targetValue) && targetValue.includes(fieldValue);
|
|
2711
|
+
case 'not_in':
|
|
2712
|
+
return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
|
|
2713
|
+
default:
|
|
2714
|
+
return false;
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2616
2717
|
}
|
|
2617
2718
|
/**
|
|
2618
|
-
*
|
|
2719
|
+
* Execute actions for a triggered event (client-side preview)
|
|
2720
|
+
* Note: Actual execution happens on the backend
|
|
2619
2721
|
*/
|
|
2620
|
-
async
|
|
2621
|
-
|
|
2622
|
-
|
|
2722
|
+
async executeActions(trigger, data) {
|
|
2723
|
+
logger.info(`Executing actions for trigger: ${trigger.name}`);
|
|
2724
|
+
for (const action of trigger.actions) {
|
|
2725
|
+
try {
|
|
2726
|
+
await this.executeAction(action, data);
|
|
2727
|
+
}
|
|
2728
|
+
catch (error) {
|
|
2729
|
+
logger.error(`Failed to execute action:`, error);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2623
2732
|
}
|
|
2624
2733
|
/**
|
|
2625
|
-
*
|
|
2734
|
+
* Execute a single action
|
|
2626
2735
|
*/
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2736
|
+
async executeAction(action, data) {
|
|
2737
|
+
switch (action.type) {
|
|
2738
|
+
case 'send_email':
|
|
2739
|
+
await this.executeSendEmail(action, data);
|
|
2740
|
+
break;
|
|
2741
|
+
case 'webhook':
|
|
2742
|
+
await this.executeWebhook(action, data);
|
|
2743
|
+
break;
|
|
2744
|
+
case 'create_task':
|
|
2745
|
+
await this.executeCreateTask(action, data);
|
|
2746
|
+
break;
|
|
2747
|
+
case 'update_contact':
|
|
2748
|
+
await this.executeUpdateContact(action, data);
|
|
2749
|
+
break;
|
|
2750
|
+
default:
|
|
2751
|
+
logger.warn(`Unknown action type:`, action);
|
|
2752
|
+
}
|
|
2633
2753
|
}
|
|
2634
2754
|
/**
|
|
2635
|
-
*
|
|
2755
|
+
* Execute send email action (via backend API)
|
|
2636
2756
|
*/
|
|
2637
|
-
|
|
2638
|
-
logger.
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2757
|
+
async executeSendEmail(action, data) {
|
|
2758
|
+
logger.debug('Sending email:', action);
|
|
2759
|
+
const payload = {
|
|
2760
|
+
to: this.replaceVariables(action.to, data),
|
|
2761
|
+
subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
|
|
2762
|
+
body: action.body ? this.replaceVariables(action.body, data) : undefined,
|
|
2763
|
+
templateId: action.templateId,
|
|
2764
|
+
cc: action.cc,
|
|
2765
|
+
bcc: action.bcc,
|
|
2766
|
+
from: action.from,
|
|
2767
|
+
delayMinutes: action.delayMinutes,
|
|
2768
|
+
};
|
|
2769
|
+
await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
2770
|
+
method: 'POST',
|
|
2771
|
+
body: JSON.stringify(payload),
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Execute webhook action
|
|
2776
|
+
*/
|
|
2777
|
+
async executeWebhook(action, data) {
|
|
2778
|
+
logger.debug('Calling webhook:', action.url);
|
|
2779
|
+
const body = action.body ? this.replaceVariables(action.body, data) : JSON.stringify(data);
|
|
2780
|
+
await fetch(action.url, {
|
|
2781
|
+
method: action.method,
|
|
2782
|
+
headers: {
|
|
2783
|
+
'Content-Type': 'application/json',
|
|
2784
|
+
...action.headers,
|
|
2785
|
+
},
|
|
2786
|
+
body,
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
/**
|
|
2790
|
+
* Execute create task action
|
|
2791
|
+
*/
|
|
2792
|
+
async executeCreateTask(action, data) {
|
|
2793
|
+
logger.debug('Creating task:', action.title);
|
|
2794
|
+
const dueDate = action.dueDays
|
|
2795
|
+
? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
|
|
2796
|
+
: undefined;
|
|
2797
|
+
await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
2798
|
+
method: 'POST',
|
|
2799
|
+
body: JSON.stringify({
|
|
2800
|
+
title: this.replaceVariables(action.title, data),
|
|
2801
|
+
description: action.description ? this.replaceVariables(action.description, data) : undefined,
|
|
2802
|
+
priority: action.priority,
|
|
2803
|
+
dueDate,
|
|
2804
|
+
assignedTo: action.assignedTo,
|
|
2805
|
+
relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
|
|
2806
|
+
}),
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Execute update contact action
|
|
2811
|
+
*/
|
|
2812
|
+
async executeUpdateContact(action, data) {
|
|
2813
|
+
const contactId = data.contactId || data._id;
|
|
2814
|
+
if (!contactId) {
|
|
2815
|
+
logger.warn('Cannot update contact: no contactId in data');
|
|
2816
|
+
return;
|
|
2667
2817
|
}
|
|
2668
|
-
|
|
2669
|
-
this.
|
|
2670
|
-
|
|
2671
|
-
|
|
2818
|
+
logger.debug('Updating contact:', contactId);
|
|
2819
|
+
await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
2820
|
+
method: 'PUT',
|
|
2821
|
+
body: JSON.stringify(action.updates),
|
|
2822
|
+
});
|
|
2672
2823
|
}
|
|
2673
2824
|
/**
|
|
2674
|
-
*
|
|
2825
|
+
* Replace variables in a string template
|
|
2826
|
+
* Supports syntax like {{contact.email}}, {{opportunity.value}}
|
|
2675
2827
|
*/
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2828
|
+
replaceVariables(template, data) {
|
|
2829
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
2830
|
+
const value = this.getNestedValue(data, path.trim());
|
|
2831
|
+
return value !== undefined ? String(value) : match;
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
/**
|
|
2835
|
+
* Get nested value from object using dot notation
|
|
2836
|
+
* Supports dynamic field access including custom fields
|
|
2837
|
+
*/
|
|
2838
|
+
getNestedValue(obj, path) {
|
|
2839
|
+
return path.split('.').reduce((current, key) => {
|
|
2840
|
+
return current !== null && current !== undefined && typeof current === 'object'
|
|
2841
|
+
? current[key]
|
|
2842
|
+
: undefined;
|
|
2843
|
+
}, obj);
|
|
2844
|
+
}
|
|
2845
|
+
/**
|
|
2846
|
+
* Extract all available field paths from a data object
|
|
2847
|
+
* Useful for dynamic field discovery based on platform-specific attributes
|
|
2848
|
+
* @param obj - The data object to extract fields from
|
|
2849
|
+
* @param prefix - Internal use for nested paths
|
|
2850
|
+
* @param maxDepth - Maximum depth to traverse (default: 3)
|
|
2851
|
+
* @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
|
|
2852
|
+
*/
|
|
2853
|
+
extractAvailableFields(obj, prefix = '', maxDepth = 3) {
|
|
2854
|
+
if (maxDepth <= 0)
|
|
2855
|
+
return [];
|
|
2856
|
+
const fields = [];
|
|
2857
|
+
for (const key in obj) {
|
|
2858
|
+
if (!obj.hasOwnProperty(key))
|
|
2859
|
+
continue;
|
|
2860
|
+
const value = obj[key];
|
|
2861
|
+
const fieldPath = prefix ? `${prefix}.${key}` : key;
|
|
2862
|
+
fields.push(fieldPath);
|
|
2863
|
+
// Recursively traverse nested objects
|
|
2864
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
2865
|
+
const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
|
|
2866
|
+
fields.push(...nestedFields);
|
|
2684
2867
|
}
|
|
2685
2868
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2869
|
+
return fields;
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* Get available fields from sample data
|
|
2873
|
+
* Helps with dynamic field detection for platform-specific attributes
|
|
2874
|
+
* @param sampleData - Sample data object to analyze
|
|
2875
|
+
* @returns Array of available field paths
|
|
2876
|
+
*/
|
|
2877
|
+
getAvailableFields(sampleData) {
|
|
2878
|
+
return this.extractAvailableFields(sampleData);
|
|
2879
|
+
}
|
|
2880
|
+
// ============================================
|
|
2881
|
+
// HELPER METHODS FOR COMMON PATTERNS
|
|
2882
|
+
// ============================================
|
|
2883
|
+
/**
|
|
2884
|
+
* Create a simple email trigger
|
|
2885
|
+
* Helper method for common use case
|
|
2886
|
+
*/
|
|
2887
|
+
async createEmailTrigger(config) {
|
|
2888
|
+
return this.createTrigger({
|
|
2889
|
+
name: config.name,
|
|
2890
|
+
eventType: config.eventType,
|
|
2891
|
+
conditions: config.conditions,
|
|
2892
|
+
actions: [
|
|
2893
|
+
{
|
|
2894
|
+
type: 'send_email',
|
|
2895
|
+
to: config.to,
|
|
2896
|
+
subject: config.subject,
|
|
2897
|
+
body: config.body,
|
|
2898
|
+
},
|
|
2899
|
+
],
|
|
2900
|
+
isActive: true,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
/**
|
|
2904
|
+
* Create a task creation trigger
|
|
2905
|
+
*/
|
|
2906
|
+
async createTaskTrigger(config) {
|
|
2907
|
+
return this.createTrigger({
|
|
2908
|
+
name: config.name,
|
|
2909
|
+
eventType: config.eventType,
|
|
2910
|
+
conditions: config.conditions,
|
|
2911
|
+
actions: [
|
|
2912
|
+
{
|
|
2913
|
+
type: 'create_task',
|
|
2914
|
+
title: config.taskTitle,
|
|
2915
|
+
description: config.taskDescription,
|
|
2916
|
+
priority: config.priority,
|
|
2917
|
+
dueDays: config.dueDays,
|
|
2918
|
+
},
|
|
2919
|
+
],
|
|
2920
|
+
isActive: true,
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
/**
|
|
2924
|
+
* Create a webhook trigger
|
|
2925
|
+
*/
|
|
2926
|
+
async createWebhookTrigger(config) {
|
|
2927
|
+
return this.createTrigger({
|
|
2928
|
+
name: config.name,
|
|
2929
|
+
eventType: config.eventType,
|
|
2930
|
+
conditions: config.conditions,
|
|
2931
|
+
actions: [
|
|
2932
|
+
{
|
|
2933
|
+
type: 'webhook',
|
|
2934
|
+
url: config.webhookUrl,
|
|
2935
|
+
method: config.method || 'POST',
|
|
2936
|
+
},
|
|
2937
|
+
],
|
|
2938
|
+
isActive: true,
|
|
2939
|
+
});
|
|
2690
2940
|
}
|
|
2691
2941
|
}
|
|
2692
2942
|
|
|
2693
2943
|
/**
|
|
2694
|
-
* Clianta SDK -
|
|
2695
|
-
*
|
|
2944
|
+
* Clianta SDK - CRM API Client
|
|
2945
|
+
* @see SDK_VERSION in core/config.ts
|
|
2696
2946
|
*/
|
|
2697
2947
|
/**
|
|
2698
|
-
*
|
|
2699
|
-
* Handles event-driven automation based on CRM actions
|
|
2700
|
-
*
|
|
2701
|
-
* Similar to:
|
|
2702
|
-
* - Salesforce: Process Builder, Flow Automation
|
|
2703
|
-
* - HubSpot: Workflows, Email Sequences
|
|
2704
|
-
* - Pipedrive: Workflow Automation
|
|
2948
|
+
* CRM API Client for managing contacts and opportunities
|
|
2705
2949
|
*/
|
|
2706
|
-
class
|
|
2707
|
-
constructor(apiEndpoint, workspaceId, authToken) {
|
|
2708
|
-
this.triggers = new Map();
|
|
2709
|
-
this.listeners = new Map();
|
|
2950
|
+
class CRMClient {
|
|
2951
|
+
constructor(apiEndpoint, workspaceId, authToken, apiKey) {
|
|
2710
2952
|
this.apiEndpoint = apiEndpoint;
|
|
2711
2953
|
this.workspaceId = workspaceId;
|
|
2712
2954
|
this.authToken = authToken;
|
|
2955
|
+
this.apiKey = apiKey;
|
|
2956
|
+
this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
|
|
2713
2957
|
}
|
|
2714
2958
|
/**
|
|
2715
|
-
* Set authentication token
|
|
2959
|
+
* Set authentication token for API requests (user JWT)
|
|
2716
2960
|
*/
|
|
2717
2961
|
setAuthToken(token) {
|
|
2718
2962
|
this.authToken = token;
|
|
2963
|
+
this.apiKey = undefined;
|
|
2964
|
+
this.triggers.setAuthToken(token);
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Set workspace API key for server-to-server requests.
|
|
2968
|
+
* Use this instead of setAuthToken when integrating from an external app.
|
|
2969
|
+
*/
|
|
2970
|
+
setApiKey(key) {
|
|
2971
|
+
this.apiKey = key;
|
|
2972
|
+
this.authToken = undefined;
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* Validate required parameter exists
|
|
2976
|
+
* @throws {Error} if value is null/undefined or empty string
|
|
2977
|
+
*/
|
|
2978
|
+
validateRequired(param, value, methodName) {
|
|
2979
|
+
if (value === null || value === undefined || value === '') {
|
|
2980
|
+
throw new Error(`[CRMClient.${methodName}] ${param} is required`);
|
|
2981
|
+
}
|
|
2719
2982
|
}
|
|
2720
2983
|
/**
|
|
2721
2984
|
* Make authenticated API request
|
|
@@ -2726,970 +2989,1206 @@
|
|
|
2726
2989
|
'Content-Type': 'application/json',
|
|
2727
2990
|
...(options.headers || {}),
|
|
2728
2991
|
};
|
|
2729
|
-
if (this.
|
|
2992
|
+
if (this.apiKey) {
|
|
2993
|
+
headers['X-Api-Key'] = this.apiKey;
|
|
2994
|
+
}
|
|
2995
|
+
else if (this.authToken) {
|
|
2996
|
+
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
2997
|
+
}
|
|
2998
|
+
try {
|
|
2999
|
+
const response = await fetch(url, {
|
|
3000
|
+
...options,
|
|
3001
|
+
headers,
|
|
3002
|
+
});
|
|
3003
|
+
const data = await response.json();
|
|
3004
|
+
if (!response.ok) {
|
|
3005
|
+
return {
|
|
3006
|
+
success: false,
|
|
3007
|
+
error: data.message || 'Request failed',
|
|
3008
|
+
status: response.status,
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
return {
|
|
3012
|
+
success: true,
|
|
3013
|
+
data: data.data || data,
|
|
3014
|
+
status: response.status,
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
catch (error) {
|
|
3018
|
+
return {
|
|
3019
|
+
success: false,
|
|
3020
|
+
error: error instanceof Error ? error.message : 'Network error',
|
|
3021
|
+
status: 0,
|
|
3022
|
+
};
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
// ============================================
|
|
3026
|
+
// INBOUND EVENTS API (API-key authenticated)
|
|
3027
|
+
// ============================================
|
|
3028
|
+
/**
|
|
3029
|
+
* Send an inbound event from an external app (e.g. user signup on client website).
|
|
3030
|
+
* Requires the client to be initialized with an API key via setApiKey() or the constructor.
|
|
3031
|
+
*
|
|
3032
|
+
* The contact is upserted in the CRM and matching workflow automations fire automatically.
|
|
3033
|
+
*
|
|
3034
|
+
* @example
|
|
3035
|
+
* const crm = new CRMClient('https://api.clianta.online', 'WORKSPACE_ID');
|
|
3036
|
+
* crm.setApiKey('mm_live_...');
|
|
3037
|
+
*
|
|
3038
|
+
* await crm.sendEvent({
|
|
3039
|
+
* event: 'user.registered',
|
|
3040
|
+
* contact: { email: 'alice@example.com', firstName: 'Alice' },
|
|
3041
|
+
* data: { plan: 'free', signupSource: 'homepage' },
|
|
3042
|
+
* });
|
|
3043
|
+
*/
|
|
3044
|
+
async sendEvent(payload) {
|
|
3045
|
+
const url = `${this.apiEndpoint}/api/public/events`;
|
|
3046
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
3047
|
+
if (this.apiKey) {
|
|
3048
|
+
headers['X-Api-Key'] = this.apiKey;
|
|
3049
|
+
}
|
|
3050
|
+
else if (this.authToken) {
|
|
2730
3051
|
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
2731
3052
|
}
|
|
2732
3053
|
try {
|
|
2733
3054
|
const response = await fetch(url, {
|
|
2734
|
-
|
|
3055
|
+
method: 'POST',
|
|
2735
3056
|
headers,
|
|
3057
|
+
body: JSON.stringify(payload),
|
|
2736
3058
|
});
|
|
2737
3059
|
const data = await response.json();
|
|
2738
3060
|
if (!response.ok) {
|
|
2739
3061
|
return {
|
|
2740
3062
|
success: false,
|
|
2741
|
-
|
|
2742
|
-
|
|
3063
|
+
contactCreated: false,
|
|
3064
|
+
event: payload.event,
|
|
3065
|
+
error: data.error || 'Request failed',
|
|
2743
3066
|
};
|
|
2744
3067
|
}
|
|
2745
3068
|
return {
|
|
2746
|
-
success:
|
|
2747
|
-
|
|
2748
|
-
|
|
3069
|
+
success: data.success,
|
|
3070
|
+
contactCreated: data.contactCreated,
|
|
3071
|
+
contactId: data.contactId,
|
|
3072
|
+
event: data.event,
|
|
2749
3073
|
};
|
|
2750
3074
|
}
|
|
2751
3075
|
catch (error) {
|
|
2752
3076
|
return {
|
|
2753
3077
|
success: false,
|
|
3078
|
+
contactCreated: false,
|
|
3079
|
+
event: payload.event,
|
|
2754
3080
|
error: error instanceof Error ? error.message : 'Network error',
|
|
2755
|
-
status: 0,
|
|
2756
3081
|
};
|
|
2757
3082
|
}
|
|
2758
3083
|
}
|
|
2759
3084
|
// ============================================
|
|
2760
|
-
//
|
|
3085
|
+
// CONTACTS API
|
|
2761
3086
|
// ============================================
|
|
2762
3087
|
/**
|
|
2763
|
-
* Get all
|
|
3088
|
+
* Get all contacts with pagination
|
|
2764
3089
|
*/
|
|
2765
|
-
async
|
|
2766
|
-
|
|
3090
|
+
async getContacts(params) {
|
|
3091
|
+
const queryParams = new URLSearchParams();
|
|
3092
|
+
if (params?.page)
|
|
3093
|
+
queryParams.set('page', params.page.toString());
|
|
3094
|
+
if (params?.limit)
|
|
3095
|
+
queryParams.set('limit', params.limit.toString());
|
|
3096
|
+
if (params?.search)
|
|
3097
|
+
queryParams.set('search', params.search);
|
|
3098
|
+
if (params?.status)
|
|
3099
|
+
queryParams.set('status', params.status);
|
|
3100
|
+
const query = queryParams.toString();
|
|
3101
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${query ? `?${query}` : ''}`;
|
|
3102
|
+
return this.request(endpoint);
|
|
2767
3103
|
}
|
|
2768
3104
|
/**
|
|
2769
|
-
* Get a single
|
|
3105
|
+
* Get a single contact by ID
|
|
2770
3106
|
*/
|
|
2771
|
-
async
|
|
2772
|
-
|
|
3107
|
+
async getContact(contactId) {
|
|
3108
|
+
this.validateRequired('contactId', contactId, 'getContact');
|
|
3109
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
|
|
2773
3110
|
}
|
|
2774
3111
|
/**
|
|
2775
|
-
* Create a new
|
|
3112
|
+
* Create a new contact
|
|
2776
3113
|
*/
|
|
2777
|
-
async
|
|
2778
|
-
|
|
3114
|
+
async createContact(contact) {
|
|
3115
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts`, {
|
|
2779
3116
|
method: 'POST',
|
|
2780
|
-
body: JSON.stringify(
|
|
3117
|
+
body: JSON.stringify(contact),
|
|
2781
3118
|
});
|
|
2782
|
-
// Cache the trigger locally if successful
|
|
2783
|
-
if (result.success && result.data?._id) {
|
|
2784
|
-
this.triggers.set(result.data._id, result.data);
|
|
2785
|
-
}
|
|
2786
|
-
return result;
|
|
2787
3119
|
}
|
|
2788
3120
|
/**
|
|
2789
|
-
* Update an existing
|
|
3121
|
+
* Update an existing contact
|
|
2790
3122
|
*/
|
|
2791
|
-
async
|
|
2792
|
-
|
|
3123
|
+
async updateContact(contactId, updates) {
|
|
3124
|
+
this.validateRequired('contactId', contactId, 'updateContact');
|
|
3125
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
2793
3126
|
method: 'PUT',
|
|
2794
3127
|
body: JSON.stringify(updates),
|
|
2795
3128
|
});
|
|
2796
|
-
// Update cache if successful
|
|
2797
|
-
if (result.success && result.data?._id) {
|
|
2798
|
-
this.triggers.set(result.data._id, result.data);
|
|
2799
|
-
}
|
|
2800
|
-
return result;
|
|
2801
3129
|
}
|
|
2802
3130
|
/**
|
|
2803
|
-
* Delete a
|
|
3131
|
+
* Delete a contact
|
|
2804
3132
|
*/
|
|
2805
|
-
async
|
|
2806
|
-
|
|
3133
|
+
async deleteContact(contactId) {
|
|
3134
|
+
this.validateRequired('contactId', contactId, 'deleteContact');
|
|
3135
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
2807
3136
|
method: 'DELETE',
|
|
2808
3137
|
});
|
|
2809
|
-
// Remove from cache if successful
|
|
2810
|
-
if (result.success) {
|
|
2811
|
-
this.triggers.delete(triggerId);
|
|
2812
|
-
}
|
|
2813
|
-
return result;
|
|
2814
|
-
}
|
|
2815
|
-
/**
|
|
2816
|
-
* Activate a trigger
|
|
2817
|
-
*/
|
|
2818
|
-
async activateTrigger(triggerId) {
|
|
2819
|
-
return this.updateTrigger(triggerId, { isActive: true });
|
|
2820
|
-
}
|
|
2821
|
-
/**
|
|
2822
|
-
* Deactivate a trigger
|
|
2823
|
-
*/
|
|
2824
|
-
async deactivateTrigger(triggerId) {
|
|
2825
|
-
return this.updateTrigger(triggerId, { isActive: false });
|
|
2826
3138
|
}
|
|
2827
3139
|
// ============================================
|
|
2828
|
-
//
|
|
3140
|
+
// OPPORTUNITIES API
|
|
2829
3141
|
// ============================================
|
|
2830
3142
|
/**
|
|
2831
|
-
*
|
|
2832
|
-
* This allows immediate client-side reactions to events
|
|
2833
|
-
*/
|
|
2834
|
-
on(eventType, callback) {
|
|
2835
|
-
if (!this.listeners.has(eventType)) {
|
|
2836
|
-
this.listeners.set(eventType, new Set());
|
|
2837
|
-
}
|
|
2838
|
-
this.listeners.get(eventType).add(callback);
|
|
2839
|
-
logger.debug(`Event listener registered: ${eventType}`);
|
|
2840
|
-
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Remove an event listener
|
|
3143
|
+
* Get all opportunities with pagination
|
|
2843
3144
|
*/
|
|
2844
|
-
|
|
2845
|
-
const
|
|
2846
|
-
if (
|
|
2847
|
-
|
|
2848
|
-
|
|
3145
|
+
async getOpportunities(params) {
|
|
3146
|
+
const queryParams = new URLSearchParams();
|
|
3147
|
+
if (params?.page)
|
|
3148
|
+
queryParams.set('page', params.page.toString());
|
|
3149
|
+
if (params?.limit)
|
|
3150
|
+
queryParams.set('limit', params.limit.toString());
|
|
3151
|
+
if (params?.pipelineId)
|
|
3152
|
+
queryParams.set('pipelineId', params.pipelineId);
|
|
3153
|
+
if (params?.stageId)
|
|
3154
|
+
queryParams.set('stageId', params.stageId);
|
|
3155
|
+
const query = queryParams.toString();
|
|
3156
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities${query ? `?${query}` : ''}`;
|
|
3157
|
+
return this.request(endpoint);
|
|
2849
3158
|
}
|
|
2850
3159
|
/**
|
|
2851
|
-
*
|
|
2852
|
-
* This will trigger any registered local listeners
|
|
3160
|
+
* Get a single opportunity by ID
|
|
2853
3161
|
*/
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const listeners = this.listeners.get(eventType);
|
|
2857
|
-
if (listeners) {
|
|
2858
|
-
listeners.forEach(callback => {
|
|
2859
|
-
try {
|
|
2860
|
-
callback(data);
|
|
2861
|
-
}
|
|
2862
|
-
catch (error) {
|
|
2863
|
-
logger.error(`Error in event listener for ${eventType}:`, error);
|
|
2864
|
-
}
|
|
2865
|
-
});
|
|
2866
|
-
}
|
|
3162
|
+
async getOpportunity(opportunityId) {
|
|
3163
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`);
|
|
2867
3164
|
}
|
|
2868
3165
|
/**
|
|
2869
|
-
*
|
|
2870
|
-
* Supports dynamic field evaluation including custom fields and nested paths
|
|
3166
|
+
* Create a new opportunity
|
|
2871
3167
|
*/
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
return conditions.every(condition => {
|
|
2877
|
-
// Support dot notation for nested fields (e.g., 'customFields.industry')
|
|
2878
|
-
const fieldValue = condition.field.includes('.')
|
|
2879
|
-
? this.getNestedValue(data, condition.field)
|
|
2880
|
-
: data[condition.field];
|
|
2881
|
-
const targetValue = condition.value;
|
|
2882
|
-
switch (condition.operator) {
|
|
2883
|
-
case 'equals':
|
|
2884
|
-
return fieldValue === targetValue;
|
|
2885
|
-
case 'not_equals':
|
|
2886
|
-
return fieldValue !== targetValue;
|
|
2887
|
-
case 'contains':
|
|
2888
|
-
return String(fieldValue).includes(String(targetValue));
|
|
2889
|
-
case 'greater_than':
|
|
2890
|
-
return Number(fieldValue) > Number(targetValue);
|
|
2891
|
-
case 'less_than':
|
|
2892
|
-
return Number(fieldValue) < Number(targetValue);
|
|
2893
|
-
case 'in':
|
|
2894
|
-
return Array.isArray(targetValue) && targetValue.includes(fieldValue);
|
|
2895
|
-
case 'not_in':
|
|
2896
|
-
return Array.isArray(targetValue) && !targetValue.includes(fieldValue);
|
|
2897
|
-
default:
|
|
2898
|
-
return false;
|
|
2899
|
-
}
|
|
3168
|
+
async createOpportunity(opportunity) {
|
|
3169
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities`, {
|
|
3170
|
+
method: 'POST',
|
|
3171
|
+
body: JSON.stringify(opportunity),
|
|
2900
3172
|
});
|
|
2901
3173
|
}
|
|
2902
3174
|
/**
|
|
2903
|
-
*
|
|
2904
|
-
* Note: Actual execution happens on the backend
|
|
3175
|
+
* Update an existing opportunity
|
|
2905
3176
|
*/
|
|
2906
|
-
async
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
}
|
|
2912
|
-
catch (error) {
|
|
2913
|
-
logger.error(`Failed to execute action:`, error);
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
3177
|
+
async updateOpportunity(opportunityId, updates) {
|
|
3178
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
|
|
3179
|
+
method: 'PUT',
|
|
3180
|
+
body: JSON.stringify(updates),
|
|
3181
|
+
});
|
|
2916
3182
|
}
|
|
2917
3183
|
/**
|
|
2918
|
-
*
|
|
3184
|
+
* Delete an opportunity
|
|
2919
3185
|
*/
|
|
2920
|
-
async
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
break;
|
|
2925
|
-
case 'webhook':
|
|
2926
|
-
await this.executeWebhook(action, data);
|
|
2927
|
-
break;
|
|
2928
|
-
case 'create_task':
|
|
2929
|
-
await this.executeCreateTask(action, data);
|
|
2930
|
-
break;
|
|
2931
|
-
case 'update_contact':
|
|
2932
|
-
await this.executeUpdateContact(action, data);
|
|
2933
|
-
break;
|
|
2934
|
-
default:
|
|
2935
|
-
logger.warn(`Unknown action type:`, action);
|
|
2936
|
-
}
|
|
3186
|
+
async deleteOpportunity(opportunityId) {
|
|
3187
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}`, {
|
|
3188
|
+
method: 'DELETE',
|
|
3189
|
+
});
|
|
2937
3190
|
}
|
|
2938
3191
|
/**
|
|
2939
|
-
*
|
|
3192
|
+
* Move opportunity to a different stage
|
|
2940
3193
|
*/
|
|
2941
|
-
async
|
|
2942
|
-
|
|
2943
|
-
const payload = {
|
|
2944
|
-
to: this.replaceVariables(action.to, data),
|
|
2945
|
-
subject: action.subject ? this.replaceVariables(action.subject, data) : undefined,
|
|
2946
|
-
body: action.body ? this.replaceVariables(action.body, data) : undefined,
|
|
2947
|
-
templateId: action.templateId,
|
|
2948
|
-
cc: action.cc,
|
|
2949
|
-
bcc: action.bcc,
|
|
2950
|
-
from: action.from,
|
|
2951
|
-
delayMinutes: action.delayMinutes,
|
|
2952
|
-
};
|
|
2953
|
-
await this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
3194
|
+
async moveOpportunity(opportunityId, stageId) {
|
|
3195
|
+
return this.request(`/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/move`, {
|
|
2954
3196
|
method: 'POST',
|
|
2955
|
-
body: JSON.stringify(
|
|
3197
|
+
body: JSON.stringify({ stageId }),
|
|
2956
3198
|
});
|
|
2957
3199
|
}
|
|
3200
|
+
// ============================================
|
|
3201
|
+
// COMPANIES API
|
|
3202
|
+
// ============================================
|
|
2958
3203
|
/**
|
|
2959
|
-
*
|
|
3204
|
+
* Get all companies with pagination
|
|
2960
3205
|
*/
|
|
2961
|
-
async
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
3206
|
+
async getCompanies(params) {
|
|
3207
|
+
const queryParams = new URLSearchParams();
|
|
3208
|
+
if (params?.page)
|
|
3209
|
+
queryParams.set('page', params.page.toString());
|
|
3210
|
+
if (params?.limit)
|
|
3211
|
+
queryParams.set('limit', params.limit.toString());
|
|
3212
|
+
if (params?.search)
|
|
3213
|
+
queryParams.set('search', params.search);
|
|
3214
|
+
if (params?.status)
|
|
3215
|
+
queryParams.set('status', params.status);
|
|
3216
|
+
if (params?.industry)
|
|
3217
|
+
queryParams.set('industry', params.industry);
|
|
3218
|
+
const query = queryParams.toString();
|
|
3219
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies${query ? `?${query}` : ''}`;
|
|
3220
|
+
return this.request(endpoint);
|
|
2972
3221
|
}
|
|
2973
3222
|
/**
|
|
2974
|
-
*
|
|
3223
|
+
* Get a single company by ID
|
|
2975
3224
|
*/
|
|
2976
|
-
async
|
|
2977
|
-
|
|
2978
|
-
const dueDate = action.dueDays
|
|
2979
|
-
? new Date(Date.now() + action.dueDays * 24 * 60 * 60 * 1000).toISOString()
|
|
2980
|
-
: undefined;
|
|
2981
|
-
await this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
2982
|
-
method: 'POST',
|
|
2983
|
-
body: JSON.stringify({
|
|
2984
|
-
title: this.replaceVariables(action.title, data),
|
|
2985
|
-
description: action.description ? this.replaceVariables(action.description, data) : undefined,
|
|
2986
|
-
priority: action.priority,
|
|
2987
|
-
dueDate,
|
|
2988
|
-
assignedTo: action.assignedTo,
|
|
2989
|
-
relatedContactId: typeof data.contactId === 'string' ? data.contactId : undefined,
|
|
2990
|
-
}),
|
|
2991
|
-
});
|
|
3225
|
+
async getCompany(companyId) {
|
|
3226
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`);
|
|
2992
3227
|
}
|
|
2993
3228
|
/**
|
|
2994
|
-
*
|
|
3229
|
+
* Create a new company
|
|
2995
3230
|
*/
|
|
2996
|
-
async
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
return;
|
|
3001
|
-
}
|
|
3002
|
-
logger.debug('Updating contact:', contactId);
|
|
3003
|
-
await this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
3004
|
-
method: 'PUT',
|
|
3005
|
-
body: JSON.stringify(action.updates),
|
|
3231
|
+
async createCompany(company) {
|
|
3232
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies`, {
|
|
3233
|
+
method: 'POST',
|
|
3234
|
+
body: JSON.stringify(company),
|
|
3006
3235
|
});
|
|
3007
3236
|
}
|
|
3008
3237
|
/**
|
|
3009
|
-
*
|
|
3010
|
-
* Supports syntax like {{contact.email}}, {{opportunity.value}}
|
|
3238
|
+
* Update an existing company
|
|
3011
3239
|
*/
|
|
3012
|
-
|
|
3013
|
-
return
|
|
3014
|
-
|
|
3015
|
-
|
|
3240
|
+
async updateCompany(companyId, updates) {
|
|
3241
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
|
|
3242
|
+
method: 'PUT',
|
|
3243
|
+
body: JSON.stringify(updates),
|
|
3016
3244
|
});
|
|
3017
3245
|
}
|
|
3018
3246
|
/**
|
|
3019
|
-
*
|
|
3020
|
-
* Supports dynamic field access including custom fields
|
|
3247
|
+
* Delete a company
|
|
3021
3248
|
*/
|
|
3022
|
-
|
|
3023
|
-
return
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
: undefined;
|
|
3027
|
-
}, obj);
|
|
3249
|
+
async deleteCompany(companyId) {
|
|
3250
|
+
return this.request(`/api/workspaces/${this.workspaceId}/companies/${companyId}`, {
|
|
3251
|
+
method: 'DELETE',
|
|
3252
|
+
});
|
|
3028
3253
|
}
|
|
3029
3254
|
/**
|
|
3030
|
-
*
|
|
3031
|
-
* Useful for dynamic field discovery based on platform-specific attributes
|
|
3032
|
-
* @param obj - The data object to extract fields from
|
|
3033
|
-
* @param prefix - Internal use for nested paths
|
|
3034
|
-
* @param maxDepth - Maximum depth to traverse (default: 3)
|
|
3035
|
-
* @returns Array of field paths (e.g., ['email', 'contact.firstName', 'customFields.industry'])
|
|
3255
|
+
* Get contacts belonging to a company
|
|
3036
3256
|
*/
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
fields.push(fieldPath);
|
|
3047
|
-
// Recursively traverse nested objects
|
|
3048
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
3049
|
-
const nestedFields = this.extractAvailableFields(value, fieldPath, maxDepth - 1);
|
|
3050
|
-
fields.push(...nestedFields);
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
return fields;
|
|
3257
|
+
async getCompanyContacts(companyId, params) {
|
|
3258
|
+
const queryParams = new URLSearchParams();
|
|
3259
|
+
if (params?.page)
|
|
3260
|
+
queryParams.set('page', params.page.toString());
|
|
3261
|
+
if (params?.limit)
|
|
3262
|
+
queryParams.set('limit', params.limit.toString());
|
|
3263
|
+
const query = queryParams.toString();
|
|
3264
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/contacts${query ? `?${query}` : ''}`;
|
|
3265
|
+
return this.request(endpoint);
|
|
3054
3266
|
}
|
|
3055
3267
|
/**
|
|
3056
|
-
* Get
|
|
3057
|
-
* Helps with dynamic field detection for platform-specific attributes
|
|
3058
|
-
* @param sampleData - Sample data object to analyze
|
|
3059
|
-
* @returns Array of available field paths
|
|
3268
|
+
* Get deals/opportunities belonging to a company
|
|
3060
3269
|
*/
|
|
3061
|
-
|
|
3062
|
-
|
|
3270
|
+
async getCompanyDeals(companyId, params) {
|
|
3271
|
+
const queryParams = new URLSearchParams();
|
|
3272
|
+
if (params?.page)
|
|
3273
|
+
queryParams.set('page', params.page.toString());
|
|
3274
|
+
if (params?.limit)
|
|
3275
|
+
queryParams.set('limit', params.limit.toString());
|
|
3276
|
+
const query = queryParams.toString();
|
|
3277
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/companies/${companyId}/deals${query ? `?${query}` : ''}`;
|
|
3278
|
+
return this.request(endpoint);
|
|
3063
3279
|
}
|
|
3064
3280
|
// ============================================
|
|
3065
|
-
//
|
|
3281
|
+
// PIPELINES API
|
|
3066
3282
|
// ============================================
|
|
3067
3283
|
/**
|
|
3068
|
-
*
|
|
3069
|
-
* Helper method for common use case
|
|
3284
|
+
* Get all pipelines
|
|
3070
3285
|
*/
|
|
3071
|
-
async
|
|
3072
|
-
return this.
|
|
3073
|
-
name: config.name,
|
|
3074
|
-
eventType: config.eventType,
|
|
3075
|
-
conditions: config.conditions,
|
|
3076
|
-
actions: [
|
|
3077
|
-
{
|
|
3078
|
-
type: 'send_email',
|
|
3079
|
-
to: config.to,
|
|
3080
|
-
subject: config.subject,
|
|
3081
|
-
body: config.body,
|
|
3082
|
-
},
|
|
3083
|
-
],
|
|
3084
|
-
isActive: true,
|
|
3085
|
-
});
|
|
3286
|
+
async getPipelines() {
|
|
3287
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines`);
|
|
3086
3288
|
}
|
|
3087
3289
|
/**
|
|
3088
|
-
*
|
|
3290
|
+
* Get a single pipeline by ID
|
|
3089
3291
|
*/
|
|
3090
|
-
async
|
|
3091
|
-
return this.
|
|
3092
|
-
name: config.name,
|
|
3093
|
-
eventType: config.eventType,
|
|
3094
|
-
conditions: config.conditions,
|
|
3095
|
-
actions: [
|
|
3096
|
-
{
|
|
3097
|
-
type: 'create_task',
|
|
3098
|
-
title: config.taskTitle,
|
|
3099
|
-
description: config.taskDescription,
|
|
3100
|
-
priority: config.priority,
|
|
3101
|
-
dueDays: config.dueDays,
|
|
3102
|
-
},
|
|
3103
|
-
],
|
|
3104
|
-
isActive: true,
|
|
3105
|
-
});
|
|
3292
|
+
async getPipeline(pipelineId) {
|
|
3293
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`);
|
|
3106
3294
|
}
|
|
3107
3295
|
/**
|
|
3108
|
-
* Create a
|
|
3296
|
+
* Create a new pipeline
|
|
3109
3297
|
*/
|
|
3110
|
-
async
|
|
3111
|
-
return this.
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
conditions: config.conditions,
|
|
3115
|
-
actions: [
|
|
3116
|
-
{
|
|
3117
|
-
type: 'webhook',
|
|
3118
|
-
url: config.webhookUrl,
|
|
3119
|
-
method: config.method || 'POST',
|
|
3120
|
-
},
|
|
3121
|
-
],
|
|
3122
|
-
isActive: true,
|
|
3298
|
+
async createPipeline(pipeline) {
|
|
3299
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines`, {
|
|
3300
|
+
method: 'POST',
|
|
3301
|
+
body: JSON.stringify(pipeline),
|
|
3123
3302
|
});
|
|
3124
3303
|
}
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
/**
|
|
3128
|
-
* Clianta SDK - CRM API Client
|
|
3129
|
-
* @see SDK_VERSION in core/config.ts
|
|
3130
|
-
*/
|
|
3131
|
-
/**
|
|
3132
|
-
* CRM API Client for managing contacts and opportunities
|
|
3133
|
-
*/
|
|
3134
|
-
class CRMClient {
|
|
3135
|
-
constructor(apiEndpoint, workspaceId, authToken) {
|
|
3136
|
-
this.apiEndpoint = apiEndpoint;
|
|
3137
|
-
this.workspaceId = workspaceId;
|
|
3138
|
-
this.authToken = authToken;
|
|
3139
|
-
this.triggers = new EventTriggersManager(apiEndpoint, workspaceId, authToken);
|
|
3140
|
-
}
|
|
3141
3304
|
/**
|
|
3142
|
-
*
|
|
3143
|
-
*/
|
|
3144
|
-
setAuthToken(token) {
|
|
3145
|
-
this.authToken = token;
|
|
3146
|
-
this.triggers.setAuthToken(token);
|
|
3147
|
-
}
|
|
3148
|
-
/**
|
|
3149
|
-
* Validate required parameter exists
|
|
3150
|
-
* @throws {Error} if value is null/undefined or empty string
|
|
3305
|
+
* Update an existing pipeline
|
|
3151
3306
|
*/
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3307
|
+
async updatePipeline(pipelineId, updates) {
|
|
3308
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
|
|
3309
|
+
method: 'PUT',
|
|
3310
|
+
body: JSON.stringify(updates),
|
|
3311
|
+
});
|
|
3156
3312
|
}
|
|
3157
3313
|
/**
|
|
3158
|
-
*
|
|
3314
|
+
* Delete a pipeline
|
|
3159
3315
|
*/
|
|
3160
|
-
async
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
...(options.headers || {}),
|
|
3165
|
-
};
|
|
3166
|
-
if (this.authToken) {
|
|
3167
|
-
headers['Authorization'] = `Bearer ${this.authToken}`;
|
|
3168
|
-
}
|
|
3169
|
-
try {
|
|
3170
|
-
const response = await fetch(url, {
|
|
3171
|
-
...options,
|
|
3172
|
-
headers,
|
|
3173
|
-
});
|
|
3174
|
-
const data = await response.json();
|
|
3175
|
-
if (!response.ok) {
|
|
3176
|
-
return {
|
|
3177
|
-
success: false,
|
|
3178
|
-
error: data.message || 'Request failed',
|
|
3179
|
-
status: response.status,
|
|
3180
|
-
};
|
|
3181
|
-
}
|
|
3182
|
-
return {
|
|
3183
|
-
success: true,
|
|
3184
|
-
data: data.data || data,
|
|
3185
|
-
status: response.status,
|
|
3186
|
-
};
|
|
3187
|
-
}
|
|
3188
|
-
catch (error) {
|
|
3189
|
-
return {
|
|
3190
|
-
success: false,
|
|
3191
|
-
error: error instanceof Error ? error.message : 'Network error',
|
|
3192
|
-
status: 0,
|
|
3193
|
-
};
|
|
3194
|
-
}
|
|
3316
|
+
async deletePipeline(pipelineId) {
|
|
3317
|
+
return this.request(`/api/workspaces/${this.workspaceId}/pipelines/${pipelineId}`, {
|
|
3318
|
+
method: 'DELETE',
|
|
3319
|
+
});
|
|
3195
3320
|
}
|
|
3196
3321
|
// ============================================
|
|
3197
|
-
//
|
|
3322
|
+
// TASKS API
|
|
3198
3323
|
// ============================================
|
|
3199
3324
|
/**
|
|
3200
|
-
* Get all
|
|
3325
|
+
* Get all tasks with pagination
|
|
3201
3326
|
*/
|
|
3202
|
-
async
|
|
3327
|
+
async getTasks(params) {
|
|
3203
3328
|
const queryParams = new URLSearchParams();
|
|
3204
3329
|
if (params?.page)
|
|
3205
3330
|
queryParams.set('page', params.page.toString());
|
|
3206
3331
|
if (params?.limit)
|
|
3207
3332
|
queryParams.set('limit', params.limit.toString());
|
|
3208
|
-
if (params?.search)
|
|
3209
|
-
queryParams.set('search', params.search);
|
|
3210
3333
|
if (params?.status)
|
|
3211
3334
|
queryParams.set('status', params.status);
|
|
3335
|
+
if (params?.priority)
|
|
3336
|
+
queryParams.set('priority', params.priority);
|
|
3337
|
+
if (params?.contactId)
|
|
3338
|
+
queryParams.set('contactId', params.contactId);
|
|
3339
|
+
if (params?.companyId)
|
|
3340
|
+
queryParams.set('companyId', params.companyId);
|
|
3341
|
+
if (params?.opportunityId)
|
|
3342
|
+
queryParams.set('opportunityId', params.opportunityId);
|
|
3212
3343
|
const query = queryParams.toString();
|
|
3213
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/
|
|
3344
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/tasks${query ? `?${query}` : ''}`;
|
|
3214
3345
|
return this.request(endpoint);
|
|
3215
3346
|
}
|
|
3216
3347
|
/**
|
|
3217
|
-
* Get a single
|
|
3348
|
+
* Get a single task by ID
|
|
3218
3349
|
*/
|
|
3219
|
-
async
|
|
3220
|
-
this.
|
|
3221
|
-
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`);
|
|
3350
|
+
async getTask(taskId) {
|
|
3351
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`);
|
|
3222
3352
|
}
|
|
3223
3353
|
/**
|
|
3224
|
-
* Create a new
|
|
3354
|
+
* Create a new task
|
|
3225
3355
|
*/
|
|
3226
|
-
async
|
|
3227
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3356
|
+
async createTask(task) {
|
|
3357
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks`, {
|
|
3228
3358
|
method: 'POST',
|
|
3229
|
-
body: JSON.stringify(
|
|
3359
|
+
body: JSON.stringify(task),
|
|
3230
3360
|
});
|
|
3231
3361
|
}
|
|
3232
3362
|
/**
|
|
3233
|
-
* Update an existing
|
|
3363
|
+
* Update an existing task
|
|
3234
3364
|
*/
|
|
3235
|
-
async
|
|
3236
|
-
this.
|
|
3237
|
-
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}`, {
|
|
3365
|
+
async updateTask(taskId, updates) {
|
|
3366
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
|
|
3238
3367
|
method: 'PUT',
|
|
3239
3368
|
body: JSON.stringify(updates),
|
|
3240
3369
|
});
|
|
3241
3370
|
}
|
|
3242
3371
|
/**
|
|
3243
|
-
*
|
|
3372
|
+
* Mark a task as completed
|
|
3244
3373
|
*/
|
|
3245
|
-
async
|
|
3246
|
-
this.
|
|
3247
|
-
|
|
3374
|
+
async completeTask(taskId) {
|
|
3375
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}/complete`, {
|
|
3376
|
+
method: 'PATCH',
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Delete a task
|
|
3381
|
+
*/
|
|
3382
|
+
async deleteTask(taskId) {
|
|
3383
|
+
return this.request(`/api/workspaces/${this.workspaceId}/tasks/${taskId}`, {
|
|
3248
3384
|
method: 'DELETE',
|
|
3249
3385
|
});
|
|
3250
3386
|
}
|
|
3251
3387
|
// ============================================
|
|
3252
|
-
//
|
|
3388
|
+
// ACTIVITIES API
|
|
3253
3389
|
// ============================================
|
|
3254
3390
|
/**
|
|
3255
|
-
* Get
|
|
3391
|
+
* Get activities for a contact
|
|
3256
3392
|
*/
|
|
3257
|
-
async
|
|
3393
|
+
async getContactActivities(contactId, params) {
|
|
3258
3394
|
const queryParams = new URLSearchParams();
|
|
3259
3395
|
if (params?.page)
|
|
3260
3396
|
queryParams.set('page', params.page.toString());
|
|
3261
3397
|
if (params?.limit)
|
|
3262
3398
|
queryParams.set('limit', params.limit.toString());
|
|
3263
|
-
if (params?.
|
|
3264
|
-
queryParams.set('
|
|
3265
|
-
if (params?.stageId)
|
|
3266
|
-
queryParams.set('stageId', params.stageId);
|
|
3399
|
+
if (params?.type)
|
|
3400
|
+
queryParams.set('type', params.type);
|
|
3267
3401
|
const query = queryParams.toString();
|
|
3268
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/
|
|
3402
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
3269
3403
|
return this.request(endpoint);
|
|
3270
3404
|
}
|
|
3271
3405
|
/**
|
|
3272
|
-
* Get
|
|
3406
|
+
* Get activities for an opportunity/deal
|
|
3273
3407
|
*/
|
|
3274
|
-
async
|
|
3275
|
-
|
|
3408
|
+
async getOpportunityActivities(opportunityId, params) {
|
|
3409
|
+
const queryParams = new URLSearchParams();
|
|
3410
|
+
if (params?.page)
|
|
3411
|
+
queryParams.set('page', params.page.toString());
|
|
3412
|
+
if (params?.limit)
|
|
3413
|
+
queryParams.set('limit', params.limit.toString());
|
|
3414
|
+
if (params?.type)
|
|
3415
|
+
queryParams.set('type', params.type);
|
|
3416
|
+
const query = queryParams.toString();
|
|
3417
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
|
|
3418
|
+
return this.request(endpoint);
|
|
3276
3419
|
}
|
|
3277
3420
|
/**
|
|
3278
|
-
* Create a new
|
|
3421
|
+
* Create a new activity
|
|
3279
3422
|
*/
|
|
3280
|
-
async
|
|
3281
|
-
|
|
3423
|
+
async createActivity(activity) {
|
|
3424
|
+
// Determine the correct endpoint based on related entity
|
|
3425
|
+
let endpoint;
|
|
3426
|
+
if (activity.opportunityId) {
|
|
3427
|
+
endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
|
|
3428
|
+
}
|
|
3429
|
+
else if (activity.contactId) {
|
|
3430
|
+
endpoint = `/api/workspaces/${this.workspaceId}/contacts/${activity.contactId}/activities`;
|
|
3431
|
+
}
|
|
3432
|
+
else {
|
|
3433
|
+
endpoint = `/api/workspaces/${this.workspaceId}/activities`;
|
|
3434
|
+
}
|
|
3435
|
+
return this.request(endpoint, {
|
|
3282
3436
|
method: 'POST',
|
|
3283
|
-
body: JSON.stringify(
|
|
3437
|
+
body: JSON.stringify(activity),
|
|
3284
3438
|
});
|
|
3285
3439
|
}
|
|
3286
3440
|
/**
|
|
3287
|
-
* Update an existing
|
|
3441
|
+
* Update an existing activity
|
|
3288
3442
|
*/
|
|
3289
|
-
async
|
|
3290
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3291
|
-
method: '
|
|
3443
|
+
async updateActivity(activityId, updates) {
|
|
3444
|
+
return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
|
|
3445
|
+
method: 'PATCH',
|
|
3292
3446
|
body: JSON.stringify(updates),
|
|
3293
3447
|
});
|
|
3294
3448
|
}
|
|
3295
3449
|
/**
|
|
3296
|
-
* Delete an
|
|
3450
|
+
* Delete an activity
|
|
3297
3451
|
*/
|
|
3298
|
-
async
|
|
3299
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3452
|
+
async deleteActivity(activityId) {
|
|
3453
|
+
return this.request(`/api/workspaces/${this.workspaceId}/activities/${activityId}`, {
|
|
3300
3454
|
method: 'DELETE',
|
|
3301
3455
|
});
|
|
3302
3456
|
}
|
|
3303
3457
|
/**
|
|
3304
|
-
*
|
|
3458
|
+
* Log a call activity
|
|
3305
3459
|
*/
|
|
3306
|
-
async
|
|
3307
|
-
return this.
|
|
3308
|
-
|
|
3309
|
-
|
|
3460
|
+
async logCall(data) {
|
|
3461
|
+
return this.createActivity({
|
|
3462
|
+
type: 'call',
|
|
3463
|
+
title: `${data.direction === 'inbound' ? 'Inbound' : 'Outbound'} Call`,
|
|
3464
|
+
direction: data.direction,
|
|
3465
|
+
duration: data.duration,
|
|
3466
|
+
outcome: data.outcome,
|
|
3467
|
+
description: data.notes,
|
|
3468
|
+
contactId: data.contactId,
|
|
3469
|
+
opportunityId: data.opportunityId,
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Log a meeting activity
|
|
3474
|
+
*/
|
|
3475
|
+
async logMeeting(data) {
|
|
3476
|
+
return this.createActivity({
|
|
3477
|
+
type: 'meeting',
|
|
3478
|
+
title: data.title,
|
|
3479
|
+
duration: data.duration,
|
|
3480
|
+
outcome: data.outcome,
|
|
3481
|
+
description: data.notes,
|
|
3482
|
+
contactId: data.contactId,
|
|
3483
|
+
opportunityId: data.opportunityId,
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
/**
|
|
3487
|
+
* Add a note to a contact or opportunity
|
|
3488
|
+
*/
|
|
3489
|
+
async addNote(data) {
|
|
3490
|
+
return this.createActivity({
|
|
3491
|
+
type: 'note',
|
|
3492
|
+
title: 'Note',
|
|
3493
|
+
description: data.content,
|
|
3494
|
+
contactId: data.contactId,
|
|
3495
|
+
opportunityId: data.opportunityId,
|
|
3310
3496
|
});
|
|
3311
3497
|
}
|
|
3312
3498
|
// ============================================
|
|
3313
|
-
//
|
|
3499
|
+
// EMAIL TEMPLATES API
|
|
3314
3500
|
// ============================================
|
|
3315
3501
|
/**
|
|
3316
|
-
* Get all
|
|
3502
|
+
* Get all email templates
|
|
3317
3503
|
*/
|
|
3318
|
-
async
|
|
3504
|
+
async getEmailTemplates(params) {
|
|
3319
3505
|
const queryParams = new URLSearchParams();
|
|
3320
3506
|
if (params?.page)
|
|
3321
3507
|
queryParams.set('page', params.page.toString());
|
|
3322
3508
|
if (params?.limit)
|
|
3323
3509
|
queryParams.set('limit', params.limit.toString());
|
|
3324
|
-
if (params?.search)
|
|
3325
|
-
queryParams.set('search', params.search);
|
|
3326
|
-
if (params?.status)
|
|
3327
|
-
queryParams.set('status', params.status);
|
|
3328
|
-
if (params?.industry)
|
|
3329
|
-
queryParams.set('industry', params.industry);
|
|
3330
3510
|
const query = queryParams.toString();
|
|
3331
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/
|
|
3511
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
|
|
3332
3512
|
return this.request(endpoint);
|
|
3333
3513
|
}
|
|
3334
3514
|
/**
|
|
3335
|
-
* Get a single
|
|
3515
|
+
* Get a single email template by ID
|
|
3336
3516
|
*/
|
|
3337
|
-
async
|
|
3338
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3517
|
+
async getEmailTemplate(templateId) {
|
|
3518
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`);
|
|
3339
3519
|
}
|
|
3340
3520
|
/**
|
|
3341
|
-
* Create a new
|
|
3521
|
+
* Create a new email template
|
|
3342
3522
|
*/
|
|
3343
|
-
async
|
|
3344
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3523
|
+
async createEmailTemplate(template) {
|
|
3524
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates`, {
|
|
3345
3525
|
method: 'POST',
|
|
3346
|
-
body: JSON.stringify(
|
|
3526
|
+
body: JSON.stringify(template),
|
|
3347
3527
|
});
|
|
3348
3528
|
}
|
|
3349
3529
|
/**
|
|
3350
|
-
* Update an
|
|
3530
|
+
* Update an email template
|
|
3351
3531
|
*/
|
|
3352
|
-
async
|
|
3353
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3532
|
+
async updateEmailTemplate(templateId, updates) {
|
|
3533
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
|
|
3354
3534
|
method: 'PUT',
|
|
3355
3535
|
body: JSON.stringify(updates),
|
|
3356
3536
|
});
|
|
3357
3537
|
}
|
|
3358
3538
|
/**
|
|
3359
|
-
* Delete
|
|
3539
|
+
* Delete an email template
|
|
3360
3540
|
*/
|
|
3361
|
-
async
|
|
3362
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3541
|
+
async deleteEmailTemplate(templateId) {
|
|
3542
|
+
return this.request(`/api/workspaces/${this.workspaceId}/email-templates/${templateId}`, {
|
|
3363
3543
|
method: 'DELETE',
|
|
3364
3544
|
});
|
|
3365
3545
|
}
|
|
3366
3546
|
/**
|
|
3367
|
-
*
|
|
3547
|
+
* Send an email using a template
|
|
3368
3548
|
*/
|
|
3369
|
-
async
|
|
3549
|
+
async sendEmail(data) {
|
|
3550
|
+
return this.request(`/api/workspaces/${this.workspaceId}/emails/send`, {
|
|
3551
|
+
method: 'POST',
|
|
3552
|
+
body: JSON.stringify(data),
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
// ============================================
|
|
3556
|
+
// READ-BACK / DATA RETRIEVAL API
|
|
3557
|
+
// ============================================
|
|
3558
|
+
/**
|
|
3559
|
+
* Get a contact by email address.
|
|
3560
|
+
* Returns the first matching contact from a search query.
|
|
3561
|
+
*/
|
|
3562
|
+
async getContactByEmail(email) {
|
|
3563
|
+
this.validateRequired('email', email, 'getContactByEmail');
|
|
3564
|
+
const queryParams = new URLSearchParams({ search: email, limit: '1' });
|
|
3565
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
|
|
3566
|
+
}
|
|
3567
|
+
/**
|
|
3568
|
+
* Get activity timeline for a contact
|
|
3569
|
+
*/
|
|
3570
|
+
async getContactActivity(contactId, params) {
|
|
3571
|
+
this.validateRequired('contactId', contactId, 'getContactActivity');
|
|
3370
3572
|
const queryParams = new URLSearchParams();
|
|
3371
3573
|
if (params?.page)
|
|
3372
3574
|
queryParams.set('page', params.page.toString());
|
|
3373
3575
|
if (params?.limit)
|
|
3374
3576
|
queryParams.set('limit', params.limit.toString());
|
|
3577
|
+
if (params?.type)
|
|
3578
|
+
queryParams.set('type', params.type);
|
|
3579
|
+
if (params?.startDate)
|
|
3580
|
+
queryParams.set('startDate', params.startDate);
|
|
3581
|
+
if (params?.endDate)
|
|
3582
|
+
queryParams.set('endDate', params.endDate);
|
|
3375
3583
|
const query = queryParams.toString();
|
|
3376
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/
|
|
3584
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
3377
3585
|
return this.request(endpoint);
|
|
3378
3586
|
}
|
|
3379
3587
|
/**
|
|
3380
|
-
* Get
|
|
3588
|
+
* Get engagement metrics for a contact (via their linked visitor data)
|
|
3381
3589
|
*/
|
|
3382
|
-
async
|
|
3590
|
+
async getContactEngagement(contactId) {
|
|
3591
|
+
this.validateRequired('contactId', contactId, 'getContactEngagement');
|
|
3592
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
|
|
3593
|
+
}
|
|
3594
|
+
/**
|
|
3595
|
+
* Get a full timeline for a contact including events, activities, and opportunities
|
|
3596
|
+
*/
|
|
3597
|
+
async getContactTimeline(contactId, params) {
|
|
3598
|
+
this.validateRequired('contactId', contactId, 'getContactTimeline');
|
|
3383
3599
|
const queryParams = new URLSearchParams();
|
|
3384
3600
|
if (params?.page)
|
|
3385
3601
|
queryParams.set('page', params.page.toString());
|
|
3386
3602
|
if (params?.limit)
|
|
3387
3603
|
queryParams.set('limit', params.limit.toString());
|
|
3388
3604
|
const query = queryParams.toString();
|
|
3389
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/
|
|
3605
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
|
|
3606
|
+
return this.request(endpoint);
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Search contacts with advanced filters
|
|
3610
|
+
*/
|
|
3611
|
+
async searchContacts(query, filters) {
|
|
3612
|
+
const queryParams = new URLSearchParams();
|
|
3613
|
+
queryParams.set('search', query);
|
|
3614
|
+
if (filters?.status)
|
|
3615
|
+
queryParams.set('status', filters.status);
|
|
3616
|
+
if (filters?.lifecycleStage)
|
|
3617
|
+
queryParams.set('lifecycleStage', filters.lifecycleStage);
|
|
3618
|
+
if (filters?.source)
|
|
3619
|
+
queryParams.set('source', filters.source);
|
|
3620
|
+
if (filters?.tags)
|
|
3621
|
+
queryParams.set('tags', filters.tags.join(','));
|
|
3622
|
+
if (filters?.page)
|
|
3623
|
+
queryParams.set('page', filters.page.toString());
|
|
3624
|
+
if (filters?.limit)
|
|
3625
|
+
queryParams.set('limit', filters.limit.toString());
|
|
3626
|
+
const qs = queryParams.toString();
|
|
3627
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
|
|
3390
3628
|
return this.request(endpoint);
|
|
3391
3629
|
}
|
|
3392
3630
|
// ============================================
|
|
3393
|
-
//
|
|
3631
|
+
// WEBHOOK MANAGEMENT API
|
|
3394
3632
|
// ============================================
|
|
3395
3633
|
/**
|
|
3396
|
-
*
|
|
3634
|
+
* List all webhook subscriptions
|
|
3397
3635
|
*/
|
|
3398
|
-
async
|
|
3399
|
-
|
|
3636
|
+
async listWebhooks(params) {
|
|
3637
|
+
const queryParams = new URLSearchParams();
|
|
3638
|
+
if (params?.page)
|
|
3639
|
+
queryParams.set('page', params.page.toString());
|
|
3640
|
+
if (params?.limit)
|
|
3641
|
+
queryParams.set('limit', params.limit.toString());
|
|
3642
|
+
const query = queryParams.toString();
|
|
3643
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
|
|
3400
3644
|
}
|
|
3401
3645
|
/**
|
|
3402
|
-
*
|
|
3646
|
+
* Create a new webhook subscription
|
|
3403
3647
|
*/
|
|
3404
|
-
async
|
|
3405
|
-
return this.request(`/api/workspaces/${this.workspaceId}/
|
|
3648
|
+
async createWebhook(data) {
|
|
3649
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
|
|
3650
|
+
method: 'POST',
|
|
3651
|
+
body: JSON.stringify(data),
|
|
3652
|
+
});
|
|
3406
3653
|
}
|
|
3407
3654
|
/**
|
|
3408
|
-
*
|
|
3655
|
+
* Delete a webhook subscription
|
|
3409
3656
|
*/
|
|
3410
|
-
async
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3657
|
+
async deleteWebhook(webhookId) {
|
|
3658
|
+
this.validateRequired('webhookId', webhookId, 'deleteWebhook');
|
|
3659
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
|
|
3660
|
+
method: 'DELETE',
|
|
3414
3661
|
});
|
|
3415
3662
|
}
|
|
3663
|
+
// ============================================
|
|
3664
|
+
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3665
|
+
// ============================================
|
|
3666
|
+
/**
|
|
3667
|
+
* Get all event triggers
|
|
3668
|
+
*/
|
|
3669
|
+
async getEventTriggers() {
|
|
3670
|
+
return this.triggers.getTriggers();
|
|
3671
|
+
}
|
|
3416
3672
|
/**
|
|
3417
|
-
*
|
|
3673
|
+
* Create a new event trigger
|
|
3418
3674
|
*/
|
|
3419
|
-
async
|
|
3420
|
-
return this.
|
|
3421
|
-
method: 'PUT',
|
|
3422
|
-
body: JSON.stringify(updates),
|
|
3423
|
-
});
|
|
3675
|
+
async createEventTrigger(trigger) {
|
|
3676
|
+
return this.triggers.createTrigger(trigger);
|
|
3424
3677
|
}
|
|
3425
3678
|
/**
|
|
3426
|
-
*
|
|
3679
|
+
* Update an event trigger
|
|
3427
3680
|
*/
|
|
3428
|
-
async
|
|
3429
|
-
return this.
|
|
3430
|
-
|
|
3681
|
+
async updateEventTrigger(triggerId, updates) {
|
|
3682
|
+
return this.triggers.updateTrigger(triggerId, updates);
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Delete an event trigger
|
|
3686
|
+
*/
|
|
3687
|
+
async deleteEventTrigger(triggerId) {
|
|
3688
|
+
return this.triggers.deleteTrigger(triggerId);
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/**
|
|
3693
|
+
* Clianta SDK - Main Tracker Class
|
|
3694
|
+
* @see SDK_VERSION in core/config.ts
|
|
3695
|
+
*/
|
|
3696
|
+
/**
|
|
3697
|
+
* Main Clianta Tracker Class
|
|
3698
|
+
*/
|
|
3699
|
+
class Tracker {
|
|
3700
|
+
constructor(workspaceId, userConfig = {}) {
|
|
3701
|
+
this.plugins = [];
|
|
3702
|
+
this.isInitialized = false;
|
|
3703
|
+
/** contactId after a successful identify() call */
|
|
3704
|
+
this.contactId = null;
|
|
3705
|
+
/** Pending identify retry on next flush */
|
|
3706
|
+
this.pendingIdentify = null;
|
|
3707
|
+
/** Registered event schemas for validation */
|
|
3708
|
+
this.eventSchemas = new Map();
|
|
3709
|
+
if (!workspaceId) {
|
|
3710
|
+
throw new Error('[Clianta] Workspace ID is required');
|
|
3711
|
+
}
|
|
3712
|
+
this.workspaceId = workspaceId;
|
|
3713
|
+
this.config = mergeConfig(userConfig);
|
|
3714
|
+
// Setup debug mode
|
|
3715
|
+
logger.enabled = this.config.debug;
|
|
3716
|
+
logger.info(`Initializing SDK v${SDK_VERSION}`, { workspaceId });
|
|
3717
|
+
// Initialize consent manager
|
|
3718
|
+
this.consentManager = new ConsentManager({
|
|
3719
|
+
...this.config.consent,
|
|
3720
|
+
onConsentChange: (state, previous) => {
|
|
3721
|
+
this.onConsentChange(state, previous);
|
|
3722
|
+
},
|
|
3723
|
+
});
|
|
3724
|
+
// Initialize transport and queue
|
|
3725
|
+
this.transport = new Transport({ apiEndpoint: this.config.apiEndpoint });
|
|
3726
|
+
this.queue = new EventQueue(this.transport, {
|
|
3727
|
+
batchSize: this.config.batchSize,
|
|
3728
|
+
flushInterval: this.config.flushInterval,
|
|
3431
3729
|
});
|
|
3730
|
+
// Get or create visitor and session IDs based on mode
|
|
3731
|
+
this.visitorId = this.createVisitorId();
|
|
3732
|
+
this.sessionId = this.createSessionId();
|
|
3733
|
+
logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
|
|
3734
|
+
// Security warnings
|
|
3735
|
+
if (this.config.apiEndpoint.startsWith('http://') &&
|
|
3736
|
+
typeof window !== 'undefined' &&
|
|
3737
|
+
!window.location.hostname.includes('localhost') &&
|
|
3738
|
+
!window.location.hostname.includes('127.0.0.1')) {
|
|
3739
|
+
logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
|
|
3740
|
+
}
|
|
3741
|
+
if (this.config.apiKey && typeof window !== 'undefined') {
|
|
3742
|
+
logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
|
|
3743
|
+
}
|
|
3744
|
+
// Initialize plugins
|
|
3745
|
+
this.initPlugins();
|
|
3746
|
+
this.isInitialized = true;
|
|
3747
|
+
logger.info('SDK initialized successfully');
|
|
3432
3748
|
}
|
|
3433
|
-
// ============================================
|
|
3434
|
-
// TASKS API
|
|
3435
|
-
// ============================================
|
|
3436
3749
|
/**
|
|
3437
|
-
*
|
|
3750
|
+
* Create visitor ID based on storage mode
|
|
3438
3751
|
*/
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
if (
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
if (
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3752
|
+
createVisitorId() {
|
|
3753
|
+
// Anonymous mode: use temporary ID until consent
|
|
3754
|
+
if (this.config.consent.anonymousMode && !this.consentManager.hasExplicit()) {
|
|
3755
|
+
const key = STORAGE_KEYS.VISITOR_ID + '_anon';
|
|
3756
|
+
let anonId = getSessionStorage(key);
|
|
3757
|
+
if (!anonId) {
|
|
3758
|
+
anonId = 'anon_' + generateUUID();
|
|
3759
|
+
setSessionStorage(key, anonId);
|
|
3760
|
+
}
|
|
3761
|
+
return anonId;
|
|
3762
|
+
}
|
|
3763
|
+
// Cookie-less mode: use sessionStorage only
|
|
3764
|
+
if (this.config.cookielessMode) {
|
|
3765
|
+
let visitorId = getSessionStorage(STORAGE_KEYS.VISITOR_ID);
|
|
3766
|
+
if (!visitorId) {
|
|
3767
|
+
visitorId = generateUUID();
|
|
3768
|
+
setSessionStorage(STORAGE_KEYS.VISITOR_ID, visitorId);
|
|
3769
|
+
}
|
|
3770
|
+
return visitorId;
|
|
3771
|
+
}
|
|
3772
|
+
// Normal mode
|
|
3773
|
+
return getOrCreateVisitorId(this.config.useCookies);
|
|
3458
3774
|
}
|
|
3459
3775
|
/**
|
|
3460
|
-
*
|
|
3776
|
+
* Create session ID
|
|
3461
3777
|
*/
|
|
3462
|
-
|
|
3463
|
-
return
|
|
3778
|
+
createSessionId() {
|
|
3779
|
+
return getOrCreateSessionId(this.config.sessionTimeout);
|
|
3464
3780
|
}
|
|
3465
3781
|
/**
|
|
3466
|
-
*
|
|
3782
|
+
* Handle consent state changes
|
|
3467
3783
|
*/
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3784
|
+
onConsentChange(state, previous) {
|
|
3785
|
+
logger.debug('Consent changed:', { from: previous, to: state });
|
|
3786
|
+
// If analytics consent was just granted
|
|
3787
|
+
if (state.analytics && !previous.analytics) {
|
|
3788
|
+
// Upgrade from anonymous ID to persistent ID
|
|
3789
|
+
if (this.config.consent.anonymousMode) {
|
|
3790
|
+
this.visitorId = getOrCreateVisitorId(this.config.useCookies);
|
|
3791
|
+
logger.info('Upgraded from anonymous to persistent visitor ID');
|
|
3792
|
+
}
|
|
3793
|
+
// Flush buffered events
|
|
3794
|
+
const buffered = this.consentManager.flushBuffer();
|
|
3795
|
+
for (const event of buffered) {
|
|
3796
|
+
// Update event with new visitor ID
|
|
3797
|
+
event.visitorId = this.visitorId;
|
|
3798
|
+
this.queue.push(event);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3473
3801
|
}
|
|
3474
3802
|
/**
|
|
3475
|
-
*
|
|
3803
|
+
* Initialize enabled plugins
|
|
3804
|
+
* Handles both sync and async plugin init methods
|
|
3476
3805
|
*/
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3806
|
+
initPlugins() {
|
|
3807
|
+
const pluginsToLoad = this.config.plugins;
|
|
3808
|
+
// Skip pageView plugin if autoPageView is disabled
|
|
3809
|
+
const filteredPlugins = this.config.autoPageView
|
|
3810
|
+
? pluginsToLoad
|
|
3811
|
+
: pluginsToLoad.filter((p) => p !== 'pageView');
|
|
3812
|
+
for (const pluginName of filteredPlugins) {
|
|
3813
|
+
try {
|
|
3814
|
+
const plugin = getPlugin(pluginName);
|
|
3815
|
+
// Handle both sync and async init (fire-and-forget for async)
|
|
3816
|
+
const result = plugin.init(this);
|
|
3817
|
+
if (result instanceof Promise) {
|
|
3818
|
+
result.catch((error) => {
|
|
3819
|
+
logger.error(`Async plugin init failed: ${pluginName}`, error);
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
this.plugins.push(plugin);
|
|
3823
|
+
logger.debug(`Plugin loaded: ${pluginName}`);
|
|
3824
|
+
}
|
|
3825
|
+
catch (error) {
|
|
3826
|
+
logger.error(`Failed to load plugin: ${pluginName}`, error);
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3482
3829
|
}
|
|
3483
3830
|
/**
|
|
3484
|
-
*
|
|
3831
|
+
* Track a custom event
|
|
3485
3832
|
*/
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3833
|
+
track(eventType, eventName, properties = {}) {
|
|
3834
|
+
if (!this.isInitialized) {
|
|
3835
|
+
logger.warn('SDK not initialized, event dropped');
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
const event = {
|
|
3839
|
+
workspaceId: this.workspaceId,
|
|
3840
|
+
visitorId: this.visitorId,
|
|
3841
|
+
sessionId: this.sessionId,
|
|
3842
|
+
eventType: eventType,
|
|
3843
|
+
eventName,
|
|
3844
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
3845
|
+
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
3846
|
+
properties: {
|
|
3847
|
+
...properties,
|
|
3848
|
+
eventId: generateUUID(), // Unique ID for deduplication on retry
|
|
3849
|
+
websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
|
|
3850
|
+
},
|
|
3851
|
+
device: getDeviceInfo(),
|
|
3852
|
+
...getUTMParams(),
|
|
3853
|
+
timestamp: new Date().toISOString(),
|
|
3854
|
+
sdkVersion: SDK_VERSION,
|
|
3855
|
+
};
|
|
3856
|
+
// Attach contactId if known (from a prior identify() call)
|
|
3857
|
+
if (this.contactId) {
|
|
3858
|
+
event.contactId = this.contactId;
|
|
3859
|
+
}
|
|
3860
|
+
// Validate event against registered schema (debug mode only)
|
|
3861
|
+
this.validateEventSchema(eventType, properties);
|
|
3862
|
+
// Check consent before tracking
|
|
3863
|
+
if (!this.consentManager.canTrack()) {
|
|
3864
|
+
// Buffer event for later if waitForConsent is enabled
|
|
3865
|
+
if (this.config.consent.waitForConsent) {
|
|
3866
|
+
this.consentManager.bufferEvent(event);
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
// Otherwise drop the event
|
|
3870
|
+
logger.debug('Event dropped (no consent):', eventName);
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
this.queue.push(event);
|
|
3874
|
+
logger.debug('Event tracked:', eventName, properties);
|
|
3490
3875
|
}
|
|
3491
3876
|
/**
|
|
3492
|
-
*
|
|
3877
|
+
* Track a page view
|
|
3493
3878
|
*/
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3879
|
+
page(name, properties = {}) {
|
|
3880
|
+
const pageName = name || (typeof document !== 'undefined' ? document.title : 'Page View');
|
|
3881
|
+
this.track('page_view', pageName, {
|
|
3882
|
+
...properties,
|
|
3883
|
+
path: typeof window !== 'undefined' ? window.location.pathname : '',
|
|
3497
3884
|
});
|
|
3498
3885
|
}
|
|
3499
|
-
// ============================================
|
|
3500
|
-
// ACTIVITIES API
|
|
3501
|
-
// ============================================
|
|
3502
3886
|
/**
|
|
3503
|
-
*
|
|
3887
|
+
* Identify a visitor.
|
|
3888
|
+
* Links the anonymous visitorId to a CRM contact and returns the contactId.
|
|
3889
|
+
* All subsequent track() calls will include the contactId automatically.
|
|
3504
3890
|
*/
|
|
3505
|
-
async
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3891
|
+
async identify(email, traits = {}) {
|
|
3892
|
+
if (!email) {
|
|
3893
|
+
logger.warn('Email is required for identification');
|
|
3894
|
+
return null;
|
|
3895
|
+
}
|
|
3896
|
+
if (!isValidEmail(email)) {
|
|
3897
|
+
logger.warn('Invalid email format, identification skipped:', email);
|
|
3898
|
+
return null;
|
|
3899
|
+
}
|
|
3900
|
+
logger.info('Identifying visitor:', email);
|
|
3901
|
+
const result = await this.transport.sendIdentify({
|
|
3902
|
+
workspaceId: this.workspaceId,
|
|
3903
|
+
visitorId: this.visitorId,
|
|
3904
|
+
email,
|
|
3905
|
+
properties: traits,
|
|
3906
|
+
});
|
|
3907
|
+
if (result.success) {
|
|
3908
|
+
logger.info('Visitor identified successfully, contactId:', result.contactId);
|
|
3909
|
+
// Store contactId so all future track() calls include it
|
|
3910
|
+
this.contactId = result.contactId ?? null;
|
|
3911
|
+
this.pendingIdentify = null;
|
|
3912
|
+
return this.contactId;
|
|
3913
|
+
}
|
|
3914
|
+
else {
|
|
3915
|
+
logger.error('Failed to identify visitor:', result.error);
|
|
3916
|
+
// Store for retry on next flush
|
|
3917
|
+
this.pendingIdentify = { email, traits };
|
|
3918
|
+
return null;
|
|
3919
|
+
}
|
|
3516
3920
|
}
|
|
3517
3921
|
/**
|
|
3518
|
-
*
|
|
3922
|
+
* Send a server-side inbound event via the API key endpoint.
|
|
3923
|
+
* Convenience proxy to CRMClient.sendEvent() — requires apiKey in config.
|
|
3519
3924
|
*/
|
|
3520
|
-
async
|
|
3521
|
-
const
|
|
3522
|
-
if (
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
const query = queryParams.toString();
|
|
3529
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${opportunityId}/activities${query ? `?${query}` : ''}`;
|
|
3530
|
-
return this.request(endpoint);
|
|
3925
|
+
async sendEvent(payload) {
|
|
3926
|
+
const apiKey = this.config.apiKey;
|
|
3927
|
+
if (!apiKey) {
|
|
3928
|
+
logger.error('sendEvent() requires an apiKey in the SDK config');
|
|
3929
|
+
return { success: false, contactCreated: false, event: payload.event, error: 'No API key configured' };
|
|
3930
|
+
}
|
|
3931
|
+
const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
|
|
3932
|
+
return client.sendEvent(payload);
|
|
3531
3933
|
}
|
|
3532
3934
|
/**
|
|
3533
|
-
*
|
|
3935
|
+
* Get the current visitor's profile from the CRM.
|
|
3936
|
+
* Returns visitor data and linked contact info if identified.
|
|
3937
|
+
* Only returns data for the current visitor (privacy-safe for frontend).
|
|
3534
3938
|
*/
|
|
3535
|
-
async
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
endpoint = `/api/workspaces/${this.workspaceId}/opportunities/${activity.opportunityId}/activities`;
|
|
3939
|
+
async getVisitorProfile() {
|
|
3940
|
+
if (!this.isInitialized) {
|
|
3941
|
+
logger.warn('SDK not initialized');
|
|
3942
|
+
return null;
|
|
3540
3943
|
}
|
|
3541
|
-
|
|
3542
|
-
|
|
3944
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
|
|
3945
|
+
if (result.success && result.data) {
|
|
3946
|
+
logger.debug('Visitor profile fetched:', result.data);
|
|
3947
|
+
return result.data;
|
|
3543
3948
|
}
|
|
3544
|
-
|
|
3545
|
-
|
|
3949
|
+
logger.warn('Failed to fetch visitor profile:', result.error);
|
|
3950
|
+
return null;
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* Get the current visitor's recent activity/events.
|
|
3954
|
+
* Returns paginated list of tracking events for this visitor.
|
|
3955
|
+
*/
|
|
3956
|
+
async getVisitorActivity(options) {
|
|
3957
|
+
if (!this.isInitialized) {
|
|
3958
|
+
logger.warn('SDK not initialized');
|
|
3959
|
+
return null;
|
|
3546
3960
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3961
|
+
const params = {};
|
|
3962
|
+
if (options?.page)
|
|
3963
|
+
params.page = options.page.toString();
|
|
3964
|
+
if (options?.limit)
|
|
3965
|
+
params.limit = options.limit.toString();
|
|
3966
|
+
if (options?.eventType)
|
|
3967
|
+
params.eventType = options.eventType;
|
|
3968
|
+
if (options?.startDate)
|
|
3969
|
+
params.startDate = options.startDate;
|
|
3970
|
+
if (options?.endDate)
|
|
3971
|
+
params.endDate = options.endDate;
|
|
3972
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
|
|
3973
|
+
if (result.success && result.data) {
|
|
3974
|
+
return result.data;
|
|
3975
|
+
}
|
|
3976
|
+
logger.warn('Failed to fetch visitor activity:', result.error);
|
|
3977
|
+
return null;
|
|
3551
3978
|
}
|
|
3552
3979
|
/**
|
|
3553
|
-
*
|
|
3980
|
+
* Get a summarized journey timeline for the current visitor.
|
|
3981
|
+
* Includes top pages, sessions, time spent, and recent activities.
|
|
3554
3982
|
*/
|
|
3555
|
-
async
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
}
|
|
3983
|
+
async getVisitorTimeline() {
|
|
3984
|
+
if (!this.isInitialized) {
|
|
3985
|
+
logger.warn('SDK not initialized');
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
|
|
3989
|
+
if (result.success && result.data) {
|
|
3990
|
+
return result.data;
|
|
3991
|
+
}
|
|
3992
|
+
logger.warn('Failed to fetch visitor timeline:', result.error);
|
|
3993
|
+
return null;
|
|
3560
3994
|
}
|
|
3561
3995
|
/**
|
|
3562
|
-
*
|
|
3996
|
+
* Get engagement metrics for the current visitor.
|
|
3997
|
+
* Includes time on site, page views, bounce rate, and engagement score.
|
|
3563
3998
|
*/
|
|
3564
|
-
async
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3999
|
+
async getVisitorEngagement() {
|
|
4000
|
+
if (!this.isInitialized) {
|
|
4001
|
+
logger.warn('SDK not initialized');
|
|
4002
|
+
return null;
|
|
4003
|
+
}
|
|
4004
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
|
|
4005
|
+
if (result.success && result.data) {
|
|
4006
|
+
return result.data;
|
|
4007
|
+
}
|
|
4008
|
+
logger.warn('Failed to fetch visitor engagement:', result.error);
|
|
4009
|
+
return null;
|
|
3568
4010
|
}
|
|
3569
4011
|
/**
|
|
3570
|
-
*
|
|
4012
|
+
* Retry pending identify call
|
|
3571
4013
|
*/
|
|
3572
|
-
async
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
outcome: data.outcome,
|
|
3579
|
-
description: data.notes,
|
|
3580
|
-
contactId: data.contactId,
|
|
3581
|
-
opportunityId: data.opportunityId,
|
|
3582
|
-
});
|
|
4014
|
+
async retryPendingIdentify() {
|
|
4015
|
+
if (!this.pendingIdentify)
|
|
4016
|
+
return;
|
|
4017
|
+
const { email, traits } = this.pendingIdentify;
|
|
4018
|
+
this.pendingIdentify = null;
|
|
4019
|
+
await this.identify(email, traits);
|
|
3583
4020
|
}
|
|
3584
4021
|
/**
|
|
3585
|
-
*
|
|
4022
|
+
* Update consent state
|
|
3586
4023
|
*/
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
type: 'meeting',
|
|
3590
|
-
title: data.title,
|
|
3591
|
-
duration: data.duration,
|
|
3592
|
-
outcome: data.outcome,
|
|
3593
|
-
description: data.notes,
|
|
3594
|
-
contactId: data.contactId,
|
|
3595
|
-
opportunityId: data.opportunityId,
|
|
3596
|
-
});
|
|
4024
|
+
consent(state) {
|
|
4025
|
+
this.consentManager.update(state);
|
|
3597
4026
|
}
|
|
3598
4027
|
/**
|
|
3599
|
-
*
|
|
4028
|
+
* Get current consent state
|
|
3600
4029
|
*/
|
|
3601
|
-
|
|
3602
|
-
return this.
|
|
3603
|
-
type: 'note',
|
|
3604
|
-
title: 'Note',
|
|
3605
|
-
description: data.content,
|
|
3606
|
-
contactId: data.contactId,
|
|
3607
|
-
opportunityId: data.opportunityId,
|
|
3608
|
-
});
|
|
4030
|
+
getConsentState() {
|
|
4031
|
+
return this.consentManager.getState();
|
|
3609
4032
|
}
|
|
3610
|
-
// ============================================
|
|
3611
|
-
// EMAIL TEMPLATES API
|
|
3612
|
-
// ============================================
|
|
3613
4033
|
/**
|
|
3614
|
-
*
|
|
4034
|
+
* Toggle debug mode
|
|
3615
4035
|
*/
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
queryParams.set('page', params.page.toString());
|
|
3620
|
-
if (params?.limit)
|
|
3621
|
-
queryParams.set('limit', params.limit.toString());
|
|
3622
|
-
const query = queryParams.toString();
|
|
3623
|
-
const endpoint = `/api/workspaces/${this.workspaceId}/email-templates${query ? `?${query}` : ''}`;
|
|
3624
|
-
return this.request(endpoint);
|
|
4036
|
+
debug(enabled) {
|
|
4037
|
+
logger.enabled = enabled;
|
|
4038
|
+
logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
3625
4039
|
}
|
|
3626
4040
|
/**
|
|
3627
|
-
*
|
|
4041
|
+
* Register a schema for event validation.
|
|
4042
|
+
* When debug mode is enabled, events will be validated against registered schemas.
|
|
4043
|
+
*
|
|
4044
|
+
* @example
|
|
4045
|
+
* tracker.registerEventSchema('purchase', {
|
|
4046
|
+
* productId: 'string',
|
|
4047
|
+
* price: 'number',
|
|
4048
|
+
* quantity: 'number',
|
|
4049
|
+
* });
|
|
3628
4050
|
*/
|
|
3629
|
-
|
|
3630
|
-
|
|
4051
|
+
registerEventSchema(eventType, schema) {
|
|
4052
|
+
this.eventSchemas.set(eventType, schema);
|
|
4053
|
+
logger.debug('Event schema registered:', eventType);
|
|
3631
4054
|
}
|
|
3632
4055
|
/**
|
|
3633
|
-
*
|
|
4056
|
+
* Validate event properties against a registered schema (debug mode only)
|
|
3634
4057
|
*/
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
4058
|
+
validateEventSchema(eventType, properties) {
|
|
4059
|
+
if (!this.config.debug)
|
|
4060
|
+
return;
|
|
4061
|
+
const schema = this.eventSchemas.get(eventType);
|
|
4062
|
+
if (!schema)
|
|
4063
|
+
return;
|
|
4064
|
+
for (const [key, expectedType] of Object.entries(schema)) {
|
|
4065
|
+
const value = properties[key];
|
|
4066
|
+
if (value === undefined) {
|
|
4067
|
+
logger.warn(`[Schema] Missing property "${key}" for event type "${eventType}"`);
|
|
4068
|
+
continue;
|
|
4069
|
+
}
|
|
4070
|
+
let valid = false;
|
|
4071
|
+
switch (expectedType) {
|
|
4072
|
+
case 'string':
|
|
4073
|
+
valid = typeof value === 'string';
|
|
4074
|
+
break;
|
|
4075
|
+
case 'number':
|
|
4076
|
+
valid = typeof value === 'number';
|
|
4077
|
+
break;
|
|
4078
|
+
case 'boolean':
|
|
4079
|
+
valid = typeof value === 'boolean';
|
|
4080
|
+
break;
|
|
4081
|
+
case 'object':
|
|
4082
|
+
valid = typeof value === 'object' && !Array.isArray(value);
|
|
4083
|
+
break;
|
|
4084
|
+
case 'array':
|
|
4085
|
+
valid = Array.isArray(value);
|
|
4086
|
+
break;
|
|
4087
|
+
}
|
|
4088
|
+
if (!valid) {
|
|
4089
|
+
logger.warn(`[Schema] Property "${key}" for event "${eventType}" expected ${expectedType}, got ${typeof value}`);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
3640
4092
|
}
|
|
3641
4093
|
/**
|
|
3642
|
-
*
|
|
4094
|
+
* Get visitor ID
|
|
3643
4095
|
*/
|
|
3644
|
-
|
|
3645
|
-
return this.
|
|
3646
|
-
method: 'PUT',
|
|
3647
|
-
body: JSON.stringify(updates),
|
|
3648
|
-
});
|
|
4096
|
+
getVisitorId() {
|
|
4097
|
+
return this.visitorId;
|
|
3649
4098
|
}
|
|
3650
4099
|
/**
|
|
3651
|
-
*
|
|
4100
|
+
* Get session ID
|
|
3652
4101
|
*/
|
|
3653
|
-
|
|
3654
|
-
return this.
|
|
3655
|
-
method: 'DELETE',
|
|
3656
|
-
});
|
|
4102
|
+
getSessionId() {
|
|
4103
|
+
return this.sessionId;
|
|
3657
4104
|
}
|
|
3658
4105
|
/**
|
|
3659
|
-
*
|
|
4106
|
+
* Get workspace ID
|
|
3660
4107
|
*/
|
|
3661
|
-
|
|
3662
|
-
return this.
|
|
3663
|
-
method: 'POST',
|
|
3664
|
-
body: JSON.stringify(data),
|
|
3665
|
-
});
|
|
4108
|
+
getWorkspaceId() {
|
|
4109
|
+
return this.workspaceId;
|
|
3666
4110
|
}
|
|
3667
|
-
// ============================================
|
|
3668
|
-
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3669
|
-
// ============================================
|
|
3670
4111
|
/**
|
|
3671
|
-
* Get
|
|
4112
|
+
* Get current configuration
|
|
3672
4113
|
*/
|
|
3673
|
-
|
|
3674
|
-
return this.
|
|
4114
|
+
getConfig() {
|
|
4115
|
+
return { ...this.config };
|
|
3675
4116
|
}
|
|
3676
4117
|
/**
|
|
3677
|
-
*
|
|
4118
|
+
* Force flush event queue
|
|
3678
4119
|
*/
|
|
3679
|
-
async
|
|
3680
|
-
|
|
4120
|
+
async flush() {
|
|
4121
|
+
await this.retryPendingIdentify();
|
|
4122
|
+
await this.queue.flush();
|
|
3681
4123
|
}
|
|
3682
4124
|
/**
|
|
3683
|
-
*
|
|
4125
|
+
* Reset visitor and session (for logout)
|
|
3684
4126
|
*/
|
|
3685
|
-
|
|
3686
|
-
|
|
4127
|
+
reset() {
|
|
4128
|
+
logger.info('Resetting visitor data');
|
|
4129
|
+
resetIds(this.config.useCookies);
|
|
4130
|
+
this.visitorId = this.createVisitorId();
|
|
4131
|
+
this.sessionId = this.createSessionId();
|
|
4132
|
+
this.contactId = null;
|
|
4133
|
+
this.pendingIdentify = null;
|
|
4134
|
+
this.queue.clear();
|
|
3687
4135
|
}
|
|
3688
4136
|
/**
|
|
3689
|
-
* Delete
|
|
4137
|
+
* Delete all stored user data (GDPR right-to-erasure)
|
|
3690
4138
|
*/
|
|
3691
|
-
|
|
3692
|
-
|
|
4139
|
+
deleteData() {
|
|
4140
|
+
logger.info('Deleting all user data (GDPR request)');
|
|
4141
|
+
// Clear queue
|
|
4142
|
+
this.queue.clear();
|
|
4143
|
+
// Reset consent
|
|
4144
|
+
this.consentManager.reset();
|
|
4145
|
+
// Clear all stored IDs
|
|
4146
|
+
resetIds(this.config.useCookies);
|
|
4147
|
+
// Clear session storage items
|
|
4148
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
4149
|
+
try {
|
|
4150
|
+
sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
|
|
4151
|
+
sessionStorage.removeItem(STORAGE_KEYS.VISITOR_ID + '_anon');
|
|
4152
|
+
sessionStorage.removeItem(STORAGE_KEYS.SESSION_ID);
|
|
4153
|
+
sessionStorage.removeItem(STORAGE_KEYS.SESSION_TIMESTAMP);
|
|
4154
|
+
}
|
|
4155
|
+
catch {
|
|
4156
|
+
// Ignore errors
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
// Clear localStorage items
|
|
4160
|
+
if (typeof localStorage !== 'undefined') {
|
|
4161
|
+
try {
|
|
4162
|
+
localStorage.removeItem(STORAGE_KEYS.VISITOR_ID);
|
|
4163
|
+
localStorage.removeItem(STORAGE_KEYS.CONSENT);
|
|
4164
|
+
localStorage.removeItem(STORAGE_KEYS.EVENT_QUEUE);
|
|
4165
|
+
}
|
|
4166
|
+
catch {
|
|
4167
|
+
// Ignore errors
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
// Generate new IDs
|
|
4171
|
+
this.visitorId = this.createVisitorId();
|
|
4172
|
+
this.sessionId = this.createSessionId();
|
|
4173
|
+
logger.info('All user data deleted');
|
|
4174
|
+
}
|
|
4175
|
+
/**
|
|
4176
|
+
* Destroy tracker and cleanup
|
|
4177
|
+
*/
|
|
4178
|
+
async destroy() {
|
|
4179
|
+
logger.info('Destroying tracker');
|
|
4180
|
+
// Flush any remaining events (await to ensure completion)
|
|
4181
|
+
await this.queue.flush();
|
|
4182
|
+
// Destroy plugins
|
|
4183
|
+
for (const plugin of this.plugins) {
|
|
4184
|
+
if (plugin.destroy) {
|
|
4185
|
+
plugin.destroy();
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
this.plugins = [];
|
|
4189
|
+
// Destroy queue
|
|
4190
|
+
this.queue.destroy();
|
|
4191
|
+
this.isInitialized = false;
|
|
3693
4192
|
}
|
|
3694
4193
|
}
|
|
3695
4194
|
|