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