Skip to content

Commit fbd22ce

Browse files
authored
chore: structured performance responses (#809)
This PR moves formatting out of the tools and to the McpResponse. Drive-by: clears up past trace results.
1 parent b0d041a commit fbd22ce

8 files changed

Lines changed: 413 additions & 66 deletions

File tree

src/McpContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ export class McpContext implements Context {
667667
}
668668

669669
storeTraceRecording(result: TraceResult): void {
670+
// Clear the trace results because we only consume the latest trace currently.
671+
this.#traceResults = [];
670672
this.#traceResults.push(result);
671673
}
672674

src/McpResponse.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,17 @@ import type {
2323
Response,
2424
SnapshotParams,
2525
} from './tools/ToolDefinition.js';
26+
import type {InsightName, TraceResult} from './trace-processing/parse.js';
27+
import {getInsightOutput, getTraceSummary} from './trace-processing/parse.js';
2628
import {paginate} from './utils/pagination.js';
2729
import type {PaginationOptions} from './utils/types.js';
2830

31+
interface TraceInsightData {
32+
trace: TraceResult;
33+
insightSetId: string;
34+
insightName: InsightName;
35+
}
36+
2937
export class McpResponse implements Response {
3038
#includePages = false;
3139
#snapshotParams?: SnapshotParams;
@@ -35,6 +43,8 @@ export class McpResponse implements Response {
3543
responseFilePath?: string;
3644
};
3745
#attachedConsoleMessageId?: number;
46+
#attachedTraceSummary?: TraceResult;
47+
#attachedTraceInsight?: TraceInsightData;
3848
#textResponseLines: string[] = [];
3949
#images: ImageContentData[] = [];
4050
#networkRequestsOptions?: {
@@ -137,10 +147,34 @@ export class McpResponse implements Response {
137147
this.#attachedConsoleMessageId = msgid;
138148
}
139149

150+
attachTraceSummary(result: TraceResult): void {
151+
this.#attachedTraceSummary = result;
152+
}
153+
154+
attachTraceInsight(
155+
trace: TraceResult,
156+
insightSetId: string,
157+
insightName: InsightName,
158+
): void {
159+
this.#attachedTraceInsight = {
160+
trace,
161+
insightSetId,
162+
insightName,
163+
};
164+
}
165+
140166
get includePages(): boolean {
141167
return this.#includePages;
142168
}
143169

170+
get attachedTraceSummary(): TraceResult | undefined {
171+
return this.#attachedTraceSummary;
172+
}
173+
174+
get attachedTracedInsight(): TraceInsightData | undefined {
175+
return this.#attachedTraceInsight;
176+
}
177+
144178
get includeNetworkRequests(): boolean {
145179
return this.#networkRequestsOptions?.include ?? false;
146180
}
@@ -359,6 +393,8 @@ export class McpResponse implements Response {
359393
snapshot,
360394
detailedNetworkRequest,
361395
networkRequests,
396+
traceInsight: this.#attachedTraceInsight,
397+
traceSummary: this.#attachedTraceSummary,
362398
});
363399
}
364400

@@ -371,6 +407,8 @@ export class McpResponse implements Response {
371407
snapshot: SnapshotFormatter | string | undefined;
372408
detailedNetworkRequest?: NetworkFormatter;
373409
networkRequests?: NetworkFormatter[];
410+
traceSummary?: TraceResult;
411+
traceInsight?: TraceInsightData;
374412
},
375413
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
376414
const response = [`# ${toolName} response`];
@@ -434,12 +472,42 @@ Call ${handleDialog.name} to handle it before continuing.`);
434472
networkRequests?: object[];
435473
consoleMessage?: object;
436474
consoleMessages?: object[];
475+
traceSummary?: string;
476+
traceInsights?: Array<{insightName: string; insightKey: string}>;
437477
} = {};
438478

439479
if (this.#tabId) {
440480
structuredContent.tabId = this.#tabId;
441481
}
442482

483+
if (data.traceSummary) {
484+
const summary = getTraceSummary(data.traceSummary);
485+
response.push(summary);
486+
structuredContent.traceSummary = summary;
487+
structuredContent.traceInsights = [];
488+
for (const insightSet of data.traceSummary.insights?.values() ?? []) {
489+
for (const [insightName, model] of Object.entries(insightSet.model)) {
490+
structuredContent.traceInsights.push({
491+
insightName,
492+
insightKey: model.insightKey,
493+
});
494+
}
495+
}
496+
}
497+
498+
if (data.traceInsight) {
499+
const insightOutput = getInsightOutput(
500+
data.traceInsight.trace,
501+
data.traceInsight.insightSetId,
502+
data.traceInsight.insightName,
503+
);
504+
if ('error' in insightOutput) {
505+
response.push(insightOutput.error);
506+
} else {
507+
response.push(insightOutput.output);
508+
}
509+
}
510+
443511
if (data.snapshot) {
444512
if (typeof data.snapshot === 'string') {
445513
response.push(`Saved snapshot to ${data.snapshot}.`);

src/tools/ToolDefinition.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
Page,
1313
Viewport,
1414
} from '../third_party/index.js';
15-
import type {TraceResult} from '../trace-processing/parse.js';
15+
import type {InsightName, TraceResult} from '../trace-processing/parse.js';
1616
import type {PaginationOptions} from '../utils/types.js';
1717

1818
import type {ToolCategory} from './categories.js';
@@ -86,6 +86,12 @@ export interface Response {
8686
// Allows re-using DevTools data queried by some tools.
8787
attachDevToolsData(data: DevToolsData): void;
8888
setTabId(tabId: string): void;
89+
attachTraceSummary(trace: TraceResult): void;
90+
attachTraceInsight(
91+
trace: TraceResult,
92+
insightSetId: string,
93+
insightName: InsightName,
94+
): void;
8995
}
9096

9197
/**

src/tools/performance.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,10 @@
66

77
import zlib from 'node:zlib';
88

9-
import {logger} from '../logger.js';
109
import {zod} from '../third_party/index.js';
1110
import type {Page} from '../third_party/index.js';
1211
import type {InsightName} from '../trace-processing/parse.js';
1312
import {
14-
getInsightOutput,
15-
getTraceSummary,
1613
parseRawTraceBuffer,
1714
traceResultIsSuccess,
1815
} from '../trace-processing/parse.js';
@@ -168,17 +165,11 @@ export const analyzeInsight = defineTool({
168165
return;
169166
}
170167

171-
const insightOutput = getInsightOutput(
168+
response.attachTraceInsight(
172169
lastRecording,
173170
request.params.insightSetId,
174171
request.params.insightName as InsightName,
175172
);
176-
if ('error' in insightOutput) {
177-
response.appendResponseLine(insightOutput.error);
178-
return;
179-
}
180-
181-
response.appendResponseLine(insightOutput.output);
182173
},
183174
});
184175

@@ -212,21 +203,12 @@ async function stopTracingAndAppendOutput(
212203
response.appendResponseLine('The performance trace has been stopped.');
213204
if (traceResultIsSuccess(result)) {
214205
context.storeTraceRecording(result);
215-
const traceSummaryText = getTraceSummary(result);
216-
response.appendResponseLine(traceSummaryText);
206+
response.attachTraceSummary(result);
217207
} else {
218-
response.appendResponseLine(
219-
'There was an unexpected error parsing the trace:',
208+
throw new Error(
209+
`There was an unexpected error parsing the trace: ${result.error}`,
220210
);
221-
response.appendResponseLine(result.error);
222211
}
223-
} catch (e) {
224-
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
225-
logger(`Error stopping performance trace: ${errorText}`);
226-
response.appendResponseLine(
227-
'An error occurred generating the response for this trace:',
228-
);
229-
response.appendResponseLine(errorText);
230212
} finally {
231213
context.setIsRunningPerformanceTrace(false);
232214
}

tests/McpContext.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ describe('McpContext', () => {
4141
});
4242
});
4343

44-
it('can store and retrieve performance traces', async () => {
44+
it('can store and retrieve the latest performance trace', async () => {
4545
await withMcpContext(async (_response, context) => {
4646
const fakeTrace1 = {} as unknown as TraceResult;
4747
const fakeTrace2 = {} as unknown as TraceResult;
4848
context.storeTraceRecording(fakeTrace1);
4949
context.storeTraceRecording(fakeTrace2);
50-
assert.deepEqual(context.recordedTraces(), [fakeTrace1, fakeTrace2]);
50+
assert.deepEqual(context.recordedTraces(), [fakeTrace2]);
5151
});
5252
});
5353

tests/McpResponse.test.js.snapshot

Lines changed: 246 additions & 0 deletions
Large diffs are not rendered by default.

tests/McpResponse.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import {tmpdir} from 'node:os';
1010
import {join} from 'node:path';
1111
import {describe, it} from 'node:test';
1212

13+
import type {InsightName} from '../src/trace-processing/parse.js';
14+
import {
15+
parseRawTraceBuffer,
16+
traceResultIsSuccess,
17+
} from '../src/trace-processing/parse.js';
18+
19+
import {loadTraceAsBuffer} from './trace-processing/fixtures/load.js';
1320
import {
1421
getImageContent,
1522
getMockAggregatedIssue,
@@ -529,4 +536,74 @@ describe('McpResponse network pagination', () => {
529536
assert.ok(text.includes('Showing 1-2 of 5 (Page 1 of 3).'));
530537
});
531538
});
539+
540+
describe('trace summaries', () => {
541+
it('includes the trace summary text and structured data', async t => {
542+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
543+
const result = await parseRawTraceBuffer(rawData);
544+
if (!traceResultIsSuccess(result)) {
545+
throw new Error(result.error);
546+
}
547+
548+
await withMcpContext(async (response, context) => {
549+
response.attachTraceSummary(result);
550+
const {content, structuredContent} = await response.handle(
551+
'test',
552+
context,
553+
);
554+
555+
t.assert.snapshot?.(getTextContent(content[0]));
556+
const typedStructuredContent = structuredContent as {
557+
traceSummary?: string;
558+
traceInsights?: unknown[];
559+
};
560+
t.assert.snapshot?.(
561+
JSON.stringify(typedStructuredContent.traceSummary, null, 2),
562+
);
563+
t.assert.snapshot?.(
564+
JSON.stringify(typedStructuredContent.traceInsights, null, 2),
565+
);
566+
});
567+
});
568+
});
569+
570+
describe('trace insights', () => {
571+
it('includes the trace insight output', async t => {
572+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
573+
const result = await parseRawTraceBuffer(rawData);
574+
if (!traceResultIsSuccess(result)) {
575+
throw new Error(result.error);
576+
}
577+
578+
await withMcpContext(async (response, context) => {
579+
response.attachTraceInsight(
580+
result,
581+
'NAVIGATION_0',
582+
'LCPBreakdown' as InsightName,
583+
);
584+
const {content} = await response.handle('test', context);
585+
586+
t.assert.snapshot?.(getTextContent(content[0]));
587+
});
588+
});
589+
590+
it('includes error if insight not found', async t => {
591+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
592+
const result = await parseRawTraceBuffer(rawData);
593+
if (!traceResultIsSuccess(result)) {
594+
throw new Error(result.error);
595+
}
596+
597+
await withMcpContext(async (response, context) => {
598+
response.attachTraceInsight(
599+
result,
600+
'BAD_ID',
601+
'LCPBreakdown' as InsightName,
602+
);
603+
const {content} = await response.handle('test', context);
604+
605+
t.assert.snapshot?.(getTextContent(content[0]));
606+
});
607+
});
608+
});
532609
});

tests/tools/performance.test.ts

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ describe('performance', () => {
194194
return result;
195195
}
196196

197-
it('returns the information on the insight', async t => {
197+
it('returns the information on the insight', async () => {
198198
const trace = await parseTrace('web-dev-with-commit.json.gz');
199199
await withMcpContext(async (response, context) => {
200200
context.storeTraceRecording(trace);
@@ -211,31 +211,7 @@ describe('performance', () => {
211211
context,
212212
);
213213

214-
t.assert.snapshot?.(response.responseLines.join('\n'));
215-
});
216-
});
217-
218-
it('returns an error if the insight does not exist', async () => {
219-
const trace = await parseTrace('web-dev-with-commit.json.gz');
220-
await withMcpContext(async (response, context) => {
221-
context.storeTraceRecording(trace);
222-
context.setIsRunningPerformanceTrace(false);
223-
224-
await analyzeInsight.handler(
225-
{
226-
params: {
227-
insightSetId: '8463DF94CD61B265B664E7F768183DE3',
228-
insightName: 'MadeUpInsightName',
229-
},
230-
},
231-
response,
232-
context,
233-
);
234-
assert.ok(
235-
response.responseLines
236-
.join('\n')
237-
.match(/No Performance Insights for the given insight set id/),
238-
);
214+
assert.ok(response.attachedTracedInsight);
239215
});
240216
});
241217

@@ -295,28 +271,18 @@ describe('performance', () => {
295271
});
296272
});
297273

298-
it('returns an error message if parsing the trace buffer fails', async t => {
274+
it('throws an error if parsing the trace buffer fails', async () => {
299275
await withMcpContext(async (response, context) => {
300276
context.setIsRunningPerformanceTrace(true);
301277
const selectedPage = context.getSelectedPage();
302278
sinon
303279
.stub(selectedPage.tracing, 'stop')
304280
.returns(Promise.resolve(undefined));
305-
await stopTrace.handler({params: {}}, response, context);
306-
t.assert.snapshot?.(response.responseLines.join('\n'));
307-
});
308-
});
309281

310-
it('returns the high level summary of the performance trace', async t => {
311-
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
312-
await withMcpContext(async (response, context) => {
313-
context.setIsRunningPerformanceTrace(true);
314-
const selectedPage = context.getSelectedPage();
315-
sinon.stub(selectedPage.tracing, 'stop').callsFake(async () => {
316-
return rawData;
317-
});
318-
await stopTrace.handler({params: {}}, response, context);
319-
t.assert.snapshot?.(response.responseLines.join('\n'));
282+
await assert.rejects(
283+
stopTrace.handler({params: {}}, response, context),
284+
/There was an unexpected error parsing the trace/,
285+
);
320286
});
321287
});
322288

0 commit comments

Comments
 (0)