Skip to content

Commit b391311

Browse files
authored
chore: reflect emulated device in CrUX data (#2131)
CrUX data returned with the performance tool now reflects the emulated device Closes: #1813
1 parent 7b5ec3a commit b391311

3 files changed

Lines changed: 125 additions & 6 deletions

File tree

src/McpResponse.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ export class McpResponse implements Response {
209209
#error?: Error;
210210
#attachedWaitForResult?: WaitForEventsResult;
211211

212+
get #deviceScope(): DevTools.CrUXManager.DeviceScope {
213+
return this.#page?.viewport?.isMobile ? 'PHONE' : 'DESKTOP';
214+
}
215+
212216
constructor(args: ParsedArguments) {
213217
this.#args = args;
214218
}
@@ -898,7 +902,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
898902
}
899903

900904
if (data.traceSummary) {
901-
const summary = getTraceSummary(data.traceSummary);
905+
const summary = getTraceSummary(data.traceSummary, this.#deviceScope);
902906
response.push(summary);
903907
structuredContent.traceSummary = summary;
904908
structuredContent.traceInsights = [];
@@ -917,6 +921,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
917921
data.traceInsight.trace,
918922
data.traceInsight.insightSetId,
919923
data.traceInsight.insightName,
924+
this.#deviceScope,
920925
);
921926
if ('error' in insightOutput) {
922927
response.push(insightOutput.error);

src/trace-processing/parse.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,12 @@ ${DevTools.PerformanceTraceFormatter.callFrameDataFormatDescription}
8080
8181
${DevTools.PerformanceTraceFormatter.networkDataFormatDescription}`;
8282

83-
export function getTraceSummary(result: TraceResult): string {
83+
export function getTraceSummary(
84+
result: TraceResult,
85+
deviceScope?: DevTools.CrUXManager.DeviceScope | null,
86+
): string {
8487
const focus = DevTools.AgentFocus.fromParsedTrace(result.parsedTrace);
85-
const formatter = new DevTools.PerformanceTraceFormatter(focus);
88+
const formatter = new DevTools.PerformanceTraceFormatter(focus, deviceScope);
8689
const summaryText = formatter.formatTraceSummary();
8790
return `## Summary of Performance trace findings:
8891
${summaryText}
@@ -99,6 +102,7 @@ export function getInsightOutput(
99102
result: TraceResult,
100103
insightSetId: string,
101104
insightName: InsightName,
105+
deviceScope?: DevTools.CrUXManager.DeviceScope | null,
102106
): InsightOutput {
103107
if (!result.insights) {
104108
return {
@@ -125,6 +129,7 @@ export function getInsightOutput(
125129
const formatter = new DevTools.PerformanceInsightFormatter(
126130
DevTools.AgentFocus.fromParsedTrace(result.parsedTrace),
127131
matchingInsight,
132+
deviceScope,
128133
);
129134
return {output: formatter.formatInsight()};
130135
}

tests/tools/performance.test.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,16 +387,125 @@ describe('performance', () => {
387387
{performanceCrux: false},
388388
);
389389
});
390+
391+
it('fetches CrUX data for desktop and includes it in the summary', async () => {
392+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
393+
await withMcpContext(async (response, context) => {
394+
context.setIsRunningPerformanceTrace(true);
395+
const selectedPage = context.getSelectedPptrPage();
396+
sinon.stub(selectedPage.tracing, 'stop').resolves(rawData);
397+
398+
const fetchStub = globalThis.fetch as sinon.SinonStub;
399+
fetchStub.resetHistory();
400+
fetchStub.callsFake(async (url, options) => {
401+
const body = options?.body ? JSON.parse(options.body as string) : {};
402+
const requestedUrl = body.url || body.origin || 'https://web.dev/';
403+
const lcp = body.formFactor === 'DESKTOP' ? 1000 : 2595;
404+
return new Response(
405+
JSON.stringify(cruxResponseFixture(requestedUrl, lcp)),
406+
{
407+
status: 200,
408+
headers: {'Content-Type': 'application/json'},
409+
},
410+
);
411+
});
412+
413+
await stopTrace.handler(
414+
{params: {}, page: context.getSelectedMcpPage()},
415+
response,
416+
context,
417+
);
418+
419+
const result = await response.handle('performance_stop_trace', context);
420+
const fullOutput = result.content
421+
.map(c => (c.type === 'text' ? c.text : ''))
422+
.join('\n');
423+
424+
assert.ok(fetchStub.called, 'CrUX fetch should have been called');
425+
assert.ok(
426+
fullOutput.includes('Metrics (field / real users)'),
427+
'Summary should include field data',
428+
);
429+
assert.ok(
430+
fullOutput.includes('LCP: 1000 ms'),
431+
'Summary should include desktop LCP value',
432+
);
433+
});
434+
});
435+
436+
it('fetches CrUX data for mobile and includes it in the summary', async () => {
437+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
438+
// Use a unique URL to avoid cache issues
439+
const jsonString = new TextDecoder().decode(rawData);
440+
const modifiedJsonString = jsonString.replaceAll(
441+
'https://web.dev/',
442+
'https://mobile.web.dev/',
443+
);
444+
const modifiedData = new TextEncoder().encode(modifiedJsonString);
445+
446+
await withMcpContext(async (response, context) => {
447+
context.setIsRunningPerformanceTrace(true);
448+
const selectedPage = context.getSelectedPptrPage();
449+
sinon.stub(selectedPage.tracing, 'stop').resolves(modifiedData);
450+
451+
// Emulate mobile
452+
await context.emulate({
453+
viewport: {
454+
width: 375,
455+
height: 667,
456+
isMobile: true,
457+
hasTouch: true,
458+
deviceScaleFactor: 2,
459+
},
460+
});
461+
462+
const fetchStub = globalThis.fetch as sinon.SinonStub;
463+
fetchStub.resetHistory();
464+
fetchStub.callsFake(async (url, options) => {
465+
const body = options?.body ? JSON.parse(options.body as string) : {};
466+
const requestedUrl = body.url || body.origin || 'https://web.dev/';
467+
const lcp = body.formFactor === 'PHONE' ? 2000 : 2595;
468+
return new Response(
469+
JSON.stringify(cruxResponseFixture(requestedUrl, lcp)),
470+
{
471+
status: 200,
472+
headers: {'Content-Type': 'application/json'},
473+
},
474+
);
475+
});
476+
477+
await stopTrace.handler(
478+
{params: {}, page: context.getSelectedMcpPage()},
479+
response,
480+
context,
481+
);
482+
483+
const result = await response.handle('performance_stop_trace', context);
484+
const fullOutput = result.content
485+
.map(c => (c.type === 'text' ? c.text : ''))
486+
.join('\n');
487+
488+
assert.ok(fetchStub.called, 'CrUX fetch should have been called');
489+
assert.ok(
490+
fullOutput.includes('Metrics (field / real users)'),
491+
'Summary should include field data',
492+
);
493+
assert.ok(
494+
fullOutput.includes('LCP: 2000 ms'),
495+
'Summary should include mobile LCP value',
496+
);
497+
});
498+
});
390499
});
391500
});
392501

393-
function cruxResponseFixture() {
502+
function cruxResponseFixture(url = 'https://web.dev/', lcp = 2595) {
394503
// Ideally we could use `mockResponse` from 'chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.test.ts'
395504
// But test files are not published in the cdtf npm package.
396505
return {
397506
record: {
398507
key: {
399-
url: 'https://web.dev/',
508+
url,
400509
},
401510
metrics: {
402511
form_factors: {
@@ -408,7 +517,7 @@ function cruxResponseFixture() {
408517
{start: 2500, end: 4000, density: 0.163},
409518
{start: 4000, density: 0.1061},
410519
],
411-
percentiles: {p75: 2595},
520+
percentiles: {p75: lcp},
412521
},
413522
largest_contentful_paint_image_element_render_delay: {
414523
percentiles: {p75: 786},

0 commit comments

Comments
 (0)