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