Any way to detect an async stack trace (like Chrome devtools does)?
here's a simple throwaway-function you can wrap around promises like fetch
to get the caller's async-stack-trace promiseWithErrorStack
:
<!doctype html>
<html lang="en">
<body>
<h1>test.html</h1>
<script>
(async function foo() {
function promiseWithErrorStack(promise) {
/*
* this function will append current-stack to any err caught from
<promise>
*/
let errStack;
errStack = new Error().stack;
return new Promise(function (resolve, reject) {
promise.then(resolve).catch(function (err) {
// append current errStack to err.stack
if (err && typeof err.stack === "string") {
err.stack += "\n" + errStack;
}
reject(err);
});
});
}
await promiseWithErrorStack(fetch("https://example.com")); // at foo
(test.html:23)
/*
console-output:
Uncaught (in promise) TypeError: Failed to fetch
Error
at promiseWithErrorStack (test.html:12)
at foo (test.html:23) // async-stack-trace
at test.html:32
*/
}());
</script>
</body>
Thanks for the idea! That's similar to what I thought would be necessary. I
was hoping to monkey patch fetch()
to have it get the trace if any
fetch
call, but they would not be async stack traces. For async traces it
would require instrumentation by injecting code similar to yours (I want to
detect traces in third-party code installed locally).
#!/JoePea
(I want to detect traces in third-party code installed locally).
- here's 15-line javascript-hack to trace async-fetch-calls from 3rd-party-cdn-library
<!doctype html>
<html lang="en">
<body>
<h1>test.html</h1>
<script>
(function () {
/*
* 15-line-javascript-hack to trace async-fetch-calls
* enable hack by adding search-query "?modeDebugFetch=1" to web-url
*/
"use strict";
if ((/\bmodeDebugFetch=1\b/).test(location.search)) {
let fetch0;
fetch0 = globalThis.fetch;
globalThis.fetch = function (...argList) {
let errStack = new Error().stack;
console.error("\n\nfetch-call-arguments:");
console.error(JSON.stringify(argList, undefined, 4));
console.error("\n\nfetch-call-trace:");
console.error(errStack);
return fetch0(...argList);
};
}
}());
</script>
<!-- 3rd-party-cdn-library -->
<script src="https://unpkg.com/http-client/umd/http-client.js"></script>
<script>
(async function foo() {
"use strict";
let thirdParty = window.HTTPClient;
let myFetch = thirdParty.createFetch(
thirdParty.base("https://api.stripe.com/v1"),
thirdParty.accept("application/json")
);
let response = await myFetch("/customers/5");
console.log(response.jsonData);
/*
dev-console-output - http://localhost:8081/test.html?modeDebugFetch=1
fetch-call-arguments:
[
"https://api.stripe.com/v1/customers/5",
{
"headers": {
"Accept": "application/json"
},
"responseHandlers": [
null
]
}
]
fetch-call-trace:
Error
at globalThis.fetch (test.html?modeDebugFetch=1:16)
at http-client.js:194
at http-client.js:127
at http-client.js:217
at http-client.js:126
at http-client.js:154
at http-client.js:95
at foo (test.html?modeDebugFetch=1:35)
at test.html?modeDebugFetch=1:64
*/
}());
</script>
</body>
- here's real-world-hack added to npm-cli.js to debug its http-requests
C:\Program Files\nodejs\node_modules\npm>git diff
diff --git a/bin/npm-cli.js b/bin/npm-cli.js
index 561dec0..98cafb8 100644
--- a/bin/npm-cli.js
+++ b/bin/npm-cli.js
@@ -2,17 +2,6 @@
;(function () { // wrapper in case we're in module_context mode
+// hack - debug http-request
+let httpRequest;
+httpRequest = require("https").request.bind(require("https"));
+require("https").request = function (...argList) {
+ if (process.env.NPM_DEBUG) {
+ console.error(
+ "npm - httpRequest - " + JSON.stringify(argList.slice(0, 2),
undefined, 4)
+ );
+ }
+ return httpRequest(...argList);
+};
// windows: running "npm blah" in this folder will invoke WSH, not node.
C:\Program Files\nodejs\node_modules\npm>
$ NPM_DEBUG=1 npm publish foo
# console-ouput:
# npm - httpRequest - [
# {
# "protocol": "https:",
# "href": "https://registry.npmjs.org/foo",
# "method": "GET",
# "headers": {
# "connection": [
# "keep-alive"
# ],
# "user-agent": [
# "npm/6.13.4 node/v12.16.1 win32 x64"
# ],
# ...
Hello Kai! That example is so meta with its own output showing the numbers relative to the code including its own output! :)
That's what I originally wanted to do, but that doesn't give us an async stack trace, it only gives a sync stack trace (I presume within the current event loop task).
For example:
const originalFetch = globalThis.fetch
globalThis.fetch = function(...args) {
const stack = new Error().stack
console.log(stack)
return originalFetch.apply(globalThis, args)
}
const sleep = t => new Promise(r => setTimeout(r, t))
async function one() {
console.log('1')
await sleep(10)
two()
}
async function two() {
console.log('2')
await sleep(10)
three()
}
async function three() {
console.log('3')
await sleep(10)
return await fetch('https://unpkg.com/[email protected]')
}
async function main() {
await one()
}
main()
Output:
1
2
3
Error
at globalThis.fetch (pen.js:5)
at three (pen.js:27)
Live example:
codepen.io/trusktr/pen/b8fd92752b4671268f516ad3804869e4?editors=1010
I opened a request for this over here: tc39/proposal-error-stacks#34
#!/JoePea
Reading through the issue JoePea linked to, it looks like the difficulties with standardized async stack traces are twofold:
- Even with the error stacks proposal, the implementer has total say over what qualifies as a stack frame
- The way promises are chained means that some patterns like async loops would theoretically unroll to very long stack traces, and right now engines aren't required to keep track of those frames; requiring them to do so would place a burden on implementers that could quickly lead to slowdowns and memory issues
What if there was a standard way to mark a Promise as an important stack
frame, which the implementer is free to ignore? Maybe something like
Promise.prototype.trace()
The way promises are chained means that some patterns like async loops would theoretically unroll to very long stack traces
Devtools in fact does that (has a button at the bottom of the stack to keep loading more frames).
I think it would be better for it to be smart and not have duplicate frames in the stack, so a loop would result in only one unique set of entries in the stack trace and not repeat.
#!/JoePea
Is there some way (perhaps by patching methods on objects?) so we can track async call stacks?
When we pause code in devtools, we are able to see the async stack trace of the code.
What I'd like to do is to effectively detect the same thing as devtools does at some point in any code. As a specific example, I'd like to detect the async stack trace of any call to
fetch
.Is such a thing possible with runtime code?
Or would it require instrumentation of the source code?
#!/JoePea