chore: remove some usage of client from Page (#163)

This brings us closer to reusing Page between browsers.
This commit is contained in:
Dmitry Gozman 2019-12-06 13:36:47 -08:00 committed by GitHub
parent 349ce22565
commit 14f078308d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 143 deletions

View File

@ -29,6 +29,10 @@ import { LifecycleWatcher } from './LifecycleWatcher';
import { NetworkManager } from './NetworkManager';
import { Page } from './Page';
import { Protocol } from './protocol';
import { Events } from './events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './protocolHelper';
import * as dialog from '../dialog';
import * as console from '../console';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
@ -64,15 +68,24 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
this._networkManager = new NetworkManager(client, ignoreHTTPSErrors, this);
this._timeoutSettings = timeoutSettings;
this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
this._client.on('Page.domContentEventFired', event => page.emit(Events.Page.DOMContentLoaded));
this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event));
this._client.on('Page.frameAttached', event => this._onFrameAttached(event.frameId, event.parentFrameId));
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
this._client.on('Page.frameDetached', event => this._onFrameDetached(event.frameId));
this._client.on('Page.frameNavigated', event => this._onFrameNavigated(event.frame));
this._client.on('Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId));
this._client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
this._client.on('Page.loadEventFired', event => page.emit(Events.Page.Load));
this._client.on('Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url));
this._client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
this._client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
this._client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
this._client.on('Runtime.executionContextCreated', event => this._onExecutionContextCreated(event.context));
this._client.on('Runtime.executionContextDestroyed', event => this._onExecutionContextDestroyed(event.executionContextId));
this._client.on('Runtime.executionContextsCleared', event => this._onExecutionContextsCleared());
this._client.on('Page.lifecycleEvent', event => this._onLifecycleEvent(event));
}
async initialize() {
@ -82,6 +95,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
]);
this._handleFrameTree(frameTree);
await Promise.all([
this._client.send('Log.enable', {}),
this._client.send('Page.setInterceptFileChooserDialog', {enabled: true}),
this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
this._networkManager.initialize(),
@ -357,6 +372,72 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate {
this._frames.delete(this._frameData(frame).id);
this.emit(FrameManagerEvents.FrameDetached, frame);
}
async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) {
if (event.executionContextId === 0) {
// DevTools protocol stores the last 1000 console messages. These
// messages are always reported even for removed execution contexts. In
// this case, they are marked with executionContextId = 0 and are
// reported upon enabling Runtime agent.
//
// Ignore these messages since:
// - there's no execution context we can use to operate with message
// arguments
// - these messages are reported before Playwright clients can subscribe
// to the 'console'
// page event.
//
// @see https://github.com/GoogleChrome/puppeteer/issues/3865
return;
}
const context = this.executionContextById(event.executionContextId);
const values = event.args.map(arg => context._createHandle(arg));
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
}
async _exposeBinding(name: string, bindingFunction: string) {
await this._client.send('Runtime.addBinding', {name: name});
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction});
await Promise.all(this.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError)));
}
_onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
const context = this.executionContextById(event.executionContextId);
this._page._onBindingCalled(event.payload, context);
}
_onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) {
this._page.emit(Events.Page.Dialog, new dialog.Dialog(
event.type as dialog.DialogType,
event.message,
async (accept: boolean, promptText?: string) => {
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
},
event.defaultPrompt));
}
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
this._page.emit(Events.Page.PageError, exceptionToError(exceptionDetails));
}
_onTargetCrashed() {
this._page.emit('error', new Error('Page crashed!'));
}
_onLogEntryAdded(event: Protocol.Log.entryAddedPayload) {
const {level, text, args, source, url, lineNumber} = event.entry;
if (args)
args.map(arg => releaseObject(this._client, arg));
if (source !== 'worker')
this._page.emit(Events.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber}));
}
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
const frame = this.frame(event.frameId);
const utilityWorld = await frame._utilityDOMWorld();
const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld);
this._page._onFileChooserOpened(handle);
}
}
function assertNoLegacyNavigationOptions(options) {

View File

@ -17,7 +17,6 @@
import { EventEmitter } from 'events';
import * as console from '../console';
import * as dialog from '../dialog';
import * as dom from '../dom';
import * as frames from '../frames';
import { assert, debugError, helper } from '../helper';
@ -30,7 +29,7 @@ import { TimeoutSettings } from '../TimeoutSettings';
import * as types from '../types';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import { CDPSession, CDPSessionEvents } from './Connection';
import { CDPSession } from './Connection';
import { EmulationManager } from './EmulationManager';
import { Events } from './events';
import { Accessibility } from './features/accessibility';
@ -41,20 +40,20 @@ import { PDF } from './features/pdf';
import { Workers } from './features/workers';
import { FrameManager, FrameManagerEvents } from './FrameManager';
import { RawKeyboardImpl, RawMouseImpl } from './Input';
import { DOMWorldDelegate } from './JSHandle';
import { NetworkManagerEvents } from './NetworkManager';
import { Protocol } from './protocol';
import { getExceptionMessage, releaseObject } from './protocolHelper';
import { CRScreenshotDelegate } from './Screenshotter';
export class Page extends EventEmitter {
private _closed = false;
private _closedCallback: () => void;
private _closedPromise: Promise<void>;
private _disconnected = false;
private _disconnectedCallback: (e: Error) => void;
private _disconnectedPromise: Promise<Error>;
_client: CDPSession;
private _browserContext: BrowserContext;
private _keyboard: input.Keyboard;
private _mouse: input.Mouse;
readonly keyboard: input.Keyboard;
readonly mouse: input.Mouse;
private _timeoutSettings: TimeoutSettings;
private _frameManager: FrameManager;
private _emulationManager: EmulationManager;
@ -69,12 +68,11 @@ export class Page extends EventEmitter {
private _viewport: types.Viewport | null = null;
_screenshotter: Screenshotter;
private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>();
private _disconnectPromise: Promise<Error> | undefined;
private _emulatedMediaType: string | undefined;
static async create(client: CDPSession, browserContext: BrowserContext, ignoreHTTPSErrors: boolean, defaultViewport: types.Viewport | null): Promise<Page> {
const page = new Page(client, browserContext, ignoreHTTPSErrors);
await page._initialize();
await page._frameManager.initialize();
if (defaultViewport)
await page.setViewport(defaultViewport);
return page;
@ -84,30 +82,21 @@ export class Page extends EventEmitter {
super();
this._client = client;
this._closedPromise = new Promise(f => this._closedCallback = f);
this._disconnectedPromise = new Promise(f => this._disconnectedCallback = f);
this._browserContext = browserContext;
this._keyboard = new input.Keyboard(new RawKeyboardImpl(client));
this._mouse = new input.Mouse(new RawMouseImpl(client), this._keyboard);
this.keyboard = new input.Keyboard(new RawKeyboardImpl(client));
this.mouse = new input.Mouse(new RawMouseImpl(client), this.keyboard);
this._timeoutSettings = new TimeoutSettings();
this.accessibility = new Accessibility(client);
this._frameManager = new FrameManager(client, this, ignoreHTTPSErrors, this._timeoutSettings);
this._emulationManager = new EmulationManager(client);
this.coverage = new Coverage(client);
this.pdf = new PDF(client);
this.workers = new Workers(client, this._addConsoleMessage.bind(this), this._handleException.bind(this));
this.workers = new Workers(client, this._addConsoleMessage.bind(this), error => this.emit(Events.Page.PageError, error));
this.overrides = new Overrides(client);
this.interception = new Interception(this._frameManager.networkManager());
this._screenshotter = new Screenshotter(this, new CRScreenshotDelegate(this._client), browserContext.browser());
client.on('Target.attachedToTarget', event => {
if (event.targetInfo.type !== 'worker') {
// If we don't detach from service workers, they will never die.
client.send('Target.detachFromTarget', {
sessionId: event.sessionId
}).catch(debugError);
return;
}
});
this._frameManager.on(FrameManagerEvents.FrameAttached, event => this.emit(Events.Page.FrameAttached, event));
this._frameManager.on(FrameManagerEvents.FrameDetached, event => this.emit(Events.Page.FrameDetached, event));
this._frameManager.on(FrameManagerEvents.FrameNavigated, event => this.emit(Events.Page.FrameNavigated, event));
@ -117,16 +106,6 @@ export class Page extends EventEmitter {
networkManager.on(NetworkManagerEvents.Response, event => this.emit(Events.Page.Response, event));
networkManager.on(NetworkManagerEvents.RequestFailed, event => this.emit(Events.Page.RequestFailed, event));
networkManager.on(NetworkManagerEvents.RequestFinished, event => this.emit(Events.Page.RequestFinished, event));
client.on('Page.domContentEventFired', event => this.emit(Events.Page.DOMContentLoaded));
client.on('Page.loadEventFired', event => this.emit(Events.Page.Load));
client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event));
}
_didClose() {
@ -136,22 +115,17 @@ export class Page extends EventEmitter {
this._closedCallback();
}
async _initialize() {
await Promise.all([
this._frameManager.initialize(),
this._client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true}),
this._client.send('Performance.enable', {}),
this._client.send('Log.enable', {}),
this._client.send('Page.setInterceptFileChooserDialog', {enabled: true})
]);
_didDisconnect() {
assert(!this._disconnected, 'Page disconnected twice');
this._disconnected = true;
this._disconnectedCallback(new Error('Target closed'));
}
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
if (!this._fileChooserInterceptors.size)
async _onFileChooserOpened(handle: dom.ElementHandle) {
if (!this._fileChooserInterceptors.size) {
await handle.dispose();
return;
const frame = this._frameManager.frame(event.frameId);
const utilityWorld = await frame._utilityDOMWorld();
const handle = await (utilityWorld.delegate as DOMWorldDelegate).adoptBackendNodeId(event.backendNodeId, utilityWorld);
}
const interceptors = Array.from(this._fileChooserInterceptors);
this._fileChooserInterceptors.clear();
const multiple = await handle.evaluate((element: HTMLInputElement) => !!element.multiple);
@ -182,26 +156,10 @@ export class Page extends EventEmitter {
return this._browserContext;
}
_onTargetCrashed() {
this.emit('error', new Error('Page crashed!'));
}
_onLogEntryAdded(event: Protocol.Log.entryAddedPayload) {
const {level, text, args, source, url, lineNumber} = event.entry;
if (args)
args.map(arg => releaseObject(this._client, arg));
if (source !== 'worker')
this.emit(Events.Page.Console, new console.ConsoleMessage(level, text, [], {url, lineNumber}));
}
mainFrame(): frames.Frame {
return this._frameManager.mainFrame();
}
get keyboard(): input.Keyboard {
return this._keyboard;
}
frames(): frames.Frame[] {
return this._frameManager.frames();
}
@ -251,11 +209,7 @@ export class Page extends EventEmitter {
if (this._pageBindings.has(name))
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
this._pageBindings.set(name, playwrightFunction);
const expression = helper.evaluationString(addPageBinding, name);
await this._client.send('Runtime.addBinding', {name: name});
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: expression});
await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
await this._frameManager._exposeBinding(name, helper.evaluationString(addPageBinding, name));
function addPageBinding(bindingName: string) {
const binding = window[bindingName];
@ -283,37 +237,8 @@ export class Page extends EventEmitter {
return this._frameManager.networkManager().setUserAgent(userAgent);
}
_handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails) {
const message = getExceptionMessage(exceptionDetails);
const err = new Error(message);
err.stack = ''; // Don't report clientside error with a node stack attached
this.emit(Events.Page.PageError, err);
}
async _onConsoleAPI(event: Protocol.Runtime.consoleAPICalledPayload) {
if (event.executionContextId === 0) {
// DevTools protocol stores the last 1000 console messages. These
// messages are always reported even for removed execution contexts. In
// this case, they are marked with executionContextId = 0 and are
// reported upon enabling Runtime agent.
//
// Ignore these messages since:
// - there's no execution context we can use to operate with message
// arguments
// - these messages are reported before Playwright clients can subscribe
// to the 'console'
// page event.
//
// @see https://github.com/GoogleChrome/puppeteer/issues/3865
return;
}
const context = this._frameManager.executionContextById(event.executionContextId);
const values = event.args.map(arg => context._createHandle(arg));
this._addConsoleMessage(event.type, values, event.stackTrace);
}
async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
const {name, seq, args} = JSON.parse(event.payload);
async _onBindingCalled(payload: string, context: js.ExecutionContext) {
const {name, seq, args} = JSON.parse(payload);
let expression = null;
try {
const result = await this._pageBindings.get(name)(...args);
@ -324,7 +249,7 @@ export class Page extends EventEmitter {
else
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
}
this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError);
context.evaluate(expression).catch(debugError);
function deliverResult(name: string, seq: number, result: any) {
window[name]['callbacks'].get(seq).resolve(result);
@ -344,43 +269,28 @@ export class Page extends EventEmitter {
}
}
_addConsoleMessage(type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) {
_addConsoleMessage(type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) {
if (!this.listenerCount(Events.Page.Console)) {
args.forEach(arg => arg.dispose());
return;
}
const location = stackTrace && stackTrace.callFrames.length ? {
url: stackTrace.callFrames[0].url,
lineNumber: stackTrace.callFrames[0].lineNumber,
columnNumber: stackTrace.callFrames[0].columnNumber,
} : {};
this.emit(Events.Page.Console, new console.ConsoleMessage(type, undefined, args, location));
}
_onDialog(event : Protocol.Page.javascriptDialogOpeningPayload) {
this.emit(Events.Page.Dialog, new dialog.Dialog(
event.type as dialog.DialogType,
event.message,
async (accept: boolean, promptText?: string) => {
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
},
event.defaultPrompt));
}
url(): string {
return this.mainFrame().url();
}
async content(): Promise<string> {
return await this._frameManager.mainFrame().content();
return await this.mainFrame().content();
}
async setContent(html: string, options: { timeout?: number; waitUntil?: string | string[]; } | undefined) {
await this._frameManager.mainFrame().setContent(html, options);
await this.mainFrame().setContent(html, options);
}
async goto(url: string, options: { referer?: string; timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
return await this._frameManager.mainFrame().goto(url, options);
return await this.mainFrame().goto(url, options);
}
async reload(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
@ -392,39 +302,33 @@ export class Page extends EventEmitter {
}
async waitForNavigation(options: { timeout?: number; waitUntil?: string | string[]; } = {}): Promise<network.Response | null> {
return await this._frameManager.mainFrame().waitForNavigation(options);
}
_sessionClosePromise() {
if (!this._disconnectPromise)
this._disconnectPromise = new Promise(fulfill => this._client.once(CDPSessionEvents.Disconnected, () => fulfill(new Error('Target closed'))));
return this._disconnectPromise;
return await this.mainFrame().waitForNavigation(options);
}
async waitForRequest(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<Request> {
const {
timeout = this._timeoutSettings.timeout(),
} = options;
return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Request, request => {
return helper.waitForEvent(this, Events.Page.Request, (request: network.Request) => {
if (helper.isString(urlOrPredicate))
return (urlOrPredicate === request.url());
if (typeof urlOrPredicate === 'function')
return !!(urlOrPredicate(request));
return false;
}, timeout, this._sessionClosePromise());
}, timeout, this._disconnectedPromise);
}
async waitForResponse(urlOrPredicate: (string | Function), options: { timeout?: number; } = {}): Promise<network.Response> {
const {
timeout = this._timeoutSettings.timeout(),
} = options;
return helper.waitForEvent(this._frameManager.networkManager(), NetworkManagerEvents.Response, response => {
return helper.waitForEvent(this, Events.Page.Response, (response: network.Response) => {
if (helper.isString(urlOrPredicate))
return (urlOrPredicate === response.url());
if (typeof urlOrPredicate === 'function')
return !!(urlOrPredicate(response));
return false;
}, timeout, this._sessionClosePromise());
}, timeout, this._disconnectedPromise);
}
async goBack(options: { timeout?: number; waitUntil?: string | string[]; } | undefined): Promise<network.Response | null> {
@ -488,7 +392,7 @@ export class Page extends EventEmitter {
}
evaluate: types.Evaluate = (pageFunction, ...args) => {
return this._frameManager.mainFrame().evaluate(pageFunction, ...args as any);
return this.mainFrame().evaluate(pageFunction, ...args as any);
}
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
@ -509,7 +413,7 @@ export class Page extends EventEmitter {
}
async close(options: { runBeforeUnload: (boolean | undefined); } = {runBeforeUnload: undefined}) {
assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
assert(!this._disconnected, 'Protocol error: Connection closed. Most likely the page has been closed.');
const runBeforeUnload = !!options.runBeforeUnload;
if (runBeforeUnload) {
await this._client.send('Page.close');
@ -523,10 +427,6 @@ export class Page extends EventEmitter {
return this._closed;
}
get mouse(): input.Mouse {
return this._mouse;
}
click(selector: string | types.Selector, options?: ClickOptions) {
return this.mainFrame().click(selector, options);
}

View File

@ -18,11 +18,12 @@
import * as types from '../types';
import { Browser } from './Browser';
import { BrowserContext } from './BrowserContext';
import { CDPSession } from './Connection';
import { CDPSession, CDPSessionEvents } from './Connection';
import { Events } from './events';
import { Worker } from './features/workers';
import { Page } from './Page';
import { Protocol } from './protocol';
import { debugError } from '../helper';
const targetSymbol = Symbol('target');
@ -83,6 +84,14 @@ export class Target {
this._pagePromise = this._sessionFactory().then(async client => {
const page = await Page.create(client, this._browserContext, this._ignoreHTTPSErrors, this._defaultViewport);
page[targetSymbol] = this;
client.once(CDPSessionEvents.Disconnected, () => page._didDisconnect());
client.on('Target.attachedToTarget', event => {
if (event.targetInfo.type !== 'worker') {
// If we don't detach from service workers, they will never die.
client.send('Target.detachFromTarget', { sessionId: event.sessionId }).catch(debugError);
}
});
await client.send('Target.setAutoAttach', {autoAttach: true, waitForDebuggerOnStart: false, flatten: true});
return page;
});
}

View File

@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import { CDPSession, Connection } from '../Connection';
import { debugError } from '../../helper';
@ -21,10 +22,12 @@ import { Protocol } from '../protocol';
import { Events } from '../events';
import * as types from '../../types';
import * as js from '../../javascript';
import * as console from '../../console';
import { ExecutionContextDelegate } from '../ExecutionContext';
import { toConsoleMessageLocation, exceptionToError } from '../protocolHelper';
type AddToConsoleCallback = (type: string, args: js.JSHandle[], stackTrace: Protocol.Runtime.StackTrace | undefined) => void;
type HandleExceptionCallback = (exceptionDetails: Protocol.Runtime.ExceptionDetails) => void;
type AddToConsoleCallback = (type: string, args: js.JSHandle[], location: console.ConsoleMessageLocation) => void;
type HandleExceptionCallback = (error: Error) => void;
export class Workers extends EventEmitter {
private _workers = new Map<string, Worker>();
@ -74,8 +77,8 @@ export class Worker extends EventEmitter {
// This might fail if the target is closed before we recieve all execution contexts.
this._client.send('Runtime.enable', {}).catch(debugError);
this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), event.stackTrace));
this._client.on('Runtime.exceptionThrown', exception => handleException(exception.exceptionDetails));
this._client.on('Runtime.consoleAPICalled', event => addToConsole(event.type, event.args.map(jsHandleFactory), toConsoleMessageLocation(event.stackTrace)));
this._client.on('Runtime.exceptionThrown', exception => handleException(exceptionToError(exception.exceptionDetails)));
}
url(): string {

View File

@ -94,4 +94,17 @@ export async function readProtocolStream(client: CDPSession, handle: string, pat
}
}
export function toConsoleMessageLocation(stackTrace: Protocol.Runtime.StackTrace | undefined) {
return stackTrace && stackTrace.callFrames.length ? {
url: stackTrace.callFrames[0].url,
lineNumber: stackTrace.callFrames[0].lineNumber,
columnNumber: stackTrace.callFrames[0].columnNumber,
} : {};
}
export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDetails): Error {
const message = getExceptionMessage(exceptionDetails);
const err = new Error(message);
err.stack = ''; // Don't report clientside error with a node stack attached
return err;
}

View File

@ -3,7 +3,7 @@
import * as js from './javascript';
type ConsoleMessageLocation = {
export type ConsoleMessageLocation = {
url?: string,
lineNumber?: number,
columnNumber?: number,