@mcp-b/chrome-devtools-mcp 2.3.0 → 2.3.1-beta.20260528050333
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/package.json +1 -1
- package/build/src/DevToolsConnectionAdapter.js +0 -70
- package/build/src/DevtoolsUtils.js +0 -290
- package/build/src/McpContext.js +0 -687
- package/build/src/McpPage.js +0 -95
- package/build/src/McpResponse.js +0 -588
- package/build/src/Mutex.js +0 -37
- package/build/src/PageCollector.js +0 -308
- package/build/src/SlimMcpResponse.js +0 -18
- package/build/src/WaitForHelper.js +0 -135
- package/build/src/bin/chrome-devtools-cli-options.js +0 -651
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +0 -317
- package/build/src/bin/chrome-devtools-mcp-main.js +0 -35
- package/build/src/bin/chrome-devtools-mcp.js +0 -21
- package/build/src/bin/chrome-devtools.js +0 -185
- package/build/src/bin/cliDefinitions.js +0 -615
- package/build/src/browser.js +0 -198
- package/build/src/daemon/client.js +0 -152
- package/build/src/daemon/daemon.js +0 -206
- package/build/src/daemon/types.js +0 -6
- package/build/src/daemon/utils.js +0 -108
- package/build/src/formatters/ConsoleFormatter.js +0 -234
- package/build/src/formatters/IssueFormatter.js +0 -192
- package/build/src/formatters/NetworkFormatter.js +0 -215
- package/build/src/formatters/SnapshotFormatter.js +0 -131
- package/build/src/index.js +0 -202
- package/build/src/issue-descriptions.js +0 -39
- package/build/src/logger.js +0 -36
- package/build/src/polyfill.js +0 -7
- package/build/src/telemetry/ClearcutLogger.js +0 -102
- package/build/src/telemetry/WatchdogClient.js +0 -60
- package/build/src/telemetry/flagUtils.js +0 -45
- package/build/src/telemetry/metricUtils.js +0 -14
- package/build/src/telemetry/persistence.js +0 -53
- package/build/src/telemetry/types.js +0 -33
- package/build/src/telemetry/watchdog/ClearcutSender.js +0 -203
- package/build/src/telemetry/watchdog/main.js +0 -127
- package/build/src/third_party/devtools-formatter-worker.js +0 -7
- package/build/src/third_party/index.js +0 -26
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +0 -54183
- package/build/src/tools/ToolDefinition.js +0 -72
- package/build/src/tools/categories.js +0 -24
- package/build/src/tools/console.js +0 -85
- package/build/src/tools/emulation.js +0 -55
- package/build/src/tools/extensions.js +0 -96
- package/build/src/tools/input.js +0 -368
- package/build/src/tools/lighthouse.js +0 -123
- package/build/src/tools/memory.js +0 -28
- package/build/src/tools/network.js +0 -120
- package/build/src/tools/pages.js +0 -319
- package/build/src/tools/performance.js +0 -190
- package/build/src/tools/screencast.js +0 -79
- package/build/src/tools/screenshot.js +0 -84
- package/build/src/tools/script.js +0 -119
- package/build/src/tools/slim/tools.js +0 -81
- package/build/src/tools/snapshot.js +0 -56
- package/build/src/tools/tools.js +0 -52
- package/build/src/tools/webmcp.js +0 -416
- package/build/src/trace-processing/parse.js +0 -84
- package/build/src/types.js +0 -6
- package/build/src/utils/ExtensionRegistry.js +0 -35
- package/build/src/utils/files.js +0 -19
- package/build/src/utils/keyboard.js +0 -296
- package/build/src/utils/pagination.js +0 -49
- package/build/src/utils/string.js +0 -36
- package/build/src/utils/types.js +0 -6
- package/build/src/version.js +0 -9
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import { createStackTraceForConsoleMessage, SymbolizedError, } from '../DevtoolsUtils.js';
|
|
7
|
-
import { UncaughtError } from '../PageCollector.js';
|
|
8
|
-
import * as DevTools from '../third_party/index.js';
|
|
9
|
-
export class ConsoleFormatter {
|
|
10
|
-
#id;
|
|
11
|
-
#type;
|
|
12
|
-
#text;
|
|
13
|
-
#argCount;
|
|
14
|
-
#resolvedArgs;
|
|
15
|
-
#stack;
|
|
16
|
-
#cause;
|
|
17
|
-
isIgnored;
|
|
18
|
-
constructor(params) {
|
|
19
|
-
this.#id = params.id;
|
|
20
|
-
this.#type = params.type;
|
|
21
|
-
this.#text = params.text;
|
|
22
|
-
this.#argCount = params.argCount ?? 0;
|
|
23
|
-
this.#resolvedArgs = params.resolvedArgs ?? [];
|
|
24
|
-
this.#stack = params.stack;
|
|
25
|
-
this.#cause = params.cause;
|
|
26
|
-
this.isIgnored = params.isIgnored;
|
|
27
|
-
}
|
|
28
|
-
static async from(msg, options) {
|
|
29
|
-
const ignoreListManager = options?.devTools?.universe.context.get(DevTools.DevTools.IgnoreListManager);
|
|
30
|
-
const isIgnored = options.isIgnoredForTesting ||
|
|
31
|
-
((frame) => {
|
|
32
|
-
if (!ignoreListManager) {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
if (frame.uiSourceCode) {
|
|
36
|
-
return ignoreListManager.isUserOrSourceMapIgnoreListedUISourceCode(frame.uiSourceCode);
|
|
37
|
-
}
|
|
38
|
-
if (frame.url) {
|
|
39
|
-
return ignoreListManager.isUserIgnoreListedURL(frame.url);
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
});
|
|
43
|
-
if (msg instanceof UncaughtError) {
|
|
44
|
-
const error = await SymbolizedError.fromDetails({
|
|
45
|
-
devTools: options?.devTools,
|
|
46
|
-
details: msg.details,
|
|
47
|
-
targetId: msg.targetId,
|
|
48
|
-
includeStackAndCause: options?.fetchDetailedData,
|
|
49
|
-
resolvedStackTraceForTesting: options?.resolvedStackTraceForTesting,
|
|
50
|
-
resolvedCauseForTesting: options?.resolvedCauseForTesting,
|
|
51
|
-
});
|
|
52
|
-
return new ConsoleFormatter({
|
|
53
|
-
id: options.id,
|
|
54
|
-
type: 'error',
|
|
55
|
-
text: error.message,
|
|
56
|
-
stack: error.stackTrace,
|
|
57
|
-
cause: error.cause,
|
|
58
|
-
isIgnored,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
let resolvedArgs = [];
|
|
62
|
-
if (options.resolvedArgsForTesting) {
|
|
63
|
-
resolvedArgs = options.resolvedArgsForTesting;
|
|
64
|
-
}
|
|
65
|
-
else if (options.fetchDetailedData) {
|
|
66
|
-
resolvedArgs = await Promise.all(msg.args().map(async (arg, i) => {
|
|
67
|
-
try {
|
|
68
|
-
const remoteObject = arg.remoteObject();
|
|
69
|
-
if (remoteObject.type === 'object' && remoteObject.subtype === 'error') {
|
|
70
|
-
return await SymbolizedError.fromError({
|
|
71
|
-
devTools: options.devTools,
|
|
72
|
-
error: remoteObject,
|
|
73
|
-
// @ts-expect-error Internal ConsoleMessage API
|
|
74
|
-
targetId: msg._targetId(),
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
return await arg.jsonValue();
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return `<error: Argument ${i} is no longer available>`;
|
|
81
|
-
}
|
|
82
|
-
}));
|
|
83
|
-
}
|
|
84
|
-
let stack;
|
|
85
|
-
if (options.resolvedStackTraceForTesting) {
|
|
86
|
-
stack = options.resolvedStackTraceForTesting;
|
|
87
|
-
}
|
|
88
|
-
else if (options.fetchDetailedData && options.devTools) {
|
|
89
|
-
try {
|
|
90
|
-
stack = await createStackTraceForConsoleMessage(options.devTools, msg);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// ignore
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return new ConsoleFormatter({
|
|
97
|
-
id: options.id,
|
|
98
|
-
type: msg.type(),
|
|
99
|
-
text: msg.text(),
|
|
100
|
-
argCount: resolvedArgs.length || msg.args().length,
|
|
101
|
-
resolvedArgs,
|
|
102
|
-
stack,
|
|
103
|
-
isIgnored,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
// The short format for a console message.
|
|
107
|
-
toString() {
|
|
108
|
-
return convertConsoleMessageConciseToString(this.toJSON());
|
|
109
|
-
}
|
|
110
|
-
// The verbose format for a console message, including all details.
|
|
111
|
-
toStringDetailed() {
|
|
112
|
-
return convertConsoleMessageConciseDetailedToString(this.toJSONDetailed());
|
|
113
|
-
}
|
|
114
|
-
#getArgs() {
|
|
115
|
-
if (this.#resolvedArgs.length > 0) {
|
|
116
|
-
const args = [...this.#resolvedArgs];
|
|
117
|
-
// If there is no text, the first argument serves as text (see formatMessage).
|
|
118
|
-
if (!this.#text) {
|
|
119
|
-
args.shift();
|
|
120
|
-
}
|
|
121
|
-
return args;
|
|
122
|
-
}
|
|
123
|
-
return [];
|
|
124
|
-
}
|
|
125
|
-
toJSON() {
|
|
126
|
-
return {
|
|
127
|
-
type: this.#type,
|
|
128
|
-
text: this.#text,
|
|
129
|
-
argsCount: this.#argCount,
|
|
130
|
-
id: this.#id,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
toJSONDetailed() {
|
|
134
|
-
return {
|
|
135
|
-
id: this.#id,
|
|
136
|
-
type: this.#type,
|
|
137
|
-
text: this.#text,
|
|
138
|
-
argsCount: this.#argCount,
|
|
139
|
-
args: this.#getArgs().map((arg) => formatArg(arg, this)),
|
|
140
|
-
stackTrace: this.#stack ? formatStackTrace(this.#stack, this.#cause, this) : undefined,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function convertConsoleMessageConciseToString(msg) {
|
|
145
|
-
return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)`;
|
|
146
|
-
}
|
|
147
|
-
function convertConsoleMessageConciseDetailedToString(msg) {
|
|
148
|
-
const result = [
|
|
149
|
-
`ID: ${msg.id}`,
|
|
150
|
-
`Message: ${msg.type}> ${msg.text}`,
|
|
151
|
-
formatArgs(msg),
|
|
152
|
-
...(msg.stackTrace ? ['### Stack trace', msg.stackTrace] : []),
|
|
153
|
-
].filter((line) => !!line);
|
|
154
|
-
return result.join('\n');
|
|
155
|
-
}
|
|
156
|
-
function formatArgs(msg) {
|
|
157
|
-
const args = msg.args;
|
|
158
|
-
if (!args.length) {
|
|
159
|
-
return '';
|
|
160
|
-
}
|
|
161
|
-
const result = ['### Arguments'];
|
|
162
|
-
for (const [key, arg] of args.entries()) {
|
|
163
|
-
result.push(`Arg #${key}: ${arg}`);
|
|
164
|
-
}
|
|
165
|
-
return result.join('\n');
|
|
166
|
-
}
|
|
167
|
-
function formatArg(arg, formatter) {
|
|
168
|
-
if (arg instanceof SymbolizedError) {
|
|
169
|
-
return [
|
|
170
|
-
arg.message,
|
|
171
|
-
arg.stackTrace ? formatStackTrace(arg.stackTrace, arg.cause, formatter) : undefined,
|
|
172
|
-
]
|
|
173
|
-
.filter((line) => !!line)
|
|
174
|
-
.join('\n');
|
|
175
|
-
}
|
|
176
|
-
return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
|
|
177
|
-
}
|
|
178
|
-
const STACK_TRACE_MAX_LINES = 50;
|
|
179
|
-
function formatStackTrace(stackTrace, cause, formatter) {
|
|
180
|
-
const lines = formatStackTraceInner(stackTrace, cause, formatter);
|
|
181
|
-
const includedLines = lines.slice(0, STACK_TRACE_MAX_LINES);
|
|
182
|
-
const reminderCount = lines.length - includedLines.length;
|
|
183
|
-
return [
|
|
184
|
-
...includedLines,
|
|
185
|
-
reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
|
|
186
|
-
'Note: line and column numbers use 1-based indexing',
|
|
187
|
-
]
|
|
188
|
-
.filter((line) => !!line)
|
|
189
|
-
.join('\n');
|
|
190
|
-
}
|
|
191
|
-
function formatStackTraceInner(stackTrace, cause, formatter) {
|
|
192
|
-
if (!stackTrace) {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
return [
|
|
196
|
-
...formatFragment(stackTrace.syncFragment, formatter),
|
|
197
|
-
...stackTrace.asyncFragments.map((item) => formatAsyncFragment(item, formatter)).flat(),
|
|
198
|
-
...formatCause(cause, formatter),
|
|
199
|
-
];
|
|
200
|
-
}
|
|
201
|
-
function formatFragment(fragment, formatter) {
|
|
202
|
-
const frames = fragment.frames.filter((frame) => !formatter.isIgnored(frame));
|
|
203
|
-
return frames.map(formatFrame);
|
|
204
|
-
}
|
|
205
|
-
function formatAsyncFragment(fragment, formatter) {
|
|
206
|
-
const formattedFrames = formatFragment(fragment, formatter);
|
|
207
|
-
if (formattedFrames.length === 0) {
|
|
208
|
-
return [];
|
|
209
|
-
}
|
|
210
|
-
const separatorLineLength = 40;
|
|
211
|
-
const prefix = `--- ${fragment.description || 'async'} `;
|
|
212
|
-
const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
|
|
213
|
-
return [separator, ...formattedFrames];
|
|
214
|
-
}
|
|
215
|
-
function formatFrame(frame) {
|
|
216
|
-
let result = `at ${frame.name ?? '<anonymous>'}`;
|
|
217
|
-
if (frame.uiSourceCode) {
|
|
218
|
-
const location = frame.uiSourceCode.uiLocation(frame.line, frame.column);
|
|
219
|
-
result += ` (${location.linkText(/* skipTrim */ false, /* showColumnNumber */ true)})`;
|
|
220
|
-
}
|
|
221
|
-
else if (frame.url) {
|
|
222
|
-
result += ` (${frame.url}:${frame.line}:${frame.column})`;
|
|
223
|
-
}
|
|
224
|
-
return result;
|
|
225
|
-
}
|
|
226
|
-
function formatCause(cause, formatter) {
|
|
227
|
-
if (!cause) {
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
return [
|
|
231
|
-
`Caused by: ${cause.message}`,
|
|
232
|
-
...formatStackTraceInner(cause.stackTrace, cause.cause, formatter),
|
|
233
|
-
];
|
|
234
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import { ISSUE_UTILS } from '../issue-descriptions.js';
|
|
7
|
-
import { logger } from '../logger.js';
|
|
8
|
-
import { DevTools } from '../third_party/index.js';
|
|
9
|
-
export class IssueFormatter {
|
|
10
|
-
#issue;
|
|
11
|
-
#options;
|
|
12
|
-
constructor(issue, options) {
|
|
13
|
-
this.#issue = issue;
|
|
14
|
-
this.#options = options;
|
|
15
|
-
}
|
|
16
|
-
toString() {
|
|
17
|
-
return convertIssueConciseToString(this.toJSON());
|
|
18
|
-
}
|
|
19
|
-
toStringDetailed() {
|
|
20
|
-
return convertIssueDetailedToString(this.toJSONDetailed());
|
|
21
|
-
}
|
|
22
|
-
toJSON() {
|
|
23
|
-
return {
|
|
24
|
-
type: 'issue',
|
|
25
|
-
title: this.#getTitle(),
|
|
26
|
-
count: this.#issue.getAggregatedIssuesCount(),
|
|
27
|
-
id: this.#options.id,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
toJSONDetailed() {
|
|
31
|
-
return {
|
|
32
|
-
id: this.#options.id,
|
|
33
|
-
type: 'issue',
|
|
34
|
-
count: this.#issue.getAggregatedIssuesCount(),
|
|
35
|
-
title: this.#getTitle(),
|
|
36
|
-
description: this.#getDescription(),
|
|
37
|
-
links: this.#issue.getDescription()?.links,
|
|
38
|
-
affectedResources: this.#getAffectedResources(),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
#getAffectedResources() {
|
|
42
|
-
const issues = this.#issue.getAllIssues();
|
|
43
|
-
const affectedResources = [];
|
|
44
|
-
for (const singleIssue of issues) {
|
|
45
|
-
const details = singleIssue.details();
|
|
46
|
-
if (!details) {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
// We send the remaining details as untyped JSON because the DevTools
|
|
50
|
-
// frontend code is currently not re-usable.
|
|
51
|
-
const data = structuredClone(details);
|
|
52
|
-
let uid;
|
|
53
|
-
let request;
|
|
54
|
-
if ('violatingNodeId' in details &&
|
|
55
|
-
details.violatingNodeId &&
|
|
56
|
-
this.#options.elementIdResolver) {
|
|
57
|
-
uid = this.#options.elementIdResolver(details.violatingNodeId);
|
|
58
|
-
delete data.violatingNodeId;
|
|
59
|
-
}
|
|
60
|
-
if ('nodeId' in details &&
|
|
61
|
-
details.nodeId &&
|
|
62
|
-
this.#options.elementIdResolver) {
|
|
63
|
-
uid = this.#options.elementIdResolver(details.nodeId);
|
|
64
|
-
delete data.nodeId;
|
|
65
|
-
}
|
|
66
|
-
if ('documentNodeId' in details &&
|
|
67
|
-
details.documentNodeId &&
|
|
68
|
-
this.#options.elementIdResolver) {
|
|
69
|
-
uid = this.#options.elementIdResolver(details.documentNodeId);
|
|
70
|
-
delete data.documentNodeId;
|
|
71
|
-
}
|
|
72
|
-
if ('request' in details && details.request) {
|
|
73
|
-
request = details.request.url;
|
|
74
|
-
if (details.request.requestId && this.#options.requestIdResolver) {
|
|
75
|
-
const resolvedId = this.#options.requestIdResolver(details.request.requestId);
|
|
76
|
-
if (resolvedId) {
|
|
77
|
-
request = resolvedId;
|
|
78
|
-
const requestData = data.request;
|
|
79
|
-
delete requestData.requestId;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
// These fields has no use for the MCP client (redundant or irrelevant).
|
|
84
|
-
delete data.errorType;
|
|
85
|
-
delete data.frameId;
|
|
86
|
-
affectedResources.push({
|
|
87
|
-
uid,
|
|
88
|
-
data: data,
|
|
89
|
-
request,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
return affectedResources;
|
|
93
|
-
}
|
|
94
|
-
isValid() {
|
|
95
|
-
return this.#getTitle() !== undefined;
|
|
96
|
-
}
|
|
97
|
-
// Helper to extract title
|
|
98
|
-
#getTitle() {
|
|
99
|
-
const markdownDescription = this.#issue.getDescription();
|
|
100
|
-
const filename = markdownDescription?.file;
|
|
101
|
-
if (!filename) {
|
|
102
|
-
logger(`no description found for issue:` + this.#issue.code());
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
// We already have the description logic in #getDescription, but title extraction is separate
|
|
106
|
-
// We can reuse the logic or cache it.
|
|
107
|
-
// Ideally we should process markdown once.
|
|
108
|
-
const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
|
|
109
|
-
if (!rawMarkdown) {
|
|
110
|
-
logger(`no markdown ${filename} found for issue:` + this.#issue.code());
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const processedMarkdown = DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
|
|
115
|
-
const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
|
|
116
|
-
const title = DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
|
|
117
|
-
if (!title) {
|
|
118
|
-
logger('cannot read issue title from ' + filename);
|
|
119
|
-
return undefined;
|
|
120
|
-
}
|
|
121
|
-
return title;
|
|
122
|
-
}
|
|
123
|
-
catch {
|
|
124
|
-
logger('error parsing markdown for issue ' + this.#issue.code());
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
#getDescription() {
|
|
129
|
-
const markdownDescription = this.#issue.getDescription();
|
|
130
|
-
const filename = markdownDescription?.file;
|
|
131
|
-
if (!filename) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
|
|
135
|
-
if (!rawMarkdown) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
return DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return undefined;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function convertIssueConciseToString(issue) {
|
|
147
|
-
return `msgid=${issue.id} [issue] ${issue.title} (count: ${issue.count})`;
|
|
148
|
-
}
|
|
149
|
-
function convertIssueDetailedToString(issue) {
|
|
150
|
-
const result = [];
|
|
151
|
-
result.push(`ID: ${issue.id}`);
|
|
152
|
-
const bodyParts = [];
|
|
153
|
-
const description = issue.description;
|
|
154
|
-
let processedMarkdown = description?.trim();
|
|
155
|
-
// Remove heading in order not to conflict with the whole console message response markdown
|
|
156
|
-
if (processedMarkdown?.startsWith('# ')) {
|
|
157
|
-
processedMarkdown = processedMarkdown.substring(2).trimStart();
|
|
158
|
-
}
|
|
159
|
-
if (processedMarkdown) {
|
|
160
|
-
bodyParts.push(processedMarkdown);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
bodyParts.push(issue.title ?? 'Unknown Issue');
|
|
164
|
-
}
|
|
165
|
-
const links = issue.links;
|
|
166
|
-
if (links && links.length > 0) {
|
|
167
|
-
bodyParts.push('Learn more:');
|
|
168
|
-
for (const link of links) {
|
|
169
|
-
bodyParts.push(`[${link.linkTitle}](${link.link})`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const affectedResources = issue.affectedResources;
|
|
173
|
-
if (affectedResources.length) {
|
|
174
|
-
bodyParts.push('### Affected resources');
|
|
175
|
-
bodyParts.push(...affectedResources.map(item => {
|
|
176
|
-
const details = [];
|
|
177
|
-
if (item.uid) {
|
|
178
|
-
details.push(`uid=${item.uid}`);
|
|
179
|
-
}
|
|
180
|
-
if (item.request) {
|
|
181
|
-
details.push((typeof item.request === 'number' ? `reqid=` : 'url=') +
|
|
182
|
-
item.request);
|
|
183
|
-
}
|
|
184
|
-
if (item.data) {
|
|
185
|
-
details.push(`data=${JSON.stringify(item.data)}`);
|
|
186
|
-
}
|
|
187
|
-
return details.join(' ');
|
|
188
|
-
}));
|
|
189
|
-
}
|
|
190
|
-
result.push(`Message: issue> ${bodyParts.join('\n')}`);
|
|
191
|
-
return result.join('\n');
|
|
192
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
* */
|
|
6
|
-
import { isUtf8 } from 'node:buffer';
|
|
7
|
-
const BODY_CONTEXT_SIZE_LIMIT = 10000;
|
|
8
|
-
export class NetworkFormatter {
|
|
9
|
-
#request;
|
|
10
|
-
#options;
|
|
11
|
-
#requestBody;
|
|
12
|
-
#responseBody;
|
|
13
|
-
#requestBodyFilePath;
|
|
14
|
-
#responseBodyFilePath;
|
|
15
|
-
constructor(request, options) {
|
|
16
|
-
this.#request = request;
|
|
17
|
-
this.#options = options;
|
|
18
|
-
}
|
|
19
|
-
static async from(request, options) {
|
|
20
|
-
const instance = new NetworkFormatter(request, options);
|
|
21
|
-
if (options.fetchData) {
|
|
22
|
-
await instance.#loadDetailedData();
|
|
23
|
-
}
|
|
24
|
-
return instance;
|
|
25
|
-
}
|
|
26
|
-
async #loadDetailedData() {
|
|
27
|
-
// Load Request Body
|
|
28
|
-
if (this.#request.hasPostData()) {
|
|
29
|
-
let data;
|
|
30
|
-
try {
|
|
31
|
-
data = this.#request.postData() ?? (await this.#request.fetchPostData());
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// Ignore parsing errors
|
|
35
|
-
}
|
|
36
|
-
const requestBodyNotAvailableMessage = '<Request body not available anymore>';
|
|
37
|
-
if (this.#options.requestFilePath) {
|
|
38
|
-
if (!this.#options.saveFile) {
|
|
39
|
-
throw new Error('saveFile is not provided');
|
|
40
|
-
}
|
|
41
|
-
if (data) {
|
|
42
|
-
await this.#options.saveFile(Buffer.from(data), this.#options.requestFilePath);
|
|
43
|
-
this.#requestBodyFilePath = this.#options.requestFilePath;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
this.#requestBody = requestBodyNotAvailableMessage;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
if (data) {
|
|
51
|
-
this.#requestBody = getSizeLimitedString(data, BODY_CONTEXT_SIZE_LIMIT);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
this.#requestBody = requestBodyNotAvailableMessage;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Load Response Body
|
|
59
|
-
const response = this.#request.response();
|
|
60
|
-
if (response) {
|
|
61
|
-
const responseBodyNotAvailableMessage = '<Response body not available anymore>';
|
|
62
|
-
if (this.#options.responseFilePath) {
|
|
63
|
-
try {
|
|
64
|
-
const buffer = await response.buffer();
|
|
65
|
-
if (!this.#options.saveFile) {
|
|
66
|
-
throw new Error('saveFile is not provided');
|
|
67
|
-
}
|
|
68
|
-
await this.#options.saveFile(buffer, this.#options.responseFilePath);
|
|
69
|
-
this.#responseBodyFilePath = this.#options.responseFilePath;
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// Flatten error handling for buffer() failure and save failure
|
|
73
|
-
}
|
|
74
|
-
if (!this.#responseBodyFilePath) {
|
|
75
|
-
this.#responseBody = responseBodyNotAvailableMessage;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
this.#responseBody = await this.#getFormattedResponseBody(response, BODY_CONTEXT_SIZE_LIMIT);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
toString() {
|
|
84
|
-
return convertNetworkRequestConciseToString(this.toJSON());
|
|
85
|
-
}
|
|
86
|
-
toStringDetailed() {
|
|
87
|
-
return converNetworkRequestDetailedToStringDetailed(this.toJSONDetailed());
|
|
88
|
-
}
|
|
89
|
-
toJSON() {
|
|
90
|
-
return {
|
|
91
|
-
requestId: this.#options.requestId,
|
|
92
|
-
method: this.#request.method(),
|
|
93
|
-
url: this.#request.url(),
|
|
94
|
-
status: this.#getStatusFromRequest(this.#request),
|
|
95
|
-
selectedInDevToolsUI: this.#options.selectedInDevToolsUI,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
toJSONDetailed() {
|
|
99
|
-
const redirectChain = this.#request.redirectChain();
|
|
100
|
-
const formattedRedirectChain = redirectChain.reverse().map((request) => {
|
|
101
|
-
const id = this.#options.requestIdResolver
|
|
102
|
-
? this.#options.requestIdResolver(request)
|
|
103
|
-
: undefined;
|
|
104
|
-
const formatter = new NetworkFormatter(request, {
|
|
105
|
-
requestId: id,
|
|
106
|
-
saveFile: this.#options.saveFile,
|
|
107
|
-
});
|
|
108
|
-
return formatter.toJSON();
|
|
109
|
-
});
|
|
110
|
-
return {
|
|
111
|
-
...this.toJSON(),
|
|
112
|
-
requestHeaders: this.#request.headers(),
|
|
113
|
-
requestBody: this.#requestBody,
|
|
114
|
-
requestBodyFilePath: this.#requestBodyFilePath,
|
|
115
|
-
responseHeaders: this.#request.response()?.headers(),
|
|
116
|
-
responseBody: this.#responseBody,
|
|
117
|
-
responseBodyFilePath: this.#responseBodyFilePath,
|
|
118
|
-
failure: this.#request.failure()?.errorText,
|
|
119
|
-
redirectChain: formattedRedirectChain.length ? formattedRedirectChain : undefined,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
#getStatusFromRequest(request) {
|
|
123
|
-
const httpResponse = request.response();
|
|
124
|
-
const failure = request.failure();
|
|
125
|
-
let status;
|
|
126
|
-
if (httpResponse) {
|
|
127
|
-
status = httpResponse.status().toString();
|
|
128
|
-
}
|
|
129
|
-
else if (failure) {
|
|
130
|
-
status = failure.errorText;
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
status = 'pending';
|
|
134
|
-
}
|
|
135
|
-
return status;
|
|
136
|
-
}
|
|
137
|
-
async #getFormattedResponseBody(httpResponse, sizeLimit = BODY_CONTEXT_SIZE_LIMIT) {
|
|
138
|
-
try {
|
|
139
|
-
const responseBuffer = await httpResponse.buffer();
|
|
140
|
-
if (isUtf8(responseBuffer)) {
|
|
141
|
-
const responseAsTest = responseBuffer.toString('utf-8');
|
|
142
|
-
if (responseAsTest.length === 0) {
|
|
143
|
-
return '<empty response>';
|
|
144
|
-
}
|
|
145
|
-
return getSizeLimitedString(responseAsTest, sizeLimit);
|
|
146
|
-
}
|
|
147
|
-
return '<binary data>';
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return '<not available anymore>';
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function getSizeLimitedString(text, sizeLimit) {
|
|
155
|
-
if (text.length > sizeLimit) {
|
|
156
|
-
return text.substring(0, sizeLimit) + '... <truncated>';
|
|
157
|
-
}
|
|
158
|
-
return text;
|
|
159
|
-
}
|
|
160
|
-
function convertNetworkRequestConciseToString(data) {
|
|
161
|
-
// TODO truncate the URL
|
|
162
|
-
return `reqid=${data.requestId} ${data.method} ${data.url} [${data.status}]${data.selectedInDevToolsUI ? ` [selected in the DevTools Network panel]` : ''}`;
|
|
163
|
-
}
|
|
164
|
-
function formatHeadlers(headers) {
|
|
165
|
-
const response = [];
|
|
166
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
167
|
-
response.push(`- ${name}:${value}`);
|
|
168
|
-
}
|
|
169
|
-
return response;
|
|
170
|
-
}
|
|
171
|
-
function converNetworkRequestDetailedToStringDetailed(data) {
|
|
172
|
-
const response = [];
|
|
173
|
-
response.push(`## Request ${data.url}`);
|
|
174
|
-
response.push(`Status: ${data.status}`);
|
|
175
|
-
response.push(`### Request Headers`);
|
|
176
|
-
for (const line of formatHeadlers(data.requestHeaders)) {
|
|
177
|
-
response.push(line);
|
|
178
|
-
}
|
|
179
|
-
if (data.requestBody) {
|
|
180
|
-
response.push(`### Request Body`);
|
|
181
|
-
response.push(data.requestBody);
|
|
182
|
-
}
|
|
183
|
-
else if (data.requestBodyFilePath) {
|
|
184
|
-
response.push(`### Request Body`);
|
|
185
|
-
response.push(`Saved to ${data.requestBodyFilePath}.`);
|
|
186
|
-
}
|
|
187
|
-
if (data.responseHeaders) {
|
|
188
|
-
response.push(`### Response Headers`);
|
|
189
|
-
for (const line of formatHeadlers(data.responseHeaders)) {
|
|
190
|
-
response.push(line);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
if (data.responseBody) {
|
|
194
|
-
response.push(`### Response Body`);
|
|
195
|
-
response.push(data.responseBody);
|
|
196
|
-
}
|
|
197
|
-
else if (data.responseBodyFilePath) {
|
|
198
|
-
response.push(`### Response Body`);
|
|
199
|
-
response.push(`Saved to ${data.responseBodyFilePath}.`);
|
|
200
|
-
}
|
|
201
|
-
if (data.failure) {
|
|
202
|
-
response.push(`### Request failed with`);
|
|
203
|
-
response.push(data.failure);
|
|
204
|
-
}
|
|
205
|
-
const redirectChain = data.redirectChain;
|
|
206
|
-
if (redirectChain?.length) {
|
|
207
|
-
response.push(`### Redirect chain`);
|
|
208
|
-
let indent = 0;
|
|
209
|
-
for (const request of redirectChain.reverse()) {
|
|
210
|
-
response.push(`${' '.repeat(indent)}${convertNetworkRequestConciseToString(request)})}`);
|
|
211
|
-
indent++;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return response.join('\n');
|
|
215
|
-
}
|