@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/clianta.esm.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
|
*/
|
|
@@ -9,15 +9,22 @@
|
|
|
9
9
|
*/
|
|
10
10
|
/** SDK Version */
|
|
11
11
|
const SDK_VERSION = '1.4.0';
|
|
12
|
-
/** Default API endpoint
|
|
12
|
+
/** Default API endpoint — reads from env or falls back to localhost */
|
|
13
13
|
const getDefaultApiEndpoint = () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
// Build-time env var (works with Next.js, Vite, CRA, etc.)
|
|
15
|
+
if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_CLIANTA_API_ENDPOINT) {
|
|
16
|
+
return process.env.NEXT_PUBLIC_CLIANTA_API_ENDPOINT;
|
|
17
|
+
}
|
|
18
|
+
if (typeof process !== 'undefined' && process.env?.VITE_CLIANTA_API_ENDPOINT) {
|
|
19
|
+
return process.env.VITE_CLIANTA_API_ENDPOINT;
|
|
20
|
+
}
|
|
21
|
+
if (typeof process !== 'undefined' && process.env?.REACT_APP_CLIANTA_API_ENDPOINT) {
|
|
22
|
+
return process.env.REACT_APP_CLIANTA_API_ENDPOINT;
|
|
19
23
|
}
|
|
20
|
-
|
|
24
|
+
if (typeof process !== 'undefined' && process.env?.CLIANTA_API_ENDPOINT) {
|
|
25
|
+
return process.env.CLIANTA_API_ENDPOINT;
|
|
26
|
+
}
|
|
27
|
+
return 'http://localhost:5000';
|
|
21
28
|
};
|
|
22
29
|
/** Core plugins enabled by default */
|
|
23
30
|
const DEFAULT_PLUGINS = [
|
|
@@ -50,6 +57,7 @@ const DEFAULT_CONFIG = {
|
|
|
50
57
|
cookieDomain: '',
|
|
51
58
|
useCookies: false,
|
|
52
59
|
cookielessMode: false,
|
|
60
|
+
persistMode: 'session',
|
|
53
61
|
};
|
|
54
62
|
/** Storage keys */
|
|
55
63
|
const STORAGE_KEYS = {
|
|
@@ -243,6 +251,39 @@ class Transport {
|
|
|
243
251
|
return false;
|
|
244
252
|
}
|
|
245
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Fetch data from the tracking API (GET request)
|
|
256
|
+
* Used for read-back APIs (visitor profile, activity, etc.)
|
|
257
|
+
*/
|
|
258
|
+
async fetchData(path, params) {
|
|
259
|
+
const url = new URL(`${this.config.apiEndpoint}${path}`);
|
|
260
|
+
if (params) {
|
|
261
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
262
|
+
if (value !== undefined && value !== null) {
|
|
263
|
+
url.searchParams.set(key, value);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const response = await this.fetchWithTimeout(url.toString(), {
|
|
269
|
+
method: 'GET',
|
|
270
|
+
headers: {
|
|
271
|
+
'Accept': 'application/json',
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
if (response.ok) {
|
|
275
|
+
const body = await response.json();
|
|
276
|
+
logger.debug('Fetch successful:', path);
|
|
277
|
+
return { success: true, data: body.data ?? body, status: response.status };
|
|
278
|
+
}
|
|
279
|
+
logger.error(`Fetch failed with status ${response.status}`);
|
|
280
|
+
return { success: false, status: response.status };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
logger.error('Fetch request failed:', error);
|
|
284
|
+
return { success: false, error: error };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
246
287
|
/**
|
|
247
288
|
* Internal send with retry logic
|
|
248
289
|
*/
|
|
@@ -407,7 +448,9 @@ function cookie(name, value, days) {
|
|
|
407
448
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
408
449
|
expires = '; expires=' + date.toUTCString();
|
|
409
450
|
}
|
|
410
|
-
|
|
451
|
+
// Add Secure flag on HTTPS to prevent cookie leakage over plaintext
|
|
452
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
|
|
453
|
+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax' + secure;
|
|
411
454
|
return value;
|
|
412
455
|
}
|
|
413
456
|
// ============================================
|
|
@@ -571,6 +614,17 @@ function isMobile() {
|
|
|
571
614
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
572
615
|
}
|
|
573
616
|
// ============================================
|
|
617
|
+
// VALIDATION UTILITIES
|
|
618
|
+
// ============================================
|
|
619
|
+
/**
|
|
620
|
+
* Validate email format
|
|
621
|
+
*/
|
|
622
|
+
function isValidEmail(email) {
|
|
623
|
+
if (typeof email !== 'string' || !email)
|
|
624
|
+
return false;
|
|
625
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
626
|
+
}
|
|
627
|
+
// ============================================
|
|
574
628
|
// DEVICE INFO
|
|
575
629
|
// ============================================
|
|
576
630
|
/**
|
|
@@ -624,6 +678,7 @@ class EventQueue {
|
|
|
624
678
|
maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
625
679
|
storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
|
|
626
680
|
};
|
|
681
|
+
this.persistMode = config.persistMode || 'session';
|
|
627
682
|
// Restore persisted queue
|
|
628
683
|
this.restoreQueue();
|
|
629
684
|
// Start auto-flush timer
|
|
@@ -729,6 +784,13 @@ class EventQueue {
|
|
|
729
784
|
clear() {
|
|
730
785
|
this.queue = [];
|
|
731
786
|
this.persistQueue([]);
|
|
787
|
+
// Also clear localStorage if used
|
|
788
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
789
|
+
try {
|
|
790
|
+
localStorage.removeItem(this.config.storageKey);
|
|
791
|
+
}
|
|
792
|
+
catch { /* ignore */ }
|
|
793
|
+
}
|
|
732
794
|
}
|
|
733
795
|
/**
|
|
734
796
|
* Stop the flush timer and cleanup handlers
|
|
@@ -783,22 +845,44 @@ class EventQueue {
|
|
|
783
845
|
window.addEventListener('pagehide', this.boundPageHide);
|
|
784
846
|
}
|
|
785
847
|
/**
|
|
786
|
-
* Persist queue to
|
|
848
|
+
* Persist queue to storage based on persistMode
|
|
787
849
|
*/
|
|
788
850
|
persistQueue(events) {
|
|
851
|
+
if (this.persistMode === 'none')
|
|
852
|
+
return;
|
|
789
853
|
try {
|
|
790
|
-
|
|
854
|
+
const serialized = JSON.stringify(events);
|
|
855
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
856
|
+
try {
|
|
857
|
+
localStorage.setItem(this.config.storageKey, serialized);
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
// localStorage quota exceeded — fallback to sessionStorage
|
|
861
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
866
|
+
}
|
|
791
867
|
}
|
|
792
868
|
catch {
|
|
793
869
|
// Ignore storage errors
|
|
794
870
|
}
|
|
795
871
|
}
|
|
796
872
|
/**
|
|
797
|
-
* Restore queue from
|
|
873
|
+
* Restore queue from storage
|
|
798
874
|
*/
|
|
799
875
|
restoreQueue() {
|
|
800
876
|
try {
|
|
801
|
-
|
|
877
|
+
let stored = null;
|
|
878
|
+
// Check localStorage first (cross-session persistence)
|
|
879
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
880
|
+
stored = localStorage.getItem(this.config.storageKey);
|
|
881
|
+
}
|
|
882
|
+
// Fall back to sessionStorage
|
|
883
|
+
if (!stored) {
|
|
884
|
+
stored = getSessionStorage(this.config.storageKey);
|
|
885
|
+
}
|
|
802
886
|
if (stored) {
|
|
803
887
|
const events = JSON.parse(stored);
|
|
804
888
|
if (Array.isArray(events) && events.length > 0) {
|
|
@@ -866,10 +950,13 @@ class PageViewPlugin extends BasePlugin {
|
|
|
866
950
|
history.pushState = function (...args) {
|
|
867
951
|
self.originalPushState.apply(history, args);
|
|
868
952
|
self.trackPageView();
|
|
953
|
+
// Notify other plugins (e.g. ScrollPlugin) about navigation
|
|
954
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
869
955
|
};
|
|
870
956
|
history.replaceState = function (...args) {
|
|
871
957
|
self.originalReplaceState.apply(history, args);
|
|
872
958
|
self.trackPageView();
|
|
959
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
873
960
|
};
|
|
874
961
|
// Handle back/forward navigation
|
|
875
962
|
this.popstateHandler = () => this.trackPageView();
|
|
@@ -923,9 +1010,8 @@ class ScrollPlugin extends BasePlugin {
|
|
|
923
1010
|
this.pageLoadTime = 0;
|
|
924
1011
|
this.scrollTimeout = null;
|
|
925
1012
|
this.boundHandler = null;
|
|
926
|
-
/** SPA navigation
|
|
927
|
-
this.
|
|
928
|
-
this.originalReplaceState = null;
|
|
1013
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1014
|
+
this.navigationHandler = null;
|
|
929
1015
|
this.popstateHandler = null;
|
|
930
1016
|
}
|
|
931
1017
|
init(tracker) {
|
|
@@ -934,8 +1020,13 @@ class ScrollPlugin extends BasePlugin {
|
|
|
934
1020
|
if (typeof window !== 'undefined') {
|
|
935
1021
|
this.boundHandler = this.handleScroll.bind(this);
|
|
936
1022
|
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
937
|
-
//
|
|
938
|
-
|
|
1023
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1024
|
+
// instead of independently monkey-patching history.pushState
|
|
1025
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1026
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1027
|
+
// Handle back/forward navigation
|
|
1028
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1029
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
939
1030
|
}
|
|
940
1031
|
}
|
|
941
1032
|
destroy() {
|
|
@@ -945,16 +1036,10 @@ class ScrollPlugin extends BasePlugin {
|
|
|
945
1036
|
if (this.scrollTimeout) {
|
|
946
1037
|
clearTimeout(this.scrollTimeout);
|
|
947
1038
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
this.originalPushState = null;
|
|
952
|
-
}
|
|
953
|
-
if (this.originalReplaceState) {
|
|
954
|
-
history.replaceState = this.originalReplaceState;
|
|
955
|
-
this.originalReplaceState = null;
|
|
1039
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1040
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1041
|
+
this.navigationHandler = null;
|
|
956
1042
|
}
|
|
957
|
-
// Remove popstate listener
|
|
958
1043
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
959
1044
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
960
1045
|
this.popstateHandler = null;
|
|
@@ -969,29 +1054,6 @@ class ScrollPlugin extends BasePlugin {
|
|
|
969
1054
|
this.maxScrollDepth = 0;
|
|
970
1055
|
this.pageLoadTime = Date.now();
|
|
971
1056
|
}
|
|
972
|
-
/**
|
|
973
|
-
* Setup History API interception for SPA navigation
|
|
974
|
-
*/
|
|
975
|
-
setupNavigationReset() {
|
|
976
|
-
if (typeof window === 'undefined')
|
|
977
|
-
return;
|
|
978
|
-
// Store originals for cleanup
|
|
979
|
-
this.originalPushState = history.pushState;
|
|
980
|
-
this.originalReplaceState = history.replaceState;
|
|
981
|
-
// Intercept pushState and replaceState
|
|
982
|
-
const self = this;
|
|
983
|
-
history.pushState = function (...args) {
|
|
984
|
-
self.originalPushState.apply(history, args);
|
|
985
|
-
self.resetForNavigation();
|
|
986
|
-
};
|
|
987
|
-
history.replaceState = function (...args) {
|
|
988
|
-
self.originalReplaceState.apply(history, args);
|
|
989
|
-
self.resetForNavigation();
|
|
990
|
-
};
|
|
991
|
-
// Handle back/forward navigation
|
|
992
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
993
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
994
|
-
}
|
|
995
1057
|
handleScroll() {
|
|
996
1058
|
// Debounce scroll tracking
|
|
997
1059
|
if (this.scrollTimeout) {
|
|
@@ -1191,6 +1253,10 @@ class ClicksPlugin extends BasePlugin {
|
|
|
1191
1253
|
elementId: elementInfo.id,
|
|
1192
1254
|
elementClass: elementInfo.className,
|
|
1193
1255
|
href: target.href || undefined,
|
|
1256
|
+
x: Math.round((e.clientX / window.innerWidth) * 100),
|
|
1257
|
+
y: Math.round((e.clientY / window.innerHeight) * 100),
|
|
1258
|
+
viewportWidth: window.innerWidth,
|
|
1259
|
+
viewportHeight: window.innerHeight,
|
|
1194
1260
|
});
|
|
1195
1261
|
}
|
|
1196
1262
|
}
|
|
@@ -1213,6 +1279,9 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1213
1279
|
this.boundMarkEngaged = null;
|
|
1214
1280
|
this.boundTrackTimeOnPage = null;
|
|
1215
1281
|
this.boundVisibilityHandler = null;
|
|
1282
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1283
|
+
this.navigationHandler = null;
|
|
1284
|
+
this.popstateHandler = null;
|
|
1216
1285
|
}
|
|
1217
1286
|
init(tracker) {
|
|
1218
1287
|
super.init(tracker);
|
|
@@ -1238,6 +1307,13 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1238
1307
|
// Track time on page before unload
|
|
1239
1308
|
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1240
1309
|
document.addEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1310
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1311
|
+
// instead of independently monkey-patching history.pushState
|
|
1312
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1313
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1314
|
+
// Handle back/forward navigation
|
|
1315
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1316
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1241
1317
|
}
|
|
1242
1318
|
destroy() {
|
|
1243
1319
|
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
@@ -1251,11 +1327,28 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1251
1327
|
if (this.boundVisibilityHandler && typeof document !== 'undefined') {
|
|
1252
1328
|
document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1253
1329
|
}
|
|
1330
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1331
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1332
|
+
this.navigationHandler = null;
|
|
1333
|
+
}
|
|
1334
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1335
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1336
|
+
this.popstateHandler = null;
|
|
1337
|
+
}
|
|
1254
1338
|
if (this.engagementTimeout) {
|
|
1255
1339
|
clearTimeout(this.engagementTimeout);
|
|
1256
1340
|
}
|
|
1257
1341
|
super.destroy();
|
|
1258
1342
|
}
|
|
1343
|
+
resetForNavigation() {
|
|
1344
|
+
this.pageLoadTime = Date.now();
|
|
1345
|
+
this.engagementStartTime = Date.now();
|
|
1346
|
+
this.isEngaged = false;
|
|
1347
|
+
if (this.engagementTimeout) {
|
|
1348
|
+
clearTimeout(this.engagementTimeout);
|
|
1349
|
+
this.engagementTimeout = null;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1259
1352
|
markEngaged() {
|
|
1260
1353
|
if (!this.isEngaged) {
|
|
1261
1354
|
this.isEngaged = true;
|
|
@@ -1295,9 +1388,8 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1295
1388
|
this.name = 'downloads';
|
|
1296
1389
|
this.trackedDownloads = new Set();
|
|
1297
1390
|
this.boundHandler = null;
|
|
1298
|
-
/** SPA navigation
|
|
1299
|
-
this.
|
|
1300
|
-
this.originalReplaceState = null;
|
|
1391
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1392
|
+
this.navigationHandler = null;
|
|
1301
1393
|
this.popstateHandler = null;
|
|
1302
1394
|
}
|
|
1303
1395
|
init(tracker) {
|
|
@@ -1305,24 +1397,25 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1305
1397
|
if (typeof document !== 'undefined') {
|
|
1306
1398
|
this.boundHandler = this.handleClick.bind(this);
|
|
1307
1399
|
document.addEventListener('click', this.boundHandler, true);
|
|
1308
|
-
|
|
1309
|
-
|
|
1400
|
+
}
|
|
1401
|
+
if (typeof window !== 'undefined') {
|
|
1402
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1403
|
+
// instead of independently monkey-patching history.pushState
|
|
1404
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1405
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1406
|
+
// Handle back/forward navigation
|
|
1407
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1408
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1310
1409
|
}
|
|
1311
1410
|
}
|
|
1312
1411
|
destroy() {
|
|
1313
1412
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1314
1413
|
document.removeEventListener('click', this.boundHandler, true);
|
|
1315
1414
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
this.originalPushState = null;
|
|
1320
|
-
}
|
|
1321
|
-
if (this.originalReplaceState) {
|
|
1322
|
-
history.replaceState = this.originalReplaceState;
|
|
1323
|
-
this.originalReplaceState = null;
|
|
1415
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1416
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1417
|
+
this.navigationHandler = null;
|
|
1324
1418
|
}
|
|
1325
|
-
// Remove popstate listener
|
|
1326
1419
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1327
1420
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
1328
1421
|
this.popstateHandler = null;
|
|
@@ -1335,29 +1428,6 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1335
1428
|
resetForNavigation() {
|
|
1336
1429
|
this.trackedDownloads.clear();
|
|
1337
1430
|
}
|
|
1338
|
-
/**
|
|
1339
|
-
* Setup History API interception for SPA navigation
|
|
1340
|
-
*/
|
|
1341
|
-
setupNavigationReset() {
|
|
1342
|
-
if (typeof window === 'undefined')
|
|
1343
|
-
return;
|
|
1344
|
-
// Store originals for cleanup
|
|
1345
|
-
this.originalPushState = history.pushState;
|
|
1346
|
-
this.originalReplaceState = history.replaceState;
|
|
1347
|
-
// Intercept pushState and replaceState
|
|
1348
|
-
const self = this;
|
|
1349
|
-
history.pushState = function (...args) {
|
|
1350
|
-
self.originalPushState.apply(history, args);
|
|
1351
|
-
self.resetForNavigation();
|
|
1352
|
-
};
|
|
1353
|
-
history.replaceState = function (...args) {
|
|
1354
|
-
self.originalReplaceState.apply(history, args);
|
|
1355
|
-
self.resetForNavigation();
|
|
1356
|
-
};
|
|
1357
|
-
// Handle back/forward navigation
|
|
1358
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
1359
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
1360
|
-
}
|
|
1361
1431
|
handleClick(e) {
|
|
1362
1432
|
const link = e.target.closest('a');
|
|
1363
1433
|
if (!link || !link.href)
|
|
@@ -1393,6 +1463,9 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1393
1463
|
this.exitIntentShown = false;
|
|
1394
1464
|
this.pageLoadTime = 0;
|
|
1395
1465
|
this.boundHandler = null;
|
|
1466
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1467
|
+
this.navigationHandler = null;
|
|
1468
|
+
this.popstateHandler = null;
|
|
1396
1469
|
}
|
|
1397
1470
|
init(tracker) {
|
|
1398
1471
|
super.init(tracker);
|
|
@@ -1404,13 +1477,34 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1404
1477
|
this.boundHandler = this.handleMouseLeave.bind(this);
|
|
1405
1478
|
document.addEventListener('mouseleave', this.boundHandler);
|
|
1406
1479
|
}
|
|
1480
|
+
if (typeof window !== 'undefined') {
|
|
1481
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1482
|
+
// instead of independently monkey-patching history.pushState
|
|
1483
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1484
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1485
|
+
// Handle back/forward navigation
|
|
1486
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1487
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1488
|
+
}
|
|
1407
1489
|
}
|
|
1408
1490
|
destroy() {
|
|
1409
1491
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1410
1492
|
document.removeEventListener('mouseleave', this.boundHandler);
|
|
1411
1493
|
}
|
|
1494
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1495
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1496
|
+
this.navigationHandler = null;
|
|
1497
|
+
}
|
|
1498
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1499
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1500
|
+
this.popstateHandler = null;
|
|
1501
|
+
}
|
|
1412
1502
|
super.destroy();
|
|
1413
1503
|
}
|
|
1504
|
+
resetForNavigation() {
|
|
1505
|
+
this.exitIntentShown = false;
|
|
1506
|
+
this.pageLoadTime = Date.now();
|
|
1507
|
+
}
|
|
1414
1508
|
handleMouseLeave(e) {
|
|
1415
1509
|
// Only trigger when mouse leaves from the top of the page
|
|
1416
1510
|
if (e.clientY > 0 || this.exitIntentShown)
|
|
@@ -1638,6 +1732,8 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1638
1732
|
this.shownForms = new Set();
|
|
1639
1733
|
this.scrollHandler = null;
|
|
1640
1734
|
this.exitHandler = null;
|
|
1735
|
+
this.delayTimers = [];
|
|
1736
|
+
this.clickTriggerListeners = [];
|
|
1641
1737
|
}
|
|
1642
1738
|
async init(tracker) {
|
|
1643
1739
|
super.init(tracker);
|
|
@@ -1652,6 +1748,14 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1652
1748
|
}
|
|
1653
1749
|
destroy() {
|
|
1654
1750
|
this.removeTriggers();
|
|
1751
|
+
for (const timer of this.delayTimers) {
|
|
1752
|
+
clearTimeout(timer);
|
|
1753
|
+
}
|
|
1754
|
+
this.delayTimers = [];
|
|
1755
|
+
for (const { element, handler } of this.clickTriggerListeners) {
|
|
1756
|
+
element.removeEventListener('click', handler);
|
|
1757
|
+
}
|
|
1758
|
+
this.clickTriggerListeners = [];
|
|
1655
1759
|
super.destroy();
|
|
1656
1760
|
}
|
|
1657
1761
|
loadShownForms() {
|
|
@@ -1682,7 +1786,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1682
1786
|
return;
|
|
1683
1787
|
const config = this.tracker.getConfig();
|
|
1684
1788
|
const workspaceId = this.tracker.getWorkspaceId();
|
|
1685
|
-
const apiEndpoint = config.apiEndpoint || '
|
|
1789
|
+
const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
|
|
1686
1790
|
try {
|
|
1687
1791
|
const url = encodeURIComponent(window.location.href);
|
|
1688
1792
|
const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${workspaceId}?url=${url}`);
|
|
@@ -1714,7 +1818,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1714
1818
|
this.forms.forEach(form => {
|
|
1715
1819
|
switch (form.trigger.type) {
|
|
1716
1820
|
case 'delay':
|
|
1717
|
-
setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
|
|
1821
|
+
this.delayTimers.push(setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000));
|
|
1718
1822
|
break;
|
|
1719
1823
|
case 'scroll':
|
|
1720
1824
|
this.setupScrollTrigger(form);
|
|
@@ -1757,7 +1861,9 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1757
1861
|
return;
|
|
1758
1862
|
const elements = document.querySelectorAll(form.trigger.selector);
|
|
1759
1863
|
elements.forEach(el => {
|
|
1760
|
-
|
|
1864
|
+
const handler = () => this.showForm(form);
|
|
1865
|
+
el.addEventListener('click', handler);
|
|
1866
|
+
this.clickTriggerListeners.push({ element: el, handler });
|
|
1761
1867
|
});
|
|
1762
1868
|
}
|
|
1763
1869
|
removeTriggers() {
|
|
@@ -1785,7 +1891,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1785
1891
|
if (!this.tracker)
|
|
1786
1892
|
return;
|
|
1787
1893
|
const config = this.tracker.getConfig();
|
|
1788
|
-
const apiEndpoint = config.apiEndpoint || '
|
|
1894
|
+
const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
|
|
1789
1895
|
try {
|
|
1790
1896
|
await fetch(`${apiEndpoint}/api/public/lead-forms/${formId}/view`, {
|
|
1791
1897
|
method: 'POST',
|
|
@@ -2032,7 +2138,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2032
2138
|
if (!this.tracker)
|
|
2033
2139
|
return;
|
|
2034
2140
|
const config = this.tracker.getConfig();
|
|
2035
|
-
const apiEndpoint = config.apiEndpoint || '
|
|
2141
|
+
const apiEndpoint = config.apiEndpoint || 'http://localhost:5000';
|
|
2036
2142
|
const visitorId = this.tracker.getVisitorId();
|
|
2037
2143
|
// Collect form data
|
|
2038
2144
|
const formData = new FormData(formElement);
|
|
@@ -2044,7 +2150,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2044
2150
|
const submitBtn = formElement.querySelector('button[type="submit"]');
|
|
2045
2151
|
if (submitBtn) {
|
|
2046
2152
|
submitBtn.disabled = true;
|
|
2047
|
-
submitBtn.
|
|
2153
|
+
submitBtn.textContent = 'Submitting...';
|
|
2048
2154
|
}
|
|
2049
2155
|
try {
|
|
2050
2156
|
const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
|
|
@@ -2085,11 +2191,24 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2085
2191
|
if (data.email) {
|
|
2086
2192
|
this.tracker?.identify(data.email, data);
|
|
2087
2193
|
}
|
|
2088
|
-
// Redirect if configured
|
|
2194
|
+
// Redirect if configured (validate URL to prevent open redirect)
|
|
2089
2195
|
if (form.redirectUrl) {
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2196
|
+
try {
|
|
2197
|
+
const redirect = new URL(form.redirectUrl, window.location.origin);
|
|
2198
|
+
const isSameOrigin = redirect.origin === window.location.origin;
|
|
2199
|
+
const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
|
|
2200
|
+
if (isSameOrigin || isSafeProtocol) {
|
|
2201
|
+
setTimeout(() => {
|
|
2202
|
+
window.location.href = redirect.href;
|
|
2203
|
+
}, 1500);
|
|
2204
|
+
}
|
|
2205
|
+
else {
|
|
2206
|
+
console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
catch {
|
|
2210
|
+
console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
|
|
2211
|
+
}
|
|
2093
2212
|
}
|
|
2094
2213
|
// Close after delay
|
|
2095
2214
|
setTimeout(() => {
|
|
@@ -2104,7 +2223,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2104
2223
|
console.error('[Clianta] Form submit error:', error);
|
|
2105
2224
|
if (submitBtn) {
|
|
2106
2225
|
submitBtn.disabled = false;
|
|
2107
|
-
submitBtn.
|
|
2226
|
+
submitBtn.textContent = form.submitButtonText || 'Subscribe';
|
|
2108
2227
|
}
|
|
2109
2228
|
}
|
|
2110
2229
|
}
|
|
@@ -2914,7 +3033,7 @@ class CRMClient {
|
|
|
2914
3033
|
* The contact is upserted in the CRM and matching workflow automations fire automatically.
|
|
2915
3034
|
*
|
|
2916
3035
|
* @example
|
|
2917
|
-
* const crm = new CRMClient('
|
|
3036
|
+
* const crm = new CRMClient('http://localhost:5000', 'WORKSPACE_ID');
|
|
2918
3037
|
* crm.setApiKey('mm_live_...');
|
|
2919
3038
|
*
|
|
2920
3039
|
* await crm.sendEvent({
|
|
@@ -3435,6 +3554,114 @@ class CRMClient {
|
|
|
3435
3554
|
});
|
|
3436
3555
|
}
|
|
3437
3556
|
// ============================================
|
|
3557
|
+
// READ-BACK / DATA RETRIEVAL API
|
|
3558
|
+
// ============================================
|
|
3559
|
+
/**
|
|
3560
|
+
* Get a contact by email address.
|
|
3561
|
+
* Returns the first matching contact from a search query.
|
|
3562
|
+
*/
|
|
3563
|
+
async getContactByEmail(email) {
|
|
3564
|
+
this.validateRequired('email', email, 'getContactByEmail');
|
|
3565
|
+
const queryParams = new URLSearchParams({ search: email, limit: '1' });
|
|
3566
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
|
|
3567
|
+
}
|
|
3568
|
+
/**
|
|
3569
|
+
* Get activity timeline for a contact
|
|
3570
|
+
*/
|
|
3571
|
+
async getContactActivity(contactId, params) {
|
|
3572
|
+
this.validateRequired('contactId', contactId, 'getContactActivity');
|
|
3573
|
+
const queryParams = new URLSearchParams();
|
|
3574
|
+
if (params?.page)
|
|
3575
|
+
queryParams.set('page', params.page.toString());
|
|
3576
|
+
if (params?.limit)
|
|
3577
|
+
queryParams.set('limit', params.limit.toString());
|
|
3578
|
+
if (params?.type)
|
|
3579
|
+
queryParams.set('type', params.type);
|
|
3580
|
+
if (params?.startDate)
|
|
3581
|
+
queryParams.set('startDate', params.startDate);
|
|
3582
|
+
if (params?.endDate)
|
|
3583
|
+
queryParams.set('endDate', params.endDate);
|
|
3584
|
+
const query = queryParams.toString();
|
|
3585
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
3586
|
+
return this.request(endpoint);
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Get engagement metrics for a contact (via their linked visitor data)
|
|
3590
|
+
*/
|
|
3591
|
+
async getContactEngagement(contactId) {
|
|
3592
|
+
this.validateRequired('contactId', contactId, 'getContactEngagement');
|
|
3593
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Get a full timeline for a contact including events, activities, and opportunities
|
|
3597
|
+
*/
|
|
3598
|
+
async getContactTimeline(contactId, params) {
|
|
3599
|
+
this.validateRequired('contactId', contactId, 'getContactTimeline');
|
|
3600
|
+
const queryParams = new URLSearchParams();
|
|
3601
|
+
if (params?.page)
|
|
3602
|
+
queryParams.set('page', params.page.toString());
|
|
3603
|
+
if (params?.limit)
|
|
3604
|
+
queryParams.set('limit', params.limit.toString());
|
|
3605
|
+
const query = queryParams.toString();
|
|
3606
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
|
|
3607
|
+
return this.request(endpoint);
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* Search contacts with advanced filters
|
|
3611
|
+
*/
|
|
3612
|
+
async searchContacts(query, filters) {
|
|
3613
|
+
const queryParams = new URLSearchParams();
|
|
3614
|
+
queryParams.set('search', query);
|
|
3615
|
+
if (filters?.status)
|
|
3616
|
+
queryParams.set('status', filters.status);
|
|
3617
|
+
if (filters?.lifecycleStage)
|
|
3618
|
+
queryParams.set('lifecycleStage', filters.lifecycleStage);
|
|
3619
|
+
if (filters?.source)
|
|
3620
|
+
queryParams.set('source', filters.source);
|
|
3621
|
+
if (filters?.tags)
|
|
3622
|
+
queryParams.set('tags', filters.tags.join(','));
|
|
3623
|
+
if (filters?.page)
|
|
3624
|
+
queryParams.set('page', filters.page.toString());
|
|
3625
|
+
if (filters?.limit)
|
|
3626
|
+
queryParams.set('limit', filters.limit.toString());
|
|
3627
|
+
const qs = queryParams.toString();
|
|
3628
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
|
|
3629
|
+
return this.request(endpoint);
|
|
3630
|
+
}
|
|
3631
|
+
// ============================================
|
|
3632
|
+
// WEBHOOK MANAGEMENT API
|
|
3633
|
+
// ============================================
|
|
3634
|
+
/**
|
|
3635
|
+
* List all webhook subscriptions
|
|
3636
|
+
*/
|
|
3637
|
+
async listWebhooks(params) {
|
|
3638
|
+
const queryParams = new URLSearchParams();
|
|
3639
|
+
if (params?.page)
|
|
3640
|
+
queryParams.set('page', params.page.toString());
|
|
3641
|
+
if (params?.limit)
|
|
3642
|
+
queryParams.set('limit', params.limit.toString());
|
|
3643
|
+
const query = queryParams.toString();
|
|
3644
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
|
|
3645
|
+
}
|
|
3646
|
+
/**
|
|
3647
|
+
* Create a new webhook subscription
|
|
3648
|
+
*/
|
|
3649
|
+
async createWebhook(data) {
|
|
3650
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
|
|
3651
|
+
method: 'POST',
|
|
3652
|
+
body: JSON.stringify(data),
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
/**
|
|
3656
|
+
* Delete a webhook subscription
|
|
3657
|
+
*/
|
|
3658
|
+
async deleteWebhook(webhookId) {
|
|
3659
|
+
this.validateRequired('webhookId', webhookId, 'deleteWebhook');
|
|
3660
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
|
|
3661
|
+
method: 'DELETE',
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
// ============================================
|
|
3438
3665
|
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3439
3666
|
// ============================================
|
|
3440
3667
|
/**
|
|
@@ -3478,6 +3705,8 @@ class Tracker {
|
|
|
3478
3705
|
this.contactId = null;
|
|
3479
3706
|
/** Pending identify retry on next flush */
|
|
3480
3707
|
this.pendingIdentify = null;
|
|
3708
|
+
/** Registered event schemas for validation */
|
|
3709
|
+
this.eventSchemas = new Map();
|
|
3481
3710
|
if (!workspaceId) {
|
|
3482
3711
|
throw new Error('[Clianta] Workspace ID is required');
|
|
3483
3712
|
}
|
|
@@ -3503,6 +3732,16 @@ class Tracker {
|
|
|
3503
3732
|
this.visitorId = this.createVisitorId();
|
|
3504
3733
|
this.sessionId = this.createSessionId();
|
|
3505
3734
|
logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
|
|
3735
|
+
// Security warnings
|
|
3736
|
+
if (this.config.apiEndpoint.startsWith('http://') &&
|
|
3737
|
+
typeof window !== 'undefined' &&
|
|
3738
|
+
!window.location.hostname.includes('localhost') &&
|
|
3739
|
+
!window.location.hostname.includes('127.0.0.1')) {
|
|
3740
|
+
logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
|
|
3741
|
+
}
|
|
3742
|
+
if (this.config.apiKey && typeof window !== 'undefined') {
|
|
3743
|
+
logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
|
|
3744
|
+
}
|
|
3506
3745
|
// Initialize plugins
|
|
3507
3746
|
this.initPlugins();
|
|
3508
3747
|
this.isInitialized = true;
|
|
@@ -3608,6 +3847,7 @@ class Tracker {
|
|
|
3608
3847
|
properties: {
|
|
3609
3848
|
...properties,
|
|
3610
3849
|
eventId: generateUUID(), // Unique ID for deduplication on retry
|
|
3850
|
+
websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
|
|
3611
3851
|
},
|
|
3612
3852
|
device: getDeviceInfo(),
|
|
3613
3853
|
...getUTMParams(),
|
|
@@ -3618,6 +3858,8 @@ class Tracker {
|
|
|
3618
3858
|
if (this.contactId) {
|
|
3619
3859
|
event.contactId = this.contactId;
|
|
3620
3860
|
}
|
|
3861
|
+
// Validate event against registered schema (debug mode only)
|
|
3862
|
+
this.validateEventSchema(eventType, properties);
|
|
3621
3863
|
// Check consent before tracking
|
|
3622
3864
|
if (!this.consentManager.canTrack()) {
|
|
3623
3865
|
// Buffer event for later if waitForConsent is enabled
|
|
@@ -3652,6 +3894,10 @@ class Tracker {
|
|
|
3652
3894
|
logger.warn('Email is required for identification');
|
|
3653
3895
|
return null;
|
|
3654
3896
|
}
|
|
3897
|
+
if (!isValidEmail(email)) {
|
|
3898
|
+
logger.warn('Invalid email format, identification skipped:', email);
|
|
3899
|
+
return null;
|
|
3900
|
+
}
|
|
3655
3901
|
logger.info('Identifying visitor:', email);
|
|
3656
3902
|
const result = await this.transport.sendIdentify({
|
|
3657
3903
|
workspaceId: this.workspaceId,
|
|
@@ -3686,6 +3932,83 @@ class Tracker {
|
|
|
3686
3932
|
const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
|
|
3687
3933
|
return client.sendEvent(payload);
|
|
3688
3934
|
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Get the current visitor's profile from the CRM.
|
|
3937
|
+
* Returns visitor data and linked contact info if identified.
|
|
3938
|
+
* Only returns data for the current visitor (privacy-safe for frontend).
|
|
3939
|
+
*/
|
|
3940
|
+
async getVisitorProfile() {
|
|
3941
|
+
if (!this.isInitialized) {
|
|
3942
|
+
logger.warn('SDK not initialized');
|
|
3943
|
+
return null;
|
|
3944
|
+
}
|
|
3945
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
|
|
3946
|
+
if (result.success && result.data) {
|
|
3947
|
+
logger.debug('Visitor profile fetched:', result.data);
|
|
3948
|
+
return result.data;
|
|
3949
|
+
}
|
|
3950
|
+
logger.warn('Failed to fetch visitor profile:', result.error);
|
|
3951
|
+
return null;
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Get the current visitor's recent activity/events.
|
|
3955
|
+
* Returns paginated list of tracking events for this visitor.
|
|
3956
|
+
*/
|
|
3957
|
+
async getVisitorActivity(options) {
|
|
3958
|
+
if (!this.isInitialized) {
|
|
3959
|
+
logger.warn('SDK not initialized');
|
|
3960
|
+
return null;
|
|
3961
|
+
}
|
|
3962
|
+
const params = {};
|
|
3963
|
+
if (options?.page)
|
|
3964
|
+
params.page = options.page.toString();
|
|
3965
|
+
if (options?.limit)
|
|
3966
|
+
params.limit = options.limit.toString();
|
|
3967
|
+
if (options?.eventType)
|
|
3968
|
+
params.eventType = options.eventType;
|
|
3969
|
+
if (options?.startDate)
|
|
3970
|
+
params.startDate = options.startDate;
|
|
3971
|
+
if (options?.endDate)
|
|
3972
|
+
params.endDate = options.endDate;
|
|
3973
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
|
|
3974
|
+
if (result.success && result.data) {
|
|
3975
|
+
return result.data;
|
|
3976
|
+
}
|
|
3977
|
+
logger.warn('Failed to fetch visitor activity:', result.error);
|
|
3978
|
+
return null;
|
|
3979
|
+
}
|
|
3980
|
+
/**
|
|
3981
|
+
* Get a summarized journey timeline for the current visitor.
|
|
3982
|
+
* Includes top pages, sessions, time spent, and recent activities.
|
|
3983
|
+
*/
|
|
3984
|
+
async getVisitorTimeline() {
|
|
3985
|
+
if (!this.isInitialized) {
|
|
3986
|
+
logger.warn('SDK not initialized');
|
|
3987
|
+
return null;
|
|
3988
|
+
}
|
|
3989
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
|
|
3990
|
+
if (result.success && result.data) {
|
|
3991
|
+
return result.data;
|
|
3992
|
+
}
|
|
3993
|
+
logger.warn('Failed to fetch visitor timeline:', result.error);
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
/**
|
|
3997
|
+
* Get engagement metrics for the current visitor.
|
|
3998
|
+
* Includes time on site, page views, bounce rate, and engagement score.
|
|
3999
|
+
*/
|
|
4000
|
+
async getVisitorEngagement() {
|
|
4001
|
+
if (!this.isInitialized) {
|
|
4002
|
+
logger.warn('SDK not initialized');
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
|
|
4006
|
+
if (result.success && result.data) {
|
|
4007
|
+
return result.data;
|
|
4008
|
+
}
|
|
4009
|
+
logger.warn('Failed to fetch visitor engagement:', result.error);
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
3689
4012
|
/**
|
|
3690
4013
|
* Retry pending identify call
|
|
3691
4014
|
*/
|
|
@@ -3715,6 +4038,59 @@ class Tracker {
|
|
|
3715
4038
|
logger.enabled = enabled;
|
|
3716
4039
|
logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
3717
4040
|
}
|
|
4041
|
+
/**
|
|
4042
|
+
* Register a schema for event validation.
|
|
4043
|
+
* When debug mode is enabled, events will be validated against registered schemas.
|
|
4044
|
+
*
|
|
4045
|
+
* @example
|
|
4046
|
+
* tracker.registerEventSchema('purchase', {
|
|
4047
|
+
* productId: 'string',
|
|
4048
|
+
* price: 'number',
|
|
4049
|
+
* quantity: 'number',
|
|
4050
|
+
* });
|
|
4051
|
+
*/
|
|
4052
|
+
registerEventSchema(eventType, schema) {
|
|
4053
|
+
this.eventSchemas.set(eventType, schema);
|
|
4054
|
+
logger.debug('Event schema registered:', eventType);
|
|
4055
|
+
}
|
|
4056
|
+
/**
|
|
4057
|
+
* Validate event properties against a registered schema (debug mode only)
|
|
4058
|
+
*/
|
|
4059
|
+
validateEventSchema(eventType, properties) {
|
|
4060
|
+
if (!this.config.debug)
|
|
4061
|
+
return;
|
|
4062
|
+
const schema = this.eventSchemas.get(eventType);
|
|
4063
|
+
if (!schema)
|
|
4064
|
+
return;
|
|
4065
|
+
for (const [key, expectedType] of Object.entries(schema)) {
|
|
4066
|
+
const value = properties[key];
|
|
4067
|
+
if (value === undefined) {
|
|
4068
|
+
logger.warn(`[Schema] Missing property "${key}" for event type "${eventType}"`);
|
|
4069
|
+
continue;
|
|
4070
|
+
}
|
|
4071
|
+
let valid = false;
|
|
4072
|
+
switch (expectedType) {
|
|
4073
|
+
case 'string':
|
|
4074
|
+
valid = typeof value === 'string';
|
|
4075
|
+
break;
|
|
4076
|
+
case 'number':
|
|
4077
|
+
valid = typeof value === 'number';
|
|
4078
|
+
break;
|
|
4079
|
+
case 'boolean':
|
|
4080
|
+
valid = typeof value === 'boolean';
|
|
4081
|
+
break;
|
|
4082
|
+
case 'object':
|
|
4083
|
+
valid = typeof value === 'object' && !Array.isArray(value);
|
|
4084
|
+
break;
|
|
4085
|
+
case 'array':
|
|
4086
|
+
valid = Array.isArray(value);
|
|
4087
|
+
break;
|
|
4088
|
+
}
|
|
4089
|
+
if (!valid) {
|
|
4090
|
+
logger.warn(`[Schema] Property "${key}" for event "${eventType}" expected ${expectedType}, got ${typeof value}`);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
3718
4094
|
/**
|
|
3719
4095
|
* Get visitor ID
|
|
3720
4096
|
*/
|
|
@@ -3754,6 +4130,8 @@ class Tracker {
|
|
|
3754
4130
|
resetIds(this.config.useCookies);
|
|
3755
4131
|
this.visitorId = this.createVisitorId();
|
|
3756
4132
|
this.sessionId = this.createSessionId();
|
|
4133
|
+
this.contactId = null;
|
|
4134
|
+
this.pendingIdentify = null;
|
|
3757
4135
|
this.queue.clear();
|
|
3758
4136
|
}
|
|
3759
4137
|
/**
|
|
@@ -3795,6 +4173,86 @@ class Tracker {
|
|
|
3795
4173
|
this.sessionId = this.createSessionId();
|
|
3796
4174
|
logger.info('All user data deleted');
|
|
3797
4175
|
}
|
|
4176
|
+
// ============================================
|
|
4177
|
+
// PUBLIC CRM METHODS (no API key required)
|
|
4178
|
+
// ============================================
|
|
4179
|
+
/**
|
|
4180
|
+
* Create or update a contact by email (upsert).
|
|
4181
|
+
* Secured by domain whitelist — no API key needed.
|
|
4182
|
+
*/
|
|
4183
|
+
async createContact(data) {
|
|
4184
|
+
return this.publicCrmRequest('/api/public/crm/contacts', 'POST', {
|
|
4185
|
+
workspaceId: this.workspaceId,
|
|
4186
|
+
...data,
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4189
|
+
/**
|
|
4190
|
+
* Update an existing contact by ID (limited fields only).
|
|
4191
|
+
*/
|
|
4192
|
+
async updateContact(contactId, data) {
|
|
4193
|
+
return this.publicCrmRequest(`/api/public/crm/contacts/${contactId}`, 'PUT', {
|
|
4194
|
+
workspaceId: this.workspaceId,
|
|
4195
|
+
...data,
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
4198
|
+
/**
|
|
4199
|
+
* Submit a form — creates/updates contact from form data.
|
|
4200
|
+
*/
|
|
4201
|
+
async submitForm(formId, data) {
|
|
4202
|
+
const payload = {
|
|
4203
|
+
...data,
|
|
4204
|
+
metadata: {
|
|
4205
|
+
...data.metadata,
|
|
4206
|
+
visitorId: this.visitorId,
|
|
4207
|
+
sessionId: this.sessionId,
|
|
4208
|
+
pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
4209
|
+
referrer: typeof document !== 'undefined' ? document.referrer || undefined : undefined,
|
|
4210
|
+
},
|
|
4211
|
+
};
|
|
4212
|
+
return this.publicCrmRequest(`/api/public/crm/forms/${formId}/submit`, 'POST', payload);
|
|
4213
|
+
}
|
|
4214
|
+
/**
|
|
4215
|
+
* Log an activity linked to a contact (append-only).
|
|
4216
|
+
*/
|
|
4217
|
+
async logActivity(data) {
|
|
4218
|
+
return this.publicCrmRequest('/api/public/crm/activities', 'POST', {
|
|
4219
|
+
workspaceId: this.workspaceId,
|
|
4220
|
+
...data,
|
|
4221
|
+
});
|
|
4222
|
+
}
|
|
4223
|
+
/**
|
|
4224
|
+
* Create an opportunity (e.g., from "Request Demo" forms).
|
|
4225
|
+
*/
|
|
4226
|
+
async createOpportunity(data) {
|
|
4227
|
+
return this.publicCrmRequest('/api/public/crm/opportunities', 'POST', {
|
|
4228
|
+
workspaceId: this.workspaceId,
|
|
4229
|
+
...data,
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
4232
|
+
/**
|
|
4233
|
+
* Internal helper for public CRM API calls.
|
|
4234
|
+
*/
|
|
4235
|
+
async publicCrmRequest(path, method, body) {
|
|
4236
|
+
const url = `${this.config.apiEndpoint}${path}`;
|
|
4237
|
+
try {
|
|
4238
|
+
const response = await fetch(url, {
|
|
4239
|
+
method,
|
|
4240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4241
|
+
body: JSON.stringify(body),
|
|
4242
|
+
});
|
|
4243
|
+
const data = await response.json().catch(() => ({}));
|
|
4244
|
+
if (response.ok) {
|
|
4245
|
+
logger.debug(`Public CRM ${method} ${path} succeeded`);
|
|
4246
|
+
return { success: true, data: data.data ?? data, status: response.status };
|
|
4247
|
+
}
|
|
4248
|
+
logger.error(`Public CRM ${method} ${path} failed (${response.status}):`, data.message);
|
|
4249
|
+
return { success: false, error: data.message, status: response.status };
|
|
4250
|
+
}
|
|
4251
|
+
catch (error) {
|
|
4252
|
+
logger.error(`Public CRM ${method} ${path} error:`, error);
|
|
4253
|
+
return { success: false, error: error.message };
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
3798
4256
|
/**
|
|
3799
4257
|
* Destroy tracker and cleanup
|
|
3800
4258
|
*/
|