From ab9d5a0dc4744fbfad6be2ed219b81977b6aa97d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Sat, 22 Jan 2022 11:25:13 -0800 Subject: [PATCH] chore(tracing): add tracing to APIRequestContext (#11502) --- .../playwright-core/src/client/browser.ts | 2 +- .../src/client/browserContext.ts | 4 +- .../playwright-core/src/client/browserType.ts | 2 +- .../playwright-core/src/client/connection.ts | 4 + packages/playwright-core/src/client/fetch.ts | 4 + .../playwright-core/src/client/tracing.ts | 35 ++++--- .../dispatchers/browserContextDispatcher.ts | 19 +--- .../src/dispatchers/networkDispatchers.ts | 7 +- .../src/dispatchers/tracingDispatcher.ts | 52 ++++++++++ .../playwright-core/src/protocol/channels.ts | 95 ++++++++++-------- .../playwright-core/src/protocol/protocol.yml | 66 +++++++------ .../playwright-core/src/protocol/validator.ts | 27 +++--- .../src/server/browserContext.ts | 5 +- packages/playwright-core/src/server/fetch.ts | 26 ++++- .../src/server/instrumentation.ts | 9 +- .../src/server/supplements/har/harRecorder.ts | 3 +- .../src/server/supplements/har/harTracer.ts | 28 +++--- .../src/server/trace/recorder/tracing.ts | 97 +++++++++++++------ tests/channels.spec.ts | 6 +- utils/check_deps.js | 1 + 20 files changed, 321 insertions(+), 171 deletions(-) create mode 100644 packages/playwright-core/src/dispatchers/tracingDispatcher.ts diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 9ef8e0551e..abafa579d1 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -64,7 +64,7 @@ export class Browser extends ChannelOwner implements ap this._contexts.add(context); context._logger = options.logger || this._logger; context._setBrowserType(this._browserType); - context._localUtils = this._localUtils; + context.tracing._localUtils = this._localUtils; await this._browserType._onDidCreateContext?.(context); return context; } diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 5356b6bbd5..f0740642ab 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -38,14 +38,12 @@ import type { BrowserType } from './browserType'; import { Artifact } from './artifact'; import { APIRequestContext } from './fetch'; import { createInstrumentation } from './clientInstrumentation'; -import { LocalUtils } from './localUtils'; export class BrowserContext extends ChannelOwner implements api.BrowserContext { _pages = new Set(); private _routes: network.RouteHandler[] = []; readonly _browser: Browser | null = null; private _browserType: BrowserType | undefined; - _localUtils!: LocalUtils; readonly _bindings = new Map any>(); _timeoutSettings = new TimeoutSettings(); _ownerPage: Page | undefined; @@ -71,7 +69,7 @@ export class BrowserContext extends ChannelOwner if (parent instanceof Browser) this._browser = parent; this._isChromium = this._browser?._name === 'chromium'; - this.tracing = new Tracing(this); + this.tracing = Tracing.from(initializer.tracing); this.request = APIRequestContext.from(initializer.APIRequestContext); this._channel.on('bindingCall', ({ binding }) => this._onBinding(BindingCall.from(binding))); diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 344044360a..14716faf6a 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -109,7 +109,7 @@ export class BrowserType extends ChannelOwner imple context._options = contextParams; context._logger = logger; context._setBrowserType(this); - context._localUtils = this._playwright._utils; + context.tracing._localUtils = this._playwright._utils; await this._onDidCreateContext?.(context); return context; } diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 5fff680c9d..8bdda581cb 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -41,6 +41,7 @@ import { EventEmitter } from 'events'; import { JsonPipe } from './jsonPipe'; import { APIRequestContext } from './fetch'; import { LocalUtils } from './localUtils'; +import { Tracing } from './tracing'; class Root extends ChannelOwner { constructor(connection: Connection) { @@ -254,6 +255,9 @@ export class Connection extends EventEmitter { case 'Selectors': result = new SelectorsOwner(parent, type, guid, initializer); break; + case 'Tracing': + result = new Tracing(parent, type, guid, initializer); + break; case 'WebSocket': result = new WebSocket(parent, type, guid, initializer); break; diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index 5193a5237c..4f1905ca2d 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -29,6 +29,7 @@ import { RawHeaders } from './network'; import { FilePayload, Headers, StorageState } from './types'; import { Playwright } from './playwright'; import { createInstrumentation } from './clientInstrumentation'; +import { Tracing } from './tracing'; export type FetchOptions = { params?: { [key: string]: string; }, @@ -71,6 +72,7 @@ export class APIRequest implements api.APIRequest { extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, storageState, })).request); + context._tracing._localUtils = this._playwright._utils; this._contexts.add(context); await this._onDidCreateContext?.(context); return context; @@ -79,6 +81,7 @@ export class APIRequest implements api.APIRequest { export class APIRequestContext extends ChannelOwner implements api.APIRequestContext { private _request?: APIRequest; + readonly _tracing: Tracing; static from(channel: channels.APIRequestContextChannel): APIRequestContext { return (channel as any)._object; @@ -88,6 +91,7 @@ export class APIRequestContext extends ChannelOwner { diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 5af502322d..e46e59c38f 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -17,24 +17,29 @@ import * as api from '../../types/types'; import * as channels from '../protocol/channels'; import { Artifact } from './artifact'; -import { BrowserContext } from './browserContext'; +import { ChannelOwner } from './channelOwner'; +import { LocalUtils } from './localUtils'; -export class Tracing implements api.Tracing { - private _context: BrowserContext; +export class Tracing extends ChannelOwner implements api.Tracing { + _localUtils!: LocalUtils; - constructor(channel: BrowserContext) { - this._context = channel; + static from(channel: channels.TracingChannel): Tracing { + return (channel as any)._object; + } + + constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) { + super(parent, type, guid, initializer); } async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) { - await this._context._wrapApiCall(async () => { - await this._context._channel.tracingStart(options); - await this._context._channel.tracingStartChunk({ title: options.title }); + await this._wrapApiCall(async () => { + await this._channel.tracingStart(options); + await this._channel.tracingStartChunk({ title: options.title }); }); } async startChunk(options: { title?: string } = {}) { - await this._context._channel.tracingStartChunk(options); + await this._channel.tracingStartChunk(options); } async stopChunk(options: { path?: string } = {}) { @@ -42,16 +47,16 @@ export class Tracing implements api.Tracing { } async stop(options: { path?: string } = {}) { - await this._context._wrapApiCall(async () => { + await this._wrapApiCall(async () => { await this._doStopChunk(options.path); - await this._context._channel.tracingStop(); + await this._channel.tracingStop(); }); } private async _doStopChunk(filePath: string | undefined) { - const isLocal = !this._context._connection.isRemote(); + const isLocal = !this._connection.isRemote(); - let mode: channels.BrowserContextTracingStopChunkParams['mode'] = 'doNotSave'; + let mode: channels.TracingTracingStopChunkParams['mode'] = 'doNotSave'; if (filePath) { if (isLocal) mode = 'compressTraceAndSources'; @@ -59,7 +64,7 @@ export class Tracing implements api.Tracing { mode = 'compressTrace'; } - const result = await this._context._channel.tracingStopChunk({ mode }); + const result = await this._channel.tracingStopChunk({ mode }); if (!filePath) { // Not interested in artifacts. return; @@ -76,6 +81,6 @@ export class Tracing implements api.Tracing { // Add local sources to the remote trace if necessary. if (result.sourceEntries?.length) - await this._context._localUtils.zip(filePath, result.sourceEntries); + await this._localUtils.zip(filePath, result.sourceEntries); } } diff --git a/packages/playwright-core/src/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/dispatchers/browserContextDispatcher.ts index 26f44fe778..f3d9baf9bf 100644 --- a/packages/playwright-core/src/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/dispatchers/browserContextDispatcher.ts @@ -27,6 +27,7 @@ import { CallMetadata } from '../server/instrumentation'; import { ArtifactDispatcher } from './artifactDispatcher'; import { Artifact } from '../server/artifact'; import { Request, Response } from '../server/network'; +import { TracingDispatcher } from './tracingDispatcher'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -37,6 +38,7 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.tracing.start(params); - } - - async tracingStartChunk(params: channels.BrowserContextTracingStartChunkParams): Promise { - await this._context.tracing.startChunk(params); - } - - async tracingStopChunk(params: channels.BrowserContextTracingStopChunkParams): Promise { - const { artifact, sourceEntries } = await this._context.tracing.stopChunk(params); - return { artifact: artifact ? new ArtifactDispatcher(this._scope, artifact) : undefined, sourceEntries }; - } - - async tracingStop(params: channels.BrowserContextTracingStopParams): Promise { - await this._context.tracing.stop(); - } - async harExport(params: channels.BrowserContextHarExportParams): Promise { const artifact = await this._context._harRecorder?.export(); if (!artifact) diff --git a/packages/playwright-core/src/dispatchers/networkDispatchers.ts b/packages/playwright-core/src/dispatchers/networkDispatchers.ts index e4b528f057..6889072886 100644 --- a/packages/playwright-core/src/dispatchers/networkDispatchers.ts +++ b/packages/playwright-core/src/dispatchers/networkDispatchers.ts @@ -20,6 +20,7 @@ import { CallMetadata } from '../server/instrumentation'; import { Request, Response, Route, WebSocket } from '../server/network'; import { Dispatcher, DispatcherScope, existingDispatcher, lookupNullableDispatcher } from './dispatcher'; import { FrameDispatcher } from './frameDispatcher'; +import { TracingDispatcher } from './tracingDispatcher'; export class RequestDispatcher extends Dispatcher implements channels.RequestChannel { _type_Request: boolean; @@ -163,7 +164,9 @@ export class APIRequestContextDispatcher extends Dispatcher { if (!this._disposed) super._dispose(); @@ -175,7 +178,7 @@ export class APIRequestContextDispatcher extends Dispatcher { - this._object.dispose(); + await this._object.dispose(); } async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise { diff --git a/packages/playwright-core/src/dispatchers/tracingDispatcher.ts b/packages/playwright-core/src/dispatchers/tracingDispatcher.ts new file mode 100644 index 0000000000..ba89a4707a --- /dev/null +++ b/packages/playwright-core/src/dispatchers/tracingDispatcher.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as channels from '../protocol/channels'; +import { Tracing } from '../server/trace/recorder/tracing'; +import { ArtifactDispatcher } from './artifactDispatcher'; +import { Dispatcher, DispatcherScope, existingDispatcher } from './dispatcher'; + +export class TracingDispatcher extends Dispatcher implements channels.TracingChannel { + _type_Tracing = true; + + static from(scope: DispatcherScope, tracing: Tracing): TracingDispatcher { + const result = existingDispatcher(tracing); + return result || new TracingDispatcher(scope, tracing); + } + + constructor(scope: DispatcherScope, tracing: Tracing) { + super(scope, tracing, 'Tracing', {}, true); + tracing.on(Tracing.Events.Dispose, () => this._dispose()); + } + + async tracingStart(params: channels.TracingTracingStartParams): Promise { + await this._object.start(params); + } + + async tracingStartChunk(params: channels.TracingTracingStartChunkParams): Promise { + await this._object.startChunk(params); + } + + async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise { + const { artifact, sourceEntries } = await this._object.stopChunk(params); + return { artifact: artifact ? new ArtifactDispatcher(this._scope, artifact) : undefined, sourceEntries }; + } + + async tracingStop(params: channels.TracingTracingStopParams): Promise { + await this._object.stop(); + } + +} diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index 6893926cdd..9b5068d797 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -32,6 +32,7 @@ export type InitializerTraits = T extends CDPSessionChannel ? CDPSessionInitializer : T extends StreamChannel ? StreamInitializer : T extends ArtifactChannel ? ArtifactInitializer : + T extends TracingChannel ? TracingInitializer : T extends DialogChannel ? DialogInitializer : T extends BindingCallChannel ? BindingCallInitializer : T extends ConsoleMessageChannel ? ConsoleMessageInitializer : @@ -66,6 +67,7 @@ export type EventsTraits = T extends CDPSessionChannel ? CDPSessionEvents : T extends StreamChannel ? StreamEvents : T extends ArtifactChannel ? ArtifactEvents : + T extends TracingChannel ? TracingEvents : T extends DialogChannel ? DialogEvents : T extends BindingCallChannel ? BindingCallEvents : T extends ConsoleMessageChannel ? ConsoleMessageEvents : @@ -100,6 +102,7 @@ export type EventTargetTraits = T extends CDPSessionChannel ? CDPSessionEventTarget : T extends StreamChannel ? StreamEventTarget : T extends ArtifactChannel ? ArtifactEventTarget : + T extends TracingChannel ? TracingEventTarget : T extends DialogChannel ? DialogEventTarget : T extends BindingCallChannel ? BindingCallEventTarget : T extends ConsoleMessageChannel ? ConsoleMessageEventTarget : @@ -262,7 +265,9 @@ export type FormField = { }; // ----------- APIRequestContext ----------- -export type APIRequestContextInitializer = {}; +export type APIRequestContextInitializer = { + tracing: TracingChannel, +}; export interface APIRequestContextEventTarget { } export interface APIRequestContextChannel extends APIRequestContextEventTarget, Channel { @@ -506,6 +511,7 @@ export type PlaywrightNewRequestParams = { cookies: NetworkCookie[], origins: OriginStorage[], }, + tracesDir?: string, }; export type PlaywrightNewRequestOptions = { baseURL?: string, @@ -527,6 +533,7 @@ export type PlaywrightNewRequestOptions = { cookies: NetworkCookie[], origins: OriginStorage[], }, + tracesDir?: string, }; export type PlaywrightNewRequestResult = { request: APIRequestContextChannel, @@ -1010,6 +1017,7 @@ export interface EventTargetEvents { export type BrowserContextInitializer = { isChromium: boolean, APIRequestContext: APIRequestContextChannel, + tracing: TracingChannel, }; export interface BrowserContextEventTarget { on(event: 'bindingCall', callback: (params: BrowserContextBindingCallEvent) => void): this; @@ -1046,10 +1054,6 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise; recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise; newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: Metadata): Promise; - tracingStart(params: BrowserContextTracingStartParams, metadata?: Metadata): Promise; - tracingStartChunk(params: BrowserContextTracingStartChunkParams, metadata?: Metadata): Promise; - tracingStopChunk(params: BrowserContextTracingStopChunkParams, metadata?: Metadata): Promise; - tracingStop(params?: BrowserContextTracingStopParams, metadata?: Metadata): Promise; harExport(params?: BrowserContextHarExportParams, metadata?: Metadata): Promise; } export type BrowserContextBindingCallEvent = { @@ -1249,39 +1253,6 @@ export type BrowserContextNewCDPSessionOptions = { export type BrowserContextNewCDPSessionResult = { session: CDPSessionChannel, }; -export type BrowserContextTracingStartParams = { - name?: string, - snapshots?: boolean, - screenshots?: boolean, - sources?: boolean, -}; -export type BrowserContextTracingStartOptions = { - name?: string, - snapshots?: boolean, - screenshots?: boolean, - sources?: boolean, -}; -export type BrowserContextTracingStartResult = void; -export type BrowserContextTracingStartChunkParams = { - title?: string, -}; -export type BrowserContextTracingStartChunkOptions = { - title?: string, -}; -export type BrowserContextTracingStartChunkResult = void; -export type BrowserContextTracingStopChunkParams = { - mode: 'doNotSave' | 'compressTrace' | 'compressTraceAndSources', -}; -export type BrowserContextTracingStopChunkOptions = { - -}; -export type BrowserContextTracingStopChunkResult = { - artifact?: ArtifactChannel, - sourceEntries?: NameValue[], -}; -export type BrowserContextTracingStopParams = {}; -export type BrowserContextTracingStopOptions = {}; -export type BrowserContextTracingStopResult = void; export type BrowserContextHarExportParams = {}; export type BrowserContextHarExportOptions = {}; export type BrowserContextHarExportResult = { @@ -3224,6 +3195,54 @@ export type DialogDismissResult = void; export interface DialogEvents { } +// ----------- Tracing ----------- +export type TracingInitializer = {}; +export interface TracingEventTarget { +} +export interface TracingChannel extends TracingEventTarget, Channel { + _type_Tracing: boolean; + tracingStart(params: TracingTracingStartParams, metadata?: Metadata): Promise; + tracingStartChunk(params: TracingTracingStartChunkParams, metadata?: Metadata): Promise; + tracingStopChunk(params: TracingTracingStopChunkParams, metadata?: Metadata): Promise; + tracingStop(params?: TracingTracingStopParams, metadata?: Metadata): Promise; +} +export type TracingTracingStartParams = { + name?: string, + snapshots?: boolean, + screenshots?: boolean, + sources?: boolean, +}; +export type TracingTracingStartOptions = { + name?: string, + snapshots?: boolean, + screenshots?: boolean, + sources?: boolean, +}; +export type TracingTracingStartResult = void; +export type TracingTracingStartChunkParams = { + title?: string, +}; +export type TracingTracingStartChunkOptions = { + title?: string, +}; +export type TracingTracingStartChunkResult = void; +export type TracingTracingStopChunkParams = { + mode: 'doNotSave' | 'compressTrace' | 'compressTraceAndSources', +}; +export type TracingTracingStopChunkOptions = { + +}; +export type TracingTracingStopChunkResult = { + artifact?: ArtifactChannel, + sourceEntries?: NameValue[], +}; +export type TracingTracingStopParams = {}; +export type TracingTracingStopOptions = {}; +export type TracingTracingStopResult = void; + +export interface TracingEvents { +} + // ----------- Artifact ----------- export type ArtifactInitializer = { absolutePath: string, diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index 5c98008217..6266515d39 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -230,6 +230,9 @@ FormField: APIRequestContext: type: interface + initializer: + tracing: Tracing + commands: fetch: @@ -538,6 +541,7 @@ Playwright: origins: type: array items: OriginStorage + tracesDir: string? returns: request: APIRequestContext @@ -707,6 +711,7 @@ BrowserContext: initializer: isChromium: boolean APIRequestContext: APIRequestContext + tracing: Tracing commands: @@ -822,34 +827,6 @@ BrowserContext: returns: session: CDPSession - tracingStart: - parameters: - name: string? - snapshots: boolean? - screenshots: boolean? - sources: boolean? - - tracingStartChunk: - parameters: - title: string? - - tracingStopChunk: - parameters: - mode: - type: enum - literals: - - doNotSave - - compressTrace - - compressTraceAndSources - returns: - # The artifact may be missing if the browser closes while tracing is beeing stopped. - artifact: Artifact? - sourceEntries: - type: array? - items: NameValue - - tracingStop: - harExport: returns: artifact: Artifact @@ -2530,6 +2507,39 @@ Dialog: dismiss: +Tracing: + type: interface + + commands: + + tracingStart: + parameters: + name: string? + snapshots: boolean? + screenshots: boolean? + sources: boolean? + + tracingStartChunk: + parameters: + title: string? + + tracingStopChunk: + parameters: + mode: + type: enum + literals: + - doNotSave + - compressTrace + - compressTraceAndSources + returns: + # The artifact may be missing if the browser closes while tracing is beeing stopped. + artifact: Artifact? + sourceEntries: + type: array? + items: NameValue + + tracingStop: + Artifact: type: interface diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5de44413b0..4e52219081 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -236,6 +236,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { cookies: tArray(tType('NetworkCookie')), origins: tArray(tType('OriginStorage')), })), + tracesDir: tOptional(tString), }); scheme.PlaywrightHideHighlightParams = tOptional(tObject({})); scheme.SelectorsRegisterParams = tObject({ @@ -500,19 +501,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { page: tOptional(tChannel('Page')), frame: tOptional(tChannel('Frame')), }); - scheme.BrowserContextTracingStartParams = tObject({ - name: tOptional(tString), - snapshots: tOptional(tBoolean), - screenshots: tOptional(tBoolean), - sources: tOptional(tBoolean), - }); - scheme.BrowserContextTracingStartChunkParams = tObject({ - title: tOptional(tString), - }); - scheme.BrowserContextTracingStopChunkParams = tObject({ - mode: tEnum(['doNotSave', 'compressTrace', 'compressTraceAndSources']), - }); - scheme.BrowserContextTracingStopParams = tOptional(tObject({})); scheme.BrowserContextHarExportParams = tOptional(tObject({})); scheme.PageSetDefaultNavigationTimeoutNoReplyParams = tObject({ timeout: tOptional(tNumber), @@ -1167,6 +1155,19 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { promptText: tOptional(tString), }); scheme.DialogDismissParams = tOptional(tObject({})); + scheme.TracingTracingStartParams = tObject({ + name: tOptional(tString), + snapshots: tOptional(tBoolean), + screenshots: tOptional(tBoolean), + sources: tOptional(tBoolean), + }); + scheme.TracingTracingStartChunkParams = tObject({ + title: tOptional(tString), + }); + scheme.TracingTracingStopChunkParams = tObject({ + mode: tEnum(['doNotSave', 'compressTrace', 'compressTraceAndSources']), + }); + scheme.TracingTracingStopParams = tOptional(tObject({})); scheme.ArtifactPathAfterFinishedParams = tOptional(tObject({})); scheme.ArtifactSaveAsParams = tObject({ path: tString, diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 9514220513..bcf3af9279 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -81,7 +81,7 @@ export abstract class BrowserContext extends SdkObject { if (this._options.recordHar) this._harRecorder = new HarRecorder(this, { ...this._options.recordHar, path: path.join(this._browser.options.artifactsDir, `${createGuid()}.har`) }); - this.tracing = new Tracing(this); + this.tracing = new Tracing(this, browser.options.tracesDir); } isPersistentContext(): boolean { @@ -138,6 +138,7 @@ export abstract class BrowserContext extends SdkObject { this._closedStatus = 'closed'; this._deleteAllDownloads(); this._downloads.clear(); + this.tracing.dispose(); if (this._isPersistentContext) this._onClosePersistent(); this._closePromiseFulfill!(new Error('Context closed')); @@ -283,7 +284,7 @@ export abstract class BrowserContext extends SdkObject { this._closedStatus = 'closing'; await this._harRecorder?.flush(); - await this.tracing.dispose(); + await this.tracing.flush(); // Cleanup. const promises: Promise[] = []; diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 664a106b12..bf51a954fd 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -17,7 +17,6 @@ import * as http from 'http'; import * as https from 'https'; import { HttpsProxyAgent } from 'https-proxy-agent'; -import { Progress, ProgressController } from './progress'; import { SocksProxyAgent } from 'socks-proxy-agent'; import { pipeline, Readable, Transform } from 'stream'; import url from 'url'; @@ -31,6 +30,8 @@ import { CookieStore, domainMatches } from './cookieStore'; import { MultipartFormData } from './formData'; import { CallMetadata, SdkObject } from './instrumentation'; import { Playwright } from './playwright'; +import { Progress, ProgressController } from './progress'; +import { Tracing } from './trace/recorder/tracing'; import * as types from './types'; import { HeadersArray, ProxySettings } from './types'; @@ -101,7 +102,9 @@ export abstract class APIRequestContext extends SdkObject { this.fetchLog.delete(fetchUid); } - abstract dispose(): void; + abstract tracing(): Tracing; + + abstract dispose(): Promise; abstract _defaultOptions(): FetchRequestOptions; abstract _addCookies(cookies: types.NetworkCookie[]): Promise; @@ -404,7 +407,11 @@ export class BrowserContextAPIRequestContext extends APIRequestContext { context.once(BrowserContext.Events.Close, () => this._disposeImpl()); } - override dispose() { + override tracing() { + return this._context.tracing; + } + + override async dispose() { this.fetchResponses.clear(); } @@ -438,9 +445,11 @@ export class GlobalAPIRequestContext extends APIRequestContext { private readonly _cookieStore: CookieStore = new CookieStore(); private readonly _options: FetchRequestOptions; private readonly _origins: channels.OriginStorage[] | undefined; + private readonly _tracing: Tracing; constructor(playwright: Playwright, options: channels.PlaywrightNewRequestOptions) { super(playwright); + this.attribution.context = this; const timeoutSettings = new TimeoutSettings(); if (options.timeout !== undefined) timeoutSettings.setDefaultTimeout(options.timeout); @@ -464,10 +473,17 @@ export class GlobalAPIRequestContext extends APIRequestContext { proxy, timeoutSettings, }; - + this._tracing = new Tracing(this, options.tracesDir); } - override dispose() { + override tracing() { + return this._tracing; + } + + override async dispose() { + await this._tracing.flush(); + await this._tracing.deleteTmpTracesDir(); + this._tracing.dispose(); this._disposeImpl(); } diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts index 785df08d96..4a31d10007 100644 --- a/packages/playwright-core/src/server/instrumentation.ts +++ b/packages/playwright-core/src/server/instrumentation.ts @@ -16,6 +16,7 @@ import { EventEmitter } from 'events'; import { createGuid } from '../utils/utils'; +import type { APIRequestContext } from './fetch'; import type { Browser } from './browser'; import type { BrowserContext } from './browserContext'; import type { BrowserType } from './browserType'; @@ -27,7 +28,7 @@ export type Attribution = { isInternal: boolean, browserType?: BrowserType; browser?: Browser; - context?: BrowserContext; + context?: BrowserContext | APIRequestContext; page?: Page; frame?: Frame; }; @@ -50,7 +51,7 @@ export class SdkObject extends EventEmitter { } export interface Instrumentation { - addListener(listener: InstrumentationListener, context: BrowserContext | null): void; + addListener(listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null): void; removeListener(listener: InstrumentationListener): void; onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise; @@ -72,11 +73,11 @@ export interface InstrumentationListener { } export function createInstrumentation(): Instrumentation { - const listeners = new Map(); + const listeners = new Map(); return new Proxy({}, { get: (obj: any, prop: string) => { if (prop === 'addListener') - return (listener: InstrumentationListener, context: BrowserContext | null) => listeners.set(listener, context); + return (listener: InstrumentationListener, context: BrowserContext | APIRequestContext | null) => listeners.set(listener, context); if (prop === 'removeListener') return (listener: InstrumentationListener) => listeners.delete(listener); if (!prop.startsWith('on')) diff --git a/packages/playwright-core/src/server/supplements/har/harRecorder.ts b/packages/playwright-core/src/server/supplements/har/harRecorder.ts index 3f38f47ef4..881a98d11c 100644 --- a/packages/playwright-core/src/server/supplements/har/harRecorder.ts +++ b/packages/playwright-core/src/server/supplements/har/harRecorder.ts @@ -15,6 +15,7 @@ */ import fs from 'fs'; +import { APIRequestContext } from '../../fetch'; import { Artifact } from '../../artifact'; import { BrowserContext } from '../../browserContext'; import * as har from './har'; @@ -32,7 +33,7 @@ export class HarRecorder { private _tracer: HarTracer; private _entries: har.Entry[] = []; - constructor(context: BrowserContext, options: HarOptions) { + constructor(context: BrowserContext | APIRequestContext, options: HarOptions) { this._artifact = new Artifact(context, options.path); this._options = options; this._tracer = new HarTracer(context, this, { diff --git a/packages/playwright-core/src/server/supplements/har/harTracer.ts b/packages/playwright-core/src/server/supplements/har/harTracer.ts index b6936574bf..6a0e791e99 100644 --- a/packages/playwright-core/src/server/supplements/har/harTracer.ts +++ b/packages/playwright-core/src/server/supplements/har/harTracer.ts @@ -40,7 +40,7 @@ type HarTracerOptions = { }; export class HarTracer { - private _context: BrowserContext; + private _context: BrowserContext | APIRequestContext; private _barrierPromises = new Set>(); private _delegate: HarTracerDelegate; private _options: HarTracerOptions; @@ -49,7 +49,7 @@ export class HarTracer { private _started = false; private _entrySymbol: symbol; - constructor(context: BrowserContext, delegate: HarTracerDelegate, options: HarTracerOptions) { + constructor(context: BrowserContext | APIRequestContext, delegate: HarTracerDelegate, options: HarTracerOptions) { this._context = context; this._delegate = delegate; this._options = options; @@ -60,15 +60,20 @@ export class HarTracer { if (this._started) return; this._started = true; + const apiRequest = this._context instanceof APIRequestContext ? this._context : this._context.fetchRequest; this._eventListeners = [ - eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})), - eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFailed, request => this._onRequestFailed(request)), - eventsHelper.addEventListener(this._context, BrowserContext.Events.Response, (response: network.Response) => this._onResponse(response)), - eventsHelper.addEventListener(this._context.fetchRequest, APIRequestContext.Events.Request, (event: APIRequestEvent) => this._onAPIRequest(event)), - eventsHelper.addEventListener(this._context.fetchRequest, APIRequestContext.Events.RequestFinished, (event: APIRequestFinishedEvent) => this._onAPIRequestFinished(event)), + eventsHelper.addEventListener(apiRequest, APIRequestContext.Events.Request, (event: APIRequestEvent) => this._onAPIRequest(event)), + eventsHelper.addEventListener(apiRequest, APIRequestContext.Events.RequestFinished, (event: APIRequestFinishedEvent) => this._onAPIRequestFinished(event)), ]; + if (this._context instanceof BrowserContext) { + this._eventListeners.push( + eventsHelper.addEventListener(this._context, BrowserContext.Events.Page, (page: Page) => this._ensurePageEntry(page)), + eventsHelper.addEventListener(this._context, BrowserContext.Events.Request, (request: network.Request) => this._onRequest(request)), + eventsHelper.addEventListener(this._context, BrowserContext.Events.RequestFinished, ({ request, response }) => this._onRequestFinished(request, response).catch(() => {})), + 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 _entryForRequest(request: network.Request | APIRequestEvent): har.Entry | undefined { @@ -370,6 +375,7 @@ export class HarTracer { eventsHelper.removeEventListeners(this._eventListeners); this._barrierPromises.clear(); + const context = this._context instanceof BrowserContext ? this._context : undefined; const log: har.Log = { version: '1.2', creator: { @@ -377,8 +383,8 @@ export class HarTracer { version: require('../../../../package.json')['version'], }, browser: { - name: this._context._browser.options.name, - version: this._context._browser.version() + name: context?._browser.options.name || '', + version: context?._browser.version() || '' }, pages: Array.from(this._pageEntries.values()), entries: [], diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 45bbc5a36f..bf54dd099c 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -16,13 +16,14 @@ import { EventEmitter } from 'events'; import fs from 'fs'; +import { APIRequestContext } from '../../fetch'; import path from 'path'; import yazl from 'yazl'; import { NameValue } from '../../../common/types'; -import { commandsWithTracingSnapshots, BrowserContextTracingStopChunkParams } from '../../../protocol/channels'; +import { commandsWithTracingSnapshots, TracingTracingStopChunkParams } from '../../../protocol/channels'; import { ManualPromise } from '../../../utils/async'; import { eventsHelper, RegisteredListener } from '../../../utils/eventsHelper'; -import { calculateSha1, createGuid, mkdirIfNeeded, monotonicTime } from '../../../utils/utils'; +import { assert, calculateSha1, createGuid, mkdirIfNeeded, monotonicTime, removeFolders } from '../../../utils/utils'; import { Artifact } from '../../artifact'; import { BrowserContext } from '../../browserContext'; import { ElementHandle } from '../../dom'; @@ -47,6 +48,8 @@ type RecordingState = { traceName: string, networkFile: string, traceFile: string, + tracesDir: string, + resourcesDir: string, filesCount: number, networkSha1s: Set, traceSha1s: Set, @@ -56,25 +59,28 @@ type RecordingState = { const kScreencastOptions = { width: 800, height: 600, quality: 90 }; -export class Tracing implements InstrumentationListener, SnapshotterDelegate, HarTracerDelegate { +export class Tracing extends SdkObject implements InstrumentationListener, SnapshotterDelegate, HarTracerDelegate { + static Events = { + Dispose: 'dispose', + }; + private _writeChain = Promise.resolve(); - private _snapshotter: Snapshotter; + private _snapshotter?: Snapshotter; private _harTracer: HarTracer; private _screencastListeners: RegisteredListener[] = []; private _pendingCalls = new Map, actionSnapshot?: Promise, afterSnapshot?: Promise }>(); - private _context: BrowserContext; - private _resourcesDir: string; + private _context: BrowserContext | APIRequestContext; private _state: RecordingState | undefined; private _isStopping = false; - private _tracesDir: string; + private _precreatedTracesDir: string | undefined; + private _tracesTmpDir: string | undefined; private _allResources = new Set(); private _contextCreatedEvent: trace.ContextCreatedTraceEvent; - constructor(context: BrowserContext) { + constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { + super(context, 'Tracing'); this._context = context; - this._tracesDir = context._browser.options.tracesDir; - this._resourcesDir = path.join(this._tracesDir, 'resources'); - this._snapshotter = new Snapshotter(context, this); + this._precreatedTracesDir = tracesDir; this._harTracer = new HarTracer(context, this, { content: 'sha1', waitForContentOnStop: false, @@ -83,14 +89,20 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha this._contextCreatedEvent = { version: VERSION, type: 'context-options', - browserName: this._context._browser.options.name, - options: this._context._options, + browserName: '', + options: {}, platform: process.platform, wallTime: 0, }; + if (context instanceof BrowserContext) { + this._snapshotter = new Snapshotter(context, this); + assert(tracesDir, 'tracesDir must be specified for BrowserContext'); + this._contextCreatedEvent.browserName = context._browser.options.name; + this._contextCreatedEvent.options = context._options; + } } - start(options: TracerOptions) { + async start(options: TracerOptions) { if (this._isStopping) throw new Error('Cannot start tracing while stopping'); if (this._state) { @@ -99,14 +111,18 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha throw new Error('Tracing has been already started with different options'); return; } - // TODO: passing the same name for two contexts makes them write into a single file // and conflict. const traceName = options.name || createGuid(); - const traceFile = path.join(this._tracesDir, traceName + '.trace'); - const networkFile = path.join(this._tracesDir, traceName + '.network'); - this._state = { options, traceName, traceFile, networkFile, filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), sources: new Set(), recording: false }; - this._writeChain = fs.promises.mkdir(this._resourcesDir, { recursive: true }).then(() => fs.promises.writeFile(networkFile, '')); + // Init the state synchrounously. + this._state = { options, traceName, traceFile: '', networkFile: '', tracesDir: '', resourcesDir: '', filesCount: 0, traceSha1s: new Set(), networkSha1s: new Set(), sources: new Set(), recording: false }; + const state = this._state; + + state.tracesDir = await this._createTracesDirIfNeeded(); + state.resourcesDir = path.join(state.tracesDir, 'resources'); + state.traceFile = path.join(state.tracesDir, traceName + '.trace'); + state.networkFile = path.join(state.tracesDir, traceName + '.network'); + this._writeChain = fs.promises.mkdir(state.resourcesDir, { recursive: true }).then(() => fs.promises.writeFile(state.networkFile, '')); if (options.snapshots) this._harTracer.start(); } @@ -123,7 +139,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha const state = this._state; const suffix = state.filesCount ? `-${state.filesCount}` : ``; state.filesCount++; - state.traceFile = path.join(this._tracesDir, `${state.traceName}${suffix}.trace`); + state.traceFile = path.join(state.tracesDir, `${state.traceName}${suffix}.trace`); state.recording = true; this._appendTraceOperation(async () => { @@ -135,10 +151,12 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha if (state.options.screenshots) this._startScreencast(); if (state.options.snapshots) - await this._snapshotter.start(); + await this._snapshotter?.start(); } private _startScreencast() { + if (!(this._context instanceof BrowserContext)) + return; for (const page of this._context.pages()) this._startScreencastInPage(page); this._screencastListeners.push( @@ -148,6 +166,8 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha private _stopScreencast() { eventsHelper.removeEventListeners(this._screencastListeners); + if (!(this._context instanceof BrowserContext)) + return; for (const page of this._context.pages()) page.setScreencastOptions(null); } @@ -164,12 +184,29 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha this._state = undefined; } - async dispose() { - this._snapshotter.dispose(); + async deleteTmpTracesDir() { + if (this._tracesTmpDir) + await removeFolders([this._tracesTmpDir]); + } + + private async _createTracesDirIfNeeded() { + if (this._precreatedTracesDir) + return this._precreatedTracesDir; + this._tracesTmpDir = await fs.promises.mkdtemp('playwright-tracing-'); + return this._tracesTmpDir; + } + + async flush() { + this._snapshotter?.dispose(); await this._writeChain; } - async stopChunk(params: BrowserContextTracingStopChunkParams): Promise<{ artifact: Artifact | null, sourceEntries: NameValue[] | undefined }> { + async dispose() { + this._snapshotter?.dispose(); + this.emit(Tracing.Events.Dispose); + } + + async stopChunk(params: TracingTracingStopChunkParams): Promise<{ artifact: Artifact | null, sourceEntries: NameValue[] | undefined }> { if (this._isStopping) throw new Error(`Tracing is already stopping`); this._isStopping = true; @@ -200,7 +237,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha } if (state.options.snapshots) - await this._snapshotter.stop(); + await this._snapshotter?.stop(); // Chain the export operation against write operations, // so that neither trace files nor sha1s change during the export. @@ -216,7 +253,7 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha entries.push({ name: 'trace.trace', value: state.traceFile }); entries.push({ name: 'trace.network', value: networkFile }); for (const sha1 of new Set([...state.traceSha1s, ...state.networkSha1s])) - entries.push({ name: path.join('resources', sha1), value: path.join(this._resourcesDir, sha1) }); + entries.push({ name: path.join('resources', sha1), value: path.join(state.resourcesDir, sha1) }); let sourceEntries: NameValue[] | undefined; if (state.sources.size) { @@ -260,6 +297,8 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha } async _captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) { + if (!this._snapshotter) + return; if (!sdkObject.attribution.page) return; if (!this._snapshotter.started()) @@ -376,8 +415,8 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha if (this._allResources.has(sha1)) return; this._allResources.add(sha1); + const resourcePath = path.join(this._state!.resourcesDir, sha1); this._appendTraceOperation(async () => { - const resourcePath = path.join(this._resourcesDir, sha1); try { // Perhaps we've already written this resource? await fs.promises.access(resourcePath); @@ -394,7 +433,9 @@ export class Tracing implements InstrumentationListener, SnapshotterDelegate, Ha let error: Error | undefined; let result: T | undefined; this._writeChain = this._writeChain.then(async () => { - if (!this._context._browser.isConnected()) + // This check is here because closing the browser removes the tracesDir and tracing + // dies trying to archive. + if (this._context instanceof BrowserContext && !this._context._browser.isConnected()) return; try { result = await cb(); diff --git a/tests/channels.spec.ts b/tests/channels.spec.ts index d06a9b4f29..1ead59a433 100644 --- a/tests/channels.spec.ts +++ b/tests/channels.spec.ts @@ -64,7 +64,8 @@ it('should scope context handles', async ({ browserType, server }) => { { _guid: 'request', objects: [] }, { _guid: 'response', objects: [] }, ] }, - { _guid: 'fetchRequest', objects: [] } + { _guid: 'fetchRequest', objects: [] }, + { _guid: 'Tracing', objects: [] } ] }, ] }, { _guid: 'electron', objects: [] }, @@ -153,7 +154,8 @@ it('should scope browser handles', async ({ browserType }) => { { _guid: 'browser', objects: [ { _guid: 'browser-context', objects: [] }, - { _guid: 'fetchRequest', objects: [] } + { _guid: 'fetchRequest', objects: [] }, + { _guid: 'Tracing', objects: [] } ] }, ] diff --git a/utils/check_deps.js b/utils/check_deps.js index 9a8ad6d6c0..8a3bf9c220 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -210,6 +210,7 @@ DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/']; DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/']; DEPS['src/server/browserContext.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracing.ts']; +DEPS['src/server/fetch.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracing.ts']; DEPS['src/cli/driver.ts'] = DEPS['src/inProcessFactory.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**']; // Tracing is a client/server plugin, nothing should depend on it.