Skip to content

Commit 056f0e2

Browse files
authored
feat(rpc): ensure that error stack traces point to the user code (#2961)
This also adds more "_wrapApiCall" calls for correct logs and stack traces.
1 parent b890569 commit 056f0e2

20 files changed

+201
-111
lines changed

src/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ const colorMap = new Map<string, number>([
140140
['reset', 0],
141141
]);
142142

143-
class DebugLoggerSink {
143+
export class DebugLoggerSink {
144144
private _debuggers = new Map<string, debug.IDebugger>();
145145

146146
isEnabled(name: string, severity: LoggerSeverity): boolean {

src/rpc/client/browser.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,16 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
5454
async newContext(options: types.BrowserContextOptions & { logger?: LoggerSink } = {}): Promise<BrowserContext> {
5555
const logger = options.logger;
5656
options = { ...options, logger: undefined };
57-
const contextOptions: BrowserContextOptions = {
58-
...options,
59-
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
60-
};
61-
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
62-
this._contexts.add(context);
63-
context._logger = logger || this._logger;
64-
return context;
57+
return this._wrapApiCall('browser.newContext', async () => {
58+
const contextOptions: BrowserContextOptions = {
59+
...options,
60+
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
61+
};
62+
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
63+
this._contexts.add(context);
64+
context._logger = logger || this._logger;
65+
return context;
66+
});
6567
}
6668

6769
contexts(): BrowserContext[] {
@@ -81,10 +83,12 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
8183
}
8284

8385
async close(): Promise<void> {
84-
if (!this._isClosedOrClosing) {
85-
this._isClosedOrClosing = true;
86-
await this._channel.close();
87-
}
88-
await this._closedPromise;
86+
return this._wrapApiCall('browser.close', async () => {
87+
if (!this._isClosedOrClosing) {
88+
this._isClosedOrClosing = true;
89+
await this._channel.close();
90+
}
91+
await this._closedPromise;
92+
});
8993
}
9094
}

src/rpc/client/browserContext.ts

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -98,81 +98,109 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
9898
}
9999

100100
async newPage(): Promise<Page> {
101-
if (this._ownerPage)
102-
throw new Error('Please use browser.newContext()');
103-
return Page.from((await this._channel.newPage()).page);
101+
return this._wrapApiCall('browserContext.newPage', async () => {
102+
if (this._ownerPage)
103+
throw new Error('Please use browser.newContext()');
104+
return Page.from((await this._channel.newPage()).page);
105+
});
104106
}
105107

106108
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
107109
if (!urls)
108110
urls = [];
109111
if (urls && typeof urls === 'string')
110112
urls = [ urls ];
111-
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
113+
return this._wrapApiCall('browserContext.cookies', async () => {
114+
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
115+
});
112116
}
113117

114118
async addCookies(cookies: network.SetNetworkCookieParam[]): Promise<void> {
115-
await this._channel.addCookies({ cookies });
119+
return this._wrapApiCall('browserContext.addCookies', async () => {
120+
await this._channel.addCookies({ cookies });
121+
});
116122
}
117123

118124
async clearCookies(): Promise<void> {
119-
await this._channel.clearCookies();
125+
return this._wrapApiCall('browserContext.clearCookies', async () => {
126+
await this._channel.clearCookies();
127+
});
120128
}
121129

122130
async grantPermissions(permissions: string[], options?: { origin?: string }): Promise<void> {
123-
await this._channel.grantPermissions({ permissions, ...options });
131+
return this._wrapApiCall('browserContext.grantPermissions', async () => {
132+
await this._channel.grantPermissions({ permissions, ...options });
133+
});
124134
}
125135

126136
async clearPermissions(): Promise<void> {
127-
await this._channel.clearPermissions();
137+
return this._wrapApiCall('browserContext.clearPermissions', async () => {
138+
await this._channel.clearPermissions();
139+
});
128140
}
129141

130142
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
131-
await this._channel.setGeolocation({ geolocation });
143+
return this._wrapApiCall('browserContext.setGeolocation', async () => {
144+
await this._channel.setGeolocation({ geolocation });
145+
});
132146
}
133147

134148
async setExtraHTTPHeaders(headers: types.Headers): Promise<void> {
135-
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
149+
return this._wrapApiCall('browserContext.setExtraHTTPHeaders', async () => {
150+
await this._channel.setExtraHTTPHeaders({ headers: headersObjectToArray(headers) });
151+
});
136152
}
137153

138154
async setOffline(offline: boolean): Promise<void> {
139-
await this._channel.setOffline({ offline });
155+
return this._wrapApiCall('browserContext.setOffline', async () => {
156+
await this._channel.setOffline({ offline });
157+
});
140158
}
141159

142160
async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void> {
143-
await this._channel.setHTTPCredentials({ httpCredentials });
161+
return this._wrapApiCall('browserContext.setHTTPCredentials', async () => {
162+
await this._channel.setHTTPCredentials({ httpCredentials });
163+
});
144164
}
145165

146166
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
147-
const source = await helper.evaluationScript(script, arg);
148-
await this._channel.addInitScript({ source });
167+
return this._wrapApiCall('browserContext.addInitScript', async () => {
168+
const source = await helper.evaluationScript(script, arg);
169+
await this._channel.addInitScript({ source });
170+
});
149171
}
150172

151173
async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise<void> {
152-
for (const page of this.pages()) {
153-
if (page._bindings.has(name))
154-
throw new Error(`Function "${name}" has been already registered in one of the pages`);
155-
}
156-
if (this._bindings.has(name))
157-
throw new Error(`Function "${name}" has been already registered`);
158-
this._bindings.set(name, binding);
159-
await this._channel.exposeBinding({ name });
174+
return this._wrapApiCall('browserContext.exposeBinding', async () => {
175+
for (const page of this.pages()) {
176+
if (page._bindings.has(name))
177+
throw new Error(`Function "${name}" has been already registered in one of the pages`);
178+
}
179+
if (this._bindings.has(name))
180+
throw new Error(`Function "${name}" has been already registered`);
181+
this._bindings.set(name, binding);
182+
await this._channel.exposeBinding({ name });
183+
});
160184
}
161185

162186
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
163187
await this.exposeBinding(name, (source, ...args) => playwrightFunction(...args));
164188
}
165189

166190
async route(url: types.URLMatch, handler: network.RouteHandler): Promise<void> {
167-
this._routes.push({ url, handler });
168-
if (this._routes.length === 1)
169-
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
191+
return this._wrapApiCall('browserContext.route', async () => {
192+
this._routes.push({ url, handler });
193+
if (this._routes.length === 1)
194+
await this._channel.setNetworkInterceptionEnabled({ enabled: true });
195+
});
170196
}
171197

172198
async unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void> {
173-
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
174-
if (this._routes.length === 0)
175-
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
199+
return this._wrapApiCall('browserContext.unroute', async () => {
200+
this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler));
201+
if (this._routes.length === 0)
202+
await this._channel.setNetworkInterceptionEnabled({ enabled: false });
203+
});
176204
}
177205

178206
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
@@ -196,10 +224,12 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel, BrowserC
196224
}
197225

198226
async close(): Promise<void> {
199-
if (!this._isClosedOrClosing) {
200-
this._isClosedOrClosing = true;
201-
await this._channel.close();
202-
}
203-
await this._closedPromise;
227+
return this._wrapApiCall('browserContext.close', async () => {
228+
if (!this._isClosedOrClosing) {
229+
this._isClosedOrClosing = true;
230+
await this._channel.close();
231+
}
232+
await this._closedPromise;
233+
});
204234
}
205235
}

src/rpc/client/browserServer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,15 @@ export class BrowserServer extends ChannelOwner<BrowserServerChannel, BrowserSer
3838
}
3939

4040
async kill(): Promise<void> {
41-
await this._channel.kill();
41+
return this._wrapApiCall('browserServer.kill', async () => {
42+
await this._channel.kill();
43+
});
4244
}
4345

4446
async close(): Promise<void> {
45-
await this._channel.close();
47+
return this._wrapApiCall('browserServer.close', async () => {
48+
await this._channel.close();
49+
});
4650
}
4751

4852
_checkLeaks() {}

src/rpc/client/cdpSession.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ export class CDPSession extends ChannelOwner<CDPSessionChannel, CDPSessionInitia
4646
method: T,
4747
params?: Protocol.CommandParameters[T]
4848
): Promise<Protocol.CommandReturnValues[T]> {
49-
const result = await this._channel.send({ method, params });
50-
return result.result as Protocol.CommandReturnValues[T];
49+
return this._wrapApiCall('cdpSession.send', async () => {
50+
const result = await this._channel.send({ method, params });
51+
return result.result as Protocol.CommandReturnValues[T];
52+
});
5153
}
5254

5355
async detach() {
54-
return this._channel.detach();
56+
return this._wrapApiCall('cdpSession.detach', async () => {
57+
return this._channel.detach();
58+
});
5559
}
5660
}

src/rpc/client/channelOwner.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Channel } from '../channels';
1919
import { Connection } from './connection';
2020
import { assert } from '../../helper';
2121
import { LoggerSink } from '../../loggerSink';
22-
import { rewriteErrorMessage } from '../../utils/stackTrace';
22+
import { DebugLoggerSink } from '../../logger';
2323

2424
export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}> extends EventEmitter {
2525
private _connection: Connection;
@@ -99,19 +99,30 @@ export abstract class ChannelOwner<T extends Channel = Channel, Initializer = {}
9999
}
100100

101101
protected async _wrapApiCall<T>(apiName: string, func: () => Promise<T>, logger?: LoggerSink): Promise<T> {
102+
const stackObject: any = {};
103+
Error.captureStackTrace(stackObject);
104+
const stack = stackObject.stack.startsWith('Error') ? stackObject.stack.substring(5) : stackObject.stack;
102105
logger = logger || this._logger;
103106
try {
104-
if (logger && logger.isEnabled('api', 'info'))
105-
logger.log('api', 'info', `=> ${apiName} started`, [], { color: 'cyan' });
107+
logApiCall(logger, `=> ${apiName} started`);
106108
const result = await func();
107-
if (logger && logger.isEnabled('api', 'info'))
108-
logger.log('api', 'info', `=> ${apiName} succeeded`, [], { color: 'cyan' });
109+
logApiCall(logger, `<= ${apiName} succeeded`);
109110
return result;
110111
} catch (e) {
111-
if (logger && logger.isEnabled('api', 'info'))
112-
logger.log('api', 'info', `=> ${apiName} failed`, [], { color: 'cyan' });
113-
rewriteErrorMessage(e, `${apiName}: ` + e.message);
112+
logApiCall(logger, `<= ${apiName} failed`);
113+
// TODO: we could probably save "e.stack" in some log-heavy mode
114+
// because it gives some insights into the server part.
115+
e.message = `${apiName}: ` + e.message;
116+
e.stack = e.message + stack;
114117
throw e;
115118
}
116119
}
117120
}
121+
122+
const debugLogger = new DebugLoggerSink();
123+
function logApiCall(logger: LoggerSink | undefined, message: string) {
124+
if (logger && logger.isEnabled('api', 'info'))
125+
logger.log('api', 'info', message, [], { color: 'cyan' });
126+
if (debugLogger.isEnabled('api', 'info'))
127+
debugLogger.log('api', 'info', message, [], { color: 'cyan' });
128+
}

src/rpc/client/chromiumBrowser.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,20 @@ import { Browser } from './browser';
2020

2121
export class ChromiumBrowser extends Browser {
2222
async newBrowserCDPSession(): Promise<CDPSession> {
23-
return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session);
23+
return this._wrapApiCall('chromiumBrowser.newBrowserCDPSession', async () => {
24+
return CDPSession.from((await this._channel.crNewBrowserCDPSession()).session);
25+
});
2426
}
2527

2628
async startTracing(page?: Page, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
27-
await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined });
29+
return this._wrapApiCall('chromiumBrowser.startTracing', async () => {
30+
await this._channel.crStartTracing({ ...options, page: page ? page._channel : undefined });
31+
});
2832
}
2933

3034
async stopTracing(): Promise<Buffer> {
31-
return Buffer.from((await this._channel.crStopTracing()).binary, 'base64');
35+
return this._wrapApiCall('chromiumBrowser.stopTracing', async () => {
36+
return Buffer.from((await this._channel.crStopTracing()).binary, 'base64');
37+
});
3238
}
3339
}

src/rpc/client/chromiumBrowserContext.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export class ChromiumBrowserContext extends BrowserContext {
5151
}
5252

5353
async newCDPSession(page: Page): Promise<CDPSession> {
54-
const result = await this._channel.crNewCDPSession({ page: page._channel });
55-
return CDPSession.from(result.session);
54+
return this._wrapApiCall('chromiumBrowserContext.newCDPSession', async () => {
55+
const result = await this._channel.crNewCDPSession({ page: page._channel });
56+
return CDPSession.from(result.session);
57+
});
5658
}
5759
}

src/rpc/client/dialog.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {
3939
}
4040

4141
async accept(promptText: string | undefined) {
42-
await this._channel.accept({ promptText });
42+
return this._wrapApiCall('dialog.accept', async () => {
43+
await this._channel.accept({ promptText });
44+
});
4345
}
4446

4547
async dismiss() {
46-
await this._channel.dismiss();
48+
return this._wrapApiCall('dialog.dismiss', async () => {
49+
await this._channel.dismiss();
50+
});
4751
}
4852
}

src/rpc/client/electron.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { TimeoutSettings } from '../../timeoutSettings';
2525
import { Waiter } from './waiter';
2626
import { TimeoutError } from '../../errors';
2727
import { Events } from '../../events';
28+
import { LoggerSink } from '../../loggerSink';
2829

2930
export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer> {
3031
static from(electron: ElectronChannel): Electron {
@@ -35,10 +36,12 @@ export class Electron extends ChannelOwner<ElectronChannel, ElectronInitializer>
3536
super(parent, type, guid, initializer, true);
3637
}
3738

38-
async launch(executablePath: string, options: ElectronLaunchOptions = {}): Promise<ElectronApplication> {
39-
options = { ...options };
40-
delete (options as any).logger;
41-
return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication);
39+
async launch(executablePath: string, options: ElectronLaunchOptions & { logger?: LoggerSink } = {}): Promise<ElectronApplication> {
40+
const logger = options.logger;
41+
options = { ...options, logger: undefined };
42+
return this._wrapApiCall('electron.launch', async () => {
43+
return ElectronApplication.from((await this._channel.launch({ executablePath, ...options })).electronApplication);
44+
}, logger);
4245
}
4346
}
4447

0 commit comments

Comments
 (0)