feat(har): introduce urlFilter (#14693)
This is a glob or regex pattern that filters entries recorder in the HAR.
This commit is contained in:
parent
b2d0fae3b1
commit
fdcdd58d7f
|
|
@ -562,6 +562,7 @@ Logger sink for Playwright logging.
|
|||
- `omitContent` ?<[boolean]> Optional setting to control whether to omit request content from the HAR. Defaults to
|
||||
`false`.
|
||||
- `path` <[path]> Path on the filesystem to write the HAR file to.
|
||||
- `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
|
||||
specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.close`] for the HAR to be
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { Events } from './events';
|
|||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { Waiter } from './waiter';
|
||||
import type { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
|
||||
import { headersObjectToArray } from '../utils';
|
||||
import { headersObjectToArray, isRegExp, isString } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { isSafeCloseError } from '../common/errors';
|
||||
import type * as api from '../../types/types';
|
||||
|
|
@ -390,6 +390,18 @@ async function prepareStorageState(options: BrowserContextOptions): Promise<chan
|
|||
}
|
||||
}
|
||||
|
||||
function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): channels.RecordHarOptions | undefined {
|
||||
if (!options)
|
||||
return;
|
||||
return {
|
||||
path: options.path,
|
||||
omitContent: options.omitContent,
|
||||
urlGlob: isString(options.urlFilter) ? options.urlFilter : undefined,
|
||||
urlRegexSource: isRegExp(options.urlFilter) ? options.urlFilter.source : undefined,
|
||||
urlRegexFlags: isRegExp(options.urlFilter) ? options.urlFilter.flags : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
if (options.videoSize && !options.videosPath)
|
||||
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||
|
|
@ -401,6 +413,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
noDefaultViewport: options.viewport === null,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
storageState: await prepareStorageState(options),
|
||||
recordHar: prepareRecordHarOptions(options.recordHar),
|
||||
};
|
||||
if (!contextParams.recordVideo && options.videosPath) {
|
||||
contextParams.recordVideo = {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import type * as types from './types';
|
||||
import fs from 'fs';
|
||||
import { isString, isRegExp, constructURLBasedOnBaseURL } from '../utils';
|
||||
import { isString } from '../utils';
|
||||
|
||||
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
|
||||
const result: { name: string, value: string }[] = [];
|
||||
|
|
@ -48,90 +48,3 @@ export async function evaluationScript(fun: Function | string | { path?: string,
|
|||
}
|
||||
throw new Error('Either path or content property must be present');
|
||||
}
|
||||
|
||||
export function parsedURL(url: string): URL | null {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function urlMatches(baseURL: string | undefined, urlString: string, match: types.URLMatch | undefined): boolean {
|
||||
if (match === undefined || match === '')
|
||||
return true;
|
||||
if (isString(match) && !match.startsWith('*'))
|
||||
match = constructURLBasedOnBaseURL(baseURL, match);
|
||||
if (isString(match))
|
||||
match = globToRegex(match);
|
||||
if (isRegExp(match))
|
||||
return match.test(urlString);
|
||||
if (typeof match === 'string' && match === urlString)
|
||||
return true;
|
||||
const url = parsedURL(urlString);
|
||||
if (!url)
|
||||
return false;
|
||||
if (typeof match === 'string')
|
||||
return url.pathname === match;
|
||||
if (typeof match !== 'function')
|
||||
throw new Error('url parameter should be string, RegExp or function');
|
||||
return match(url);
|
||||
}
|
||||
|
||||
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
|
||||
|
||||
export function globToRegex(glob: string): RegExp {
|
||||
const tokens = ['^'];
|
||||
let inGroup;
|
||||
for (let i = 0; i < glob.length; ++i) {
|
||||
const c = glob[i];
|
||||
if (escapeGlobChars.has(c)) {
|
||||
tokens.push('\\' + c);
|
||||
continue;
|
||||
}
|
||||
if (c === '*') {
|
||||
const beforeDeep = glob[i - 1];
|
||||
let starCount = 1;
|
||||
while (glob[i + 1] === '*') {
|
||||
starCount++;
|
||||
i++;
|
||||
}
|
||||
const afterDeep = glob[i + 1];
|
||||
const isDeep = starCount > 1 &&
|
||||
(beforeDeep === '/' || beforeDeep === undefined) &&
|
||||
(afterDeep === '/' || afterDeep === undefined);
|
||||
if (isDeep) {
|
||||
tokens.push('((?:[^/]*(?:\/|$))*)');
|
||||
i++;
|
||||
} else {
|
||||
tokens.push('([^/]*)');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (c) {
|
||||
case '?':
|
||||
tokens.push('.');
|
||||
break;
|
||||
case '{':
|
||||
inGroup = true;
|
||||
tokens.push('(');
|
||||
break;
|
||||
case '}':
|
||||
inGroup = false;
|
||||
tokens.push(')');
|
||||
break;
|
||||
case ',':
|
||||
if (inGroup) {
|
||||
tokens.push('|');
|
||||
break;
|
||||
}
|
||||
tokens.push('\\' + c);
|
||||
break;
|
||||
default:
|
||||
tokens.push(c);
|
||||
}
|
||||
}
|
||||
tokens.push('$');
|
||||
return new RegExp(tokens.join(''));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { Waiter } from './waiter';
|
|||
import { Events } from './events';
|
||||
import type { LifecycleEvent, URLMatch, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions, StrictOptions } from './types';
|
||||
import { kLifecycleEvents } from './types';
|
||||
import { urlMatches } from './clientHelper';
|
||||
import { urlMatches } from '../common/netUtils';
|
||||
import type * as api from '../../types/types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import type { Page } from './page';
|
|||
import { Waiter } from './waiter';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray, URLMatch } from '../common/types';
|
||||
import { urlMatches } from './clientHelper';
|
||||
import { urlMatches } from '../common/netUtils';
|
||||
import { MultiMap } from '../utils/multimap';
|
||||
import { APIResponse } from './fetch';
|
||||
|
||||
|
|
|
|||
|
|
@ -45,13 +45,14 @@ import type * as structs from '../../types/structs';
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { Size, URLMatch, Headers, LifecycleEvent, WaitForEventOptions, SelectOption, SelectOptionOptions, FilePayload, WaitForFunctionOptions } from './types';
|
||||
import { evaluationScript, urlMatches } from './clientHelper';
|
||||
import { evaluationScript } from './clientHelper';
|
||||
import { isString, isRegExp, isObject, headersObjectToArray } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { isSafeCloseError } from '../common/errors';
|
||||
import { Video } from './video';
|
||||
import { Artifact } from './artifact';
|
||||
import type { APIRequestContext } from './fetch';
|
||||
import { urlMatches } from '../common/netUtils';
|
||||
|
||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||
width?: string | number,
|
||||
|
|
|
|||
|
|
@ -47,13 +47,18 @@ export type SetStorageState = {
|
|||
export type LifecycleEvent = channels.LifecycleEvent;
|
||||
export const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
||||
|
||||
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'storageState'> & {
|
||||
export type BrowserContextOptions = Omit<channels.BrowserNewContextOptions, 'viewport' | 'noDefaultViewport' | 'extraHTTPHeaders' | 'storageState' | 'recordHar'> & {
|
||||
viewport?: Size | null,
|
||||
extraHTTPHeaders?: Headers,
|
||||
logger?: Logger,
|
||||
videosPath?: string,
|
||||
videoSize?: Size,
|
||||
storageState?: string | SetStorageState,
|
||||
recordHar?: {
|
||||
path: string,
|
||||
omitContent?: boolean,
|
||||
urlFilter?: string | RegExp,
|
||||
},
|
||||
};
|
||||
|
||||
type LaunchOverrides = {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import net from 'net';
|
|||
import { getProxyForUrl } from '../utilsBundle';
|
||||
import { HttpsProxyAgent } from '../utilsBundle';
|
||||
import * as URL from 'url';
|
||||
import type { URLMatch } from './types';
|
||||
import { isString, constructURLBasedOnBaseURL, isRegExp } from '../utils';
|
||||
|
||||
export async function createSocket(host: string, port: number): Promise<net.Socket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -101,3 +103,91 @@ export function fetchData(params: HTTPRequestParams, onError?: (params: HTTPRequ
|
|||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
export function urlMatches(baseURL: string | undefined, urlString: string, match: URLMatch | undefined): boolean {
|
||||
if (match === undefined || match === '')
|
||||
return true;
|
||||
if (isString(match) && !match.startsWith('*'))
|
||||
match = constructURLBasedOnBaseURL(baseURL, match);
|
||||
if (isString(match))
|
||||
match = globToRegex(match);
|
||||
if (isRegExp(match))
|
||||
return match.test(urlString);
|
||||
if (typeof match === 'string' && match === urlString)
|
||||
return true;
|
||||
const url = parsedURL(urlString);
|
||||
if (!url)
|
||||
return false;
|
||||
if (typeof match === 'string')
|
||||
return url.pathname === match;
|
||||
if (typeof match !== 'function')
|
||||
throw new Error('url parameter should be string, RegExp or function');
|
||||
return match(url);
|
||||
}
|
||||
|
||||
function parsedURL(url: string): URL | null {
|
||||
try {
|
||||
return new URL.URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']);
|
||||
|
||||
// Note: this function is exported so it can be unit-tested.
|
||||
export function globToRegex(glob: string): RegExp {
|
||||
const tokens = ['^'];
|
||||
let inGroup;
|
||||
for (let i = 0; i < glob.length; ++i) {
|
||||
const c = glob[i];
|
||||
if (escapeGlobChars.has(c)) {
|
||||
tokens.push('\\' + c);
|
||||
continue;
|
||||
}
|
||||
if (c === '*') {
|
||||
const beforeDeep = glob[i - 1];
|
||||
let starCount = 1;
|
||||
while (glob[i + 1] === '*') {
|
||||
starCount++;
|
||||
i++;
|
||||
}
|
||||
const afterDeep = glob[i + 1];
|
||||
const isDeep = starCount > 1 &&
|
||||
(beforeDeep === '/' || beforeDeep === undefined) &&
|
||||
(afterDeep === '/' || afterDeep === undefined);
|
||||
if (isDeep) {
|
||||
tokens.push('((?:[^/]*(?:\/|$))*)');
|
||||
i++;
|
||||
} else {
|
||||
tokens.push('([^/]*)');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (c) {
|
||||
case '?':
|
||||
tokens.push('.');
|
||||
break;
|
||||
case '{':
|
||||
inGroup = true;
|
||||
tokens.push('(');
|
||||
break;
|
||||
case '}':
|
||||
inGroup = false;
|
||||
tokens.push(')');
|
||||
break;
|
||||
case ',':
|
||||
if (inGroup) {
|
||||
tokens.push('|');
|
||||
break;
|
||||
}
|
||||
tokens.push('\\' + c);
|
||||
break;
|
||||
default:
|
||||
tokens.push(c);
|
||||
}
|
||||
}
|
||||
tokens.push('$');
|
||||
return new RegExp(tokens.join(''));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -263,6 +263,14 @@ export type SerializedError = {
|
|||
value?: SerializedValue,
|
||||
};
|
||||
|
||||
export type RecordHarOptions = {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
urlGlob?: string,
|
||||
urlRegexSource?: string,
|
||||
urlRegexFlags?: string,
|
||||
};
|
||||
|
||||
export type FormField = {
|
||||
name: string,
|
||||
value?: string,
|
||||
|
|
@ -737,10 +745,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
strictSelectors?: boolean,
|
||||
userDataDir: string,
|
||||
slowMo?: number,
|
||||
|
|
@ -809,10 +814,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
strictSelectors?: boolean,
|
||||
slowMo?: number,
|
||||
};
|
||||
|
|
@ -905,10 +907,7 @@ export type BrowserNewContextParams = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
strictSelectors?: boolean,
|
||||
proxy?: {
|
||||
server: string,
|
||||
|
|
@ -964,10 +963,7 @@ export type BrowserNewContextOptions = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
strictSelectors?: boolean,
|
||||
proxy?: {
|
||||
server: string,
|
||||
|
|
@ -3573,10 +3569,7 @@ export type ElectronLaunchParams = {
|
|||
ignoreHTTPSErrors?: boolean,
|
||||
locale?: string,
|
||||
offline?: boolean,
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
|
|
@ -3609,10 +3602,7 @@ export type ElectronLaunchOptions = {
|
|||
ignoreHTTPSErrors?: boolean,
|
||||
locale?: string,
|
||||
offline?: boolean,
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
|
|
@ -3991,10 +3981,7 @@ export type AndroidDeviceLaunchBrowserParams = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
strictSelectors?: boolean,
|
||||
pkg?: string,
|
||||
proxy?: {
|
||||
|
|
@ -4047,10 +4034,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
|||
height: number,
|
||||
},
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
},
|
||||
recordHar?: RecordHarOptions,
|
||||
strictSelectors?: boolean,
|
||||
pkg?: string,
|
||||
proxy?: {
|
||||
|
|
|
|||
|
|
@ -220,6 +220,17 @@ SerializedError:
|
|||
stack: string?
|
||||
value: SerializedValue?
|
||||
|
||||
|
||||
RecordHarOptions:
|
||||
type: object
|
||||
properties:
|
||||
omitContent: boolean?
|
||||
path: string
|
||||
urlGlob: string?
|
||||
urlRegexSource: string?
|
||||
urlRegexFlags: string?
|
||||
|
||||
|
||||
FormField:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -442,11 +453,7 @@ ContextOptions:
|
|||
properties:
|
||||
width: number
|
||||
height: number
|
||||
recordHar:
|
||||
type: object?
|
||||
properties:
|
||||
omitContent: boolean?
|
||||
path: string
|
||||
recordHar: RecordHarOptions?
|
||||
strictSelectors: boolean?
|
||||
|
||||
LocalUtils:
|
||||
|
|
@ -2786,11 +2793,7 @@ Electron:
|
|||
ignoreHTTPSErrors: boolean?
|
||||
locale: string?
|
||||
offline: boolean?
|
||||
recordHar:
|
||||
type: object?
|
||||
properties:
|
||||
omitContent: boolean?
|
||||
path: string
|
||||
recordHar: RecordHarOptions?
|
||||
recordVideo:
|
||||
type: object?
|
||||
properties:
|
||||
|
|
|
|||
|
|
@ -153,6 +153,13 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
})),
|
||||
value: tOptional(tType('SerializedValue')),
|
||||
});
|
||||
scheme.RecordHarOptions = tObject({
|
||||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
urlGlob: tOptional(tString),
|
||||
urlRegexSource: tOptional(tString),
|
||||
urlRegexFlags: tOptional(tString),
|
||||
});
|
||||
scheme.FormField = tObject({
|
||||
name: tString,
|
||||
value: tOptional(tString),
|
||||
|
|
@ -345,10 +352,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
height: tNumber,
|
||||
})),
|
||||
})),
|
||||
recordHar: tOptional(tObject({
|
||||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
recordHar: tOptional(tType('RecordHarOptions')),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
userDataDir: tString,
|
||||
slowMo: tOptional(tNumber),
|
||||
|
|
@ -404,10 +408,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
height: tNumber,
|
||||
})),
|
||||
})),
|
||||
recordHar: tOptional(tObject({
|
||||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
recordHar: tOptional(tType('RecordHarOptions')),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
proxy: tOptional(tObject({
|
||||
server: tString,
|
||||
|
|
@ -1279,10 +1280,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
ignoreHTTPSErrors: tOptional(tBoolean),
|
||||
locale: tOptional(tString),
|
||||
offline: tOptional(tBoolean),
|
||||
recordHar: tOptional(tObject({
|
||||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
recordHar: tOptional(tType('RecordHarOptions')),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
size: tOptional(tObject({
|
||||
|
|
@ -1441,10 +1439,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
height: tNumber,
|
||||
})),
|
||||
})),
|
||||
recordHar: tOptional(tObject({
|
||||
omitContent: tOptional(tBoolean),
|
||||
path: tString,
|
||||
})),
|
||||
recordHar: tOptional(tType('RecordHarOptions')),
|
||||
strictSelectors: tOptional(tBoolean),
|
||||
pkg: tOptional(tString),
|
||||
proxy: tOptional(tObject({
|
||||
|
|
|
|||
|
|
@ -20,11 +20,7 @@ import { Artifact } from '../artifact';
|
|||
import type { BrowserContext } from '../browserContext';
|
||||
import type * as har from './har';
|
||||
import { HarTracer } from './harTracer';
|
||||
|
||||
type HarOptions = {
|
||||
path: string;
|
||||
omitContent?: boolean;
|
||||
};
|
||||
import type { HarOptions } from '../types';
|
||||
|
||||
export class HarRecorder {
|
||||
private _artifact: Artifact;
|
||||
|
|
@ -36,10 +32,12 @@ export class HarRecorder {
|
|||
constructor(context: BrowserContext | APIRequestContext, options: HarOptions) {
|
||||
this._artifact = new Artifact(context, options.path);
|
||||
this._options = options;
|
||||
const urlFilterRe = options.urlRegexSource !== undefined && options.urlRegexFlags !== undefined ? new RegExp(options.urlRegexSource, options.urlRegexFlags) : undefined;
|
||||
this._tracer = new HarTracer(context, this, {
|
||||
content: options.omitContent ? 'omit' : 'embedded',
|
||||
waitForContentOnStop: true,
|
||||
skipScripts: false,
|
||||
urlFilter: urlFilterRe ?? options.urlGlob,
|
||||
});
|
||||
this._tracer.start();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
|
|||
import { mime } from '../../utilsBundle';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { getPlaywrightVersion } from '../../common/userAgent';
|
||||
import { urlMatches } from '../../common/netUtils';
|
||||
|
||||
const FALLBACK_HTTP_VERSION = 'HTTP/1.1';
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ type HarTracerOptions = {
|
|||
content: 'omit' | 'sha1' | 'embedded';
|
||||
skipScripts: boolean;
|
||||
waitForContentOnStop: boolean;
|
||||
urlFilter?: string | RegExp;
|
||||
};
|
||||
|
||||
export class HarTracer {
|
||||
|
|
@ -51,12 +53,14 @@ export class HarTracer {
|
|||
private _eventListeners: RegisteredListener[] = [];
|
||||
private _started = false;
|
||||
private _entrySymbol: symbol;
|
||||
private _baseURL: string | undefined;
|
||||
|
||||
constructor(context: BrowserContext | APIRequestContext, delegate: HarTracerDelegate, options: HarTracerOptions) {
|
||||
this._context = context;
|
||||
this._delegate = delegate;
|
||||
this._options = options;
|
||||
this._entrySymbol = Symbol('requestHarEntry');
|
||||
this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL;
|
||||
}
|
||||
|
||||
start() {
|
||||
|
|
@ -76,7 +80,10 @@ export class HarTracer {
|
|||
eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)),
|
||||
eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)));
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldIncludeEntryWithUrl(urlString: string) {
|
||||
return !this._options.urlFilter || urlMatches(this._baseURL, urlString, this._options.urlFilter);
|
||||
}
|
||||
|
||||
private _entryForRequest(request: network.Request | APIRequestEvent): har.Entry | undefined {
|
||||
|
|
@ -146,6 +153,8 @@ export class HarTracer {
|
|||
}
|
||||
|
||||
private _onAPIRequest(event: APIRequestEvent) {
|
||||
if (!this._shouldIncludeEntryWithUrl(event.url.toString()))
|
||||
return;
|
||||
const harEntry = createHarEntry(event.method, event.url, '', '');
|
||||
harEntry.request.cookies = event.cookies;
|
||||
harEntry.request.headers = Object.entries(event.headers).map(([name, value]) => ({ name, value }));
|
||||
|
|
@ -189,6 +198,8 @@ export class HarTracer {
|
|||
}
|
||||
|
||||
private _onRequest(request: network.Request) {
|
||||
if (!this._shouldIncludeEntryWithUrl(request.url()))
|
||||
return;
|
||||
const page = request.frame()._page;
|
||||
const url = network.parsedURL(request.url());
|
||||
if (!url)
|
||||
|
|
|
|||
|
|
@ -227,6 +227,14 @@ export type SetNetworkCookieParam = {
|
|||
|
||||
export type EmulatedSize = { viewport: Size, screen: Size };
|
||||
|
||||
export type HarOptions = {
|
||||
omitContent?: boolean,
|
||||
path: string,
|
||||
urlGlob?: string,
|
||||
urlRegexSource?: string,
|
||||
urlRegexFlags?: string,
|
||||
};
|
||||
|
||||
export type BrowserContextOptions = {
|
||||
viewport?: Size,
|
||||
screen?: Size,
|
||||
|
|
@ -253,10 +261,7 @@ export type BrowserContextOptions = {
|
|||
dir: string,
|
||||
size?: Size,
|
||||
},
|
||||
recordHar?: {
|
||||
omitContent?: boolean,
|
||||
path: string
|
||||
},
|
||||
recordHar?: HarOptions,
|
||||
storageState?: SetStorageState,
|
||||
strictSelectors?: boolean,
|
||||
proxy?: ProxySettings,
|
||||
|
|
|
|||
35
packages/playwright-core/types/types.d.ts
vendored
35
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -10310,6 +10310,13 @@ export interface BrowserType<Unused = {}> {
|
|||
* Path on the filesystem to write the HAR file to.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
*/
|
||||
urlFilter?: string|RegExp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -11459,6 +11466,13 @@ export interface AndroidDevice {
|
|||
* Path on the filesystem to write the HAR file to.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
*/
|
||||
urlFilter?: string|RegExp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -12985,6 +12999,13 @@ export interface Browser extends EventEmitter {
|
|||
* Path on the filesystem to write the HAR file to.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
*/
|
||||
urlFilter?: string|RegExp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -13744,6 +13765,13 @@ export interface Electron {
|
|||
* Path on the filesystem to write the HAR file to.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
*/
|
||||
urlFilter?: string|RegExp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -15426,6 +15454,13 @@ export interface BrowserContextOptions {
|
|||
* Path on the filesystem to write the HAR file to.
|
||||
*/
|
||||
path: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor.
|
||||
*/
|
||||
urlFilter?: string|RegExp;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -265,6 +265,28 @@ it('should include content @smoke', async ({ contextFactory, server }, testInfo)
|
|||
expect(log.entries[1].response.content.compression).toBe(0);
|
||||
});
|
||||
|
||||
it('should filter by glob', async ({ contextFactory, server }, testInfo) => {
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
const context = await contextFactory({ baseURL: server.PREFIX, recordHar: { path: harPath, urlFilter: '/*.css' }, ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto('/har.html');
|
||||
await context.close();
|
||||
const log = JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log;
|
||||
expect(log.entries.length).toBe(1);
|
||||
expect(log.entries[0].request.url.endsWith('one-style.css')).toBe(true);
|
||||
});
|
||||
|
||||
it('should filter by regexp', async ({ contextFactory, server }, testInfo) => {
|
||||
const harPath = testInfo.outputPath('test.har');
|
||||
const context = await contextFactory({ recordHar: { path: harPath, urlFilter: /HAR.X?HTML/i }, ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.PREFIX + '/har.html');
|
||||
await context.close();
|
||||
const log = JSON.parse(fs.readFileSync(harPath).toString())['log'] as Log;
|
||||
expect(log.entries.length).toBe(1);
|
||||
expect(log.entries[0].request.url.endsWith('har.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should include sizes', async ({ contextFactory, server, asset }, testInfo) => {
|
||||
const { page, getLog } = await pageWithHar(contextFactory, testInfo);
|
||||
await page.goto(server.PREFIX + '/har.html');
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
import { test as it, expect } from './pageTest';
|
||||
import { globToRegex } from '../../packages/playwright-core/lib/client/clientHelper';
|
||||
import { globToRegex } from '../../packages/playwright-core/lib/common/netUtils';
|
||||
import vm from 'vm';
|
||||
|
||||
it('should work with navigation @smoke', async ({ page, server }) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue