diff --git a/src/McpContext.ts b/src/McpContext.ts index 4cf6b661..6bf2fa20 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -426,4 +426,8 @@ export class McpContext implements Context { ); return waitForHelper.waitForEventsAfterAction(action); } + + getNetworkRequestStableId(request: HTTPRequest): number { + return this.#networkCollector.getIdForResource(request); + } } diff --git a/src/McpResponse.ts b/src/McpResponse.ts index 4ea9f796..ab36a511 100644 --- a/src/McpResponse.ts +++ b/src/McpResponse.ts @@ -246,7 +246,12 @@ Call ${handleDialog.name} to handle it before continuing.`); ); response.push(...data.info); for (const request of data.items) { - response.push(getShortDescriptionForRequest(request)); + response.push( + getShortDescriptionForRequest( + request, + context.getNetworkRequestStableId(request), + ), + ); } } else { response.push('No requests found.'); @@ -347,7 +352,7 @@ Call ${handleDialog.name} to handle it before continuing.`); let indent = 0; for (const request of redirectChain.reverse()) { response.push( - `${' '.repeat(indent)}${getShortDescriptionForRequest(request)}`, + `${' '.repeat(indent)}${getShortDescriptionForRequest(request, context.getNetworkRequestStableId(request))}`, ); indent++; } diff --git a/src/PageCollector.ts b/src/PageCollector.ts index acbf3239..fa10c215 100644 --- a/src/PageCollector.ts +++ b/src/PageCollector.ts @@ -17,6 +17,21 @@ export type ListenerMap = { [K in keyof EventMap]?: (event: EventMap[K]) => void; }; +function createIdGenerator() { + let i = 1; + return () => { + if (i === Number.MAX_SAFE_INTEGER) { + i = 0; + } + return i++; + }; +} + +export const stableIdSymbol = Symbol('stableIdSymbol'); +type WithSymbolId = T & { + [stableIdSymbol]?: number; +}; + export class PageCollector { #browser: Browser; #listenersInitializer: ( @@ -28,7 +43,7 @@ export class PageCollector { * As we use the reference to it. * Use methods that manipulate the array in place. */ - protected storage = new WeakMap(); + protected storage = new WeakMap>>(); constructor( browser: Browser, @@ -56,7 +71,6 @@ export class PageCollector { if (!page) { return; } - console.log('destro'); this.#cleanupPageDestroyed(page); }); } @@ -70,10 +84,14 @@ export class PageCollector { return; } - const stored: T[] = []; + const idGenerator = createIdGenerator(); + const stored: Array> = []; this.storage.set(page, stored); + const listeners = this.#listenersInitializer(value => { - stored.push(value); + const withId = value as WithSymbolId; + withId[stableIdSymbol] = idGenerator(); + stored.push(withId); }); listeners['framenavigated'] = (frame: Frame) => { // Only reset the storage on main frame navigation @@ -111,6 +129,10 @@ export class PageCollector { getData(page: Page): T[] { return this.storage.get(page) ?? []; } + + getIdForResource(resource: WithSymbolId): number { + return resource[stableIdSymbol] ?? -1; + } } export class NetworkCollector extends PageCollector { diff --git a/src/formatters/networkFormatter.ts b/src/formatters/networkFormatter.ts index 7796f01a..43be8ae6 100644 --- a/src/formatters/networkFormatter.ts +++ b/src/formatters/networkFormatter.ts @@ -10,8 +10,12 @@ import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; const BODY_CONTEXT_SIZE_LIMIT = 10000; -export function getShortDescriptionForRequest(request: HTTPRequest): string { - return `${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; +export function getShortDescriptionForRequest( + request: HTTPRequest, + id: number, +): string { + // TODO truncate the URL + return `reqid ${id} - ${request.url()} ${request.method()} ${getStatusFromRequest(request)}`; } export function getStatusFromRequest(request: HTTPRequest): string { diff --git a/tests/McpResponse.test.ts b/tests/McpResponse.test.ts index 11416447..266495f8 100644 --- a/tests/McpResponse.test.ts +++ b/tests/McpResponse.test.ts @@ -202,15 +202,16 @@ Call handle_dialog to handle it before continuing.`, await withBrowser(async (response, context) => { response.setIncludeNetworkRequests(true); context.getNetworkRequests = () => { - return [getMockRequest()]; + return [getMockRequest({stableId: 1}), getMockRequest({stableId: 2})]; }; const result = await response.handle('test', context); assert.strictEqual( result[0].text, `# test response ## Network requests -Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +Showing 1-2 of 2 (Page 1 of 1). +reqid 1 - http://example.com GET [pending] +reqid 2 - http://example.com GET [pending]`, ); }); }); @@ -266,7 +267,7 @@ ${JSON.stringify({request: 'body'})} ${JSON.stringify({response: 'body'})} ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com POST [success - 200]`, +reqid 1 - http://example.com POST [success - 200]`, ); }); }); @@ -289,7 +290,7 @@ Status: [pending] - content-size:10 ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -354,8 +355,8 @@ describe('McpResponse network request filtering', () => { `# test response ## Network requests Showing 1-2 of 2 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -378,7 +379,7 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-1 of 1 (Page 1 of 1). -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -423,11 +424,11 @@ No requests found.`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); @@ -452,11 +453,11 @@ http://example.com GET [pending]`, `# test response ## Network requests Showing 1-5 of 5 (Page 1 of 1). -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending] -http://example.com GET [pending]`, +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending] +reqid 1 - http://example.com GET [pending]`, ); }); }); diff --git a/tests/PageCollector.test.ts b/tests/PageCollector.test.ts index 43c82380..431687aa 100644 --- a/tests/PageCollector.test.ts +++ b/tests/PageCollector.test.ts @@ -6,7 +6,7 @@ import assert from 'node:assert'; import {describe, it} from 'node:test'; -import type {Browser, Frame, Page, Target} from 'puppeteer-core'; +import type {Browser, Frame, HTTPRequest, Page, Target} from 'puppeteer-core'; import type {ListenerMap} from '../src/PageCollector.js'; import {PageCollector} from '../src/PageCollector.js'; @@ -196,4 +196,27 @@ describe('PageCollector', () => { assert.equal(collector.getData(page).length, 0); }); + + it('should assign ids to requests', async () => { + const browser = getMockBrowser(); + const page = (await browser.pages())[0]; + const request1 = getMockRequest(); + const request2 = getMockRequest(); + const collector = new PageCollector(browser, collect => { + return { + request: req => { + collect(req); + }, + } as ListenerMap; + }); + await collector.init(); + + page.emit('request', request1); + page.emit('request', request2); + + assert.equal(collector.getData(page).length, 2); + + assert.equal(collector.getIdForResource(request1), 1); + assert.equal(collector.getIdForResource(request2), 2); + }); }); diff --git a/tests/formatters/networkFormatter.test.ts b/tests/formatters/networkFormatter.test.ts index 23c8a323..7f2fddc4 100644 --- a/tests/formatters/networkFormatter.test.ts +++ b/tests/formatters/networkFormatter.test.ts @@ -21,40 +21,40 @@ describe('networkFormatter', () => { describe('getShortDescriptionForRequest', () => { it('works', async () => { const request = getMockRequest(); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [pending]'); + assert.equal(result, 'reqid 1 - http://example.com GET [pending]'); }); it('shows correct method', async () => { const request = getMockRequest({method: 'POST'}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com POST [pending]'); + assert.equal(result, 'reqid 1 - http://example.com POST [pending]'); }); it('shows correct status for request with response code in 200', async () => { const response = getMockResponse(); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [success - 200]'); + assert.equal(result, 'reqid 1 - http://example.com GET [success - 200]'); }); it('shows correct status for request with response code in 100', async () => { const response = getMockResponse({ status: 199, }); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [failed - 199]'); + assert.equal(result, 'reqid 1 - http://example.com GET [failed - 199]'); }); it('shows correct status for request with response code above 200', async () => { const response = getMockResponse({ status: 300, }); const request = getMockRequest({response}); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); - assert.equal(result, 'http://example.com GET [failed - 300]'); + assert.equal(result, 'reqid 1 - http://example.com GET [failed - 300]'); }); it('shows correct status for request that failed', async () => { const request = getMockRequest({ @@ -64,11 +64,11 @@ describe('networkFormatter', () => { }; }, }); - const result = getShortDescriptionForRequest(request); + const result = getShortDescriptionForRequest(request, 1); assert.equal( result, - 'http://example.com GET [failed - Error in Network]', + 'reqid 1 - http://example.com GET [failed - Error in Network]', ); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 0197e181..48cd6b77 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -10,6 +10,7 @@ import type {HTTPRequest, HTTPResponse} from 'puppeteer-core'; import {McpContext} from '../src/McpContext.js'; import {McpResponse} from '../src/McpResponse.js'; +import {stableIdSymbol} from '../src/PageCollector.js'; let browser: Browser | undefined; @@ -49,6 +50,7 @@ export function getMockRequest( hasPostData?: boolean; postData?: string; fetchPostData?: Promise; + stableId?: number; } = {}, ): HTTPRequest { return { @@ -84,7 +86,8 @@ export function getMockRequest( redirectChain(): HTTPRequest[] { return []; }, - } as HTTPRequest; + [stableIdSymbol]: options.stableId ?? 1, + } as unknown as HTTPRequest; } export function getMockResponse(