@affogatosoftware/recorder 1.0.8 → 1.1.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.
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export { Recorder, type RecorderSettings, MaskingLevel } from './recorder/recorder';
1
+ export { Recorder, type RecorderSettings } from './recorder/recorder';
2
+ export { NetworkRecorder, type NetworkRequest } from './recorder/networkRecorder';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
- export { Recorder, MaskingLevel } from './recorder/recorder';
1
+ export { Recorder } from './recorder/recorder';
2
+ export { NetworkRecorder } from './recorder/networkRecorder';
@@ -0,0 +1,29 @@
1
+ export interface ConsoleError {
2
+ errorType: "console_error" | "console_warn" | "uncaught_error" | "unhandled_rejection";
3
+ message: string;
4
+ stack?: string;
5
+ timestamp: number;
6
+ source?: string;
7
+ }
8
+ export declare class ErrorRecorder {
9
+ private window;
10
+ private errorsBuffer;
11
+ private isRunning;
12
+ private readonly flushIntervalMs;
13
+ private capturedSessionId;
14
+ private flushTimerId;
15
+ private originalConsoleError;
16
+ private originalConsoleWarn;
17
+ private enabled;
18
+ constructor(window: Window, consoleErrorSettings?: {
19
+ enabled: boolean;
20
+ });
21
+ start: () => void;
22
+ stop: () => void;
23
+ private captureConsoleError;
24
+ private handleUncaughtError;
25
+ private handleUnhandledRejection;
26
+ private scheduleFlush;
27
+ private flush;
28
+ setCapturedSessionId(uuid: string): void;
29
+ }
@@ -0,0 +1,143 @@
1
+ // Captures console errors, warnings, and uncaught errors/promise rejections
2
+ // Buffers error events and flushes to API endpoint periodically similar to EventRecorder
3
+ import { logger } from "../logger";
4
+ import { post } from "../requests";
5
+ export class ErrorRecorder {
6
+ window;
7
+ errorsBuffer = [];
8
+ isRunning = false;
9
+ flushIntervalMs = 2000;
10
+ capturedSessionId;
11
+ flushTimerId = null;
12
+ originalConsoleError;
13
+ originalConsoleWarn;
14
+ enabled;
15
+ constructor(window, consoleErrorSettings) {
16
+ this.window = window;
17
+ this.originalConsoleError = console.error.bind(console);
18
+ this.originalConsoleWarn = console.warn.bind(console);
19
+ this.enabled = consoleErrorSettings?.enabled ?? false; // Default enabled for backwards compatibility
20
+ }
21
+ start = () => {
22
+ if (this.isRunning || !this.enabled) {
23
+ return;
24
+ }
25
+ this.isRunning = true;
26
+ this.errorsBuffer = [];
27
+ // Intercept console.error
28
+ console.error = (...args) => {
29
+ this.captureConsoleError("console_error", args);
30
+ this.originalConsoleError(...args);
31
+ };
32
+ // Intercept console.warn
33
+ console.warn = (...args) => {
34
+ this.captureConsoleError("console_warn", args);
35
+ this.originalConsoleWarn(...args);
36
+ };
37
+ // Capture uncaught errors
38
+ this.window.addEventListener("error", this.handleUncaughtError);
39
+ // Capture unhandled promise rejections
40
+ this.window.addEventListener("unhandledrejection", this.handleUnhandledRejection);
41
+ this.scheduleFlush();
42
+ };
43
+ stop = () => {
44
+ this.isRunning = false;
45
+ // Restore original console methods
46
+ console.error = this.originalConsoleError;
47
+ console.warn = this.originalConsoleWarn;
48
+ // Remove event listeners
49
+ this.window.removeEventListener("error", this.handleUncaughtError);
50
+ this.window.removeEventListener("unhandledrejection", this.handleUnhandledRejection);
51
+ if (this.flushTimerId) {
52
+ clearTimeout(this.flushTimerId);
53
+ this.flushTimerId = null;
54
+ }
55
+ };
56
+ captureConsoleError = (errorType, args) => {
57
+ if (!this.isRunning) {
58
+ return;
59
+ }
60
+ try {
61
+ const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
62
+ const error = {
63
+ errorType,
64
+ message,
65
+ timestamp: Date.now()
66
+ };
67
+ // Try to extract stack trace if available
68
+ if (args.length > 0 && args[0] instanceof Error) {
69
+ error.stack = args[0].stack;
70
+ }
71
+ this.errorsBuffer.push(error);
72
+ }
73
+ catch (captureError) {
74
+ logger.error("Failed to capture console error", captureError);
75
+ }
76
+ };
77
+ handleUncaughtError = (event) => {
78
+ if (!this.isRunning) {
79
+ return;
80
+ }
81
+ try {
82
+ const error = {
83
+ errorType: "uncaught_error",
84
+ message: event.message,
85
+ stack: event.error?.stack,
86
+ timestamp: Date.now(),
87
+ source: event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : undefined
88
+ };
89
+ this.errorsBuffer.push(error);
90
+ }
91
+ catch (captureError) {
92
+ logger.error("Failed to capture uncaught error", captureError);
93
+ }
94
+ };
95
+ handleUnhandledRejection = (event) => {
96
+ if (!this.isRunning) {
97
+ return;
98
+ }
99
+ try {
100
+ let message;
101
+ let stack;
102
+ if (event.reason instanceof Error) {
103
+ message = event.reason.message;
104
+ stack = event.reason.stack;
105
+ }
106
+ else {
107
+ message = String(event.reason);
108
+ }
109
+ const error = {
110
+ errorType: "unhandled_rejection",
111
+ message,
112
+ stack,
113
+ timestamp: Date.now()
114
+ };
115
+ this.errorsBuffer.push(error);
116
+ }
117
+ catch (captureError) {
118
+ logger.error("Failed to capture unhandled rejection", captureError);
119
+ }
120
+ };
121
+ scheduleFlush = () => {
122
+ if (this.flushTimerId) {
123
+ clearTimeout(this.flushTimerId);
124
+ }
125
+ this.flushTimerId = setTimeout(this.flush, this.flushIntervalMs);
126
+ };
127
+ flush = async () => {
128
+ if (this.errorsBuffer.length > 0 && this.capturedSessionId) {
129
+ try {
130
+ const response = await post(`/public/captured-sessions/${this.capturedSessionId}/console-errors`, this.errorsBuffer, { withCredentials: false });
131
+ this.errorsBuffer = [];
132
+ }
133
+ catch (error) {
134
+ logger.error("Failed to flush console errors", error);
135
+ this.errorsBuffer = [];
136
+ }
137
+ }
138
+ this.scheduleFlush();
139
+ };
140
+ setCapturedSessionId(uuid) {
141
+ this.capturedSessionId = uuid;
142
+ }
143
+ }
@@ -1,6 +1,6 @@
1
1
  import { logger } from "../logger";
2
2
  import { post } from "../requests";
3
- import { MaskingLevel } from "./recorder";
3
+ import {} from "./recorder";
4
4
  import { assertNever } from "../utils";
5
5
  export class EventRecorder {
6
6
  window;
@@ -181,18 +181,17 @@ export class EventRecorder {
181
181
  text = target.options[target.selectedIndex].text;
182
182
  }
183
183
  switch (this.recorderSettings.maskingLevel) {
184
- case MaskingLevel.None:
184
+ case "none":
185
185
  break;
186
- case MaskingLevel.All:
186
+ case "all":
187
187
  text = null;
188
188
  break;
189
- case null:
190
- case MaskingLevel.InputAndTextArea:
189
+ case "input-and-textarea":
191
190
  break;
192
- case MaskingLevel.InputPasswordOrEmailAndTextArea:
191
+ case "input-password-or-email-and-textarea":
193
192
  break;
194
193
  default:
195
- assertNever(this.recorderSettings.maskingLevel);
194
+ text = null;
196
195
  }
197
196
  return {
198
197
  eventType: "change",
@@ -234,18 +233,17 @@ export class EventRecorder {
234
233
  text = target.src;
235
234
  }
236
235
  switch (this.recorderSettings.maskingLevel) {
237
- case MaskingLevel.None:
236
+ case "none":
238
237
  break;
239
- case MaskingLevel.All:
238
+ case "all":
240
239
  text = null;
241
240
  break;
242
- case null:
243
- case MaskingLevel.InputAndTextArea:
241
+ case "input-and-textarea":
244
242
  if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
245
243
  text = null;
246
244
  }
247
245
  break;
248
- case MaskingLevel.InputPasswordOrEmailAndTextArea:
246
+ case "input-password-or-email-and-textarea":
249
247
  if (target instanceof HTMLTextAreaElement) {
250
248
  text = null;
251
249
  }
@@ -256,7 +254,7 @@ export class EventRecorder {
256
254
  }
257
255
  break;
258
256
  default:
259
- assertNever(this.recorderSettings.maskingLevel);
257
+ text = null;
260
258
  }
261
259
  return {
262
260
  eventType: "click",
@@ -282,18 +280,17 @@ export class EventRecorder {
282
280
  }
283
281
  }
284
282
  switch (this.recorderSettings.maskingLevel) {
285
- case MaskingLevel.None:
283
+ case "none":
286
284
  break;
287
- case MaskingLevel.All:
285
+ case "all":
288
286
  text = null;
289
287
  break;
290
- case null:
291
- case MaskingLevel.InputAndTextArea:
288
+ case "input-and-textarea":
292
289
  if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
293
290
  text = null;
294
291
  }
295
292
  break;
296
- case MaskingLevel.InputPasswordOrEmailAndTextArea:
293
+ case "input-password-or-email-and-textarea":
297
294
  if (target instanceof HTMLTextAreaElement) {
298
295
  text = null;
299
296
  }
@@ -304,7 +301,7 @@ export class EventRecorder {
304
301
  }
305
302
  break;
306
303
  default:
307
- assertNever(this.recorderSettings.maskingLevel);
304
+ text = null;
308
305
  }
309
306
  return {
310
307
  eventType: "keydown",
@@ -0,0 +1,54 @@
1
+ export interface NetworkRequest {
2
+ requestId: string;
3
+ type: "xhr" | "fetch";
4
+ method: string;
5
+ url: string;
6
+ requestHeaders?: Record<string, string>;
7
+ requestBody?: string;
8
+ responseStatus?: number;
9
+ responseHeaders?: Record<string, string>;
10
+ responseBody?: string;
11
+ timestamp: number;
12
+ duration?: number;
13
+ error?: string;
14
+ }
15
+ interface NetworkRecorderSettings {
16
+ enabled: boolean;
17
+ maxRequestBodySize: number;
18
+ maxResponseBodySize: number;
19
+ excludeDomains: string[];
20
+ captureHeaders: boolean;
21
+ captureRequestBodies: boolean;
22
+ captureResponseBodies: boolean;
23
+ excludeHeaders: string[];
24
+ requestBodyMaskingFunction?: (body: string) => string;
25
+ }
26
+ export declare class NetworkRecorder {
27
+ private window;
28
+ private requestsBuffer;
29
+ private pendingRequests;
30
+ private isRunning;
31
+ private readonly flushIntervalMs;
32
+ private capturedSessionId;
33
+ private flushTimerId;
34
+ private originalFetch;
35
+ private originalXHROpen;
36
+ private originalXHRSend;
37
+ private queryParamsAllowed;
38
+ private networkSettings;
39
+ constructor(window: Window, settings?: Partial<NetworkRecorderSettings>);
40
+ start: () => void;
41
+ stop: () => void;
42
+ setCapturedSessionId(uuid: string): void;
43
+ private scheduleFlush;
44
+ private flush;
45
+ private patchFetch;
46
+ private patchXHR;
47
+ private shouldCaptureRequest;
48
+ private sanitizeUrl;
49
+ private filterHeaders;
50
+ private truncateContent;
51
+ private generateRequestId;
52
+ private addCompletedRequest;
53
+ }
54
+ export {};