@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/browser/recorder.iife.js +3 -1
- package/dist/browser/recorder.iife.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/recorder/errorRecorder.d.ts +29 -0
- package/dist/recorder/errorRecorder.js +143 -0
- package/dist/recorder/eventRecorder.js +16 -19
- package/dist/recorder/networkRecorder.d.ts +54 -0
- package/dist/recorder/networkRecorder.js +376 -0
- package/dist/recorder/recorder.d.ts +21 -11
- package/dist/recorder/recorder.js +48 -17
- package/dist/recorder/sessionRecorder.js +9 -9
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { Recorder, type RecorderSettings
|
|
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
|
|
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 {
|
|
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
|
|
184
|
+
case "none":
|
|
185
185
|
break;
|
|
186
|
-
case
|
|
186
|
+
case "all":
|
|
187
187
|
text = null;
|
|
188
188
|
break;
|
|
189
|
-
case
|
|
190
|
-
case MaskingLevel.InputAndTextArea:
|
|
189
|
+
case "input-and-textarea":
|
|
191
190
|
break;
|
|
192
|
-
case
|
|
191
|
+
case "input-password-or-email-and-textarea":
|
|
193
192
|
break;
|
|
194
193
|
default:
|
|
195
|
-
|
|
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
|
|
236
|
+
case "none":
|
|
238
237
|
break;
|
|
239
|
-
case
|
|
238
|
+
case "all":
|
|
240
239
|
text = null;
|
|
241
240
|
break;
|
|
242
|
-
case
|
|
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
|
|
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
|
-
|
|
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
|
|
283
|
+
case "none":
|
|
286
284
|
break;
|
|
287
|
-
case
|
|
285
|
+
case "all":
|
|
288
286
|
text = null;
|
|
289
287
|
break;
|
|
290
|
-
case
|
|
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
|
|
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
|
-
|
|
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 {};
|