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