feat(rpc): log api calls into LoggerSink (#2904)

This commit is contained in:
Dmitry Gozman 2020-07-10 18:00:10 -07:00 committed by GitHub
parent c63b706aac
commit 6674458496
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 127 additions and 83 deletions

View file

@ -21,6 +21,7 @@ import { Page } from './page';
import { ChannelOwner } from './channelOwner';
import { Events } from '../../events';
import { CDPSession } from './cdpSession';
import { LoggerSink } from '../../loggerSink';
export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
readonly _contexts = new Set<BrowserContext>();
@ -36,8 +37,8 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
return browser ? Browser.from(browser) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: BrowserInitializer) {
super(parent, guid, initializer, true);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: BrowserInitializer) {
super(parent, type, guid, initializer, true);
this._channel.on('close', () => {
this._isConnected = false;
this.emit(Events.Browser.Disconnected);
@ -47,10 +48,12 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
this._closedPromise = new Promise(f => this.once(Events.Browser.Disconnected, f));
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
delete (options as any).logger;
async newContext(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<BrowserContext> {
const logger = options.logger;
options = { ...options, logger: undefined };
const context = BrowserContext.from(await this._channel.newContext(options));
this._contexts.add(context);
context._logger = logger || this._logger;
context._browser = this;
return context;
}
@ -59,8 +62,7 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
return [...this._contexts];
}
async newPage(options: types.BrowserContextOptions = {}): Promise<Page> {
delete (options as any).logger;
async newPage(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<Page> {
const context = await this.newContext(options);
const page = await context.newPage();
page._ownedContext = context;

View file

@ -50,8 +50,8 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
return context ? BrowserContext.from(context) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: BrowserContextInitializer) {
super(parent, guid, initializer, true);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: BrowserContextInitializer) {
super(parent, type, guid, initializer, true);
initializer.pages.forEach(p => {
const page = Page.from(p);
this._pages.add(page);

View file

@ -24,8 +24,8 @@ export class BrowserServer extends ChannelOwner<BrowserServerChannel, BrowserSer
return (server as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: BrowserServerInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: BrowserServerInitializer) {
super(parent, type, guid, initializer);
this._channel.on('close', () => this.emit(Events.BrowserServer.Close));
}

View file

@ -20,15 +20,16 @@ import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { BrowserServer } from './browserServer';
import { LoggerSink } from '../../loggerSink';
export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeInitializer> {
static from(browserTyep: BrowserTypeChannel): BrowserType {
return (browserTyep as any)._object;
static from(browserType: BrowserTypeChannel): BrowserType {
return (browserType as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: BrowserTypeInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: BrowserTypeInitializer) {
super(parent, type, guid, initializer);
}
executablePath(): string {
@ -39,23 +40,32 @@ export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeIni
return this._initializer.name;
}
async launch(options: types.LaunchOptions = {}): Promise<Browser> {
delete (options as any).logger;
return Browser.from(await this._channel.launch(options));
async launch(options: types.LaunchOptions & { logger?: LoggerSink } = {}): Promise<Browser> {
const logger = options.logger;
options = { ...options, logger: undefined };
const browser = Browser.from(await this._channel.launch(options));
browser._logger = logger;
return browser;
}
async launchServer(options: types.LaunchServerOptions = {}): Promise<BrowserServer> {
delete (options as any).logger;
async launchServer(options: types.LaunchServerOptions & { logger?: LoggerSink } = {}): Promise<BrowserServer> {
options = { ...options, logger: undefined };
return BrowserServer.from(await this._channel.launchServer(options));
}
async launchPersistentContext(userDataDir: string, options: types.LaunchOptions & types.BrowserContextOptions = {}): Promise<BrowserContext> {
delete (options as any).logger;
return BrowserContext.from(await this._channel.launchPersistentContext({ userDataDir, ...options }));
async launchPersistentContext(userDataDir: string, options: types.LaunchOptions & types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<BrowserContext> {
const logger = options.logger;
options = { ...options, logger: undefined };
const context = BrowserContext.from(await this._channel.launchPersistentContext({ userDataDir, ...options }));
context._logger = logger;
return context;
}
async connect(options: types.ConnectOptions): Promise<Browser> {
delete (options as any).logger;
return Browser.from(await this._channel.connect(options));
async connect(options: types.ConnectOptions & { logger?: LoggerSink }): Promise<Browser> {
const logger = options.logger;
options = { ...options, logger: undefined };
const browser = Browser.from(await this._channel.connect(options));
browser._logger = logger;
return browser;
}
}

View file

@ -29,8 +29,8 @@ export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitia
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
constructor(parent: ChannelOwner, guid: string, initializer: CDPSessionInitializer) {
super(parent, guid, initializer, true);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: CDPSessionInitializer) {
super(parent, type, guid, initializer, true);
this._channel.on('event', ({ method, params }) => this.emit(method, params));
this._channel.on('disconnected', () => this._dispose());

View file

@ -18,6 +18,7 @@ import { EventEmitter } from 'events';
import { Channel } from '../channels';
import { Connection } from './connection';
import { assert } from '../../helper';
import { LoggerSink } from '../../loggerSink';
export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}> extends EventEmitter {
private _connection: Connection;
@ -27,21 +28,25 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
// Only "isScope" channel owners have registered objects inside.
private _objects = new Map<string, ChannelOwner>();
readonly _type: string;
readonly _guid: string;
readonly _channel: T;
readonly _initializer: Initializer;
_logger: LoggerSink | undefined;
constructor(parent: ChannelOwner | Connection, guid: string, initializer: Initializer, isScope?: boolean) {
constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: Initializer, isScope?: boolean) {
super();
this._connection = parent instanceof Connection ? parent : parent._connection;
this._type = type;
this._guid = guid;
this._isScope = !!isScope;
this._parent = parent instanceof Connection ? undefined : parent;
this._connection._objects.set(guid, this);
if (this._parent)
if (this._parent) {
this._parent._objects.set(guid, this);
this._logger = this._parent._logger;
}
const base = new EventEmitter();
this._channel = new Proxy(base, {
@ -60,7 +65,23 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
return obj.addListener;
if (prop === 'removeEventListener')
return obj.removeListener;
return (params: any) => this._connection.sendMessageToServer({ guid, method: String(prop), params });
return async (params: any) => {
const method = String(prop);
const apiName = this._type + '.' + method;
if (this._logger && this._logger.isEnabled('api', 'info'))
this._logger.log('api', 'info', `=> ${apiName} started`, [], { color: 'cyan' });
try {
const result = await this._connection.sendMessageToServer({ guid, method: String(prop), params });
if (this._logger && this._logger.isEnabled('api', 'info'))
this._logger.log('api', 'info', `=> ${apiName} succeeded`, [], { color: 'cyan' });
return result;
} catch (e) {
if (this._logger && this._logger.isEnabled('api', 'info'))
this._logger.log('api', 'info', `=> ${apiName} failed`, [], { color: 'cyan' });
throw e;
}
};
},
});
(this._channel as any)._object = this;

View file

@ -36,7 +36,7 @@ import { Channel } from '../channels';
class Root extends ChannelOwner<Channel, {}> {
constructor(connection: Connection) {
super(connection, '', {}, true);
super(connection, '', '', {}, true);
}
}
@ -131,59 +131,58 @@ export class Connection {
initializer = this._replaceGuidsWithChannels(initializer);
switch (type) {
case 'bindingCall':
result = new BindingCall(parent, guid, initializer);
result = new BindingCall(parent, type, guid, initializer);
break;
case 'browser':
result = new Browser(parent, guid, initializer);
result = new Browser(parent, type, guid, initializer);
break;
case 'browserServer':
result = new BrowserServer(parent, guid, initializer);
result = new BrowserServer(parent, type, guid, initializer);
break;
case 'browserType':
result = new BrowserType(parent, guid, initializer);
result = new BrowserType(parent, type, guid, initializer);
break;
case 'cdpSession':
// Chromium-specific.
result = new CDPSession(parent, guid, initializer);
result = new CDPSession(parent, type, guid, initializer);
break;
case 'context':
result = new BrowserContext(parent, guid, initializer);
result = new BrowserContext(parent, type, guid, initializer);
break;
case 'consoleMessage':
result = new ConsoleMessage(parent, guid, initializer);
result = new ConsoleMessage(parent, type, guid, initializer);
break;
case 'dialog':
result = new Dialog(parent, guid, initializer);
result = new Dialog(parent, type, guid, initializer);
break;
case 'download':
result = new Download(parent, guid, initializer);
result = new Download(parent, type, guid, initializer);
break;
case 'elementHandle':
result = new ElementHandle(parent, guid, initializer);
result = new ElementHandle(parent, type, guid, initializer);
break;
case 'frame':
result = new Frame(parent, guid, initializer);
result = new Frame(parent, type, guid, initializer);
break;
case 'jsHandle':
result = new JSHandle(parent, guid, initializer);
result = new JSHandle(parent, type, guid, initializer);
break;
case 'page':
result = new Page(parent, guid, initializer);
result = new Page(parent, type, guid, initializer);
break;
case 'playwright':
result = new Playwright(parent, guid, initializer);
result = new Playwright(parent, type, guid, initializer);
break;
case 'request':
result = new Request(parent, guid, initializer);
result = new Request(parent, type, guid, initializer);
break;
case 'response':
result = new Response(parent, guid, initializer);
result = new Response(parent, type, guid, initializer);
break;
case 'route':
result = new Route(parent, guid, initializer);
result = new Route(parent, type, guid, initializer);
break;
case 'worker':
result = new Worker(parent, guid, initializer);
result = new Worker(parent, type, guid, initializer);
break;
default:
throw new Error('Missing type ' + type);

View file

@ -25,8 +25,8 @@ export class ConsoleMessage extends ChannelOwner<ConsoleMessageChannel, ConsoleM
return (message as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: ConsoleMessageInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: ConsoleMessageInitializer) {
super(parent, type, guid, initializer);
}
type(): string {

View file

@ -22,8 +22,8 @@ export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {
return (dialog as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: DialogInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: DialogInitializer) {
super(parent, type, guid, initializer);
}
type(): string {

View file

@ -24,8 +24,8 @@ export class Download extends ChannelOwner<DownloadChannel, DownloadInitializer>
return (download as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: DownloadInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: DownloadInitializer) {
super(parent, type, guid, initializer);
}
url(): string {

View file

@ -33,8 +33,8 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
return handle ? ElementHandle.from(handle) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: JSHandleInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: JSHandleInitializer) {
super(parent, type, guid, initializer);
this._elementChannel = this._channel as ElementHandleChannel;
}

View file

@ -48,8 +48,8 @@ export class Frame extends ChannelOwner<FrameChannel, FrameInitializer> {
return frame ? Frame.from(frame) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: FrameInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: FrameInitializer) {
super(parent, type, guid, initializer);
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
if (this._parentFrame)
this._parentFrame._childFrames.add(this);

View file

@ -46,8 +46,8 @@ export class JSHandle<T = any> extends ChannelOwner<JSHandleChannel, JSHandleIni
return handle ? JSHandle.from(handle) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: JSHandleInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: JSHandleInitializer) {
super(parent, type, guid, initializer);
this._preview = this._initializer.preview;
this._channel.on('previewUpdated', preview => this._preview = preview);
}

View file

@ -57,8 +57,8 @@ export class Request extends ChannelOwner<RequestChannel, RequestInitializer> {
return request ? Request.from(request) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: RequestInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: RequestInitializer) {
super(parent, type, guid, initializer);
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
if (this._redirectedFrom)
this._redirectedFrom._redirectedTo = this;
@ -137,8 +137,8 @@ export class Route extends ChannelOwner<RouteChannel, RouteInitializer> {
return (route as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: RouteInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: RouteInitializer) {
super(parent, type, guid, initializer);
}
request(): Request {
@ -170,8 +170,8 @@ export class Response extends ChannelOwner<ResponseChannel, ResponseInitializer>
return response ? Response.from(response) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: ResponseInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: ResponseInitializer) {
super(parent, type, guid, initializer);
}
url(): string {

View file

@ -68,8 +68,8 @@ export class Page extends ChannelOwner<PageChannel, PageInitializer> {
return page ? Page.from(page) : null;
}
constructor(parent: ChannelOwner, guid: string, initializer: PageInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: PageInitializer) {
super(parent, type, guid, initializer);
this.accessibility = new Accessibility(this._channel);
this.keyboard = new Keyboard(this._channel);
this.mouse = new Mouse(this._channel);
@ -522,8 +522,8 @@ export class BindingCall extends ChannelOwner<BindingCallChannel, BindingCallIni
return (channel as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: BindingCallInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: BindingCallInitializer) {
super(parent, type, guid, initializer);
}
async call(func: FunctionWithSource) {

View file

@ -25,8 +25,8 @@ export class Playwright extends ChannelOwner<PlaywrightChannel, PlaywrightInitia
webkit: BrowserType;
devices: types.Devices;
constructor(parent: ChannelOwner, guid: string, initializer: PlaywrightInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: PlaywrightInitializer) {
super(parent, type, guid, initializer);
this.chromium = BrowserType.from(initializer.chromium);
this.firefox = BrowserType.from(initializer.firefox);
this.webkit = BrowserType.from(initializer.webkit);

View file

@ -30,8 +30,8 @@ export class Worker extends ChannelOwner<WorkerChannel, WorkerInitializer> {
return (worker as any)._object;
}
constructor(parent: ChannelOwner, guid: string, initializer: WorkerInitializer) {
super(parent, guid, initializer);
constructor(parent: ChannelOwner, type: string, guid: string, initializer: WorkerInitializer) {
super(parent, type, guid, initializer);
this._channel.on('close', () => {
if (this._page)
this._page._workers.delete(this);

View file

@ -18,7 +18,7 @@
const path = require('path');
const fs = require('fs');
const utils = require('./utils');
const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS} = utils.testOptions(browserType);
const {FFOX, CHROMIUM, WEBKIT, WIN, USES_HOOKS} = utils.testOptions(browserType);
describe('Playwright', function() {
describe('browserType.launch', function() {

View file

@ -18,33 +18,45 @@ const fs = require('fs');
const path = require('path');
const {FFOX, CHROMIUM, WEBKIT, CHANNEL} = require('./utils').testOptions(browserType);
describe.skip(CHANNEL)('Logger', function() {
describe('Logger', function() {
it('should log', async({browserType, defaultBrowserOptions}) => {
const log = [];
const browser = await browserType.launch({...defaultBrowserOptions, logger: {
log: (name, severity, message) => log.push({name, severity, message}),
isEnabled: (name, severity) => severity !== 'verbose'
}});
await browser.newContext();
await browser.close();
expect(log.length > 0).toBeTruthy();
expect(log.filter(item => item.name.includes('browser')).length > 0).toBeTruthy();
expect(log.filter(item => item.severity === 'info').length > 0).toBeTruthy();
expect(log.filter(item => item.message.includes('<launching>')).length > 0).toBeTruthy();
if (CHANNEL) {
expect(log.filter(item => item.message.includes('browser.newContext started')).length > 0).toBeTruthy();
expect(log.filter(item => item.message.includes('browser.newContext succeeded')).length > 0).toBeTruthy();
} else {
expect(log.filter(item => item.message.includes('browserType.launch started')).length > 0).toBeTruthy();
expect(log.filter(item => item.message.includes('browserType.launch succeeded')).length > 0).toBeTruthy();
}
});
it('should log context-level', async({browserType, defaultBrowserOptions}) => {
const log = [];
const browser = await browserType.launch(defaultBrowserOptions);
const page = await browser.newPage({
const context = await browser.newContext({
logger: {
log: (name, severity, message) => log.push({name, severity, message}),
isEnabled: (name, severity) => severity !== 'verbose'
}
});
const page = await context.newPage();
await page.setContent('<button>Button</button>');
await page.click('button');
await browser.close();
expect(log.length > 0).toBeTruthy();
expect(log.filter(item => item.message.includes('waiting for element')).length > 0).toBeTruthy();
if (CHANNEL) {
expect(log.filter(item => item.message.includes('context.newPage')).length > 0).toBeTruthy();
expect(log.filter(item => item.message.includes('frame.click')).length > 0).toBeTruthy();
} else {
expect(log.filter(item => item.message.includes('page.click')).length > 0).toBeTruthy();
}
});
});