@tracelog/lib 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@ export declare const DUPLICATE_EVENT_THRESHOLD_MS = 500;
7
7
  export declare const EVENT_SENT_INTERVAL_MS = 10000;
8
8
  export declare const SCROLL_DEBOUNCE_TIME_MS = 250;
9
9
  export declare const DEFAULT_VISIBILITY_TIMEOUT_MS = 2000;
10
- export declare const EVENT_EXPIRY_HOURS = 24;
10
+ export declare const EVENT_EXPIRY_HOURS = 2;
11
11
  export declare const EVENT_PERSISTENCE_MAX_AGE_MS: number;
12
12
  export declare const MAX_EVENTS_QUEUE_LENGTH = 100;
13
13
  export declare const MAX_RETRIES = 3;
@@ -16,8 +16,8 @@ exports.EVENT_SENT_INTERVAL_MS = 10000; // 10 seconds
16
16
  exports.SCROLL_DEBOUNCE_TIME_MS = 250;
17
17
  exports.DEFAULT_VISIBILITY_TIMEOUT_MS = 2000;
18
18
  // Event expiry
19
- exports.EVENT_EXPIRY_HOURS = 24;
20
- exports.EVENT_PERSISTENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
19
+ exports.EVENT_EXPIRY_HOURS = 2;
20
+ exports.EVENT_PERSISTENCE_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
21
21
  // ============================================================================
22
22
  // LIMITS & RETRIES
23
23
  // ============================================================================
@@ -32,3 +32,9 @@ export declare const MAX_TRACKED_ERRORS_HARD_LIMIT: number;
32
32
  * Controls what percentage of errors are actually reported
33
33
  */
34
34
  export declare const DEFAULT_ERROR_SAMPLING_RATE = 0.1;
35
+ /**
36
+ * Time window for throttling permanent error logs in milliseconds
37
+ * Same error status codes are logged at most once per this window
38
+ * Prevents console spam when backend repeatedly returns 4xx errors
39
+ */
40
+ export declare const PERMANENT_ERROR_LOG_THROTTLE_MS = 60000;
@@ -4,7 +4,7 @@
4
4
  * Centralizes patterns and limits for error tracking and data protection
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.DEFAULT_ERROR_SAMPLING_RATE = exports.MAX_TRACKED_ERRORS_HARD_LIMIT = exports.MAX_TRACKED_ERRORS = exports.ERROR_SUPPRESSION_WINDOW_MS = exports.MAX_ERROR_MESSAGE_LENGTH = exports.PII_PATTERNS = void 0;
7
+ exports.PERMANENT_ERROR_LOG_THROTTLE_MS = exports.DEFAULT_ERROR_SAMPLING_RATE = exports.MAX_TRACKED_ERRORS_HARD_LIMIT = exports.MAX_TRACKED_ERRORS = exports.ERROR_SUPPRESSION_WINDOW_MS = exports.MAX_ERROR_MESSAGE_LENGTH = exports.PII_PATTERNS = void 0;
8
8
  // ============================================================================
9
9
  // PII SANITIZATION PATTERNS
10
10
  // ============================================================================
@@ -59,3 +59,12 @@ exports.MAX_TRACKED_ERRORS_HARD_LIMIT = exports.MAX_TRACKED_ERRORS * 2;
59
59
  * Controls what percentage of errors are actually reported
60
60
  */
61
61
  exports.DEFAULT_ERROR_SAMPLING_RATE = 0.1; // 10% of errors
62
+ // ============================================================================
63
+ // PERMANENT ERROR LOGGING
64
+ // ============================================================================
65
+ /**
66
+ * Time window for throttling permanent error logs in milliseconds
67
+ * Same error status codes are logged at most once per this window
68
+ * Prevents console spam when backend repeatedly returns 4xx errors
69
+ */
70
+ exports.PERMANENT_ERROR_LOG_THROTTLE_MS = 60000; // 1 minute
@@ -10,6 +10,7 @@ export declare class SenderManager extends StateManager {
10
10
  private retryTimeoutId;
11
11
  private retryCount;
12
12
  private isRetrying;
13
+ private lastPermanentErrorLog;
13
14
  constructor(storeManager: StorageManager);
14
15
  private getQueueStorageKey;
15
16
  sendEventsQueueSync(body: BaseEventsQueueDto): boolean;
@@ -33,5 +34,6 @@ export declare class SenderManager extends StateManager {
33
34
  private simulateSuccessfulSend;
34
35
  private isSendBeaconAvailable;
35
36
  private clearRetryTimeout;
37
+ private logPermanentError;
36
38
  }
37
39
  export {};
@@ -11,6 +11,7 @@ class SenderManager extends state_manager_1.StateManager {
11
11
  this.retryTimeoutId = null;
12
12
  this.retryCount = 0;
13
13
  this.isRetrying = false;
14
+ this.lastPermanentErrorLog = null;
14
15
  this.storeManager = storeManager;
15
16
  }
16
17
  getQueueStorageKey() {
@@ -42,17 +43,31 @@ class SenderManager extends state_manager_1.StateManager {
42
43
  (0, utils_1.log)('warn', 'Failed to persist events, attempting immediate send');
43
44
  }
44
45
  }
45
- const success = await this.send(body);
46
- if (success) {
47
- this.clearPersistedEvents();
48
- this.resetRetryState();
49
- callbacks?.onSuccess?.(body.events.length, body.events, body);
46
+ try {
47
+ const success = await this.send(body);
48
+ if (success) {
49
+ this.clearPersistedEvents();
50
+ this.resetRetryState();
51
+ callbacks?.onSuccess?.(body.events.length, body.events, body);
52
+ }
53
+ else {
54
+ this.scheduleRetry(body, callbacks);
55
+ callbacks?.onFailure?.();
56
+ }
57
+ return success;
50
58
  }
51
- else {
52
- this.scheduleRetry(body, callbacks);
53
- callbacks?.onFailure?.();
59
+ catch (error) {
60
+ // Permanent errors should not be retried
61
+ if (error instanceof types_1.PermanentError) {
62
+ this.logPermanentError('Permanent error, not retrying', error);
63
+ this.clearPersistedEvents();
64
+ this.resetRetryState();
65
+ callbacks?.onFailure?.();
66
+ return false;
67
+ }
68
+ // Re-throw unexpected errors
69
+ throw error;
54
70
  }
55
- return success;
56
71
  }
57
72
  async recoverPersistedEvents(callbacks) {
58
73
  try {
@@ -74,6 +89,14 @@ class SenderManager extends state_manager_1.StateManager {
74
89
  }
75
90
  }
76
91
  catch (error) {
92
+ // Permanent errors should clear persisted events immediately
93
+ if (error instanceof types_1.PermanentError) {
94
+ this.logPermanentError('Permanent error during recovery, clearing persisted events', error);
95
+ this.clearPersistedEvents();
96
+ this.resetRetryState();
97
+ callbacks?.onFailure?.();
98
+ return;
99
+ }
77
100
  (0, utils_1.log)('error', 'Failed to recover persisted events', { error });
78
101
  this.clearPersistedEvents();
79
102
  }
@@ -105,6 +128,10 @@ class SenderManager extends state_manager_1.StateManager {
105
128
  return response.ok;
106
129
  }
107
130
  catch (error) {
131
+ // Re-throw PermanentError to be caught by caller
132
+ if (error instanceof types_1.PermanentError) {
133
+ throw error;
134
+ }
108
135
  (0, utils_1.log)('error', 'Send request failed', {
109
136
  error,
110
137
  data: {
@@ -130,6 +157,13 @@ class SenderManager extends state_manager_1.StateManager {
130
157
  },
131
158
  });
132
159
  if (!response.ok) {
160
+ // 4xx errors are permanent client errors - don't retry
161
+ const isPermanentError = response.status >= 400 && response.status < 500;
162
+ if (isPermanentError) {
163
+ // Note: Logging handled by caller with throttling to prevent spam
164
+ throw new types_1.PermanentError(`HTTP ${response.status}: ${response.statusText}`, response.status);
165
+ }
166
+ // 5xx or other errors - retry
133
167
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
134
168
  }
135
169
  return response;
@@ -265,6 +299,25 @@ class SenderManager extends state_manager_1.StateManager {
265
299
  this.scheduleRetry(body, originalCallbacks);
266
300
  }
267
301
  }
302
+ catch (error) {
303
+ // If it's a permanent error, don't retry
304
+ if (error instanceof types_1.PermanentError) {
305
+ this.logPermanentError('Permanent error detected during retry, giving up', error);
306
+ this.clearPersistedEvents();
307
+ this.resetRetryState();
308
+ originalCallbacks?.onFailure?.();
309
+ return;
310
+ }
311
+ // For other errors, continue normal retry flow
312
+ if (this.retryCount >= constants_1.MAX_RETRIES) {
313
+ this.clearPersistedEvents();
314
+ this.resetRetryState();
315
+ originalCallbacks?.onFailure?.();
316
+ }
317
+ else {
318
+ this.scheduleRetry(body, originalCallbacks);
319
+ }
320
+ }
268
321
  finally {
269
322
  this.isRetrying = false;
270
323
  }
@@ -287,5 +340,17 @@ class SenderManager extends state_manager_1.StateManager {
287
340
  this.retryTimeoutId = null;
288
341
  }
289
342
  }
343
+ logPermanentError(context, error) {
344
+ const now = Date.now();
345
+ const shouldLog = !this.lastPermanentErrorLog ||
346
+ this.lastPermanentErrorLog.statusCode !== error.statusCode ||
347
+ now - this.lastPermanentErrorLog.timestamp >= constants_1.PERMANENT_ERROR_LOG_THROTTLE_MS;
348
+ if (shouldLog) {
349
+ (0, utils_1.log)('error', context, {
350
+ data: { status: error.statusCode, message: error.message },
351
+ });
352
+ this.lastPermanentErrorLog = { statusCode: error.statusCode, timestamp: now };
353
+ }
354
+ }
290
355
  }
291
356
  exports.SenderManager = SenderManager;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Custom error types for TraceLog
3
+ */
4
+ /**
5
+ * Represents a permanent HTTP error (4xx) that should not be retried
6
+ * Examples: 400 Bad Request, 403 Forbidden, 404 Not Found
7
+ */
8
+ export declare class PermanentError extends Error {
9
+ readonly statusCode?: number | undefined;
10
+ constructor(message: string, statusCode?: number | undefined);
11
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * Custom error types for TraceLog
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PermanentError = void 0;
7
+ /**
8
+ * Represents a permanent HTTP error (4xx) that should not be retried
9
+ * Examples: 400 Bad Request, 403 Forbidden, 404 Not Found
10
+ */
11
+ class PermanentError extends Error {
12
+ constructor(message, statusCode) {
13
+ super(message);
14
+ this.statusCode = statusCode;
15
+ this.name = 'PermanentError';
16
+ // Maintain proper stack trace for where our error was thrown (only available on V8)
17
+ if (Error.captureStackTrace) {
18
+ Error.captureStackTrace(this, PermanentError);
19
+ }
20
+ }
21
+ }
22
+ exports.PermanentError = PermanentError;
@@ -2,6 +2,7 @@ export * from './common.types';
2
2
  export * from './config.types';
3
3
  export * from './device.types';
4
4
  export * from './emitter.types';
5
+ export * from './error.types';
5
6
  export * from './event.types';
6
7
  export * from './log.types';
7
8
  export * from './mode.types';
@@ -18,6 +18,7 @@ __exportStar(require("./common.types"), exports);
18
18
  __exportStar(require("./config.types"), exports);
19
19
  __exportStar(require("./device.types"), exports);
20
20
  __exportStar(require("./emitter.types"), exports);
21
+ __exportStar(require("./error.types"), exports);
21
22
  __exportStar(require("./event.types"), exports);
22
23
  __exportStar(require("./log.types"), exports);
23
24
  __exportStar(require("./mode.types"), exports);
@@ -7,7 +7,7 @@ export declare const DUPLICATE_EVENT_THRESHOLD_MS = 500;
7
7
  export declare const EVENT_SENT_INTERVAL_MS = 10000;
8
8
  export declare const SCROLL_DEBOUNCE_TIME_MS = 250;
9
9
  export declare const DEFAULT_VISIBILITY_TIMEOUT_MS = 2000;
10
- export declare const EVENT_EXPIRY_HOURS = 24;
10
+ export declare const EVENT_EXPIRY_HOURS = 2;
11
11
  export declare const EVENT_PERSISTENCE_MAX_AGE_MS: number;
12
12
  export declare const MAX_EVENTS_QUEUE_LENGTH = 100;
13
13
  export declare const MAX_RETRIES = 3;
@@ -12,8 +12,8 @@ export const EVENT_SENT_INTERVAL_MS = 10000; // 10 seconds
12
12
  export const SCROLL_DEBOUNCE_TIME_MS = 250;
13
13
  export const DEFAULT_VISIBILITY_TIMEOUT_MS = 2000;
14
14
  // Event expiry
15
- export const EVENT_EXPIRY_HOURS = 24;
16
- export const EVENT_PERSISTENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
15
+ export const EVENT_EXPIRY_HOURS = 2;
16
+ export const EVENT_PERSISTENCE_MAX_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours
17
17
  // ============================================================================
18
18
  // LIMITS & RETRIES
19
19
  // ============================================================================
@@ -32,3 +32,9 @@ export declare const MAX_TRACKED_ERRORS_HARD_LIMIT: number;
32
32
  * Controls what percentage of errors are actually reported
33
33
  */
34
34
  export declare const DEFAULT_ERROR_SAMPLING_RATE = 0.1;
35
+ /**
36
+ * Time window for throttling permanent error logs in milliseconds
37
+ * Same error status codes are logged at most once per this window
38
+ * Prevents console spam when backend repeatedly returns 4xx errors
39
+ */
40
+ export declare const PERMANENT_ERROR_LOG_THROTTLE_MS = 60000;
@@ -56,3 +56,12 @@ export const MAX_TRACKED_ERRORS_HARD_LIMIT = MAX_TRACKED_ERRORS * 2;
56
56
  * Controls what percentage of errors are actually reported
57
57
  */
58
58
  export const DEFAULT_ERROR_SAMPLING_RATE = 0.1; // 10% of errors
59
+ // ============================================================================
60
+ // PERMANENT ERROR LOGGING
61
+ // ============================================================================
62
+ /**
63
+ * Time window for throttling permanent error logs in milliseconds
64
+ * Same error status codes are logged at most once per this window
65
+ * Prevents console spam when backend repeatedly returns 4xx errors
66
+ */
67
+ export const PERMANENT_ERROR_LOG_THROTTLE_MS = 60000; // 1 minute
@@ -10,6 +10,7 @@ export declare class SenderManager extends StateManager {
10
10
  private retryTimeoutId;
11
11
  private retryCount;
12
12
  private isRetrying;
13
+ private lastPermanentErrorLog;
13
14
  constructor(storeManager: StorageManager);
14
15
  private getQueueStorageKey;
15
16
  sendEventsQueueSync(body: BaseEventsQueueDto): boolean;
@@ -33,5 +34,6 @@ export declare class SenderManager extends StateManager {
33
34
  private simulateSuccessfulSend;
34
35
  private isSendBeaconAvailable;
35
36
  private clearRetryTimeout;
37
+ private logPermanentError;
36
38
  }
37
39
  export {};
@@ -1,5 +1,5 @@
1
- import { QUEUE_KEY, EVENT_EXPIRY_HOURS, MAX_RETRIES, RETRY_DELAY_MS, REQUEST_TIMEOUT_MS } from '../constants';
2
- import { SpecialApiUrl } from '../types';
1
+ import { QUEUE_KEY, EVENT_EXPIRY_HOURS, MAX_RETRIES, RETRY_DELAY_MS, REQUEST_TIMEOUT_MS, PERMANENT_ERROR_LOG_THROTTLE_MS, } from '../constants';
2
+ import { SpecialApiUrl, PermanentError } from '../types';
3
3
  import { log } from '../utils';
4
4
  import { StateManager } from './state.manager';
5
5
  export class SenderManager extends StateManager {
@@ -8,6 +8,7 @@ export class SenderManager extends StateManager {
8
8
  this.retryTimeoutId = null;
9
9
  this.retryCount = 0;
10
10
  this.isRetrying = false;
11
+ this.lastPermanentErrorLog = null;
11
12
  this.storeManager = storeManager;
12
13
  }
13
14
  getQueueStorageKey() {
@@ -39,17 +40,31 @@ export class SenderManager extends StateManager {
39
40
  log('warn', 'Failed to persist events, attempting immediate send');
40
41
  }
41
42
  }
42
- const success = await this.send(body);
43
- if (success) {
44
- this.clearPersistedEvents();
45
- this.resetRetryState();
46
- callbacks?.onSuccess?.(body.events.length, body.events, body);
43
+ try {
44
+ const success = await this.send(body);
45
+ if (success) {
46
+ this.clearPersistedEvents();
47
+ this.resetRetryState();
48
+ callbacks?.onSuccess?.(body.events.length, body.events, body);
49
+ }
50
+ else {
51
+ this.scheduleRetry(body, callbacks);
52
+ callbacks?.onFailure?.();
53
+ }
54
+ return success;
47
55
  }
48
- else {
49
- this.scheduleRetry(body, callbacks);
50
- callbacks?.onFailure?.();
56
+ catch (error) {
57
+ // Permanent errors should not be retried
58
+ if (error instanceof PermanentError) {
59
+ this.logPermanentError('Permanent error, not retrying', error);
60
+ this.clearPersistedEvents();
61
+ this.resetRetryState();
62
+ callbacks?.onFailure?.();
63
+ return false;
64
+ }
65
+ // Re-throw unexpected errors
66
+ throw error;
51
67
  }
52
- return success;
53
68
  }
54
69
  async recoverPersistedEvents(callbacks) {
55
70
  try {
@@ -71,6 +86,14 @@ export class SenderManager extends StateManager {
71
86
  }
72
87
  }
73
88
  catch (error) {
89
+ // Permanent errors should clear persisted events immediately
90
+ if (error instanceof PermanentError) {
91
+ this.logPermanentError('Permanent error during recovery, clearing persisted events', error);
92
+ this.clearPersistedEvents();
93
+ this.resetRetryState();
94
+ callbacks?.onFailure?.();
95
+ return;
96
+ }
74
97
  log('error', 'Failed to recover persisted events', { error });
75
98
  this.clearPersistedEvents();
76
99
  }
@@ -102,6 +125,10 @@ export class SenderManager extends StateManager {
102
125
  return response.ok;
103
126
  }
104
127
  catch (error) {
128
+ // Re-throw PermanentError to be caught by caller
129
+ if (error instanceof PermanentError) {
130
+ throw error;
131
+ }
105
132
  log('error', 'Send request failed', {
106
133
  error,
107
134
  data: {
@@ -127,6 +154,13 @@ export class SenderManager extends StateManager {
127
154
  },
128
155
  });
129
156
  if (!response.ok) {
157
+ // 4xx errors are permanent client errors - don't retry
158
+ const isPermanentError = response.status >= 400 && response.status < 500;
159
+ if (isPermanentError) {
160
+ // Note: Logging handled by caller with throttling to prevent spam
161
+ throw new PermanentError(`HTTP ${response.status}: ${response.statusText}`, response.status);
162
+ }
163
+ // 5xx or other errors - retry
130
164
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
131
165
  }
132
166
  return response;
@@ -262,6 +296,25 @@ export class SenderManager extends StateManager {
262
296
  this.scheduleRetry(body, originalCallbacks);
263
297
  }
264
298
  }
299
+ catch (error) {
300
+ // If it's a permanent error, don't retry
301
+ if (error instanceof PermanentError) {
302
+ this.logPermanentError('Permanent error detected during retry, giving up', error);
303
+ this.clearPersistedEvents();
304
+ this.resetRetryState();
305
+ originalCallbacks?.onFailure?.();
306
+ return;
307
+ }
308
+ // For other errors, continue normal retry flow
309
+ if (this.retryCount >= MAX_RETRIES) {
310
+ this.clearPersistedEvents();
311
+ this.resetRetryState();
312
+ originalCallbacks?.onFailure?.();
313
+ }
314
+ else {
315
+ this.scheduleRetry(body, originalCallbacks);
316
+ }
317
+ }
265
318
  finally {
266
319
  this.isRetrying = false;
267
320
  }
@@ -284,4 +337,16 @@ export class SenderManager extends StateManager {
284
337
  this.retryTimeoutId = null;
285
338
  }
286
339
  }
340
+ logPermanentError(context, error) {
341
+ const now = Date.now();
342
+ const shouldLog = !this.lastPermanentErrorLog ||
343
+ this.lastPermanentErrorLog.statusCode !== error.statusCode ||
344
+ now - this.lastPermanentErrorLog.timestamp >= PERMANENT_ERROR_LOG_THROTTLE_MS;
345
+ if (shouldLog) {
346
+ log('error', context, {
347
+ data: { status: error.statusCode, message: error.message },
348
+ });
349
+ this.lastPermanentErrorLog = { statusCode: error.statusCode, timestamp: now };
350
+ }
351
+ }
287
352
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Custom error types for TraceLog
3
+ */
4
+ /**
5
+ * Represents a permanent HTTP error (4xx) that should not be retried
6
+ * Examples: 400 Bad Request, 403 Forbidden, 404 Not Found
7
+ */
8
+ export declare class PermanentError extends Error {
9
+ readonly statusCode?: number | undefined;
10
+ constructor(message: string, statusCode?: number | undefined);
11
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Custom error types for TraceLog
3
+ */
4
+ /**
5
+ * Represents a permanent HTTP error (4xx) that should not be retried
6
+ * Examples: 400 Bad Request, 403 Forbidden, 404 Not Found
7
+ */
8
+ export class PermanentError extends Error {
9
+ constructor(message, statusCode) {
10
+ super(message);
11
+ this.statusCode = statusCode;
12
+ this.name = 'PermanentError';
13
+ // Maintain proper stack trace for where our error was thrown (only available on V8)
14
+ if (Error.captureStackTrace) {
15
+ Error.captureStackTrace(this, PermanentError);
16
+ }
17
+ }
18
+ }
@@ -2,6 +2,7 @@ export * from './common.types';
2
2
  export * from './config.types';
3
3
  export * from './device.types';
4
4
  export * from './emitter.types';
5
+ export * from './error.types';
5
6
  export * from './event.types';
6
7
  export * from './log.types';
7
8
  export * from './mode.types';
@@ -2,6 +2,7 @@ export * from './common.types';
2
2
  export * from './config.types';
3
3
  export * from './device.types';
4
4
  export * from './emitter.types';
5
+ export * from './error.types';
5
6
  export * from './event.types';
6
7
  export * from './log.types';
7
8
  export * from './mode.types';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@tracelog/lib",
3
3
  "description": "JavaScript library for web analytics and real-time event tracking",
4
4
  "license": "MIT",
5
- "version": "0.7.2",
5
+ "version": "0.8.0",
6
6
  "main": "./dist/cjs/public-api.js",
7
7
  "module": "./dist/esm/public-api.js",
8
8
  "types": "./dist/esm/public-api.d.ts",
@@ -15,6 +15,14 @@
15
15
  "import": "./dist/esm/public-api.js",
16
16
  "require": "./dist/cjs/public-api.js"
17
17
  },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/nacorga/tracelog-lib.git"
21
+ },
22
+ "homepage": "https://github.com/nacorga/tracelog-lib#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/nacorga/tracelog-lib/issues"
25
+ },
18
26
  "publishConfig": {
19
27
  "access": "public"
20
28
  },