Skip to content

Commit d4ce491

Browse files
author
Natallia Harshunova
committed
Subscribe for Audits.issueAdded event
1 parent 4a83574 commit d4ce491

9 files changed

Lines changed: 130 additions & 19 deletions

File tree

scripts/post-build.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const i18n = {
6363
return str;
6464
},
6565
lockedLazyString: () => {},
66-
getLazilyComputedLocalizedString: () => {},
66+
getLazilyComputedLocalizedString: () => ()=>{},
6767
};
6868
6969
// TODO(jacktfranklin): once the DocumentLatency insight does not depend on
@@ -169,6 +169,19 @@ export const hostConfig = {};
169169
fs.copyFileSync(devtoolsLicenseFileSource, devtoolsLicenseFileDestination);
170170

171171
copyThirdPartyLicenseFiles();
172+
copyDevToolsDescriptionFiles();
173+
}
174+
175+
function copyDevToolsDescriptionFiles() {
176+
const sourceDir = path.join(
177+
process.cwd(),
178+
'node_modules/chrome-devtools-frontend/front_end/models/issues_manager/descriptions',
179+
);
180+
const destDir = path.join(
181+
BUILD_DIR,
182+
'node_modules/chrome-devtools-frontend/front_end/models/issues_manager/descriptions',
183+
);
184+
fs.cpSync(sourceDir, destDir, {recursive: true});
172185
}
173186

174187
main();

src/McpContext.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import fs from 'node:fs/promises';
77
import os from 'node:os';
88
import path from 'node:path';
99

10+
import {type Issue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
11+
1012
import {extractUrlLikeFromDevToolsTitle, urlsEqual} from './DevtoolsUtils.js';
1113
import type {ListenerMap} from './PageCollector.js';
1214
import {NetworkCollector, PageCollector} from './PageCollector.js';
@@ -29,6 +31,8 @@ import type {Context, DevToolsData} from './tools/ToolDefinition.js';
2931
import type {TraceResult} from './trace-processing/parse.js';
3032
import {WaitForHelper} from './WaitForHelper.js';
3133

34+
35+
3236
export interface TextSnapshotNode extends SerializedAXNode {
3337
id: string;
3438
backendNodeId?: number;
@@ -90,7 +94,7 @@ export class McpContext implements Context {
9094
// The most recent snapshot.
9195
#textSnapshot: TextSnapshot | null = null;
9296
#networkCollector: NetworkCollector;
93-
#consoleCollector: PageCollector<ConsoleMessage | Error>;
97+
#consoleCollector: PageCollector<ConsoleMessage | Error | Issue.Issue>;
9498

9599
#isRunningTrace = false;
96100
#networkConditionsMap = new WeakMap<Page, string>();
@@ -130,6 +134,9 @@ export class McpContext implements Context {
130134
collect(error);
131135
}
132136
},
137+
issue: issue => {
138+
collect(issue);
139+
},
133140
} as ListenerMap;
134141
});
135142
}
@@ -196,25 +203,25 @@ export class McpContext implements Context {
196203

197204
getConsoleData(
198205
includePreservedMessages?: boolean,
199-
): Array<ConsoleMessage | Error> {
206+
): Array<ConsoleMessage | Error | Issue.Issue> {
200207
const page = this.getSelectedPage();
201208
return this.#consoleCollector.getData(page, includePreservedMessages);
202209
}
203210

204-
getConsoleMessageStableId(message: ConsoleMessage | Error): number {
211+
getConsoleMessageStableId(message: ConsoleMessage | Error | Issue.Issue): number {
205212
return this.#consoleCollector.getIdForResource(message);
206213
}
207214

208-
getConsoleMessageById(id: number): ConsoleMessage | Error {
215+
getConsoleMessageById(id: number): ConsoleMessage | Error | Issue.Issue {
209216
return this.#consoleCollector.getById(this.getSelectedPage(), id);
210217
}
211218

212219
async newPage(): Promise<Page> {
213220
const page = await this.browser.newPage();
214221
const pages = await this.createPagesSnapshot();
215222
this.setSelectedPageIdx(pages.indexOf(page));
216-
this.#networkCollector.addPage(page);
217-
this.#consoleCollector.addPage(page);
223+
await this.#networkCollector.addPage(page);
224+
await this.#consoleCollector.addPage(page);
218225
return page;
219226
}
220227
async closePage(pageIdx: number): Promise<void> {

src/McpResponse.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* Copyright 2025 Google LLC
44
* SPDX-License-Identifier: Apache-2.0
55
*/
6+
import { Issue } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
7+
68
import type {ConsoleMessageData} from './formatters/consoleFormatter.js';
79
import {
810
formatConsoleEventShort,
@@ -16,6 +18,7 @@ import {
1618
getStatusFromRequest,
1719
} from './formatters/networkFormatter.js';
1820
import {formatSnapshotNode} from './formatters/snapshotFormatter.js';
21+
import {getIssueDescription} from './issue-descriptions.js';
1922
import type {McpContext} from './McpContext.js';
2023
import type {
2124
ConsoleMessage,
@@ -269,7 +272,7 @@ export class McpResponse implements Response {
269272
if ('type' in message) {
270273
return normalizedTypes.has(message.type());
271274
}
272-
return normalizedTypes.has('error');
275+
return normalizedTypes.has('error'); // TODO add filtering
273276
});
274277
}
275278

@@ -295,6 +298,18 @@ export class McpResponse implements Response {
295298
),
296299
};
297300
}
301+
if (item instanceof Issue.Issue) {
302+
const descriptionFile = item.getDescription()?.file;
303+
const description = descriptionFile
304+
? getIssueDescription(descriptionFile)
305+
: null;
306+
return {
307+
consoleMessageStableId,
308+
type: 'issue',
309+
message: item.primaryKey(),
310+
args: description ? [description] : [],
311+
};
312+
}
298313
return {
299314
consoleMessageStableId,
300315
type: 'error',

src/PageCollector.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import type * as CdpProtocol from '../node_modules/chrome-devtools-frontend/front_end/generated/protocol-proxy-api.js';
8+
import {IssuesManager, Issue} from '../node_modules/chrome-devtools-frontend/mcp/mcp.js';
9+
710
import {
811
type Browser,
912
type Frame,
1013
type Handler,
1114
type HTTPRequest,
1215
type Page,
13-
type PageEvents,
16+
type PageEvents as PuppeteerPageEvents,
1417
} from './third_party/index.js';
1518

19+
interface PageEvents extends PuppeteerPageEvents {
20+
issue: Issue.Issue;
21+
}
22+
1623
export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
1724
[K in keyof EventMap]?: (event: EventMap[K]) => void;
1825
};
@@ -58,15 +65,15 @@ export class PageCollector<T> {
5865
async init() {
5966
const pages = await this.#browser.pages();
6067
for (const page of pages) {
61-
this.#initializePage(page);
68+
await this.addPage(page);
6269
}
6370

6471
this.#browser.on('targetcreated', async target => {
6572
const page = await target.page();
6673
if (!page) {
6774
return;
6875
}
69-
this.#initializePage(page);
76+
await this.addPage(page);
7077
});
7178
this.#browser.on('targetdestroyed', async target => {
7279
const page = await target.page();
@@ -77,15 +84,15 @@ export class PageCollector<T> {
7784
});
7885
}
7986

80-
public addPage(page: Page) {
81-
this.#initializePage(page);
82-
}
83-
84-
#initializePage(page: Page) {
87+
public async addPage(page: Page) {
8588
if (this.storage.has(page)) {
8689
return;
8790
}
91+
await this.#initializePage(page);
92+
}
8893

94+
async #initializePage(page: Page) {
95+
await this.subscribeForIssues(page);
8996
const idGenerator = createIdGenerator();
9097
const storedLists: Array<Array<WithSymbolId<T>>> = [[]];
9198
this.storage.set(page, storedLists);
@@ -113,6 +120,19 @@ export class PageCollector<T> {
113120
this.#listeners.set(page, listeners);
114121
}
115122

123+
protected async subscribeForIssues(page: Page) {
124+
const session = await page.createCDPSession();
125+
session.on('Audits.issueAdded',(data) => { // TODO unsubscribe
126+
// @ts-expect-error Types of protocol from Puppeteer and CDP are incopatible for Issues
127+
const issue = IssuesManager.createIssuesFromProtocolIssue(null, data.issue)[0]; // returns issue wrapped in array, need to get first element
128+
if (!issue) {
129+
return;
130+
}
131+
page.emit('issue', issue);
132+
});
133+
await session.send('Audits.enable');
134+
}
135+
116136
protected splitAfterNavigation(page: Page) {
117137
const navigations = this.storage.get(page);
118138
if (!navigations) {

src/issue-descriptions.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import * as fs from 'node:fs';
8+
import * as path from 'node:path';
9+
10+
const DESCRIPTIONS_PATH = path.join(
11+
process.cwd(),
12+
'node_modules/chrome-devtools-frontend/front_end/models/issues_manager/descriptions',
13+
);
14+
15+
let issueDescriptions: Record<string, string>= {};
16+
17+
/**
18+
* Reads all issue descriptions from the filesystem into memory.
19+
*/
20+
export async function loadIssueDescriptions(): Promise<void> {
21+
if (Object.keys(issueDescriptions).length > 0) {
22+
return;
23+
}
24+
25+
const files = await fs.promises.readdir(DESCRIPTIONS_PATH);
26+
const descriptions: Record<string, string> = {};
27+
28+
for (const file of files) {
29+
if (!file.endsWith('.md')) {
30+
continue;
31+
}
32+
const content = await fs.promises.readFile(
33+
path.join(DESCRIPTIONS_PATH, file),
34+
'utf-8',
35+
);
36+
descriptions[file] = content;
37+
}
38+
39+
issueDescriptions = descriptions;
40+
}
41+
42+
/**
43+
* Gets an issue description from the in-memory cache.
44+
* @param fileName The file name of the issue description.
45+
* @returns The description of the issue, or null if it doesn't exist.
46+
*/
47+
export function getIssueDescription(fileName: string): string | null {
48+
return issueDescriptions[fileName] ?? null;
49+
}

src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import './polyfill.js';
99
import type {Channel} from './browser.js';
1010
import {ensureBrowserConnected, ensureBrowserLaunched} from './browser.js';
1111
import {parseArguments} from './cli.js';
12+
import {loadIssueDescriptions} from './issue-descriptions.js';
1213
import {logger, saveLogsToFile} from './logger.js';
1314
import {McpContext} from './McpContext.js';
1415
import {McpResponse} from './McpResponse.js';
@@ -188,6 +189,7 @@ for (const tool of tools) {
188189
registerTool(tool);
189190
}
190191

192+
await loadIssueDescriptions();
191193
const transport = new StdioServerTransport();
192194
await server.connect(transport);
193195
logger('Chrome DevTools MCP Server connected');

src/tools/console.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import type {ConsoleMessageType} from '../third_party/index.js';
99

1010
import {ToolCategory} from './categories.js';
1111
import {defineTool} from './ToolDefinition.js';
12+
type ConsoleResponseType = ConsoleMessageType | 'issue';
1213

1314
const FILTERABLE_MESSAGE_TYPES: readonly [
14-
ConsoleMessageType,
15-
...ConsoleMessageType[],
15+
ConsoleResponseType,
16+
...ConsoleResponseType[],
1617
] = [
1718
'log',
1819
'debug',
@@ -33,6 +34,7 @@ const FILTERABLE_MESSAGE_TYPES: readonly [
3334
'count',
3435
'timeEnd',
3536
'verbose',
37+
'issue'
3638
];
3739

3840
export const listConsoleMessages = defineTool({

test.txt

Whitespace-only changes.

tsconfig.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@
5656
"node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript",
5757
"node_modules/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec",
5858
"node_modules/chrome-devtools-frontend/front_end/core/root",
59-
"node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web"
59+
"node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web",
60+
61+
"node_modules/chrome-devtools-frontend/front_end/models/issues_manager",
62+
"node_modules/chrome-devtools-frontend/front_end/third_party/marked"
6063
],
6164
"exclude": ["node_modules/chrome-devtools-frontend/**/*.test.ts"]
6265
}

0 commit comments

Comments
 (0)