chore: make instrumentation per-context (#6302)

This commit is contained in:
Pavel Feldman 2021-04-23 18:34:52 -07:00 committed by GitHub
parent 10c76ff56f
commit 97cf86d20a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 118 additions and 216 deletions

View File

@ -27,7 +27,11 @@ import { Progress } from './progress';
import { Selectors, serverSelectors } from './selectors';
import * as types from './types';
import path from 'path';
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
import { CallMetadata, internalCallMetadata, createInstrumentation, SdkObject } from './instrumentation';
import { Debugger } from './supplements/debugger';
import { Tracer } from './trace/recorder/tracer';
import { HarTracer } from './supplements/har/harTracer';
import { RecorderSupplement } from './supplements/recorderSupplement';
export abstract class BrowserContext extends SdkObject {
static Events = {
@ -51,6 +55,7 @@ export abstract class BrowserContext extends SdkObject {
readonly _browserContextId: string | undefined;
private _selectors?: Selectors;
private _origins = new Set<string>();
private _harTracer: HarTracer | undefined;
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
super(browser, 'browser-context');
@ -60,6 +65,9 @@ export abstract class BrowserContext extends SdkObject {
this._browserContextId = browserContextId;
this._isPersistentContext = !browserContextId;
this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill);
if (this._options.recordHar)
this._harTracer = new HarTracer(this, this._options.recordHar);
}
_setSelectors(selectors: Selectors) {
@ -71,7 +79,31 @@ export abstract class BrowserContext extends SdkObject {
}
async _initialize() {
await this.instrumentation.onContextCreated(this);
if (this.attribution.isInternal)
return;
// Create instrumentation per context.
this.instrumentation = createInstrumentation();
// Debugger will pause execution upon page.pause in headed mode.
const contextDebugger = new Debugger(this);
this.instrumentation.addListener(contextDebugger);
if (this._options._traceDir)
this.instrumentation.addListener(new Tracer(this, this._options._traceDir));
// When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector')
await RecorderSupplement.show(this, { pauseOnNextStatement: true });
// When paused, show inspector.
if (contextDebugger.isPaused())
RecorderSupplement.showInspector(this);
contextDebugger.on(Debugger.Events.PausedStateChanged, () => {
RecorderSupplement.showInspector(this);
});
await this.instrumentation.onContextCreated();
}
async _ensureVideosPath() {
@ -231,7 +263,7 @@ export abstract class BrowserContext extends SdkObject {
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
await this.instrumentation.onContextWillDestroy(this);
await this._harTracer?.flush();
// Cleanup.
const promises: Promise<void>[] = [];
@ -260,7 +292,7 @@ export abstract class BrowserContext extends SdkObject {
await this._browser.close();
// Bookkeeping.
await this.instrumentation.onContextDidDestroy(this);
await this.instrumentation.onContextDestroyed();
this._didCloseInternal();
}
await this._closePromise;

View File

@ -25,6 +25,7 @@ import type { Frame } from './frames';
import type { Page } from './page';
export type Attribution = {
isInternal: boolean,
browserType?: BrowserType;
browser?: Browser;
context?: BrowserContext;
@ -67,34 +68,32 @@ export class SdkObject extends EventEmitter {
}
export interface Instrumentation {
onContextCreated(context: BrowserContext): Promise<void>;
onContextWillDestroy(context: BrowserContext): Promise<void>;
onContextDidDestroy(context: BrowserContext): Promise<void>;
addListener(listener: InstrumentationListener): void;
onContextCreated(): Promise<void>;
onContextDestroyed(): Promise<void>;
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
}
export interface InstrumentationListener {
onContextCreated?(context: BrowserContext): Promise<void>;
onContextWillDestroy?(context: BrowserContext): Promise<void>;
onContextDidDestroy?(context: BrowserContext): Promise<void>;
onContextCreated?(): Promise<void>;
onContextDestroyed?(): Promise<void>;
onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void>;
onCallLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
}
export function multiplexInstrumentation(listeners: InstrumentationListener[]): Instrumentation {
export function createInstrumentation(): Instrumentation {
const listeners: InstrumentationListener[] = [];
return new Proxy({}, {
get: (obj: any, prop: string) => {
if (prop === 'addListener')
return (listener: InstrumentationListener) => listeners.push(listener);
if (!prop.startsWith('on'))
return obj[prop];
return async (...params: any[]) => {

View File

@ -15,7 +15,6 @@
*/
import path from 'path';
import { Tracer } from './trace/recorder/tracer';
import { Android } from './android/android';
import { AdbBackend } from './android/backendAdb';
import { PlaywrightOptions } from './browser';
@ -23,12 +22,9 @@ import { Chromium } from './chromium/chromium';
import { Electron } from './electron/electron';
import { Firefox } from './firefox/firefox';
import { Selectors, serverSelectors } from './selectors';
import { HarTracer } from './supplements/har/harTracer';
import { InspectorController } from './supplements/inspectorController';
import { WebKit } from './webkit/webkit';
import { Registry } from '../utils/registry';
import { InstrumentationListener, multiplexInstrumentation, SdkObject } from './instrumentation';
import { Debugger } from './supplements/debugger';
import { createInstrumentation, SdkObject } from './instrumentation';
export class Playwright extends SdkObject {
readonly selectors: Selectors;
@ -40,15 +36,7 @@ export class Playwright extends SdkObject {
readonly options: PlaywrightOptions;
constructor(isInternal: boolean) {
const listeners: InstrumentationListener[] = [];
if (!isInternal) {
listeners.push(new Debugger());
listeners.push(new Tracer());
listeners.push(new HarTracer());
listeners.push(new InspectorController());
}
const instrumentation = multiplexInstrumentation(listeners);
super({ attribution: {}, instrumentation } as any, undefined, 'Playwright');
super({ attribution: { isInternal }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
this.options = {
registry: new Registry(path.join(__dirname, '..', '..')),
rootSdkObject: this,

View File

@ -19,54 +19,38 @@ import { debugMode, isUnderTest, monotonicTime } from '../../utils/utils';
import { BrowserContext } from '../browserContext';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { debugLogger } from '../../utils/debugLogger';
export class Debugger implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> {
ContextDebugger.getOrCreate(context);
if (debugMode() === 'console')
await context.extendInjectedScript(consoleApiSource.source);
}
const symbol = Symbol('Debugger');
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata);
}
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
await ContextDebugger.lookup(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata);
}
}
const symbol = Symbol('ContextDebugger');
export class ContextDebugger extends EventEmitter {
export class Debugger extends EventEmitter implements InstrumentationListener {
private _pauseOnNextStatement = false;
private _pausedCallsMetadata = new Map<CallMetadata, { resolve: () => void, sdkObject: SdkObject }>();
private _enabled: boolean;
private _context: BrowserContext;
static Events = {
PausedStateChanged: 'pausedstatechanged'
};
static getOrCreate(context: BrowserContext): ContextDebugger {
let contextDebugger = (context as any)[symbol] as ContextDebugger;
if (!contextDebugger) {
contextDebugger = new ContextDebugger();
(context as any)[symbol] = contextDebugger;
}
return contextDebugger;
}
constructor() {
constructor(context: BrowserContext) {
super();
this._context = context;
(this._context as any)[symbol] = this;
this._enabled = debugMode() === 'inspector';
if (this._enabled)
this.pauseOnNextStatement();
}
static lookup(context?: BrowserContext): ContextDebugger | undefined {
static lookup(context?: BrowserContext): Debugger | undefined {
if (!context)
return;
return (context as any)[symbol] as ContextDebugger | undefined;
return (context as any)[symbol] as Debugger | undefined;
}
async onContextCreated() {
if (debugMode() === 'console')
await this._context.extendInjectedScript(consoleApiSource.source);
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
@ -79,13 +63,17 @@ export class ContextDebugger extends EventEmitter {
await this.pause(sdkObject, metadata);
}
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
debugLogger.log(logName as any, message);
}
async pause(sdkObject: SdkObject, metadata: CallMetadata) {
this._enabled = true;
metadata.pauseStartTime = monotonicTime();
const result = new Promise<void>(resolve => {
this._pausedCallsMetadata.set(metadata, { resolve, sdkObject });
});
this.emit(ContextDebugger.Events.PausedStateChanged);
this.emit(Debugger.Events.PausedStateChanged);
return result;
}
@ -97,7 +85,7 @@ export class ContextDebugger extends EventEmitter {
resolve();
}
this._pausedCallsMetadata.clear();
this.emit(ContextDebugger.Events.PausedStateChanged);
this.emit(Debugger.Events.PausedStateChanged);
}
pauseOnNextStatement() {

View File

@ -20,36 +20,16 @@ import { BrowserContext } from '../../browserContext';
import { helper } from '../../helper';
import * as network from '../../network';
import { Page } from '../../page';
import { InstrumentationListener } from '../../instrumentation';
import * as har from './har';
const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs));
export class HarTracer implements InstrumentationListener {
private _contextTracers = new Map<BrowserContext, HarContextTracer>();
async onContextCreated(context: BrowserContext): Promise<void> {
if (!context._options.recordHar)
return;
const contextTracer = new HarContextTracer(context, context._options.recordHar);
this._contextTracers.set(context, contextTracer);
}
async onContextWillDestroy(context: BrowserContext): Promise<void> {
const contextTracer = this._contextTracers.get(context);
if (contextTracer) {
this._contextTracers.delete(context);
await contextTracer.flush();
}
}
}
type HarOptions = {
path: string;
omitContent?: boolean;
};
class HarContextTracer {
export class HarTracer {
private _options: HarOptions;
private _log: har.Log;
private _pageEntries = new Map<Page, har.Page>();

View File

@ -1,53 +0,0 @@
/**
* 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 { BrowserContext } from '../browserContext';
import { RecorderSupplement } from './recorderSupplement';
import { debugLogger } from '../../utils/debugLogger';
import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumentation';
import { ContextDebugger } from './debugger';
export class InspectorController implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> {
const contextDebugger = ContextDebugger.lookup(context)!;
if (contextDebugger.isPaused())
RecorderSupplement.show(context, {}).catch(() => {});
contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => {
RecorderSupplement.show(context, {}).catch(() => {});
});
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.onBeforeCall(sdkObject, metadata);
}
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.onAfterCall(sdkObject, metadata);
}
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.onBeforeInputAction(sdkObject, metadata);
}
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
debugLogger.log(logName as any, message);
const recorder = await RecorderSupplement.lookup(sdkObject.attribution.context);
recorder?.updateCallLog([metadata]);
}
}

View File

@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS
import { ActionInContext } from './codeGenerator';
import { actionTitle, Action } from './recorderActions';
import { MouseClickOptions, toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors');
import deviceDescriptors from '../../deviceDescriptors';
export class CSharpLanguageGenerator implements LanguageGenerator {
id = 'csharp';

View File

@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, toSignalMap } from './lang
import { ActionInContext } from './codeGenerator';
import { Action, actionTitle } from './recorderActions';
import { toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors');
import deviceDescriptors from '../../deviceDescriptors';
import { JavaScriptFormatter } from './javascript';
export class JavaLanguageGenerator implements LanguageGenerator {

View File

@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS
import { ActionInContext } from './codeGenerator';
import { Action, actionTitle } from './recorderActions';
import { MouseClickOptions, toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors');
import deviceDescriptors from '../../deviceDescriptors';
export class JavaScriptLanguageGenerator implements LanguageGenerator {
id = 'javascript';

View File

@ -19,7 +19,7 @@ import { LanguageGenerator, LanguageGeneratorOptions, sanitizeDeviceOptions, toS
import { ActionInContext } from './codeGenerator';
import { actionTitle, Action } from './recorderActions';
import { MouseClickOptions, toModifiers } from './utils';
import deviceDescriptors = require('../../deviceDescriptors');
import deviceDescriptors from '../../deviceDescriptors';
export class PythonLanguageGenerator implements LanguageGenerator {
id = 'python';

View File

@ -29,19 +29,19 @@ import { PythonLanguageGenerator } from './recorder/python';
import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { RecorderApp } from './recorder/recorderApp';
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
import { CallMetadata, InstrumentationListener, internalCallMetadata, SdkObject } from '../instrumentation';
import { Point } from '../../common/types';
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
import { isUnderTest } from '../../utils/utils';
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
import { metadataToCallLog } from './recorder/recorderUtils';
import { ContextDebugger } from './debugger';
import { Debugger } from './debugger';
type BindingSource = { frame: Frame, page: Page };
const symbol = Symbol('RecorderSupplement');
export class RecorderSupplement {
export class RecorderSupplement implements InstrumentationListener {
private _generator: CodeGenerator;
private _pageAliases = new Map<Page, string>();
private _lastPopupOrdinal = 0;
@ -59,7 +59,11 @@ export class RecorderSupplement {
private _hoveredSnapshot: { callLogId: string, phase: 'before' | 'after' | 'action' } | undefined;
private _snapshots = new Set<string>();
private _allMetadatas = new Map<string, CallMetadata>();
private _contextDebugger: ContextDebugger;
private _debugger: Debugger;
static showInspector(context: BrowserContext) {
RecorderSupplement.show(context, {}).catch(() => {});
}
static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
@ -71,15 +75,10 @@ export class RecorderSupplement {
return recorderPromise;
}
static lookup(context: BrowserContext | undefined): Promise<RecorderSupplement> | undefined {
if (!context)
return;
return (context as any)[symbol] as Promise<RecorderSupplement> | undefined;
}
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._context = context;
this._contextDebugger = ContextDebugger.getOrCreate(context);
this._debugger = Debugger.lookup(context)!;
context.instrumentation.addListener(this);
this._params = params;
this._mode = params.startRecording ? 'recording' : 'none';
const language = params.language || context._options.sdkLanguage;
@ -152,21 +151,21 @@ export class RecorderSupplement {
}
if (data.event === 'callLogHovered') {
this._hoveredSnapshot = undefined;
if (this._contextDebugger.isPaused() && data.params.callLogId)
if (this._debugger.isPaused() && data.params.callLogId)
this._hoveredSnapshot = data.params;
this._refreshOverlay();
return;
}
if (data.event === 'step') {
this._contextDebugger.resume(true);
this._debugger.resume(true);
return;
}
if (data.event === 'resume') {
this._contextDebugger.resume(false);
this._debugger.resume(false);
return;
}
if (data.event === 'pause') {
this._contextDebugger.pauseOnNextStatement();
this._debugger.pauseOnNextStatement();
return;
}
if (data.event === 'clear') {
@ -177,7 +176,7 @@ export class RecorderSupplement {
await Promise.all([
recorderApp.setMode(this._mode),
recorderApp.setPaused(this._contextDebugger.isPaused()),
recorderApp.setPaused(this._debugger.isPaused()),
this._pushAllSources()
]);
@ -233,27 +232,27 @@ export class RecorderSupplement {
});
await this._context.exposeBinding('_playwrightResume', false, () => {
this._contextDebugger.resume(false);
this._debugger.resume(false);
});
const snapshotBaseUrl = await this._snapshotter.initialize() + '/snapshot/';
await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest(), snapshotBaseUrl });
await this._context.extendInjectedScript(consoleApiSource.source);
if (this._contextDebugger.isPaused())
if (this._debugger.isPaused())
this._pausedStateChanged();
this._contextDebugger.on(ContextDebugger.Events.PausedStateChanged, () => this._pausedStateChanged());
this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged());
(this._context as any).recorderAppForTest = recorderApp;
}
_pausedStateChanged() {
// If we are called upon page.pause, we don't have metadatas, populate them.
for (const { metadata, sdkObject } of this._contextDebugger.pausedDetails()) {
for (const { metadata, sdkObject } of this._debugger.pausedDetails()) {
if (!this._currentCallsMetadata.has(metadata))
this.onBeforeCall(sdkObject, metadata);
}
this._recorderApp!.setPaused(this._contextDebugger.isPaused());
this._recorderApp!.setPaused(this._debugger.isPaused());
this._updateUserSources();
this.updateCallLog([...this._currentCallsMetadata.keys()]);
}
@ -398,7 +397,7 @@ export class RecorderSupplement {
}
}
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
if (this._mode === 'recording')
return;
this._captureSnapshot(sdkObject, metadata, 'before');
@ -412,7 +411,7 @@ export class RecorderSupplement {
}
}
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
if (this._mode === 'recording')
return;
this._captureSnapshot(sdkObject, metadata, 'after');
@ -441,7 +440,7 @@ export class RecorderSupplement {
this._userSources.set(file, source);
}
if (line) {
const paused = this._contextDebugger.isPaused(metadata);
const paused = this._debugger.isPaused(metadata);
source.highlight.push({ line, type: metadata.error ? 'error' : (paused ? 'paused' : 'running') });
source.revealLine = line;
fileToSelect = source.file;
@ -456,12 +455,16 @@ export class RecorderSupplement {
this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]);
}
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) {
if (this._mode === 'recording')
return;
this._captureSnapshot(sdkObject, metadata, 'action');
}
async onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this.updateCallLog([metadata]);
}
updateCallLog(metadatas: CallMetadata[]) {
if (this._mode === 'recording')
return;
@ -472,7 +475,7 @@ export class RecorderSupplement {
let status: CallLogStatus = 'done';
if (this._currentCallsMetadata.has(metadata))
status = 'in-progress';
if (this._contextDebugger.isPaused(metadata))
if (this._debugger.isPaused(metadata))
status = 'paused';
logs.push(metadataToCallLog(metadata, status, this._snapshots));
}

View File

@ -63,7 +63,6 @@ export type ActionTraceEvent = {
type: 'action' | 'event',
contextId: string,
metadata: CallMetadata,
snapshots?: { title: string, snapshotName: string }[],
};
export type DialogOpenedEvent = {

View File

@ -32,53 +32,18 @@ const fsAppendFileAsync = util.promisify(fs.appendFile.bind(fs));
const envTrace = getFromENV('PWTRACE_RESOURCE_DIR');
export class Tracer implements InstrumentationListener {
private _contextTracers = new Map<BrowserContext, ContextTracer>();
async onContextCreated(context: BrowserContext): Promise<void> {
const traceDir = context._options._traceDir;
if (!traceDir)
return;
const resourcesDir = envTrace || path.join(traceDir, 'resources');
const tracePath = path.join(traceDir, context._options._debugName!);
const contextTracer = new ContextTracer(context, resourcesDir, tracePath);
await contextTracer.start();
this._contextTracers.set(context, contextTracer);
}
async onContextDidDestroy(context: BrowserContext): Promise<void> {
const contextTracer = this._contextTracers.get(context);
if (contextTracer) {
await contextTracer.dispose().catch(e => {});
this._contextTracers.delete(context);
}
}
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onBeforeInputAction(sdkObject, metadata, element);
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onBeforeCall(sdkObject, metadata);
}
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterCall(sdkObject, metadata);
}
async onEvent(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onEvent(sdkObject, metadata);
}
}
class ContextTracer {
private _contextId: string;
private _appendEventChain: Promise<string>;
private _snapshotter: PersistentSnapshotter;
private _eventListeners: RegisteredListener[];
private _disposed = false;
private _pendingCalls = new Map<string, { sdkObject: SdkObject, metadata: CallMetadata }>();
private _context: BrowserContext;
constructor(context: BrowserContext, resourcesDir: string, tracePrefix: string) {
constructor(context: BrowserContext, traceDir: string) {
this._context = context;
const resourcesDir = envTrace || path.join(traceDir, 'resources');
const tracePrefix = path.join(traceDir, context._options._debugName!);
const traceFile = tracePrefix + '-actions.trace';
this._contextId = 'context@' + createGuid();
this._appendEventChain = mkdirIfNeeded(traceFile).then(() => traceFile);
@ -99,10 +64,6 @@ class ContextTracer {
];
}
async start() {
await this._snapshotter.start(false);
}
_captureSnapshot(name: 'before' | 'after' | 'action' | 'event', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle) {
if (!sdkObject.attribution.page)
return;
@ -111,16 +72,20 @@ class ContextTracer {
this._snapshotter.captureSnapshot(sdkObject.attribution.page, snapshotName, element);
}
onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
async onContextCreated(): Promise<void> {
await this._snapshotter.start(false);
}
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {
this._captureSnapshot('before', sdkObject, metadata);
this._pendingCalls.set(metadata.id, { sdkObject, metadata });
}
onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata, element: ElementHandle) {
this._captureSnapshot('action', sdkObject, metadata, element);
}
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata) {
this._captureSnapshot('after', sdkObject, metadata);
if (!sdkObject.attribution.page)
return;
@ -239,7 +204,7 @@ class ContextTracer {
});
}
async dispose() {
async onContextDestroyed() {
this._disposed = true;
helper.removeEventListeners(this._eventListeners);
await this._snapshotter.dispose();

View File

@ -30,7 +30,7 @@ export const SnapshotTab: React.FunctionComponent<{
let [snapshotIndex, setSnapshotIndex] = React.useState(0);
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
for (const snapshot of actionEntry?.snapshots || [])
for (const snapshot of actionEntry?.metadata.snapshots || [])
snapshotMap.set(snapshot.title, snapshot);
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('after');
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];

View File

@ -140,7 +140,8 @@ DEPS['src/server/injected/'] = ['src/server/common/'];
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/trace/recorder/tracer.ts', 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/android/', 'src/server/electron/'];
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/tracer.ts'];
DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
// Tracing is a client/server plugin, nothing should depend on it.