chore: move before/after action instrumentation into dispatcher (#5416)

This commit is contained in:
Pavel Feldman 2021-02-10 21:55:46 -08:00 committed by GitHub
parent 6e6e36b565
commit 6576bd8b31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 88 additions and 63 deletions

View file

@ -101,7 +101,7 @@ function waitForTimeout(timeout: number): { promise: Promise<void>, dispose: ()
return { promise, dispose }; return { promise, dispose };
} }
const kLoggingNote = `\nNote: use DEBUG=pw:api environment variable and rerun to capture Playwright logs.`; const kLoggingNote = `\nNote: use DEBUG=pw:api environment variable to capture Playwright logs.`;
function formatLogRecording(log: string[]): string { function formatLogRecording(log: string[]): string {
if (!log.length) if (!log.length)

View file

@ -21,8 +21,9 @@ import { createScheme, Validator, ValidationError } from '../protocol/validator'
import { assert, createGuid, debugAssert, isUnderTest, monotonicTime } from '../utils/utils'; import { assert, createGuid, debugAssert, isUnderTest, monotonicTime } from '../utils/utils';
import { tOptional } from '../protocol/validatorPrimitives'; import { tOptional } from '../protocol/validatorPrimitives';
import { kBrowserOrContextClosedError } from '../utils/errors'; import { kBrowserOrContextClosedError } from '../utils/errors';
import { CallMetadata } from '../server/instrumentation'; import { CallMetadata, SdkObject } from '../server/instrumentation';
import { StackFrame } from '../common/types'; import { StackFrame } from '../common/types';
import { rewriteErrorMessage } from '../utils/stackTrace';
export const dispatcherSymbol = Symbol('dispatcher'); export const dispatcherSymbol = Symbol('dispatcher');
@ -175,23 +176,42 @@ export class DispatcherConnection {
this.onmessage({ id, result: this._rootDispatcher._debugScopeState() }); this.onmessage({ id, result: this._rootDispatcher._debugScopeState() });
return; return;
} }
let validParams: any;
let validMetadata: channels.Metadata;
try { try {
const validated = this._validateParams(dispatcher._type, method, params); validParams = this._validateParams(dispatcher._type, method, params);
validMetadata = this._validateMetadata(metadata);
if (typeof (dispatcher as any)[method] !== 'function') if (typeof (dispatcher as any)[method] !== 'function')
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`); throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
const callMetadata: CallMetadata = {
...this._validateMetadata(metadata),
startTime: monotonicTime(),
endTime: 0,
type: dispatcher._type,
method,
params,
log: [],
};
const result = await (dispatcher as any)[method](validated, callMetadata);
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
} catch (e) { } catch (e) {
this.onmessage({ id, error: serializeError(e) }); this.onmessage({ id, error: serializeError(e) });
return;
}
const callMetadata: CallMetadata = {
...validMetadata,
startTime: monotonicTime(),
endTime: 0,
type: dispatcher._type,
method,
params,
log: [],
};
try {
if (dispatcher instanceof SdkObject)
await dispatcher.instrumentation.onBeforeCall(dispatcher, callMetadata);
const result = await (dispatcher as any)[method](validParams, callMetadata);
this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) });
} catch (e) {
// Dispatching error
if (callMetadata.log.length)
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log) + kLoggingNote);
this.onmessage({ id, error: serializeError(e) });
} finally {
if (dispatcher instanceof SdkObject)
await dispatcher.instrumentation.onAfterCall(dispatcher, callMetadata);
} }
} }
@ -211,3 +231,15 @@ export class DispatcherConnection {
return payload; return payload;
} }
} }
const kLoggingNote = `\nNote: use DEBUG=pw:api environment variable to capture Playwright logs.`;
function formatLogRecording(log: string[]): string {
if (!log.length)
return '';
const header = ` logs `;
const headerLength = 60;
const leftLength = (headerLength - header.length) / 2;
const rightLength = headerLength - header.length - leftLength;
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
}

View file

@ -382,7 +382,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (options && options.modifiers) if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
progress.log(` performing ${actionName} action`); progress.log(` performing ${actionName} action`);
await progress.checkpoint('before'); await progress.beforeInputAction();
await action(point); await action(point);
progress.log(` ${actionName} action done`); progress.log(` ${actionName} action done`);
progress.log(' waiting for scheduled navigations to finish'); progress.log(' waiting for scheduled navigations to finish');
@ -457,7 +457,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { return this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
progress.log(' selecting specified option(s)'); progress.log(' selecting specified option(s)');
await progress.checkpoint('before'); await progress.beforeInputAction();
const poll = await this._evaluateHandleInUtility(([injected, node, optionsToSelect]) => { const poll = await this._evaluateHandleInUtility(([injected, node, optionsToSelect]) => {
return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], injected.selectOptions.bind(injected, optionsToSelect)); return injected.waitForElementStatesAndPerformAction(node, ['visible', 'enabled'], injected.selectOptions.bind(injected, optionsToSelect));
}, optionsToSelect); }, optionsToSelect);
@ -489,7 +489,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (filled === 'error:notconnected') if (filled === 'error:notconnected')
return filled; return filled;
progress.log(' element is visible, enabled and editable'); progress.log(' element is visible, enabled and editable');
await progress.checkpoint('before'); await progress.beforeInputAction();
if (filled === 'needsinput') { if (filled === 'needsinput') {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
if (value) if (value)
@ -538,7 +538,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!'); assert(multiple || files.length <= 1, 'Non-multiple file input can only accept single file!');
await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => { await this._page._frameManager.waitForSignalsCreatedBy(progress, options.noWaitAfter, async () => {
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
await progress.checkpoint('before'); await progress.beforeInputAction();
await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, files); await this._page._delegate.setInputFiles(this as any as ElementHandle<HTMLInputElement>, files);
}); });
await this._page._doSlowMo(); await this._page._doSlowMo();
@ -575,7 +575,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (result !== 'done') if (result !== 'done')
return result; return result;
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
await progress.checkpoint('before'); await progress.beforeInputAction();
await this._page.keyboard.type(text, options); await this._page.keyboard.type(text, options);
return 'done'; return 'done';
}, 'input'); }, 'input');
@ -596,7 +596,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (result !== 'done') if (result !== 'done')
return result; return result;
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
await progress.checkpoint('before'); await progress.beforeInputAction();
await this._page.keyboard.press(key, options); await this._page.keyboard.press(key, options);
return 'done'; return 'done';
}, 'input'); }, 'input');

View file

@ -135,7 +135,7 @@ export class FrameManager {
if (source === 'input') { if (source === 'input') {
await this._page._delegate.inputActionEpilogue(); await this._page._delegate.inputActionEpilogue();
if (progress) if (progress)
await progress.checkpoint('after'); await progress.afterInputAction();
} }
await barrier.waitFor(); await barrier.waitFor();
this._signalBarriers.delete(barrier); this._signalBarriers.delete(barrier);

View file

@ -57,18 +57,24 @@ export interface Instrumentation {
onContextCreated(context: BrowserContext): Promise<void>; onContextCreated(context: BrowserContext): Promise<void>;
onContextWillDestroy(context: BrowserContext): Promise<void>; onContextWillDestroy(context: BrowserContext): Promise<void>;
onContextDidDestroy(context: BrowserContext): Promise<void>; onContextDidDestroy(context: BrowserContext): Promise<void>;
onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void; onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onCallLog(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
} }
export interface InstrumentationListener { export interface InstrumentationListener {
onContextCreated?(context: BrowserContext): Promise<void>; onContextCreated?(context: BrowserContext): Promise<void>;
onContextWillDestroy?(context: BrowserContext): Promise<void>; onContextWillDestroy?(context: BrowserContext): Promise<void>;
onContextDidDestroy?(context: BrowserContext): Promise<void>; onContextDidDestroy?(context: BrowserContext): Promise<void>;
onActionCheckpoint?(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>; onBeforeCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void; onBeforeInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onAfterInputAction?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
onCallLog?(logName: string, message: string, sdkObject: SdkObject, metadata: CallMetadata): void;
onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise<void>;
} }
export function multiplexInstrumentation(listeners: InstrumentationListener[]): Instrumentation { export function multiplexInstrumentation(listeners: InstrumentationListener[]): Instrumentation {

View file

@ -16,7 +16,6 @@
import { TimeoutError } from '../utils/errors'; import { TimeoutError } from '../utils/errors';
import { assert, monotonicTime } from '../utils/utils'; import { assert, monotonicTime } from '../utils/utils';
import { rewriteErrorMessage } from '../utils/stackTrace';
import { LogName } from '../utils/debugLogger'; import { LogName } from '../utils/debugLogger';
import { CallMetadata, Instrumentation, SdkObject } from './instrumentation'; import { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
@ -26,7 +25,8 @@ export interface Progress {
isRunning(): boolean; isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void; cleanupWhenAborted(cleanup: () => any): void;
throwIfAborted(): void; throwIfAborted(): void;
checkpoint(name: string): Promise<void>; beforeInputAction(): Promise<void>;
afterInputAction(): Promise<void>;
} }
export class ProgressController { export class ProgressController {
@ -71,7 +71,7 @@ export class ProgressController {
log: message => { log: message => {
if (this._state === 'running') { if (this._state === 'running') {
this.metadata.log.push(message); this.metadata.log.push(message);
this.instrumentation.onLog(this._logName, message, this.sdkObject, this.metadata); this.instrumentation.onCallLog(this._logName, message, this.sdkObject, this.metadata);
} }
}, },
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node. timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
@ -86,8 +86,11 @@ export class ProgressController {
if (this._state === 'aborted') if (this._state === 'aborted')
throw new AbortedError(); throw new AbortedError();
}, },
checkpoint: async (name: string) => { beforeInputAction: async () => {
await this.instrumentation.onActionCheckpoint(name, this.sdkObject, this.metadata); await this.instrumentation.onBeforeInputAction(this.sdkObject, this.metadata);
},
afterInputAction: async () => {
await this.instrumentation.onAfterInputAction(this.sdkObject, this.metadata);
}, },
}; };
@ -96,23 +99,15 @@ export class ProgressController {
try { try {
const promise = task(progress); const promise = task(progress);
const result = await Promise.race([promise, this._forceAbortPromise]); const result = await Promise.race([promise, this._forceAbortPromise]);
clearTimeout(timer);
this._state = 'finished'; this._state = 'finished';
this.metadata.endTime = monotonicTime();
await this.instrumentation.onAfterAction(this.sdkObject, this.metadata);
return result; return result;
} catch (e) { } catch (e) {
clearTimeout(timer);
this._state = 'aborted'; this._state = 'aborted';
await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup))); await Promise.all(this._cleanups.splice(0).map(cleanup => runCleanup(cleanup)));
this.metadata.endTime = monotonicTime();
this.metadata.error = e;
await this.instrumentation.onAfterAction(this.sdkObject, this.metadata);
rewriteErrorMessage(e,
e.message +
formatLogRecording(this.metadata.log) +
kLoggingNote);
throw e; throw e;
} finally {
clearTimeout(timer);
this.metadata.endTime = monotonicTime();
} }
} }
@ -128,16 +123,4 @@ async function runCleanup(cleanup: () => any) {
} }
} }
const kLoggingNote = `\nNote: use DEBUG=pw:api environment variable and rerun to capture Playwright logs.`;
function formatLogRecording(log: string[]): string {
if (!log.length)
return '';
const header = ` logs `;
const headerLength = 60;
const leftLength = (headerLength - header.length) / 2;
const rightLength = headerLength - header.length - leftLength;
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
}
class AbortedError extends Error {} class AbortedError extends Error {}

View file

@ -30,7 +30,7 @@ export class InspectorController implements InstrumentationListener {
} }
} }
onLog(logName: string, message: string): void { onCallLog(logName: string, message: string): void {
debugLogger.log(logName as any, message); debugLogger.log(logName as any, message);
} }
} }

View file

@ -55,19 +55,23 @@ export class Tracer implements InstrumentationListener {
} }
} }
async onActionCheckpoint(name: string, sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint(name, sdkObject, metadata); this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint('before', sdkObject, metadata);
} }
async onAfterAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onAfterInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterAction(sdkObject, metadata); this._contextTracers.get(sdkObject.attribution.context!)?.onActionCheckpoint('after', sdkObject, metadata);
}
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
this._contextTracers.get(sdkObject.attribution.context!)?.onAfterCall(sdkObject, metadata);
} }
} }
const pageIdSymbol = Symbol('pageId'); const pageIdSymbol = Symbol('pageId');
const snapshotsSymbol = Symbol('snapshots'); const snapshotsSymbol = Symbol('snapshots');
// TODO: this is a hacky way to pass snapshots between onActionCheckpoint and onAfterAction. // This is an official way to pass snapshots between onBefore/AfterInputAction and onAfterCall.
function snapshotsForMetadata(metadata: CallMetadata): { name: string, snapshotId: string }[] { function snapshotsForMetadata(metadata: CallMetadata): { name: string, snapshotId: string }[] {
if (!(metadata as any)[snapshotsSymbol]) if (!(metadata as any)[snapshotsSymbol])
(metadata as any)[snapshotsSymbol] = []; (metadata as any)[snapshotsSymbol] = [];
@ -160,7 +164,7 @@ class ContextTracer implements SnapshotterDelegate {
await this._snapshotter.forceSnapshot(sdkObject.attribution.page, snapshotId); await this._snapshotter.forceSnapshot(sdkObject.attribution.page, snapshotId);
} }
async onAfterAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.page) if (!sdkObject.attribution.page)
return; return;
const event: trace.ActionTraceEvent = { const event: trace.ActionTraceEvent = {

View file

@ -288,7 +288,7 @@ it('should press Enter', async ({page, server}) => {
it('should throw on unknown keys', async ({page, server}) => { it('should throw on unknown keys', async ({page, server}) => {
let error = await page.keyboard.press('NotARealKey').catch(e => e); let error = await page.keyboard.press('NotARealKey').catch(e => e);
expect(error.message).toBe('Unknown key: "NotARealKey"'); expect(error.message).toContain('Unknown key: "NotARealKey"');
error = await page.keyboard.press('ё').catch(e => e); error = await page.keyboard.press('ё').catch(e => e);
expect(error && error.message).toBe('Unknown key: "ё"'); expect(error && error.message).toBe('Unknown key: "ё"');