@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
@@ -3,19 +3,61 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.log = exports.formatLogMsg = void 0;
4
4
  const formatLogMsg = (msg, error) => {
5
5
  if (error) {
6
+ // In production, sanitize error messages to avoid exposing sensitive paths
7
+ if (process.env.NODE_ENV !== 'dev' && error instanceof Error) {
8
+ // Remove file paths and line numbers from error messages
9
+ const sanitizedMessage = error.message.replace(/\s+at\s+.*$/gm, '').replace(/\(.*?:\d+:\d+\)/g, '');
10
+ return `[TraceLog] ${msg}: ${sanitizedMessage}`;
11
+ }
6
12
  return `[TraceLog] ${msg}: ${error instanceof Error ? error.message : 'Unknown error'}`;
7
13
  }
8
14
  return `[TraceLog] ${msg}`;
9
15
  };
10
16
  exports.formatLogMsg = formatLogMsg;
17
+ /**
18
+ * Safe logging utility that respects production environment
19
+ *
20
+ * @param type - Log level (info, warn, error, debug)
21
+ * @param msg - Message to log
22
+ * @param extra - Optional extra data
23
+ *
24
+ * Production behavior:
25
+ * - debug: Never logged in production
26
+ * - info: Only logged if showToClient=true
27
+ * - warn: Always logged (important for debugging production issues)
28
+ * - error: Always logged
29
+ * - Stack traces are sanitized
30
+ * - Data objects are sanitized
31
+ */
11
32
  const log = (type, msg, extra) => {
12
- const { error, data, showToClient } = extra ?? {};
33
+ const { error, data, showToClient = false } = extra ?? {};
13
34
  const formattedMsg = error ? (0, exports.formatLogMsg)(msg, error) : `[TraceLog] ${msg}`;
14
35
  const method = type === 'error' ? 'error' : type === 'warn' ? 'warn' : 'log';
15
- if (process.env.NODE_ENV !== 'dev' && !showToClient) {
16
- return;
36
+ // Production logging strategy:
37
+ // - Development: Log everything
38
+ // - Production:
39
+ // - debug: never logged
40
+ // - info: only if showToClient=true
41
+ // - warn: always logged (critical for debugging)
42
+ // - error: always logged
43
+ const isProduction = process.env.NODE_ENV !== 'dev';
44
+ if (isProduction) {
45
+ // Never log debug in production
46
+ if (type === 'debug') {
47
+ return;
48
+ }
49
+ // Log info only if explicitly flagged
50
+ if (type === 'info' && !showToClient) {
51
+ return;
52
+ }
53
+ // warn and error always logged in production
17
54
  }
18
- if (data !== undefined) {
55
+ // In production, sanitize data to avoid exposing sensitive information
56
+ if (isProduction && data !== undefined) {
57
+ const sanitizedData = sanitizeLogData(data);
58
+ console[method](formattedMsg, sanitizedData);
59
+ }
60
+ else if (data !== undefined) {
19
61
  console[method](formattedMsg, data);
20
62
  }
21
63
  else {
@@ -23,3 +65,22 @@ const log = (type, msg, extra) => {
23
65
  }
24
66
  };
25
67
  exports.log = log;
68
+ /**
69
+ * Sanitizes log data in production to prevent sensitive information leakage
70
+ * Simple approach: redact sensitive keys only
71
+ */
72
+ const sanitizeLogData = (data) => {
73
+ const sanitized = {};
74
+ const sensitiveKeys = ['token', 'password', 'secret', 'key', 'apikey', 'api_key', 'sessionid', 'session_id'];
75
+ for (const [key, value] of Object.entries(data)) {
76
+ const lowerKey = key.toLowerCase();
77
+ // Redact sensitive keys
78
+ if (sensitiveKeys.some((sensitiveKey) => lowerKey.includes(sensitiveKey))) {
79
+ sanitized[key] = '[REDACTED]';
80
+ }
81
+ else {
82
+ sanitized[key] = value;
83
+ }
84
+ }
85
+ return sanitized;
86
+ };
@@ -4,7 +4,7 @@ import { Config } from '@/types';
4
4
  * @param id - The project ID
5
5
  * @returns The generated API URL
6
6
  */
7
- export declare const getApiUrl: (config: Config) => string;
7
+ export declare const getCollectApiUrl: (config: Config) => string;
8
8
  /**
9
9
  * Normalizes a URL by removing sensitive query parameters
10
10
  * @param url - The URL to normalize
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizeUrl = exports.getApiUrl = void 0;
3
+ exports.normalizeUrl = exports.getCollectApiUrl = void 0;
4
4
  const logging_utils_1 = require("../logging.utils");
5
5
  /**
6
6
  * Validates if a URL is valid and optionally allows HTTP URLs
@@ -24,8 +24,7 @@ const isValidUrl = (url, allowHttp = false) => {
24
24
  * @param id - The project ID
25
25
  * @returns The generated API URL
26
26
  */
27
- const getApiUrl = (config) => {
28
- const allowHttp = config.allowHttp ?? false;
27
+ const getCollectApiUrl = (config) => {
29
28
  if (config.integrations?.tracelog?.projectId) {
30
29
  const url = new URL(window.location.href);
31
30
  const host = url.hostname;
@@ -35,25 +34,25 @@ const getApiUrl = (config) => {
35
34
  }
36
35
  const projectId = config.integrations.tracelog.projectId;
37
36
  const cleanDomain = parts.slice(-2).join('.');
38
- const protocol = allowHttp && url.protocol === 'http:' ? 'http' : 'https';
39
- const apiUrl = `${protocol}://${projectId}.${cleanDomain}`;
40
- const isValid = isValidUrl(apiUrl, allowHttp);
37
+ const collectApiUrl = `https://${projectId}.${cleanDomain}/collect`;
38
+ const isValid = isValidUrl(collectApiUrl);
41
39
  if (!isValid) {
42
40
  throw new Error('Invalid URL');
43
41
  }
44
- return apiUrl;
42
+ return collectApiUrl;
45
43
  }
46
- if (config.integrations?.custom?.apiUrl) {
47
- const apiUrl = config.integrations.custom.apiUrl;
48
- const isValid = isValidUrl(apiUrl, allowHttp);
44
+ const collectApiUrl = config.integrations?.custom?.collectApiUrl;
45
+ if (collectApiUrl) {
46
+ const allowHttp = config.integrations?.custom?.allowHttp ?? false;
47
+ const isValid = isValidUrl(collectApiUrl, allowHttp);
49
48
  if (!isValid) {
50
49
  throw new Error('Invalid URL');
51
50
  }
52
- return apiUrl;
51
+ return collectApiUrl;
53
52
  }
54
53
  return '';
55
54
  };
56
- exports.getApiUrl = getApiUrl;
55
+ exports.getCollectApiUrl = getCollectApiUrl;
57
56
  /**
58
57
  * Normalizes a URL by removing sensitive query parameters
59
58
  * @param url - The URL to normalize
@@ -6,7 +6,7 @@ import { Config } from '../../types';
6
6
  * @throws {ProjectIdValidationError} If project ID validation fails
7
7
  * @throws {AppConfigValidationError} If other configuration validation fails
8
8
  */
9
- export declare const validateAppConfig: (config: Config) => void;
9
+ export declare const validateAppConfig: (config?: Config) => void;
10
10
  /**
11
11
  * Validates and normalizes the app configuration
12
12
  * This is the primary validation entry point that ensures consistent behavior
@@ -15,4 +15,4 @@ export declare const validateAppConfig: (config: Config) => void;
15
15
  * @throws {ProjectIdValidationError} If project ID validation fails after normalization
16
16
  * @throws {AppConfigValidationError} If other configuration validation fails
17
17
  */
18
- export declare const validateAndNormalizeConfig: (config: Config) => Config;
18
+ export declare const validateAndNormalizeConfig: (config?: Config) => Config;
@@ -12,9 +12,12 @@ const logging_utils_1 = require("../logging.utils");
12
12
  * @throws {AppConfigValidationError} If other configuration validation fails
13
13
  */
14
14
  const validateAppConfig = (config) => {
15
- if (!config || typeof config !== 'object') {
15
+ if (config !== undefined && (config === null || typeof config !== 'object')) {
16
16
  throw new validation_error_types_1.AppConfigValidationError('Configuration must be an object', 'config');
17
17
  }
18
+ if (!config) {
19
+ return;
20
+ }
18
21
  if (config.sessionTimeout !== undefined) {
19
22
  if (typeof config.sessionTimeout !== 'number' ||
20
23
  config.sessionTimeout < constants_1.MIN_SESSION_TIMEOUT_MS ||
@@ -53,11 +56,6 @@ const validateAppConfig = (config) => {
53
56
  throw new validation_error_types_1.SamplingRateValidationError(constants_1.VALIDATION_MESSAGES.INVALID_SAMPLING_RATE, 'config');
54
57
  }
55
58
  }
56
- if (config.allowHttp !== undefined) {
57
- if (typeof config.allowHttp !== 'boolean') {
58
- throw new validation_error_types_1.AppConfigValidationError('allowHttp must be a boolean', 'config');
59
- }
60
- }
61
59
  };
62
60
  exports.validateAppConfig = validateAppConfig;
63
61
  /**
@@ -149,13 +147,21 @@ const validateIntegrations = (integrations) => {
149
147
  }
150
148
  }
151
149
  if (integrations.custom) {
152
- if (!integrations.custom.apiUrl ||
153
- typeof integrations.custom.apiUrl !== 'string' ||
154
- integrations.custom.apiUrl.trim() === '') {
150
+ if (!integrations.custom.collectApiUrl ||
151
+ typeof integrations.custom.collectApiUrl !== 'string' ||
152
+ integrations.custom.collectApiUrl.trim() === '') {
155
153
  throw new validation_error_types_1.IntegrationValidationError(constants_1.VALIDATION_MESSAGES.INVALID_CUSTOM_API_URL, 'config');
156
154
  }
157
- if (!integrations.custom.apiUrl.startsWith('http')) {
158
- throw new validation_error_types_1.IntegrationValidationError('Custom API URL must start with "http"', 'config');
155
+ if (integrations.custom.allowHttp !== undefined && typeof integrations.custom.allowHttp !== 'boolean') {
156
+ throw new validation_error_types_1.IntegrationValidationError('allowHttp must be a boolean', 'config');
157
+ }
158
+ const collectApiUrl = integrations.custom.collectApiUrl.trim();
159
+ if (!collectApiUrl.startsWith('http://') && !collectApiUrl.startsWith('https://')) {
160
+ throw new validation_error_types_1.IntegrationValidationError('Custom API URL must start with "http://" or "https://"', 'config');
161
+ }
162
+ const allowHttp = integrations.custom.allowHttp ?? false;
163
+ if (!allowHttp && collectApiUrl.startsWith('http://')) {
164
+ throw new validation_error_types_1.IntegrationValidationError('Custom API URL must use HTTPS in production. Set allowHttp: true in integration config to allow HTTP (not recommended)', 'config');
159
165
  }
160
166
  }
161
167
  if (integrations.googleAnalytics) {
@@ -181,14 +187,20 @@ const validateIntegrations = (integrations) => {
181
187
  const validateAndNormalizeConfig = (config) => {
182
188
  (0, exports.validateAppConfig)(config);
183
189
  const normalizedConfig = {
184
- ...config,
185
- sessionTimeout: config.sessionTimeout ?? constants_1.DEFAULT_SESSION_TIMEOUT,
186
- globalMetadata: config.globalMetadata ?? {},
187
- sensitiveQueryParams: config.sensitiveQueryParams ?? [],
188
- errorSampling: config.errorSampling ?? 1,
189
- samplingRate: config.samplingRate ?? 1,
190
- allowHttp: config.allowHttp ?? false,
190
+ ...(config ?? {}),
191
+ sessionTimeout: config?.sessionTimeout ?? constants_1.DEFAULT_SESSION_TIMEOUT,
192
+ globalMetadata: config?.globalMetadata ?? {},
193
+ sensitiveQueryParams: config?.sensitiveQueryParams ?? [],
194
+ errorSampling: config?.errorSampling ?? 1,
195
+ samplingRate: config?.samplingRate ?? 1,
191
196
  };
197
+ // Normalize integrations
198
+ if (normalizedConfig.integrations?.custom) {
199
+ normalizedConfig.integrations.custom = {
200
+ ...normalizedConfig.integrations.custom,
201
+ allowHttp: normalizedConfig.integrations.custom.allowHttp ?? false,
202
+ };
203
+ }
192
204
  return normalizedConfig;
193
205
  };
194
206
  exports.validateAndNormalizeConfig = validateAndNormalizeConfig;
package/dist/esm/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/esm/api.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { App } from './app';
2
2
  import { log, validateAndNormalizeConfig } from './utils';
3
3
  import { TestBridge } from './test-bridge';
4
+ import { INITIALIZATION_TIMEOUT_MS } from './constants';
4
5
  import './types/window.types';
5
6
  // Buffer for listeners registered before init()
6
7
  const pendingListeners = [];
@@ -22,7 +23,7 @@ export const init = async (config) => {
22
23
  }
23
24
  isInitializing = true;
24
25
  try {
25
- const validatedConfig = validateAndNormalizeConfig(config);
26
+ const validatedConfig = validateAndNormalizeConfig(config ?? {});
26
27
  const instance = new App();
27
28
  try {
28
29
  // Attach buffered listeners BEFORE init() so they capture initial events
@@ -30,7 +31,14 @@ export const init = async (config) => {
30
31
  instance.on(event, callback);
31
32
  });
32
33
  pendingListeners.length = 0;
33
- await instance.init(validatedConfig);
34
+ // Wrap initialization with timeout using Promise.race
35
+ const initPromise = instance.init(validatedConfig);
36
+ const timeoutPromise = new Promise((_, reject) => {
37
+ setTimeout(() => {
38
+ reject(new Error(`[TraceLog] Initialization timeout after ${INITIALIZATION_TIMEOUT_MS}ms`));
39
+ }, INITIALIZATION_TIMEOUT_MS);
40
+ });
41
+ await Promise.race([initPromise, timeoutPromise]);
34
42
  app = instance;
35
43
  }
36
44
  catch (error) {
@@ -105,8 +113,9 @@ export const destroy = async () => {
105
113
  app = null;
106
114
  isInitializing = false;
107
115
  pendingListeners.length = 0;
108
- log('error', 'Error during destroy, forced cleanup', { error });
109
- throw error;
116
+ // Log error but don't re-throw - destroy should always complete successfully
117
+ // Applications should be able to tear down TraceLog even if internal cleanup fails
118
+ log('warn', 'Error during destroy, forced cleanup completed', { error });
110
119
  }
111
120
  finally {
112
121
  isDestroying = false;
package/dist/esm/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/esm/app.js CHANGED
@@ -7,7 +7,7 @@ import { ClickHandler } from './handlers/click.handler';
7
7
  import { ScrollHandler } from './handlers/scroll.handler';
8
8
  import { EventType, Mode } from './types';
9
9
  import { GoogleAnalyticsIntegration } from './integrations/google-analytics.integration';
10
- import { isEventValid, getDeviceType, normalizeUrl, Emitter, getApiUrl, detectQaMode, log } from './utils';
10
+ import { isEventValid, getDeviceType, normalizeUrl, Emitter, getCollectApiUrl, detectQaMode, log } from './utils';
11
11
  import { StorageManager } from './managers/storage.manager';
12
12
  import { SCROLL_DEBOUNCE_TIME_MS, SCROLL_SUPPRESS_MULTIPLIER } from './constants/config.constants';
13
13
  import { PerformanceHandler } from './handlers/performance.handler';
@@ -25,7 +25,7 @@ export class App extends StateManager {
25
25
  get initialized() {
26
26
  return this.isInitialized;
27
27
  }
28
- async init(config) {
28
+ async init(config = {}) {
29
29
  if (this.isInitialized) {
30
30
  return;
31
31
  }
@@ -100,12 +100,12 @@ export class App extends StateManager {
100
100
  this.isInitialized = false;
101
101
  this.handlers = {};
102
102
  }
103
- setupState(config) {
103
+ setupState(config = {}) {
104
104
  this.set('config', config);
105
105
  const userId = UserManager.getId(this.managers.storage);
106
106
  this.set('userId', userId);
107
- const apiUrl = getApiUrl(config);
108
- this.set('apiUrl', apiUrl);
107
+ const collectApiUrl = getCollectApiUrl(config);
108
+ this.set('collectApiUrl', collectApiUrl);
109
109
  const device = getDeviceType();
110
110
  this.set('device', device);
111
111
  const pageUrl = 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;
@@ -32,8 +32,11 @@ export const MAX_SCROLL_EVENTS_PER_SESSION = 120;
32
32
  export const DEFAULT_SAMPLING_RATE = 1;
33
33
  export const MIN_SAMPLING_RATE = 0;
34
34
  export const MAX_SAMPLING_RATE = 1;
35
+ export const RATE_LIMIT_WINDOW_MS = 1000; // 1 second window
36
+ export const MAX_EVENTS_PER_SECOND = 200; // Maximum 200 events per second
35
37
  // Queue and batch limits
36
38
  export const BATCH_SIZE_THRESHOLD = 50;
39
+ export const MAX_PENDING_EVENTS_BUFFER = 100; // Maximum events to buffer before session init
37
40
  // Session timeout validation limits
38
41
  export const MIN_SESSION_TIMEOUT_MS = 30000; // 30 seconds minimum
39
42
  export const MAX_SESSION_TIMEOUT_MS = 86400000; // 24 hours maximum
@@ -14,9 +14,9 @@ export class ClickHandler extends StateManager {
14
14
  this.clickHandler = (event) => {
15
15
  const mouseEvent = event;
16
16
  const target = mouseEvent.target;
17
- const clickedElement = target instanceof HTMLElement
17
+ const clickedElement = typeof HTMLElement !== 'undefined' && target instanceof HTMLElement
18
18
  ? target
19
- : target instanceof Node && target.parentElement instanceof HTMLElement
19
+ : typeof HTMLElement !== 'undefined' && target instanceof Node && target.parentElement instanceof HTMLElement
20
20
  ? target.parentElement
21
21
  : null;
22
22
  if (!clickedElement) {
@@ -42,7 +42,7 @@ export class ScrollHandler extends StateManager {
42
42
  trySetupContainers(selectors, attempt) {
43
43
  const elements = selectors
44
44
  .map((sel) => this.safeQuerySelector(sel))
45
- .filter((element) => element instanceof HTMLElement);
45
+ .filter((element) => element != null && typeof HTMLElement !== 'undefined' && element instanceof HTMLElement);
46
46
  if (elements.length > 0) {
47
47
  for (const element of elements) {
48
48
  const isAlreadyTracking = this.containers.some((c) => c.element === element);
@@ -18,7 +18,7 @@ export class SessionHandler extends StateManager {
18
18
  return;
19
19
  }
20
20
  const config = this.get('config');
21
- const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.apiUrl ?? 'default';
21
+ const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.collectApiUrl ?? 'default';
22
22
  if (!projectId) {
23
23
  throw new Error('Cannot start session tracking: config not available');
24
24
  }
@@ -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;
@@ -1,4 +1,4 @@
1
- import { EVENT_SENT_INTERVAL_MS, MAX_EVENTS_QUEUE_LENGTH, DUPLICATE_EVENT_THRESHOLD_MS, } from '../constants/config.constants';
1
+ import { EVENT_SENT_INTERVAL_MS, MAX_EVENTS_QUEUE_LENGTH, DUPLICATE_EVENT_THRESHOLD_MS, RATE_LIMIT_WINDOW_MS, MAX_EVENTS_PER_SECOND, MAX_PENDING_EVENTS_BUFFER, } from '../constants/config.constants';
2
2
  import { EmitterEvent, EventType, Mode } from '../types';
3
3
  import { getUTMParameters, log, generateEventId } from '../utils';
4
4
  import { SenderManager } from './sender.manager';
@@ -11,6 +11,8 @@ export class EventManager extends StateManager {
11
11
  this.lastEventFingerprint = null;
12
12
  this.lastEventTime = 0;
13
13
  this.sendIntervalId = null;
14
+ this.rateLimitCounter = 0;
15
+ this.rateLimitWindowStart = 0;
14
16
  this.googleAnalytics = googleAnalytics;
15
17
  this.dataSender = new SenderManager(storeManager);
16
18
  this.emitter = emitter;
@@ -33,10 +35,19 @@ export class EventManager extends StateManager {
33
35
  }
34
36
  track({ type, page_url, from_page_url, scroll_data, click_data, custom_event, web_vitals, error_data, session_end_reason, }) {
35
37
  if (!type) {
36
- log('warn', 'Event type is required');
38
+ log('error', 'Event type is required - event will be ignored');
37
39
  return;
38
40
  }
41
+ // Check session BEFORE rate limiting to avoid consuming quota for buffered events
39
42
  if (!this.get('sessionId')) {
43
+ // Protect against unbounded buffer growth during initialization delays
44
+ if (this.pendingEventsBuffer.length >= MAX_PENDING_EVENTS_BUFFER) {
45
+ // Drop oldest event (FIFO) to make room for new one
46
+ this.pendingEventsBuffer.shift();
47
+ log('warn', 'Pending events buffer full - dropping oldest event', {
48
+ data: { maxBufferSize: MAX_PENDING_EVENTS_BUFFER },
49
+ });
50
+ }
40
51
  this.pendingEventsBuffer.push({
41
52
  type,
42
53
  page_url,
@@ -50,10 +61,16 @@ export class EventManager extends StateManager {
50
61
  });
51
62
  return;
52
63
  }
64
+ // Rate limiting check (except for critical events)
65
+ // Applied AFTER session check to only rate-limit processable events
66
+ const isCriticalEvent = type === EventType.SESSION_START || type === EventType.SESSION_END;
67
+ if (!isCriticalEvent && !this.checkRateLimit()) {
68
+ // Rate limit exceeded - drop event silently
69
+ // Logging would itself cause performance issues
70
+ return;
71
+ }
53
72
  const eventType = type;
54
73
  const isSessionStart = eventType === EventType.SESSION_START;
55
- const isSessionEnd = eventType === EventType.SESSION_END;
56
- const isCriticalEvent = isSessionStart || isSessionEnd;
57
74
  const currentPageUrl = page_url || this.get('pageUrl');
58
75
  const payload = this.buildEventPayload({
59
76
  type: eventType,
@@ -72,7 +89,7 @@ export class EventManager extends StateManager {
72
89
  if (isSessionStart) {
73
90
  const currentSessionId = this.get('sessionId');
74
91
  if (!currentSessionId) {
75
- log('warn', 'Session start event ignored: missing sessionId');
92
+ log('error', 'Session start event requires sessionId - event will be ignored');
76
93
  return;
77
94
  }
78
95
  if (this.get('hasStartSession')) {
@@ -105,6 +122,8 @@ export class EventManager extends StateManager {
105
122
  this.pendingEventsBuffer = [];
106
123
  this.lastEventFingerprint = null;
107
124
  this.lastEventTime = 0;
125
+ this.rateLimitCounter = 0;
126
+ this.rateLimitWindowStart = 0;
108
127
  this.dataSender.stop();
109
128
  }
110
129
  async flushImmediately() {
@@ -120,12 +139,19 @@ export class EventManager extends StateManager {
120
139
  if (this.pendingEventsBuffer.length === 0) {
121
140
  return;
122
141
  }
123
- if (!this.get('sessionId')) {
124
- log('warn', 'Cannot flush pending events: session not initialized');
142
+ const currentSessionId = this.get('sessionId');
143
+ if (!currentSessionId) {
144
+ // Keep events in buffer for future retry - do NOT discard
145
+ // This prevents data loss during legitimate race conditions
146
+ log('warn', 'Cannot flush pending events: session not initialized - keeping in buffer', {
147
+ data: { bufferedEventCount: this.pendingEventsBuffer.length },
148
+ });
125
149
  return;
126
150
  }
151
+ // Create copy before clearing to avoid infinite recursion
127
152
  const bufferedEvents = [...this.pendingEventsBuffer];
128
153
  this.pendingEventsBuffer = [];
154
+ // Process all buffered events now that session exists
129
155
  bufferedEvents.forEach((event) => {
130
156
  this.track(event);
131
157
  });
@@ -301,6 +327,21 @@ export class EventManager extends StateManager {
301
327
  const samplingRate = this.get('config')?.samplingRate ?? 1;
302
328
  return Math.random() < samplingRate;
303
329
  }
330
+ checkRateLimit() {
331
+ const now = Date.now();
332
+ // Reset counter if window has expired
333
+ if (now - this.rateLimitWindowStart > RATE_LIMIT_WINDOW_MS) {
334
+ this.rateLimitCounter = 0;
335
+ this.rateLimitWindowStart = now;
336
+ }
337
+ // Check if limit exceeded
338
+ if (this.rateLimitCounter >= MAX_EVENTS_PER_SECOND) {
339
+ return false;
340
+ }
341
+ // Increment counter
342
+ this.rateLimitCounter++;
343
+ return true;
344
+ }
304
345
  removeProcessedEvents(eventIds) {
305
346
  const eventIdSet = new Set(eventIds);
306
347
  this.eventsQueue = this.eventsQueue.filter((event) => {
@@ -20,7 +20,7 @@ export class SenderManager extends StateManager {
20
20
  return true;
21
21
  }
22
22
  const config = this.get('config');
23
- if (config?.integrations?.custom?.apiUrl === SpecialApiUrl.Fail) {
23
+ if (config?.integrations?.custom?.collectApiUrl === SpecialApiUrl.Fail) {
24
24
  log('warn', 'Fail mode: simulating network failure (sync)', {
25
25
  data: { events: body.events.length },
26
26
  });
@@ -90,7 +90,7 @@ export class SenderManager extends StateManager {
90
90
  return this.simulateSuccessfulSend();
91
91
  }
92
92
  const config = this.get('config');
93
- if (config?.integrations?.custom?.apiUrl === SpecialApiUrl.Fail) {
93
+ if (config?.integrations?.custom?.collectApiUrl === SpecialApiUrl.Fail) {
94
94
  log('warn', 'Fail mode: simulating network failure', {
95
95
  data: { events: body.events.length },
96
96
  });
@@ -152,7 +152,6 @@ export class SenderManager extends StateManager {
152
152
  return false;
153
153
  }
154
154
  prepareRequest(body) {
155
- const url = `${this.get('apiUrl')}/collect`;
156
155
  // Enrich payload with metadata for sendBeacon() fallback
157
156
  // sendBeacon() doesn't send custom headers, so we include referer in payload
158
157
  const enrichedBody = {
@@ -163,7 +162,7 @@ export class SenderManager extends StateManager {
163
162
  },
164
163
  };
165
164
  return {
166
- url,
165
+ url: this.get('collectApiUrl'),
167
166
  payload: JSON.stringify(enrichedBody),
168
167
  };
169
168
  }
@@ -269,7 +268,7 @@ export class SenderManager extends StateManager {
269
268
  }, retryDelay);
270
269
  }
271
270
  shouldSkipSend() {
272
- return !this.get('apiUrl');
271
+ return !this.get('collectApiUrl');
273
272
  }
274
273
  async simulateSuccessfulSend() {
275
274
  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
  */