@tracelog/lib 0.6.0 → 0.6.3
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/README.md +9 -9
- package/dist/browser/tracelog.esm.js +406 -304
- package/dist/browser/tracelog.esm.js.map +1 -0
- package/dist/browser/tracelog.js +2 -2
- package/dist/browser/tracelog.js.map +1 -0
- package/dist/cjs/api.d.ts +1 -1
- package/dist/cjs/api.js +13 -4
- package/dist/cjs/app.d.ts +1 -1
- package/dist/cjs/app.js +4 -4
- package/dist/cjs/constants/config.constants.d.ts +3 -0
- package/dist/cjs/constants/config.constants.js +5 -2
- package/dist/cjs/handlers/click.handler.js +2 -2
- package/dist/cjs/handlers/scroll.handler.js +1 -1
- package/dist/cjs/handlers/session.handler.js +1 -1
- package/dist/cjs/managers/event.manager.d.ts +3 -0
- package/dist/cjs/managers/event.manager.js +47 -6
- package/dist/cjs/managers/sender.manager.js +4 -5
- package/dist/cjs/managers/storage.manager.d.ts +5 -0
- package/dist/cjs/managers/storage.manager.js +95 -6
- package/dist/cjs/public-api.d.ts +1 -1
- package/dist/cjs/test-bridge.d.ts +1 -1
- package/dist/cjs/test-bridge.js +1 -1
- package/dist/cjs/types/config.types.d.ts +4 -4
- package/dist/cjs/types/state.types.d.ts +1 -1
- package/dist/cjs/types/test-bridge.types.d.ts +1 -1
- package/dist/cjs/utils/logging.utils.d.ts +16 -1
- package/dist/cjs/utils/logging.utils.js +65 -4
- package/dist/cjs/utils/network/url.utils.d.ts +1 -1
- package/dist/cjs/utils/network/url.utils.js +11 -12
- package/dist/cjs/utils/validations/config-validations.utils.d.ts +2 -2
- package/dist/cjs/utils/validations/config-validations.utils.js +30 -18
- package/dist/esm/api.d.ts +1 -1
- package/dist/esm/api.js +13 -4
- package/dist/esm/app.d.ts +1 -1
- package/dist/esm/app.js +5 -5
- package/dist/esm/constants/config.constants.d.ts +3 -0
- package/dist/esm/constants/config.constants.js +3 -0
- package/dist/esm/handlers/click.handler.js +2 -2
- package/dist/esm/handlers/scroll.handler.js +1 -1
- package/dist/esm/handlers/session.handler.js +1 -1
- package/dist/esm/managers/event.manager.d.ts +3 -0
- package/dist/esm/managers/event.manager.js +48 -7
- package/dist/esm/managers/sender.manager.js +4 -5
- package/dist/esm/managers/storage.manager.d.ts +5 -0
- package/dist/esm/managers/storage.manager.js +95 -6
- package/dist/esm/public-api.d.ts +1 -1
- package/dist/esm/test-bridge.d.ts +1 -1
- package/dist/esm/test-bridge.js +1 -1
- package/dist/esm/types/config.types.d.ts +4 -4
- package/dist/esm/types/state.types.d.ts +1 -1
- package/dist/esm/types/test-bridge.types.d.ts +1 -1
- package/dist/esm/utils/logging.utils.d.ts +16 -1
- package/dist/esm/utils/logging.utils.js +65 -4
- package/dist/esm/utils/network/url.utils.d.ts +1 -1
- package/dist/esm/utils/network/url.utils.js +9 -10
- package/dist/esm/utils/validations/config-validations.utils.d.ts +2 -2
- package/dist/esm/utils/validations/config-validations.utils.js +30 -18
- package/package.json +7 -6
package/dist/cjs/api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { App } from './app';
|
|
2
2
|
import { MetadataType, Config, EmitterCallback, EmitterMap } from './types';
|
|
3
3
|
import './types/window.types';
|
|
4
|
-
export declare const init: (config
|
|
4
|
+
export declare const init: (config?: Config) => Promise<void>;
|
|
5
5
|
export declare const event: (name: string, metadata?: Record<string, MetadataType> | Record<string, MetadataType>[]) => void;
|
|
6
6
|
export declare const on: <K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>) => void;
|
|
7
7
|
export declare const off: <K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>) => void;
|
package/dist/cjs/api.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.__setAppInstance = exports.destroy = exports.isInitialized = exports.off
|
|
|
4
4
|
const app_1 = require("./app");
|
|
5
5
|
const utils_1 = require("./utils");
|
|
6
6
|
const test_bridge_1 = require("./test-bridge");
|
|
7
|
+
const constants_1 = require("./constants");
|
|
7
8
|
require("./types/window.types");
|
|
8
9
|
// Buffer for listeners registered before init()
|
|
9
10
|
const pendingListeners = [];
|
|
@@ -25,7 +26,7 @@ const init = async (config) => {
|
|
|
25
26
|
}
|
|
26
27
|
isInitializing = true;
|
|
27
28
|
try {
|
|
28
|
-
const validatedConfig = (0, utils_1.validateAndNormalizeConfig)(config);
|
|
29
|
+
const validatedConfig = (0, utils_1.validateAndNormalizeConfig)(config ?? {});
|
|
29
30
|
const instance = new app_1.App();
|
|
30
31
|
try {
|
|
31
32
|
// Attach buffered listeners BEFORE init() so they capture initial events
|
|
@@ -33,7 +34,14 @@ const init = async (config) => {
|
|
|
33
34
|
instance.on(event, callback);
|
|
34
35
|
});
|
|
35
36
|
pendingListeners.length = 0;
|
|
36
|
-
|
|
37
|
+
// Wrap initialization with timeout using Promise.race
|
|
38
|
+
const initPromise = instance.init(validatedConfig);
|
|
39
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
reject(new Error(`[TraceLog] Initialization timeout after ${constants_1.INITIALIZATION_TIMEOUT_MS}ms`));
|
|
42
|
+
}, constants_1.INITIALIZATION_TIMEOUT_MS);
|
|
43
|
+
});
|
|
44
|
+
await Promise.race([initPromise, timeoutPromise]);
|
|
37
45
|
app = instance;
|
|
38
46
|
}
|
|
39
47
|
catch (error) {
|
|
@@ -113,8 +121,9 @@ const destroy = async () => {
|
|
|
113
121
|
app = null;
|
|
114
122
|
isInitializing = false;
|
|
115
123
|
pendingListeners.length = 0;
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
// Log error but don't re-throw - destroy should always complete successfully
|
|
125
|
+
// Applications should be able to tear down TraceLog even if internal cleanup fails
|
|
126
|
+
(0, utils_1.log)('warn', 'Error during destroy, forced cleanup completed', { error });
|
|
118
127
|
}
|
|
119
128
|
finally {
|
|
120
129
|
isDestroying = false;
|
package/dist/cjs/app.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export declare class App extends StateManager {
|
|
|
29
29
|
googleAnalytics?: GoogleAnalyticsIntegration;
|
|
30
30
|
};
|
|
31
31
|
get initialized(): boolean;
|
|
32
|
-
init(config
|
|
32
|
+
init(config?: Config): Promise<void>;
|
|
33
33
|
sendCustomEvent(name: string, metadata?: Record<string, unknown> | Record<string, unknown>[]): void;
|
|
34
34
|
on<K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>): void;
|
|
35
35
|
off<K extends keyof EmitterMap>(event: K, callback: EmitterCallback<EmitterMap[K]>): void;
|
package/dist/cjs/app.js
CHANGED
|
@@ -28,7 +28,7 @@ class App extends state_manager_1.StateManager {
|
|
|
28
28
|
get initialized() {
|
|
29
29
|
return this.isInitialized;
|
|
30
30
|
}
|
|
31
|
-
async init(config) {
|
|
31
|
+
async init(config = {}) {
|
|
32
32
|
if (this.isInitialized) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
@@ -103,12 +103,12 @@ class App extends state_manager_1.StateManager {
|
|
|
103
103
|
this.isInitialized = false;
|
|
104
104
|
this.handlers = {};
|
|
105
105
|
}
|
|
106
|
-
setupState(config) {
|
|
106
|
+
setupState(config = {}) {
|
|
107
107
|
this.set('config', config);
|
|
108
108
|
const userId = user_manager_1.UserManager.getId(this.managers.storage);
|
|
109
109
|
this.set('userId', userId);
|
|
110
|
-
const
|
|
111
|
-
this.set('
|
|
110
|
+
const collectApiUrl = (0, utils_1.getCollectApiUrl)(config);
|
|
111
|
+
this.set('collectApiUrl', collectApiUrl);
|
|
112
112
|
const device = (0, utils_1.getDeviceType)();
|
|
113
113
|
this.set('device', device);
|
|
114
114
|
const pageUrl = (0, utils_1.normalizeUrl)(window.location.href, config.sensitiveQueryParams);
|
|
@@ -22,7 +22,10 @@ export declare const MAX_SCROLL_EVENTS_PER_SESSION = 120;
|
|
|
22
22
|
export declare const DEFAULT_SAMPLING_RATE = 1;
|
|
23
23
|
export declare const MIN_SAMPLING_RATE = 0;
|
|
24
24
|
export declare const MAX_SAMPLING_RATE = 1;
|
|
25
|
+
export declare const RATE_LIMIT_WINDOW_MS = 1000;
|
|
26
|
+
export declare const MAX_EVENTS_PER_SECOND = 200;
|
|
25
27
|
export declare const BATCH_SIZE_THRESHOLD = 50;
|
|
28
|
+
export declare const MAX_PENDING_EVENTS_BUFFER = 100;
|
|
26
29
|
export declare const MIN_SESSION_TIMEOUT_MS = 30000;
|
|
27
30
|
export declare const MAX_SESSION_TIMEOUT_MS = 86400000;
|
|
28
31
|
export declare const MAX_CUSTOM_EVENT_NAME_LENGTH = 120;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* This file centralizes all timing, limits, browser, and initialization constants
|
|
5
5
|
*/
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.
|
|
8
|
-
exports.XSS_PATTERNS = exports.VALIDATION_MESSAGES = exports.MAX_RETRY_ATTEMPTS = exports.RATE_LIMIT_INTERVAL = exports.RETRY_BACKOFF_MAX = exports.RETRY_BACKOFF_INITIAL = exports.SCROLL_SUPPRESS_MULTIPLIER = exports.MIN_SESSION_RECOVERY_WINDOW_MS = exports.MAX_SESSION_RECOVERY_WINDOW_MS = exports.MAX_SESSION_RECOVERY_ATTEMPTS = exports.SESSION_RECOVERY_WINDOW_MULTIPLIER = void 0;
|
|
7
|
+
exports.CROSS_TAB_INITIALIZATION_LOCK_TIMEOUT_MS = exports.SESSION_CLEANUP_DELAY_MS = exports.SESSION_MAX_RETRY_ATTEMPTS = exports.SESSION_SYNC_TIMEOUT_MS = exports.INITIALIZATION_TIMEOUT_MS = exports.INITIALIZATION_CONCURRENT_RETRY_DELAY_MS = exports.INITIALIZATION_MAX_CONCURRENT_RETRIES = exports.UTM_PARAMS = exports.INTERACTIVE_SELECTORS = exports.HTML_DATA_ATTR_PREFIX = exports.MAX_FINGERPRINTS_HARD_LIMIT = exports.FINGERPRINT_CLEANUP_MULTIPLIER = exports.MAX_FINGERPRINTS = exports.SYNC_XHR_TIMEOUT_MS = exports.PRECISION_TWO_DECIMALS = exports.MAX_OBJECT_DEPTH = exports.MAX_ARRAY_LENGTH = exports.MAX_STRING_LENGTH = exports.MAX_TEXT_LENGTH = exports.MAX_NESTED_OBJECT_KEYS = exports.MAX_CUSTOM_EVENT_ARRAY_SIZE = exports.MAX_CUSTOM_EVENT_KEYS = exports.MAX_CUSTOM_EVENT_STRING_SIZE = exports.MAX_CUSTOM_EVENT_NAME_LENGTH = exports.MAX_SESSION_TIMEOUT_MS = exports.MIN_SESSION_TIMEOUT_MS = exports.MAX_PENDING_EVENTS_BUFFER = exports.BATCH_SIZE_THRESHOLD = exports.MAX_EVENTS_PER_SECOND = exports.RATE_LIMIT_WINDOW_MS = exports.MAX_SAMPLING_RATE = exports.MIN_SAMPLING_RATE = exports.DEFAULT_SAMPLING_RATE = exports.MAX_SCROLL_EVENTS_PER_SESSION = exports.SCROLL_MIN_EVENT_INTERVAL_MS = exports.MIN_SCROLL_DEPTH_CHANGE = exports.SIGNIFICANT_SCROLL_DELTA = exports.DEFAULT_MOTION_THRESHOLD = exports.MAX_METADATA_SIZE = exports.REQUEST_TIMEOUT_MS = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.MAX_EVENTS_QUEUE_LENGTH = exports.EVENT_PERSISTENCE_MAX_AGE_MS = exports.EVENT_EXPIRY_HOURS = exports.DEFAULT_VISIBILITY_TIMEOUT_MS = exports.SCROLL_DEBOUNCE_TIME_MS = exports.EVENT_SENT_INTERVAL_MS = exports.DUPLICATE_EVENT_THRESHOLD_MS = exports.DEFAULT_SESSION_TIMEOUT = void 0;
|
|
8
|
+
exports.XSS_PATTERNS = exports.VALIDATION_MESSAGES = exports.MAX_RETRY_ATTEMPTS = exports.RATE_LIMIT_INTERVAL = exports.RETRY_BACKOFF_MAX = exports.RETRY_BACKOFF_INITIAL = exports.SCROLL_SUPPRESS_MULTIPLIER = exports.MIN_SESSION_RECOVERY_WINDOW_MS = exports.MAX_SESSION_RECOVERY_WINDOW_MS = exports.MAX_SESSION_RECOVERY_ATTEMPTS = exports.SESSION_RECOVERY_WINDOW_MULTIPLIER = exports.TAB_CLEANUP_DELAY_MS = exports.TAB_ELECTION_TIMEOUT_MS = exports.TAB_HEARTBEAT_INTERVAL_MS = void 0;
|
|
9
9
|
// ============================================================================
|
|
10
10
|
// SESSION & TIMING
|
|
11
11
|
// ============================================================================
|
|
@@ -36,8 +36,11 @@ exports.MAX_SCROLL_EVENTS_PER_SESSION = 120;
|
|
|
36
36
|
exports.DEFAULT_SAMPLING_RATE = 1;
|
|
37
37
|
exports.MIN_SAMPLING_RATE = 0;
|
|
38
38
|
exports.MAX_SAMPLING_RATE = 1;
|
|
39
|
+
exports.RATE_LIMIT_WINDOW_MS = 1000; // 1 second window
|
|
40
|
+
exports.MAX_EVENTS_PER_SECOND = 200; // Maximum 200 events per second
|
|
39
41
|
// Queue and batch limits
|
|
40
42
|
exports.BATCH_SIZE_THRESHOLD = 50;
|
|
43
|
+
exports.MAX_PENDING_EVENTS_BUFFER = 100; // Maximum events to buffer before session init
|
|
41
44
|
// Session timeout validation limits
|
|
42
45
|
exports.MIN_SESSION_TIMEOUT_MS = 30000; // 30 seconds minimum
|
|
43
46
|
exports.MAX_SESSION_TIMEOUT_MS = 86400000; // 24 hours maximum
|
|
@@ -17,9 +17,9 @@ class ClickHandler extends state_manager_1.StateManager {
|
|
|
17
17
|
this.clickHandler = (event) => {
|
|
18
18
|
const mouseEvent = event;
|
|
19
19
|
const target = mouseEvent.target;
|
|
20
|
-
const clickedElement = target instanceof HTMLElement
|
|
20
|
+
const clickedElement = typeof HTMLElement !== 'undefined' && target instanceof HTMLElement
|
|
21
21
|
? target
|
|
22
|
-
: target instanceof Node && target.parentElement instanceof HTMLElement
|
|
22
|
+
: typeof HTMLElement !== 'undefined' && target instanceof Node && target.parentElement instanceof HTMLElement
|
|
23
23
|
? target.parentElement
|
|
24
24
|
: null;
|
|
25
25
|
if (!clickedElement) {
|
|
@@ -45,7 +45,7 @@ class ScrollHandler extends state_manager_1.StateManager {
|
|
|
45
45
|
trySetupContainers(selectors, attempt) {
|
|
46
46
|
const elements = selectors
|
|
47
47
|
.map((sel) => this.safeQuerySelector(sel))
|
|
48
|
-
.filter((element) => element instanceof HTMLElement);
|
|
48
|
+
.filter((element) => element != null && typeof HTMLElement !== 'undefined' && element instanceof HTMLElement);
|
|
49
49
|
if (elements.length > 0) {
|
|
50
50
|
for (const element of elements) {
|
|
51
51
|
const isAlreadyTracking = this.containers.some((c) => c.element === element);
|
|
@@ -21,7 +21,7 @@ class SessionHandler extends state_manager_1.StateManager {
|
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
const config = this.get('config');
|
|
24
|
-
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.
|
|
24
|
+
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.collectApiUrl ?? 'default';
|
|
25
25
|
if (!projectId) {
|
|
26
26
|
throw new Error('Cannot start session tracking: config not available');
|
|
27
27
|
}
|
|
@@ -12,6 +12,8 @@ export declare class EventManager extends StateManager {
|
|
|
12
12
|
private lastEventFingerprint;
|
|
13
13
|
private lastEventTime;
|
|
14
14
|
private sendIntervalId;
|
|
15
|
+
private rateLimitCounter;
|
|
16
|
+
private rateLimitWindowStart;
|
|
15
17
|
constructor(storeManager: StorageManager, googleAnalytics?: GoogleAnalyticsIntegration | null, emitter?: Emitter | null);
|
|
16
18
|
recoverPersistedEvents(): Promise<void>;
|
|
17
19
|
track({ type, page_url, from_page_url, scroll_data, click_data, custom_event, web_vitals, error_data, session_end_reason, }: Partial<EventData>): void;
|
|
@@ -32,6 +34,7 @@ export declare class EventManager extends StateManager {
|
|
|
32
34
|
private startSendInterval;
|
|
33
35
|
private handleGoogleAnalyticsIntegration;
|
|
34
36
|
private shouldSample;
|
|
37
|
+
private checkRateLimit;
|
|
35
38
|
private removeProcessedEvents;
|
|
36
39
|
private emitEvent;
|
|
37
40
|
private emitEventsQueue;
|
|
@@ -14,6 +14,8 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
14
14
|
this.lastEventFingerprint = null;
|
|
15
15
|
this.lastEventTime = 0;
|
|
16
16
|
this.sendIntervalId = null;
|
|
17
|
+
this.rateLimitCounter = 0;
|
|
18
|
+
this.rateLimitWindowStart = 0;
|
|
17
19
|
this.googleAnalytics = googleAnalytics;
|
|
18
20
|
this.dataSender = new sender_manager_1.SenderManager(storeManager);
|
|
19
21
|
this.emitter = emitter;
|
|
@@ -36,10 +38,19 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
36
38
|
}
|
|
37
39
|
track({ type, page_url, from_page_url, scroll_data, click_data, custom_event, web_vitals, error_data, session_end_reason, }) {
|
|
38
40
|
if (!type) {
|
|
39
|
-
(0, utils_1.log)('
|
|
41
|
+
(0, utils_1.log)('error', 'Event type is required - event will be ignored');
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
44
|
+
// Check session BEFORE rate limiting to avoid consuming quota for buffered events
|
|
42
45
|
if (!this.get('sessionId')) {
|
|
46
|
+
// Protect against unbounded buffer growth during initialization delays
|
|
47
|
+
if (this.pendingEventsBuffer.length >= config_constants_1.MAX_PENDING_EVENTS_BUFFER) {
|
|
48
|
+
// Drop oldest event (FIFO) to make room for new one
|
|
49
|
+
this.pendingEventsBuffer.shift();
|
|
50
|
+
(0, utils_1.log)('warn', 'Pending events buffer full - dropping oldest event', {
|
|
51
|
+
data: { maxBufferSize: config_constants_1.MAX_PENDING_EVENTS_BUFFER },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
43
54
|
this.pendingEventsBuffer.push({
|
|
44
55
|
type,
|
|
45
56
|
page_url,
|
|
@@ -53,10 +64,16 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
53
64
|
});
|
|
54
65
|
return;
|
|
55
66
|
}
|
|
67
|
+
// Rate limiting check (except for critical events)
|
|
68
|
+
// Applied AFTER session check to only rate-limit processable events
|
|
69
|
+
const isCriticalEvent = type === types_1.EventType.SESSION_START || type === types_1.EventType.SESSION_END;
|
|
70
|
+
if (!isCriticalEvent && !this.checkRateLimit()) {
|
|
71
|
+
// Rate limit exceeded - drop event silently
|
|
72
|
+
// Logging would itself cause performance issues
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
56
75
|
const eventType = type;
|
|
57
76
|
const isSessionStart = eventType === types_1.EventType.SESSION_START;
|
|
58
|
-
const isSessionEnd = eventType === types_1.EventType.SESSION_END;
|
|
59
|
-
const isCriticalEvent = isSessionStart || isSessionEnd;
|
|
60
77
|
const currentPageUrl = page_url || this.get('pageUrl');
|
|
61
78
|
const payload = this.buildEventPayload({
|
|
62
79
|
type: eventType,
|
|
@@ -75,7 +92,7 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
75
92
|
if (isSessionStart) {
|
|
76
93
|
const currentSessionId = this.get('sessionId');
|
|
77
94
|
if (!currentSessionId) {
|
|
78
|
-
(0, utils_1.log)('
|
|
95
|
+
(0, utils_1.log)('error', 'Session start event requires sessionId - event will be ignored');
|
|
79
96
|
return;
|
|
80
97
|
}
|
|
81
98
|
if (this.get('hasStartSession')) {
|
|
@@ -108,6 +125,8 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
108
125
|
this.pendingEventsBuffer = [];
|
|
109
126
|
this.lastEventFingerprint = null;
|
|
110
127
|
this.lastEventTime = 0;
|
|
128
|
+
this.rateLimitCounter = 0;
|
|
129
|
+
this.rateLimitWindowStart = 0;
|
|
111
130
|
this.dataSender.stop();
|
|
112
131
|
}
|
|
113
132
|
async flushImmediately() {
|
|
@@ -123,12 +142,19 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
123
142
|
if (this.pendingEventsBuffer.length === 0) {
|
|
124
143
|
return;
|
|
125
144
|
}
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
const currentSessionId = this.get('sessionId');
|
|
146
|
+
if (!currentSessionId) {
|
|
147
|
+
// Keep events in buffer for future retry - do NOT discard
|
|
148
|
+
// This prevents data loss during legitimate race conditions
|
|
149
|
+
(0, utils_1.log)('warn', 'Cannot flush pending events: session not initialized - keeping in buffer', {
|
|
150
|
+
data: { bufferedEventCount: this.pendingEventsBuffer.length },
|
|
151
|
+
});
|
|
128
152
|
return;
|
|
129
153
|
}
|
|
154
|
+
// Create copy before clearing to avoid infinite recursion
|
|
130
155
|
const bufferedEvents = [...this.pendingEventsBuffer];
|
|
131
156
|
this.pendingEventsBuffer = [];
|
|
157
|
+
// Process all buffered events now that session exists
|
|
132
158
|
bufferedEvents.forEach((event) => {
|
|
133
159
|
this.track(event);
|
|
134
160
|
});
|
|
@@ -304,6 +330,21 @@ class EventManager extends state_manager_1.StateManager {
|
|
|
304
330
|
const samplingRate = this.get('config')?.samplingRate ?? 1;
|
|
305
331
|
return Math.random() < samplingRate;
|
|
306
332
|
}
|
|
333
|
+
checkRateLimit() {
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
// Reset counter if window has expired
|
|
336
|
+
if (now - this.rateLimitWindowStart > config_constants_1.RATE_LIMIT_WINDOW_MS) {
|
|
337
|
+
this.rateLimitCounter = 0;
|
|
338
|
+
this.rateLimitWindowStart = now;
|
|
339
|
+
}
|
|
340
|
+
// Check if limit exceeded
|
|
341
|
+
if (this.rateLimitCounter >= config_constants_1.MAX_EVENTS_PER_SECOND) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
// Increment counter
|
|
345
|
+
this.rateLimitCounter++;
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
307
348
|
removeProcessedEvents(eventIds) {
|
|
308
349
|
const eventIdSet = new Set(eventIds);
|
|
309
350
|
this.eventsQueue = this.eventsQueue.filter((event) => {
|
|
@@ -23,7 +23,7 @@ class SenderManager extends state_manager_1.StateManager {
|
|
|
23
23
|
return true;
|
|
24
24
|
}
|
|
25
25
|
const config = this.get('config');
|
|
26
|
-
if (config?.integrations?.custom?.
|
|
26
|
+
if (config?.integrations?.custom?.collectApiUrl === types_1.SpecialApiUrl.Fail) {
|
|
27
27
|
(0, utils_1.log)('warn', 'Fail mode: simulating network failure (sync)', {
|
|
28
28
|
data: { events: body.events.length },
|
|
29
29
|
});
|
|
@@ -93,7 +93,7 @@ class SenderManager extends state_manager_1.StateManager {
|
|
|
93
93
|
return this.simulateSuccessfulSend();
|
|
94
94
|
}
|
|
95
95
|
const config = this.get('config');
|
|
96
|
-
if (config?.integrations?.custom?.
|
|
96
|
+
if (config?.integrations?.custom?.collectApiUrl === types_1.SpecialApiUrl.Fail) {
|
|
97
97
|
(0, utils_1.log)('warn', 'Fail mode: simulating network failure', {
|
|
98
98
|
data: { events: body.events.length },
|
|
99
99
|
});
|
|
@@ -155,7 +155,6 @@ class SenderManager extends state_manager_1.StateManager {
|
|
|
155
155
|
return false;
|
|
156
156
|
}
|
|
157
157
|
prepareRequest(body) {
|
|
158
|
-
const url = `${this.get('apiUrl')}/collect`;
|
|
159
158
|
// Enrich payload with metadata for sendBeacon() fallback
|
|
160
159
|
// sendBeacon() doesn't send custom headers, so we include referer in payload
|
|
161
160
|
const enrichedBody = {
|
|
@@ -166,7 +165,7 @@ class SenderManager extends state_manager_1.StateManager {
|
|
|
166
165
|
},
|
|
167
166
|
};
|
|
168
167
|
return {
|
|
169
|
-
url,
|
|
168
|
+
url: this.get('collectApiUrl'),
|
|
170
169
|
payload: JSON.stringify(enrichedBody),
|
|
171
170
|
};
|
|
172
171
|
}
|
|
@@ -272,7 +271,7 @@ class SenderManager extends state_manager_1.StateManager {
|
|
|
272
271
|
}, retryDelay);
|
|
273
272
|
}
|
|
274
273
|
shouldSkipSend() {
|
|
275
|
-
return !this.get('
|
|
274
|
+
return !this.get('collectApiUrl');
|
|
276
275
|
}
|
|
277
276
|
async simulateSuccessfulSend() {
|
|
278
277
|
const delay = Math.random() * 400 + 100;
|
|
@@ -35,6 +35,11 @@ export declare class StorageManager {
|
|
|
35
35
|
* This indicates localStorage is full and data may not persist
|
|
36
36
|
*/
|
|
37
37
|
hasQuotaError(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Attempts to cleanup old TraceLog data from storage to free up space
|
|
40
|
+
* Returns true if any data was removed, false otherwise
|
|
41
|
+
*/
|
|
42
|
+
private cleanupOldData;
|
|
38
43
|
/**
|
|
39
44
|
* Initialize storage (localStorage or sessionStorage) with feature detection
|
|
40
45
|
*/
|
|
@@ -40,6 +40,9 @@ class StorageManager {
|
|
|
40
40
|
* Stores an item in storage
|
|
41
41
|
*/
|
|
42
42
|
setItem(key, value) {
|
|
43
|
+
// Always update fallback FIRST for consistency
|
|
44
|
+
// This ensures fallback is in sync and can serve as backup if storage fails
|
|
45
|
+
this.fallbackStorage.set(key, value);
|
|
43
46
|
try {
|
|
44
47
|
if (this.storage) {
|
|
45
48
|
this.storage.setItem(key, value);
|
|
@@ -49,15 +52,37 @@ class StorageManager {
|
|
|
49
52
|
catch (error) {
|
|
50
53
|
if (error instanceof DOMException && error.name === 'QuotaExceededError') {
|
|
51
54
|
this.hasQuotaExceededError = true;
|
|
52
|
-
(0, utils_1.log)('
|
|
53
|
-
error,
|
|
55
|
+
(0, utils_1.log)('warn', 'localStorage quota exceeded, attempting cleanup', {
|
|
54
56
|
data: { key, valueSize: value.length },
|
|
55
57
|
});
|
|
58
|
+
// Attempt to free up space by removing old TraceLog data
|
|
59
|
+
const cleanedUp = this.cleanupOldData();
|
|
60
|
+
if (cleanedUp) {
|
|
61
|
+
// Retry after cleanup
|
|
62
|
+
try {
|
|
63
|
+
if (this.storage) {
|
|
64
|
+
this.storage.setItem(key, value);
|
|
65
|
+
// Successfully stored after cleanup
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (retryError) {
|
|
70
|
+
(0, utils_1.log)('error', 'localStorage quota exceeded even after cleanup - data will not persist', {
|
|
71
|
+
error: retryError,
|
|
72
|
+
data: { key, valueSize: value.length },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
(0, utils_1.log)('error', 'localStorage quota exceeded and no data to cleanup - data will not persist', {
|
|
78
|
+
error,
|
|
79
|
+
data: { key, valueSize: value.length },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
56
82
|
}
|
|
57
83
|
// Else: Silent fallback - user already warned in constructor
|
|
84
|
+
// Data is already in fallbackStorage (set at beginning)
|
|
58
85
|
}
|
|
59
|
-
// Always update fallback for consistency
|
|
60
|
-
this.fallbackStorage.set(key, value);
|
|
61
86
|
}
|
|
62
87
|
/**
|
|
63
88
|
* Removes an item from storage
|
|
@@ -111,6 +136,69 @@ class StorageManager {
|
|
|
111
136
|
hasQuotaError() {
|
|
112
137
|
return this.hasQuotaExceededError;
|
|
113
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Attempts to cleanup old TraceLog data from storage to free up space
|
|
141
|
+
* Returns true if any data was removed, false otherwise
|
|
142
|
+
*/
|
|
143
|
+
cleanupOldData() {
|
|
144
|
+
if (!this.storage) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const tracelogKeys = [];
|
|
149
|
+
const persistedEventsKeys = [];
|
|
150
|
+
// Collect all TraceLog keys
|
|
151
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
152
|
+
const key = this.storage.key(i);
|
|
153
|
+
if (key?.startsWith('tracelog_')) {
|
|
154
|
+
tracelogKeys.push(key);
|
|
155
|
+
// Prioritize removing old persisted events
|
|
156
|
+
if (key.startsWith('tracelog_persisted_events_')) {
|
|
157
|
+
persistedEventsKeys.push(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// First, try to remove old persisted events (usually the largest data)
|
|
162
|
+
if (persistedEventsKeys.length > 0) {
|
|
163
|
+
persistedEventsKeys.forEach((key) => {
|
|
164
|
+
try {
|
|
165
|
+
this.storage.removeItem(key);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Ignore errors during cleanup
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Successfully cleaned up - no need to log in production
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
// If no persisted events, remove non-critical keys
|
|
175
|
+
// Define critical key prefixes that should be preserved
|
|
176
|
+
const criticalPrefixes = ['tracelog_session_', 'tracelog_user_id', 'tracelog_device_id', 'tracelog_config'];
|
|
177
|
+
const nonCriticalKeys = tracelogKeys.filter((key) => {
|
|
178
|
+
// Keep keys that start with any critical prefix
|
|
179
|
+
return !criticalPrefixes.some((prefix) => key.startsWith(prefix));
|
|
180
|
+
});
|
|
181
|
+
if (nonCriticalKeys.length > 0) {
|
|
182
|
+
// Remove up to 5 non-critical keys
|
|
183
|
+
const keysToRemove = nonCriticalKeys.slice(0, 5);
|
|
184
|
+
keysToRemove.forEach((key) => {
|
|
185
|
+
try {
|
|
186
|
+
this.storage.removeItem(key);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Ignore errors during cleanup
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Successfully cleaned up - no need to log in production
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
(0, utils_1.log)('error', 'Failed to cleanup old data', { error });
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
114
202
|
/**
|
|
115
203
|
* Initialize storage (localStorage or sessionStorage) with feature detection
|
|
116
204
|
*/
|
|
@@ -148,6 +236,8 @@ class StorageManager {
|
|
|
148
236
|
* Stores an item in sessionStorage
|
|
149
237
|
*/
|
|
150
238
|
setSessionItem(key, value) {
|
|
239
|
+
// Always update fallback FIRST for consistency
|
|
240
|
+
this.fallbackSessionStorage.set(key, value);
|
|
151
241
|
try {
|
|
152
242
|
if (this.sessionStorageRef) {
|
|
153
243
|
this.sessionStorageRef.setItem(key, value);
|
|
@@ -162,9 +252,8 @@ class StorageManager {
|
|
|
162
252
|
});
|
|
163
253
|
}
|
|
164
254
|
// Else: Silent fallback - user already warned in constructor
|
|
255
|
+
// Data is already in fallbackSessionStorage (set at beginning)
|
|
165
256
|
}
|
|
166
|
-
// Always update fallback for consistency
|
|
167
|
-
this.fallbackSessionStorage.set(key, value);
|
|
168
257
|
}
|
|
169
258
|
/**
|
|
170
259
|
* Removes an item from sessionStorage
|
package/dist/cjs/public-api.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './app.constants';
|
|
2
2
|
export * from './types';
|
|
3
3
|
export declare const tracelog: {
|
|
4
|
-
init: (config
|
|
4
|
+
init: (config?: import("./types").Config) => Promise<void>;
|
|
5
5
|
event: (name: string, metadata?: Record<string, import("./types").MetadataType> | Record<string, import("./types").MetadataType>[]) => void;
|
|
6
6
|
on: <K extends keyof import("./types").EmitterMap>(event: K, callback: import("./types").EmitterCallback<import("./types").EmitterMap[K]>) => void;
|
|
7
7
|
off: <K extends keyof import("./types").EmitterMap>(event: K, callback: import("./types").EmitterCallback<import("./types").EmitterMap[K]>) => void;
|
|
@@ -16,7 +16,7 @@ export declare class TestBridge extends App implements TraceLogTestBridge {
|
|
|
16
16
|
private _isInitializing;
|
|
17
17
|
private _isDestroying;
|
|
18
18
|
constructor(isInitializing: boolean, isDestroying: boolean);
|
|
19
|
-
init(config
|
|
19
|
+
init(config?: any): Promise<void>;
|
|
20
20
|
isInitializing(): boolean;
|
|
21
21
|
sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[]): void;
|
|
22
22
|
getSessionData(): Record<string, unknown> | null;
|
package/dist/cjs/test-bridge.js
CHANGED
|
@@ -77,7 +77,7 @@ class TestBridge extends app_1.App {
|
|
|
77
77
|
throw new Error('Storage manager not available');
|
|
78
78
|
}
|
|
79
79
|
const config = this.get('config');
|
|
80
|
-
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.
|
|
80
|
+
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.collectApiUrl ?? 'test';
|
|
81
81
|
const userId = this.get('userId');
|
|
82
82
|
const sessionId = this.get('sessionId');
|
|
83
83
|
if (!projectId || !userId) {
|
|
@@ -6,8 +6,6 @@ export interface Config {
|
|
|
6
6
|
globalMetadata?: Record<string, MetadataType>;
|
|
7
7
|
/** Selectors defining custom scroll containers to monitor. */
|
|
8
8
|
scrollContainerSelectors?: string | string[];
|
|
9
|
-
/** Enables HTTP requests for testing and development flows. */
|
|
10
|
-
allowHttp?: boolean;
|
|
11
9
|
/** Query parameters to remove before tracking URLs. */
|
|
12
10
|
sensitiveQueryParams?: string[];
|
|
13
11
|
/** Error event sampling rate between 0 and 1. */
|
|
@@ -23,8 +21,10 @@ export interface Config {
|
|
|
23
21
|
};
|
|
24
22
|
/** Custom integration options. */
|
|
25
23
|
custom?: {
|
|
26
|
-
/**
|
|
27
|
-
|
|
24
|
+
/** Endpoint for collecting events. */
|
|
25
|
+
collectApiUrl: string;
|
|
26
|
+
/** Allow HTTP URLs (not recommended for production). @default false */
|
|
27
|
+
allowHttp?: boolean;
|
|
28
28
|
};
|
|
29
29
|
/** Google Analytics integration options. */
|
|
30
30
|
googleAnalytics?: {
|
|
@@ -15,7 +15,7 @@ import { State } from './state.types';
|
|
|
15
15
|
*/
|
|
16
16
|
export interface TraceLogTestBridge {
|
|
17
17
|
readonly initialized: boolean;
|
|
18
|
-
init(config
|
|
18
|
+
init(config?: Config): Promise<void>;
|
|
19
19
|
destroy(): Promise<void>;
|
|
20
20
|
isInitializing(): boolean;
|
|
21
21
|
sendCustomEvent(name: string, data?: Record<string, unknown> | Record<string, unknown>[]): void;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
export declare const formatLogMsg: (msg: string, error?: unknown) => string;
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Safe logging utility that respects production environment
|
|
4
|
+
*
|
|
5
|
+
* @param type - Log level (info, warn, error, debug)
|
|
6
|
+
* @param msg - Message to log
|
|
7
|
+
* @param extra - Optional extra data
|
|
8
|
+
*
|
|
9
|
+
* Production behavior:
|
|
10
|
+
* - debug: Never logged in production
|
|
11
|
+
* - info: Only logged if showToClient=true
|
|
12
|
+
* - warn: Always logged (important for debugging production issues)
|
|
13
|
+
* - error: Always logged
|
|
14
|
+
* - Stack traces are sanitized
|
|
15
|
+
* - Data objects are sanitized
|
|
16
|
+
*/
|
|
17
|
+
export declare const log: (type: "info" | "warn" | "error" | "debug", msg: string, extra?: {
|
|
3
18
|
error?: unknown;
|
|
4
19
|
data?: Record<string, unknown>;
|
|
5
20
|
showToClient?: boolean;
|