@clianta/sdk 1.6.8 → 1.7.1
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/dist/angular.cjs.js +211 -86
- package/dist/angular.cjs.js.map +1 -1
- package/dist/angular.d.ts +1 -1
- package/dist/angular.esm.js +211 -86
- package/dist/angular.esm.js.map +1 -1
- package/dist/clianta.cjs.js +211 -86
- package/dist/clianta.cjs.js.map +1 -1
- package/dist/clianta.esm.js +211 -86
- package/dist/clianta.esm.js.map +1 -1
- package/dist/clianta.umd.js +211 -86
- 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 +2 -2
- package/dist/react.cjs.js +218 -93
- package/dist/react.cjs.js.map +1 -1
- package/dist/react.d.ts +2 -1
- package/dist/react.esm.js +219 -94
- package/dist/react.esm.js.map +1 -1
- package/dist/svelte.cjs.js +211 -86
- package/dist/svelte.cjs.js.map +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.esm.js +211 -86
- package/dist/svelte.esm.js.map +1 -1
- package/dist/vue.cjs.js +211 -86
- package/dist/vue.cjs.js.map +1 -1
- package/dist/vue.d.ts +1 -1
- package/dist/vue.esm.js +211 -86
- package/dist/vue.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/vue.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Clianta SDK v1.
|
|
2
|
+
* Clianta SDK v1.7.1
|
|
3
3
|
* (c) 2026 Clianta
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -10,7 +10,7 @@ import { ref, inject } from 'vue';
|
|
|
10
10
|
* @see SDK_VERSION in core/config.ts
|
|
11
11
|
*/
|
|
12
12
|
/** SDK Version */
|
|
13
|
-
const SDK_VERSION = '1.
|
|
13
|
+
const SDK_VERSION = '1.7.0';
|
|
14
14
|
/** Default API endpoint — reads from env or falls back to localhost */
|
|
15
15
|
const getDefaultApiEndpoint = () => {
|
|
16
16
|
// Next.js (process.env)
|
|
@@ -36,6 +36,15 @@ const getDefaultApiEndpoint = () => {
|
|
|
36
36
|
if (typeof process !== 'undefined' && process.env?.CLIANTA_API_ENDPOINT) {
|
|
37
37
|
return process.env.CLIANTA_API_ENDPOINT;
|
|
38
38
|
}
|
|
39
|
+
// No env var found — warn if we're not on localhost (likely a production misconfiguration)
|
|
40
|
+
const isLocalhost = typeof window !== 'undefined' &&
|
|
41
|
+
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
|
42
|
+
if (!isLocalhost && typeof console !== 'undefined') {
|
|
43
|
+
console.warn('[Clianta] No API endpoint configured. ' +
|
|
44
|
+
'Set NEXT_PUBLIC_CLIANTA_API_ENDPOINT (Next.js), VITE_CLIANTA_API_ENDPOINT (Vite), ' +
|
|
45
|
+
'or pass apiEndpoint directly to clianta(). ' +
|
|
46
|
+
'Falling back to localhost — tracking will not work in production.');
|
|
47
|
+
}
|
|
39
48
|
return 'http://localhost:5000';
|
|
40
49
|
};
|
|
41
50
|
/** Core plugins enabled by default — all auto-track with zero config */
|
|
@@ -181,7 +190,9 @@ const logger = createLogger(false);
|
|
|
181
190
|
*/
|
|
182
191
|
const DEFAULT_TIMEOUT = 10000; // 10 seconds
|
|
183
192
|
const DEFAULT_MAX_RETRIES = 3;
|
|
184
|
-
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
|
193
|
+
const DEFAULT_RETRY_DELAY = 1000; // 1 second base — doubles each attempt (exponential backoff)
|
|
194
|
+
/** fetch keepalive hard limit in browsers (64KB) */
|
|
195
|
+
const KEEPALIVE_SIZE_LIMIT = 60000; // leave 4KB margin
|
|
185
196
|
/**
|
|
186
197
|
* Transport class for sending data to the backend
|
|
187
198
|
*/
|
|
@@ -200,6 +211,11 @@ class Transport {
|
|
|
200
211
|
async sendEvents(events) {
|
|
201
212
|
const url = `${this.config.apiEndpoint}/api/public/track/event`;
|
|
202
213
|
const payload = JSON.stringify({ events });
|
|
214
|
+
// keepalive has a 64KB hard limit — fall back to beacon if too large
|
|
215
|
+
if (payload.length > KEEPALIVE_SIZE_LIMIT) {
|
|
216
|
+
const sent = this.sendBeacon(events);
|
|
217
|
+
return sent ? { success: true } : this.send(url, payload, 1, false);
|
|
218
|
+
}
|
|
203
219
|
return this.send(url, payload);
|
|
204
220
|
}
|
|
205
221
|
/**
|
|
@@ -264,6 +280,15 @@ class Transport {
|
|
|
264
280
|
return false;
|
|
265
281
|
}
|
|
266
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Send an arbitrary POST request through the transport (with timeout + retry).
|
|
285
|
+
* Used for one-off calls like alias() that don't fit the event-batch or identify shapes.
|
|
286
|
+
*/
|
|
287
|
+
async sendPost(path, body) {
|
|
288
|
+
const url = `${this.config.apiEndpoint}${path}`;
|
|
289
|
+
const payload = JSON.stringify(body);
|
|
290
|
+
return this.send(url, payload);
|
|
291
|
+
}
|
|
267
292
|
/**
|
|
268
293
|
* Fetch data from the tracking API (GET request)
|
|
269
294
|
* Used for read-back APIs (visitor profile, activity, etc.)
|
|
@@ -298,38 +323,44 @@ class Transport {
|
|
|
298
323
|
}
|
|
299
324
|
}
|
|
300
325
|
/**
|
|
301
|
-
* Internal send with retry logic
|
|
326
|
+
* Internal send with exponential backoff retry logic
|
|
302
327
|
*/
|
|
303
|
-
async send(url, payload, attempt = 1) {
|
|
328
|
+
async send(url, payload, attempt = 1, useKeepalive = true) {
|
|
329
|
+
// Don't bother sending when offline — caller should re-queue
|
|
330
|
+
if (typeof navigator !== 'undefined' && !navigator.onLine) {
|
|
331
|
+
logger.warn('Device offline, skipping send');
|
|
332
|
+
return { success: false, error: new Error('offline') };
|
|
333
|
+
}
|
|
304
334
|
try {
|
|
305
335
|
const response = await this.fetchWithTimeout(url, {
|
|
306
336
|
method: 'POST',
|
|
307
|
-
headers: {
|
|
308
|
-
'Content-Type': 'application/json',
|
|
309
|
-
},
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
310
338
|
body: payload,
|
|
311
|
-
keepalive:
|
|
339
|
+
keepalive: useKeepalive,
|
|
312
340
|
});
|
|
313
341
|
if (response.ok) {
|
|
314
342
|
logger.debug('Request successful:', url);
|
|
315
343
|
return { success: true, status: response.status };
|
|
316
344
|
}
|
|
317
|
-
// Server error
|
|
345
|
+
// Server error — retry with exponential backoff
|
|
318
346
|
if (response.status >= 500 && attempt < this.config.maxRetries) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
347
|
+
const backoff = this.config.retryDelay * Math.pow(2, attempt - 1);
|
|
348
|
+
logger.warn(`Server error (${response.status}), retrying in ${backoff}ms...`);
|
|
349
|
+
await this.delay(backoff);
|
|
350
|
+
return this.send(url, payload, attempt + 1, useKeepalive);
|
|
322
351
|
}
|
|
323
|
-
//
|
|
352
|
+
// 4xx — don't retry (bad payload, auth failure, etc.)
|
|
324
353
|
logger.error(`Request failed with status ${response.status}`);
|
|
325
354
|
return { success: false, status: response.status };
|
|
326
355
|
}
|
|
327
356
|
catch (error) {
|
|
328
|
-
// Network error
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
357
|
+
// Network error — retry with exponential backoff if still online
|
|
358
|
+
const isOnline = typeof navigator === 'undefined' || navigator.onLine;
|
|
359
|
+
if (isOnline && attempt < this.config.maxRetries) {
|
|
360
|
+
const backoff = this.config.retryDelay * Math.pow(2, attempt - 1);
|
|
361
|
+
logger.warn(`Network error, retrying in ${backoff}ms (${attempt}/${this.config.maxRetries})...`);
|
|
362
|
+
await this.delay(backoff);
|
|
363
|
+
return this.send(url, payload, attempt + 1, useKeepalive);
|
|
333
364
|
}
|
|
334
365
|
logger.error('Request failed after retries:', error);
|
|
335
366
|
return { success: false, error: error };
|
|
@@ -678,12 +709,17 @@ class EventQueue {
|
|
|
678
709
|
this.queue = [];
|
|
679
710
|
this.flushTimer = null;
|
|
680
711
|
this.isFlushing = false;
|
|
712
|
+
this.isOnline = true;
|
|
681
713
|
/** Rate limiting: timestamps of recent events */
|
|
682
714
|
this.eventTimestamps = [];
|
|
683
715
|
/** Unload handler references for cleanup */
|
|
684
716
|
this.boundBeforeUnload = null;
|
|
685
717
|
this.boundVisibilityChange = null;
|
|
686
718
|
this.boundPageHide = null;
|
|
719
|
+
this.boundOnline = null;
|
|
720
|
+
this.boundOffline = null;
|
|
721
|
+
/** Guards against double-flush on unload (beforeunload + pagehide + visibilitychange all fire) */
|
|
722
|
+
this.unloadFlushed = false;
|
|
687
723
|
this.transport = transport;
|
|
688
724
|
this.config = {
|
|
689
725
|
batchSize: config.batchSize ?? 10,
|
|
@@ -692,6 +728,7 @@ class EventQueue {
|
|
|
692
728
|
storageKey: config.storageKey ?? STORAGE_KEYS.EVENT_QUEUE,
|
|
693
729
|
};
|
|
694
730
|
this.persistMode = config.persistMode || 'session';
|
|
731
|
+
this.isOnline = typeof navigator === 'undefined' || navigator.onLine;
|
|
695
732
|
// Restore persisted queue
|
|
696
733
|
this.restoreQueue();
|
|
697
734
|
// Start auto-flush timer
|
|
@@ -740,7 +777,7 @@ class EventQueue {
|
|
|
740
777
|
* Flush the queue (send all events)
|
|
741
778
|
*/
|
|
742
779
|
async flush() {
|
|
743
|
-
if (this.isFlushing || this.queue.length === 0) {
|
|
780
|
+
if (this.isFlushing || this.queue.length === 0 || !this.isOnline) {
|
|
744
781
|
return;
|
|
745
782
|
}
|
|
746
783
|
this.isFlushing = true;
|
|
@@ -771,11 +808,14 @@ class EventQueue {
|
|
|
771
808
|
}
|
|
772
809
|
}
|
|
773
810
|
/**
|
|
774
|
-
* Flush synchronously using sendBeacon (for page unload)
|
|
811
|
+
* Flush synchronously using sendBeacon (for page unload).
|
|
812
|
+
* Guarded: no-ops after the first call per navigation to prevent
|
|
813
|
+
* triple-flush from beforeunload + visibilitychange + pagehide.
|
|
775
814
|
*/
|
|
776
815
|
flushSync() {
|
|
777
|
-
if (this.queue.length === 0)
|
|
816
|
+
if (this.unloadFlushed || this.queue.length === 0)
|
|
778
817
|
return;
|
|
818
|
+
this.unloadFlushed = true;
|
|
779
819
|
const events = this.queue.splice(0, this.queue.length);
|
|
780
820
|
logger.debug(`Sync flushing ${events.length} events via beacon`);
|
|
781
821
|
const success = this.transport.sendBeacon(events);
|
|
@@ -813,17 +853,17 @@ class EventQueue {
|
|
|
813
853
|
clearInterval(this.flushTimer);
|
|
814
854
|
this.flushTimer = null;
|
|
815
855
|
}
|
|
816
|
-
// Remove unload handlers
|
|
817
856
|
if (typeof window !== 'undefined') {
|
|
818
|
-
if (this.boundBeforeUnload)
|
|
857
|
+
if (this.boundBeforeUnload)
|
|
819
858
|
window.removeEventListener('beforeunload', this.boundBeforeUnload);
|
|
820
|
-
|
|
821
|
-
if (this.boundVisibilityChange) {
|
|
859
|
+
if (this.boundVisibilityChange)
|
|
822
860
|
window.removeEventListener('visibilitychange', this.boundVisibilityChange);
|
|
823
|
-
|
|
824
|
-
if (this.boundPageHide) {
|
|
861
|
+
if (this.boundPageHide)
|
|
825
862
|
window.removeEventListener('pagehide', this.boundPageHide);
|
|
826
|
-
|
|
863
|
+
if (this.boundOnline)
|
|
864
|
+
window.removeEventListener('online', this.boundOnline);
|
|
865
|
+
if (this.boundOffline)
|
|
866
|
+
window.removeEventListener('offline', this.boundOffline);
|
|
827
867
|
}
|
|
828
868
|
}
|
|
829
869
|
/**
|
|
@@ -838,24 +878,38 @@ class EventQueue {
|
|
|
838
878
|
}, this.config.flushInterval);
|
|
839
879
|
}
|
|
840
880
|
/**
|
|
841
|
-
* Setup page unload handlers
|
|
881
|
+
* Setup page unload handlers and online/offline listeners
|
|
842
882
|
*/
|
|
843
883
|
setupUnloadHandlers() {
|
|
844
884
|
if (typeof window === 'undefined')
|
|
845
885
|
return;
|
|
846
|
-
//
|
|
886
|
+
// All three unload events share the same guarded flushSync()
|
|
847
887
|
this.boundBeforeUnload = () => this.flushSync();
|
|
848
888
|
window.addEventListener('beforeunload', this.boundBeforeUnload);
|
|
849
|
-
// Flush when page becomes hidden
|
|
850
889
|
this.boundVisibilityChange = () => {
|
|
851
890
|
if (document.visibilityState === 'hidden') {
|
|
852
891
|
this.flushSync();
|
|
853
892
|
}
|
|
893
|
+
else {
|
|
894
|
+
// Page became visible again (e.g. tab switch back) — reset guard
|
|
895
|
+
this.unloadFlushed = false;
|
|
896
|
+
}
|
|
854
897
|
};
|
|
855
898
|
window.addEventListener('visibilitychange', this.boundVisibilityChange);
|
|
856
|
-
// Flush on page hide (iOS Safari)
|
|
857
899
|
this.boundPageHide = () => this.flushSync();
|
|
858
900
|
window.addEventListener('pagehide', this.boundPageHide);
|
|
901
|
+
// Pause queue when offline, resume + flush when back online
|
|
902
|
+
this.boundOnline = () => {
|
|
903
|
+
logger.info('Connection restored — flushing queued events');
|
|
904
|
+
this.isOnline = true;
|
|
905
|
+
this.flush();
|
|
906
|
+
};
|
|
907
|
+
this.boundOffline = () => {
|
|
908
|
+
logger.warn('Connection lost — pausing event queue');
|
|
909
|
+
this.isOnline = false;
|
|
910
|
+
};
|
|
911
|
+
window.addEventListener('online', this.boundOnline);
|
|
912
|
+
window.addEventListener('offline', this.boundOffline);
|
|
859
913
|
}
|
|
860
914
|
/**
|
|
861
915
|
* Persist queue to storage based on persistMode
|
|
@@ -938,6 +992,8 @@ class BasePlugin {
|
|
|
938
992
|
* Clianta SDK - Page View Plugin
|
|
939
993
|
* @see SDK_VERSION in core/config.ts
|
|
940
994
|
*/
|
|
995
|
+
/** Sentinel flag to prevent double-wrapping history methods across multiple SDK instances */
|
|
996
|
+
const WRAPPED_FLAG = '__clianta_pv_wrapped__';
|
|
941
997
|
/**
|
|
942
998
|
* Page View Plugin - Tracks page views
|
|
943
999
|
*/
|
|
@@ -947,50 +1003,64 @@ class PageViewPlugin extends BasePlugin {
|
|
|
947
1003
|
this.name = 'pageView';
|
|
948
1004
|
this.originalPushState = null;
|
|
949
1005
|
this.originalReplaceState = null;
|
|
1006
|
+
this.navHandler = null;
|
|
950
1007
|
this.popstateHandler = null;
|
|
951
1008
|
}
|
|
952
1009
|
init(tracker) {
|
|
953
1010
|
super.init(tracker);
|
|
954
1011
|
// Track initial page view
|
|
955
1012
|
this.trackPageView();
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1013
|
+
if (typeof window === 'undefined')
|
|
1014
|
+
return;
|
|
1015
|
+
// Only wrap history methods once — guard against multiple SDK instances (e.g. microfrontends)
|
|
1016
|
+
// wrapping them repeatedly, which would cause duplicate navigation events and broken cleanup.
|
|
1017
|
+
if (!history.pushState[WRAPPED_FLAG]) {
|
|
959
1018
|
this.originalPushState = history.pushState;
|
|
960
1019
|
this.originalReplaceState = history.replaceState;
|
|
961
|
-
|
|
962
|
-
const
|
|
1020
|
+
const originalPush = this.originalPushState;
|
|
1021
|
+
const originalReplace = this.originalReplaceState;
|
|
963
1022
|
history.pushState = function (...args) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
// Notify other plugins (e.g. ScrollPlugin) about navigation
|
|
1023
|
+
originalPush.apply(history, args);
|
|
1024
|
+
// Dispatch event so all listening instances track the navigation
|
|
967
1025
|
window.dispatchEvent(new Event('clianta:navigation'));
|
|
968
1026
|
};
|
|
1027
|
+
history.pushState[WRAPPED_FLAG] = true;
|
|
969
1028
|
history.replaceState = function (...args) {
|
|
970
|
-
|
|
971
|
-
self.trackPageView();
|
|
1029
|
+
originalReplace.apply(history, args);
|
|
972
1030
|
window.dispatchEvent(new Event('clianta:navigation'));
|
|
973
1031
|
};
|
|
974
|
-
|
|
975
|
-
this.popstateHandler = () => this.trackPageView();
|
|
976
|
-
window.addEventListener('popstate', this.popstateHandler);
|
|
1032
|
+
history.replaceState[WRAPPED_FLAG] = true;
|
|
977
1033
|
}
|
|
1034
|
+
// Each instance listens to the shared navigation event rather than embedding
|
|
1035
|
+
// tracking directly in the pushState wrapper — decouples tracking from wrapping.
|
|
1036
|
+
this.navHandler = () => this.trackPageView();
|
|
1037
|
+
window.addEventListener('clianta:navigation', this.navHandler);
|
|
1038
|
+
// Handle back/forward navigation
|
|
1039
|
+
this.popstateHandler = () => this.trackPageView();
|
|
1040
|
+
window.addEventListener('popstate', this.popstateHandler);
|
|
978
1041
|
}
|
|
979
1042
|
destroy() {
|
|
980
|
-
|
|
1043
|
+
if (typeof window !== 'undefined') {
|
|
1044
|
+
if (this.navHandler) {
|
|
1045
|
+
window.removeEventListener('clianta:navigation', this.navHandler);
|
|
1046
|
+
this.navHandler = null;
|
|
1047
|
+
}
|
|
1048
|
+
if (this.popstateHandler) {
|
|
1049
|
+
window.removeEventListener('popstate', this.popstateHandler);
|
|
1050
|
+
this.popstateHandler = null;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// Restore original history methods only if this instance was the one that wrapped them
|
|
981
1054
|
if (this.originalPushState) {
|
|
982
1055
|
history.pushState = this.originalPushState;
|
|
1056
|
+
delete history.pushState[WRAPPED_FLAG];
|
|
983
1057
|
this.originalPushState = null;
|
|
984
1058
|
}
|
|
985
1059
|
if (this.originalReplaceState) {
|
|
986
1060
|
history.replaceState = this.originalReplaceState;
|
|
1061
|
+
delete history.replaceState[WRAPPED_FLAG];
|
|
987
1062
|
this.originalReplaceState = null;
|
|
988
1063
|
}
|
|
989
|
-
// Remove popstate listener
|
|
990
|
-
if (this.popstateHandler && typeof window !== 'undefined') {
|
|
991
|
-
window.removeEventListener('popstate', this.popstateHandler);
|
|
992
|
-
this.popstateHandler = null;
|
|
993
|
-
}
|
|
994
1064
|
super.destroy();
|
|
995
1065
|
}
|
|
996
1066
|
trackPageView() {
|
|
@@ -1119,6 +1189,7 @@ class FormsPlugin extends BasePlugin {
|
|
|
1119
1189
|
this.trackedForms = new WeakSet();
|
|
1120
1190
|
this.formInteractions = new Set();
|
|
1121
1191
|
this.observer = null;
|
|
1192
|
+
this.observerTimer = null;
|
|
1122
1193
|
this.listeners = [];
|
|
1123
1194
|
}
|
|
1124
1195
|
init(tracker) {
|
|
@@ -1127,13 +1198,21 @@ class FormsPlugin extends BasePlugin {
|
|
|
1127
1198
|
return;
|
|
1128
1199
|
// Track existing forms
|
|
1129
1200
|
this.trackAllForms();
|
|
1130
|
-
// Watch for dynamically added forms
|
|
1201
|
+
// Watch for dynamically added forms — debounced to avoid O(DOM) cost on every mutation
|
|
1131
1202
|
if (typeof MutationObserver !== 'undefined') {
|
|
1132
|
-
this.observer = new MutationObserver(() =>
|
|
1203
|
+
this.observer = new MutationObserver(() => {
|
|
1204
|
+
if (this.observerTimer)
|
|
1205
|
+
clearTimeout(this.observerTimer);
|
|
1206
|
+
this.observerTimer = setTimeout(() => this.trackAllForms(), 100);
|
|
1207
|
+
});
|
|
1133
1208
|
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
1134
1209
|
}
|
|
1135
1210
|
}
|
|
1136
1211
|
destroy() {
|
|
1212
|
+
if (this.observerTimer) {
|
|
1213
|
+
clearTimeout(this.observerTimer);
|
|
1214
|
+
this.observerTimer = null;
|
|
1215
|
+
}
|
|
1137
1216
|
if (this.observer) {
|
|
1138
1217
|
this.observer.disconnect();
|
|
1139
1218
|
this.observer = null;
|
|
@@ -1255,8 +1334,13 @@ class ClicksPlugin extends BasePlugin {
|
|
|
1255
1334
|
super.destroy();
|
|
1256
1335
|
}
|
|
1257
1336
|
handleClick(e) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1337
|
+
// Walk up the DOM to find the nearest trackable ancestor.
|
|
1338
|
+
// Without this, clicks on <span> or <img> inside a <button> are silently dropped.
|
|
1339
|
+
let target = e.target;
|
|
1340
|
+
while (target && !isTrackableClickElement(target)) {
|
|
1341
|
+
target = target.parentElement;
|
|
1342
|
+
}
|
|
1343
|
+
if (!target)
|
|
1260
1344
|
return;
|
|
1261
1345
|
const buttonText = getElementText(target, 100);
|
|
1262
1346
|
const elementInfo = getElementInfo(target);
|
|
@@ -1289,6 +1373,8 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1289
1373
|
this.engagementStartTime = 0;
|
|
1290
1374
|
this.isEngaged = false;
|
|
1291
1375
|
this.engagementTimeout = null;
|
|
1376
|
+
/** Guard: beforeunload + visibilitychange:hidden both fire on tab close — only report once */
|
|
1377
|
+
this.unloadReported = false;
|
|
1292
1378
|
this.boundMarkEngaged = null;
|
|
1293
1379
|
this.boundTrackTimeOnPage = null;
|
|
1294
1380
|
this.boundVisibilityHandler = null;
|
|
@@ -1310,8 +1396,9 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1310
1396
|
this.trackTimeOnPage();
|
|
1311
1397
|
}
|
|
1312
1398
|
else {
|
|
1313
|
-
//
|
|
1399
|
+
// Page is visible again — reset both the time counter and the unload guard
|
|
1314
1400
|
this.engagementStartTime = Date.now();
|
|
1401
|
+
this.unloadReported = false;
|
|
1315
1402
|
}
|
|
1316
1403
|
};
|
|
1317
1404
|
['mousemove', 'keydown', 'touchstart', 'scroll'].forEach((event) => {
|
|
@@ -1357,6 +1444,7 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1357
1444
|
this.pageLoadTime = Date.now();
|
|
1358
1445
|
this.engagementStartTime = Date.now();
|
|
1359
1446
|
this.isEngaged = false;
|
|
1447
|
+
this.unloadReported = false;
|
|
1360
1448
|
if (this.engagementTimeout) {
|
|
1361
1449
|
clearTimeout(this.engagementTimeout);
|
|
1362
1450
|
this.engagementTimeout = null;
|
|
@@ -1378,6 +1466,10 @@ class EngagementPlugin extends BasePlugin {
|
|
|
1378
1466
|
}, 30000); // 30 seconds of inactivity
|
|
1379
1467
|
}
|
|
1380
1468
|
trackTimeOnPage() {
|
|
1469
|
+
// Guard: beforeunload and visibilitychange:hidden both fire on tab close — only report once
|
|
1470
|
+
if (this.unloadReported)
|
|
1471
|
+
return;
|
|
1472
|
+
this.unloadReported = true;
|
|
1381
1473
|
const timeSpent = Math.floor((Date.now() - this.engagementStartTime) / 1000);
|
|
1382
1474
|
if (timeSpent > 0) {
|
|
1383
1475
|
this.track('time_on_page', 'Time Spent', {
|
|
@@ -1536,12 +1628,16 @@ class ExitIntentPlugin extends BasePlugin {
|
|
|
1536
1628
|
/**
|
|
1537
1629
|
* Error Tracking Plugin - Tracks JavaScript errors
|
|
1538
1630
|
*/
|
|
1631
|
+
/** Max unique errors to track per page (prevents queue flooding from error loops) */
|
|
1632
|
+
const MAX_UNIQUE_ERRORS = 20;
|
|
1539
1633
|
class ErrorsPlugin extends BasePlugin {
|
|
1540
1634
|
constructor() {
|
|
1541
1635
|
super(...arguments);
|
|
1542
1636
|
this.name = 'errors';
|
|
1543
1637
|
this.boundErrorHandler = null;
|
|
1544
1638
|
this.boundRejectionHandler = null;
|
|
1639
|
+
/** Seen error fingerprints — deduplicates repeated identical errors */
|
|
1640
|
+
this.seenErrors = new Set();
|
|
1545
1641
|
}
|
|
1546
1642
|
init(tracker) {
|
|
1547
1643
|
super.init(tracker);
|
|
@@ -1564,6 +1660,9 @@ class ErrorsPlugin extends BasePlugin {
|
|
|
1564
1660
|
super.destroy();
|
|
1565
1661
|
}
|
|
1566
1662
|
handleError(e) {
|
|
1663
|
+
const fingerprint = `${e.message}:${e.filename}:${e.lineno}`;
|
|
1664
|
+
if (!this.dedup(fingerprint))
|
|
1665
|
+
return;
|
|
1567
1666
|
this.track('error', 'JavaScript Error', {
|
|
1568
1667
|
message: e.message,
|
|
1569
1668
|
filename: e.filename,
|
|
@@ -1573,9 +1672,22 @@ class ErrorsPlugin extends BasePlugin {
|
|
|
1573
1672
|
});
|
|
1574
1673
|
}
|
|
1575
1674
|
handleRejection(e) {
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1675
|
+
const reason = String(e.reason).substring(0, 200);
|
|
1676
|
+
if (!this.dedup(reason))
|
|
1677
|
+
return;
|
|
1678
|
+
this.track('error', 'Unhandled Promise Rejection', { reason });
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Returns true if this error fingerprint is new (should be tracked).
|
|
1682
|
+
* Caps at MAX_UNIQUE_ERRORS to prevent queue flooding from error loops.
|
|
1683
|
+
*/
|
|
1684
|
+
dedup(fingerprint) {
|
|
1685
|
+
if (this.seenErrors.has(fingerprint))
|
|
1686
|
+
return false;
|
|
1687
|
+
if (this.seenErrors.size >= MAX_UNIQUE_ERRORS)
|
|
1688
|
+
return false;
|
|
1689
|
+
this.seenErrors.add(fingerprint);
|
|
1690
|
+
return true;
|
|
1579
1691
|
}
|
|
1580
1692
|
}
|
|
1581
1693
|
|
|
@@ -3514,28 +3626,17 @@ class Tracker {
|
|
|
3514
3626
|
}
|
|
3515
3627
|
const prevId = previousId || this.visitorId;
|
|
3516
3628
|
logger.info('Aliasing visitor:', { from: prevId, to: newId });
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
newId,
|
|
3526
|
-
}),
|
|
3527
|
-
});
|
|
3528
|
-
if (response.ok) {
|
|
3529
|
-
logger.info('Alias successful');
|
|
3530
|
-
return true;
|
|
3531
|
-
}
|
|
3532
|
-
logger.error('Alias failed:', response.status);
|
|
3533
|
-
return false;
|
|
3534
|
-
}
|
|
3535
|
-
catch (error) {
|
|
3536
|
-
logger.error('Alias request failed:', error);
|
|
3537
|
-
return false;
|
|
3629
|
+
const result = await this.transport.sendPost('/api/public/track/alias', {
|
|
3630
|
+
workspaceId: this.workspaceId,
|
|
3631
|
+
previousId: prevId,
|
|
3632
|
+
newId,
|
|
3633
|
+
});
|
|
3634
|
+
if (result.success) {
|
|
3635
|
+
logger.info('Alias successful');
|
|
3636
|
+
return true;
|
|
3538
3637
|
}
|
|
3638
|
+
logger.error('Alias failed:', result.error ?? result.status);
|
|
3639
|
+
return false;
|
|
3539
3640
|
}
|
|
3540
3641
|
/**
|
|
3541
3642
|
* Track a screen view (for mobile-first PWAs and SPAs).
|
|
@@ -3898,13 +3999,24 @@ let globalInstance = null;
|
|
|
3898
3999
|
* });
|
|
3899
4000
|
*/
|
|
3900
4001
|
function clianta(workspaceId, config) {
|
|
3901
|
-
// Return existing instance if same workspace
|
|
4002
|
+
// Return existing instance if same workspace and no config change
|
|
3902
4003
|
if (globalInstance && globalInstance.getWorkspaceId() === workspaceId) {
|
|
4004
|
+
if (config && Object.keys(config).length > 0) {
|
|
4005
|
+
// Config was passed to an already-initialized instance — warn the developer
|
|
4006
|
+
// because the new config is ignored. They must call destroy() first to reconfigure.
|
|
4007
|
+
if (typeof console !== 'undefined') {
|
|
4008
|
+
console.warn('[Clianta] clianta() called with config on an already-initialized instance ' +
|
|
4009
|
+
'for workspace "' + workspaceId + '". The new config was ignored. ' +
|
|
4010
|
+
'Call tracker.destroy() first if you need to reconfigure.');
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
3903
4013
|
return globalInstance;
|
|
3904
4014
|
}
|
|
3905
|
-
// Destroy existing instance if workspace changed
|
|
4015
|
+
// Destroy existing instance if workspace changed (fire-and-forget flush, then destroy)
|
|
3906
4016
|
if (globalInstance) {
|
|
3907
|
-
|
|
4017
|
+
// Kick off async flush+destroy without blocking the new instance creation.
|
|
4018
|
+
// Using void to make the intentional fire-and-forget explicit.
|
|
4019
|
+
void globalInstance.destroy();
|
|
3908
4020
|
}
|
|
3909
4021
|
// Create new instance
|
|
3910
4022
|
globalInstance = new Tracker(workspaceId, config);
|
|
@@ -3932,8 +4044,21 @@ if (typeof window !== 'undefined') {
|
|
|
3932
4044
|
const projectId = script.getAttribute('data-project-id');
|
|
3933
4045
|
if (!projectId)
|
|
3934
4046
|
return;
|
|
3935
|
-
const
|
|
3936
|
-
|
|
4047
|
+
const initConfig = {
|
|
4048
|
+
debug: script.hasAttribute('data-debug'),
|
|
4049
|
+
};
|
|
4050
|
+
// Support additional config via script tag attributes:
|
|
4051
|
+
// data-api-endpoint="https://api.yourhost.com"
|
|
4052
|
+
// data-cookieless (boolean flag)
|
|
4053
|
+
// data-use-cookies (boolean flag)
|
|
4054
|
+
const apiEndpoint = script.getAttribute('data-api-endpoint');
|
|
4055
|
+
if (apiEndpoint)
|
|
4056
|
+
initConfig.apiEndpoint = apiEndpoint;
|
|
4057
|
+
if (script.hasAttribute('data-cookieless'))
|
|
4058
|
+
initConfig.cookielessMode = true;
|
|
4059
|
+
if (script.hasAttribute('data-use-cookies'))
|
|
4060
|
+
initConfig.useCookies = true;
|
|
4061
|
+
const instance = clianta(projectId, initConfig);
|
|
3937
4062
|
// Expose the auto-initialized instance globally
|
|
3938
4063
|
window.__clianta = instance;
|
|
3939
4064
|
};
|