mirror of
https://github.com/ChromeDevTools/chrome-devtools-mcp.git
synced 2026-11-04 15:00:11 +00:00
perf: use CDP to find open DevTools pages. (#1150)
This should reduce the flakiness and issue with finding open DevTools tabs for the corresponding pages. Currently we do multiple loops over all the targets, so this should have a nice performance improvement when multiple pages are opened.
This commit is contained in:
1 parent
a06d8e64c9
commit
94de19cdcd
3 files changed
+16
-165
No files matched your search
@@ -15,61 +15,6 @@ import type {
|
||||
Target as PuppeteerTarget,
|
||||
} from './third_party/index.js';
|
||||
|
||||
export function extractUrlLikeFromDevToolsTitle(
|
||||
title: string,
|
||||
): string | undefined {
|
||||
const match = title.match(new RegExp(`DevTools - (.*)`));
|
||||
return match?.[1] ?? undefined;
|
||||
}
|
||||
|
||||
export function urlsEqual(url1: string, url2: string): boolean {
|
||||
const normalizedUrl1 = normalizeUrl(url1);
|
||||
const normalizedUrl2 = normalizeUrl(url2);
|
||||
return normalizedUrl1 === normalizedUrl2;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the sake of the MCP server, when we determine if two URLs are equal we
|
||||
* remove some parts:
|
||||
*
|
||||
* 1. We do not care about the protocol.
|
||||
* 2. We do not care about trailing slashes.
|
||||
* 3. We do not care about "www".
|
||||
* 4. We ignore the hash parts.
|
||||
*
|
||||
* For example, if the user types "record a trace on foo.com", we would want to
|
||||
* match a tab in the connected Chrome instance that is showing "www.foo.com/"
|
||||
*/
|
||||
function normalizeUrl(url: string): string {
|
||||
let result = url.trim();
|
||||
|
||||
// Remove protocols
|
||||
if (result.startsWith('https://')) {
|
||||
result = result.slice(8);
|
||||
} else if (result.startsWith('http://')) {
|
||||
result = result.slice(7);
|
||||
}
|
||||
|
||||
// Remove 'www.'. This ensures that we find the right URL regardless of if the user adds `www` or not.
|
||||
if (result.startsWith('www.')) {
|
||||
result = result.slice(4);
|
||||
}
|
||||
|
||||
// We use target URLs to locate DevTools but those often do
|
||||
// no include hash.
|
||||
const hashIdx = result.lastIndexOf('#');
|
||||
if (hashIdx !== -1) {
|
||||
result = result.slice(0, hashIdx);
|
||||
}
|
||||
|
||||
// Remove trailing slash
|
||||
if (result.endsWith('/')) {
|
||||
result = result.slice(0, -1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mock implementation of an issues manager that only implements the methods
|
||||
* that are actually used by the IssuesAggregator
|
||||
|
||||
+15
-35
@@ -8,11 +8,7 @@ import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import type {TargetUniverse} from './DevtoolsUtils.js';
|
||||
import {
|
||||
extractUrlLikeFromDevToolsTitle,
|
||||
UniverseManager,
|
||||
urlsEqual,
|
||||
} from './DevtoolsUtils.js';
|
||||
import {UniverseManager} from './DevtoolsUtils.js';
|
||||
import {McpPage} from './McpPage.js';
|
||||
import type {ListenerMap, UncaughtError} from './PageCollector.js';
|
||||
import {NetworkCollector, ConsoleCollector} from './PageCollector.js';
|
||||
@@ -653,37 +649,21 @@ export class McpContext implements Context {
|
||||
async detectOpenDevToolsWindows() {
|
||||
this.logger('Detecting open DevTools windows');
|
||||
const {pages} = await this.#getAllPages();
|
||||
// Clear all devToolsPage references before re-detecting.
|
||||
for (const mcpPage of this.#mcpPages.values()) {
|
||||
mcpPage.devToolsPage = undefined;
|
||||
}
|
||||
for (const devToolsPage of pages) {
|
||||
if (devToolsPage.url().startsWith('devtools://')) {
|
||||
try {
|
||||
this.logger('Calling getTargetInfo for ' + devToolsPage.url());
|
||||
const data = await devToolsPage
|
||||
// @ts-expect-error no types for _client().
|
||||
._client()
|
||||
.send('Target.getTargetInfo');
|
||||
const devtoolsPageTitle = data.targetInfo.title;
|
||||
const urlLike = extractUrlLikeFromDevToolsTitle(devtoolsPageTitle);
|
||||
if (!urlLike) {
|
||||
continue;
|
||||
}
|
||||
// TODO: lookup without a loop.
|
||||
for (const page of this.#pages) {
|
||||
if (urlsEqual(page.url(), urlLike)) {
|
||||
const mcpPage = this.#mcpPages.get(page);
|
||||
if (mcpPage) {
|
||||
mcpPage.devToolsPage = devToolsPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger('Issue occurred while trying to find DevTools', error);
|
||||
|
||||
await Promise.all(
|
||||
pages.map(async page => {
|
||||
const mcpPage = this.#mcpPages.get(page);
|
||||
if (!mcpPage) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (await page.hasDevTools()) {
|
||||
mcpPage.devToolsPage = await page.openDevTools();
|
||||
} else {
|
||||
mcpPage.devToolsPage = undefined;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getExtensionServiceWorkers(): ExtensionServiceWorker[] {
|
||||
|
||||
@@ -9,11 +9,7 @@ import {afterEach, describe, it} from 'node:test';
|
||||
|
||||
import sinon from 'sinon';
|
||||
|
||||
import {
|
||||
extractUrlLikeFromDevToolsTitle,
|
||||
urlsEqual,
|
||||
UniverseManager,
|
||||
} from '../src/DevtoolsUtils.js';
|
||||
import {UniverseManager} from '../src/DevtoolsUtils.js';
|
||||
import {DevTools} from '../src/third_party/index.js';
|
||||
import type {Browser, Target} from '../src/third_party/index.js';
|
||||
|
||||
@@ -24,76 +20,6 @@ import {
|
||||
withBrowser,
|
||||
} from './utils.js';
|
||||
|
||||
describe('extractUrlFromDevToolsTitle', () => {
|
||||
it('deals with no trailing /', () => {
|
||||
assert.strictEqual(
|
||||
extractUrlLikeFromDevToolsTitle('DevTools - example.com'),
|
||||
'example.com',
|
||||
);
|
||||
});
|
||||
it('deals with a trailing /', () => {
|
||||
assert.strictEqual(
|
||||
extractUrlLikeFromDevToolsTitle('DevTools - example.com/'),
|
||||
'example.com/',
|
||||
);
|
||||
});
|
||||
it('deals with www', () => {
|
||||
assert.strictEqual(
|
||||
extractUrlLikeFromDevToolsTitle('DevTools - www.example.com/'),
|
||||
'www.example.com/',
|
||||
);
|
||||
});
|
||||
it('deals with complex url', () => {
|
||||
assert.strictEqual(
|
||||
extractUrlLikeFromDevToolsTitle(
|
||||
'DevTools - www.example.com/path.html?a=b#3',
|
||||
),
|
||||
'www.example.com/path.html?a=b#3',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlsEqual', () => {
|
||||
it('ignores trailing slashes', () => {
|
||||
assert.strictEqual(
|
||||
urlsEqual('https://google.com/', 'https://google.com'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores www', () => {
|
||||
assert.strictEqual(
|
||||
urlsEqual('https://google.com/', 'https://www.google.com'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores protocols', () => {
|
||||
assert.strictEqual(
|
||||
urlsEqual('https://google.com/', 'http://www.google.com'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not ignore other subdomains', () => {
|
||||
assert.strictEqual(
|
||||
urlsEqual('https://google.com/', 'https://photos.google.com'),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores hash', () => {
|
||||
assert.strictEqual(
|
||||
urlsEqual('https://google.com/#', 'http://www.google.com'),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
urlsEqual('https://google.com/#21', 'http://www.google.com#12'),
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('UniverseManager', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
|
||||
Reference in new issue
Block a user