feat(library): record timings for APIRequestContext

This commit is contained in:
Simon Knott 2024-09-13 16:06:27 +02:00
parent 48c7fb6b06
commit e17585819a
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
3 changed files with 52 additions and 1 deletions

View file

@ -0,0 +1,7 @@
import { test, expect } from "@playwright/test";
test('should get posts', async ({ request }) => {
const posts = await request.get(`https://jsonplaceholder.typicode.com/posts/1`);
expect(posts.ok()).toBeTruthy();
expect(await posts.text()).toHaveLength(10)
});

View file

@ -41,6 +41,7 @@ import type * as types from './types';
import type { HeadersArray, ProxySettings } from './types'; import type { HeadersArray, ProxySettings } from './types';
import { kMaxCookieExpiresDateInSeconds } from './network'; import { kMaxCookieExpiresDateInSeconds } from './network';
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
import type * as har from '@trace/har';
type FetchRequestOptions = { type FetchRequestOptions = {
userAgent: string; userAgent: string;
@ -72,6 +73,7 @@ export type APIRequestFinishedEvent = {
statusCode: number; statusCode: number;
statusMessage: string; statusMessage: string;
body?: Buffer; body?: Buffer;
timings: har.Timings;
}; };
type SendRequestOptions = https.RequestOptions & { type SendRequestOptions = https.RequestOptions & {
@ -307,8 +309,30 @@ export abstract class APIRequestContext extends SdkObject {
// If we have a proxy agent already, do not override it. // If we have a proxy agent already, do not override it.
const agent = options.agent || (url.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent); const agent = options.agent || (url.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent);
const requestOptions = { ...options, agent }; const requestOptions = { ...options, agent };
const startAt = monotonicTime();
const timings: Record<'startAt' | 'requestFinishAt' | 'dnsLookupAt' | 'tcpConnectionAt' | 'tlsHandshakeAt' | 'firstByteAt' | 'endAt', number | undefined> = {
startAt,
requestFinishAt: undefined,
dnsLookupAt: undefined,
tcpConnectionAt: undefined,
tlsHandshakeAt: undefined,
firstByteAt: undefined,
endAt: undefined
};
const request = requestConstructor(url, requestOptions as any, async response => { const request = requestConstructor(url, requestOptions as any, async response => {
response.once('readable', () => { timings.firstByteAt = monotonicTime(); });
response.once('end', () => { timings.endAt = monotonicTime(); });
const notifyRequestFinished = (body?: Buffer) => { const notifyRequestFinished = (body?: Buffer) => {
const send = timings.requestFinishAt! - startAt;
const dnsLookup = timings.dnsLookupAt ? startAt - timings.dnsLookupAt : undefined;
const tcpConnection = timings.tcpConnectionAt! - (timings.dnsLookupAt ?? startAt);
const tlsHandshake = timings.tlsHandshakeAt ? (timings.tlsHandshakeAt - timings.tcpConnectionAt!) : undefined;
const firstByte = timings.firstByteAt! - (timings.tlsHandshakeAt ?? timings.tcpConnectionAt!);
const contentTransfer = timings.endAt! - timings.firstByteAt!;
const requestFinishedEvent: APIRequestFinishedEvent = { const requestFinishedEvent: APIRequestFinishedEvent = {
requestEvent, requestEvent,
httpVersion: response.httpVersion, httpVersion: response.httpVersion,
@ -317,7 +341,16 @@ export abstract class APIRequestContext extends SdkObject {
headers: response.headers, headers: response.headers,
rawHeaders: response.rawHeaders, rawHeaders: response.rawHeaders,
cookies, cookies,
body body,
timings: {
send,
wait: firstByte,
receive: contentTransfer,
dns: dnsLookup,
connect: tcpConnection,
ssl: tlsHandshake,
blocked: firstByte,
},
}; };
this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent);
}; };
@ -463,6 +496,13 @@ export abstract class APIRequestContext extends SdkObject {
this.on(APIRequestContext.Events.Dispose, disposeListener); this.on(APIRequestContext.Events.Dispose, disposeListener);
request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener)); request.on('close', () => this.off(APIRequestContext.Events.Dispose, disposeListener));
request.on('socket', socket => {
socket.on('lookup', () => { timings.dnsLookupAt = monotonicTime(); });
socket.on('connect', () => { timings.tcpConnectionAt = monotonicTime(); });
socket.on('secureConnect', () => { timings.tlsHandshakeAt = monotonicTime(); });
});
request.on('finish', () => { timings.requestFinishAt = monotonicTime(); });
progress.log(`${options.method} ${url.toString()}`); progress.log(`${options.method} ${url.toString()}`);
if (options.headers) { if (options.headers) {
for (const [name, value] of Object.entries(options.headers)) for (const [name, value] of Object.entries(options.headers))

View file

@ -212,6 +212,10 @@ export class HarTracer {
harEntry.response.statusText = event.statusMessage; harEntry.response.statusText = event.statusMessage;
harEntry.response.httpVersion = event.httpVersion; harEntry.response.httpVersion = event.httpVersion;
harEntry.response.redirectURL = event.headers.location || ''; harEntry.response.redirectURL = event.headers.location || '';
harEntry.timings = event.timings;
this._computeHarEntryTotalTime(harEntry);
for (let i = 0; i < event.rawHeaders.length; i += 2) { for (let i = 0; i < event.rawHeaders.length; i += 2) {
harEntry.response.headers.push({ harEntry.response.headers.push({
name: event.rawHeaders[i], name: event.rawHeaders[i],