browser-debugger-cli 0.13.0 → 0.15.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/.claude/skills/bdg/SKILL.md +101 -187
- package/README.md +4 -4
- package/dist/commands/cdp.js +1 -0
- package/dist/commands/cleanup.js +3 -0
- package/dist/commands/console.js +5 -1
- package/dist/commands/dom/a11y.d.ts +1 -1
- package/dist/commands/dom/a11y.js +20 -20
- package/dist/commands/dom/eval.d.ts +3 -1
- package/dist/commands/dom/eval.js +8 -5
- package/dist/commands/dom/formInteraction.js +1 -1
- package/dist/commands/dom/get.js +25 -7
- package/dist/commands/dom/helpers/evalResult.d.ts +36 -0
- package/dist/commands/dom/helpers/evalResult.js +59 -0
- package/dist/commands/dom/index.js +7 -2
- package/dist/commands/dom/query.d.ts +2 -1
- package/dist/commands/dom/query.js +5 -3
- package/dist/commands/dom/screenshot.js +1 -0
- package/dist/commands/helpJson.d.ts +1 -1
- package/dist/commands/helpJson.js +4 -4
- package/dist/commands/helpTopic.js +10 -4
- package/dist/commands/network/har.js +18 -14
- package/dist/commands/network/list.js +46 -3
- package/dist/commands/optionBehaviors.d.ts +25 -2
- package/dist/commands/optionBehaviors.js +60 -42
- package/dist/commands/peek.js +3 -0
- package/dist/commands/shared/CommandRunner.js +13 -13
- package/dist/commands/shared/daemonErrorHandler.js +2 -2
- package/dist/commands/shared/dataFetcher.d.ts +4 -2
- package/dist/commands/shared/dataFetcher.js +11 -3
- package/dist/commands/shared/handleValidationError.js +3 -3
- package/dist/commands/shared/optionTypes.d.ts +15 -3
- package/dist/commands/shared/outputFile.d.ts +2 -1
- package/dist/commands/shared/outputFile.js +7 -4
- package/dist/commands/shared/startHelpers.js +3 -3
- package/dist/commands/status.js +3 -1
- package/dist/commands/stop.js +2 -1
- package/dist/connection/chromeIdentity.d.ts +8 -2
- package/dist/connection/chromeIdentity.js +85 -13
- package/dist/connection/launcher/flagsBuilder.d.ts +46 -0
- package/dist/connection/launcher/flagsBuilder.js +107 -23
- package/dist/connection/launcher.d.ts +1 -1
- package/dist/connection/launcher.js +1 -2
- package/dist/constants.d.ts +31 -5
- package/dist/constants.js +37 -5
- package/dist/daemon/SessionController.js +2 -0
- package/dist/daemon/launcher.d.ts +17 -3
- package/dist/daemon/launcher.js +37 -7
- package/dist/daemon/session/Session.d.ts +2 -1
- package/dist/daemon/session/Session.js +10 -2
- package/dist/daemon/session/TelemetryStore.d.ts +7 -0
- package/dist/daemon/session/TelemetryStore.js +6 -0
- package/dist/daemon/session/commandRegistry.js +25 -7
- package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
- package/dist/daemon/session/matchedStylesReset.js +46 -0
- package/dist/daemon/session/plugins.js +1 -0
- package/dist/daemon/session/triggeredRequests.d.ts +0 -5
- package/dist/daemon/session/triggeredRequests.js +13 -7
- package/dist/daemon.js +8742 -8315
- package/dist/errors/messages.d.ts +31 -0
- package/dist/errors/messages.js +96 -6
- package/dist/index.js +1129 -548
- package/dist/ipc/client.d.ts +6 -1
- package/dist/ipc/client.js +11 -2
- package/dist/ipc/protocol/commands.d.ts +8 -0
- package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
- package/dist/ipc/session/types.d.ts +5 -1
- package/dist/ipc/transport/index.d.ts +6 -0
- package/dist/ipc/transport/index.js +16 -1
- package/dist/program.d.ts +14 -0
- package/dist/program.js +53 -0
- package/dist/runtime/dom/elementGeometry.d.ts +23 -0
- package/dist/runtime/dom/elementGeometry.js +17 -15
- package/dist/runtime/dom/elementInfo.d.ts +13 -4
- package/dist/runtime/dom/elementInfo.js +15 -5
- package/dist/runtime/dom/evalHelpers.d.ts +24 -4
- package/dist/runtime/dom/evalHelpers.js +40 -12
- package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
- package/dist/runtime/dom/frameScopedConnection.js +2 -2
- package/dist/runtime/dom/frames.d.ts +2 -1
- package/dist/runtime/dom/frames.js +3 -1
- package/dist/runtime/dom/inspect.d.ts +17 -3
- package/dist/runtime/dom/inspect.js +40 -26
- package/dist/runtime/dom/inspectModel.d.ts +3 -3
- package/dist/runtime/dom/inspectRules.d.ts +29 -3
- package/dist/runtime/dom/inspectRules.js +205 -11
- package/dist/runtime/dom/layout.d.ts +0 -2
- package/dist/runtime/dom/layout.js +1 -2
- package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
- package/dist/runtime/dom/reactEventHelpers.js +9 -2
- package/dist/runtime/dom/targetNode.d.ts +10 -6
- package/dist/runtime/dom/targetNode.js +15 -8
- package/dist/runtime/page/emulation.js +6 -5
- package/dist/runtime/page/userAgent.d.ts +86 -2
- package/dist/runtime/page/userAgent.js +154 -33
- package/dist/session/paths.d.ts +38 -3
- package/dist/session/paths.js +154 -7
- package/dist/session/portClaims.d.ts +0 -8
- package/dist/session/portClaims.js +1 -22
- package/dist/session/sessionList.d.ts +5 -1
- package/dist/session/sessionList.js +5 -1
- package/dist/telemetry/a11y.d.ts +15 -1
- package/dist/telemetry/a11y.js +83 -0
- package/dist/telemetry/har/builder.d.ts +12 -1
- package/dist/telemetry/har/builder.js +11 -3
- package/dist/telemetry/har/sanitize.d.ts +24 -0
- package/dist/telemetry/har/sanitize.js +138 -0
- package/dist/telemetry/har/sanitizeBody.d.ts +38 -0
- package/dist/telemetry/har/sanitizeBody.js +168 -0
- package/dist/telemetry/network.d.ts +13 -16
- package/dist/telemetry/network.js +30 -52
- package/dist/telemetry/networkRetention.d.ts +83 -0
- package/dist/telemetry/networkRetention.js +117 -0
- package/dist/types.d.ts +26 -0
- package/dist/ui/OutputBuilder.d.ts +10 -0
- package/dist/ui/OutputBuilder.js +12 -0
- package/dist/ui/formatters/a11y.d.ts +5 -7
- package/dist/ui/formatters/a11y.js +7 -61
- package/dist/ui/formatters/console/chronological.js +4 -4
- package/dist/ui/formatters/console/follow.d.ts +4 -2
- package/dist/ui/formatters/console/follow.js +6 -3
- package/dist/ui/formatters/console/json.d.ts +3 -6
- package/dist/ui/formatters/console/json.js +9 -13
- package/dist/ui/formatters/console/shared.d.ts +17 -2
- package/dist/ui/formatters/console/shared.js +17 -0
- package/dist/ui/formatters/console/summarize.d.ts +2 -2
- package/dist/ui/formatters/console/summarize.js +22 -7
- package/dist/ui/formatters/console.d.ts +1 -1
- package/dist/ui/formatters/console.js +1 -5
- package/dist/ui/formatters/details.js +1 -1
- package/dist/ui/formatters/dom.d.ts +13 -4
- package/dist/ui/formatters/dom.js +25 -7
- package/dist/ui/formatters/layout.js +2 -1
- package/dist/ui/formatters/longValues.d.ts +14 -0
- package/dist/ui/formatters/longValues.js +23 -0
- package/dist/ui/formatters/networkList.d.ts +8 -2
- package/dist/ui/formatters/networkList.js +11 -2
- package/dist/ui/formatters/preview.d.ts +4 -1
- package/dist/ui/formatters/preview.js +55 -13
- package/dist/ui/formatters/sessions.d.ts +3 -2
- package/dist/ui/formatters/sessions.js +10 -3
- package/dist/ui/formatters/status.js +7 -0
- package/dist/ui/formatters/triggeredRequests.js +2 -1
- package/dist/ui/messages/chrome.d.ts +34 -7
- package/dist/ui/messages/chrome.js +81 -15
- package/dist/ui/messages/commands.d.ts +29 -8
- package/dist/ui/messages/commands.js +36 -8
- package/dist/ui/messages/networkMessages.d.ts +50 -0
- package/dist/ui/messages/networkMessages.js +66 -0
- package/dist/ui/messages/session.d.ts +8 -0
- package/dist/ui/messages/session.js +10 -0
- package/dist/utils/atomicFile.d.ts +2 -1
- package/dist/utils/atomicFile.js +5 -2
- package/dist/utils/directories.d.ts +41 -0
- package/dist/utils/directories.js +48 -0
- package/dist/utils/http.d.ts +9 -2
- package/dist/utils/http.js +4 -3
- package/dist/utils/strings.d.ts +19 -0
- package/dist/utils/strings.js +16 -0
- package/package.json +2 -2
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction in request bodies and `name=value` lists, for
|
|
3
|
+
* sanitized HAR exports (see sanitize.ts).
|
|
4
|
+
*
|
|
5
|
+
* Matching is by field name only, so it over-redacts: any primitive whose
|
|
6
|
+
* name looks like a credential (`tokenCount: 5`) is replaced too.
|
|
7
|
+
*/
|
|
8
|
+
/** Replaces a credential value */
|
|
9
|
+
export declare const REDACTED = "[redacted]";
|
|
10
|
+
/**
|
|
11
|
+
* Whether a body field, form field or query parameter name looks like it
|
|
12
|
+
* holds a credential.
|
|
13
|
+
*
|
|
14
|
+
* @param name - Field name
|
|
15
|
+
* @returns True for password, token, secret, key, session and signature names
|
|
16
|
+
*/
|
|
17
|
+
export declare function isSensitiveField(name: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Redact credential values in an `&`-separated `name=value` list (a form body,
|
|
20
|
+
* query string or fragment), leaving the other pairs byte for byte.
|
|
21
|
+
*
|
|
22
|
+
* @param text - List like `user=ann&password=hunter2`
|
|
23
|
+
* @param isSensitive - Whether a decoded name holds a credential
|
|
24
|
+
* @param replacement - Value written instead
|
|
25
|
+
* @returns List with credential values replaced
|
|
26
|
+
*/
|
|
27
|
+
export declare function redactPairs(text: string, isSensitive: (name: string) => boolean, replacement: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* Redact credential fields of a request body: multipart parts, form fields
|
|
30
|
+
* (by Content-Type or shape) and JSON fields at any depth.
|
|
31
|
+
*
|
|
32
|
+
* @param text - Body text
|
|
33
|
+
* @param mimeType - Content-Type of the body
|
|
34
|
+
* @returns Body with credential values replaced; the text unchanged when
|
|
35
|
+
* there were none
|
|
36
|
+
*/
|
|
37
|
+
export declare function redactRequestBody(text: string, mimeType: string): string;
|
|
38
|
+
//# sourceMappingURL=sanitizeBody.d.ts.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential redaction in request bodies and `name=value` lists, for
|
|
3
|
+
* sanitized HAR exports (see sanitize.ts).
|
|
4
|
+
*
|
|
5
|
+
* Matching is by field name only, so it over-redacts: any primitive whose
|
|
6
|
+
* name looks like a credential (`tokenCount: 5`) is replaced too.
|
|
7
|
+
*/
|
|
8
|
+
import { SENSITIVE_NAME_SOURCE } from '../../runtime/dom/elementInfo.js';
|
|
9
|
+
import { createLogger } from '../../ui/logging/index.js';
|
|
10
|
+
import { getErrorMessage } from '../../utils/errors.js';
|
|
11
|
+
const log = createLogger('network');
|
|
12
|
+
/** Replaces a credential value */
|
|
13
|
+
export const REDACTED = '[redacted]';
|
|
14
|
+
/**
|
|
15
|
+
* Field names holding credentials: password-like names (shared with form
|
|
16
|
+
* masking), tokens, secrets, keys, sessions, signatures and credentials
|
|
17
|
+
*/
|
|
18
|
+
const SENSITIVE_FIELD = new RegExp(`${SENSITIVE_NAME_SOURCE}|token|secret|api[-_]?key|authoriz|credential|jwt|private[-_]?key|access[-_]?key|session|signature`, 'i');
|
|
19
|
+
/** A body that is a form whatever its Content-Type: `k=v&k=v`, no whitespace, not JSON */
|
|
20
|
+
const FORM_BODY = /^[^\s=&{["]+=[^\s&]*(?:&[^\s=&]+=[^\s&]*)*$/;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a body field, form field or query parameter name looks like it
|
|
23
|
+
* holds a credential.
|
|
24
|
+
*
|
|
25
|
+
* @param name - Field name
|
|
26
|
+
* @returns True for password, token, secret, key, session and signature names
|
|
27
|
+
*/
|
|
28
|
+
export function isSensitiveField(name) {
|
|
29
|
+
return SENSITIVE_FIELD.test(name);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Redact credential values in an `&`-separated `name=value` list (a form body,
|
|
33
|
+
* query string or fragment), leaving the other pairs byte for byte.
|
|
34
|
+
*
|
|
35
|
+
* @param text - List like `user=ann&password=hunter2`
|
|
36
|
+
* @param isSensitive - Whether a decoded name holds a credential
|
|
37
|
+
* @param replacement - Value written instead
|
|
38
|
+
* @returns List with credential values replaced
|
|
39
|
+
*/
|
|
40
|
+
export function redactPairs(text, isSensitive, replacement) {
|
|
41
|
+
return text
|
|
42
|
+
.split('&')
|
|
43
|
+
.map((pair) => {
|
|
44
|
+
const eq = pair.indexOf('=');
|
|
45
|
+
if (eq === -1 || !isSensitive(decodeName(pair.slice(0, eq))))
|
|
46
|
+
return pair;
|
|
47
|
+
return `${pair.slice(0, eq)}=${replacement}`;
|
|
48
|
+
})
|
|
49
|
+
.join('&');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Redact credential fields of a request body: multipart parts, form fields
|
|
53
|
+
* (by Content-Type or shape) and JSON fields at any depth.
|
|
54
|
+
*
|
|
55
|
+
* @param text - Body text
|
|
56
|
+
* @param mimeType - Content-Type of the body
|
|
57
|
+
* @returns Body with credential values replaced; the text unchanged when
|
|
58
|
+
* there were none
|
|
59
|
+
*/
|
|
60
|
+
export function redactRequestBody(text, mimeType) {
|
|
61
|
+
if (/multipart\/form-data/i.test(mimeType))
|
|
62
|
+
return redactMultipart(text, mimeType);
|
|
63
|
+
if (/x-www-form-urlencoded/i.test(mimeType) || FORM_BODY.test(text)) {
|
|
64
|
+
return redactPairs(text, isSensitiveField, REDACTED);
|
|
65
|
+
}
|
|
66
|
+
return redactJsonBody(text);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode a form or query name.
|
|
70
|
+
*
|
|
71
|
+
* @param name - Encoded name
|
|
72
|
+
* @returns Decoded name, or the raw name when it is not valid encoding
|
|
73
|
+
*/
|
|
74
|
+
function decodeName(name) {
|
|
75
|
+
try {
|
|
76
|
+
return decodeURIComponent(name.replace(/\+/g, ' '));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
log.debug(`Field name not decodable: ${getErrorMessage(error)}`);
|
|
80
|
+
return name;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Redact the values of credential parts of a multipart body, leaving the
|
|
85
|
+
* other parts byte for byte.
|
|
86
|
+
*
|
|
87
|
+
* @param text - Body text
|
|
88
|
+
* @param mimeType - Content-Type with the boundary
|
|
89
|
+
* @returns Body with credential part values replaced
|
|
90
|
+
*/
|
|
91
|
+
function redactMultipart(text, mimeType) {
|
|
92
|
+
const match = /boundary=(?:"([^"]+)"|([^;\s]+))/i.exec(mimeType);
|
|
93
|
+
const boundary = match?.[1] ?? match?.[2];
|
|
94
|
+
if (!boundary)
|
|
95
|
+
return text;
|
|
96
|
+
const delimiter = `--${boundary}`;
|
|
97
|
+
return text.split(delimiter).map(redactPart).join(delimiter);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Redact the value of one multipart part if its name holds a credential.
|
|
101
|
+
*
|
|
102
|
+
* @param part - Text between two boundary delimiters
|
|
103
|
+
* @returns The part, or the part with its value replaced
|
|
104
|
+
*/
|
|
105
|
+
function redactPart(part) {
|
|
106
|
+
const headerEnd = part.indexOf('\r\n\r\n');
|
|
107
|
+
if (headerEnd === -1)
|
|
108
|
+
return part;
|
|
109
|
+
const name = /;\s*name="([^"]*)"/i.exec(part.slice(0, headerEnd))?.[1];
|
|
110
|
+
if (name === undefined || !isSensitiveField(name))
|
|
111
|
+
return part;
|
|
112
|
+
const valueEnd = part.endsWith('\r\n') ? part.length - 2 : part.length;
|
|
113
|
+
return `${part.slice(0, headerEnd + 4)}${REDACTED}${part.slice(valueEnd)}`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Redact credential fields of a JSON body.
|
|
117
|
+
*
|
|
118
|
+
* @param text - Body text
|
|
119
|
+
* @returns Re-serialized JSON when a field was redacted, the text unchanged
|
|
120
|
+
* when none was or it is not JSON, and {@link REDACTED} for JSON too deep
|
|
121
|
+
* to walk
|
|
122
|
+
*/
|
|
123
|
+
function redactJsonBody(text) {
|
|
124
|
+
if (!/^\s*[[{]/.test(text))
|
|
125
|
+
return text;
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = JSON.parse(text);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
log.debug(`Request body not JSON: ${getErrorMessage(error)}`);
|
|
132
|
+
return error instanceof SyntaxError ? text : REDACTED;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const hits = { count: 0 };
|
|
136
|
+
const redacted = redactJson(parsed, false, hits);
|
|
137
|
+
return hits.count > 0 ? JSON.stringify(redacted) : text;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
log.debug(`Request body redacted whole: ${getErrorMessage(error)}`);
|
|
141
|
+
return REDACTED;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Copy of parsed JSON with the primitives under credential names replaced.
|
|
146
|
+
* Objects and arrays keep their structure; every string, number and boolean
|
|
147
|
+
* inside a credential-named field is replaced, at any depth.
|
|
148
|
+
*
|
|
149
|
+
* @param value - Parsed JSON value
|
|
150
|
+
* @param sensitive - Whether an enclosing field name holds a credential
|
|
151
|
+
* @param hits - Counter of replaced values
|
|
152
|
+
* @returns Redacted copy
|
|
153
|
+
*/
|
|
154
|
+
function redactJson(value, sensitive, hits) {
|
|
155
|
+
if (Array.isArray(value))
|
|
156
|
+
return value.map((item) => redactJson(item, sensitive, hits));
|
|
157
|
+
if (value !== null && typeof value === 'object') {
|
|
158
|
+
return Object.fromEntries(Object.entries(value).map(([key, field]) => [
|
|
159
|
+
key,
|
|
160
|
+
redactJson(field, sensitive || isSensitiveField(key), hits),
|
|
161
|
+
]));
|
|
162
|
+
}
|
|
163
|
+
if (!sensitive || !['string', 'number', 'boolean'].includes(typeof value))
|
|
164
|
+
return value;
|
|
165
|
+
hits.count++;
|
|
166
|
+
return REDACTED;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=sanitizeBody.js.map
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import type { CDPConnection } from '../connection/cdp.js';
|
|
2
2
|
import type { NetworkRequest, WebSocketConnection, CleanupFunction } from '../types.js';
|
|
3
|
-
|
|
4
|
-
* Placeholder stored instead of a response body that was not fetched.
|
|
5
|
-
*
|
|
6
|
-
* @param reason - Why the body was skipped
|
|
7
|
-
* @returns Placeholder text shown by `bdg details`
|
|
8
|
-
*/
|
|
9
|
-
export declare function skippedBodyPlaceholder(reason: string): string;
|
|
10
|
-
/**
|
|
11
|
-
* Extract the reason from a skipped-body placeholder.
|
|
12
|
-
*
|
|
13
|
-
* @param body - Stored response body
|
|
14
|
-
* @returns Reason if `body` is a placeholder, otherwise undefined
|
|
15
|
-
*/
|
|
16
|
-
export declare function skippedBodyReason(body: string | undefined): string | undefined;
|
|
3
|
+
import { type NetworkEvictions } from './networkRetention.js';
|
|
17
4
|
/**
|
|
18
5
|
* A request that has started but not finished or failed yet.
|
|
19
6
|
*/
|
|
@@ -41,6 +28,12 @@ export interface NetworkCollectionOptions {
|
|
|
41
28
|
networkInclude?: string[];
|
|
42
29
|
networkExclude?: string[];
|
|
43
30
|
maxBodySize?: number;
|
|
31
|
+
/** Finished requests kept at most; past it the oldest are dropped (default {@link MAX_NETWORK_REQUESTS}) */
|
|
32
|
+
maxRequests?: number;
|
|
33
|
+
/** Total size of stored response bodies; past it the oldest are evicted (default {@link MAX_TOTAL_BODY_BYTES}) */
|
|
34
|
+
maxTotalBodyBytes?: number;
|
|
35
|
+
/** Counters of dropped requests and evicted bodies; otherwise the collector keeps private ones */
|
|
36
|
+
evictions?: NetworkEvictions | undefined;
|
|
44
37
|
getCurrentNavigationId?: (() => number) | undefined;
|
|
45
38
|
}
|
|
46
39
|
/**
|
|
@@ -56,8 +49,12 @@ export interface NetworkCollectionOptions {
|
|
|
56
49
|
*
|
|
57
50
|
* @remarks
|
|
58
51
|
* - Chrome buffer limits: 50MB total, 10MB per resource, 1MB POST data (with fallback)
|
|
59
|
-
* -
|
|
60
|
-
*
|
|
52
|
+
* - The newest 10,000 finished requests are kept: past that the oldest finished
|
|
53
|
+
* ones are dropped (counted in `evictions.requestsDropped`); requests in flight
|
|
54
|
+
* are tracked separately and never dropped mid-flight
|
|
55
|
+
* - Stored response bodies total at most 100MB: past that the oldest bodies are
|
|
56
|
+
* replaced by a placeholder (counted in `evictions.bodiesEvicted`), their
|
|
57
|
+
* request metadata stays
|
|
61
58
|
* - Response bodies are automatically skipped for images, fonts, CSS, and source maps (see DEFAULT_SKIP_BODY_PATTERNS)
|
|
62
59
|
* - Response bodies larger than 5MB are skipped with a placeholder message
|
|
63
60
|
* - By default, common tracking/analytics domains are filtered out (use includeAll to disable)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { CDPHandlerRegistry } from '../connection/handlers.js';
|
|
2
2
|
import { TypedCDPConnection } from '../connection/typed-cdp.js';
|
|
3
|
-
import { MAX_NETWORK_REQUESTS, MAX_RESPONSE_SIZE, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, } from '../constants.js';
|
|
3
|
+
import { MAX_NETWORK_REQUESTS, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, } from '../constants.js';
|
|
4
4
|
import { attachChildTargets } from './attachedTargets.js';
|
|
5
5
|
import { createLogger } from '../ui/logging/index.js';
|
|
6
6
|
import { getErrorMessage } from '../utils/errors.js';
|
|
7
7
|
import { filterDefined } from '../utils/objects.js';
|
|
8
8
|
import { shouldExcludeDomain, shouldExcludeUrl, shouldFetchBodyWithReason } from './filters.js';
|
|
9
9
|
import { ExtraInfoTracker } from './networkExtraInfo.js';
|
|
10
|
+
import { RequestRetention, skippedBodyPlaceholder, } from './networkRetention.js';
|
|
10
11
|
const log = createLogger('network');
|
|
11
12
|
/**
|
|
12
13
|
* Check if a request should be filtered out based on domain and URL patterns.
|
|
@@ -21,27 +22,26 @@ function shouldFilterRequest(url, includeAll, networkInclude, networkExclude) {
|
|
|
21
22
|
return false;
|
|
22
23
|
}
|
|
23
24
|
/**
|
|
24
|
-
* Fetch response body for a request with cancellation support
|
|
25
|
+
* Fetch response body for a request with cancellation support: a body that
|
|
26
|
+
* arrives after its fetch was cancelled (removed from `pendingFetches`: the
|
|
27
|
+
* collector stopped, or the request was dropped) is discarded.
|
|
25
28
|
*
|
|
26
29
|
* @param cdp - CDP connection instance
|
|
27
30
|
* @param requestId - Request ID to fetch body for
|
|
28
|
-
* @param request - Network request
|
|
31
|
+
* @param request - Network request the body belongs to
|
|
29
32
|
* @param pendingFetches - Set to track pending fetch operations for cleanup
|
|
33
|
+
* @param retention - Stores the body within the session's body budget
|
|
34
|
+
* @param sessionId - Session of the iframe or worker that made the request
|
|
30
35
|
*/
|
|
31
|
-
function fetchResponseBody(cdp, requestId, request, pendingFetches, sessionId) {
|
|
36
|
+
function fetchResponseBody(cdp, requestId, request, pendingFetches, retention, sessionId) {
|
|
32
37
|
pendingFetches.add(requestId);
|
|
33
38
|
void cdp
|
|
34
39
|
.send('Network.getResponseBody', { requestId }, sessionId)
|
|
35
40
|
.then((response) => {
|
|
36
41
|
if (!pendingFetches.has(requestId))
|
|
37
42
|
return;
|
|
38
|
-
const
|
|
39
|
-
request
|
|
40
|
-
if (typedResponse.base64Encoded)
|
|
41
|
-
request.responseBodyBase64 = true;
|
|
42
|
-
if (typedResponse.body) {
|
|
43
|
-
request.decodedBodyLength = Buffer.byteLength(typedResponse.body, typedResponse.base64Encoded ? 'base64' : 'utf-8');
|
|
44
|
-
}
|
|
43
|
+
const { body, base64Encoded } = response;
|
|
44
|
+
retention.storeBody(request, body, base64Encoded);
|
|
45
45
|
})
|
|
46
46
|
.catch((error) => {
|
|
47
47
|
log.debug(`Failed to fetch response body for request ${requestId}: ${getErrorMessage(error)}`);
|
|
@@ -83,25 +83,6 @@ function createNetworkRequest(params, getCurrentNavigationId) {
|
|
|
83
83
|
...(params.type !== undefined && { resourceType: params.type }),
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
-
const SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
|
|
87
|
-
/**
|
|
88
|
-
* Placeholder stored instead of a response body that was not fetched.
|
|
89
|
-
*
|
|
90
|
-
* @param reason - Why the body was skipped
|
|
91
|
-
* @returns Placeholder text shown by `bdg details`
|
|
92
|
-
*/
|
|
93
|
-
export function skippedBodyPlaceholder(reason) {
|
|
94
|
-
return `[SKIPPED: ${reason}]`;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Extract the reason from a skipped-body placeholder.
|
|
98
|
-
*
|
|
99
|
-
* @param body - Stored response body
|
|
100
|
-
* @returns Reason if `body` is a placeholder, otherwise undefined
|
|
101
|
-
*/
|
|
102
|
-
export function skippedBodyReason(body) {
|
|
103
|
-
return body === undefined ? undefined : SKIPPED_BODY_PATTERN.exec(body)?.[1];
|
|
104
|
-
}
|
|
105
86
|
/**
|
|
106
87
|
* Copy response fields (status, headers, timing, connection) onto a request.
|
|
107
88
|
*
|
|
@@ -272,21 +253,31 @@ async function collectChildTargetNetwork(cdp) {
|
|
|
272
253
|
*
|
|
273
254
|
* @remarks
|
|
274
255
|
* - Chrome buffer limits: 50MB total, 10MB per resource, 1MB POST data (with fallback)
|
|
275
|
-
* -
|
|
276
|
-
*
|
|
256
|
+
* - The newest 10,000 finished requests are kept: past that the oldest finished
|
|
257
|
+
* ones are dropped (counted in `evictions.requestsDropped`); requests in flight
|
|
258
|
+
* are tracked separately and never dropped mid-flight
|
|
259
|
+
* - Stored response bodies total at most 100MB: past that the oldest bodies are
|
|
260
|
+
* replaced by a placeholder (counted in `evictions.bodiesEvicted`), their
|
|
261
|
+
* request metadata stays
|
|
277
262
|
* - Response bodies are automatically skipped for images, fonts, CSS, and source maps (see DEFAULT_SKIP_BODY_PATTERNS)
|
|
278
263
|
* - Response bodies larger than 5MB are skipped with a placeholder message
|
|
279
264
|
* - By default, common tracking/analytics domains are filtered out (use includeAll to disable)
|
|
280
265
|
* - Pattern precedence: include patterns always trump exclude patterns
|
|
281
266
|
*/
|
|
282
267
|
export async function startNetworkCollection(cdp, requests, options = {}) {
|
|
283
|
-
const { includeAll = false, fetchAllBodies = false, fetchBodiesInclude = [], fetchBodiesExclude = [], networkInclude = [], networkExclude = [], maxBodySize = MAX_RESPONSE_SIZE, getCurrentNavigationId, } = options;
|
|
268
|
+
const { includeAll = false, fetchAllBodies = false, fetchBodiesInclude = [], fetchBodiesExclude = [], networkInclude = [], networkExclude = [], maxBodySize = MAX_RESPONSE_SIZE, maxRequests = MAX_NETWORK_REQUESTS, maxTotalBodyBytes = MAX_TOTAL_BODY_BYTES, evictions = { requestsDropped: 0, bodiesEvicted: 0 }, getCurrentNavigationId, } = options;
|
|
284
269
|
const requestMap = options.pendingRequests ?? new Map();
|
|
285
270
|
const pendingFetches = new Set();
|
|
286
271
|
const redirectHops = new Map();
|
|
287
272
|
const extraInfo = new ExtraInfoTracker((requestId) => requestMap.get(requestId)?.request);
|
|
288
273
|
const registry = new CDPHandlerRegistry();
|
|
289
274
|
const typed = new TypedCDPConnection(cdp);
|
|
275
|
+
const retention = new RequestRetention(requests, { maxRequests, maxTotalBodyBytes }, evictions);
|
|
276
|
+
const record = (request) => {
|
|
277
|
+
const dropped = retention.add(request);
|
|
278
|
+
if (dropped)
|
|
279
|
+
pendingFetches.delete(dropped.requestId);
|
|
280
|
+
};
|
|
290
281
|
let bodiesFetched = 0;
|
|
291
282
|
let bodiesSkipped = 0;
|
|
292
283
|
try {
|
|
@@ -305,11 +296,9 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
|
|
|
305
296
|
const entry = requestMap.get(requestId);
|
|
306
297
|
if (!entry)
|
|
307
298
|
return;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
extraInfo.complete(requestId, entry.request);
|
|
312
|
-
}
|
|
299
|
+
applyFailure(entry.request, failure);
|
|
300
|
+
record(entry.request);
|
|
301
|
+
extraInfo.complete(requestId, entry.request);
|
|
313
302
|
requestMap.delete(requestId);
|
|
314
303
|
redirectHops.delete(requestId);
|
|
315
304
|
};
|
|
@@ -349,16 +338,11 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
|
|
|
349
338
|
const hop = completeRedirectHop(previous.request, params, redirectHops);
|
|
350
339
|
extraInfo.applyResponse(params.requestId, hop);
|
|
351
340
|
extraInfo.recordRedirectHop(params.requestId, hop);
|
|
352
|
-
|
|
353
|
-
requests.push(hop);
|
|
341
|
+
record(hop);
|
|
354
342
|
}
|
|
355
343
|
if (shouldFilterRequest(params.request.url, includeAll, networkInclude, networkExclude)) {
|
|
356
344
|
return;
|
|
357
345
|
}
|
|
358
|
-
if (requestMap.size >= MAX_NETWORK_REQUESTS) {
|
|
359
|
-
log.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS}), dropping new requests`);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
346
|
const request = createNetworkRequest(params, getCurrentNavigationId);
|
|
363
347
|
extraInfo.applyRequest(params.requestId, request);
|
|
364
348
|
if (params.type === 'Document' && params.loaderId) {
|
|
@@ -399,12 +383,6 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
|
|
|
399
383
|
const entry = requestMap.get(params.requestId);
|
|
400
384
|
if (!entry)
|
|
401
385
|
return;
|
|
402
|
-
if (requests.length >= MAX_NETWORK_REQUESTS) {
|
|
403
|
-
log.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS})`);
|
|
404
|
-
requestMap.delete(params.requestId);
|
|
405
|
-
redirectHops.delete(params.requestId);
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
386
|
const request = entry.request;
|
|
409
387
|
if (params.encodedDataLength !== undefined) {
|
|
410
388
|
request.encodedDataLength = params.encodedDataLength;
|
|
@@ -419,13 +397,13 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
|
|
|
419
397
|
});
|
|
420
398
|
if (decision.should) {
|
|
421
399
|
bodiesFetched++;
|
|
422
|
-
fetchResponseBody(cdp, params.requestId, request, pendingFetches, entry.sessionId);
|
|
400
|
+
fetchResponseBody(cdp, params.requestId, request, pendingFetches, retention, entry.sessionId);
|
|
423
401
|
}
|
|
424
402
|
else {
|
|
425
403
|
bodiesSkipped++;
|
|
426
404
|
request.responseBody = skippedBodyPlaceholder(decision.reason ?? 'not captured');
|
|
427
405
|
}
|
|
428
|
-
|
|
406
|
+
record(request);
|
|
429
407
|
extraInfo.complete(params.requestId, request);
|
|
430
408
|
requestMap.delete(params.requestId);
|
|
431
409
|
redirectHops.delete(params.requestId);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the network collector keeps of a long session: the newest finished
|
|
3
|
+
* requests up to a cap, and the newest response bodies up to a total size.
|
|
4
|
+
*/
|
|
5
|
+
import type { NetworkRequest } from '../types.js';
|
|
6
|
+
/** Counts of what the session let go at its limits */
|
|
7
|
+
export interface NetworkEvictions {
|
|
8
|
+
/** Finished requests dropped at the request cap, oldest first */
|
|
9
|
+
requestsDropped: number;
|
|
10
|
+
/** Response bodies replaced by a placeholder at the total body budget, oldest first */
|
|
11
|
+
bodiesEvicted: number;
|
|
12
|
+
}
|
|
13
|
+
/** Limits of {@link RequestRetention} */
|
|
14
|
+
export interface RetentionLimits {
|
|
15
|
+
/** Finished requests kept at most */
|
|
16
|
+
maxRequests: number;
|
|
17
|
+
/** Total size of the stored response bodies (bytes) */
|
|
18
|
+
maxTotalBodyBytes: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Keeps finished requests, oldest first, dropping the oldest past the cap
|
|
22
|
+
* (requests in flight are never in the list, so never dropped), and tracks
|
|
23
|
+
* the size of their stored bodies, replacing the oldest bodies past the
|
|
24
|
+
* budget with a placeholder that says why.
|
|
25
|
+
*
|
|
26
|
+
* Stored bodies are kept in a Map, which iterates in insertion order: its
|
|
27
|
+
* first entry is the oldest body, and a dropped request's body is removed in
|
|
28
|
+
* O(1). Dropping from the front of the list is `Array.shift`, which V8 does
|
|
29
|
+
* without copying for arrays of this size.
|
|
30
|
+
*/
|
|
31
|
+
export declare class RequestRetention {
|
|
32
|
+
private readonly requests;
|
|
33
|
+
private readonly limits;
|
|
34
|
+
private readonly evictions;
|
|
35
|
+
private readonly bodySizes;
|
|
36
|
+
private storedBodyBytes;
|
|
37
|
+
/**
|
|
38
|
+
* @param requests - Finished requests, oldest first (updated in place)
|
|
39
|
+
* @param limits - Request cap and body budget
|
|
40
|
+
* @param evictions - Counters updated on each drop and eviction
|
|
41
|
+
*/
|
|
42
|
+
constructor(requests: NetworkRequest[], limits: RetentionLimits, evictions: NetworkEvictions);
|
|
43
|
+
/**
|
|
44
|
+
* Add a finished request, dropping the oldest one past the cap.
|
|
45
|
+
*
|
|
46
|
+
* @param request - Finished request
|
|
47
|
+
* @returns The dropped request, if one was
|
|
48
|
+
*/
|
|
49
|
+
add(request: NetworkRequest): NetworkRequest | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Store a fetched response body on a kept request, then evict the oldest
|
|
52
|
+
* bodies until the total fits the budget (a body larger than the whole
|
|
53
|
+
* budget is evicted itself). Storing a body again replaces the earlier one.
|
|
54
|
+
*
|
|
55
|
+
* @param request - Request the body belongs to
|
|
56
|
+
* @param body - Body as Chrome returned it
|
|
57
|
+
* @param base64Encoded - Whether `body` is base64
|
|
58
|
+
*/
|
|
59
|
+
storeBody(request: NetworkRequest, body: string, base64Encoded: boolean): void;
|
|
60
|
+
/** Evict the oldest stored bodies while their total is over the budget. */
|
|
61
|
+
private enforceBodyBudget;
|
|
62
|
+
/**
|
|
63
|
+
* Stop counting a request's stored body.
|
|
64
|
+
*
|
|
65
|
+
* @param request - Request whose body is let go
|
|
66
|
+
*/
|
|
67
|
+
private forgetBody;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Placeholder stored instead of a response body that was not fetched.
|
|
71
|
+
*
|
|
72
|
+
* @param reason - Why the body was skipped
|
|
73
|
+
* @returns Placeholder text shown by `bdg details`
|
|
74
|
+
*/
|
|
75
|
+
export declare function skippedBodyPlaceholder(reason: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Extract the reason from a skipped-body placeholder.
|
|
78
|
+
*
|
|
79
|
+
* @param body - Stored response body
|
|
80
|
+
* @returns Reason if `body` is a placeholder, otherwise undefined
|
|
81
|
+
*/
|
|
82
|
+
export declare function skippedBodyReason(body: string | undefined): string | undefined;
|
|
83
|
+
//# sourceMappingURL=networkRetention.d.ts.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the network collector keeps of a long session: the newest finished
|
|
3
|
+
* requests up to a cap, and the newest response bodies up to a total size.
|
|
4
|
+
*/
|
|
5
|
+
import { bodyEvictedReason } from '../ui/messages/networkMessages.js';
|
|
6
|
+
/**
|
|
7
|
+
* Keeps finished requests, oldest first, dropping the oldest past the cap
|
|
8
|
+
* (requests in flight are never in the list, so never dropped), and tracks
|
|
9
|
+
* the size of their stored bodies, replacing the oldest bodies past the
|
|
10
|
+
* budget with a placeholder that says why.
|
|
11
|
+
*
|
|
12
|
+
* Stored bodies are kept in a Map, which iterates in insertion order: its
|
|
13
|
+
* first entry is the oldest body, and a dropped request's body is removed in
|
|
14
|
+
* O(1). Dropping from the front of the list is `Array.shift`, which V8 does
|
|
15
|
+
* without copying for arrays of this size.
|
|
16
|
+
*/
|
|
17
|
+
export class RequestRetention {
|
|
18
|
+
requests;
|
|
19
|
+
limits;
|
|
20
|
+
evictions;
|
|
21
|
+
bodySizes = new Map();
|
|
22
|
+
storedBodyBytes = 0;
|
|
23
|
+
/**
|
|
24
|
+
* @param requests - Finished requests, oldest first (updated in place)
|
|
25
|
+
* @param limits - Request cap and body budget
|
|
26
|
+
* @param evictions - Counters updated on each drop and eviction
|
|
27
|
+
*/
|
|
28
|
+
constructor(requests, limits, evictions) {
|
|
29
|
+
this.requests = requests;
|
|
30
|
+
this.limits = limits;
|
|
31
|
+
this.evictions = evictions;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Add a finished request, dropping the oldest one past the cap.
|
|
35
|
+
*
|
|
36
|
+
* @param request - Finished request
|
|
37
|
+
* @returns The dropped request, if one was
|
|
38
|
+
*/
|
|
39
|
+
add(request) {
|
|
40
|
+
this.requests.push(request);
|
|
41
|
+
if (this.requests.length <= this.limits.maxRequests)
|
|
42
|
+
return undefined;
|
|
43
|
+
const dropped = this.requests.shift();
|
|
44
|
+
if (!dropped)
|
|
45
|
+
return undefined;
|
|
46
|
+
this.evictions.requestsDropped++;
|
|
47
|
+
this.forgetBody(dropped);
|
|
48
|
+
return dropped;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Store a fetched response body on a kept request, then evict the oldest
|
|
52
|
+
* bodies until the total fits the budget (a body larger than the whole
|
|
53
|
+
* budget is evicted itself). Storing a body again replaces the earlier one.
|
|
54
|
+
*
|
|
55
|
+
* @param request - Request the body belongs to
|
|
56
|
+
* @param body - Body as Chrome returned it
|
|
57
|
+
* @param base64Encoded - Whether `body` is base64
|
|
58
|
+
*/
|
|
59
|
+
storeBody(request, body, base64Encoded) {
|
|
60
|
+
this.forgetBody(request);
|
|
61
|
+
request.responseBody = body;
|
|
62
|
+
if (base64Encoded)
|
|
63
|
+
request.responseBodyBase64 = true;
|
|
64
|
+
if (body) {
|
|
65
|
+
request.decodedBodyLength = Buffer.byteLength(body, base64Encoded ? 'base64' : 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
const size = Buffer.byteLength(body);
|
|
68
|
+
this.bodySizes.set(request, size);
|
|
69
|
+
this.storedBodyBytes += size;
|
|
70
|
+
this.enforceBodyBudget();
|
|
71
|
+
}
|
|
72
|
+
/** Evict the oldest stored bodies while their total is over the budget. */
|
|
73
|
+
enforceBodyBudget() {
|
|
74
|
+
while (this.storedBodyBytes > this.limits.maxTotalBodyBytes) {
|
|
75
|
+
const oldest = this.bodySizes.keys().next();
|
|
76
|
+
if (oldest.done)
|
|
77
|
+
return;
|
|
78
|
+
const request = oldest.value;
|
|
79
|
+
this.forgetBody(request);
|
|
80
|
+
request.responseBody = skippedBodyPlaceholder(bodyEvictedReason(this.limits.maxTotalBodyBytes));
|
|
81
|
+
delete request.responseBodyBase64;
|
|
82
|
+
this.evictions.bodiesEvicted++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Stop counting a request's stored body.
|
|
87
|
+
*
|
|
88
|
+
* @param request - Request whose body is let go
|
|
89
|
+
*/
|
|
90
|
+
forgetBody(request) {
|
|
91
|
+
const size = this.bodySizes.get(request);
|
|
92
|
+
if (size === undefined)
|
|
93
|
+
return;
|
|
94
|
+
this.bodySizes.delete(request);
|
|
95
|
+
this.storedBodyBytes -= size;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
|
|
99
|
+
/**
|
|
100
|
+
* Placeholder stored instead of a response body that was not fetched.
|
|
101
|
+
*
|
|
102
|
+
* @param reason - Why the body was skipped
|
|
103
|
+
* @returns Placeholder text shown by `bdg details`
|
|
104
|
+
*/
|
|
105
|
+
export function skippedBodyPlaceholder(reason) {
|
|
106
|
+
return `[SKIPPED: ${reason}]`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Extract the reason from a skipped-body placeholder.
|
|
110
|
+
*
|
|
111
|
+
* @param body - Stored response body
|
|
112
|
+
* @returns Reason if `body` is a placeholder, otherwise undefined
|
|
113
|
+
*/
|
|
114
|
+
export function skippedBodyReason(body) {
|
|
115
|
+
return body === undefined ? undefined : SKIPPED_BODY_PATTERN.exec(body)?.[1];
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=networkRetention.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -204,6 +204,8 @@ export interface ConsoleMessage {
|
|
|
204
204
|
stackTrace?: StackFrame[];
|
|
205
205
|
/** Browser subsystem of a browser message (`network`, `security`, ...); absent for page console calls */
|
|
206
206
|
source?: Protocol.Log.LogEntry['source'];
|
|
207
|
+
/** Original length of `text` when JSON output cut it (`--full` keeps it whole) */
|
|
208
|
+
truncatedFrom?: number;
|
|
207
209
|
}
|
|
208
210
|
/**
|
|
209
211
|
* Console message level categories for user-facing filtering.
|
|
@@ -234,6 +236,10 @@ export interface BdgOutput {
|
|
|
234
236
|
console: number;
|
|
235
237
|
/** Console messages dropped at the limit, oldest first (indices start after them) */
|
|
236
238
|
consoleDropped?: number;
|
|
239
|
+
/** Finished network requests dropped at the request cap, oldest first */
|
|
240
|
+
networkDropped?: number;
|
|
241
|
+
/** Response bodies evicted at the total body budget, oldest first */
|
|
242
|
+
networkBodiesEvicted?: number;
|
|
237
243
|
};
|
|
238
244
|
error?: string;
|
|
239
245
|
partial?: boolean;
|
|
@@ -286,6 +292,24 @@ export interface A11yTree {
|
|
|
286
292
|
/** Total node count */
|
|
287
293
|
count: number;
|
|
288
294
|
}
|
|
295
|
+
/** An accessibility node as `dom a11y tree` lists it */
|
|
296
|
+
export type ListedA11yNode = Omit<A11yNode, 'childIds'> & {
|
|
297
|
+
/** Indentation level (0 = root; left-out wrappers add none) */
|
|
298
|
+
depth: number;
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* The part of an accessibility tree `dom a11y tree` lists (`--limit`, `--depth`).
|
|
302
|
+
*/
|
|
303
|
+
export interface ListedA11yTree {
|
|
304
|
+
/** Listed nodes, depth-first from the root */
|
|
305
|
+
nodes: ListedA11yNode[];
|
|
306
|
+
/** Nodes in the whole tree (listed + omitted + skipped) */
|
|
307
|
+
count: number;
|
|
308
|
+
/** Nodes left out by `--limit` or `--depth` */
|
|
309
|
+
omitted?: number;
|
|
310
|
+
/** Nodes never listed: text boxes, blank or repeated text, nameless layout wrappers */
|
|
311
|
+
skipped?: number;
|
|
312
|
+
}
|
|
289
313
|
/**
|
|
290
314
|
* Query pattern for searching accessibility tree.
|
|
291
315
|
*
|
|
@@ -463,6 +487,8 @@ export interface DomGetResult {
|
|
|
463
487
|
attributes?: Record<string, unknown>;
|
|
464
488
|
classes?: string[];
|
|
465
489
|
outerHTML?: string;
|
|
490
|
+
/** Original length of `outerHTML` when JSON output cut it (`--full` keeps it whole) */
|
|
491
|
+
truncatedFrom?: number;
|
|
466
492
|
}>;
|
|
467
493
|
}
|
|
468
494
|
/**
|