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