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