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