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