cdp-skill 1.0.2 → 1.0.4
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/README.md +3 -0
- package/SKILL.md +34 -5
- package/package.json +2 -1
- package/src/capture/console-capture.js +241 -0
- package/src/capture/debug-capture.js +144 -0
- package/src/capture/error-aggregator.js +151 -0
- package/src/capture/eval-serializer.js +320 -0
- package/src/capture/index.js +40 -0
- package/src/capture/network-capture.js +211 -0
- package/src/capture/pdf-capture.js +256 -0
- package/src/capture/screenshot-capture.js +325 -0
- package/src/cdp/browser.js +569 -0
- package/src/cdp/connection.js +369 -0
- package/src/cdp/discovery.js +138 -0
- package/src/cdp/index.js +29 -0
- package/src/cdp/target-and-session.js +439 -0
- package/src/cdp-skill.js +25 -11
- package/src/constants.js +79 -0
- package/src/dom/actionability.js +638 -0
- package/src/dom/click-executor.js +923 -0
- package/src/dom/element-handle.js +496 -0
- package/src/dom/element-locator.js +475 -0
- package/src/dom/element-validator.js +120 -0
- package/src/dom/fill-executor.js +489 -0
- package/src/dom/index.js +248 -0
- package/src/dom/input-emulator.js +406 -0
- package/src/dom/keyboard-executor.js +202 -0
- package/src/dom/quad-helpers.js +89 -0
- package/src/dom/react-filler.js +94 -0
- package/src/dom/wait-executor.js +423 -0
- package/src/index.js +6 -6
- package/src/page/cookie-manager.js +202 -0
- package/src/page/dom-stability.js +181 -0
- package/src/page/index.js +36 -0
- package/src/{page.js → page/page-controller.js} +109 -839
- package/src/page/wait-utilities.js +302 -0
- package/src/page/web-storage-manager.js +108 -0
- package/src/runner/context-helpers.js +224 -0
- package/src/runner/execute-browser.js +518 -0
- package/src/runner/execute-form.js +315 -0
- package/src/runner/execute-input.js +308 -0
- package/src/runner/execute-interaction.js +672 -0
- package/src/runner/execute-navigation.js +180 -0
- package/src/runner/execute-query.js +771 -0
- package/src/runner/index.js +51 -0
- package/src/runner/step-executors.js +421 -0
- package/src/runner/step-validator.js +641 -0
- package/src/tests/Actionability.test.js +613 -0
- package/src/tests/BrowserClient.test.js +1 -1
- package/src/tests/ChromeDiscovery.test.js +1 -1
- package/src/tests/ClickExecutor.test.js +554 -0
- package/src/tests/ConsoleCapture.test.js +1 -1
- package/src/tests/ContextHelpers.test.js +453 -0
- package/src/tests/CookieManager.test.js +450 -0
- package/src/tests/DebugCapture.test.js +307 -0
- package/src/tests/ElementHandle.test.js +1 -1
- package/src/tests/ElementLocator.test.js +1 -1
- package/src/tests/ErrorAggregator.test.js +1 -1
- package/src/tests/EvalSerializer.test.js +391 -0
- package/src/tests/FillExecutor.test.js +611 -0
- package/src/tests/InputEmulator.test.js +1 -1
- package/src/tests/KeyboardExecutor.test.js +430 -0
- package/src/tests/NetworkErrorCapture.test.js +1 -1
- package/src/tests/PageController.test.js +1 -1
- package/src/tests/PdfCapture.test.js +333 -0
- package/src/tests/ScreenshotCapture.test.js +1 -1
- package/src/tests/SessionRegistry.test.js +1 -1
- package/src/tests/StepValidator.test.js +527 -0
- package/src/tests/TargetManager.test.js +1 -1
- package/src/tests/TestRunner.test.js +1 -1
- package/src/tests/WaitStrategy.test.js +1 -1
- package/src/tests/WaitUtilities.test.js +508 -0
- package/src/tests/WebStorageManager.test.js +333 -0
- package/src/types.js +309 -0
- package/src/capture.js +0 -1400
- package/src/cdp.js +0 -1286
- package/src/dom.js +0 -4379
- package/src/runner.js +0 -3676
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eval Serializer Module
|
|
3
|
+
* Handles serialization of JavaScript values for eval results
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createEvalSerializer() - Factory for eval serializer
|
|
7
|
+
* - getEvalSerializationFunction() - Get browser-side serialization code
|
|
8
|
+
* - processEvalResult(serialized) - Process serialized result
|
|
9
|
+
*
|
|
10
|
+
* @module cdp-skill/capture/eval-serializer
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create an eval serializer for handling serialization of JavaScript values
|
|
15
|
+
* Provides special handling for non-JSON-serializable values
|
|
16
|
+
* @returns {Object} Eval serializer interface
|
|
17
|
+
*/
|
|
18
|
+
export function createEvalSerializer() {
|
|
19
|
+
/**
|
|
20
|
+
* Get the serialization function that runs in browser context
|
|
21
|
+
* @returns {string} JavaScript function declaration
|
|
22
|
+
*/
|
|
23
|
+
function getSerializationFunction() {
|
|
24
|
+
return `function(value) {
|
|
25
|
+
// Handle primitives and null
|
|
26
|
+
if (value === null) return { type: 'null', value: null };
|
|
27
|
+
if (value === undefined) return { type: 'undefined', value: null };
|
|
28
|
+
|
|
29
|
+
const type = typeof value;
|
|
30
|
+
|
|
31
|
+
// Handle special number values (FR-039)
|
|
32
|
+
if (type === 'number') {
|
|
33
|
+
if (Number.isNaN(value)) return { type: 'number', value: null, repr: 'NaN' };
|
|
34
|
+
if (value === Infinity) return { type: 'number', value: null, repr: 'Infinity' };
|
|
35
|
+
if (value === -Infinity) return { type: 'number', value: null, repr: '-Infinity' };
|
|
36
|
+
return { type: 'number', value: value };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle strings, booleans, bigint
|
|
40
|
+
if (type === 'string') return { type: 'string', value: value };
|
|
41
|
+
if (type === 'boolean') return { type: 'boolean', value: value };
|
|
42
|
+
if (type === 'bigint') return { type: 'bigint', value: null, repr: value.toString() + 'n' };
|
|
43
|
+
if (type === 'symbol') return { type: 'symbol', value: null, repr: value.toString() };
|
|
44
|
+
if (type === 'function') return { type: 'function', value: null, repr: value.toString().substring(0, 100) };
|
|
45
|
+
|
|
46
|
+
// Handle Date (FR-040)
|
|
47
|
+
if (value instanceof Date) {
|
|
48
|
+
return {
|
|
49
|
+
type: 'Date',
|
|
50
|
+
value: value.toISOString(),
|
|
51
|
+
timestamp: value.getTime()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle Map (FR-040)
|
|
56
|
+
if (value instanceof Map) {
|
|
57
|
+
const entries = [];
|
|
58
|
+
let count = 0;
|
|
59
|
+
for (const [k, v] of value) {
|
|
60
|
+
if (count >= 50) break; // Limit entries
|
|
61
|
+
try {
|
|
62
|
+
entries.push([
|
|
63
|
+
typeof k === 'object' ? JSON.stringify(k) : String(k),
|
|
64
|
+
typeof v === 'object' ? JSON.stringify(v) : String(v)
|
|
65
|
+
]);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
entries.push([String(k), '[Circular]']);
|
|
68
|
+
}
|
|
69
|
+
count++;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
type: 'Map',
|
|
73
|
+
size: value.size,
|
|
74
|
+
entries: entries
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Handle Set (FR-040)
|
|
79
|
+
if (value instanceof Set) {
|
|
80
|
+
const items = [];
|
|
81
|
+
let count = 0;
|
|
82
|
+
for (const item of value) {
|
|
83
|
+
if (count >= 50) break; // Limit items
|
|
84
|
+
try {
|
|
85
|
+
items.push(typeof item === 'object' ? JSON.stringify(item) : item);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
items.push('[Circular]');
|
|
88
|
+
}
|
|
89
|
+
count++;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
type: 'Set',
|
|
93
|
+
size: value.size,
|
|
94
|
+
values: items
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle RegExp
|
|
99
|
+
if (value instanceof RegExp) {
|
|
100
|
+
return { type: 'RegExp', value: value.toString() };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle Error
|
|
104
|
+
if (value instanceof Error) {
|
|
105
|
+
return {
|
|
106
|
+
type: 'Error',
|
|
107
|
+
name: value.name,
|
|
108
|
+
message: value.message,
|
|
109
|
+
stack: value.stack ? value.stack.substring(0, 500) : null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Handle DOM Element (FR-041)
|
|
114
|
+
if (value instanceof Element) {
|
|
115
|
+
const attrs = {};
|
|
116
|
+
for (const attr of value.attributes) {
|
|
117
|
+
attrs[attr.name] = attr.value.substring(0, 100);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
type: 'Element',
|
|
121
|
+
tagName: value.tagName.toLowerCase(),
|
|
122
|
+
id: value.id || null,
|
|
123
|
+
className: value.className || null,
|
|
124
|
+
attributes: attrs,
|
|
125
|
+
textContent: value.textContent ? value.textContent.trim().substring(0, 200) : null,
|
|
126
|
+
innerHTML: value.innerHTML ? value.innerHTML.substring(0, 200) : null,
|
|
127
|
+
isConnected: value.isConnected,
|
|
128
|
+
childElementCount: value.childElementCount
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle NodeList
|
|
133
|
+
if (value instanceof NodeList || value instanceof HTMLCollection) {
|
|
134
|
+
const items = [];
|
|
135
|
+
const len = Math.min(value.length, 20);
|
|
136
|
+
for (let i = 0; i < len; i++) {
|
|
137
|
+
const el = value[i];
|
|
138
|
+
if (el instanceof Element) {
|
|
139
|
+
items.push({
|
|
140
|
+
tagName: el.tagName.toLowerCase(),
|
|
141
|
+
id: el.id || null,
|
|
142
|
+
className: el.className || null
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
type: value instanceof NodeList ? 'NodeList' : 'HTMLCollection',
|
|
148
|
+
length: value.length,
|
|
149
|
+
items: items
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle Document
|
|
154
|
+
if (value instanceof Document) {
|
|
155
|
+
return {
|
|
156
|
+
type: 'Document',
|
|
157
|
+
title: value.title,
|
|
158
|
+
url: value.URL,
|
|
159
|
+
readyState: value.readyState
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle Window
|
|
164
|
+
if (value === window) {
|
|
165
|
+
return {
|
|
166
|
+
type: 'Window',
|
|
167
|
+
location: value.location.href,
|
|
168
|
+
innerWidth: value.innerWidth,
|
|
169
|
+
innerHeight: value.innerHeight
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle arrays - recursively serialize each element
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
const items = [];
|
|
176
|
+
const len = Math.min(value.length, 100); // Limit to 100 items
|
|
177
|
+
for (let i = 0; i < len; i++) {
|
|
178
|
+
items.push(arguments.callee(value[i])); // Recursive call
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
type: 'array',
|
|
182
|
+
length: value.length,
|
|
183
|
+
items: items,
|
|
184
|
+
truncated: value.length > 100
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Handle plain objects - recursively serialize values
|
|
189
|
+
if (type === 'object') {
|
|
190
|
+
const keys = Object.keys(value);
|
|
191
|
+
const entries = {};
|
|
192
|
+
const len = Math.min(keys.length, 50); // Limit to 50 keys
|
|
193
|
+
for (let i = 0; i < len; i++) {
|
|
194
|
+
const k = keys[i];
|
|
195
|
+
entries[k] = arguments.callee(value[k]); // Recursive call
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
type: 'object',
|
|
199
|
+
keys: keys.length,
|
|
200
|
+
entries: entries,
|
|
201
|
+
truncated: keys.length > 50
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { type: 'unknown', repr: String(value) };
|
|
206
|
+
}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Process the serialized result into a clean output format
|
|
211
|
+
* @param {Object} serialized - The serialized result from browser
|
|
212
|
+
* @returns {Object} Processed output
|
|
213
|
+
*/
|
|
214
|
+
function processResult(serialized) {
|
|
215
|
+
if (!serialized || typeof serialized !== 'object') {
|
|
216
|
+
return { type: 'unknown', value: serialized };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = {
|
|
220
|
+
type: serialized.type
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// Include value if present
|
|
224
|
+
if (serialized.value !== undefined) {
|
|
225
|
+
result.value = serialized.value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Include repr for non-serializable values
|
|
229
|
+
if (serialized.repr !== undefined) {
|
|
230
|
+
result.repr = serialized.repr;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Include additional properties based on type
|
|
234
|
+
switch (serialized.type) {
|
|
235
|
+
case 'Date':
|
|
236
|
+
result.timestamp = serialized.timestamp;
|
|
237
|
+
break;
|
|
238
|
+
case 'Map':
|
|
239
|
+
result.size = serialized.size;
|
|
240
|
+
result.entries = serialized.entries;
|
|
241
|
+
break;
|
|
242
|
+
case 'Set':
|
|
243
|
+
result.size = serialized.size;
|
|
244
|
+
result.values = serialized.values;
|
|
245
|
+
break;
|
|
246
|
+
case 'Element':
|
|
247
|
+
result.tagName = serialized.tagName;
|
|
248
|
+
result.id = serialized.id;
|
|
249
|
+
result.className = serialized.className;
|
|
250
|
+
result.attributes = serialized.attributes;
|
|
251
|
+
result.textContent = serialized.textContent;
|
|
252
|
+
result.isConnected = serialized.isConnected;
|
|
253
|
+
result.childElementCount = serialized.childElementCount;
|
|
254
|
+
break;
|
|
255
|
+
case 'NodeList':
|
|
256
|
+
case 'HTMLCollection':
|
|
257
|
+
result.length = serialized.length;
|
|
258
|
+
result.items = serialized.items;
|
|
259
|
+
break;
|
|
260
|
+
case 'Error':
|
|
261
|
+
result.name = serialized.name;
|
|
262
|
+
result.message = serialized.message;
|
|
263
|
+
if (serialized.stack) result.stack = serialized.stack;
|
|
264
|
+
break;
|
|
265
|
+
case 'Document':
|
|
266
|
+
result.title = serialized.title;
|
|
267
|
+
result.url = serialized.url;
|
|
268
|
+
result.readyState = serialized.readyState;
|
|
269
|
+
break;
|
|
270
|
+
case 'Window':
|
|
271
|
+
result.location = serialized.location;
|
|
272
|
+
result.innerWidth = serialized.innerWidth;
|
|
273
|
+
result.innerHeight = serialized.innerHeight;
|
|
274
|
+
break;
|
|
275
|
+
case 'array':
|
|
276
|
+
result.length = serialized.length;
|
|
277
|
+
if (serialized.items) {
|
|
278
|
+
// Recursively process each item
|
|
279
|
+
result.items = serialized.items.map(item => processResult(item));
|
|
280
|
+
}
|
|
281
|
+
if (serialized.truncated) result.truncated = true;
|
|
282
|
+
break;
|
|
283
|
+
case 'object':
|
|
284
|
+
result.keys = serialized.keys;
|
|
285
|
+
if (serialized.entries) {
|
|
286
|
+
// Recursively process each entry value
|
|
287
|
+
result.entries = {};
|
|
288
|
+
for (const [k, v] of Object.entries(serialized.entries)) {
|
|
289
|
+
result.entries[k] = processResult(v);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (serialized.truncated) result.truncated = true;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
getSerializationFunction,
|
|
301
|
+
processResult
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get the serialization function (convenience export)
|
|
307
|
+
* @returns {string} JavaScript function declaration
|
|
308
|
+
*/
|
|
309
|
+
export function getEvalSerializationFunction() {
|
|
310
|
+
return createEvalSerializer().getSerializationFunction();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Process a serialized eval result (convenience export)
|
|
315
|
+
* @param {Object} serialized - The serialized result from browser
|
|
316
|
+
* @returns {Object} Processed output
|
|
317
|
+
*/
|
|
318
|
+
export function processEvalResult(serialized) {
|
|
319
|
+
return createEvalSerializer().processResult(serialized);
|
|
320
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture and Monitoring Module
|
|
3
|
+
* Re-exports all capture-related functionality
|
|
4
|
+
*
|
|
5
|
+
* @module cdp-skill/capture
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Console Capture
|
|
9
|
+
export { createConsoleCapture } from './console-capture.js';
|
|
10
|
+
|
|
11
|
+
// Screenshot Capture
|
|
12
|
+
export {
|
|
13
|
+
createScreenshotCapture,
|
|
14
|
+
captureViewport,
|
|
15
|
+
captureFullPage,
|
|
16
|
+
captureRegion,
|
|
17
|
+
saveScreenshot
|
|
18
|
+
} from './screenshot-capture.js';
|
|
19
|
+
|
|
20
|
+
// Network Capture
|
|
21
|
+
export { createNetworkCapture } from './network-capture.js';
|
|
22
|
+
|
|
23
|
+
// Error Aggregator
|
|
24
|
+
export {
|
|
25
|
+
createErrorAggregator,
|
|
26
|
+
aggregateErrors
|
|
27
|
+
} from './error-aggregator.js';
|
|
28
|
+
|
|
29
|
+
// PDF Capture
|
|
30
|
+
export { createPdfCapture } from './pdf-capture.js';
|
|
31
|
+
|
|
32
|
+
// Debug Capture
|
|
33
|
+
export { createDebugCapture } from './debug-capture.js';
|
|
34
|
+
|
|
35
|
+
// Eval Serializer
|
|
36
|
+
export {
|
|
37
|
+
createEvalSerializer,
|
|
38
|
+
getEvalSerializationFunction,
|
|
39
|
+
processEvalResult
|
|
40
|
+
} from './eval-serializer.js';
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Capture Module
|
|
3
|
+
* Captures network failures and HTTP errors during page interaction
|
|
4
|
+
*
|
|
5
|
+
* PUBLIC EXPORTS:
|
|
6
|
+
* - createNetworkCapture(session, config?) - Factory for network capture
|
|
7
|
+
*
|
|
8
|
+
* @module cdp-skill/capture/network-capture
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_PENDING_REQUESTS = 10000;
|
|
12
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a network error capture utility
|
|
16
|
+
* @param {import('../types.js').CDPSession} session - CDP session
|
|
17
|
+
* @param {Object} [config] - Configuration options
|
|
18
|
+
* @param {number} [config.maxPendingRequests=10000] - Maximum pending requests to track
|
|
19
|
+
* @param {number} [config.requestTimeoutMs=300000] - Stale request timeout
|
|
20
|
+
* @returns {Object} Network capture interface
|
|
21
|
+
*/
|
|
22
|
+
export function createNetworkCapture(session, config = {}) {
|
|
23
|
+
const maxPendingRequests = config.maxPendingRequests || DEFAULT_MAX_PENDING_REQUESTS;
|
|
24
|
+
const requestTimeoutMs = config.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
25
|
+
|
|
26
|
+
const requests = new Map();
|
|
27
|
+
let errors = [];
|
|
28
|
+
let httpErrors = [];
|
|
29
|
+
let capturing = false;
|
|
30
|
+
const handlers = {};
|
|
31
|
+
let captureOptions = {};
|
|
32
|
+
let cleanupIntervalId = null;
|
|
33
|
+
|
|
34
|
+
function cleanupStaleRequests() {
|
|
35
|
+
const now = Date.now() / 1000;
|
|
36
|
+
const timeoutSec = requestTimeoutMs / 1000;
|
|
37
|
+
|
|
38
|
+
for (const [requestId, request] of requests) {
|
|
39
|
+
if (now - request.timestamp > timeoutSec) {
|
|
40
|
+
requests.delete(requestId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Start capturing network errors
|
|
47
|
+
* @param {Object} [startOptions] - Capture options
|
|
48
|
+
* @param {boolean} [startOptions.captureHttpErrors=true] - Capture HTTP 4xx/5xx errors
|
|
49
|
+
* @param {number[]} [startOptions.ignoreStatusCodes=[]] - Status codes to ignore
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
async function startCapture(startOptions = {}) {
|
|
53
|
+
if (capturing) return;
|
|
54
|
+
|
|
55
|
+
captureOptions = {
|
|
56
|
+
captureHttpErrors: startOptions.captureHttpErrors !== false,
|
|
57
|
+
ignoreStatusCodes: new Set(startOptions.ignoreStatusCodes || [])
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await session.send('Network.enable');
|
|
61
|
+
|
|
62
|
+
handlers.requestWillBeSent = (params) => {
|
|
63
|
+
if (requests.size >= maxPendingRequests) {
|
|
64
|
+
const oldestKey = requests.keys().next().value;
|
|
65
|
+
requests.delete(oldestKey);
|
|
66
|
+
}
|
|
67
|
+
requests.set(params.requestId, {
|
|
68
|
+
url: params.request.url,
|
|
69
|
+
method: params.request.method,
|
|
70
|
+
timestamp: params.timestamp,
|
|
71
|
+
type: params.type
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
handlers.loadingFailed = (params) => {
|
|
76
|
+
const request = requests.get(params.requestId);
|
|
77
|
+
errors.push({
|
|
78
|
+
type: 'network-failure',
|
|
79
|
+
requestId: params.requestId,
|
|
80
|
+
url: request?.url || 'unknown',
|
|
81
|
+
method: request?.method || 'unknown',
|
|
82
|
+
resourceType: params.type,
|
|
83
|
+
errorText: params.errorText,
|
|
84
|
+
canceled: params.canceled || false,
|
|
85
|
+
blockedReason: params.blockedReason,
|
|
86
|
+
timestamp: params.timestamp
|
|
87
|
+
});
|
|
88
|
+
requests.delete(params.requestId);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
handlers.responseReceived = (params) => {
|
|
92
|
+
const status = params.response.status;
|
|
93
|
+
|
|
94
|
+
if (captureOptions.captureHttpErrors && status >= 400 &&
|
|
95
|
+
!captureOptions.ignoreStatusCodes.has(status)) {
|
|
96
|
+
const request = requests.get(params.requestId);
|
|
97
|
+
httpErrors.push({
|
|
98
|
+
type: 'http-error',
|
|
99
|
+
requestId: params.requestId,
|
|
100
|
+
url: params.response.url,
|
|
101
|
+
method: request?.method || 'unknown',
|
|
102
|
+
status,
|
|
103
|
+
statusText: params.response.statusText,
|
|
104
|
+
resourceType: params.type,
|
|
105
|
+
mimeType: params.response.mimeType,
|
|
106
|
+
timestamp: params.timestamp
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
handlers.loadingFinished = (params) => {
|
|
112
|
+
requests.delete(params.requestId);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
session.on('Network.requestWillBeSent', handlers.requestWillBeSent);
|
|
116
|
+
session.on('Network.loadingFailed', handlers.loadingFailed);
|
|
117
|
+
session.on('Network.responseReceived', handlers.responseReceived);
|
|
118
|
+
session.on('Network.loadingFinished', handlers.loadingFinished);
|
|
119
|
+
|
|
120
|
+
cleanupIntervalId = setInterval(
|
|
121
|
+
cleanupStaleRequests,
|
|
122
|
+
Math.min(requestTimeoutMs / 2, 60000)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
capturing = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Stop capturing network errors
|
|
130
|
+
* @returns {Promise<void>}
|
|
131
|
+
*/
|
|
132
|
+
async function stopCapture() {
|
|
133
|
+
if (!capturing) return;
|
|
134
|
+
|
|
135
|
+
session.off('Network.requestWillBeSent', handlers.requestWillBeSent);
|
|
136
|
+
session.off('Network.loadingFailed', handlers.loadingFailed);
|
|
137
|
+
session.off('Network.responseReceived', handlers.responseReceived);
|
|
138
|
+
session.off('Network.loadingFinished', handlers.loadingFinished);
|
|
139
|
+
|
|
140
|
+
if (cleanupIntervalId) {
|
|
141
|
+
clearInterval(cleanupIntervalId);
|
|
142
|
+
cleanupIntervalId = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
requests.clear();
|
|
146
|
+
await session.send('Network.disable');
|
|
147
|
+
capturing = false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get network failures (connection errors, blocked requests, etc.)
|
|
152
|
+
* @returns {import('../types.js').NetworkError[]}
|
|
153
|
+
*/
|
|
154
|
+
function getNetworkFailures() {
|
|
155
|
+
return [...errors];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get HTTP errors (4xx and 5xx responses)
|
|
160
|
+
* @returns {import('../types.js').NetworkError[]}
|
|
161
|
+
*/
|
|
162
|
+
function getHttpErrors() {
|
|
163
|
+
return [...httpErrors];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all errors sorted by timestamp
|
|
168
|
+
* @returns {import('../types.js').NetworkError[]}
|
|
169
|
+
*/
|
|
170
|
+
function getAllErrors() {
|
|
171
|
+
return [...errors, ...httpErrors].sort((a, b) => a.timestamp - b.timestamp);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if any errors were captured
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function hasErrors() {
|
|
179
|
+
return errors.length > 0 || httpErrors.length > 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get errors by resource type
|
|
184
|
+
* @param {string|string[]} types - Resource type(s) to filter
|
|
185
|
+
* @returns {import('../types.js').NetworkError[]}
|
|
186
|
+
*/
|
|
187
|
+
function getErrorsByType(types) {
|
|
188
|
+
const typeSet = new Set(Array.isArray(types) ? types : [types]);
|
|
189
|
+
return getAllErrors().filter(e => typeSet.has(e.resourceType));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Clear captured errors
|
|
194
|
+
*/
|
|
195
|
+
function clear() {
|
|
196
|
+
errors = [];
|
|
197
|
+
httpErrors = [];
|
|
198
|
+
requests.clear();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
startCapture,
|
|
203
|
+
stopCapture,
|
|
204
|
+
getNetworkFailures,
|
|
205
|
+
getHttpErrors,
|
|
206
|
+
getAllErrors,
|
|
207
|
+
hasErrors,
|
|
208
|
+
getErrorsByType,
|
|
209
|
+
clear
|
|
210
|
+
};
|
|
211
|
+
}
|