feat(routeFromHAR): add shouldSave option to control when to save HAR files

Fixes #33559
This commit is contained in:
Mark Skelton 2024-11-15 09:19:15 -06:00
parent e61cea597a
commit 7e78f8e4b9
No known key found for this signature in database
7 changed files with 92 additions and 7 deletions

View file

@ -1267,6 +1267,11 @@ When set to `minimal`, only record information necessary for routing from HAR. T
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
### option: BrowserContext.routeFromHAR.shouldSave
* since: v1.49
- `shouldSave` <[function]\(\):[boolean]>
If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
## async method: BrowserContext.routeWebSocket
* since: v1.48

View file

@ -3668,6 +3668,11 @@ When set to `minimal`, only record information necessary for routing from HAR. T
Optional setting to control resource content management. If `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is specified, content is stored inline the HAR file.
### option: Page.routeFromHAR.shouldSave
* since: v1.49
- `shouldSave` <[function]\(\):[boolean]>
If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
## async method: Page.routeWebSocket
* since: v1.48

View file

@ -700,6 +700,7 @@ Logger sink for Playwright logging.
- `path` <[path]> Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, `content: 'attach'` 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: Browser.newContext.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. Defaults to none.
- `shouldSave` ?<[function]\(\):[boolean]> If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
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

View file

@ -64,7 +64,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
readonly _backgroundPages = new Set<Page>();
readonly _serviceWorkers = new Set<Worker>();
readonly _isChromium: boolean;
private _harRecorders = new Map<string, { path: string, content: 'embed' | 'attach' | 'omit' | undefined }>();
private _harRecorders = new Map<string, {
path: string,
content: 'embed' | 'attach' | 'omit' | undefined,
shouldSave: (() => boolean) | undefined,
}>();
_closeWasCalled = false;
private _closeReason: string | undefined;
private _harRouters: HarRouter[] = [];
@ -152,8 +156,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
_setOptions(contextOptions: channels.BrowserNewContextParams, browserOptions: LaunchOptions) {
this._options = contextOptions;
if (this._options.recordHar)
this._harRecorders.set('', { path: this._options.recordHar.path, content: this._options.recordHar.content });
if (this._options.recordHar) {
this._harRecorders.set('', {
path: this._options.recordHar.path,
content: this._options.recordHar.content,
shouldSave: undefined,
});
}
this.tracing._tracesDir = browserOptions.tracesDir;
}
@ -343,7 +352,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._updateWebSocketInterceptionPatterns();
}
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full', shouldSave?: () => boolean } = {}): Promise<void> {
const { harId } = await this._channel.harStart({
page: page?._channel,
options: prepareRecordHarOptions({
@ -353,7 +362,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
urlFilter: options.url
})!
});
this._harRecorders.set(harId, { path: har, content: options.updateContent ?? 'attach' });
this._harRecorders.set(harId, { path: har, content: options.updateContent ?? 'attach', shouldSave: options.shouldSave });
}
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full' } = {}): Promise<void> {
@ -474,6 +483,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
await this._wrapApiCall(async () => {
await this._browserType?._willCloseContext(this);
for (const [harId, harParams] of this._harRecorders) {
const shouldSave = harParams.shouldSave ? harParams.shouldSave() : true;
if (!shouldSave)
continue;
const har = await this._channel.harExport({ harId });
const artifact = Artifact.from(har.artifact);
// Server side will compress artifact if content is attach or if file is .zip.

View file

@ -516,7 +516,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
await this._updateInterceptionPatterns();
}
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full', shouldSave?: () => boolean } = {}): Promise<void> {
if (options.update) {
await this._browserContext._recordIntoHAR(har, this, options);
return;

View file

@ -4007,6 +4007,11 @@ export interface Page {
*/
notFound?: "abort"|"fallback";
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
/**
* If specified, updates the given HAR with the actual network information instead of serving from file. The file is
* written to disk when
@ -9097,6 +9102,11 @@ export interface BrowserContext {
*/
notFound?: "abort"|"fallback";
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
/**
* If specified, updates the given HAR with the actual network information instead of serving from file. The file is
* written to disk when
@ -9941,6 +9951,11 @@ export interface Browser {
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none.
*/
urlFilter?: string|RegExp;
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
};
/**
@ -15024,6 +15039,11 @@ export interface BrowserType<Unused = {}> {
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none.
*/
urlFilter?: string|RegExp;
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
};
/**
@ -16772,6 +16792,11 @@ export interface AndroidDevice {
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none.
*/
urlFilter?: string|RegExp;
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
};
/**
@ -19195,6 +19220,11 @@ export interface Electron {
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none.
*/
urlFilter?: string|RegExp;
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
};
/**
@ -22075,6 +22105,11 @@ export interface BrowserContextOptions {
* [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. Defaults to none.
*/
urlFilter?: string|RegExp;
/**
* If specified, controls when the HAR file should be saved to disk. Defaults to always save the HAR file.
*/
shouldSave?: (() => boolean);
};
/**

View file

@ -556,4 +556,30 @@ it('should ignore aborted requests', async ({ contextFactory, server }) => {
const result = await Promise.race([evalPromise, page2.waitForTimeout(1000).then(() => 'timeout')]);
expect(result).toBe('timeout');
}
});
});
it('should save HAR files by default', async ({ contextFactory, server }, testInfo) => {
const harPath = testInfo.outputPath('har.har');
const context = await contextFactory();
await context.routeFromHAR(harPath, { update: true });
const page = await context.newPage();
await page.goto(server.PREFIX + '/one-style.html');
await context.close();
expect(fs.existsSync(harPath)).toBe(true);
const har = fs.readFileSync(harPath, 'utf-8');
expect(har).not.toContain('background-color');
});
it('can disable saving HAR files', async ({ contextFactory, server }, testInfo) => {
const harPath = testInfo.outputPath('har.har');
const context = await contextFactory();
await context.routeFromHAR(harPath, { update: true, shouldSave: () => false });
const page = await context.newPage();
await page.goto(server.PREFIX + '/one-style.html');
await context.close();
expect(fs.existsSync(harPath)).toBe(false);
});