Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,8 @@ export class McpContext implements Context {
}

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

Expand Down
68 changes: 68 additions & 0 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ import type {
Response,
SnapshotParams,
} from './tools/ToolDefinition.js';
import type {InsightName, TraceResult} from './trace-processing/parse.js';
import {getInsightOutput, getTraceSummary} from './trace-processing/parse.js';
import {paginate} from './utils/pagination.js';
import type {PaginationOptions} from './utils/types.js';

interface TraceInsightData {
trace: TraceResult;
insightSetId: string;
insightName: InsightName;
}

export class McpResponse implements Response {
#includePages = false;
#snapshotParams?: SnapshotParams;
Expand All @@ -35,6 +43,8 @@ export class McpResponse implements Response {
responseFilePath?: string;
};
#attachedConsoleMessageId?: number;
#attachedTraceSummary?: TraceResult;
#attachedTraceInsight?: TraceInsightData;
#textResponseLines: string[] = [];
#images: ImageContentData[] = [];
#networkRequestsOptions?: {
Expand Down Expand Up @@ -137,10 +147,34 @@ export class McpResponse implements Response {
this.#attachedConsoleMessageId = msgid;
}

attachTraceSummary(result: TraceResult): void {
this.#attachedTraceSummary = result;
}

attachTraceInsight(
trace: TraceResult,
insightSetId: string,
insightName: InsightName,
): void {
this.#attachedTraceInsight = {
trace,
insightSetId,
insightName,
};
}

get includePages(): boolean {
return this.#includePages;
}

get attachedTraceSummary(): TraceResult | undefined {
return this.#attachedTraceSummary;
}

get attachedTracedInsight(): TraceInsightData | undefined {
return this.#attachedTraceInsight;
}

get includeNetworkRequests(): boolean {
return this.#networkRequestsOptions?.include ?? false;
}
Expand Down Expand Up @@ -359,6 +393,8 @@ export class McpResponse implements Response {
snapshot,
detailedNetworkRequest,
networkRequests,
traceInsight: this.#attachedTraceInsight,
traceSummary: this.#attachedTraceSummary,
});
}

Expand All @@ -371,6 +407,8 @@ export class McpResponse implements Response {
snapshot: SnapshotFormatter | string | undefined;
detailedNetworkRequest?: NetworkFormatter;
networkRequests?: NetworkFormatter[];
traceSummary?: TraceResult;
traceInsight?: TraceInsightData;
},
): {content: Array<TextContent | ImageContent>; structuredContent: object} {
const response = [`# ${toolName} response`];
Expand Down Expand Up @@ -434,12 +472,42 @@ Call ${handleDialog.name} to handle it before continuing.`);
networkRequests?: object[];
consoleMessage?: object;
consoleMessages?: object[];
traceSummary?: string;
traceInsights?: Array<{insightName: string; insightKey: string}>;
} = {};

if (this.#tabId) {
structuredContent.tabId = this.#tabId;
}

if (data.traceSummary) {
const summary = getTraceSummary(data.traceSummary);
response.push(summary);
structuredContent.traceSummary = summary;
structuredContent.traceInsights = [];
for (const insightSet of data.traceSummary.insights?.values() ?? []) {
for (const [insightName, model] of Object.entries(insightSet.model)) {
structuredContent.traceInsights.push({
insightName,
insightKey: model.insightKey,
});
}
}
}

if (data.traceInsight) {
const insightOutput = getInsightOutput(
data.traceInsight.trace,
data.traceInsight.insightSetId,
data.traceInsight.insightName,
);
if ('error' in insightOutput) {
response.push(insightOutput.error);
} else {
response.push(insightOutput.output);
}
}

if (data.snapshot) {
if (typeof data.snapshot === 'string') {
response.push(`Saved snapshot to ${data.snapshot}.`);
Expand Down
8 changes: 7 additions & 1 deletion src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type {
Page,
Viewport,
} from '../third_party/index.js';
import type {TraceResult} from '../trace-processing/parse.js';
import type {InsightName, TraceResult} from '../trace-processing/parse.js';
import type {PaginationOptions} from '../utils/types.js';

import type {ToolCategory} from './categories.js';
Expand Down Expand Up @@ -86,6 +86,12 @@ export interface Response {
// Allows re-using DevTools data queried by some tools.
attachDevToolsData(data: DevToolsData): void;
setTabId(tabId: string): void;
attachTraceSummary(trace: TraceResult): void;
attachTraceInsight(
trace: TraceResult,
insightSetId: string,
insightName: InsightName,
): void;
}

/**
Expand Down
26 changes: 4 additions & 22 deletions src/tools/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@

import zlib from 'node:zlib';

import {logger} from '../logger.js';
import {zod} from '../third_party/index.js';
import type {Page} from '../third_party/index.js';
import type {InsightName} from '../trace-processing/parse.js';
import {
getInsightOutput,
getTraceSummary,
parseRawTraceBuffer,
traceResultIsSuccess,
} from '../trace-processing/parse.js';
Expand Down Expand Up @@ -168,17 +165,11 @@ export const analyzeInsight = defineTool({
return;
}

const insightOutput = getInsightOutput(
response.attachTraceInsight(
lastRecording,
request.params.insightSetId,
request.params.insightName as InsightName,
);
if ('error' in insightOutput) {
response.appendResponseLine(insightOutput.error);
return;
}

response.appendResponseLine(insightOutput.output);
},
});

Expand Down Expand Up @@ -212,21 +203,12 @@ async function stopTracingAndAppendOutput(
response.appendResponseLine('The performance trace has been stopped.');
if (traceResultIsSuccess(result)) {
context.storeTraceRecording(result);
const traceSummaryText = getTraceSummary(result);
response.appendResponseLine(traceSummaryText);
response.attachTraceSummary(result);
} else {
response.appendResponseLine(
'There was an unexpected error parsing the trace:',
throw new Error(
`There was an unexpected error parsing the trace: ${result.error}`,
);
response.appendResponseLine(result.error);
}
} catch (e) {
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
logger(`Error stopping performance trace: ${errorText}`);
response.appendResponseLine(
'An error occurred generating the response for this trace:',
);
response.appendResponseLine(errorText);
} finally {
context.setIsRunningPerformanceTrace(false);
}
Expand Down
4 changes: 2 additions & 2 deletions tests/McpContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ describe('McpContext', () => {
});
});

it('can store and retrieve performance traces', async () => {
it('can store and retrieve the latest performance trace', async () => {
await withMcpContext(async (_response, context) => {
const fakeTrace1 = {} as unknown as TraceResult;
const fakeTrace2 = {} as unknown as TraceResult;
context.storeTraceRecording(fakeTrace1);
context.storeTraceRecording(fakeTrace2);
assert.deepEqual(context.recordedTraces(), [fakeTrace1, fakeTrace2]);
assert.deepEqual(context.recordedTraces(), [fakeTrace2]);
Comment thread
OrKoN marked this conversation as resolved.
});
});

Expand Down
246 changes: 246 additions & 0 deletions tests/McpResponse.test.js.snapshot

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions tests/McpResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import {tmpdir} from 'node:os';
import {join} from 'node:path';
import {describe, it} from 'node:test';

import type {InsightName} from '../src/trace-processing/parse.js';
import {
parseRawTraceBuffer,
traceResultIsSuccess,
} from '../src/trace-processing/parse.js';

import {loadTraceAsBuffer} from './trace-processing/fixtures/load.js';
import {
getImageContent,
getMockAggregatedIssue,
Expand Down Expand Up @@ -529,4 +536,74 @@ describe('McpResponse network pagination', () => {
assert.ok(text.includes('Showing 1-2 of 5 (Page 1 of 3).'));
});
});

describe('trace summaries', () => {
it('includes the trace summary text and structured data', async t => {
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
const result = await parseRawTraceBuffer(rawData);
if (!traceResultIsSuccess(result)) {
throw new Error(result.error);
}

await withMcpContext(async (response, context) => {
response.attachTraceSummary(result);
const {content, structuredContent} = await response.handle(
'test',
context,
);

t.assert.snapshot?.(getTextContent(content[0]));
const typedStructuredContent = structuredContent as {
traceSummary?: string;
traceInsights?: unknown[];
};
t.assert.snapshot?.(
JSON.stringify(typedStructuredContent.traceSummary, null, 2),
);
t.assert.snapshot?.(
JSON.stringify(typedStructuredContent.traceInsights, null, 2),
);
});
});
});

describe('trace insights', () => {
it('includes the trace insight output', async t => {
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
const result = await parseRawTraceBuffer(rawData);
if (!traceResultIsSuccess(result)) {
throw new Error(result.error);
}

await withMcpContext(async (response, context) => {
response.attachTraceInsight(
result,
'NAVIGATION_0',
'LCPBreakdown' as InsightName,
);
const {content} = await response.handle('test', context);

t.assert.snapshot?.(getTextContent(content[0]));
});
});

it('includes error if insight not found', async t => {
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
const result = await parseRawTraceBuffer(rawData);
if (!traceResultIsSuccess(result)) {
throw new Error(result.error);
}

await withMcpContext(async (response, context) => {
response.attachTraceInsight(
result,
'BAD_ID',
'LCPBreakdown' as InsightName,
);
const {content} = await response.handle('test', context);

t.assert.snapshot?.(getTextContent(content[0]));
});
});
});
});
48 changes: 7 additions & 41 deletions tests/tools/performance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('performance', () => {
return result;
}

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

t.assert.snapshot?.(response.responseLines.join('\n'));
});
});

it('returns an error if the insight does not exist', async () => {
const trace = await parseTrace('web-dev-with-commit.json.gz');
await withMcpContext(async (response, context) => {
context.storeTraceRecording(trace);
context.setIsRunningPerformanceTrace(false);

await analyzeInsight.handler(
{
params: {
insightSetId: '8463DF94CD61B265B664E7F768183DE3',
insightName: 'MadeUpInsightName',
},
},
response,
context,
);
assert.ok(
response.responseLines
.join('\n')
.match(/No Performance Insights for the given insight set id/),
);
assert.ok(response.attachedTracedInsight);
});
});

Expand Down Expand Up @@ -295,28 +271,18 @@ describe('performance', () => {
});
});

it('returns an error message if parsing the trace buffer fails', async t => {
it('throws an error if parsing the trace buffer fails', async () => {
await withMcpContext(async (response, context) => {
context.setIsRunningPerformanceTrace(true);
const selectedPage = context.getSelectedPage();
sinon
.stub(selectedPage.tracing, 'stop')
.returns(Promise.resolve(undefined));
await stopTrace.handler({params: {}}, response, context);
t.assert.snapshot?.(response.responseLines.join('\n'));
});
});

it('returns the high level summary of the performance trace', async t => {
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
await withMcpContext(async (response, context) => {
context.setIsRunningPerformanceTrace(true);
const selectedPage = context.getSelectedPage();
sinon.stub(selectedPage.tracing, 'stop').callsFake(async () => {
return rawData;
});
await stopTrace.handler({params: {}}, response, context);
t.assert.snapshot?.(response.responseLines.join('\n'));
await assert.rejects(
stopTrace.handler({params: {}}, response, context),
/There was an unexpected error parsing the trace/,
);
});
});

Expand Down