chore(tracing): add tracing to APIRequestContext (#11502)

This commit is contained in:
Yury Semikhatsky 2022-01-22 11:25:13 -08:00 committed by GitHub
parent 8a7e4f9814
commit ab9d5a0dc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 321 additions and 171 deletions

View File

@ -64,7 +64,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> 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;
}

View File

@ -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<channels.BrowserContextChannel> implements api.BrowserContext {
_pages = new Set<Page>();
private _routes: network.RouteHandler[] = [];
readonly _browser: Browser | null = null;
private _browserType: BrowserType | undefined;
_localUtils!: LocalUtils;
readonly _bindings = new Map<string, (source: structs.BindingSource, ...args: any[]) => any>();
_timeoutSettings = new TimeoutSettings();
_ownerPage: Page | undefined;
@ -71,7 +69,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
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)));

View File

@ -109,7 +109,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> 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;
}

View File

@ -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<channels.RootChannel> {
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;

View File

@ -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<channels.APIRequestContextChannel> 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<channels.APIRequestContextCh
super(parent, type, guid, initializer, createInstrumentation());
if (parent instanceof APIRequest)
this._request = parent;
this._tracing = Tracing.from(initializer.tracing);
}
async dispose(): Promise<void> {

View File

@ -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<channels.TracingChannel> 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);
}
}

View File

@ -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<BrowserContext, channels.BrowserContextChannel> implements channels.BrowserContextChannel {
_type_EventTarget = true;
@ -37,6 +38,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
super(scope, context, 'BrowserContext', {
isChromium: context._browser.options.isChromium,
APIRequestContext: APIRequestContextDispatcher.from(scope, context.fetchRequest),
tracing: TracingDispatcher.from(scope, context.tracing),
}, true);
this._context = context;
// Note: when launching persistent context, dispatcher is created very late,
@ -189,23 +191,6 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page ? params.page as PageDispatcher : params.frame as FrameDispatcher)._object)) };
}
async tracingStart(params: channels.BrowserContextTracingStartParams): Promise<channels.BrowserContextTracingStartResult> {
await this._context.tracing.start(params);
}
async tracingStartChunk(params: channels.BrowserContextTracingStartChunkParams): Promise<channels.BrowserContextTracingStartChunkResult> {
await this._context.tracing.startChunk(params);
}
async tracingStopChunk(params: channels.BrowserContextTracingStopChunkParams): Promise<channels.BrowserContextTracingStopChunkResult> {
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<channels.BrowserContextTracingStopResult> {
await this._context.tracing.stop();
}
async harExport(params: channels.BrowserContextHarExportParams): Promise<channels.BrowserContextHarExportResult> {
const artifact = await this._context._harRecorder?.export();
if (!artifact)

View File

@ -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<Request, channels.RequestChannel> implements channels.RequestChannel {
_type_Request: boolean;
@ -163,7 +164,9 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
}
private constructor(scope: DispatcherScope, request: APIRequestContext) {
super(scope, request, 'APIRequestContext', {}, true);
super(scope, request, 'APIRequestContext', {
tracing: TracingDispatcher.from(scope, request.tracing()),
}, true);
request.once(APIRequestContext.Events.Dispose, () => {
if (!this._disposed)
super._dispose();
@ -175,7 +178,7 @@ export class APIRequestContextDispatcher extends Dispatcher<APIRequestContext, c
}
async dispose(params?: channels.APIRequestContextDisposeParams): Promise<void> {
this._object.dispose();
await this._object.dispose();
}
async fetch(params: channels.APIRequestContextFetchParams, metadata: CallMetadata): Promise<channels.APIRequestContextFetchResult> {

View File

@ -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<Tracing, channels.TracingChannel> implements channels.TracingChannel {
_type_Tracing = true;
static from(scope: DispatcherScope, tracing: Tracing): TracingDispatcher {
const result = existingDispatcher<TracingDispatcher>(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<channels.TracingTracingStartResult> {
await this._object.start(params);
}
async tracingStartChunk(params: channels.TracingTracingStartChunkParams): Promise<channels.TracingTracingStartChunkResult> {
await this._object.startChunk(params);
}
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
const { artifact, sourceEntries } = await this._object.stopChunk(params);
return { artifact: artifact ? new ArtifactDispatcher(this._scope, artifact) : undefined, sourceEntries };
}
async tracingStop(params: channels.TracingTracingStopParams): Promise<channels.TracingTracingStopResult> {
await this._object.stop();
}
}

View File

@ -32,6 +32,7 @@ export type InitializerTraits<T> =
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> =
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> =
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<BrowserContextPauseResult>;
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextNewCDPSessionResult>;
tracingStart(params: BrowserContextTracingStartParams, metadata?: Metadata): Promise<BrowserContextTracingStartResult>;
tracingStartChunk(params: BrowserContextTracingStartChunkParams, metadata?: Metadata): Promise<BrowserContextTracingStartChunkResult>;
tracingStopChunk(params: BrowserContextTracingStopChunkParams, metadata?: Metadata): Promise<BrowserContextTracingStopChunkResult>;
tracingStop(params?: BrowserContextTracingStopParams, metadata?: Metadata): Promise<BrowserContextTracingStopResult>;
harExport(params?: BrowserContextHarExportParams, metadata?: Metadata): Promise<BrowserContextHarExportResult>;
}
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<TracingTracingStartResult>;
tracingStartChunk(params: TracingTracingStartChunkParams, metadata?: Metadata): Promise<TracingTracingStartChunkResult>;
tracingStopChunk(params: TracingTracingStopChunkParams, metadata?: Metadata): Promise<TracingTracingStopChunkResult>;
tracingStop(params?: TracingTracingStopParams, metadata?: Metadata): Promise<TracingTracingStopResult>;
}
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,

View File

@ -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

View File

@ -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,

View File

@ -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<void>[] = [];

View File

@ -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<void>;
abstract _defaultOptions(): FetchRequestOptions;
abstract _addCookies(cookies: types.NetworkCookie[]): Promise<void>;
@ -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();
}

View File

@ -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<void>;
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
@ -72,11 +73,11 @@ export interface InstrumentationListener {
}
export function createInstrumentation(): Instrumentation {
const listeners = new Map<InstrumentationListener, BrowserContext | null>();
const listeners = new Map<InstrumentationListener, BrowserContext | APIRequestContext | null>();
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'))

View File

@ -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, {

View File

@ -40,7 +40,7 @@ type HarTracerOptions = {
};
export class HarTracer {
private _context: BrowserContext;
private _context: BrowserContext | APIRequestContext;
private _barrierPromises = new Set<Promise<void>>();
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: [],

View File

@ -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<string>,
traceSha1s: Set<string>,
@ -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<string, { sdkObject: SdkObject, metadata: CallMetadata, beforeSnapshot: Promise<void>, actionSnapshot?: Promise<void>, afterSnapshot?: Promise<void> }>();
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<string>();
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();

View File

@ -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: [] }
]
},
]

View File

@ -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.