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