Skip to content

Commit

Permalink
feat: async stacks for all "async" public methods (#3262)
Browse files Browse the repository at this point in the history
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)
```
  • Loading branch information
aslushnikov committed Sep 19, 2018
1 parent 9223bca commit 0b9d8a6
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 19 deletions.
62 changes: 43 additions & 19 deletions lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down
19 changes: 19 additions & 0 deletions test/page.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 0b9d8a6

Please sign in to comment.