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