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