diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 618a1f48..85ec10d6 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -234,6 +234,7 @@ - **autoStop** (boolean) **(required)**: Determines if the trace recording should be automatically stopped. - **reload** (boolean) **(required)**: Determines if, once tracing has started, the page should be automatically reloaded. +- **filePath** (string) _(optional)_: The absolute path, or a path relative to the current working directory, to save the raw trace data. --- @@ -241,7 +242,9 @@ **Description:** Stops the active performance trace recording on the selected page. -**Parameters:** None +**Parameters:** + +- **filePath** (string) _(optional)_: The absolute path, or a path relative to the current working directory, to save the raw trace data. --- diff --git a/src/tools/performance.ts b/src/tools/performance.ts index a8b24903..d81e35c6 100644 --- a/src/tools/performance.ts +++ b/src/tools/performance.ts @@ -25,7 +25,7 @@ export const startTrace = defineTool({ 'Starts a performance trace recording on the selected page. This can be used to look for performance problems and insights to improve the performance of the page. It will also report Core Web Vital (CWV) scores for the page.', annotations: { category: ToolCategory.PERFORMANCE, - readOnlyHint: true, + readOnlyHint: false, }, schema: { reload: zod @@ -38,6 +38,12 @@ export const startTrace = defineTool({ .describe( 'Determines if the trace recording should be automatically stopped.', ), + filePath: zod + .string() + .optional() + .describe( + 'The absolute path, or a path relative to the current working directory, to save the raw trace data.', + ), }, handler: async (request, response, context) => { if (context.isRunningPerformanceTrace()) { @@ -91,7 +97,12 @@ export const startTrace = defineTool({ if (request.params.autoStop) { await new Promise(resolve => setTimeout(resolve, 5_000)); - await stopTracingAndAppendOutput(page, response, context); + await stopTracingAndAppendOutput( + page, + response, + context, + request.params.filePath, + ); } else { response.appendResponseLine( `The performance trace is being recorded. Use performance_stop_trace to stop it.`, @@ -106,15 +117,27 @@ export const stopTrace = defineTool({ 'Stops the active performance trace recording on the selected page.', annotations: { category: ToolCategory.PERFORMANCE, - readOnlyHint: true, + readOnlyHint: false, + }, + schema: { + filePath: zod + .string() + .optional() + .describe( + 'The absolute path, or a path relative to the current working directory, to save the raw trace data.', + ), }, - schema: {}, - handler: async (_request, response, context) => { + handler: async (request, response, context) => { if (!context.isRunningPerformanceTrace()) { return; } const page = context.getSelectedPage(); - await stopTracingAndAppendOutput(page, response, context); + await stopTracingAndAppendOutput( + page, + response, + context, + request.params.filePath, + ); }, }); @@ -165,9 +188,16 @@ async function stopTracingAndAppendOutput( page: Page, response: Response, context: Context, + filePath?: string, ): Promise { try { const traceEventsBuffer = await page.tracing.stop(); + if (filePath && traceEventsBuffer) { + const file = await context.saveFile(traceEventsBuffer, filePath); + response.appendResponseLine( + `The raw trace data was saved to ${file.filename}.`, + ); + } const result = await parseRawTraceBuffer(traceEventsBuffer); response.appendResponseLine('The performance trace has been stopped.'); if (traceResultIsSuccess(result)) { diff --git a/tests/tools/performance.test.ts b/tests/tools/performance.test.ts index cb390b33..509557e8 100644 --- a/tests/tools/performance.test.ts +++ b/tests/tools/performance.test.ts @@ -138,6 +138,42 @@ describe('performance', () => { ); }); }); + + it('supports filePath', async () => { + const rawData = loadTraceAsBuffer('basic-trace.json.gz'); + await withMcpContext(async (response, context) => { + const filePath = 'test-trace.json'; + const selectedPage = context.getSelectedPage(); + sinon.stub(selectedPage, 'url').callsFake(() => 'https://www.test.com'); + sinon.stub(selectedPage, 'goto').callsFake(() => Promise.resolve(null)); + sinon.stub(selectedPage.tracing, 'start'); + sinon.stub(selectedPage.tracing, 'stop').resolves(rawData); + const saveFileStub = sinon + .stub(context, 'saveFile') + .resolves({filename: filePath}); + + const clock = sinon.useFakeTimers({shouldClearNativeTimers: true}); + try { + const handlerPromise = startTrace.handler( + {params: {reload: true, autoStop: true, filePath}}, + response, + context, + ); + await clock.tickAsync(6_000); + await handlerPromise; + + assert.ok( + response.responseLines.includes( + `The raw trace data was saved to ${filePath}.`, + ), + ); + sinon.assert.calledOnce(saveFileStub); + sinon.assert.calledWith(saveFileStub, rawData, filePath); + } finally { + clock.restore(); + } + }); + }); }); describe('performance_analyze_insight', () => { @@ -275,5 +311,31 @@ describe('performance', () => { t.assert.snapshot?.(response.responseLines.join('\n')); }); }); + + it('supports filePath', async () => { + const rawData = loadTraceAsBuffer('basic-trace.json.gz'); + await withMcpContext(async (response, context) => { + const filePath = 'test-trace.json'; + context.setIsRunningPerformanceTrace(true); + const selectedPage = context.getSelectedPage(); + const stopTracingStub = sinon + .stub(selectedPage.tracing, 'stop') + .resolves(rawData); + const saveFileStub = sinon + .stub(context, 'saveFile') + .resolves({filename: filePath}); + + await stopTrace.handler({params: {filePath}}, response, context); + + sinon.assert.calledOnce(stopTracingStub); + sinon.assert.calledOnce(saveFileStub); + sinon.assert.calledWith(saveFileStub, rawData, filePath); + assert.ok( + response.responseLines.includes( + `The raw trace data was saved to ${filePath}.`, + ), + ); + }); + }); }); });