feat(har): introduce the slim mode (#15053)
This commit is contained in:
parent
033c250f6d
commit
7bd72716f9
|
|
@ -563,6 +563,7 @@ Logger sink for Playwright logging.
|
|||
`false`. Deprecated, use `content` policy instead.
|
||||
- `content` ?<[HarContentPolicy]<"omit"|"embed"|"attach">> Optional setting to control resource content management. If `omit` is specified, content is not persisted. If `attach` is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. Defaults to `embed`, which stores content inline the HAR file as per HAR specification.
|
||||
- `path` <[path]> Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `attach` mode is used by default.
|
||||
- `mode` ?<[HarMode]<"full"|"minimal">> When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
|
||||
- `urlFilter` ?<[string]|[RegExp]> A glob or regex pattern to filter requests that are stored in the HAR. When a [`option: baseURL`] via the context options was provided and the passed URL is a path, it gets merged via the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
|
||||
Enables [HAR](http://www.softwareishard.com/blog/har-12-spec) recording for all pages into `recordHar.path` file. If not
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ async function launchContext(options: Options, headless: boolean, executablePath
|
|||
// HAR
|
||||
|
||||
if (options.saveHar) {
|
||||
contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar) };
|
||||
contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar), mode: 'minimal' };
|
||||
if (options.saveHarGlob)
|
||||
contextOptions.recordHar.urlFilter = options.saveHarGlob;
|
||||
contextOptions.serviceWorkers = 'block';
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
|
|||
urlGlob: isString(options.urlFilter) ? options.urlFilter : undefined,
|
||||
urlRegexSource: isRegExp(options.urlFilter) ? options.urlFilter.source : undefined,
|
||||
urlRegexFlags: isRegExp(options.urlFilter) ? options.urlFilter.flags : undefined,
|
||||
mode: options.mode
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'vie
|
|||
path: string,
|
||||
omitContent?: boolean,
|
||||
content?: 'omit' | 'embed' | 'attach',
|
||||
mode?: 'full' | 'minimal',
|
||||
urlFilter?: string | RegExp,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ export type SerializedError = {
|
|||
export type RecordHarOptions = {
|
||||
path: string,
|
||||
content?: 'embed' | 'attach' | 'omit',
|
||||
mode?: 'full' | 'minimal',
|
||||
urlGlob?: string,
|
||||
urlRegexSource?: string,
|
||||
urlRegexFlags?: string,
|
||||
|
|
|
|||
|
|
@ -231,6 +231,11 @@ RecordHarOptions:
|
|||
- embed
|
||||
- attach
|
||||
- omit
|
||||
mode:
|
||||
type: enum?
|
||||
literals:
|
||||
- full
|
||||
- minimal
|
||||
urlGlob: string?
|
||||
urlRegexSource: string?
|
||||
urlRegexFlags: string?
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scheme.RecordHarOptions = tObject({
|
||||
path: tString,
|
||||
content: tOptional(tEnum(['embed', 'attach', 'omit'])),
|
||||
mode: tOptional(tEnum(['full', 'minimal'])),
|
||||
urlGlob: tOptional(tString),
|
||||
urlRegexSource: tOptional(tString),
|
||||
urlRegexFlags: tOptional(tString),
|
||||
|
|
|
|||
|
|
@ -176,14 +176,14 @@ class HarBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private async _loadContent(content: { text?: string, encoding?: string, _sha1?: string }): Promise<Buffer> {
|
||||
const sha1 = content._sha1;
|
||||
private async _loadContent(content: { text?: string, encoding?: string, _file?: string }): Promise<Buffer> {
|
||||
const file = content._file;
|
||||
let buffer: Buffer;
|
||||
if (sha1) {
|
||||
if (file) {
|
||||
if (this._zipFile)
|
||||
buffer = await this._zipFile.read(sha1);
|
||||
buffer = await this._zipFile.read(file);
|
||||
else
|
||||
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, sha1));
|
||||
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
|
||||
} else {
|
||||
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,9 +64,8 @@ export type Entry = {
|
|||
timings: Timings;
|
||||
serverIPAddress?: string;
|
||||
connection?: string;
|
||||
_requestref: string;
|
||||
_frameref: string;
|
||||
_monotonicTime: number;
|
||||
_frameref?: string;
|
||||
_monotonicTime?: number;
|
||||
_serverPort?: number;
|
||||
_securityDetails?: SecurityDetails;
|
||||
};
|
||||
|
|
@ -95,7 +94,7 @@ export type Response = {
|
|||
headersSize: number;
|
||||
bodySize: number;
|
||||
comment?: string;
|
||||
_transferSize: number;
|
||||
_transferSize?: number;
|
||||
_failureText?: string
|
||||
};
|
||||
|
||||
|
|
@ -129,6 +128,7 @@ export type PostData = {
|
|||
text: string;
|
||||
comment?: string;
|
||||
_sha1?: string;
|
||||
_file?: string;
|
||||
};
|
||||
|
||||
export type Param = {
|
||||
|
|
@ -147,6 +147,7 @@ export type Content = {
|
|||
encoding?: string;
|
||||
comment?: string;
|
||||
_sha1?: string;
|
||||
_file?: string;
|
||||
};
|
||||
|
||||
export type Cache = {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export class HarRecorder {
|
|||
const content = options.content || (expectsZip ? 'attach' : 'embed');
|
||||
this._tracer = new HarTracer(context, this, {
|
||||
content,
|
||||
slimMode: options.mode === 'minimal',
|
||||
includeTraceInfo: false,
|
||||
waitForContentOnStop: true,
|
||||
skipScripts: false,
|
||||
urlFilter: urlFilterRe ?? options.urlGlob,
|
||||
|
|
@ -73,7 +75,7 @@ export class HarRecorder {
|
|||
const log = this._tracer.stop();
|
||||
log.entries = this._entries;
|
||||
|
||||
const harFileContent = JSON.stringify({ log }, undefined, 2);
|
||||
const harFileContent = jsonStringify({ log });
|
||||
|
||||
if (this._zipFile) {
|
||||
const result = new ManualPromise<void>();
|
||||
|
|
@ -95,3 +97,50 @@ export class HarRecorder {
|
|||
return this._artifact;
|
||||
}
|
||||
}
|
||||
|
||||
function jsonStringify(object: any): string {
|
||||
const tokens: string[] = [];
|
||||
innerJsonStringify(object, tokens, '', false, undefined);
|
||||
return tokens.join('');
|
||||
}
|
||||
|
||||
function innerJsonStringify(object: any, tokens: string[], indent: string, flat: boolean, parentKey: string | undefined) {
|
||||
if (typeof object !== 'object' || object === null) {
|
||||
tokens.push(JSON.stringify(object));
|
||||
return;
|
||||
}
|
||||
|
||||
const isArray = Array.isArray(object);
|
||||
if (!isArray && object.constructor.name !== 'Object') {
|
||||
tokens.push(JSON.stringify(object));
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = isArray ? object : Object.entries(object).filter(e => e[1] !== undefined);
|
||||
if (!entries.length) {
|
||||
tokens.push(isArray ? `[]` : `{}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const childIndent = `${indent} `;
|
||||
let brackets: { open: string, close: string };
|
||||
if (isArray)
|
||||
brackets = flat ? { open: '[', close: ']' } : { open: `[\n${childIndent}`, close: `\n${indent}]` };
|
||||
else
|
||||
brackets = flat ? { open: '{ ', close: ' }' } : { open: `{\n${childIndent}`, close: `\n${indent}}` };
|
||||
|
||||
tokens.push(brackets.open);
|
||||
|
||||
for (let i = 0; i < entries.length; ++i) {
|
||||
const entry = entries[i];
|
||||
if (i)
|
||||
tokens.push(flat ? `, ` : `,\n${childIndent}`);
|
||||
if (!isArray)
|
||||
tokens.push(`${JSON.stringify(entry[0])}: `);
|
||||
const key = isArray ? undefined : entry[0];
|
||||
const flatten = flat || key === 'timings' || parentKey === 'headers';
|
||||
innerJsonStringify(isArray ? entry : entry[1], tokens, childIndent, flatten, key);
|
||||
}
|
||||
|
||||
tokens.push(brackets.close);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,16 @@ export interface HarTracerDelegate {
|
|||
type HarTracerOptions = {
|
||||
content: 'omit' | 'attach' | 'embed';
|
||||
skipScripts: boolean;
|
||||
includeTraceInfo: boolean;
|
||||
waitForContentOnStop: boolean;
|
||||
urlFilter?: string | RegExp;
|
||||
slimMode?: boolean;
|
||||
omitSecurityDetails?: boolean;
|
||||
omitCookies?: boolean;
|
||||
omitTiming?: boolean;
|
||||
omitServerIP?: boolean;
|
||||
omitPages?: boolean;
|
||||
omitSizes?: boolean;
|
||||
};
|
||||
|
||||
export class HarTracer {
|
||||
|
|
@ -61,6 +69,14 @@ export class HarTracer {
|
|||
this._context = context;
|
||||
this._delegate = delegate;
|
||||
this._options = options;
|
||||
if (options.slimMode) {
|
||||
options.omitSecurityDetails = true;
|
||||
options.omitCookies = true;
|
||||
options.omitTiming = true;
|
||||
options.omitServerIP = true;
|
||||
options.omitSizes = true;
|
||||
options.omitPages = true;
|
||||
}
|
||||
this._entrySymbol = Symbol('requestHarEntry');
|
||||
this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL;
|
||||
}
|
||||
|
|
@ -92,32 +108,34 @@ export class HarTracer {
|
|||
return (request as any)[this._entrySymbol];
|
||||
}
|
||||
|
||||
private _ensurePageEntry(page: Page) {
|
||||
private _ensurePageEntry(page: Page): har.Page | undefined {
|
||||
if (this._options.omitPages)
|
||||
return;
|
||||
let pageEntry = this._pageEntries.get(page);
|
||||
if (!pageEntry) {
|
||||
page.mainFrame().on(Frame.Events.AddLifecycle, (event: LifecycleEvent) => {
|
||||
if (event === 'load')
|
||||
this._onLoad(page);
|
||||
if (event === 'domcontentloaded')
|
||||
this._onDOMContentLoaded(page);
|
||||
});
|
||||
|
||||
pageEntry = {
|
||||
startedDateTime: new Date(),
|
||||
id: page.guid,
|
||||
title: '',
|
||||
pageTimings: {
|
||||
pageTimings: this._options.omitTiming ? {} : {
|
||||
onContentLoad: -1,
|
||||
onLoad: -1,
|
||||
},
|
||||
};
|
||||
|
||||
page.mainFrame().on(Frame.Events.AddLifecycle, (event: LifecycleEvent) => {
|
||||
if (event === 'load')
|
||||
this._onLoad(page, pageEntry!);
|
||||
if (event === 'domcontentloaded')
|
||||
this._onDOMContentLoaded(page, pageEntry!);
|
||||
});
|
||||
|
||||
this._pageEntries.set(page, pageEntry);
|
||||
}
|
||||
return pageEntry;
|
||||
}
|
||||
|
||||
private _onDOMContentLoaded(page: Page) {
|
||||
const pageEntry = this._ensurePageEntry(page);
|
||||
private _onDOMContentLoaded(page: Page, pageEntry: har.Page) {
|
||||
const promise = page.mainFrame().evaluateExpression(String(() => {
|
||||
return {
|
||||
title: document.title,
|
||||
|
|
@ -125,13 +143,13 @@ export class HarTracer {
|
|||
};
|
||||
}), true, undefined, 'utility').then(result => {
|
||||
pageEntry.title = result.title;
|
||||
if (!this._options.omitTiming)
|
||||
pageEntry.pageTimings.onContentLoad = result.domContentLoaded;
|
||||
}).catch(() => {});
|
||||
this._addBarrier(page, promise);
|
||||
}
|
||||
|
||||
private _onLoad(page: Page) {
|
||||
const pageEntry = this._ensurePageEntry(page);
|
||||
private _onLoad(page: Page, pageEntry: har.Page) {
|
||||
const promise = page.mainFrame().evaluateExpression(String(() => {
|
||||
return {
|
||||
title: document.title,
|
||||
|
|
@ -139,6 +157,7 @@ export class HarTracer {
|
|||
};
|
||||
}), true, undefined, 'utility').then(result => {
|
||||
pageEntry.title = result.title;
|
||||
if (!this._options.omitTiming)
|
||||
pageEntry.pageTimings.onLoad = result.loaded;
|
||||
}).catch(() => {});
|
||||
this._addBarrier(page, promise);
|
||||
|
|
@ -161,10 +180,12 @@ export class HarTracer {
|
|||
private _onAPIRequest(event: APIRequestEvent) {
|
||||
if (!this._shouldIncludeEntryWithUrl(event.url.toString()))
|
||||
return;
|
||||
const harEntry = createHarEntry(event.method, event.url, '', '');
|
||||
const harEntry = createHarEntry(event.method, event.url, undefined, this._options);
|
||||
if (!this._options.omitCookies)
|
||||
harEntry.request.cookies = event.cookies;
|
||||
harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value }));
|
||||
harEntry.request.postData = this._postDataForBuffer(event.postData || null, event.headers['content-type'], this._options.content);
|
||||
if (!this._options.omitSizes)
|
||||
harEntry.request.bodySize = event.postData?.length || 0;
|
||||
(event as any)[this._entrySymbol] = harEntry;
|
||||
if (this._started)
|
||||
|
|
@ -186,7 +207,7 @@ export class HarTracer {
|
|||
value: event.rawHeaders[i + 1]
|
||||
});
|
||||
}
|
||||
harEntry.response.cookies = event.cookies.map(c => {
|
||||
harEntry.response.cookies = this._options.omitCookies ? [] : event.cookies.map(c => {
|
||||
return {
|
||||
...c,
|
||||
expires: c.expires === -1 ? undefined : new Date(c.expires)
|
||||
|
|
@ -212,9 +233,11 @@ export class HarTracer {
|
|||
return;
|
||||
|
||||
const pageEntry = this._ensurePageEntry(page);
|
||||
const harEntry = createHarEntry(request.method(), url, request.guid, request.frame().guid);
|
||||
const harEntry = createHarEntry(request.method(), url, request.frame().guid, this._options);
|
||||
if (pageEntry)
|
||||
harEntry.pageref = pageEntry.id;
|
||||
harEntry.request.postData = this._postDataForRequest(request, this._options.content);
|
||||
if (!this._options.omitSizes)
|
||||
harEntry.request.bodySize = request.bodySize();
|
||||
if (request.redirectedFrom()) {
|
||||
const fromEntry = this._entryForRequest(request.redirectedFrom()!);
|
||||
|
|
@ -238,7 +261,7 @@ export class HarTracer {
|
|||
harEntry.request.httpVersion = httpVersion;
|
||||
harEntry.response.httpVersion = httpVersion;
|
||||
|
||||
const compressionCalculationBarrier = {
|
||||
const compressionCalculationBarrier = this._options.omitSizes ? undefined : {
|
||||
_encodedBodySize: -1,
|
||||
_decodedBodySize: -1,
|
||||
barrier: new ManualPromise<void>(),
|
||||
|
|
@ -257,33 +280,37 @@ export class HarTracer {
|
|||
this._check();
|
||||
}
|
||||
};
|
||||
if (compressionCalculationBarrier)
|
||||
this._addBarrier(page, compressionCalculationBarrier.barrier);
|
||||
|
||||
const promise = response.body().then(buffer => {
|
||||
if (this._options.skipScripts && request.resourceType() === 'script') {
|
||||
compressionCalculationBarrier.setDecodedBodySize(0);
|
||||
compressionCalculationBarrier?.setDecodedBodySize(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = harEntry.response.content;
|
||||
compressionCalculationBarrier.setDecodedBodySize(buffer.length);
|
||||
compressionCalculationBarrier?.setDecodedBodySize(buffer.length);
|
||||
this._storeResponseContent(buffer, content, request.resourceType());
|
||||
}).catch(() => {
|
||||
compressionCalculationBarrier.setDecodedBodySize(0);
|
||||
compressionCalculationBarrier?.setDecodedBodySize(0);
|
||||
}).then(() => {
|
||||
if (this._started)
|
||||
this._delegate.onEntryFinished(harEntry);
|
||||
});
|
||||
this._addBarrier(page, promise);
|
||||
|
||||
if (!this._options.omitSizes) {
|
||||
this._addBarrier(page, response.sizes().then(sizes => {
|
||||
harEntry.response.bodySize = sizes.responseBodySize;
|
||||
harEntry.response.headersSize = sizes.responseHeadersSize;
|
||||
// Fallback for WebKit by calculating it manually
|
||||
harEntry.response._transferSize = response.request().responseSize.transferSize || (sizes.responseHeadersSize + sizes.responseBodySize);
|
||||
harEntry.request.headersSize = sizes.requestHeadersSize;
|
||||
compressionCalculationBarrier.setEncodedBodySize(sizes.responseBodySize);
|
||||
compressionCalculationBarrier?.setEncodedBodySize(sizes.responseBodySize);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private async _onRequestFailed(request: network.Request) {
|
||||
const harEntry = this._entryForRequest(request);
|
||||
|
|
@ -301,7 +328,10 @@ export class HarTracer {
|
|||
content.size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._options.omitSizes)
|
||||
content.size = buffer.length;
|
||||
|
||||
if (this._options.content === 'embed') {
|
||||
// Sometimes, we can receive a font/media file with textual mime type. Browser
|
||||
// still interprets them correctly, but the 'content-type' header is obviously wrong.
|
||||
|
|
@ -312,9 +342,13 @@ export class HarTracer {
|
|||
content.encoding = 'base64';
|
||||
}
|
||||
} else if (this._options.content === 'attach') {
|
||||
content._sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(content.mimeType) || 'dat');
|
||||
const sha1 = calculateSha1(buffer) + '.' + (mime.getExtension(content.mimeType) || 'dat');
|
||||
if (this._options.includeTraceInfo)
|
||||
content._sha1 = sha1;
|
||||
else
|
||||
content._file = sha1;
|
||||
if (this._started)
|
||||
this._delegate.onContentBlob(content._sha1, buffer);
|
||||
this._delegate.onContentBlob(sha1, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -340,16 +374,19 @@ export class HarTracer {
|
|||
headersSize: -1,
|
||||
bodySize: -1,
|
||||
redirectURL: '',
|
||||
_transferSize: -1
|
||||
_transferSize: this._options.omitSizes ? undefined : -1
|
||||
};
|
||||
|
||||
if (!this._options.omitTiming) {
|
||||
const timing = response.timing();
|
||||
if (pageEntry.startedDateTime.valueOf() > timing.startTime)
|
||||
if (pageEntry && pageEntry.startedDateTime.valueOf() > timing.startTime)
|
||||
pageEntry.startedDateTime = new Date(timing.startTime);
|
||||
const dns = timing.domainLookupEnd !== -1 ? helper.millisToRoundishMillis(timing.domainLookupEnd - timing.domainLookupStart) : -1;
|
||||
const connect = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.connectStart) : -1;
|
||||
const ssl = timing.connectEnd !== -1 ? helper.millisToRoundishMillis(timing.connectEnd - timing.secureConnectionStart) : -1;
|
||||
const wait = timing.responseStart !== -1 ? helper.millisToRoundishMillis(timing.responseStart - timing.requestStart) : -1;
|
||||
const receive = response.request()._responseEndTiming !== -1 ? helper.millisToRoundishMillis(response.request()._responseEndTiming - timing.responseStart) : -1;
|
||||
|
||||
harEntry.timings = {
|
||||
dns,
|
||||
connect,
|
||||
|
|
@ -359,24 +396,34 @@ export class HarTracer {
|
|||
receive,
|
||||
};
|
||||
harEntry.time = [dns, connect, ssl, wait, receive].reduce((pre, cur) => cur > 0 ? cur + pre : pre, 0);
|
||||
}
|
||||
|
||||
if (!this._options.omitServerIP) {
|
||||
this._addBarrier(page, response.serverAddr().then(server => {
|
||||
if (server?.ipAddress)
|
||||
harEntry.serverIPAddress = server.ipAddress;
|
||||
if (server?.port)
|
||||
harEntry._serverPort = server.port;
|
||||
}));
|
||||
}
|
||||
if (!this._options.omitSecurityDetails) {
|
||||
this._addBarrier(page, response.securityDetails().then(details => {
|
||||
if (details)
|
||||
harEntry._securityDetails = details;
|
||||
}));
|
||||
}
|
||||
this._addBarrier(page, request.rawRequestHeaders().then(headers => {
|
||||
if (!this._options.omitCookies) {
|
||||
for (const header of headers.filter(header => header.name.toLowerCase() === 'cookie'))
|
||||
harEntry.request.cookies.push(...header.value.split(';').map(parseCookie));
|
||||
}
|
||||
harEntry.request.headers = headers;
|
||||
}));
|
||||
this._addBarrier(page, response.rawResponseHeaders().then(headers => {
|
||||
if (!this._options.omitCookies) {
|
||||
for (const header of headers.filter(header => header.name.toLowerCase() === 'set-cookie'))
|
||||
harEntry.response.cookies.push(parseCookie(header.value));
|
||||
}
|
||||
harEntry.response.headers = headers;
|
||||
const contentType = headers.find(header => header.name.toLowerCase() === 'content-type');
|
||||
if (contentType)
|
||||
|
|
@ -404,19 +451,21 @@ export class HarTracer {
|
|||
name: context?._browser.options.name || '',
|
||||
version: context?._browser.version() || ''
|
||||
},
|
||||
pages: Array.from(this._pageEntries.values()),
|
||||
pages: this._pageEntries.size ? Array.from(this._pageEntries.values()) : undefined,
|
||||
entries: [],
|
||||
};
|
||||
for (const pageEntry of log.pages!) {
|
||||
if (pageEntry.pageTimings.onContentLoad! >= 0)
|
||||
pageEntry.pageTimings.onContentLoad! -= pageEntry.startedDateTime.valueOf();
|
||||
if (!this._options.omitTiming) {
|
||||
for (const pageEntry of log.pages || []) {
|
||||
if (typeof pageEntry.pageTimings.onContentLoad === 'number' && pageEntry.pageTimings.onContentLoad >= 0)
|
||||
pageEntry.pageTimings.onContentLoad -= pageEntry.startedDateTime.valueOf();
|
||||
else
|
||||
pageEntry.pageTimings.onContentLoad = -1;
|
||||
if (pageEntry.pageTimings.onLoad! >= 0)
|
||||
pageEntry.pageTimings.onLoad! -= pageEntry.startedDateTime.valueOf();
|
||||
if (typeof pageEntry.pageTimings.onLoad === 'number' && pageEntry.pageTimings.onLoad >= 0)
|
||||
pageEntry.pageTimings.onLoad -= pageEntry.startedDateTime.valueOf();
|
||||
else
|
||||
pageEntry.pageTimings.onLoad = -1;
|
||||
}
|
||||
}
|
||||
this._pageEntries.clear();
|
||||
return log;
|
||||
}
|
||||
|
|
@ -446,8 +495,12 @@ export class HarTracer {
|
|||
result.text = postData.toString();
|
||||
|
||||
if (content === 'attach') {
|
||||
result._sha1 = calculateSha1(postData) + '.' + (mime.getExtension(contentType) || 'dat');
|
||||
this._delegate.onContentBlob(result._sha1, postData);
|
||||
const sha1 = calculateSha1(postData) + '.' + (mime.getExtension(contentType) || 'dat');
|
||||
if (this._options.includeTraceInfo)
|
||||
result._sha1 = sha1;
|
||||
else
|
||||
result._file = sha1;
|
||||
this._delegate.onContentBlob(sha1, postData);
|
||||
}
|
||||
|
||||
if (contentType === 'application/x-www-form-urlencoded') {
|
||||
|
|
@ -461,11 +514,10 @@ export class HarTracer {
|
|||
|
||||
}
|
||||
|
||||
function createHarEntry(method: string, url: URL, requestref: string, frameref: string): har.Entry {
|
||||
function createHarEntry(method: string, url: URL, frameref: string | undefined, options: HarTracerOptions): har.Entry {
|
||||
const harEntry: har.Entry = {
|
||||
_requestref: requestref,
|
||||
_frameref: frameref,
|
||||
_monotonicTime: monotonicTime(),
|
||||
_frameref: options.includeTraceInfo ? frameref : undefined,
|
||||
_monotonicTime: options.includeTraceInfo ? monotonicTime() : undefined,
|
||||
startedDateTime: new Date(),
|
||||
time: -1,
|
||||
request: {
|
||||
|
|
@ -476,7 +528,7 @@ function createHarEntry(method: string, url: URL, requestref: string, frameref:
|
|||
headers: [],
|
||||
queryString: [...url.searchParams].map(e => ({ name: e[0], value: e[1] })),
|
||||
headersSize: -1,
|
||||
bodySize: 0,
|
||||
bodySize: -1,
|
||||
},
|
||||
response: {
|
||||
status: -1,
|
||||
|
|
@ -491,12 +543,9 @@ function createHarEntry(method: string, url: URL, requestref: string, frameref:
|
|||
headersSize: -1,
|
||||
bodySize: -1,
|
||||
redirectURL: '',
|
||||
_transferSize: -1
|
||||
},
|
||||
cache: {
|
||||
beforeRequest: null,
|
||||
afterRequest: null,
|
||||
_transferSize: options.omitSizes ? undefined : -1
|
||||
},
|
||||
cache: {},
|
||||
timings: {
|
||||
send: -1,
|
||||
wait: -1,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
this._precreatedTracesDir = tracesDir;
|
||||
this._harTracer = new HarTracer(context, this, {
|
||||
content: 'attach',
|
||||
includeTraceInfo: true,
|
||||
waitForContentOnStop: false,
|
||||
skipScripts: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export class InMemorySnapshotter extends BaseSnapshotStorage implements Snapshot
|
|||
constructor(context: BrowserContext) {
|
||||
super();
|
||||
this._snapshotter = new Snapshotter(context, this);
|
||||
this._harTracer = new HarTracer(context, this, { content: 'attach', waitForContentOnStop: false, skipScripts: true });
|
||||
this._harTracer = new HarTracer(context, this, { content: 'attach', includeTraceInfo: true, waitForContentOnStop: false, skipScripts: true });
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
|
|
|||
30
packages/playwright-core/types/types.d.ts
vendored
30
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -10676,6 +10676,12 @@ export interface BrowserType<Unused = {}> {
|
|||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
|
||||
* security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
|
||||
*/
|
||||
mode?: "full"|"minimal";
|
||||
|
||||
/**
|
||||
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
|
||||
* provided and the passed URL is a path, it gets merged via the
|
||||
|
|
@ -11863,6 +11869,12 @@ export interface AndroidDevice {
|
|||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
|
||||
* security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
|
||||
*/
|
||||
mode?: "full"|"minimal";
|
||||
|
||||
/**
|
||||
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
|
||||
* provided and the passed URL is a path, it gets merged via the
|
||||
|
|
@ -13433,6 +13445,12 @@ export interface Browser extends EventEmitter {
|
|||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
|
||||
* security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
|
||||
*/
|
||||
mode?: "full"|"minimal";
|
||||
|
||||
/**
|
||||
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
|
||||
* provided and the passed URL is a path, it gets merged via the
|
||||
|
|
@ -14219,6 +14237,12 @@ export interface Electron {
|
|||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
|
||||
* security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
|
||||
*/
|
||||
mode?: "full"|"minimal";
|
||||
|
||||
/**
|
||||
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
|
||||
* provided and the passed URL is a path, it gets merged via the
|
||||
|
|
@ -16038,6 +16062,12 @@ export interface BrowserContextOptions {
|
|||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies,
|
||||
* security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
|
||||
*/
|
||||
mode?: "full"|"minimal";
|
||||
|
||||
/**
|
||||
* A glob or regex pattern to filter requests that are stored in the HAR. When a `baseURL` via the context options was
|
||||
* provided and the passed URL is a path, it gets merged via the
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export class SnapshotRenderer {
|
|||
|
||||
// First try locating exact resource belonging to this frame.
|
||||
for (const resource of this._resources) {
|
||||
if (resource._monotonicTime >= snapshot.timestamp)
|
||||
if (typeof resource._monotonicTime === 'number' && resource._monotonicTime >= snapshot.timestamp)
|
||||
break;
|
||||
if (resource._frameref !== snapshot.frameId)
|
||||
continue;
|
||||
|
|
@ -121,7 +121,7 @@ export class SnapshotRenderer {
|
|||
if (!result) {
|
||||
// Then fall back to resource with this URL to account for memory cache.
|
||||
for (const resource of this._resources) {
|
||||
if (resource._monotonicTime >= snapshot.timestamp)
|
||||
if (typeof resource._monotonicTime === 'number' && resource._monotonicTime >= snapshot.timestamp)
|
||||
break;
|
||||
if (resource.request.url === url)
|
||||
return resource;
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[]
|
|||
|
||||
const nextAction = next(action);
|
||||
result = context(action).resources.filter(resource => {
|
||||
return resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime);
|
||||
return typeof resource._monotonicTime === 'number' && resource._monotonicTime > action.metadata.startTime && (!nextAction || resource._monotonicTime < nextAction.metadata.startTime);
|
||||
});
|
||||
(action as any)[resourcesSymbol] = result;
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
],
|
||||
"entries": [
|
||||
{
|
||||
"_requestref": "request@ee2a0dc164935fcd4d9432d37b245f3c",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572145.898,
|
||||
"startedDateTime": "2022-06-10T04:27:32.146Z",
|
||||
|
|
@ -92,7 +91,6 @@
|
|||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@f2ff0fd79321ff90d0bc1b5d6fc13bad",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572174.683,
|
||||
"startedDateTime": "2022-06-10T04:27:32.172Z",
|
||||
|
|
@ -162,7 +160,6 @@
|
|||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@f2ff0fd79321ff90d0bc1b5d6fc13bac",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572174.683,
|
||||
"startedDateTime": "2022-06-10T04:27:32.174Z",
|
||||
|
|
@ -232,7 +229,6 @@
|
|||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@9626f59acb1f4a95f25112d32e9f7f60",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572175.042,
|
||||
"startedDateTime": "2022-06-10T04:27:32.175Z",
|
||||
|
|
@ -297,7 +293,6 @@
|
|||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@d7ee53396148a663b819c348c53b03fb",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572181.822,
|
||||
"startedDateTime": "2022-06-10T04:27:32.182Z",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
],
|
||||
"entries": [
|
||||
{
|
||||
"_requestref": "request@7d6e0ddb1e1e25f6e5c4a7c943c0bae1",
|
||||
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
|
||||
"_monotonicTime": 110928357.437,
|
||||
"startedDateTime": "2022-06-16T21:41:23.951Z",
|
||||
|
|
@ -201,7 +200,6 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@5c7a316ee46a095bda80c23ddc8c740d",
|
||||
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
|
||||
"_monotonicTime": 110928427.603,
|
||||
"startedDateTime": "2022-06-16T21:41:24.022Z",
|
||||
|
|
@ -358,7 +356,6 @@
|
|||
"_securityDetails": {}
|
||||
},
|
||||
{
|
||||
"_requestref": "request@17664a6093c12c97d41efbff3a502adb",
|
||||
"_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f",
|
||||
"_monotonicTime": 110928455.901,
|
||||
"startedDateTime": "2022-06-16T21:41:24.050Z",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
],
|
||||
"entries": [
|
||||
{
|
||||
"_requestref": "request@ee2a0dc164935fcd4d9432d37b245f3c",
|
||||
"_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea",
|
||||
"_monotonicTime": 270572145.898,
|
||||
"startedDateTime": "2022-06-10T04:27:32.146Z",
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
"size": 12,
|
||||
"mimeType": "text/html",
|
||||
"compression": 0,
|
||||
"_sha1": "har-sha1-main-response.txt"
|
||||
"_file": "har-sha1-main-response.txt"
|
||||
},
|
||||
"headersSize": 64,
|
||||
"bodySize": 71,
|
||||
|
|
|
|||
|
|
@ -255,7 +255,7 @@ it('should round-trip har.zip', async ({ contextFactory, isAndroid, server }, te
|
|||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = testInfo.outputPath('har.zip');
|
||||
const context1 = await contextFactory({ recordHar: { path: harPath } });
|
||||
const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
|
||||
const page1 = await context1.newPage();
|
||||
await page1.goto(server.PREFIX + '/one-style.html');
|
||||
await context1.close();
|
||||
|
|
@ -272,7 +272,7 @@ it('should round-trip extracted har.zip', async ({ contextFactory, isAndroid, se
|
|||
it.fixme(isAndroid);
|
||||
|
||||
const harPath = testInfo.outputPath('har.zip');
|
||||
const context1 = await contextFactory({ recordHar: { path: harPath } });
|
||||
const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
|
||||
const page1 = await context1.newPage();
|
||||
await page1.goto(server.PREFIX + '/one-style.html');
|
||||
await context1.close();
|
||||
|
|
@ -296,7 +296,7 @@ it('should round-trip har with postData', async ({ contextFactory, isAndroid, se
|
|||
});
|
||||
|
||||
const harPath = testInfo.outputPath('har.zip');
|
||||
const context1 = await contextFactory({ recordHar: { path: harPath } });
|
||||
const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
|
||||
const page1 = await context1.newPage();
|
||||
await page1.goto(server.EMPTY_PAGE);
|
||||
const fetchFunction = async (body: string) => {
|
||||
|
|
@ -327,7 +327,7 @@ it('should disambiguate by header', async ({ contextFactory, isAndroid, server }
|
|||
});
|
||||
|
||||
const harPath = testInfo.outputPath('har.zip');
|
||||
const context1 = await contextFactory({ recordHar: { path: harPath } });
|
||||
const context1 = await contextFactory({ recordHar: { mode: 'minimal', path: harPath } });
|
||||
const page1 = await context1.newPage();
|
||||
await page1.goto(server.EMPTY_PAGE);
|
||||
|
||||
|
|
|
|||
|
|
@ -291,7 +291,7 @@ it('should omit content', async ({ contextFactory, server }, testInfo) => {
|
|||
await page.evaluate(() => fetch('/pptr.png').then(r => r.arrayBuffer()));
|
||||
const log = await getLog();
|
||||
expect(log.entries[0].response.content.text).toBe(undefined);
|
||||
expect(log.entries[0].response.content._sha1).toBe(undefined);
|
||||
expect(log.entries[0].response.content._file).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should omit content legacy', async ({ contextFactory, server }, testInfo) => {
|
||||
|
|
@ -300,7 +300,7 @@ it('should omit content legacy', async ({ contextFactory, server }, testInfo) =>
|
|||
await page.evaluate(() => fetch('/pptr.png').then(r => r.arrayBuffer()));
|
||||
const log = await getLog();
|
||||
expect(log.entries[0].response.content.text).toBe(undefined);
|
||||
expect(log.entries[0].response.content._sha1).toBe(undefined);
|
||||
expect(log.entries[0].response.content._file).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should attach content', async ({ contextFactory, server }, testInfo) => {
|
||||
|
|
@ -312,19 +312,19 @@ it('should attach content', async ({ contextFactory, server }, testInfo) => {
|
|||
|
||||
expect(log.entries[0].response.content.encoding).toBe(undefined);
|
||||
expect(log.entries[0].response.content.mimeType).toBe('text/html; charset=utf-8');
|
||||
expect(log.entries[0].response.content._sha1).toContain('75841480e2606c03389077304342fac2c58ccb1b');
|
||||
expect(log.entries[0].response.content._file).toContain('75841480e2606c03389077304342fac2c58ccb1b');
|
||||
expect(log.entries[0].response.content.size).toBeGreaterThanOrEqual(96);
|
||||
expect(log.entries[0].response.content.compression).toBe(0);
|
||||
|
||||
expect(log.entries[1].response.content.encoding).toBe(undefined);
|
||||
expect(log.entries[1].response.content.mimeType).toBe('text/css; charset=utf-8');
|
||||
expect(log.entries[1].response.content._sha1).toContain('79f739d7bc88e80f55b9891a22bf13a2b4e18adb');
|
||||
expect(log.entries[1].response.content._file).toContain('79f739d7bc88e80f55b9891a22bf13a2b4e18adb');
|
||||
expect(log.entries[1].response.content.size).toBeGreaterThanOrEqual(37);
|
||||
expect(log.entries[1].response.content.compression).toBe(0);
|
||||
|
||||
expect(log.entries[2].response.content.encoding).toBe(undefined);
|
||||
expect(log.entries[2].response.content.mimeType).toBe('image/png');
|
||||
expect(log.entries[2].response.content._sha1).toContain('a4c3a18f0bb83f5d9fe7ce561e065c36205762fa');
|
||||
expect(log.entries[2].response.content._file).toContain('a4c3a18f0bb83f5d9fe7ce561e065c36205762fa');
|
||||
expect(log.entries[2].response.content.size).toBeGreaterThanOrEqual(6000);
|
||||
expect(log.entries[2].response.content.compression).toBe(0);
|
||||
|
||||
|
|
@ -689,45 +689,6 @@ it('should have different hars for concurrent contexts', async ({ contextFactory
|
|||
}
|
||||
});
|
||||
|
||||
it('should include _requestref', async ({ contextFactory, server }, testInfo) => {
|
||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||
const resp = await page.goto(server.EMPTY_PAGE);
|
||||
const log = await getLog();
|
||||
expect(log.entries.length).toBe(1);
|
||||
const entry = log.entries[0];
|
||||
expect(entry._requestref).toMatch(/^request@[a-f0-9]{32}$/);
|
||||
expect(entry._requestref).toBe((resp.request() as any)._guid);
|
||||
});
|
||||
|
||||
it('should include _requestref for redirects', async ({ contextFactory, server }, testInfo) => {
|
||||
server.setRedirect('/start', '/one-more');
|
||||
server.setRedirect('/one-more', server.EMPTY_PAGE);
|
||||
|
||||
const { page, getLog, context } = await pageWithHar(contextFactory, testInfo);
|
||||
|
||||
const requests = new Map<string, string>();
|
||||
context.on('request', request => {
|
||||
requests.set(request.url(), (request as any)._guid);
|
||||
});
|
||||
|
||||
await page.goto(server.PREFIX + '/start');
|
||||
|
||||
const log = await getLog();
|
||||
expect(log.entries.length).toBe(3);
|
||||
|
||||
const entryStart = log.entries[0];
|
||||
expect(entryStart.request.url).toBe(server.PREFIX + '/start');
|
||||
expect(entryStart._requestref).toBe(requests.get(entryStart.request.url));
|
||||
|
||||
const entryOneMore = log.entries[1];
|
||||
expect(entryOneMore.request.url).toBe(server.PREFIX + '/one-more');
|
||||
expect(entryOneMore._requestref).toBe(requests.get(entryOneMore.request.url));
|
||||
|
||||
const entryEmptyPage = log.entries[2];
|
||||
expect(entryEmptyPage.request.url).toBe(server.EMPTY_PAGE);
|
||||
expect(entryEmptyPage._requestref).toBe(requests.get(entryEmptyPage.request.url));
|
||||
});
|
||||
|
||||
it('should include API request', async ({ contextFactory, server }, testInfo) => {
|
||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||
const url = server.PREFIX + '/simple.json';
|
||||
|
|
|
|||
|
|
@ -528,7 +528,6 @@ test('should store postData for global request', async ({ request, server }, tes
|
|||
const actions = trace.events.filter(e => e.type === 'resource-snapshot');
|
||||
expect(actions).toHaveLength(1);
|
||||
const req = actions[0].snapshot.request;
|
||||
console.log(JSON.stringify(req, null, 2));
|
||||
expect(req.postData?._sha1).toBeTruthy();
|
||||
expect(req).toEqual(expect.objectContaining({
|
||||
method: 'POST',
|
||||
|
|
|
|||
Loading…
Reference in a new issue