Skip to content

Commit 9623ae6

Browse files
committed
implemented but imma change it
Change-Id: Ib9b76305409bb660a25182497d9b54591e2297a0
1 parent be31b47 commit 9623ae6

7 files changed

Lines changed: 432 additions & 3 deletions

File tree

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ allowing them to inspect, debug, and modify any data in the browser or DevTools.
2727
Avoid sharing sensitive or personal information that you don't want to share with
2828
MCP clients.
2929

30+
Performance tools may send trace URLs to the Google CrUX API to fetch real-user
31+
experience data. This helps provide a holistic performance picture by
32+
presenting field data alongside lab data. This data is collected by the [Chrome
33+
User Experience Report (CrUX)](https://developer.chrome.com/docs/crux).
34+
3035
## Requirements
3136

3237
- [Node.js](https://nodejs.org/) v20.19 or a newer [latest maintenance LTS](https://github.com/nodejs/Release#release-schedule) version.
@@ -328,10 +333,11 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
328333
- **Emulation** (2 tools)
329334
- [`emulate`](docs/tool-reference.md#emulate)
330335
- [`resize_page`](docs/tool-reference.md#resize_page)
331-
- **Performance** (3 tools)
336+
- **Performance** (4 tools)
332337
- [`performance_analyze_insight`](docs/tool-reference.md#performance_analyze_insight)
333338
- [`performance_start_trace`](docs/tool-reference.md#performance_start_trace)
334339
- [`performance_stop_trace`](docs/tool-reference.md#performance_stop_trace)
340+
- [`performance_toggle_crux`](docs/tool-reference.md#performance_toggle_crux)
335341
- **Network** (2 tools)
336342
- [`get_network_request`](docs/tool-reference.md#get_network_request)
337343
- [`list_network_requests`](docs/tool-reference.md#list_network_requests)

docs/tool-reference.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
- **[Emulation](#emulation)** (2 tools)
2222
- [`emulate`](#emulate)
2323
- [`resize_page`](#resize_page)
24-
- **[Performance](#performance)** (3 tools)
24+
- **[Performance](#performance)** (4 tools)
2525
- [`performance_analyze_insight`](#performance_analyze_insight)
2626
- [`performance_start_trace`](#performance_start_trace)
2727
- [`performance_stop_trace`](#performance_stop_trace)
28+
- [`performance_toggle_crux`](#performance_toggle_crux)
2829
- **[Network](#network)** (2 tools)
2930
- [`get_network_request`](#get_network_request)
3031
- [`list_network_requests`](#list_network_requests)
@@ -245,6 +246,16 @@
245246

246247
---
247248

249+
### `performance_toggle_crux`
250+
251+
**Description:** Enables or disables the fetching of real-user experience data from the Chrome User Experience Report (CrUX) API during performance traces. When enabled, performance summaries will include field data (LCP, INP, CLS) for the URLs in the trace.
252+
253+
**Parameters:**
254+
255+
- **enabled** (boolean) **(required)**: Whether to enable or disable CrUX data fetching.
256+
257+
---
258+
248259
## Network
249260

250261
### `get_network_request`

src/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ const logDisclaimers = () => {
9898
debug, and modify any data in the browser or DevTools.
9999
Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`,
100100
);
101+
console.error(
102+
`Performance tools may send trace URLs to the Google CrUX API to fetch real-user experience data.`,
103+
);
101104
};
102105

103106
const toolMutex = new Mutex();

src/third_party/devtools.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ export {
2929
createIssuesFromProtocolIssue,
3030
IssueAggregator,
3131
} from '../../node_modules/chrome-devtools-frontend/mcp/mcp.js';
32+
/* eslint-disable no-restricted-imports */
33+
export * as CrUXManager from '../../node_modules/chrome-devtools-frontend/front_end/models/crux-manager/crux-manager.js';
34+
/* eslint-enable no-restricted-imports */

src/tools/performance.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import {logger} from '../logger.js';
8-
import {zod} from '../third_party/index.js';
8+
import {DevTools, zod} from '../third_party/index.js';
99
import type {Page} from '../third_party/index.js';
1010
import type {InsightName} from '../trace-processing/parse.js';
1111
import {
@@ -14,6 +14,7 @@ import {
1414
parseRawTraceBuffer,
1515
traceResultIsSuccess,
1616
} from '../trace-processing/parse.js';
17+
import {populateCruxData} from '../utils/crux.js';
1718

1819
import {ToolCategory} from './categories.js';
1920
import type {Context, Response} from './ToolDefinition.js';
@@ -161,6 +162,35 @@ export const analyzeInsight = defineTool({
161162
},
162163
});
163164

165+
export const toggleCrux = defineTool({
166+
name: 'performance_toggle_crux',
167+
description:
168+
'Enables or disables the fetching of real-user experience data from the Chrome User Experience Report (CrUX) API during performance traces. When enabled, performance summaries will include field data (LCP, INP, CLS) for the URLs in the trace.',
169+
annotations: {
170+
category: ToolCategory.PERFORMANCE,
171+
readOnlyHint: false,
172+
},
173+
schema: {
174+
enabled: zod
175+
.boolean()
176+
.describe('Whether to enable or disable CrUX data fetching.'),
177+
},
178+
handler: async (request, response) => {
179+
try {
180+
const settings = DevTools.Common.Settings.Settings.instance();
181+
const cruxSetting = settings.createSetting('field-data-enabled', true);
182+
cruxSetting.set(request.params.enabled);
183+
response.appendResponseLine(
184+
`CrUX data fetching has been ${request.params.enabled ? 'enabled' : 'disabled'}.`,
185+
);
186+
} catch {
187+
response.appendResponseLine(
188+
'Error: Could not update the CrUX setting. It might not be available in this environment.',
189+
);
190+
}
191+
},
192+
});
193+
164194
async function stopTracingAndAppendOutput(
165195
page: Page,
166196
response: Response,
@@ -171,6 +201,7 @@ async function stopTracingAndAppendOutput(
171201
const result = await parseRawTraceBuffer(traceEventsBuffer);
172202
response.appendResponseLine('The performance trace has been stopped.');
173203
if (traceResultIsSuccess(result)) {
204+
await populateCruxData(result.parsedTrace);
174205
context.storeTraceRecording(result);
175206
const traceSummaryText = getTraceSummary(result);
176207
response.appendResponseLine(traceSummaryText);

src/utils/crux.ts

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import {logger} from '../logger.js';
8+
import {DevTools} from '../third_party/index.js';
9+
10+
// This key is expected to be visible. b/349721878
11+
const CRUX_API_KEY = 'AIzaSyBn5gimNjhiEyA_euicSKko6IlD3HdgUfk';
12+
const CRUX_ENDPOINT = `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${CRUX_API_KEY}`;
13+
14+
export type PageScope = 'url' | 'origin';
15+
export type DeviceScope = 'ALL' | 'DESKTOP' | 'PHONE' | 'TABLET';
16+
17+
export interface CrUXResponse {
18+
record: {
19+
key: {
20+
url?: string;
21+
origin?: string;
22+
formFactor?: string;
23+
};
24+
metrics: Record<string, unknown>;
25+
collectionPeriod: unknown;
26+
};
27+
}
28+
29+
const DEVICE_SCOPE_LIST: DeviceScope[] = ['ALL', 'DESKTOP', 'PHONE'];
30+
const PAGE_SCOPE_LIST: PageScope[] = ['origin', 'url'];
31+
32+
function mockCrUXManager(): void {
33+
const originalInstance = DevTools.CrUXManager.CrUXManager.instance;
34+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35+
(DevTools.CrUXManager.CrUXManager as any).instance = (opts: any) => {
36+
try {
37+
return originalInstance.call(DevTools.CrUXManager.CrUXManager, opts);
38+
} catch {
39+
return {
40+
getSelectedScope: () => ({pageScope: 'url', deviceScope: 'ALL'}),
41+
};
42+
}
43+
};
44+
}
45+
46+
export function ensureCrUXManager(): void {
47+
try {
48+
// Ensure Settings instance
49+
try {
50+
DevTools.Common.Settings.Settings.instance();
51+
} catch {
52+
const storage = new DevTools.Common.Settings.SettingsStorage({});
53+
DevTools.Common.Settings.Settings.instance({
54+
forceNew: true,
55+
syncedStorage: storage,
56+
globalStorage: storage,
57+
localStorage: storage,
58+
settingRegistrations:
59+
DevTools.Common.SettingRegistration.getRegisteredSettings(),
60+
});
61+
}
62+
63+
// Ensure TargetManager instance
64+
DevTools.TargetManager.instance();
65+
66+
// Ensure CrUXManager instance
67+
DevTools.CrUXManager.CrUXManager.instance();
68+
} catch {
69+
mockCrUXManager();
70+
}
71+
}
72+
73+
async function makeRequest(params: {
74+
url?: string;
75+
origin?: string;
76+
formFactor?: string;
77+
}): Promise<CrUXResponse | null> {
78+
try {
79+
const response = await fetch(CRUX_ENDPOINT, {
80+
method: 'POST',
81+
headers: {
82+
'Content-Type': 'application/json',
83+
referer: 'devtools://mcp',
84+
},
85+
body: JSON.stringify(params),
86+
});
87+
88+
if (response.status === 404) {
89+
return null;
90+
}
91+
92+
if (!response.ok) {
93+
logger(`CrUX API error: ${response.status} ${response.statusText}`);
94+
return null;
95+
}
96+
97+
return (await response.json()) as CrUXResponse;
98+
} catch (e) {
99+
logger(`CrUX API fetch failed: ${e}`);
100+
return null;
101+
}
102+
}
103+
104+
export async function getFieldDataForPage(
105+
pageUrl: string,
106+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
107+
): Promise<any /* CrUXManager.PageResult */> {
108+
const url = new URL(pageUrl);
109+
url.hash = '';
110+
url.search = '';
111+
const normalizedUrl = url.href;
112+
const origin = url.origin;
113+
const hostname = url.hostname;
114+
115+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
116+
const pageResult: any = {
117+
'origin-ALL': null,
118+
'origin-DESKTOP': null,
119+
'origin-PHONE': null,
120+
'origin-TABLET': null,
121+
'url-ALL': null,
122+
'url-DESKTOP': null,
123+
'url-PHONE': null,
124+
'url-TABLET': null,
125+
warnings: [],
126+
normalizedUrl,
127+
};
128+
129+
if (
130+
hostname === 'localhost' ||
131+
hostname === '127.0.0.1' ||
132+
!origin.startsWith('http')
133+
) {
134+
return pageResult;
135+
}
136+
137+
const promises: Array<Promise<void>> = [];
138+
139+
for (const pageScope of PAGE_SCOPE_LIST) {
140+
for (const deviceScope of DEVICE_SCOPE_LIST) {
141+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
142+
const params: any = {
143+
metrics: [
144+
'first_contentful_paint',
145+
'largest_contentful_paint',
146+
'cumulative_layout_shift',
147+
'interaction_to_next_paint',
148+
'round_trip_time',
149+
'form_factors',
150+
'largest_contentful_paint_image_time_to_first_byte',
151+
'largest_contentful_paint_image_resource_load_delay',
152+
'largest_contentful_paint_image_resource_load_duration',
153+
'largest_contentful_paint_image_element_render_delay',
154+
],
155+
};
156+
if (pageScope === 'url') {
157+
params.url = normalizedUrl;
158+
} else {
159+
params.origin = origin;
160+
}
161+
162+
if (deviceScope !== 'ALL') {
163+
params.formFactor = deviceScope;
164+
}
165+
166+
const promise = makeRequest(params).then(response => {
167+
pageResult[`${pageScope}-${deviceScope}`] = response;
168+
});
169+
promises.push(promise);
170+
}
171+
}
172+
173+
// Implement timeout
174+
const timeoutPromise = new Promise<void>(resolve =>
175+
setTimeout(resolve, 1000),
176+
);
177+
await Promise.race([Promise.all(promises), timeoutPromise]);
178+
179+
return pageResult;
180+
}
181+
182+
export async function populateCruxData(
183+
parsedTrace: DevTools.TraceEngine.TraceModel.ParsedTrace,
184+
): Promise<void> {
185+
ensureCrUXManager();
186+
try {
187+
const settings = DevTools.Common.Settings.Settings.instance();
188+
const cruxSetting = settings.createSetting('field-data-enabled', true);
189+
if (!cruxSetting.get()) {
190+
return;
191+
}
192+
} catch {
193+
// Fallback if settings are not available
194+
}
195+
196+
const urls = new Set<string>();
197+
if (parsedTrace.insights) {
198+
for (const insightSet of parsedTrace.insights.values()) {
199+
urls.add(insightSet.url.href);
200+
}
201+
} else {
202+
// Fallback to main frame URL if no insights
203+
const mainUrl = parsedTrace.data.Meta.mainFrameURL;
204+
if (mainUrl) {
205+
urls.add(mainUrl);
206+
}
207+
}
208+
209+
if (urls.size === 0) {
210+
return;
211+
}
212+
213+
const cruxData = await Promise.all(
214+
Array.from(urls).map(url => getFieldDataForPage(url)),
215+
);
216+
217+
parsedTrace.metadata.cruxFieldData = cruxData;
218+
}

0 commit comments

Comments
 (0)