@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.
Files changed (58) hide show
  1. package/README.md +9 -9
  2. package/dist/browser/tracelog.esm.js +406 -304
  3. package/dist/browser/tracelog.esm.js.map +1 -0
  4. package/dist/browser/tracelog.js +2 -2
  5. package/dist/browser/tracelog.js.map +1 -0
  6. package/dist/cjs/api.d.ts +1 -1
  7. package/dist/cjs/api.js +13 -4
  8. package/dist/cjs/app.d.ts +1 -1
  9. package/dist/cjs/app.js +4 -4
  10. package/dist/cjs/constants/config.constants.d.ts +3 -0
  11. package/dist/cjs/constants/config.constants.js +5 -2
  12. package/dist/cjs/handlers/click.handler.js +2 -2
  13. package/dist/cjs/handlers/scroll.handler.js +1 -1
  14. package/dist/cjs/handlers/session.handler.js +1 -1
  15. package/dist/cjs/managers/event.manager.d.ts +3 -0
  16. package/dist/cjs/managers/event.manager.js +47 -6
  17. package/dist/cjs/managers/sender.manager.js +4 -5
  18. package/dist/cjs/managers/storage.manager.d.ts +5 -0
  19. package/dist/cjs/managers/storage.manager.js +95 -6
  20. package/dist/cjs/public-api.d.ts +1 -1
  21. package/dist/cjs/test-bridge.d.ts +1 -1
  22. package/dist/cjs/test-bridge.js +1 -1
  23. package/dist/cjs/types/config.types.d.ts +4 -4
  24. package/dist/cjs/types/state.types.d.ts +1 -1
  25. package/dist/cjs/types/test-bridge.types.d.ts +1 -1
  26. package/dist/cjs/utils/logging.utils.d.ts +16 -1
  27. package/dist/cjs/utils/logging.utils.js +65 -4
  28. package/dist/cjs/utils/network/url.utils.d.ts +1 -1
  29. package/dist/cjs/utils/network/url.utils.js +11 -12
  30. package/dist/cjs/utils/validations/config-validations.utils.d.ts +2 -2
  31. package/dist/cjs/utils/validations/config-validations.utils.js +30 -18
  32. package/dist/esm/api.d.ts +1 -1
  33. package/dist/esm/api.js +13 -4
  34. package/dist/esm/app.d.ts +1 -1
  35. package/dist/esm/app.js +5 -5
  36. package/dist/esm/constants/config.constants.d.ts +3 -0
  37. package/dist/esm/constants/config.constants.js +3 -0
  38. package/dist/esm/handlers/click.handler.js +2 -2
  39. package/dist/esm/handlers/scroll.handler.js +1 -1
  40. package/dist/esm/handlers/session.handler.js +1 -1
  41. package/dist/esm/managers/event.manager.d.ts +3 -0
  42. package/dist/esm/managers/event.manager.js +48 -7
  43. package/dist/esm/managers/sender.manager.js +4 -5
  44. package/dist/esm/managers/storage.manager.d.ts +5 -0
  45. package/dist/esm/managers/storage.manager.js +95 -6
  46. package/dist/esm/public-api.d.ts +1 -1
  47. package/dist/esm/test-bridge.d.ts +1 -1
  48. package/dist/esm/test-bridge.js +1 -1
  49. package/dist/esm/types/config.types.d.ts +4 -4
  50. package/dist/esm/types/state.types.d.ts +1 -1
  51. package/dist/esm/types/test-bridge.types.d.ts +1 -1
  52. package/dist/esm/utils/logging.utils.d.ts +16 -1
  53. package/dist/esm/utils/logging.utils.js +65 -4
  54. package/dist/esm/utils/network/url.utils.d.ts +1 -1
  55. package/dist/esm/utils/network/url.utils.js +9 -10
  56. package/dist/esm/utils/validations/config-validations.utils.d.ts +2 -2
  57. package/dist/esm/utils/validations/config-validations.utils.js +30 -18
  58. 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: Config) => Promise<void>;
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
- await instance.init(validatedConfig);
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
- (0, utils_1.log)('error', 'Error during destroy, forced cleanup', { error });
117
- throw error;
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: Config): Promise<void>;
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 apiUrl = (0, utils_1.getApiUrl)(config);
111
- this.set('apiUrl', apiUrl);
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.TAB_CLEANUP_DELAY_MS = exports.TAB_ELECTION_TIMEOUT_MS = exports.TAB_HEARTBEAT_INTERVAL_MS = 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.BATCH_SIZE_THRESHOLD = 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 = 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?.apiUrl ?? 'default';
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)('warn', 'Event type is required');
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)('warn', 'Session start event ignored: missing sessionId');
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
- if (!this.get('sessionId')) {
127
- (0, utils_1.log)('warn', 'Cannot flush pending events: session not initialized');
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?.apiUrl === types_1.SpecialApiUrl.Fail) {
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?.apiUrl === types_1.SpecialApiUrl.Fail) {
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('apiUrl');
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)('error', 'localStorage quota exceeded - data will not persist after reload', {
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
@@ -1,7 +1,7 @@
1
1
  export * from './app.constants';
2
2
  export * from './types';
3
3
  export declare const tracelog: {
4
- init: (config: import("./types").Config) => Promise<void>;
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: any): Promise<void>;
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;
@@ -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?.apiUrl ?? 'test';
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
- /** Required API URL for custom integration. */
27
- apiUrl: string;
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?: {
@@ -3,7 +3,7 @@ import { DeviceType } from './device.types';
3
3
  import { Mode } from './mode.types';
4
4
  export interface State {
5
5
  mode?: Mode;
6
- apiUrl: string;
6
+ collectApiUrl: string;
7
7
  config: Config;
8
8
  sessionId: string | null;
9
9
  userId: string;
@@ -15,7 +15,7 @@ import { State } from './state.types';
15
15
  */
16
16
  export interface TraceLogTestBridge {
17
17
  readonly initialized: boolean;
18
- init(config: Config): Promise<void>;
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
- export declare const log: (type: "info" | "warn" | "error", msg: string, extra?: {
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;