feat: support stable id for network requests (#375)

This is part 1, we just expose the uid here, in a follow up PR we will
be used for finding the request.
This commit is contained in:
Nikolay Vitkov authored and GitHub committed 2025-10-14 12:45:16 +02:00
1 parent 76d5e9416d
commit f4d7b49bb1
8 files changed
+102 -40

No files matched your search

+4
View File
@@ -426,4 +426,8 @@ export class McpContext implements Context {
);
return waitForHelper.waitForEventsAfterAction(action);
}
getNetworkRequestStableId(request: HTTPRequest): number {
return this.#networkCollector.getIdForResource(request);
}
}
+7 -2
View File
@@ -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++;
}
+26 -4
View File
@@ -17,6 +17,21 @@ export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
[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> = T & {
[stableIdSymbol]?: number;
};
export class PageCollector<T> {
#browser: Browser;
#listenersInitializer: (
@@ -28,7 +43,7 @@ export class PageCollector<T> {
* As we use the reference to it.
* Use methods that manipulate the array in place.
*/
protected storage = new WeakMap<Page, T[]>();
protected storage = new WeakMap<Page, Array<WithSymbolId<T>>>();
constructor(
browser: Browser,
@@ -56,7 +71,6 @@ export class PageCollector<T> {
if (!page) {
return;
}
console.log('destro');
this.#cleanupPageDestroyed(page);
});
}
@@ -70,10 +84,14 @@ export class PageCollector<T> {
return;
}
const stored: T[] = [];
const idGenerator = createIdGenerator();
const stored: Array<WithSymbolId<T>> = [];
this.storage.set(page, stored);
const listeners = this.#listenersInitializer(value => {
stored.push(value);
const withId = value as WithSymbolId<T>;
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<T> {
getData(page: Page): T[] {
return this.storage.get(page) ?? [];
}
getIdForResource(resource: WithSymbolId<T>): number {
return resource[stableIdSymbol] ?? -1;
}
}
export class NetworkCollector extends PageCollector<HTTPRequest> {
+6 -2
View File
@@ -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 {
+19 -18
View File
@@ -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]`,
);
});
});
+24 -1
View File
@@ -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<HTTPRequest>(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);
});
});
+12 -12
View File
@@ -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]',
);
});
});
+4 -1
View File
@@ -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<string>;
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(