feat: async stacks for all "async" public methods (#3262)
This patch traces all public async methods and wraps them in a helper method that tags the sync stack trace. Later on, if the method call throws an exception, we add a captured stack trace to the original stack trace with the "--ASYNC--" heading. An example of a stack trace: ``` Error: net::ERR_ABORTED at http://localhost:8907/empty.html at navigate (/Users/lushnikov/prog/puppeteer/lib/Page.js:622:37) at process._tickCallback (internal/process/next_tick.js:68:7) -- ASYNC -- at Page.<anonymous> (/Users/lushnikov/prog/puppeteer/lib/helper.js:147:27) at fit (/Users/lushnikov/prog/puppeteer/test/page.spec.js:546:18) at process._tickCallback (internal/process/next_tick.js:68:7) ```
This commit is contained in:
parent
9223bca964
commit
0b9d8a6271
@ -19,6 +19,39 @@ const debugError = require('debug')(`puppeteer:error`);
|
||||
/** @type {?Map<string, boolean>} */
|
||||
let apiCoverage = null;
|
||||
|
||||
/**
|
||||
* @param {!Object} classType
|
||||
* @param {string=} publicName
|
||||
*/
|
||||
function traceAPICoverage(classType, publicName) {
|
||||
if (!apiCoverage)
|
||||
return;
|
||||
|
||||
let className = publicName || classType.prototype.constructor.name;
|
||||
className = className.substring(0, 1).toLowerCase() + className.substring(1);
|
||||
for (const methodName of Reflect.ownKeys(classType.prototype)) {
|
||||
const method = Reflect.get(classType.prototype, methodName);
|
||||
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
|
||||
continue;
|
||||
apiCoverage.set(`${className}.${methodName}`, false);
|
||||
Reflect.set(classType.prototype, methodName, function(...args) {
|
||||
apiCoverage.set(`${className}.${methodName}`, true);
|
||||
return method.call(this, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
if (classType.Events) {
|
||||
for (const event of Object.values(classType.Events))
|
||||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
|
||||
const method = Reflect.get(classType.prototype, 'emit');
|
||||
Reflect.set(classType.prototype, 'emit', function(event, ...args) {
|
||||
if (this.listenerCount(event))
|
||||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
|
||||
return method.call(this, event, ...args);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Helper {
|
||||
/**
|
||||
* @param {Function|string} fun
|
||||
@ -103,32 +136,23 @@ class Helper {
|
||||
* @param {string=} publicName
|
||||
*/
|
||||
static tracePublicAPI(classType, publicName) {
|
||||
if (!apiCoverage)
|
||||
return;
|
||||
|
||||
let className = publicName || classType.prototype.constructor.name;
|
||||
className = className.substring(0, 1).toLowerCase() + className.substring(1);
|
||||
for (const methodName of Reflect.ownKeys(classType.prototype)) {
|
||||
const method = Reflect.get(classType.prototype, methodName);
|
||||
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function')
|
||||
if (methodName === 'constructor' || typeof methodName !== 'string' || methodName.startsWith('_') || typeof method !== 'function' || method.constructor.name !== 'AsyncFunction')
|
||||
continue;
|
||||
apiCoverage.set(`${className}.${methodName}`, false);
|
||||
Reflect.set(classType.prototype, methodName, function(...args) {
|
||||
apiCoverage.set(`${className}.${methodName}`, true);
|
||||
return method.call(this, ...args);
|
||||
const syncStack = new Error();
|
||||
return method.call(this, ...args).catch(e => {
|
||||
const stack = syncStack.stack.substring(syncStack.stack.indexOf('\n') + 1);
|
||||
const clientStack = stack.substring(stack.indexOf('\n'));
|
||||
if (!e.stack.includes(clientStack))
|
||||
e.stack += '\n -- ASYNC --\n' + stack;
|
||||
throw e;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (classType.Events) {
|
||||
for (const event of Object.values(classType.Events))
|
||||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
|
||||
const method = Reflect.get(classType.prototype, 'emit');
|
||||
Reflect.set(classType.prototype, 'emit', function(event, ...args) {
|
||||
if (this.listenerCount(event))
|
||||
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
|
||||
return method.call(this, event, ...args);
|
||||
});
|
||||
}
|
||||
traceAPICoverage(classType, publicName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,6 +65,25 @@ module.exports.addTests = function({testRunner, expect, headless}) {
|
||||
});
|
||||
});
|
||||
|
||||
let asyncawait = true;
|
||||
try {
|
||||
new Function('async function foo() {await 1}');
|
||||
} catch (e) {
|
||||
asyncawait = false;
|
||||
}
|
||||
(asyncawait ? describe : xdescribe)('Async stacks', () => {
|
||||
it('should work', async({page, server}) => {
|
||||
server.setRoute('/empty.html', (req, res) => {
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
});
|
||||
let error = null;
|
||||
await page.goto(server.EMPTY_PAGE).catch(e => error = e);
|
||||
expect(error).not.toBe(null);
|
||||
expect(error.message).toContain('net::ERR_ABORTED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.Events.error', function() {
|
||||
it('should throw when page crashes', async({page}) => {
|
||||
let error = null;
|
||||
|
Loading…
Reference in New Issue
Block a user