feat(library): record timings for APIRequestContext
This commit is contained in:
parent
48c7fb6b06
commit
e17585819a
7
examples/github-api/tests/demo.spec.ts
Normal file
7
examples/github-api/tests/demo.spec.ts
Normal 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)
|
||||
});
|
||||
|
|
@ -41,6 +41,7 @@ import type * as types from './types';
|
|||
import type { HeadersArray, ProxySettings } from './types';
|
||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||
import type * as har from '@trace/har';
|
||||
|
||||
type FetchRequestOptions = {
|
||||
userAgent: string;
|
||||
|
|
@ -72,6 +73,7 @@ export type APIRequestFinishedEvent = {
|
|||
statusCode: number;
|
||||
statusMessage: string;
|
||||
body?: Buffer;
|
||||
timings: har.Timings;
|
||||
};
|
||||
|
||||
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.
|
||||
const agent = options.agent || (url.protocol === 'https:' ? httpsHappyEyeballsAgent : httpHappyEyeballsAgent);
|
||||
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 => {
|
||||
response.once('readable', () => { timings.firstByteAt = monotonicTime(); });
|
||||
response.once('end', () => { timings.endAt = monotonicTime(); });
|
||||
|
||||
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 = {
|
||||
requestEvent,
|
||||
httpVersion: response.httpVersion,
|
||||
|
|
@ -317,7 +341,16 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
headers: response.headers,
|
||||
rawHeaders: response.rawHeaders,
|
||||
cookies,
|
||||
body
|
||||
body,
|
||||
timings: {
|
||||
send,
|
||||
wait: firstByte,
|
||||
receive: contentTransfer,
|
||||
dns: dnsLookup,
|
||||
connect: tcpConnection,
|
||||
ssl: tlsHandshake,
|
||||
blocked: firstByte,
|
||||
},
|
||||
};
|
||||
this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent);
|
||||
};
|
||||
|
|
@ -463,6 +496,13 @@ export abstract class APIRequestContext extends SdkObject {
|
|||
this.on(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()}`);
|
||||
if (options.headers) {
|
||||
for (const [name, value] of Object.entries(options.headers))
|
||||
|
|
|
|||
|
|
@ -212,6 +212,10 @@ export class HarTracer {
|
|||
harEntry.response.statusText = event.statusMessage;
|
||||
harEntry.response.httpVersion = event.httpVersion;
|
||||
harEntry.response.redirectURL = event.headers.location || '';
|
||||
|
||||
harEntry.timings = event.timings;
|
||||
this._computeHarEntryTotalTime(harEntry);
|
||||
|
||||
for (let i = 0; i < event.rawHeaders.length; i += 2) {
|
||||
harEntry.response.headers.push({
|
||||
name: event.rawHeaders[i],
|
||||
|
|
|
|||
Loading…
Reference in a new issue