mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: align client side instrumentations (#9771)
This commit is contained in:
parent
06135eabe3
commit
87c64b2c1c
@ -37,6 +37,7 @@ import { Tracing } from './tracing';
|
|||||||
import type { BrowserType } from './browserType';
|
import type { BrowserType } from './browserType';
|
||||||
import { Artifact } from './artifact';
|
import { Artifact } from './artifact';
|
||||||
import { FetchRequest } from './fetch';
|
import { FetchRequest } from './fetch';
|
||||||
|
import { createInstrumentation } from './clientInstrumentation';
|
||||||
|
|
||||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
|
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel, channels.BrowserContextInitializer> implements api.BrowserContext {
|
||||||
_pages = new Set<Page>();
|
_pages = new Set<Page>();
|
||||||
@ -64,7 +65,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
|
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
|
||||||
super(parent, type, guid, initializer);
|
super(parent, type, guid, initializer, createInstrumentation());
|
||||||
if (parent instanceof Browser)
|
if (parent instanceof Browser)
|
||||||
this._browser = parent;
|
this._browser = parent;
|
||||||
this._isChromium = this._browser?._name === 'chromium';
|
this._isChromium = this._browser?._name === 'chromium';
|
||||||
|
|||||||
@ -20,8 +20,9 @@ import { createScheme, ValidationError, Validator } from '../protocol/validator'
|
|||||||
import { debugLogger } from '../utils/debugLogger';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
import { captureStackTrace, ParsedStackTrace } from '../utils/stackTrace';
|
import { captureStackTrace, ParsedStackTrace } from '../utils/stackTrace';
|
||||||
import { isUnderTest } from '../utils/utils';
|
import { isUnderTest } from '../utils/utils';
|
||||||
|
import { ClientInstrumentation } from './clientInstrumentation';
|
||||||
import type { Connection } from './connection';
|
import type { Connection } from './connection';
|
||||||
import type { ClientSideInstrumentation, Logger } from './types';
|
import type { Logger } from './types';
|
||||||
|
|
||||||
export abstract class ChannelOwner<T extends channels.Channel = channels.Channel, Initializer = {}> extends EventEmitter {
|
export abstract class ChannelOwner<T extends channels.Channel = channels.Channel, Initializer = {}> extends EventEmitter {
|
||||||
readonly _connection: Connection;
|
readonly _connection: Connection;
|
||||||
@ -33,15 +34,16 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||||||
readonly _channel: T;
|
readonly _channel: T;
|
||||||
readonly _initializer: Initializer;
|
readonly _initializer: Initializer;
|
||||||
_logger: Logger | undefined;
|
_logger: Logger | undefined;
|
||||||
_csi: ClientSideInstrumentation | undefined;
|
_instrumentation: ClientInstrumentation | undefined;
|
||||||
|
|
||||||
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: Initializer) {
|
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: Initializer, instrumentation?: ClientInstrumentation) {
|
||||||
super();
|
super();
|
||||||
this.setMaxListeners(0);
|
this.setMaxListeners(0);
|
||||||
this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
|
this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
|
||||||
this._type = type;
|
this._type = type;
|
||||||
this._guid = guid;
|
this._guid = guid;
|
||||||
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
||||||
|
this._instrumentation = instrumentation || this._parent?._instrumentation;
|
||||||
|
|
||||||
this._connection._objects.set(guid, this);
|
this._connection._objects.set(guid, this);
|
||||||
if (this._parent) {
|
if (this._parent) {
|
||||||
@ -72,7 +74,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createChannel(base: Object, stackTrace: ParsedStackTrace | null, csi?: ClientSideInstrumentation, callCookie?: { userObject: any }): T {
|
private _createChannel(base: Object, stackTrace: ParsedStackTrace | null, csi?: ClientInstrumentation, callCookie?: any): T {
|
||||||
const channel = new Proxy(base, {
|
const channel = new Proxy(base, {
|
||||||
get: (obj: any, prop) => {
|
get: (obj: any, prop) => {
|
||||||
if (prop === 'debugScopeState')
|
if (prop === 'debugScopeState')
|
||||||
@ -82,7 +84,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||||||
if (validator) {
|
if (validator) {
|
||||||
return (params: any) => {
|
return (params: any) => {
|
||||||
if (callCookie && csi) {
|
if (callCookie && csi) {
|
||||||
callCookie.userObject = csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName!, params), stackTrace).userObject;
|
csi.onApiCallBegin(renderCallWithParams(stackTrace!.apiName!, params), stackTrace, callCookie);
|
||||||
csi = undefined;
|
csi = undefined;
|
||||||
}
|
}
|
||||||
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
|
return this._connection.sendMessageToServer(this, prop, validator(params, ''), stackTrace);
|
||||||
@ -101,16 +103,12 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||||||
const stackTrace = captureStackTrace();
|
const stackTrace = captureStackTrace();
|
||||||
const { apiName, frameTexts } = stackTrace;
|
const { apiName, frameTexts } = stackTrace;
|
||||||
|
|
||||||
let ancestorWithCSI: ChannelOwner<any> = this;
|
|
||||||
while (!ancestorWithCSI._csi && ancestorWithCSI._parent)
|
|
||||||
ancestorWithCSI = ancestorWithCSI._parent;
|
|
||||||
|
|
||||||
// Do not report nested async calls to _wrapApiCall.
|
// Do not report nested async calls to _wrapApiCall.
|
||||||
isInternal = isInternal || stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
|
isInternal = isInternal || stackTrace.allFrames.filter(f => f.function?.includes('_wrapApiCall')).length > 1;
|
||||||
if (isInternal)
|
if (isInternal)
|
||||||
delete stackTrace.apiName;
|
delete stackTrace.apiName;
|
||||||
const csi = isInternal ? undefined : ancestorWithCSI._csi;
|
const csi = isInternal ? undefined : this._instrumentation;
|
||||||
const callCookie: { userObject: any } = { userObject: null };
|
const callCookie: any = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
||||||
|
|||||||
50
packages/playwright-core/src/client/clientInstrumentation.ts
Normal file
50
packages/playwright-core/src/client/clientInstrumentation.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
*
|
||||||
|
* 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 { ParsedStackTrace } from '../utils/stackTrace';
|
||||||
|
|
||||||
|
export interface ClientInstrumentation {
|
||||||
|
addListener(listener: ClientInstrumentationListener): void;
|
||||||
|
removeListener(listener: ClientInstrumentationListener): void;
|
||||||
|
removeAllListeners(): void;
|
||||||
|
onApiCallBegin(apiCall: string, stackTrace: ParsedStackTrace | null, userData: any): void;
|
||||||
|
onApiCallEnd(userData: any, error?: Error): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientInstrumentationListener {
|
||||||
|
onApiCallBegin?(apiCall: string, stackTrace: ParsedStackTrace | null, userData: any): any;
|
||||||
|
onApiCallEnd?(userData: any, error?: Error): any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInstrumentation(): ClientInstrumentation {
|
||||||
|
const listeners: ClientInstrumentationListener[] = [];
|
||||||
|
return new Proxy({}, {
|
||||||
|
get: (obj: any, prop: string) => {
|
||||||
|
if (prop === 'addListener')
|
||||||
|
return (listener: ClientInstrumentationListener) => listeners.push(listener);
|
||||||
|
if (prop === 'removeListener')
|
||||||
|
return (listener: ClientInstrumentationListener) => listeners.splice(listeners.indexOf(listener), 1);
|
||||||
|
if (prop === 'removeAllListeners')
|
||||||
|
return () => listeners.splice(0, listeners.length);
|
||||||
|
if (!prop.startsWith('on'))
|
||||||
|
return obj[prop];
|
||||||
|
return async (...params: any[]) => {
|
||||||
|
for (const listener of listeners)
|
||||||
|
await (listener as any)[prop]?.(...params);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -62,7 +62,6 @@ export class Connection extends EventEmitter {
|
|||||||
private _rootObject: Root;
|
private _rootObject: Root;
|
||||||
private _closedErrorMessage: string | undefined;
|
private _closedErrorMessage: string | undefined;
|
||||||
private _isRemote = false;
|
private _isRemote = false;
|
||||||
private _sourceCollector: Set<string> | undefined;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@ -89,10 +88,6 @@ export class Connection extends EventEmitter {
|
|||||||
return this._objects.get(guid)!;
|
return this._objects.get(guid)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSourceCollector(collector: Set<string> | undefined) {
|
|
||||||
this._sourceCollector = collector;
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessageToServer(object: ChannelOwner, method: string, params: any, maybeStackTrace: ParsedStackTrace | null): Promise<any> {
|
async sendMessageToServer(object: ChannelOwner, method: string, params: any, maybeStackTrace: ParsedStackTrace | null): Promise<any> {
|
||||||
if (this._closedErrorMessage)
|
if (this._closedErrorMessage)
|
||||||
throw new Error(this._closedErrorMessage);
|
throw new Error(this._closedErrorMessage);
|
||||||
@ -100,8 +95,6 @@ export class Connection extends EventEmitter {
|
|||||||
const guid = object._guid;
|
const guid = object._guid;
|
||||||
const stackTrace: ParsedStackTrace = maybeStackTrace || { frameTexts: [], frames: [], apiName: '', allFrames: [] };
|
const stackTrace: ParsedStackTrace = maybeStackTrace || { frameTexts: [], frames: [], apiName: '', allFrames: [] };
|
||||||
const { frames, apiName } = stackTrace;
|
const { frames, apiName } = stackTrace;
|
||||||
if (this._sourceCollector)
|
|
||||||
frames.forEach(f => this._sourceCollector!.add(f.file));
|
|
||||||
const id = ++this._lastId;
|
const id = ++this._lastId;
|
||||||
const converted = { id, guid, method, params };
|
const converted = { id, guid, method, params };
|
||||||
// Do not include metadata in debug logs to avoid noise.
|
// Do not include metadata in debug logs to avoid noise.
|
||||||
|
|||||||
@ -25,18 +25,27 @@ import yazl from 'yazl';
|
|||||||
import { assert, calculateSha1 } from '../utils/utils';
|
import { assert, calculateSha1 } from '../utils/utils';
|
||||||
import { ManualPromise } from '../utils/async';
|
import { ManualPromise } from '../utils/async';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import { ClientInstrumentationListener } from './clientInstrumentation';
|
||||||
|
import { ParsedStackTrace } from '../utils/stackTrace';
|
||||||
|
|
||||||
export class Tracing implements api.Tracing {
|
export class Tracing implements api.Tracing {
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _sources: Set<string> | undefined;
|
private _sources = new Set<string>();
|
||||||
|
private _instrumentationListener: ClientInstrumentationListener;
|
||||||
|
|
||||||
constructor(channel: BrowserContext) {
|
constructor(channel: BrowserContext) {
|
||||||
this._context = channel;
|
this._context = channel;
|
||||||
|
this._instrumentationListener = {
|
||||||
|
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null) => {
|
||||||
|
for (const frame of stackTrace?.frames || [])
|
||||||
|
this._sources.add(frame.file);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(options: { name?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) {
|
async start(options: { name?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean } = {}) {
|
||||||
this._sources = options.sources ? new Set() : undefined;
|
if (options.sources)
|
||||||
this._context._connection.setSourceCollector(this._sources);
|
this._context._instrumentation!.addListener(this._instrumentationListener);
|
||||||
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
await channel.tracingStart(options);
|
await channel.tracingStart(options);
|
||||||
await channel.tracingStartChunk();
|
await channel.tracingStartChunk();
|
||||||
@ -44,7 +53,7 @@ export class Tracing implements api.Tracing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async startChunk() {
|
async startChunk() {
|
||||||
this._context._connection.setSourceCollector(this._sources);
|
this._sources = new Set();
|
||||||
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
await this._context._wrapApiCall(async (channel: channels.BrowserContextChannel) => {
|
||||||
await channel.tracingStartChunk();
|
await channel.tracingStartChunk();
|
||||||
});
|
});
|
||||||
@ -65,7 +74,8 @@ export class Tracing implements api.Tracing {
|
|||||||
|
|
||||||
private async _doStopChunk(channel: channels.BrowserContextChannel, filePath: string | undefined) {
|
private async _doStopChunk(channel: channels.BrowserContextChannel, filePath: string | undefined) {
|
||||||
const sources = this._sources;
|
const sources = this._sources;
|
||||||
this._context._connection.setSourceCollector(undefined);
|
this._sources = new Set();
|
||||||
|
this._context._instrumentation!.removeListener(this._instrumentationListener);
|
||||||
const skipCompress = !this._context._connection.isRemote();
|
const skipCompress = !this._context._connection.isRemote();
|
||||||
const result = await channel.tracingStopChunk({ save: !!filePath, skipCompress });
|
const result = await channel.tracingStopChunk({ save: !!filePath, skipCompress });
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
|
|||||||
@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import * as channels from '../protocol/channels';
|
import * as channels from '../protocol/channels';
|
||||||
import type { Size } from '../common/types';
|
import type { Size } from '../common/types';
|
||||||
import { ParsedStackTrace } from '../utils/stackTrace';
|
|
||||||
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
|
export { Size, Point, Rect, Quad, URLMatch, TimeoutOptions, HeadersArray } from '../common/types';
|
||||||
|
|
||||||
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
type LoggerSeverity = 'verbose' | 'info' | 'warning' | 'error';
|
||||||
@ -26,11 +25,6 @@ export interface Logger {
|
|||||||
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
|
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientSideInstrumentation {
|
|
||||||
onApiCallBegin(apiCall: string, stackTrace: ParsedStackTrace | null): { userObject: any };
|
|
||||||
onApiCallEnd(userData: { userObject: any }, error?: Error): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StrictOptions = { strict?: boolean };
|
export type StrictOptions = { strict?: boolean };
|
||||||
export type Headers = { [key: string]: string };
|
export type Headers = { [key: string]: string };
|
||||||
export type Env = { [key: string]: string | number | boolean | undefined };
|
export type Env = { [key: string]: string | number | boolean | undefined };
|
||||||
|
|||||||
@ -297,8 +297,8 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||||||
(context.tracing as any)[kTracingStarted] = false;
|
(context.tracing as any)[kTracingStarted] = false;
|
||||||
await context.tracing.stop();
|
await context.tracing.stop();
|
||||||
}
|
}
|
||||||
(context as any)._csi = {
|
(context as any)._instrumentation.addListener({
|
||||||
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null) => {
|
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, userData: any) => {
|
||||||
if (apiCall.startsWith('expect.'))
|
if (apiCall.startsWith('expect.'))
|
||||||
return { userObject: null };
|
return { userObject: null };
|
||||||
const testInfoImpl = testInfo as any;
|
const testInfoImpl = testInfo as any;
|
||||||
@ -309,13 +309,13 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||||||
canHaveChildren: false,
|
canHaveChildren: false,
|
||||||
forceNoParent: false
|
forceNoParent: false
|
||||||
});
|
});
|
||||||
return { userObject: step };
|
userData.userObject = step;
|
||||||
},
|
},
|
||||||
onApiCallEnd: (data: { userObject: any }, error?: Error) => {
|
onApiCallEnd: (userData: any, error?: Error) => {
|
||||||
const step = data.userObject;
|
const step = userData.userObject;
|
||||||
step?.complete(error);
|
step?.complete(error);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onWillCloseContext = async (context: BrowserContext) => {
|
const onWillCloseContext = async (context: BrowserContext) => {
|
||||||
@ -374,7 +374,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerAndFileFixtures>({
|
|||||||
(_browserType as any)._onDidCreateContext = undefined;
|
(_browserType as any)._onDidCreateContext = undefined;
|
||||||
(_browserType as any)._onWillCloseContext = undefined;
|
(_browserType as any)._onWillCloseContext = undefined;
|
||||||
(_browserType as any)._defaultContextOptions = undefined;
|
(_browserType as any)._defaultContextOptions = undefined;
|
||||||
leftoverContexts.forEach(context => (context as any)._csi = undefined);
|
leftoverContexts.forEach(context => (context as any)._instrumentation.removeAllListeners());
|
||||||
|
|
||||||
// 5. Collect artifacts from any non-closed contexts.
|
// 5. Collect artifacts from any non-closed contexts.
|
||||||
await Promise.all(leftoverContexts.map(async context => {
|
await Promise.all(leftoverContexts.map(async context => {
|
||||||
|
|||||||
@ -147,8 +147,8 @@ export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTest
|
|||||||
context.on('close', () => contexts.get(context).closed = true);
|
context.on('close', () => contexts.get(context).closed = true);
|
||||||
if (trace)
|
if (trace)
|
||||||
await context.tracing.start({ screenshots: true, snapshots: true, sources: true } as any);
|
await context.tracing.start({ screenshots: true, snapshots: true, sources: true } as any);
|
||||||
(context as any)._csi = {
|
(context as any)._instrumentation.addListener({
|
||||||
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null) => {
|
onApiCallBegin: (apiCall: string, stackTrace: ParsedStackTrace | null, userData: any) => {
|
||||||
if (apiCall.startsWith('expect.'))
|
if (apiCall.startsWith('expect.'))
|
||||||
return { userObject: null };
|
return { userObject: null };
|
||||||
const testInfoImpl = testInfo as any;
|
const testInfoImpl = testInfo as any;
|
||||||
@ -159,13 +159,13 @@ export const playwrightFixtures: Fixtures<PlaywrightTestOptions & PlaywrightTest
|
|||||||
canHaveChildren: false,
|
canHaveChildren: false,
|
||||||
forceNoParent: false
|
forceNoParent: false
|
||||||
});
|
});
|
||||||
return { userObject: step };
|
userData.userObject = step;
|
||||||
},
|
},
|
||||||
onApiCallEnd: (data: { userObject: any }, error?: Error) => {
|
onApiCallEnd: (userData: any, error?: Error) => {
|
||||||
const step = data.userObject;
|
const step = userData.userObject;
|
||||||
step?.complete(error);
|
step?.complete(error);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
return context;
|
return context;
|
||||||
});
|
});
|
||||||
await Promise.all([...contexts.keys()].map(async context => {
|
await Promise.all([...contexts.keys()].map(async context => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user