@clianta/sdk 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +56 -1
- package/dist/angular.cjs.js +4345 -0
- package/dist/angular.cjs.js.map +1 -0
- package/dist/angular.d.ts +298 -0
- package/dist/angular.esm.js +4341 -0
- package/dist/angular.esm.js.map +1 -0
- package/dist/clianta.cjs.js +459 -88
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +459 -88
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +459 -88
- 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 +223 -6
- package/dist/react.cjs.js +472 -93
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +88 -0
- package/dist/react.esm.js +473 -94
- package/dist/react.esm.js.map +1 -1
- package/dist/svelte.cjs.js +4377 -0
- package/dist/svelte.cjs.js.map +1 -0
- package/dist/svelte.d.ts +308 -0
- package/dist/svelte.esm.js +4374 -0
- package/dist/svelte.esm.js.map +1 -0
- package/dist/vue.cjs.js +459 -88
- package/dist/vue.cjs.js.map +1 -1
- package/dist/vue.d.ts +88 -0
- package/dist/vue.esm.js +459 -88
- package/dist/vue.esm.js.map +1 -1
- package/package.json +21 -2
package/dist/vue.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
2
|
+
* Clianta SDK v1.5.0
|
|
3
3
|
* (c) 2026 Clianta
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -52,6 +52,7 @@ const DEFAULT_CONFIG = {
|
|
|
52
52
|
cookieDomain: '',
|
|
53
53
|
useCookies: false,
|
|
54
54
|
cookielessMode: false,
|
|
55
|
+
persistMode: 'session',
|
|
55
56
|
};
|
|
56
57
|
/** Storage keys */
|
|
57
58
|
const STORAGE_KEYS = {
|
|
@@ -245,6 +246,39 @@ class Transport {
|
|
|
245
246
|
return false;
|
|
246
247
|
}
|
|
247
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Fetch data from the tracking API (GET request)
|
|
251
|
+
* Used for read-back APIs (visitor profile, activity, etc.)
|
|
252
|
+
*/
|
|
253
|
+
async fetchData(path, params) {
|
|
254
|
+
const url = new URL(`${this.config.apiEndpoint}${path}`);
|
|
255
|
+
if (params) {
|
|
256
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
257
|
+
if (value !== undefined && value !== null) {
|
|
258
|
+
url.searchParams.set(key, value);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const response = await this.fetchWithTimeout(url.toString(), {
|
|
264
|
+
method: 'GET',
|
|
265
|
+
headers: {
|
|
266
|
+
'Accept': 'application/json',
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
const body = await response.json();
|
|
271
|
+
logger.debug('Fetch successful:', path);
|
|
272
|
+
return { success: true, data: body.data ?? body, status: response.status };
|
|
273
|
+
}
|
|
274
|
+
logger.error(`Fetch failed with status ${response.status}`);
|
|
275
|
+
return { success: false, status: response.status };
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
logger.error('Fetch request failed:', error);
|
|
279
|
+
return { success: false, error: error };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
248
282
|
/**
|
|
249
283
|
* Internal send with retry logic
|
|
250
284
|
*/
|
|
@@ -409,7 +443,9 @@ function cookie(name, value, days) {
|
|
|
409
443
|
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
410
444
|
expires = '; expires=' + date.toUTCString();
|
|
411
445
|
}
|
|
412
|
-
|
|
446
|
+
// Add Secure flag on HTTPS to prevent cookie leakage over plaintext
|
|
447
|
+
const secure = typeof location !== 'undefined' && location.protocol === 'https:' ? '; Secure' : '';
|
|
448
|
+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax' + secure;
|
|
413
449
|
return value;
|
|
414
450
|
}
|
|
415
451
|
// ============================================
|
|
@@ -573,6 +609,17 @@ function isMobile() {
|
|
|
573
609
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
574
610
|
}
|
|
575
611
|
// ============================================
|
|
612
|
+
// VALIDATION UTILITIES
|
|
613
|
+
// ============================================
|
|
614
|
+
/**
|
|
615
|
+
* Validate email format
|
|
616
|
+
*/
|
|
617
|
+
function isValidEmail(email) {
|
|
618
|
+
if (typeof email !== 'string' || !email)
|
|
619
|
+
return false;
|
|
620
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
621
|
+
}
|
|
622
|
+
// ============================================
|
|
576
623
|
// DEVICE INFO
|
|
577
624
|
// ============================================
|
|
578
625
|
/**
|
|
@@ -626,6 +673,7 @@ class EventQueue {
|
|
|
626
673
|
maxQueueSize: config.maxQueueSize ?? MAX_QUEUE_SIZE,
|
|
627
674
|
storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
|
|
628
675
|
};
|
|
676
|
+
this.persistMode = config.persistMode || 'session';
|
|
629
677
|
// Restore persisted queue
|
|
630
678
|
this.restoreQueue();
|
|
631
679
|
// Start auto-flush timer
|
|
@@ -731,6 +779,13 @@ class EventQueue {
|
|
|
731
779
|
clear() {
|
|
732
780
|
this.queue = [];
|
|
733
781
|
this.persistQueue([]);
|
|
782
|
+
// Also clear localStorage if used
|
|
783
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
784
|
+
try {
|
|
785
|
+
localStorage.removeItem(this.config.storageKey);
|
|
786
|
+
}
|
|
787
|
+
catch { /* ignore */ }
|
|
788
|
+
}
|
|
734
789
|
}
|
|
735
790
|
/**
|
|
736
791
|
* Stop the flush timer and cleanup handlers
|
|
@@ -785,22 +840,44 @@ class EventQueue {
|
|
|
785
840
|
window.addEventListener('pagehide', this.boundPageHide);
|
|
786
841
|
}
|
|
787
842
|
/**
|
|
788
|
-
* Persist queue to
|
|
843
|
+
* Persist queue to storage based on persistMode
|
|
789
844
|
*/
|
|
790
845
|
persistQueue(events) {
|
|
846
|
+
if (this.persistMode === 'none')
|
|
847
|
+
return;
|
|
791
848
|
try {
|
|
792
|
-
|
|
849
|
+
const serialized = JSON.stringify(events);
|
|
850
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
851
|
+
try {
|
|
852
|
+
localStorage.setItem(this.config.storageKey, serialized);
|
|
853
|
+
}
|
|
854
|
+
catch {
|
|
855
|
+
// localStorage quota exceeded — fallback to sessionStorage
|
|
856
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
setSessionStorage(this.config.storageKey, serialized);
|
|
861
|
+
}
|
|
793
862
|
}
|
|
794
863
|
catch {
|
|
795
864
|
// Ignore storage errors
|
|
796
865
|
}
|
|
797
866
|
}
|
|
798
867
|
/**
|
|
799
|
-
* Restore queue from
|
|
868
|
+
* Restore queue from storage
|
|
800
869
|
*/
|
|
801
870
|
restoreQueue() {
|
|
802
871
|
try {
|
|
803
|
-
|
|
872
|
+
let stored = null;
|
|
873
|
+
// Check localStorage first (cross-session persistence)
|
|
874
|
+
if (this.persistMode === 'local' && typeof localStorage !== 'undefined') {
|
|
875
|
+
stored = localStorage.getItem(this.config.storageKey);
|
|
876
|
+
}
|
|
877
|
+
// Fall back to sessionStorage
|
|
878
|
+
if (!stored) {
|
|
879
|
+
stored = getSessionStorage(this.config.storageKey);
|
|
880
|
+
}
|
|
804
881
|
if (stored) {
|
|
805
882
|
const events = JSON.parse(stored);
|
|
806
883
|
if (Array.isArray(events) && events.length > 0) {
|
|
@@ -868,10 +945,13 @@ class PageViewPlugin extends BasePlugin {
|
|
|
868
945
|
history.pushState = function (...args) {
|
|
869
946
|
self.originalPushState.apply(history, args);
|
|
870
947
|
self.trackPageView();
|
|
948
|
+
// Notify other plugins (e.g. ScrollPlugin) about navigation
|
|
949
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
871
950
|
};
|
|
872
951
|
history.replaceState = function (...args) {
|
|
873
952
|
self.originalReplaceState.apply(history, args);
|
|
874
953
|
self.trackPageView();
|
|
954
|
+
window.dispatchEvent(new Event('clianta:navigation'));
|
|
875
955
|
};
|
|
876
956
|
// Handle back/forward navigation
|
|
877
957
|
this.popstateHandler = () => this.trackPageView();
|
|
@@ -925,9 +1005,8 @@ class ScrollPlugin extends BasePlugin {
|
|
|
925
1005
|
this.pageLoadTime = 0;
|
|
926
1006
|
this.scrollTimeout = null;
|
|
927
1007
|
this.boundHandler = null;
|
|
928
|
-
/** SPA navigation
|
|
929
|
-
this.
|
|
930
|
-
this.originalReplaceState = null;
|
|
1008
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1009
|
+
this.navigationHandler = null;
|
|
931
1010
|
this.popstateHandler = null;
|
|
932
1011
|
}
|
|
933
1012
|
init(tracker) {
|
|
@@ -936,8 +1015,13 @@ class ScrollPlugin extends BasePlugin {
|
|
|
936
1015
|
if (typeof window !== 'undefined') {
|
|
937
1016
|
this.boundHandler = this.handleScroll.bind(this);
|
|
938
1017
|
window.addEventListener('scroll', this.boundHandler, { passive: true });
|
|
939
|
-
//
|
|
940
|
-
|
|
1018
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1019
|
+
// instead of independently monkey-patching history.pushState
|
|
1020
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1021
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1022
|
+
// Handle back/forward navigation
|
|
1023
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1024
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
941
1025
|
}
|
|
942
1026
|
}
|
|
943
1027
|
destroy() {
|
|
@@ -947,16 +1031,10 @@ class ScrollPlugin extends BasePlugin {
|
|
|
947
1031
|
if (this.scrollTimeout) {
|
|
948
1032
|
clearTimeout(this.scrollTimeout);
|
|
949
1033
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
this.originalPushState = null;
|
|
1034
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1035
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1036
|
+
this.navigationHandler = null;
|
|
954
1037
|
}
|
|
955
|
-
if (this.originalReplaceState) {
|
|
956
|
-
history.replaceState = this.originalReplaceState;
|
|
957
|
-
this.originalReplaceState = null;
|
|
958
|
-
}
|
|
959
|
-
// Remove popstate listener
|
|
960
1038
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
961
1039
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
962
1040
|
this.popstateHandler = null;
|
|
@@ -971,29 +1049,6 @@ class ScrollPlugin extends BasePlugin {
|
|
|
971
1049
|
this.maxScrollDepth = 0;
|
|
972
1050
|
this.pageLoadTime = Date.now();
|
|
973
1051
|
}
|
|
974
|
-
/**
|
|
975
|
-
* Setup History API interception for SPA navigation
|
|
976
|
-
*/
|
|
977
|
-
setupNavigationReset() {
|
|
978
|
-
if (typeof window === 'undefined')
|
|
979
|
-
return;
|
|
980
|
-
// Store originals for cleanup
|
|
981
|
-
this.originalPushState = history.pushState;
|
|
982
|
-
this.originalReplaceState = history.replaceState;
|
|
983
|
-
// Intercept pushState and replaceState
|
|
984
|
-
const self = this;
|
|
985
|
-
history.pushState = function (...args) {
|
|
986
|
-
self.originalPushState.apply(history, args);
|
|
987
|
-
self.resetForNavigation();
|
|
988
|
-
};
|
|
989
|
-
history.replaceState = function (...args) {
|
|
990
|
-
self.originalReplaceState.apply(history, args);
|
|
991
|
-
self.resetForNavigation();
|
|
992
|
-
};
|
|
993
|
-
// Handle back/forward navigation
|
|
994
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
995
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
996
|
-
}
|
|
997
1052
|
handleScroll() {
|
|
998
1053
|
// Debounce scroll tracking
|
|
999
1054
|
if (this.scrollTimeout) {
|
|
@@ -1193,6 +1248,10 @@ class ClicksPlugin extends BasePlugin {
|
|
|
1193
1248
|
elementId: elementInfo.id,
|
|
1194
1249
|
elementClass: elementInfo.className,
|
|
1195
1250
|
href: target.href || undefined,
|
|
1251
|
+
x: Math.round((e.clientX / window.innerWidth) * 100),
|
|
1252
|
+
y: Math.round((e.clientY / window.innerHeight) * 100),
|
|
1253
|
+
viewportWidth: window.innerWidth,
|
|
1254
|
+
viewportHeight: window.innerHeight,
|
|
1196
1255
|
});
|
|
1197
1256
|
}
|
|
1198
1257
|
}
|
|
@@ -1215,6 +1274,9 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1215
1274
|
this.boundMarkEngaged = null;
|
|
1216
1275
|
this.boundTrackTimeOnPage = null;
|
|
1217
1276
|
this.boundVisibilityHandler = null;
|
|
1277
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1278
|
+
this.navigationHandler = null;
|
|
1279
|
+
this.popstateHandler = null;
|
|
1218
1280
|
}
|
|
1219
1281
|
init(tracker) {
|
|
1220
1282
|
super.init(tracker);
|
|
@@ -1240,6 +1302,13 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1240
1302
|
// Track time on page before unload
|
|
1241
1303
|
window.addEventListener('beforeunload', this.boundTrackTimeOnPage);
|
|
1242
1304
|
document.addEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1305
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1306
|
+
// instead of independently monkey-patching history.pushState
|
|
1307
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1308
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1309
|
+
// Handle back/forward navigation
|
|
1310
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1311
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1243
1312
|
}
|
|
1244
1313
|
destroy() {
|
|
1245
1314
|
if (this.boundMarkEngaged && typeof document !== 'undefined') {
|
|
@@ -1253,11 +1322,28 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1253
1322
|
if (this.boundVisibilityHandler && typeof document !== 'undefined') {
|
|
1254
1323
|
document.removeEventListener('visibilitychange', this.boundVisibilityHandler);
|
|
1255
1324
|
}
|
|
1325
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1326
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1327
|
+
this.navigationHandler = null;
|
|
1328
|
+
}
|
|
1329
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1330
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1331
|
+
this.popstateHandler = null;
|
|
1332
|
+
}
|
|
1256
1333
|
if (this.engagementTimeout) {
|
|
1257
1334
|
clearTimeout(this.engagementTimeout);
|
|
1258
1335
|
}
|
|
1259
1336
|
super.destroy();
|
|
1260
1337
|
}
|
|
1338
|
+
resetForNavigation() {
|
|
1339
|
+
this.pageLoadTime = Date.now();
|
|
1340
|
+
this.engagementStartTime = Date.now();
|
|
1341
|
+
this.isEngaged = false;
|
|
1342
|
+
if (this.engagementTimeout) {
|
|
1343
|
+
clearTimeout(this.engagementTimeout);
|
|
1344
|
+
this.engagementTimeout = null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1261
1347
|
markEngaged() {
|
|
1262
1348
|
if (!this.isEngaged) {
|
|
1263
1349
|
this.isEngaged = true;
|
|
@@ -1297,9 +1383,8 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1297
1383
|
this.name = 'downloads';
|
|
1298
1384
|
this.trackedDownloads = new Set();
|
|
1299
1385
|
this.boundHandler = null;
|
|
1300
|
-
/** SPA navigation
|
|
1301
|
-
this.
|
|
1302
|
-
this.originalReplaceState = null;
|
|
1386
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1387
|
+
this.navigationHandler = null;
|
|
1303
1388
|
this.popstateHandler = null;
|
|
1304
1389
|
}
|
|
1305
1390
|
init(tracker) {
|
|
@@ -1307,24 +1392,25 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1307
1392
|
if (typeof document !== 'undefined') {
|
|
1308
1393
|
this.boundHandler = this.handleClick.bind(this);
|
|
1309
1394
|
document.addEventListener('click', this.boundHandler, true);
|
|
1310
|
-
|
|
1311
|
-
|
|
1395
|
+
}
|
|
1396
|
+
if (typeof window !== 'undefined') {
|
|
1397
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1398
|
+
// instead of independently monkey-patching history.pushState
|
|
1399
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1400
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1401
|
+
// Handle back/forward navigation
|
|
1402
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1403
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1312
1404
|
}
|
|
1313
1405
|
}
|
|
1314
1406
|
destroy() {
|
|
1315
1407
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1316
1408
|
document.removeEventListener('click', this.boundHandler, true);
|
|
1317
1409
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
this.originalPushState = null;
|
|
1322
|
-
}
|
|
1323
|
-
if (this.originalReplaceState) {
|
|
1324
|
-
history.replaceState = this.originalReplaceState;
|
|
1325
|
-
this.originalReplaceState = null;
|
|
1410
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1411
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1412
|
+
this.navigationHandler = null;
|
|
1326
1413
|
}
|
|
1327
|
-
// Remove popstate listener
|
|
1328
1414
|
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1329
1415
|
window.removeEventListener('popstate', this.popstateHandler);
|
|
1330
1416
|
this.popstateHandler = null;
|
|
@@ -1337,29 +1423,6 @@ class DownloadsPlugin extends BasePlugin {
|
|
|
1337
1423
|
resetForNavigation() {
|
|
1338
1424
|
this.trackedDownloads.clear();
|
|
1339
1425
|
}
|
|
1340
|
-
/**
|
|
1341
|
-
* Setup History API interception for SPA navigation
|
|
1342
|
-
*/
|
|
1343
|
-
setupNavigationReset() {
|
|
1344
|
-
if (typeof window === 'undefined')
|
|
1345
|
-
return;
|
|
1346
|
-
// Store originals for cleanup
|
|
1347
|
-
this.originalPushState = history.pushState;
|
|
1348
|
-
this.originalReplaceState = history.replaceState;
|
|
1349
|
-
// Intercept pushState and replaceState
|
|
1350
|
-
const self = this;
|
|
1351
|
-
history.pushState = function (...args) {
|
|
1352
|
-
self.originalPushState.apply(history, args);
|
|
1353
|
-
self.resetForNavigation();
|
|
1354
|
-
};
|
|
1355
|
-
history.replaceState = function (...args) {
|
|
1356
|
-
self.originalReplaceState.apply(history, args);
|
|
1357
|
-
self.resetForNavigation();
|
|
1358
|
-
};
|
|
1359
|
-
// Handle back/forward navigation
|
|
1360
|
-
this.popstateHandler = () => this.resetForNavigation();
|
|
1361
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
1362
|
-
}
|
|
1363
1426
|
handleClick(e) {
|
|
1364
1427
|
const link = e.target.closest('a');
|
|
1365
1428
|
if (!link || !link.href)
|
|
@@ -1395,6 +1458,9 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1395
1458
|
this.exitIntentShown = false;
|
|
1396
1459
|
this.pageLoadTime = 0;
|
|
1397
1460
|
this.boundHandler = null;
|
|
1461
|
+
/** SPA navigation — listen for PageViewPlugin's custom event instead of patching history */
|
|
1462
|
+
this.navigationHandler = null;
|
|
1463
|
+
this.popstateHandler = null;
|
|
1398
1464
|
}
|
|
1399
1465
|
init(tracker) {
|
|
1400
1466
|
super.init(tracker);
|
|
@@ -1406,13 +1472,34 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1406
1472
|
this.boundHandler = this.handleMouseLeave.bind(this);
|
|
1407
1473
|
document.addEventListener('mouseleave', this.boundHandler);
|
|
1408
1474
|
}
|
|
1475
|
+
if (typeof window !== 'undefined') {
|
|
1476
|
+
// Listen for navigation events dispatched by PageViewPlugin
|
|
1477
|
+
// instead of independently monkey-patching history.pushState
|
|
1478
|
+
this.navigationHandler = () => this.resetForNavigation();
|
|
1479
|
+
window.addEventListener('clianta:navigation', this.navigationHandler);
|
|
1480
|
+
// Handle back/forward navigation
|
|
1481
|
+
this.popstateHandler = () => this.resetForNavigation();
|
|
1482
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
1483
|
+
}
|
|
1409
1484
|
}
|
|
1410
1485
|
destroy() {
|
|
1411
1486
|
if (this.boundHandler && typeof document !== 'undefined') {
|
|
1412
1487
|
document.removeEventListener('mouseleave', this.boundHandler);
|
|
1413
1488
|
}
|
|
1489
|
+
if (this.navigationHandler && typeof window !== 'undefined') {
|
|
1490
|
+
window.removeEventListener('clianta:navigation', this.navigationHandler);
|
|
1491
|
+
this.navigationHandler = null;
|
|
1492
|
+
}
|
|
1493
|
+
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
1494
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1495
|
+
this.popstateHandler = null;
|
|
1496
|
+
}
|
|
1414
1497
|
super.destroy();
|
|
1415
1498
|
}
|
|
1499
|
+
resetForNavigation() {
|
|
1500
|
+
this.exitIntentShown = false;
|
|
1501
|
+
this.pageLoadTime = Date.now();
|
|
1502
|
+
}
|
|
1416
1503
|
handleMouseLeave(e) {
|
|
1417
1504
|
// Only trigger when mouse leaves from the top of the page
|
|
1418
1505
|
if (e.clientY > 0 || this.exitIntentShown)
|
|
@@ -1640,6 +1727,8 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1640
1727
|
this.shownForms = new Set();
|
|
1641
1728
|
this.scrollHandler = null;
|
|
1642
1729
|
this.exitHandler = null;
|
|
1730
|
+
this.delayTimers = [];
|
|
1731
|
+
this.clickTriggerListeners = [];
|
|
1643
1732
|
}
|
|
1644
1733
|
async init(tracker) {
|
|
1645
1734
|
super.init(tracker);
|
|
@@ -1654,6 +1743,14 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1654
1743
|
}
|
|
1655
1744
|
destroy() {
|
|
1656
1745
|
this.removeTriggers();
|
|
1746
|
+
for (const timer of this.delayTimers) {
|
|
1747
|
+
clearTimeout(timer);
|
|
1748
|
+
}
|
|
1749
|
+
this.delayTimers = [];
|
|
1750
|
+
for (const { element, handler } of this.clickTriggerListeners) {
|
|
1751
|
+
element.removeEventListener('click', handler);
|
|
1752
|
+
}
|
|
1753
|
+
this.clickTriggerListeners = [];
|
|
1657
1754
|
super.destroy();
|
|
1658
1755
|
}
|
|
1659
1756
|
loadShownForms() {
|
|
@@ -1716,7 +1813,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1716
1813
|
this.forms.forEach(form => {
|
|
1717
1814
|
switch (form.trigger.type) {
|
|
1718
1815
|
case 'delay':
|
|
1719
|
-
setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000);
|
|
1816
|
+
this.delayTimers.push(setTimeout(() => this.showForm(form), (form.trigger.value || 5) * 1000));
|
|
1720
1817
|
break;
|
|
1721
1818
|
case 'scroll':
|
|
1722
1819
|
this.setupScrollTrigger(form);
|
|
@@ -1759,7 +1856,9 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
1759
1856
|
return;
|
|
1760
1857
|
const elements = document.querySelectorAll(form.trigger.selector);
|
|
1761
1858
|
elements.forEach(el => {
|
|
1762
|
-
|
|
1859
|
+
const handler = () => this.showForm(form);
|
|
1860
|
+
el.addEventListener('click', handler);
|
|
1861
|
+
this.clickTriggerListeners.push({ element: el, handler });
|
|
1763
1862
|
});
|
|
1764
1863
|
}
|
|
1765
1864
|
removeTriggers() {
|
|
@@ -2046,7 +2145,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2046
2145
|
const submitBtn = formElement.querySelector('button[type="submit"]');
|
|
2047
2146
|
if (submitBtn) {
|
|
2048
2147
|
submitBtn.disabled = true;
|
|
2049
|
-
submitBtn.
|
|
2148
|
+
submitBtn.textContent = 'Submitting...';
|
|
2050
2149
|
}
|
|
2051
2150
|
try {
|
|
2052
2151
|
const response = await fetch(`${apiEndpoint}/api/public/lead-forms/${form._id}/submit`, {
|
|
@@ -2087,11 +2186,24 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2087
2186
|
if (data.email) {
|
|
2088
2187
|
this.tracker?.identify(data.email, data);
|
|
2089
2188
|
}
|
|
2090
|
-
// Redirect if configured
|
|
2189
|
+
// Redirect if configured (validate URL to prevent open redirect)
|
|
2091
2190
|
if (form.redirectUrl) {
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2191
|
+
try {
|
|
2192
|
+
const redirect = new URL(form.redirectUrl, window.location.origin);
|
|
2193
|
+
const isSameOrigin = redirect.origin === window.location.origin;
|
|
2194
|
+
const isSafeProtocol = redirect.protocol === 'https:' || redirect.protocol === 'http:';
|
|
2195
|
+
if (isSameOrigin || isSafeProtocol) {
|
|
2196
|
+
setTimeout(() => {
|
|
2197
|
+
window.location.href = redirect.href;
|
|
2198
|
+
}, 1500);
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
console.warn('[Clianta] Blocked unsafe redirect URL:', form.redirectUrl);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
catch {
|
|
2205
|
+
console.warn('[Clianta] Invalid redirect URL:', form.redirectUrl);
|
|
2206
|
+
}
|
|
2095
2207
|
}
|
|
2096
2208
|
// Close after delay
|
|
2097
2209
|
setTimeout(() => {
|
|
@@ -2106,7 +2218,7 @@ class PopupFormsPlugin extends BasePlugin {
|
|
|
2106
2218
|
console.error('[Clianta] Form submit error:', error);
|
|
2107
2219
|
if (submitBtn) {
|
|
2108
2220
|
submitBtn.disabled = false;
|
|
2109
|
-
submitBtn.
|
|
2221
|
+
submitBtn.textContent = form.submitButtonText || 'Subscribe';
|
|
2110
2222
|
}
|
|
2111
2223
|
}
|
|
2112
2224
|
}
|
|
@@ -3437,6 +3549,114 @@ class CRMClient {
|
|
|
3437
3549
|
});
|
|
3438
3550
|
}
|
|
3439
3551
|
// ============================================
|
|
3552
|
+
// READ-BACK / DATA RETRIEVAL API
|
|
3553
|
+
// ============================================
|
|
3554
|
+
/**
|
|
3555
|
+
* Get a contact by email address.
|
|
3556
|
+
* Returns the first matching contact from a search query.
|
|
3557
|
+
*/
|
|
3558
|
+
async getContactByEmail(email) {
|
|
3559
|
+
this.validateRequired('email', email, 'getContactByEmail');
|
|
3560
|
+
const queryParams = new URLSearchParams({ search: email, limit: '1' });
|
|
3561
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts?${queryParams.toString()}`);
|
|
3562
|
+
}
|
|
3563
|
+
/**
|
|
3564
|
+
* Get activity timeline for a contact
|
|
3565
|
+
*/
|
|
3566
|
+
async getContactActivity(contactId, params) {
|
|
3567
|
+
this.validateRequired('contactId', contactId, 'getContactActivity');
|
|
3568
|
+
const queryParams = new URLSearchParams();
|
|
3569
|
+
if (params?.page)
|
|
3570
|
+
queryParams.set('page', params.page.toString());
|
|
3571
|
+
if (params?.limit)
|
|
3572
|
+
queryParams.set('limit', params.limit.toString());
|
|
3573
|
+
if (params?.type)
|
|
3574
|
+
queryParams.set('type', params.type);
|
|
3575
|
+
if (params?.startDate)
|
|
3576
|
+
queryParams.set('startDate', params.startDate);
|
|
3577
|
+
if (params?.endDate)
|
|
3578
|
+
queryParams.set('endDate', params.endDate);
|
|
3579
|
+
const query = queryParams.toString();
|
|
3580
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/activities${query ? `?${query}` : ''}`;
|
|
3581
|
+
return this.request(endpoint);
|
|
3582
|
+
}
|
|
3583
|
+
/**
|
|
3584
|
+
* Get engagement metrics for a contact (via their linked visitor data)
|
|
3585
|
+
*/
|
|
3586
|
+
async getContactEngagement(contactId) {
|
|
3587
|
+
this.validateRequired('contactId', contactId, 'getContactEngagement');
|
|
3588
|
+
return this.request(`/api/workspaces/${this.workspaceId}/contacts/${contactId}/engagement`);
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* Get a full timeline for a contact including events, activities, and opportunities
|
|
3592
|
+
*/
|
|
3593
|
+
async getContactTimeline(contactId, params) {
|
|
3594
|
+
this.validateRequired('contactId', contactId, 'getContactTimeline');
|
|
3595
|
+
const queryParams = new URLSearchParams();
|
|
3596
|
+
if (params?.page)
|
|
3597
|
+
queryParams.set('page', params.page.toString());
|
|
3598
|
+
if (params?.limit)
|
|
3599
|
+
queryParams.set('limit', params.limit.toString());
|
|
3600
|
+
const query = queryParams.toString();
|
|
3601
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts/${contactId}/timeline${query ? `?${query}` : ''}`;
|
|
3602
|
+
return this.request(endpoint);
|
|
3603
|
+
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Search contacts with advanced filters
|
|
3606
|
+
*/
|
|
3607
|
+
async searchContacts(query, filters) {
|
|
3608
|
+
const queryParams = new URLSearchParams();
|
|
3609
|
+
queryParams.set('search', query);
|
|
3610
|
+
if (filters?.status)
|
|
3611
|
+
queryParams.set('status', filters.status);
|
|
3612
|
+
if (filters?.lifecycleStage)
|
|
3613
|
+
queryParams.set('lifecycleStage', filters.lifecycleStage);
|
|
3614
|
+
if (filters?.source)
|
|
3615
|
+
queryParams.set('source', filters.source);
|
|
3616
|
+
if (filters?.tags)
|
|
3617
|
+
queryParams.set('tags', filters.tags.join(','));
|
|
3618
|
+
if (filters?.page)
|
|
3619
|
+
queryParams.set('page', filters.page.toString());
|
|
3620
|
+
if (filters?.limit)
|
|
3621
|
+
queryParams.set('limit', filters.limit.toString());
|
|
3622
|
+
const qs = queryParams.toString();
|
|
3623
|
+
const endpoint = `/api/workspaces/${this.workspaceId}/contacts${qs ? `?${qs}` : ''}`;
|
|
3624
|
+
return this.request(endpoint);
|
|
3625
|
+
}
|
|
3626
|
+
// ============================================
|
|
3627
|
+
// WEBHOOK MANAGEMENT API
|
|
3628
|
+
// ============================================
|
|
3629
|
+
/**
|
|
3630
|
+
* List all webhook subscriptions
|
|
3631
|
+
*/
|
|
3632
|
+
async listWebhooks(params) {
|
|
3633
|
+
const queryParams = new URLSearchParams();
|
|
3634
|
+
if (params?.page)
|
|
3635
|
+
queryParams.set('page', params.page.toString());
|
|
3636
|
+
if (params?.limit)
|
|
3637
|
+
queryParams.set('limit', params.limit.toString());
|
|
3638
|
+
const query = queryParams.toString();
|
|
3639
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks${query ? `?${query}` : ''}`);
|
|
3640
|
+
}
|
|
3641
|
+
/**
|
|
3642
|
+
* Create a new webhook subscription
|
|
3643
|
+
*/
|
|
3644
|
+
async createWebhook(data) {
|
|
3645
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks`, {
|
|
3646
|
+
method: 'POST',
|
|
3647
|
+
body: JSON.stringify(data),
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
/**
|
|
3651
|
+
* Delete a webhook subscription
|
|
3652
|
+
*/
|
|
3653
|
+
async deleteWebhook(webhookId) {
|
|
3654
|
+
this.validateRequired('webhookId', webhookId, 'deleteWebhook');
|
|
3655
|
+
return this.request(`/api/workspaces/${this.workspaceId}/webhooks/${webhookId}`, {
|
|
3656
|
+
method: 'DELETE',
|
|
3657
|
+
});
|
|
3658
|
+
}
|
|
3659
|
+
// ============================================
|
|
3440
3660
|
// EVENT TRIGGERS API (delegated to triggers manager)
|
|
3441
3661
|
// ============================================
|
|
3442
3662
|
/**
|
|
@@ -3480,6 +3700,8 @@ class Tracker {
|
|
|
3480
3700
|
this.contactId = null;
|
|
3481
3701
|
/** Pending identify retry on next flush */
|
|
3482
3702
|
this.pendingIdentify = null;
|
|
3703
|
+
/** Registered event schemas for validation */
|
|
3704
|
+
this.eventSchemas = new Map();
|
|
3483
3705
|
if (!workspaceId) {
|
|
3484
3706
|
throw new Error('[Clianta] Workspace ID is required');
|
|
3485
3707
|
}
|
|
@@ -3505,6 +3727,16 @@ class Tracker {
|
|
|
3505
3727
|
this.visitorId = this.createVisitorId();
|
|
3506
3728
|
this.sessionId = this.createSessionId();
|
|
3507
3729
|
logger.debug('IDs created', { visitorId: this.visitorId, sessionId: this.sessionId });
|
|
3730
|
+
// Security warnings
|
|
3731
|
+
if (this.config.apiEndpoint.startsWith('http://') &&
|
|
3732
|
+
typeof window !== 'undefined' &&
|
|
3733
|
+
!window.location.hostname.includes('localhost') &&
|
|
3734
|
+
!window.location.hostname.includes('127.0.0.1')) {
|
|
3735
|
+
logger.warn('apiEndpoint uses HTTP — events and visitor data will be sent unencrypted. Use HTTPS in production.');
|
|
3736
|
+
}
|
|
3737
|
+
if (this.config.apiKey && typeof window !== 'undefined') {
|
|
3738
|
+
logger.warn('API key is exposed in client-side code. Use API keys only in server-side (Node.js) environments.');
|
|
3739
|
+
}
|
|
3508
3740
|
// Initialize plugins
|
|
3509
3741
|
this.initPlugins();
|
|
3510
3742
|
this.isInitialized = true;
|
|
@@ -3610,6 +3842,7 @@ class Tracker {
|
|
|
3610
3842
|
properties: {
|
|
3611
3843
|
...properties,
|
|
3612
3844
|
eventId: generateUUID(), // Unique ID for deduplication on retry
|
|
3845
|
+
websiteDomain: typeof window !== 'undefined' ? window.location.hostname : undefined,
|
|
3613
3846
|
},
|
|
3614
3847
|
device: getDeviceInfo(),
|
|
3615
3848
|
...getUTMParams(),
|
|
@@ -3620,6 +3853,8 @@ class Tracker {
|
|
|
3620
3853
|
if (this.contactId) {
|
|
3621
3854
|
event.contactId = this.contactId;
|
|
3622
3855
|
}
|
|
3856
|
+
// Validate event against registered schema (debug mode only)
|
|
3857
|
+
this.validateEventSchema(eventType, properties);
|
|
3623
3858
|
// Check consent before tracking
|
|
3624
3859
|
if (!this.consentManager.canTrack()) {
|
|
3625
3860
|
// Buffer event for later if waitForConsent is enabled
|
|
@@ -3654,6 +3889,10 @@ class Tracker {
|
|
|
3654
3889
|
logger.warn('Email is required for identification');
|
|
3655
3890
|
return null;
|
|
3656
3891
|
}
|
|
3892
|
+
if (!isValidEmail(email)) {
|
|
3893
|
+
logger.warn('Invalid email format, identification skipped:', email);
|
|
3894
|
+
return null;
|
|
3895
|
+
}
|
|
3657
3896
|
logger.info('Identifying visitor:', email);
|
|
3658
3897
|
const result = await this.transport.sendIdentify({
|
|
3659
3898
|
workspaceId: this.workspaceId,
|
|
@@ -3688,6 +3927,83 @@ class Tracker {
|
|
|
3688
3927
|
const client = new CRMClient(this.config.apiEndpoint, this.workspaceId, undefined, apiKey);
|
|
3689
3928
|
return client.sendEvent(payload);
|
|
3690
3929
|
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Get the current visitor's profile from the CRM.
|
|
3932
|
+
* Returns visitor data and linked contact info if identified.
|
|
3933
|
+
* Only returns data for the current visitor (privacy-safe for frontend).
|
|
3934
|
+
*/
|
|
3935
|
+
async getVisitorProfile() {
|
|
3936
|
+
if (!this.isInitialized) {
|
|
3937
|
+
logger.warn('SDK not initialized');
|
|
3938
|
+
return null;
|
|
3939
|
+
}
|
|
3940
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/profile`);
|
|
3941
|
+
if (result.success && result.data) {
|
|
3942
|
+
logger.debug('Visitor profile fetched:', result.data);
|
|
3943
|
+
return result.data;
|
|
3944
|
+
}
|
|
3945
|
+
logger.warn('Failed to fetch visitor profile:', result.error);
|
|
3946
|
+
return null;
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* Get the current visitor's recent activity/events.
|
|
3950
|
+
* Returns paginated list of tracking events for this visitor.
|
|
3951
|
+
*/
|
|
3952
|
+
async getVisitorActivity(options) {
|
|
3953
|
+
if (!this.isInitialized) {
|
|
3954
|
+
logger.warn('SDK not initialized');
|
|
3955
|
+
return null;
|
|
3956
|
+
}
|
|
3957
|
+
const params = {};
|
|
3958
|
+
if (options?.page)
|
|
3959
|
+
params.page = options.page.toString();
|
|
3960
|
+
if (options?.limit)
|
|
3961
|
+
params.limit = options.limit.toString();
|
|
3962
|
+
if (options?.eventType)
|
|
3963
|
+
params.eventType = options.eventType;
|
|
3964
|
+
if (options?.startDate)
|
|
3965
|
+
params.startDate = options.startDate;
|
|
3966
|
+
if (options?.endDate)
|
|
3967
|
+
params.endDate = options.endDate;
|
|
3968
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/activity`, params);
|
|
3969
|
+
if (result.success && result.data) {
|
|
3970
|
+
return result.data;
|
|
3971
|
+
}
|
|
3972
|
+
logger.warn('Failed to fetch visitor activity:', result.error);
|
|
3973
|
+
return null;
|
|
3974
|
+
}
|
|
3975
|
+
/**
|
|
3976
|
+
* Get a summarized journey timeline for the current visitor.
|
|
3977
|
+
* Includes top pages, sessions, time spent, and recent activities.
|
|
3978
|
+
*/
|
|
3979
|
+
async getVisitorTimeline() {
|
|
3980
|
+
if (!this.isInitialized) {
|
|
3981
|
+
logger.warn('SDK not initialized');
|
|
3982
|
+
return null;
|
|
3983
|
+
}
|
|
3984
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/timeline`);
|
|
3985
|
+
if (result.success && result.data) {
|
|
3986
|
+
return result.data;
|
|
3987
|
+
}
|
|
3988
|
+
logger.warn('Failed to fetch visitor timeline:', result.error);
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
/**
|
|
3992
|
+
* Get engagement metrics for the current visitor.
|
|
3993
|
+
* Includes time on site, page views, bounce rate, and engagement score.
|
|
3994
|
+
*/
|
|
3995
|
+
async getVisitorEngagement() {
|
|
3996
|
+
if (!this.isInitialized) {
|
|
3997
|
+
logger.warn('SDK not initialized');
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
const result = await this.transport.fetchData(`/api/public/track/visitor/${this.workspaceId}/${this.visitorId}/engagement`);
|
|
4001
|
+
if (result.success && result.data) {
|
|
4002
|
+
return result.data;
|
|
4003
|
+
}
|
|
4004
|
+
logger.warn('Failed to fetch visitor engagement:', result.error);
|
|
4005
|
+
return null;
|
|
4006
|
+
}
|
|
3691
4007
|
/**
|
|
3692
4008
|
* Retry pending identify call
|
|
3693
4009
|
*/
|
|
@@ -3717,6 +4033,59 @@ class Tracker {
|
|
|
3717
4033
|
logger.enabled = enabled;
|
|
3718
4034
|
logger.info(`Debug mode ${enabled ? 'enabled' : 'disabled'}`);
|
|
3719
4035
|
}
|
|
4036
|
+
/**
|
|
4037
|
+
* Register a schema for event validation.
|
|
4038
|
+
* When debug mode is enabled, events will be validated against registered schemas.
|
|
4039
|
+
*
|
|
4040
|
+
* @example
|
|
4041
|
+
* tracker.registerEventSchema('purchase', {
|
|
4042
|
+
* productId: 'string',
|
|
4043
|
+
* price: 'number',
|
|
4044
|
+
* quantity: 'number',
|
|
4045
|
+
* });
|
|
4046
|
+
*/
|
|
4047
|
+
registerEventSchema(eventType, schema) {
|
|
4048
|
+
this.eventSchemas.set(eventType, schema);
|
|
4049
|
+
logger.debug('Event schema registered:', eventType);
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Validate event properties against a registered schema (debug mode only)
|
|
4053
|
+
*/
|
|
4054
|
+
validateEventSchema(eventType, properties) {
|
|
4055
|
+
if (!this.config.debug)
|
|
4056
|
+
return;
|
|
4057
|
+
const schema = this.eventSchemas.get(eventType);
|
|
4058
|
+
if (!schema)
|
|
4059
|
+
return;
|
|
4060
|
+
for (const [key, expectedType] of Object.entries(schema)) {
|
|
4061
|
+
const value = properties[key];
|
|
4062
|
+
if (value === undefined) {
|
|
4063
|
+
logger.warn(`[Schema] Missing property "${key}" for event type "${eventType}"`);
|
|
4064
|
+
continue;
|
|
4065
|
+
}
|
|
4066
|
+
let valid = false;
|
|
4067
|
+
switch (expectedType) {
|
|
4068
|
+
case 'string':
|
|
4069
|
+
valid = typeof value === 'string';
|
|
4070
|
+
break;
|
|
4071
|
+
case 'number':
|
|
4072
|
+
valid = typeof value === 'number';
|
|
4073
|
+
break;
|
|
4074
|
+
case 'boolean':
|
|
4075
|
+
valid = typeof value === 'boolean';
|
|
4076
|
+
break;
|
|
4077
|
+
case 'object':
|
|
4078
|
+
valid = typeof value === 'object' && !Array.isArray(value);
|
|
4079
|
+
break;
|
|
4080
|
+
case 'array':
|
|
4081
|
+
valid = Array.isArray(value);
|
|
4082
|
+
break;
|
|
4083
|
+
}
|
|
4084
|
+
if (!valid) {
|
|
4085
|
+
logger.warn(`[Schema] Property "${key}" for event "${eventType}" expected ${expectedType}, got ${typeof value}`);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
3720
4089
|
/**
|
|
3721
4090
|
* Get visitor ID
|
|
3722
4091
|
*/
|
|
@@ -3756,6 +4125,8 @@ class Tracker {
|
|
|
3756
4125
|
resetIds(this.config.useCookies);
|
|
3757
4126
|
this.visitorId = this.createVisitorId();
|
|
3758
4127
|
this.sessionId = this.createSessionId();
|
|
4128
|
+
this.contactId = null;
|
|
4129
|
+
this.pendingIdentify = null;
|
|
3759
4130
|
this.queue.clear();
|
|
3760
4131
|
}
|
|
3761
4132
|
/**
|