@clianta/sdk 1.4.0 → 1.5.1
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 +4432 -0
- package/dist/angular.cjs.js.map +1 -0
- package/dist/angular.d.ts +364 -0
- package/dist/angular.esm.js +4428 -0
- package/dist/angular.esm.js.map +1 -0
- package/dist/clianta.cjs.js +557 -99
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +557 -99
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +557 -99
- 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 +315 -7
- package/dist/react.cjs.js +571 -105
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +155 -1
- package/dist/react.esm.js +572 -106
- package/dist/react.esm.js.map +1 -1
- package/dist/svelte.cjs.js +4464 -0
- package/dist/svelte.cjs.js.map +1 -0
- package/dist/svelte.d.ts +374 -0
- package/dist/svelte.esm.js +4461 -0
- package/dist/svelte.esm.js.map +1 -0
- package/dist/vue.cjs.js +558 -100
- package/dist/vue.cjs.js.map +1 -1
- package/dist/vue.d.ts +155 -1
- package/dist/vue.esm.js +558 -100
- package/dist/vue.esm.js.map +1 -1
- package/package.json +21 -2
package/dist/vue.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
2
|
+
* Clianta SDK v1.5.1
|
|
3
3
|
* (c) 2026 Clianta
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -13,15 +13,22 @@ var vue = require('vue');
|
|
|
13
13
|
*/
|
|
14
14
|
/** SDK Version */
|
|
15
15
|
const SDK_VERSION = '1.4.0';
|
|
16
|
-
/** Default API endpoint
|
|
16
|
+
/** Default API endpoint — reads from env or falls back to localhost */
|
|
17
17
|
const getDefaultApiEndpoint = () => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Build-time env var (works with Next.js, Vite, CRA, etc.)
|
|
19
|
+
if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_CLIANTA_API_ENDPOINT) {
|
|
20
|
+
return process.env.NEXT_PUBLIC_CLIANTA_API_ENDPOINT;
|
|
21
|
+
}
|
|
22
|
+
if (typeof process !== 'undefined' && process.env?.VITE_CLIANTA_API_ENDPOINT) {
|
|
23
|
+
return process.env.VITE_CLIANTA_API_ENDPOINT;
|
|
24
|
+
}
|
|
25
|
+
if (typeof process !== 'undefined' && process.env?.REACT_APP_CLIANTA_API_ENDPOINT) {
|
|
26
|
+
return process.env.REACT_APP_CLIANTA_API_ENDPOINT;
|
|
23
27
|
}
|
|
24
|
-
|
|
28
|
+
if (typeof process !== 'undefined' && process.env?.CLIANTA_API_ENDPOINT) {
|
|
29
|
+
return process.env.CLIANTA_API_ENDPOINT;
|
|
30
|
+
}
|
|
31
|
+
return 'http://localhost:5000';
|
|
25
32
|
};
|
|
26
33
|
/** Core plugins enabled by default */
|
|
27
34
|
const DEFAULT_PLUGINS = [
|
|
@@ -54,6 +61,7 @@ const DEFAULT_CONFIG = {
|
|
|
54
61
|
cookieDomain: '',
|
|
55
62
|
useCookies: false,
|
|
56
63
|
cookielessMode: false,
|
|
64
|
+
persistMode: 'session',
|
|
57
65
|
};
|
|
58
66
|
/** Storage keys */
|
|
59
67
|
const STORAGE_KEYS = {
|
|
@@ -247,6 +255,39 @@ class Transport {
|
|
|
247
255
|
return false;
|
|
248
256
|
}
|
|
249
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Fetch data from the tracking API (GET request)
|
|
260
|
+
* Used for read-back APIs (visitor profile, activity, etc.)
|
|
261
|
+
*/
|
|
262
|
+
async fetchData(path, params) {
|
|
263
|
+
const url = new URL(`${this.config.apiEndpoint}${path}`);
|
|
264
|
+
if (params) {
|
|
265
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
266
|
+
if (value !== undefined && value !== null) {
|
|
267
|
+
url.searchParams.set(key, value);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const response = await this.fetchWithTimeout(url.toString(), {
|
|
273
|
+
method: 'GET',
|
|
274
|
+
headers: {
|
|
275
|
+
'Accept': 'application/json',
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
if (response.ok) {
|
|
279
|
+
const body = await response.json();
|
|
280
|
+
logger.debug('Fetch successful:', path);
|
|
281
|
+
return { success: true, data: body.data ?? body, status: response.status };
|
|
282
|
+
}
|
|
283
|
+
logger.error(`Fetch failed with status ${response.status}`);
|
|
284
|
+
return { success: false, status: response.status };
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
logger.error('Fetch request failed:', error);
|
|
288
|
+
return { success: false, error: error };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
250
291
|
/**
|
|
251
292
|
* Internal send with retry logic
|
|
252
293
|
*/
|
|
@@ -411,7 +452,9 @@ function cookie(name, value, days) {
|
|
|
411
452
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
412
453
|
expires = '; expires=' + date.toUTCString();
|
|
413
454
|
}
|
|
414
|
-
|
|
455
|
+
// Add Secure flag on HTTPS to prevent cookie leakage over plaintext
|
|
456
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
|
|
457
|
+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax' + secure;
|
|
415
458
|
return value;
|
|
416
459
|
}
|
|
417
460
|
// ============================================
|
|
@@ -575,6 +618,17 @@ function isMobile() {
|
|
|
575
618
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
576
619
|
}
|
|
577
620
|
// ============================================
|
|
621
|
+
// VALIDATION UTILITIES
|
|
622
|
+
// ============================================
|
|
623
|
+
/**
|
|
624
|
+
* Validate email format
|
|
625
|
+
*/
|
|
626
|
+
function isValidEmail(email) {
|
|
627
|
+
if (typeof email !== 'string' || !email)
|
|
628
|
+
return false;
|
|
629
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
630
|
+
}
|
|
631
|
+
// ============================================
|
|
578
632
|
// DEVICE INFO
|
|
579
633
|
// ============================================
|
|
580
634
|
/**
|
|
@@ -628,6 +682,7 @@ class EventQueue {
|
|
|
628
682
|
maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
629
683
|
storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
|
|
630
684
|
};
|
|
685
|
+
this.persistMode = config.persistMode || 'session';
|
|
631
686
|
// Restore persisted queue
|
|
632
687
|
this.restoreQueue();
|
|
633
688
|
// Start auto-flush timer
|
|
@@ -733,6 +788,13 @@ class EventQueue {
|
|
|
733
788
|
clear() {
|
|
734
789
|
this.queue = [];
|
|
735
790
|
this.persistQueue([]);
|
|
791
|
+
// Also clear localStorage if used
|
|
792
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
793
|
+
try {
|
|
794
|
+
localStorage.removeItem(this.config.storageKey);
|
|
795
|
+
}
|
|
796
|
+
catch { /* ignore */ }
|
|
797
|
+
}
|
|
736
798
|
}
|
|
737
799
|
/**
|
|
738
800
|
* Stop the flush timer and cleanup handlers
|
|
@@ -787,22 +849,44 @@ class EventQueue {
|
|
|
787
849
|
window.addEventListener('pagehide', this.boundPageHide);
|
|
788
850
|
}
|
|
789
851
|
/**
|
|
790
|
-
* Persist queue to
|
|
852
|
+
* Persist queue to storage based on persistMode
|
|
791
853
|
*/
|
|
792
854
|
persistQueue(events) {
|
|
855
|
+
if (this.persistMode === 'none')
|
|
856
|
+
return;
|
|
793
857
|
try {
|
|
794
|
-
|
|
858
|
+
const serialized = JSON.stringify(events);
|
|
859
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
860
|
+
try {
|
|
861
|
+
localStorage.setItem(this.config.storageKey, serialized);
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
// localStorage quota exceeded — fallback to sessionStorage
|
|
865
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
870
|
+
}
|
|
795
871
|
}
|
|
796
872
|
catch {
|
|
797
873
|
// Ignore storage errors
|
|
798
874
|
}
|
|
799
875
|
}
|
|
800
876
|
/**
|
|
801
|
-
* Restore queue from
|
|
877
|
+
* Restore queue from storage
|
|
802
878
|
*/
|
|
803
879
|
restoreQueue() {
|
|
804
880
|
try {
|
|
805
|
-
|
|
881
|
+
let stored = null;
|
|
882
|
+
// Check localStorage first (cross-session persistence)
|
|
883
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
884
|
+
stored = localStorage.getItem(this.config.storageKey);
|
|
885
|
+
}
|
|
886
|
+
// Fall back to sessionStorage
|
|
887
|
+
if (!stored) {
|
|
888
|
+
stored = getSessionStorage(this.config.storageKey);
|
|
889
|
+
}
|
|
806
890
|
if (stored) {
|
|
807
891
|
const events = JSON.parse(stored);
|
|
808
892
|
if (Array.isArray(events) && events.length > 0) {
|
|
@@ -870,10 +954,13 @@ class PageViewPlugin extends BasePlugin {
|
|
|
870
954
|
history.pushState = function (...args) {
|
|
871
955
|
self.originalPushState.apply(history, args);
|
|
872
956
|
self.trackPageView();
|
|
957
|
+
// Notify other plugins (e.g. ScrollPlugin) about navigation
|
|
958
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
873
959
|
};
|
|
874
960
|
history.replaceState = function (...args) {
|
|
875
961
|
self.originalReplaceState.apply(history, args);
|
|
876
962
|
self.trackPageView();
|
|
963
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
877
964
|
};
|
|
878
965
|
// Handle back/forward navigation
|
|
879
966
|
this.popstateHandler = () => this.trackPageView();
|
|
@@ -927,9 +1014,8 @@ class ScrollPlugin extends BasePlugin {
|
|
|
927
1014
|
this.pageLoadTime = 0;
|
|
928
1015
|
this.scrollTimeout = null;
|
|
929
1016
|
this.boundHandler = null;
|
|
930
|
-
/** SPA navigation
|
|
931
|
-
this.
|
|
932
|
-
this.originalReplaceState = null;
|
|
1017
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1018
|
+
this.navigationHandler = null;
|
|
933
1019
|
this.popstateHandler = null;
|
|
934
1020
|
}
|
|
935
1021
|
init(tracker) {
|
|
@@ -938,8 +1024,13 @@ class ScrollPlugin extends BasePlugin {
|
|
|
938
1024
|
if (typeof window !== 'undefined') {
|
|
939
1025
|
this.boundHandler = this.handleScroll.bind(this);
|
|
940
1026
|
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
941
|
-
//
|
|
942
|
-
|
|
1027
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1028
|
+
// instead of independently monkey-patching history.pushState
|
|
1029
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1030
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1031
|
+
// Handle back/forward navigation
|
|
1032
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1033
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
943
1034
|
}
|
|
944
1035
|
}
|
|
945
1036
|
destroy() {
|
|
@@ -949,16 +1040,10 @@ class ScrollPlugin extends BasePlugin {
|
|
|
949
1040
|
if (this.scrollTimeout) {
|
|
950
1041
|
clearTimeout(this.scrollTimeout);
|
|
951
1042
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
this.originalPushState = null;
|
|
956
|
-
}
|
|
957
|
-
if (this.originalReplaceState) {
|
|
958
|
-
history.replaceState = this.originalReplaceState;
|
|
959
|
-
this.originalReplaceState = null;
|
|
1043
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1044
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1045
|
+
this.navigationHandler = null;
|
|
960
1046
|
}
|
|
961
|
-
// Remove popstate listener
|
|
962
1047
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
963
1048
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
964
1049
|
this.popstateHandler = null;
|
|
@@ -973,29 +1058,6 @@ class ScrollPlugin extends BasePlugin {
|
|
|
973
1058
|
this.maxScrollDepth = 0;
|
|
974
1059
|
this.pageLoadTime = Date.now();
|
|
975
1060
|
}
|
|
976
|
-
/**
|
|
977
|
-
* Setup History API interception for SPA navigation
|
|
978
|
-
*/
|
|
979
|
-
setupNavigationReset() {
|
|
980
|
-
if (typeof window === 'undefined')
|
|
981
|
-
return;
|
|
982
|
-
// Store originals for cleanup
|
|
983
|
-
this.originalPushState = history.pushState;
|
|
984
|
-
this.originalReplaceState = history.replaceState;
|
|
985
|
-
// Intercept pushState and replaceState
|
|
986
|
-
const self = this;
|
|
987
|
-
history.pushState = function (...args) {
|
|
988
|
-
self.originalPushState.apply(history, args);
|
|
989
|
-
self.resetForNavigation();
|
|
990
|
-
};
|
|
991
|
-
history.replaceState = function (...args) {
|
|
992
|
-
self.originalReplaceState.apply(history, args);
|
|
993
|
-
self.resetForNavigation();
|
|
994
|
-
};
|
|
995
|
-
// Handle back/forward navigation
|
|
996
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
997
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
998
|
-
}
|
|
999
1061
|
handleScroll() {
|
|
1000
1062
|
// Debounce scroll tracking
|
|
1001
1063
|
if (this.scrollTimeout) {
|
|
@@ -1195,6 +1257,10 @@ class ClicksPlugin extends BasePlugin {
|
|
|
1195
1257
|
elementId: elementInfo.id,
|
|
1196
1258
|
elementClass: elementInfo.className,
|
|
1197
1259
|
href: target.href || undefined,
|
|
1260
|
+
x: Math.round((e.clientX / window.innerWidth) * 100),
|
|
1261
|
+
y: Math.round((e.clientY / window.innerHeight) * 100),
|
|
1262
|
+
viewportWidth: window.innerWidth,
|
|
1263
|
+
viewportHeight: window.innerHeight,
|
|
1198
1264
|
});
|
|
1199
1265
|
}
|
|
1200
1266
|
}
|
|
@@ -1217,6 +1283,9 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1217
1283
|
this.boundMarkEngaged = null;
|
|
1218
1284
|
this.boundTrackTimeOnPage = null;
|
|
1219
1285
|
this.boundVisibilityHandler = null;
|
|
1286
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1287
|
+
this.navigationHandler = null;
|
|
1288
|
+
this.popstateHandler = null;
|
|
1220
1289
|
}
|
|
1221
1290
|
init(tracker) {
|
|
1222
1291
|
super.init(tracker);
|
|
@@ -1242,6 +1311,13 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1242
1311
|
// Track time on page before unload
|
|
1243
1312
|
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1244
1313
|
document.addEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1314
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1315
|
+
// instead of independently monkey-patching history.pushState
|
|
1316
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1317
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1318
|
+
// Handle back/forward navigation
|
|
1319
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1320
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1245
1321
|
}
|
|
1246
1322
|
destroy() {
|
|
1247
1323
|
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
@@ -1255,11 +1331,28 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1255
1331
|
if (this.boundVisibilityHandler && typeof document !== 'undefined') {
|
|
1256
1332
|
document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1257
1333
|
}
|
|
1334
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1335
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1336
|
+
this.navigationHandler = null;
|
|
1337
|
+
}
|
|
1338
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1339
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1340
|
+
this.popstateHandler = null;
|
|
1341
|
+
}
|
|
1258
1342
|
if (this.engagementTimeout) {
|
|
1259
1343
|
clearTimeout(this.engagementTimeout);
|
|
1260
1344
|
}
|
|
1261
1345
|
super.destroy();
|
|
1262
1346
|
}
|
|
1347
|
+
resetForNavigation() {
|
|
1348
|
+
this.pageLoadTime = Date.now();
|
|
1349
|
+
this.engagementStartTime = Date.now();
|
|
1350
|
+
this.isEngaged = false;
|
|
1351
|
+
if (this.engagementTimeout) {
|
|
1352
|
+
clearTimeout(this.engagementTimeout);
|
|
1353
|
+
this.engagementTimeout = null;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1263
1356
|
markEngaged() {
|
|
1264
1357
|
if (!this.isEngaged) {
|
|
1265
1358
|
this.isEngaged = true;
|
|
@@ -1299,9 +1392,8 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1299
1392
|
this.name = 'downloads';
|
|
1300
1393
|
this.trackedDownloads = new Set();
|
|
1301
1394
|
this.boundHandler = null;
|
|
1302
|
-
/** SPA navigation
|
|
1303
|
-
this.
|
|
1304
|
-
this.originalReplaceState = null;
|
|
1395
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1396
|
+
this.navigationHandler = null;
|
|
1305
1397
|
this.popstateHandler = null;
|
|
1306
1398
|
}
|
|
1307
1399
|
init(tracker) {
|
|
@@ -1309,24 +1401,25 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1309
1401
|
if (typeof document !== 'undefined') {
|
|
1310
1402
|
this.boundHandler = this.handleClick.bind(this);
|
|
1311
1403
|
document.addEventListener('click', this.boundHandler, true);
|
|
1312
|
-
|
|
1313
|
-
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof window !== 'undefined') {
|
|
1406
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1407
|
+
// instead of independently monkey-patching history.pushState
|
|
1408
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1409
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1410
|
+
// Handle back/forward navigation
|
|
1411
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1412
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1314
1413
|
}
|
|
1315
1414
|
}
|
|
1316
1415
|
destroy() {
|
|
1317
1416
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1318
1417
|
document.removeEventListener('click', this.boundHandler, true);
|
|
1319
1418
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
this.originalPushState = null;
|
|
1324
|
-
}
|
|
1325
|
-
if (this.originalReplaceState) {
|
|
1326
|
-
history.replaceState = this.originalReplaceState;
|
|
1327
|
-
this.originalReplaceState = null;
|
|
1419
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1420
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1421
|
+
this.navigationHandler = null;
|
|
1328
1422
|
}
|
|
1329
|
-
// Remove popstate listener
|
|
1330
1423
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1331
1424
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
1332
1425
|
this.popstateHandler = null;
|
|
@@ -1339,29 +1432,6 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1339
1432
|
resetForNavigation() {
|
|
1340
1433
|
this.trackedDownloads.clear();
|
|
1341
1434
|
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Setup History API interception for SPA navigation
|
|
1344
|
-
*/
|
|
1345
|
-
setupNavigationReset() {
|
|
1346
|
-
if (typeof window === 'undefined')
|
|
1347
|
-
return;
|
|
1348
|
-
// Store originals for cleanup
|
|
1349
|
-
this.originalPushState = history.pushState;
|
|
1350
|
-
this.originalReplaceState = history.replaceState;
|
|
1351
|
-
// Intercept pushState and replaceState
|
|
1352
|
-
const self = this;
|
|
1353
|
-
history.pushState = function (...args) {
|
|
1354
|
-
self.originalPushState.apply(history, args);
|
|
1355
|
-
self.resetForNavigation();
|
|
1356
|
-
};
|
|
1357
|
-
history.replaceState = function (...args) {
|
|
1358
|
-
self.originalReplaceState.apply(history, args);
|
|
1359
|
-
self.resetForNavigation();
|
|
1360
|
-
};
|
|
1361
|
-
// Handle back/forward navigation
|
|
1362
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
1363
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
1364
|
-
}
|
|
1365
1435
|
handleClick(e) {
|
|
1366
1436
|
const link = e.target.closest('a');
|
|
1367
1437
|
if (!link || !link.href)
|
|
@@ -1397,6 +1467,9 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1397
1467
|
this.exitIntentShown = false;
|
|
1398
1468
|
this.pageLoadTime = 0;
|
|
1399
1469
|
this.boundHandler = null;
|
|
1470
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1471
|
+
this.navigationHandler = null;
|
|
1472
|
+
this.popstateHandler = null;
|
|
1400
1473
|
}
|
|
1401
1474
|
init(tracker) {
|
|
1402
1475
|
super.init(tracker);
|
|
@@ -1408,13 +1481,34 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1408
1481
|
this.boundHandler = this.handleMouseLeave.bind(this);
|
|
1409
1482
|
document.addEventListener('mouseleave', this.boundHandler);
|
|
1410
1483
|
}
|
|
1484
|
+
if (typeof window !== 'undefined') {
|
|
1485
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1486
|
+
// instead of independently monkey-patching history.pushState
|
|
1487
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1488
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1489
|
+
// Handle back/forward navigation
|
|
1490
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1491
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1492
|
+
}
|
|
1411
1493
|
}
|
|
1412
1494
|
destroy() {
|
|
1413
1495
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1414
1496
|
document.removeEventListener('mouseleave', this.boundHandler);
|
|
1415
1497
|
}
|
|
1498
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1499
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1500
|
+
this.navigationHandler = null;
|
|
1501
|
+
}
|
|
1502
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1503
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1504
|
+
this.popstateHandler = null;
|
|
1505
|
+
}
|
|
1416
1506
|
super.destroy();
|
|
1417
1507
|
}
|
|
1508
|
+
resetForNavigation() {
|
|
1509
|
+
this.exitIntentShown = false;
|
|
1510
|
+
this.pageLoadTime = Date.now();
|
|
1511
|
+
}
|
|
1418
1512
|
handleMouseLeave(e) {
|
|
1419
1513
|
// Only trigger when mouse leaves from the top of the page
|
|
1420
1514
|
if (e.clientY > 0 || this.exitIntentShown)
|
|
@@ -1642,6 +1736,8 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1642
1736
|
this.shownForms = new Set();
|
|
1643
1737
|
this.scrollHandler = null;
|
|
1644
1738
|
this.exitHandler = null;
|
|
1739
|
+
this.delayTimers = [];
|
|
1740
|
+
this.clickTriggerListeners = [];
|
|
1645
1741
|
}
|
|
1646
1742
|
async init(tracker) {
|
|
1647
1743
|
super.init(tracker);
|
|
@@ -1656,6 +1752,14 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1656
1752
|
}
|
|
1657
1753
|
destroy() {
|
|
1658
1754
|
this.removeTriggers();
|
|
1755
|
+
for (const timer of this.delayTimers) {
|
|
1756
|
+
clearTimeout(timer);
|
|
1757
|
+
}
|
|
1758
|
+
this.delayTimers = [];
|
|
1759
|
+
for (const { element, handler } of this.clickTriggerListeners) {
|
|
1760
|
+
element.removeEventListener('click', handler);
|
|
1761
|
+
}
|
|
1762
|
+
this.clickTriggerListeners = [];
|
|
1659
1763
|
super.destroy();
|
|
1660
1764
|
}
|
|
1661
1765
|
loadShownForms() {
|
|
@@ -1686,7 +1790,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1686
1790
|
return;
|
|
1687
1791
|
const config = this.tracker.getConfig();
|
|
1688
1792
|
const workspaceId = this.tracker.getWorkspaceId();
|
|
1689
|
-
const apiEndpoint = config.apiEndpoint || '
|
|
1793
|
+
const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
|
|
1690
1794
|
try {
|
|
1691
1795
|
const url = encodeURIComponent(window.location.href);
|
|
1692
1796
|
const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
|
|
@@ -1718,7 +1822,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1718
1822
|
this.forms.forEach(form => {
|
|
1719
1823
|
switch (form.trigger.type) {
|
|
1720
1824
|
case 'delay':
|
|
1721
|
-
setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
|
|
1825
|
+
this.delayTimers.push(setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000));
|
|
1722
1826
|
break;
|
|
1723
1827
|
case 'scroll':
|
|
1724
1828
|
this.setupScrollTrigger(form);
|
|
@@ -1761,7 +1865,9 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1761
1865
|
return;
|
|
1762
1866
|
const elements = document.querySelectorAll(form.trigger.selector);
|
|
1763
1867
|
elements.forEach(el => {
|
|
1764
|
-
|
|
1868
|
+
const handler = () => this.showForm(form);
|
|
1869
|
+
el.addEventListener('click', handler);
|
|
1870
|
+
this.clickTriggerListeners.push({ element: el, handler });
|
|
1765
1871
|
});
|
|
1766
1872
|
}
|
|
1767
1873
|
removeTriggers() {
|
|
@@ -1789,7 +1895,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1789
1895
|
if (!this.tracker)
|
|
1790
1896
|
return;
|
|
1791
1897
|
const config = this.tracker.getConfig();
|
|
1792
|
-
const apiEndpoint = config.apiEndpoint || '
|
|
1898
|
+
const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
|
|
1793
1899
|
try {
|
|
1794
1900
|
await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
|
|
1795
1901
|
method: 'POST',
|
|
@@ -2036,7 +2142,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2036
2142
|
if (!this.tracker)
|
|
2037
2143
|
return;
|
|
2038
2144
|
const config = this.tracker.getConfig();
|
|
2039
|
-
const apiEndpoint = config.apiEndpoint || '
|
|
2145
|
+
const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
|
|
2040
2146
|
const visitorId = this.tracker.getVisitorId();
|
|
2041
2147
|
// Collect form data
|
|
2042
2148
|
const formData = new FormData(formElement);
|
|
@@ -2048,7 +2154,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2048
2154
|
const submitBtn = formElement.querySelector('button[type="submit"]');
|
|
2049
2155
|
if (submitBtn) {
|
|
2050
2156
|
submitBtn.disabled = true;
|
|
2051
|
-
submitBtn.
|
|
2157
|
+
submitBtn.textContent = 'Submitting...';
|
|
2052
2158
|
}
|
|
2053
2159
|
try {
|
|
2054
2160
|
const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
|
|
@@ -2089,11 +2195,24 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2089
2195
|
if (data.email) {
|
|
2090
2196
|
this.tracker?.identify(data.email, data);
|
|
2091
2197
|
}
|
|
2092
|
-
// Redirect if configured
|
|
2198
|
+
// Redirect if configured (validate URL to prevent open redirect)
|
|
2093
2199
|
if (form.redirectUrl) {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2200
|
+
try {
|
|
2201
|
+
const redirect = new URL(form.redirectUrl, window.location.origin);
|
|
2202
|
+
const isSameOrigin = redirect.origin === window.location.origin;
|
|
2203
|
+
const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
|
|
2204
|
+
if (isSameOrigin || isSafeProtocol) {
|
|
2205
|
+
setTimeout(() => {
|
|
2206
|
+
window.location.href = redirect.href;
|
|
2207
|
+
}, 1500);
|
|
2208
|
+
}
|
|
2209
|
+
else {
|
|
2210
|
+
console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
catch {
|
|
2214
|
+
console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
|
|
2215
|
+
}
|
|
2097
2216
|
}
|
|
2098
2217
|
// Close after delay
|
|
2099
2218
|
setTimeout(() => {
|
|
@@ -2108,7 +2227,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2108
2227
|
console.error('[Clianta] Form submit error:', error);
|
|
2109
2228
|
if (submitBtn) {
|
|
2110
2229
|
submitBtn.disabled = false;
|
|
2111
|
-
submitBtn.
|
|
2230
|
+
submitBtn.textContent = form.submitButtonText || 'Subscribe';
|
|
2112
2231
|
}
|
|
2113
2232
|
}
|
|
2114
2233
|
}
|
|
@@ -2918,7 +3037,7 @@ class CRMClient {
|
|
|
2918
3037
|
* The contact is upserted in the CRM and matching workflow automations fire automatically.
|
|
2919
3038
|
*
|
|
2920
3039
|
* @example
|
|
2921
|
-
* const crm = new CRMClient('
|
|
3040
|
+
* const crm = new CRMClient('http://localhost:5000', 'WORKSPACE_ID');
|
|
2922
3041
|
* crm.setApiKey('mm_live_...');
|
|
2923
3042
|
*
|
|
2924
3043
|
* await crm.sendEvent({
|
|
@@ -3439,6 +3558,114 @@ class CRMClient {
|
|
|
3439
3558
|
});
|
|
3440
3559
|
}
|
|
3441
3560
|
// ============================================
|
|
3561
|
+
// READ-BACK / DATA RETRIEVAL API
|
|
3562
|
+
// ============================================
|
|
3563
|
+
/**
|
|
3564
|
+
* Get a contact by email address.
|
|
3565
|
+
* Returns the first matching contact from a search query.
|
|
3566
|
+
*/
|
|
3567
|
+
async getContactByEmail(email) {
|
|
3568
|
+
this.validateRequired('email', email, 'getContactByEmail');
|
|
3569
|
+
const queryParams = new URLSearchParams({ search: email, limit: '1' });
|
|
3570
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
|
|
3571
|
+
}
|
|
3572
|
+
/**
|
|
3573
|
+
* Get activity timeline for a contact
|
|
3574
|
+
*/
|
|
3575
|
+
async getContactActivity(contactId, params) {
|
|
3576
|
+
this.validateRequired('contactId', contactId, 'getContactActivity');
|
|
3577
|
+
const queryParams = new URLSearchParams();
|
|
3578
|
+
if (params?.page)
|
|
3579
|
+
queryParams.set('page', params.page.toString());
|
|
3580
|
+
if (params?.limit)
|
|
3581
|
+
queryParams.set('limit', params.limit.toString());
|
|
3582
|
+
if (params?.type)
|
|
3583
|
+
queryParams.set('type', params.type);
|
|
3584
|
+
if (params?.startDate)
|
|
3585
|
+
queryParams.set('startDate', params.startDate);
|
|
3586
|
+
if (params?.endDate)
|
|
3587
|
+
queryParams.set('endDate', params.endDate);
|
|
3588
|
+
const query = queryParams.toString();
|
|
3589
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
3590
|
+
return this.request(endpoint);
|
|
3591
|
+
}
|
|
3592
|
+
/**
|
|
3593
|
+
* Get engagement metrics for a contact (via their linked visitor data)
|
|
3594
|
+
*/
|
|
3595
|
+
async getContactEngagement(contactId) {
|
|
3596
|
+
this.validateRequired('contactId', contactId, 'getContactEngagement');
|
|
3597
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Get a full timeline for a contact including events, activities, and opportunities
|
|
3601
|
+
*/
|
|
3602
|
+
async getContactTimeline(contactId, params) {
|
|
3603
|
+
this.validateRequired('contactId', contactId, 'getContactTimeline');
|
|
3604
|
+
const queryParams = new URLSearchParams();
|
|
3605
|
+
if (params?.page)
|
|
3606
|
+
queryParams.set('page', params.page.toString());
|
|
3607
|
+
if (params?.limit)
|
|
3608
|
+
queryParams.set('limit', params.limit.toString());
|
|
3609
|
+
const query = queryParams.toString();
|
|
3610
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
|
|
3611
|
+
return this.request(endpoint);
|
|
3612
|
+
}
|
|
3613
|
+
/**
|
|
3614
|
+
* Search contacts with advanced filters
|
|
3615
|
+
*/
|
|
3616
|
+
async searchContacts(query, filters) {
|
|
3617
|
+
const queryParams = new URLSearchParams();
|
|
3618
|
+
queryParams.set('search', query);
|
|
3619
|
+
if (filters?.status)
|
|
3620
|
+
queryParams.set('status', filters.status);
|
|
3621
|
+
if (filters?.lifecycleStage)
|
|
3622
|
+
queryParams.set('lifecycleStage', filters.lifecycleStage);
|
|
3623
|
+
if (filters?.source)
|
|
3624
|
+
queryParams.set('source', filters.source);
|
|
3625
|
+
if (filters?.tags)
|
|
3626
|
+
queryParams.set('tags', filters.tags.join(','));
|
|
3627
|
+
if (filters?.page)
|
|
3628
|
+
queryParams.set('page', filters.page.toString());
|
|
3629
|
+
if (filters?.limit)
|
|
3630
|
+
queryParams.set('limit', filters.limit.toString());
|
|
3631
|
+
const qs = queryParams.toString();
|
|
3632
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
|
|
3633
|
+
return this.request(endpoint);
|
|
3634
|
+
}
|
|
3635
|
+
// ============================================
|
|
3636
|
+
// WEBHOOK MANAGEMENT API
|
|
3637
|
+
// ============================================
|
|
3638
|
+
/**
|
|
3639
|
+
* List all webhook subscriptions
|
|
3640
|
+
*/
|
|
3641
|
+
async listWebhooks(params) {
|
|
3642
|
+
const queryParams = new URLSearchParams();
|
|
3643
|
+
if (params?.page)
|
|
3644
|
+
queryParams.set('page', params.page.toString());
|
|
3645
|
+
if (params?.limit)
|
|
3646
|
+
queryParams.set('limit', params.limit.toString());
|
|
3647
|
+
const query = queryParams.toString();
|
|
3648
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* Create a new webhook subscription
|
|
3652
|
+
*/
|
|
3653
|
+
async createWebhook(data) {
|
|
3654
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
|
|
3655
|
+
method: 'POST',
|
|
3656
|
+
body: JSON.stringify(data),
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Delete a webhook subscription
|
|
3661
|
+
*/
|
|
3662
|
+
async deleteWebhook(webhookId) {
|
|
3663
|
+
this.validateRequired('webhookId', webhookId, 'deleteWebhook');
|
|
3664
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
|
|
3665
|
+
method: 'DELETE',
|
|
3666
|
+
});
|
|
3667
|
+
}
|
|
3668
|
+
// ============================================
|
|
3442
3669
|
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3443
3670
|
// ============================================
|
|
3444
3671
|
/**
|
|
@@ -3482,6 +3709,8 @@ class Tracker {
|
|
|
3482
3709
|
this.contactId = null;
|
|
3483
3710
|
/** Pending identify retry on next flush */
|
|
3484
3711
|
this.pendingIdentify = null;
|
|
3712
|
+
/** Registered event schemas for validation */
|
|
3713
|
+
this.eventSchemas = new Map();
|
|
3485
3714
|
if (!workspaceId) {
|
|
3486
3715
|
throw new Error('[Clianta] Workspace ID is required');
|
|
3487
3716
|
}
|
|
@@ -3507,6 +3736,16 @@ class Tracker {
|
|
|
3507
3736
|
this.visitorId = this.createVisitorId();
|
|
3508
3737
|
this.sessionId = this.createSessionId();
|
|
3509
3738
|
logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
|
|
3739
|
+
// Security warnings
|
|
3740
|
+
if (this.config.apiEndpoint.startsWith('http://') &&
|
|
3741
|
+
typeof window !== 'undefined' &&
|
|
3742
|
+
!window.location.hostname.includes('localhost') &&
|
|
3743
|
+
!window.location.hostname.includes('127.0.0.1')) {
|
|
3744
|
+
logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
|
|
3745
|
+
}
|
|
3746
|
+
if (this.config.apiKey && typeof window !== 'undefined') {
|
|
3747
|
+
logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
|
|
3748
|
+
}
|
|
3510
3749
|
// Initialize plugins
|
|
3511
3750
|
this.initPlugins();
|
|
3512
3751
|
this.isInitialized = true;
|
|
@@ -3612,6 +3851,7 @@ class Tracker {
|
|
|
3612
3851
|
properties: {
|
|
3613
3852
|
...properties,
|
|
3614
3853
|
eventId: generateUUID(), // Unique ID for deduplication on retry
|
|
3854
|
+
websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
|
|
3615
3855
|
},
|
|
3616
3856
|
device: getDeviceInfo(),
|
|
3617
3857
|
...getUTMParams(),
|
|
@@ -3622,6 +3862,8 @@ class Tracker {
|
|
|
3622
3862
|
if (this.contactId) {
|
|
3623
3863
|
event.contactId = this.contactId;
|
|
3624
3864
|
}
|
|
3865
|
+
// Validate event against registered schema (debug mode only)
|
|
3866
|
+
this.validateEventSchema(eventType, properties);
|
|
3625
3867
|
// Check consent before tracking
|
|
3626
3868
|
if (!this.consentManager.canTrack()) {
|
|
3627
3869
|
// Buffer event for later if waitForConsent is enabled
|
|
@@ -3656,6 +3898,10 @@ class Tracker {
|
|
|
3656
3898
|
logger.warn('Email is required for identification');
|
|
3657
3899
|
return null;
|
|
3658
3900
|
}
|
|
3901
|
+
if (!isValidEmail(email)) {
|
|
3902
|
+
logger.warn('Invalid email format, identification skipped:', email);
|
|
3903
|
+
return null;
|
|
3904
|
+
}
|
|
3659
3905
|
logger.info('Identifying visitor:', email);
|
|
3660
3906
|
const result = await this.transport.sendIdentify({
|
|
3661
3907
|
workspaceId: this.workspaceId,
|
|
@@ -3690,6 +3936,83 @@ class Tracker {
|
|
|
3690
3936
|
const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
|
|
3691
3937
|
return client.sendEvent(payload);
|
|
3692
3938
|
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Get the current visitor's profile from the CRM.
|
|
3941
|
+
* Returns visitor data and linked contact info if identified.
|
|
3942
|
+
* Only returns data for the current visitor (privacy-safe for frontend).
|
|
3943
|
+
*/
|
|
3944
|
+
async getVisitorProfile() {
|
|
3945
|
+
if (!this.isInitialized) {
|
|
3946
|
+
logger.warn('SDK not initialized');
|
|
3947
|
+
return null;
|
|
3948
|
+
}
|
|
3949
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
|
|
3950
|
+
if (result.success && result.data) {
|
|
3951
|
+
logger.debug('Visitor profile fetched:', result.data);
|
|
3952
|
+
return result.data;
|
|
3953
|
+
}
|
|
3954
|
+
logger.warn('Failed to fetch visitor profile:', result.error);
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
/**
|
|
3958
|
+
* Get the current visitor's recent activity/events.
|
|
3959
|
+
* Returns paginated list of tracking events for this visitor.
|
|
3960
|
+
*/
|
|
3961
|
+
async getVisitorActivity(options) {
|
|
3962
|
+
if (!this.isInitialized) {
|
|
3963
|
+
logger.warn('SDK not initialized');
|
|
3964
|
+
return null;
|
|
3965
|
+
}
|
|
3966
|
+
const params = {};
|
|
3967
|
+
if (options?.page)
|
|
3968
|
+
params.page = options.page.toString();
|
|
3969
|
+
if (options?.limit)
|
|
3970
|
+
params.limit = options.limit.toString();
|
|
3971
|
+
if (options?.eventType)
|
|
3972
|
+
params.eventType = options.eventType;
|
|
3973
|
+
if (options?.startDate)
|
|
3974
|
+
params.startDate = options.startDate;
|
|
3975
|
+
if (options?.endDate)
|
|
3976
|
+
params.endDate = options.endDate;
|
|
3977
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
|
|
3978
|
+
if (result.success && result.data) {
|
|
3979
|
+
return result.data;
|
|
3980
|
+
}
|
|
3981
|
+
logger.warn('Failed to fetch visitor activity:', result.error);
|
|
3982
|
+
return null;
|
|
3983
|
+
}
|
|
3984
|
+
/**
|
|
3985
|
+
* Get a summarized journey timeline for the current visitor.
|
|
3986
|
+
* Includes top pages, sessions, time spent, and recent activities.
|
|
3987
|
+
*/
|
|
3988
|
+
async getVisitorTimeline() {
|
|
3989
|
+
if (!this.isInitialized) {
|
|
3990
|
+
logger.warn('SDK not initialized');
|
|
3991
|
+
return null;
|
|
3992
|
+
}
|
|
3993
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
|
|
3994
|
+
if (result.success && result.data) {
|
|
3995
|
+
return result.data;
|
|
3996
|
+
}
|
|
3997
|
+
logger.warn('Failed to fetch visitor timeline:', result.error);
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Get engagement metrics for the current visitor.
|
|
4002
|
+
* Includes time on site, page views, bounce rate, and engagement score.
|
|
4003
|
+
*/
|
|
4004
|
+
async getVisitorEngagement() {
|
|
4005
|
+
if (!this.isInitialized) {
|
|
4006
|
+
logger.warn('SDK not initialized');
|
|
4007
|
+
return null;
|
|
4008
|
+
}
|
|
4009
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
|
|
4010
|
+
if (result.success && result.data) {
|
|
4011
|
+
return result.data;
|
|
4012
|
+
}
|
|
4013
|
+
logger.warn('Failed to fetch visitor engagement:', result.error);
|
|
4014
|
+
return null;
|
|
4015
|
+
}
|
|
3693
4016
|
/**
|
|
3694
4017
|
* Retry pending identify call
|
|
3695
4018
|
*/
|
|
@@ -3719,6 +4042,59 @@ class Tracker {
|
|
|
3719
4042
|
logger.enabled = enabled;
|
|
3720
4043
|
logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
3721
4044
|
}
|
|
4045
|
+
/**
|
|
4046
|
+
* Register a schema for event validation.
|
|
4047
|
+
* When debug mode is enabled, events will be validated against registered schemas.
|
|
4048
|
+
*
|
|
4049
|
+
* @example
|
|
4050
|
+
* tracker.registerEventSchema('purchase', {
|
|
4051
|
+
* productId: 'string',
|
|
4052
|
+
* price: 'number',
|
|
4053
|
+
* quantity: 'number',
|
|
4054
|
+
* });
|
|
4055
|
+
*/
|
|
4056
|
+
registerEventSchema(eventType, schema) {
|
|
4057
|
+
this.eventSchemas.set(eventType, schema);
|
|
4058
|
+
logger.debug('Event schema registered:', eventType);
|
|
4059
|
+
}
|
|
4060
|
+
/**
|
|
4061
|
+
* Validate event properties against a registered schema (debug mode only)
|
|
4062
|
+
*/
|
|
4063
|
+
validateEventSchema(eventType, properties) {
|
|
4064
|
+
if (!this.config.debug)
|
|
4065
|
+
return;
|
|
4066
|
+
const schema = this.eventSchemas.get(eventType);
|
|
4067
|
+
if (!schema)
|
|
4068
|
+
return;
|
|
4069
|
+
for (const [key, expectedType] of Object.entries(schema)) {
|
|
4070
|
+
const value = properties[key];
|
|
4071
|
+
if (value === undefined) {
|
|
4072
|
+
logger.warn(`[Schema] Missing property "${key}" for event type "${eventType}"`);
|
|
4073
|
+
continue;
|
|
4074
|
+
}
|
|
4075
|
+
let valid = false;
|
|
4076
|
+
switch (expectedType) {
|
|
4077
|
+
case 'string':
|
|
4078
|
+
valid = typeof value === 'string';
|
|
4079
|
+
break;
|
|
4080
|
+
case 'number':
|
|
4081
|
+
valid = typeof value === 'number';
|
|
4082
|
+
break;
|
|
4083
|
+
case 'boolean':
|
|
4084
|
+
valid = typeof value === 'boolean';
|
|
4085
|
+
break;
|
|
4086
|
+
case 'object':
|
|
4087
|
+
valid = typeof value === 'object' && !Array.isArray(value);
|
|
4088
|
+
break;
|
|
4089
|
+
case 'array':
|
|
4090
|
+
valid = Array.isArray(value);
|
|
4091
|
+
break;
|
|
4092
|
+
}
|
|
4093
|
+
if (!valid) {
|
|
4094
|
+
logger.warn(`[Schema] Property "${key}" for event "${eventType}" expected ${expectedType}, got ${typeof value}`);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
3722
4098
|
/**
|
|
3723
4099
|
* Get visitor ID
|
|
3724
4100
|
*/
|
|
@@ -3758,6 +4134,8 @@ class Tracker {
|
|
|
3758
4134
|
resetIds(this.config.useCookies);
|
|
3759
4135
|
this.visitorId = this.createVisitorId();
|
|
3760
4136
|
this.sessionId = this.createSessionId();
|
|
4137
|
+
this.contactId = null;
|
|
4138
|
+
this.pendingIdentify = null;
|
|
3761
4139
|
this.queue.clear();
|
|
3762
4140
|
}
|
|
3763
4141
|
/**
|
|
@@ -3799,6 +4177,86 @@ class Tracker {
|
|
|
3799
4177
|
this.sessionId = this.createSessionId();
|
|
3800
4178
|
logger.info('All user data deleted');
|
|
3801
4179
|
}
|
|
4180
|
+
// ============================================
|
|
4181
|
+
// PUBLIC CRM METHODS (no API key required)
|
|
4182
|
+
// ============================================
|
|
4183
|
+
/**
|
|
4184
|
+
* Create or update a contact by email (upsert).
|
|
4185
|
+
* Secured by domain whitelist — no API key needed.
|
|
4186
|
+
*/
|
|
4187
|
+
async createContact(data) {
|
|
4188
|
+
return this.publicCrmRequest('/api/public/crm/contacts', 'POST', {
|
|
4189
|
+
workspaceId: this.workspaceId,
|
|
4190
|
+
...data,
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
/**
|
|
4194
|
+
* Update an existing contact by ID (limited fields only).
|
|
4195
|
+
*/
|
|
4196
|
+
async updateContact(contactId, data) {
|
|
4197
|
+
return this.publicCrmRequest(`/api/public/crm/contacts/${contactId}`, 'PUT', {
|
|
4198
|
+
workspaceId: this.workspaceId,
|
|
4199
|
+
...data,
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
/**
|
|
4203
|
+
* Submit a form — creates/updates contact from form data.
|
|
4204
|
+
*/
|
|
4205
|
+
async submitForm(formId, data) {
|
|
4206
|
+
const payload = {
|
|
4207
|
+
...data,
|
|
4208
|
+
metadata: {
|
|
4209
|
+
...data.metadata,
|
|
4210
|
+
visitorId: this.visitorId,
|
|
4211
|
+
sessionId: this.sessionId,
|
|
4212
|
+
pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
4213
|
+
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
4214
|
+
},
|
|
4215
|
+
};
|
|
4216
|
+
return this.publicCrmRequest(`/api/public/crm/forms/${formId}/submit`, 'POST', payload);
|
|
4217
|
+
}
|
|
4218
|
+
/**
|
|
4219
|
+
* Log an activity linked to a contact (append-only).
|
|
4220
|
+
*/
|
|
4221
|
+
async logActivity(data) {
|
|
4222
|
+
return this.publicCrmRequest('/api/public/crm/activities', 'POST', {
|
|
4223
|
+
workspaceId: this.workspaceId,
|
|
4224
|
+
...data,
|
|
4225
|
+
});
|
|
4226
|
+
}
|
|
4227
|
+
/**
|
|
4228
|
+
* Create an opportunity (e.g., from "Request Demo" forms).
|
|
4229
|
+
*/
|
|
4230
|
+
async createOpportunity(data) {
|
|
4231
|
+
return this.publicCrmRequest('/api/public/crm/opportunities', 'POST', {
|
|
4232
|
+
workspaceId: this.workspaceId,
|
|
4233
|
+
...data,
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Internal helper for public CRM API calls.
|
|
4238
|
+
*/
|
|
4239
|
+
async publicCrmRequest(path, method, body) {
|
|
4240
|
+
const url = `${this.config.apiEndpoint}${path}`;
|
|
4241
|
+
try {
|
|
4242
|
+
const response = await fetch(url, {
|
|
4243
|
+
method,
|
|
4244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4245
|
+
body: JSON.stringify(body),
|
|
4246
|
+
});
|
|
4247
|
+
const data = await response.json().catch(() => ({}));
|
|
4248
|
+
if (response.ok) {
|
|
4249
|
+
logger.debug(`Public CRM ${method} ${path} succeeded`);
|
|
4250
|
+
return { success: true, data: data.data ?? data, status: response.status };
|
|
4251
|
+
}
|
|
4252
|
+
logger.error(`Public CRM ${method} ${path} failed (${response.status}):`, data.message);
|
|
4253
|
+
return { success: false, error: data.message, status: response.status };
|
|
4254
|
+
}
|
|
4255
|
+
catch (error) {
|
|
4256
|
+
logger.error(`Public CRM ${method} ${path} error:`, error);
|
|
4257
|
+
return { success: false, error: error.message };
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
3802
4260
|
/**
|
|
3803
4261
|
* Destroy tracker and cleanup
|
|
3804
4262
|
*/
|
|
@@ -3894,7 +4352,7 @@ const CliantaKey = Symbol('clianta');
|
|
|
3894
4352
|
* const app = createApp(App);
|
|
3895
4353
|
* app.use(CliantaPlugin, {
|
|
3896
4354
|
* projectId: 'your-project-id',
|
|
3897
|
-
* apiEndpoint:
|
|
4355
|
+
* apiEndpoint: import.meta.env.VITE_CLIANTA_API_ENDPOINT || 'http://localhost:5000',
|
|
3898
4356
|
* debug: import.meta.env.DEV,
|
|
3899
4357
|
* });
|
|
3900
4358
|
* app.mount('#app');
|