豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
be31b47
revert these lockfile changes
paulirish Dec 22, 2025
9623ae6
implemented but imma change it
paulirish Dec 22, 2025
31a6cf1
collapsed to a single setting
paulirish Dec 23, 2025
1f17741
remove toggle tool. bump up timeouts while i investigate them
paulirish Dec 23, 2025
de66b44
ensureCrux for test
paulirish Dec 23, 2025
a2894a9
manual test yayyyyyyyyyyy
paulirish Dec 23, 2025
e2da11a
universe mgr added to ctx
paulirish Dec 23, 2025
7e71d8f
settings is resolve but now we need to give cruxmgr some data to report
paulirish Dec 23, 2025
647f0d9
drop util crux. trying to get proper data from real thing
paulirish Dec 23, 2025
eb5bc51
new endpoint. will work when i tweak api in GCP C
paulirish Dec 23, 2025
94ac77c
Merge branch 'main' into crux-for-trace
paulirish Jan 3, 2026
44687f9
Merge remote-tracking branch 'origin/main' into crux-for-trace
paulirish Jan 6, 2026
7d8ae17
works
paulirish Jan 6, 2026
9280192
mock fetch to avoid network RT in test
paulirish Jan 6, 2026
295b192
drop diff
paulirish Jan 6, 2026
1662b6d
use proper export
paulirish Jan 12, 2026
4cef374
prep for crrev.com/c/7421201
paulirish Jan 13, 2026
a64ee0b
Merge branch 'main' into crux-for-trace
paulirish Jan 13, 2026
d892a26
Merge branch 'main' into crux-for-trace
paulirish Jan 13, 2026
009064a
skip manager? nahh
paulirish Jan 14, 2026
53736f5
Merge branch 'main' into crux-for-trace
paulirish Jan 14, 2026
0f371af
Merge remote-tracking branch 'origin/main' into crux-for-trace
paulirish Jan 29, 2026
ce92886
logger
paulirish Jan 29, 2026
446f32f
drop univermgr addition as its already done
paulirish Jan 29, 2026
010125b
add --performance-crux flag, defaulted to true
paulirish Jan 29, 2026
a2b167e
Merge branch 'crux-for-trace' of github.com:ChromeDevTools/chrome-dev…
paulirish Jan 29, 2026
a6a45a2
fix cli test
paulirish Jan 29, 2026
99059ab
docs: update cli description
OrKoN Jan 30, 2026
2730b64
Merge branch 'main' into crux-for-trace
OrKoN Jan 30, 2026
4391db4
chore: rebase
OrKoN Jan 30, 2026
70ac720
Merge branch 'main' into crux-for-trace
OrKoN Feb 2, 2026
a286ce1
Merge branch 'main' into crux-for-trace
OrKoN Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ allowing them to inspect, debug, and modify any data in the browser or DevTools.
Avoid sharing sensitive or personal information that you don't want to share with
MCP clients.

Performance tools may send trace URLs to the Google CrUX API to fetch real-user
experience data. This helps provide a holistic performance picture by
presenting field data alongside lab data. This data is collected by the [Chrome
User Experience Report (CrUX)](https://developer.chrome.com/docs/crux). To disable
this, run with the `--no-performance-crux` flag.

## **Usage statistics**

Google collects usage statistics (such as tool invocation success rates, latency, and environment information) to improve the reliability and performance of Chrome DevTools MCP.
Expand Down Expand Up @@ -466,6 +472,11 @@ The Chrome DevTools MCP server supports the following configuration option:
- **Type:** boolean
- **Default:** `true`

- **`--performanceCrux`/ `--performance-crux`**
Set to false to disable sending URLs from performance traces to CrUX API to get field performance data.
- **Type:** boolean
- **Default:** `true`

- **`--usageStatistics`/ `--usage-statistics`**
Set to false to opt-out of usage statistics collection. Google collects usage data to improve the tool, handled under the Google Privacy Policy (https://policies.google.com/privacy). This is independent from Chrome browser metrics. Disabled if CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS or CI env variables are set.
- **Type:** boolean
Expand Down
6 changes: 6 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ interface McpContextOptions {
experimentalDevToolsDebugging: boolean;
// Whether all page-like targets are exposed as pages.
experimentalIncludeAllPages?: boolean;
// Whether CrUX data should be fetched.
performanceCrux: boolean;
}

const DEFAULT_TIMEOUT = 5_000;
Expand Down Expand Up @@ -370,6 +372,10 @@ export class McpContext implements Context {
return this.#isRunningTrace;
}

isCruxEnabled(): boolean {
return this.#options.performanceCrux;
}

getDialog(): Dialog | undefined {
return this.#dialog;
}
Expand Down
10 changes: 10 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,12 @@ export const cliOptions = {
hidden: true,
describe: 'Set to false to exclude tools related to extensions.',
},
performanceCrux: {
type: 'boolean',
default: true,
describe:
'Set to false to disable sending URLs from performance traces to CrUX API to get field performance data.',
},
usageStatistics: {
type: 'boolean',
default: true,
Expand Down Expand Up @@ -297,6 +303,10 @@ export function parseArguments(version: string, argv = process.argv) {
'$0 --no-usage-statistics',
'Do not send usage statistics https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics.',
],
[
'$0 --no-performance-crux',
'Disable CrUX (field data) integration in performance tools.',
],
]);

return yargsInstance
Expand Down
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ async function getContext(): Promise<McpContext> {
context = await McpContext.from(browser, logger, {
experimentalDevToolsDebugging: devtools,
experimentalIncludeAllPages: args.experimentalIncludeAllPages,
performanceCrux: args.performanceCrux,
});
}
return context;
Expand All @@ -127,6 +128,12 @@ debug, and modify any data in the browser or DevTools.
Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`,
);

if (args.performanceCrux) {
console.error(
`Performance tools may send trace URLs to the Google CrUX API to fetch real-user experience data. To disable, run with --no-performance-crux.`,
);
}

if (args.usageStatistics) {
console.error(
`
Expand Down
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export interface Response {
export type Context = Readonly<{
isRunningPerformanceTrace(): boolean;
setIsRunningPerformanceTrace(x: boolean): void;
isCruxEnabled(): boolean;
recordedTraces(): TraceResult[];
storeTraceRecording(result: TraceResult): void;
getSelectedPage(): Page;
Expand Down
48 changes: 46 additions & 2 deletions src/tools/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

import zlib from 'node:zlib';

import {zod} from '../third_party/index.js';
import {logger} from '../logger.js';
import {zod, DevTools} from '../third_party/index.js';
import type {Page} from '../third_party/index.js';
import type {InsightName} from '../trace-processing/parse.js';
import type {InsightName, TraceResult} from '../trace-processing/parse.js';
import {
parseRawTraceBuffer,
traceResultIsSuccess,
Expand Down Expand Up @@ -202,6 +203,9 @@ async function stopTracingAndAppendOutput(
const result = await parseRawTraceBuffer(traceEventsBuffer);
response.appendResponseLine('The performance trace has been stopped.');
if (traceResultIsSuccess(result)) {
if (context.isCruxEnabled()) {
await populateCruxData(result);
}
context.storeTraceRecording(result);
response.attachTraceSummary(result);
} else {
Expand All @@ -213,3 +217,43 @@ async function stopTracingAndAppendOutput(
context.setIsRunningPerformanceTrace(false);
}
}

/** We tell CrUXManager to fetch data so it's available when DevTools.PerformanceTraceFormatter is invoked */
async function populateCruxData(result: TraceResult): Promise<void> {
logger('populateCruxData called');
const cruxManager = DevTools.CrUXManager.instance();
// go/jtfbx. Yes, we're aware this API key is public. ;)
cruxManager.setEndpointForTesting(
'https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=AIzaSyBn5gimNjhiEyA_euicSKko6IlD3HdgUfk',
);
const cruxSetting =
DevTools.Common.Settings.Settings.instance().createSetting('field-data', {
enabled: true,
});
cruxSetting.set({enabled: true});

// Gather URLs to fetch CrUX data for
const urls = [...(result.parsedTrace.insights?.values() ?? [])].map(c =>
c.url.toString(),
);
urls.push(result.parsedTrace.data.Meta.mainFrameURL);
const urlSet = new Set(urls);
Comment thread
paulirish marked this conversation as resolved.

if (urlSet.size === 0) {
logger('No URLs found for CrUX data');
return;
}

logger(
`Fetching CrUX data for ${urlSet.size} URLs: ${Array.from(urlSet).join(', ')}`,
);
const cruxData = await Promise.all(
Array.from(urlSet).map(async url => {
const data = await cruxManager.getFieldDataForPage(url);
logger(`CrUX data for ${url}: ${data ? 'found' : 'not found'}`);
return data;
}),
);

result.parsedTrace.metadata.cruxFieldData = cruxData;
}
22 changes: 22 additions & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ describe('cli args parsing', () => {
categoryNetwork: true,
'auto-connect': undefined,
autoConnect: undefined,
'performance-crux': true,
performanceCrux: true,
'usage-statistics': true,
usageStatistics: true,
};
Expand Down Expand Up @@ -272,4 +274,24 @@ describe('cli args parsing', () => {
]);
assert.strictEqual(disabledArgs.usageStatistics, false);
});

it('parses performance crux flag', async () => {
const defaultArgs = parseArguments('1.0.0', ['node', 'main.js']);
assert.strictEqual(defaultArgs.performanceCrux, true);

// force enable
const enabledArgs = parseArguments('1.0.0', [
'node',
'main.js',
'--performance-crux',
]);
assert.strictEqual(enabledArgs.performanceCrux, true);

const disabledArgs = parseArguments('1.0.0', [
'node',
'main.js',
'--no-performance-crux',
]);
assert.strictEqual(disabledArgs.performanceCrux, false);
});
});
14 changes: 13 additions & 1 deletion tests/tools/performance.test.js.snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,19 @@ Metrics (lab / observed):
- Load duration: 15 ms, bounds: {min: 122411037986, max: 122411052690}
- Render delay: 73 ms, bounds: {min: 122411052690, max: 122411126100}
- CLS: 0.00
Metrics (field / real users): n/a – no data for this page in CrUX
Metrics (field / real users):
- LCP: 2595 ms (scope: url)
- LCP breakdown:
- TTFB: 1273 ms (scope: url)
- Load delay: 86 ms (scope: url)
- Load duration: 451 ms (scope: url)
- Render delay: 786 ms (scope: url)
- INP: 140 ms (scope: url)
- CLS: 0.06 (scope: url)
- The above data is from CrUX–Chrome User Experience Report. It's how the page performs for real users.
- The values shown above are the p75 measure of all real Chrome users
- The scope indicates if the data came from the entire origin, or a specific url
- Lab metrics describe how this specific page load performed, while field metrics are an aggregation of results from real-world users. Best practice is to prioritize metrics that are bad in field data. Lab metrics may be better or worse than fields metrics depending on the developer's machine, network, or the actions performed while tracing.
Available insights:
- insight name: LCPBreakdown
description: Each [subpart has specific improvement strategies](https://developer.chrome.com/docs/performance/insights/lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays.
Expand Down
114 changes: 113 additions & 1 deletion tests/tools/performance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import assert from 'node:assert';
import {describe, it, afterEach} from 'node:test';
import {describe, it, afterEach, beforeEach} from 'node:test';
import zlib from 'node:zlib';

import sinon from 'sinon';
Expand All @@ -28,6 +28,20 @@ describe('performance', () => {
sinon.restore();
});

beforeEach(() => {
sinon.stub(globalThis, 'fetch').callsFake(async url => {
const cruxEndpoint =
'https://chromeuxreport.googleapis.com/v1/records:queryRecord';
if (url.toString().startsWith(cruxEndpoint)) {
return new Response(JSON.stringify(cruxResponseFixture()), {
status: 200,
headers: {'Content-Type': 'application/json'},
});
}
throw new Error(`Unexpected fetch to ${url}`);
});
});

describe('performance_start_trace', () => {
it('starts a trace recording', async () => {
await withMcpContext(async (response, context) => {
Expand Down Expand Up @@ -311,5 +325,103 @@ describe('performance', () => {
);
});
});

it('does not fetch CrUX data if performanceCrux is false', async () => {
const rawData = loadTraceAsBuffer('basic-trace.json.gz');
await withMcpContext(
async (response, context) => {
context.setIsRunningPerformanceTrace(true);
const selectedPage = context.getSelectedPage();
sinon.stub(selectedPage.tracing, 'stop').resolves(rawData);

await stopTrace.handler({params: {}}, response, context);

const cruxEndpoint =
'https://chromeuxreport.googleapis.com/v1/records:queryRecord';
const cruxCall = (globalThis.fetch as sinon.SinonStub)
.getCalls()
.find(call => call.args[0].toString().startsWith(cruxEndpoint));
assert.strictEqual(
cruxCall,
undefined,
'CrUX fetch should not have been called',
);
},
{performanceCrux: false},
);
});
});
});

function cruxResponseFixture() {
// Ideally we could use `mockResponse` from 'chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.test.ts'
// But test files are not published in the cdtf npm package.
return {
record: {
key: {
url: 'https://web.dev/',
},
metrics: {
form_factors: {
fractions: {desktop: 0.5056, phone: 0.4796, tablet: 0.0148},
},
largest_contentful_paint: {
histogram: [
{start: 0, end: 2500, density: 0.7309},
{start: 2500, end: 4000, density: 0.163},
{start: 4000, density: 0.1061},
],
percentiles: {p75: 2595},
},
largest_contentful_paint_image_element_render_delay: {
percentiles: {p75: 786},
},
largest_contentful_paint_image_resource_load_delay: {
percentiles: {p75: 86},
},
largest_contentful_paint_image_time_to_first_byte: {
percentiles: {p75: 1273},
},
cumulative_layout_shift: {
histogram: [
{start: '0.00', end: '0.10', density: 0.8665},
{start: '0.10', end: '0.25', density: 0.0716},
{start: '0.25', density: 0.0619},
],
percentiles: {p75: '0.06'},
},
interaction_to_next_paint: {
histogram: [
{start: 0, end: 200, density: 0.8414},
{start: 200, end: 500, density: 0.1081},
{start: 500, density: 0.0505},
],
percentiles: {p75: 140},
},
largest_contentful_paint_image_resource_load_duration: {
percentiles: {p75: 451},
},
round_trip_time: {
histogram: [
{start: 0, end: 75, density: 0.3663},
{start: 75, end: 275, density: 0.5089},
{start: 275, density: 0.1248},
],
percentiles: {p75: 178},
},
first_contentful_paint: {
histogram: [
{start: 0, end: 1800, density: 0.5899},
{start: 1800, end: 3000, density: 0.2439},
{start: 3000, density: 0.1662},
],
percentiles: {p75: 2425},
},
},
collectionPeriod: {
firstDate: {year: 2025, month: 12, day: 8},
lastDate: {year: 2026, month: 1, day: 4},
},
},
};
}
7 changes: 6 additions & 1 deletion tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ export async function withBrowser(

export async function withMcpContext(
cb: (response: McpResponse, context: McpContext) => Promise<void>,
options: {debug?: boolean; autoOpenDevTools?: boolean} = {},
options: {
debug?: boolean;
autoOpenDevTools?: boolean;
performanceCrux?: boolean;
} = {},
) {
await withBrowser(async browser => {
const response = new McpResponse();
Expand All @@ -92,6 +96,7 @@ export async function withMcpContext(
logger('test'),
{
experimentalDevToolsDebugging: false,
performanceCrux: options.performanceCrux ?? true,
},
Locator,
);
Expand Down