Skip to content

Commit 76bbfcd

Browse files
committed
feat(tracing): add tracing.startHar / tracing.stopHar
Adds on-demand HAR recording to `Tracing`, available on both `BrowserContext.tracing` and `APIRequestContext.tracing`. Unlike `recordHar`, this can be scoped to an individual flow via explicit start/stop calls.
1 parent b82aa49 commit 76bbfcd

File tree

20 files changed

+390
-139
lines changed

20 files changed

+390
-139
lines changed

docs/src/api/class-apirequestcontext.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,3 +896,7 @@ Returns storage state for this request context, contains current cookies and loc
896896
- `indexedDB` ?<boolean>
897897

898898
Set to `true` to include IndexedDB in the storage state snapshot.
899+
900+
## property: APIRequestContext.tracing
901+
* since: v1.60
902+
- type: <[Tracing]>

docs/src/api/class-tracing.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,75 @@ given name prefix inside the [`option: BrowserType.launch.tracesDir`] directory
303303
To specify the final trace zip file name, you need to pass `path` option to
304304
[`method: Tracing.stopChunk`] instead.
305305

306+
## async method: Tracing.startHar
307+
* since: v1.60
308+
- returns: <[Disposable]>
309+
310+
Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when [`method: Tracing.stopHar`] is called, or when the returned [Disposable] is disposed.
311+
312+
Only one HAR recording can be active at a time per [BrowserContext].
313+
314+
**Usage**
315+
316+
```js
317+
await context.tracing.startHar('trace.har');
318+
const page = await context.newPage();
319+
await page.goto('https://playwright.dev');
320+
await context.tracing.stopHar();
321+
```
322+
323+
```java
324+
context.tracing().startHar(Paths.get("trace.har"));
325+
Page page = context.newPage();
326+
page.navigate("https://playwright.dev");
327+
context.tracing().stopHar();
328+
```
329+
330+
```python async
331+
await context.tracing.start_har("trace.har")
332+
page = await context.new_page()
333+
await page.goto("https://playwright.dev")
334+
await context.tracing.stop_har()
335+
```
336+
337+
```python sync
338+
context.tracing.start_har("trace.har")
339+
page = context.new_page()
340+
page.goto("https://playwright.dev")
341+
context.tracing.stop_har()
342+
```
343+
344+
```csharp
345+
await context.Tracing.StartHarAsync("trace.har");
346+
var page = await context.NewPageAsync();
347+
await page.GotoAsync("https://playwright.dev");
348+
await context.Tracing.StopHarAsync();
349+
```
350+
351+
### param: Tracing.startHar.path
352+
* since: v1.60
353+
- `path` <[path]>
354+
355+
Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, the HAR is saved as a zip archive with response bodies attached as separate files.
356+
357+
### option: Tracing.startHar.content
358+
* since: v1.60
359+
- `content` <[HarContentPolicy]<"omit"|"embed"|"attach">>
360+
361+
Optional setting to control resource content management. If `omit` is specified, content is not persisted. 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 as per HAR specification. Defaults to `attach` for `.zip` output files and to `embed` for all other file extensions.
362+
363+
### option: Tracing.startHar.mode
364+
* since: v1.60
365+
- `mode` <[HarMode]<"full"|"minimal">>
366+
367+
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`.
368+
369+
### option: Tracing.startHar.urlFilter
370+
* since: v1.60
371+
- `urlFilter` <[string]|[RegExp]>
372+
373+
A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
374+
306375
## async method: Tracing.group
307376
* since: v1.49
308377
- returns: <[Disposable]>
@@ -400,3 +469,8 @@ Stop the trace chunk. See [`method: Tracing.startChunk`] for more details about
400469
- `path` <[path]>
401470

402471
Export trace collected since the last [`method: Tracing.startChunk`] call into the file with the given path.
472+
473+
## async method: Tracing.stopHar
474+
* since: v1.60
475+
476+
Stop HAR recording and save the HAR file to the path given to [`method: Tracing.startHar`].

packages/isomorphic/protocolMetainfo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
9696
['BrowserContext.disableRecorder', { internal: true, }],
9797
['BrowserContext.exposeConsoleApi', { internal: true, }],
9898
['BrowserContext.newCDPSession', { title: 'Create CDP session', group: 'configuration', }],
99-
['BrowserContext.harStart', { internal: true, }],
100-
['BrowserContext.harExport', { internal: true, }],
10199
['BrowserContext.createTempFiles', { internal: true, }],
102100
['BrowserContext.updateSubscription', { internal: true, }],
103101
['BrowserContext.clockFastForward', { title: 'Fast forward clock "{ticksNumber|ticksString}"', }],
@@ -289,6 +287,8 @@ export const methodMetainfo = new Map<string, MethodMetainfo>([
289287
['Tracing.tracingGroupEnd', { title: 'Group end', }],
290288
['Tracing.tracingStopChunk', { title: 'Stop tracing', group: 'configuration', }],
291289
['Tracing.tracingStop', { title: 'Stop tracing', group: 'configuration', }],
290+
['Tracing.harStart', { internal: true, }],
291+
['Tracing.harExport', { internal: true, }],
292292
['Artifact.pathAfterFinished', { internal: true, }],
293293
['Artifact.saveAs', { internal: true, }],
294294
['Artifact.saveAsStream', { internal: true, }],

packages/playwright-client/types/types.d.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19116,6 +19116,8 @@ export interface APIRequestContext {
1911619116
}>;
1911719117
}>;
1911819118

19119+
tracing: Tracing;
19120+
1911919121
[Symbol.asyncDispose](): Promise<void>;
1912019122
}
1912119123

@@ -22148,6 +22150,48 @@ export interface Tracing {
2214822150
title?: string;
2214922151
}): Promise<void>;
2215022152

22153+
/**
22154+
* Start recording a HAR (HTTP Archive) of network activity in this context. The HAR file is written to disk when
22155+
* [tracing.stopHar()](https://playwright.dev/docs/api/class-tracing#tracing-stop-har) is called, or when the returned
22156+
* [Disposable](https://playwright.dev/docs/api/class-disposable) is disposed.
22157+
*
22158+
* Only one HAR recording can be active at a time per
22159+
* [BrowserContext](https://playwright.dev/docs/api/class-browsercontext).
22160+
*
22161+
* **Usage**
22162+
*
22163+
* ```js
22164+
* await context.tracing.startHar('trace.har');
22165+
* const page = await context.newPage();
22166+
* await page.goto('https://playwright.dev');
22167+
* await context.tracing.stopHar();
22168+
* ```
22169+
*
22170+
* @param path Path on the filesystem to write the HAR file to. If the file name ends with `.zip`, the HAR is saved as a zip
22171+
* archive with response bodies attached as separate files.
22172+
* @param options
22173+
*/
22174+
startHar(path: string, options?: {
22175+
/**
22176+
* Optional setting to control resource content management. If `omit` is specified, content is not persisted. If
22177+
* `attach` is specified, resources are persisted as separate files or entries in the ZIP archive. If `embed` is
22178+
* specified, content is stored inline the HAR file as per HAR specification. Defaults to `attach` for `.zip` output
22179+
* files and to `embed` for all other file extensions.
22180+
*/
22181+
content?: "omit"|"embed"|"attach";
22182+
22183+
/**
22184+
* When set to `minimal`, only record information necessary for routing from HAR. This omits sizes, timing, page,
22185+
* cookies, security and other types of HAR information that are not used when replaying from HAR. Defaults to `full`.
22186+
*/
22187+
mode?: "full"|"minimal";
22188+
22189+
/**
22190+
* A glob or regex pattern to filter requests that are stored in the HAR. Defaults to none.
22191+
*/
22192+
urlFilter?: string|RegExp;
22193+
}): Promise<Disposable>;
22194+
2215122195
/**
2215222196
* Stop tracing.
2215322197
* @param options
@@ -22173,6 +22217,12 @@ export interface Tracing {
2217322217
*/
2217422218
path?: string;
2217522219
}): Promise<void>;
22220+
22221+
/**
22222+
* Stop HAR recording and save the HAR file to the path given to
22223+
* [tracing.startHar(path[, options])](https://playwright.dev/docs/api/class-tracing#tracing-start-har).
22224+
*/
22225+
stopHar(): Promise<void>;
2217622226
}
2217722227

2217822228
/**

packages/playwright-core/src/client/browserContext.ts

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import { headersObjectToArray } from '@isomorphic/headers';
1919
import { urlMatchesEqual } from '@isomorphic/urlMatch';
2020
import { isRegExp, isString } from '@isomorphic/rtti';
2121
import { rewriteErrorMessage } from '@isomorphic/stackTrace';
22-
import { Artifact } from './artifact';
2322
import { Browser } from './browser';
2423
import { CDPSession } from './cdpSession';
2524
import { ChannelOwner } from './channelOwner';
@@ -76,7 +75,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
7675
readonly clock: Clock;
7776

7877
readonly _serviceWorkers = new Set<Worker>();
79-
private _harRecorders = new Map<string, { path: string, content: 'embed' | 'attach' | 'omit' | undefined }>();
8078
private _closingStatus: 'none' | 'closing' | 'closed' = 'none';
8179
private _closeReason: string | undefined;
8280
private _harRouters: HarRouter[] = [];
@@ -179,7 +177,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
179177
if (!recordHar)
180178
return;
181179
const defaultContent = recordHar.path.endsWith('.zip') ? 'attach' : 'embed';
182-
await this._recordIntoHAR(recordHar.path, null, {
180+
await this.tracing._recordIntoHAR(recordHar.path, null, {
183181
url: recordHar.urlFilter,
184182
updateContent: recordHar.content ?? (recordHar.omitContent ? 'omit' : defaultContent),
185183
updateMode: recordHar.mode ?? 'full',
@@ -384,27 +382,12 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
384382
await this._updateWebSocketInterceptionPatterns({ title: 'Route WebSockets' });
385383
}
386384

387-
async _recordIntoHAR(har: string, page: Page | null, options: { url?: string | RegExp, updateContent?: 'attach' | 'embed' | 'omit', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
388-
const { harId } = await this._channel.harStart({
389-
page: page?._channel,
390-
options: {
391-
zip: har.endsWith('.zip'),
392-
content: options.updateContent ?? 'attach',
393-
urlGlob: isString(options.url) ? options.url : undefined,
394-
urlRegexSource: isRegExp(options.url) ? options.url.source : undefined,
395-
urlRegexFlags: isRegExp(options.url) ? options.url.flags : undefined,
396-
mode: options.updateMode ?? 'minimal',
397-
},
398-
});
399-
this._harRecorders.set(harId, { path: har, content: options.updateContent ?? 'attach' });
400-
}
401-
402385
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full' } = {}): Promise<void> {
403386
const localUtils = this._connection.localUtils();
404387
if (!localUtils)
405388
throw new Error('Route from har is not supported in thin clients');
406389
if (options.update) {
407-
await this._recordIntoHAR(har, null, options);
390+
await this.tracing._recordIntoHAR(har, null, options);
408391
return;
409392
}
410393
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });
@@ -522,25 +505,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
522505
this._closingStatus = 'closing';
523506
await this.request.dispose(options);
524507
await this._instrumentation.runBeforeCloseBrowserContext(this);
525-
await this._wrapApiCall(async () => {
526-
for (const [harId, harParams] of this._harRecorders) {
527-
const har = await this._channel.harExport({ harId });
528-
const artifact = Artifact.from(har.artifact);
529-
// Server side will compress artifact if content is attach or if file is .zip.
530-
const isCompressed = harParams.content === 'attach' || harParams.path.endsWith('.zip');
531-
const needCompressed = harParams.path.endsWith('.zip');
532-
if (isCompressed && !needCompressed) {
533-
const localUtils = this._connection.localUtils();
534-
if (!localUtils)
535-
throw new Error('Uncompressed har is not supported in thin clients');
536-
await artifact.saveAs(harParams.path + '.tmp');
537-
await localUtils.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
538-
} else {
539-
await artifact.saveAs(harParams.path);
540-
}
541-
await artifact.delete();
542-
}
543-
}, { internal: true });
508+
await this.tracing._exportAllHars();
544509
await this._channel.close(options);
545510
await this._closedPromise;
546511
}

packages/playwright-core/src/client/fetch.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,15 @@ export class APIRequest implements api.APIRequest {
8080
this._contexts.add(context);
8181
context._request = this;
8282
context._timeoutSettings.setDefaultTimeout(options.timeout ?? this._playwright._defaultContextTimeout);
83-
context._tracing._tracesDir = this._playwright._defaultLaunchOptions?.tracesDir;
83+
context.tracing._tracesDir = this._playwright._defaultLaunchOptions?.tracesDir;
8484
await context._instrumentation.runAfterCreateRequestContext(context);
8585
return context;
8686
}
8787
}
8888

8989
export class APIRequestContext extends ChannelOwner<channels.APIRequestContextChannel> implements api.APIRequestContext {
9090
_request?: APIRequest;
91-
readonly _tracing: Tracing;
91+
readonly tracing: Tracing;
9292
private _closeReason: string | undefined;
9393
_timeoutSettings: TimeoutSettings;
9494

@@ -98,7 +98,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
9898

9999
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.APIRequestContextInitializer) {
100100
super(parent, type, guid, initializer);
101-
this._tracing = Tracing.from(initializer.tracing);
101+
this.tracing = Tracing.from(initializer.tracing);
102102
this._timeoutSettings = new TimeoutSettings(this._platform);
103103
}
104104

@@ -109,14 +109,15 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
109109
async dispose(options: { reason?: string } = {}): Promise<void> {
110110
this._closeReason = options.reason;
111111
await this._instrumentation.runBeforeCloseRequestContext(this);
112+
await this.tracing._exportAllHars();
112113
try {
113114
await this._channel.dispose(options);
114115
} catch (e) {
115116
if (isTargetClosedError(e))
116117
return;
117118
throw e;
118119
}
119-
this._tracing._resetStackCounter();
120+
this.tracing._resetStackCounter();
120121
this._request?._contexts.delete(this);
121122
}
122123

packages/playwright-core/src/client/page.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
529529
if (!localUtils)
530530
throw new Error('Route from har is not supported in thin clients');
531531
if (options.update) {
532-
await this._browserContext._recordIntoHAR(har, this, options);
532+
await this._browserContext.tracing._recordIntoHAR(har, this, options);
533533
return;
534534
}
535535
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });

0 commit comments

Comments
 (0)