Skip to content

Commit

Permalink
feat(har): add bodySize, transportSize, headersSize (microsoft#7470)
Browse files Browse the repository at this point in the history
Co-authored-by: tnolet <[email protected]>
  • Loading branch information
mxschmitt and tnolet authored Jul 8, 2021
1 parent 07d4458 commit 1cc2a2d
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.swp
*.pyc
.vscode
.idea
yarn.lock
/src/generated/*
lib/
Expand Down
4 changes: 2 additions & 2 deletions src/server/chromium/crNetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export class CRNetworkManager {
responseStart: -1,
};
}
const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody);
const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody, responsePayload.protocol);
if (responsePayload?.remoteIPAddress && typeof responsePayload?.remotePort === 'number') {
response._serverAddrFinished({
ipAddress: responsePayload.remoteIPAddress,
Expand Down Expand Up @@ -361,7 +361,7 @@ export class CRNetworkManager {
// event from protocol. @see https://crbug.com/883475
const response = request.request._existingResponse();
if (response)
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp), undefined, event.encodedDataLength);
this._requestIdToRequest.delete(request._requestId);
if (request._interceptionId)
this._attemptedAuthentications.delete(request._interceptionId);
Expand Down
2 changes: 1 addition & 1 deletion src/server/firefox/ffNetworkManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class FFNetworkManager {
response._requestFinished(this._relativeTiming(event.responseEndTime), 'Response body is unavailable for redirect responses');
} else {
this._requests.delete(request._id);
response._requestFinished(this._relativeTiming(event.responseEndTime));
response._requestFinished(this._relativeTiming(event.responseEndTime), undefined, event.transferSize);
}
this._page._frameManager.requestFinished(request.request);
}
Expand Down
12 changes: 10 additions & 2 deletions src/server/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,10 @@ export class Response extends SdkObject {
private _serverAddrPromiseCallback: (arg?: RemoteAddr) => void = () => {};
private _securityDetailsPromise: Promise<SecurityDetails|undefined>;
private _securityDetailsPromiseCallback: (arg?: SecurityDetails) => void = () => {};
_httpVersion: string | undefined;
_transferSize: number | undefined;

constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) {
constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback, httpVersion?: string) {
super(request.frame(), 'response');
this._request = request;
this._timing = timing;
Expand All @@ -333,6 +335,7 @@ export class Response extends SdkObject {
this._finishedPromiseCallback = f;
});
this._request._setResponse(this);
this._httpVersion = httpVersion;
}

_serverAddrFinished(addr?: RemoteAddr) {
Expand All @@ -343,11 +346,16 @@ export class Response extends SdkObject {
this._securityDetailsPromiseCallback(securityDetails);
}

_requestFinished(responseEndTiming: number, error?: string) {
_requestFinished(responseEndTiming: number, error?: string, transferSize?: number) {
this._request._responseEndTiming = Math.max(responseEndTiming, this._timing.responseStart);
this._transferSize = transferSize;
this._finishedPromiseCallback({ error });
}

_setHttpVersion(httpVersion: string) {
this._httpVersion = httpVersion;
}

url(): string {
return this._url;
}
Expand Down
1 change: 1 addition & 0 deletions src/server/supplements/har/har.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export type Response = {
redirectURL: string;
headersSize: number;
bodySize: number;
_transferSize: number;
};

export type Cookie = {
Expand Down
102 changes: 82 additions & 20 deletions src/server/supplements/har/harTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
* limitations under the License.
*/

import { URL } from 'url';
import fs from 'fs';
import { BrowserContext } from '../../browserContext';
import { helper } from '../../helper';
import * as network from '../../network';
import { Page } from '../../page';
import * as har from './har';
import * as types from '../../types';

const FALLBACK_HTTP_VERSION = 'HTTP/1.1';

type HarOptions = {
path: string;
Expand Down Expand Up @@ -51,6 +55,7 @@ export class HarTracer {
};
context.on(BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page));
context.on(BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request));
context.on(BrowserContext.Events.RequestFinished, (request: network.Request) => this._onRequestFinished(request).catch(() => {}));
context.on(BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response));
}

Expand Down Expand Up @@ -128,27 +133,28 @@ export class HarTracer {
request: {
method: request.method(),
url: request.url(),
httpVersion: 'HTTP/1.1',
httpVersion: FALLBACK_HTTP_VERSION,
cookies: [],
headers: [],
queryString: [...url.searchParams].map(e => ({ name: e[0], value: e[1] })),
postData: undefined,
postData: postDataForHar(request),
headersSize: -1,
bodySize: -1,
bodySize: calculateRequestBodySize(request) || 0,
},
response: {
status: -1,
statusText: '',
httpVersion: 'HTTP/1.1',
httpVersion: FALLBACK_HTTP_VERSION,
cookies: [],
headers: [],
content: {
size: -1,
mimeType: request.headerValue('content-type') || 'application/octet-stream',
mimeType: request.headerValue('content-type') || 'x-unknown',
},
headersSize: -1,
bodySize: -1,
redirectURL: ''
redirectURL: '',
_transferSize: -1
},
cache: {
beforeRequest: null,
Expand All @@ -168,29 +174,63 @@ export class HarTracer {
this._entries.set(request, harEntry);
}

private async _onRequestFinished(request: network.Request) {
const page = request.frame()._page;
const harEntry = this._entries.get(request)!;
const response = await request.response();

if (!response)
return;

const httpVersion = normaliseHttpVersion(response._httpVersion);
const transferSize = response._transferSize || -1;
const headersSize = calculateResponseHeadersSize(httpVersion, response.status(), response.statusText(), response.headers());
const bodySize = transferSize !== -1 ? transferSize - headersSize : -1;

harEntry.request.httpVersion = httpVersion;
harEntry.response.bodySize = bodySize;
harEntry.response.headersSize = headersSize;
harEntry.response._transferSize = transferSize;
harEntry.request.headersSize = calculateRequestHeadersSize(request.method(), request.url(), httpVersion, request.headers());

const promise = response.body().then(buffer => {
const content = harEntry.response.content;
content.size = buffer.length;
content.compression = harEntry.response.bodySize !== -1 ? buffer.length - harEntry.response.bodySize : 0;

if (!this._options.omitContent && buffer && buffer.length > 0) {
content.text = buffer.toString('base64');
content.encoding = 'base64';
}
}).catch(() => {});
this._addBarrier(page, promise);
}

private _onResponse(response: network.Response) {
const page = response.frame()._page;
const pageEntry = this._ensurePageEntry(page);
const harEntry = this._entries.get(response.request())!;
// Rewrite provisional headers with actual
const request = response.request();

harEntry.request.headers = request.headers().map(header => ({ name: header.name, value: header.value }));
harEntry.request.cookies = cookiesForHar(request.headerValue('cookie'), ';');
harEntry.request.postData = postDataForHar(request) || undefined;
harEntry.request.postData = postDataForHar(request);

harEntry.response = {
status: response.status(),
statusText: response.statusText(),
httpVersion: 'HTTP/1.1',
httpVersion: normaliseHttpVersion(response._httpVersion),
cookies: cookiesForHar(response.headerValue('set-cookie'), '\n'),
headers: response.headers().map(header => ({ name: header.name, value: header.value })),
content: {
size: -1,
mimeType: response.headerValue('content-type') || 'application/octet-stream',
mimeType: response.headerValue('content-type') || 'x-unknown',
},
headersSize: -1,
bodySize: -1,
redirectURL: ''
redirectURL: '',
_transferSize: -1
};
const timing = response.timing();
if (pageEntry.startedDateTime.valueOf() > timing.startTime)
Expand Down Expand Up @@ -220,14 +260,6 @@ export class HarTracer {
if (details)
harEntry._securityDetails = details;
}));

if (!this._options.omitContent && response.status() === 200) {
const promise = response.body().then(buffer => {
harEntry.response.content.text = buffer.toString('base64');
harEntry.response.content.encoding = 'base64';
}).catch(() => {});
this._addBarrier(page, promise);
}
}

async flush() {
Expand All @@ -246,10 +278,10 @@ export class HarTracer {
}
}

function postDataForHar(request: network.Request): har.PostData | null {
function postDataForHar(request: network.Request): har.PostData | undefined {
const postData = request.postDataBuffer();
if (!postData)
return null;
return;

const contentType = request.headerValue('content-type') || 'application/octet-stream';
const result: har.PostData = {
Expand Down Expand Up @@ -305,3 +337,33 @@ function parseCookie(c: string): har.Cookie {
}
return cookie;
}

function calculateResponseHeadersSize(protocol: string, status: number, statusText: string , headers: types.HeadersArray) {
let rawHeaders = `${protocol} ${status} ${statusText}\r\n`;
for (const header of headers)
rawHeaders += `${header.name}: ${header.value}\r\n`;
rawHeaders += '\r\n';
return rawHeaders.length;
}

function calculateRequestHeadersSize(method: string, url: string, httpVersion: string, headers: types.HeadersArray) {
let rawHeaders = `${method} ${(new URL(url)).pathname} ${httpVersion}\r\n`;
for (const header of headers)
rawHeaders += `${header.name}: ${header.value}\r\n`;
return rawHeaders.length;
}

function normaliseHttpVersion(httpVersion?: string) {
if (!httpVersion)
return FALLBACK_HTTP_VERSION;
if (httpVersion === 'http/1.1')
return 'HTTP/1.1';
return httpVersion;
}

function calculateRequestBodySize(request: network.Request): number|undefined {
const postData = request.postDataBuffer();
if (!postData)
return;
return new TextEncoder().encode(postData.toString('utf8')).length;
}
6 changes: 5 additions & 1 deletion src/server/webkit/wkPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,7 +1044,11 @@ export class WKPage implements PageDelegate {
validFrom: responseReceivedPayload?.response.security?.certificate?.validFrom,
validTo: responseReceivedPayload?.response.security?.certificate?.validUntil,
});
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp));
const { responseBodyBytesReceived, responseHeaderBytesReceived } = event.metrics || {};
const transferSize = responseBodyBytesReceived ? responseBodyBytesReceived + (responseHeaderBytesReceived || 0) : undefined;
if (event.metrics?.protocol)
response._setHttpVersion(event.metrics.protocol);
response._requestFinished(helper.secondsToRoundishMillis(event.timestamp - request._timestamp), undefined, transferSize);
}

this._requestIdToResponseReceivedPayloadEvent.delete(request._requestId);
Expand Down
Loading

0 comments on commit 1cc2a2d

Please sign in to comment.