Whatever evil you can think of, JavaScript stack traces are worse.
After releasing a satisfactory version of CrashKit Python we've turned our attention to AJAX web applications and collection of bug reports from client-side JavaScript code. Unfortunately, whatever solutions we could find on the Web failed to collect enough information and did not suite our requirement of being browser-independent. We really wanted identical exceptions occurring in different browsers to be displayed as a single bug, which means that the topmost line of a stack trace has to be the same.
It turns out there are 3 ways to obtain a stack trace: ex.stack (Firefox-specific), ex.message (Opera-specific) and traversing a chain of arguments.callee.caller.caller.caller. The latter one is extremely limited, and overcoming some of its quirks is both hard and browser-specific.
In Firefox and Opera, the stack trace can be obtained from an exception object. This means we have to get a hold of the exceptions somehow. There is no global event loop or any other central place where a catch-all handler can be installed, so every event handler has to be wrapped in a try-catch block. JavaScript frameworks often make it easier by introducing a common place where event handlers are bound, and in fact we have an experimental jQuery support that wraps all handlers into try-catch blocks automatically.
Other browsers have to use the arguments-based approach, which has several big problems:
1) Because the arguments pseudo-variable corresponds to the current function and is not related to an exception in any way, the catch block has no way to know the function/line the exception has actually occurred at. It can only determine the callers of the function containing the catch block itself, thus the top of the stack trace will be missing.
2) The value of caller attribute is a function, not a stack frame, so the bottom of the stack trace will be missing if some function is called recursively.
3) The only thing we get by traversing the chain of callers is function bodies (the source code). No line numbers or URLs, and function names are only available in Safari, but not in IE.
Fortunately, Safari provides a line number and a URL for the topmost stack trace entry as ex.lineNumber and ex.sourceURL. Thus we can augment the partial stack trace with a correct topmost entry, which is the most important requirement for CrashKit bug grouping to work.
Internet Explorer (6.x–8.x) has no such properties in the exception object. Luckily, Internet Explorer has window.onerror event which is fired when it's too late to collect a stack trace, but does provide a line number and a URL for the topmost stack trace entry. Thus the strategy for handling IE is to collect a partial stack trace and re-throw the exception, hoping that noone would catch it and IE will end up raising onerror, allowing us to augment the stack trace with a correct topmost entry.
We also try to guess some missing information automatically. Given a function body, we use XMLHTTPRequest to download the source code of all included scripts and search for the body textually, which gives us a URL to add to the stack trace. Given a line number within a function, we look for constructs like xxx = function or 'xxx': function to guess a “real name” of an anonymous function.
We also collect a few lines of source code around the position of exception occurrence for the upcoming source code display feature in CrashKit.
The details of the described operations are quirky too. In Opera, some line numbers are relative to the start of a SCRIPT tag, so we have to adjust for that. Automatic semicolon insertion and whitespace differences interfere with the textual function body search, so we have to turn the body into a regular expression. Inline handlers like <button onclick="somecode()"> have to be treated specially for the purposes of stack trace collection (good news is that we can always determine line numbers for such one-line functions).
Another important thing to realize when coding in JavaScript is that Everything Changes. For example, both ex.stack and (of course!) ex.message are strings parsable with a few simple regular expressions, but their (undocumented) format may easily change in the future. New browser versions may be released which support or don't support something that we expect. That's why browser detection is widely recognized as an evil practise, and CrashKit JavaScript does not do any. Instead, it tries to use Firefox- and Opera-specific approaches and falls back to walking the call chain in case of any problems, so it's pretty future-proof.
You can grab the source code on GitHub: crashkit-javascript.js + sample. It defines two reusable functions, CrashKit.report and CrashKit.computeStackTrace, both documented pretty throughly. There has not been much testing yet, so consider this to be of beta quality. The post will be updated as the code matures. (And do check out the CrashKit itself, although it is in a limited beta mode at the moment.)
1 comments:
Unfortunately the version that I found to "do it" is just to place the "try-catch" clauses to EVERY method and then assemble the stack trace from Globally Unique ID-s (GUID-s). Then, I also wrote 2 scripts for the KomodoIDE:
One is "UpGUID", which replaces GUIDs within the selected text region in the KomodoIDE. I write code in NetBeans, because it has a better Vim emulation than the KomodoIDE and the NetBeans has really fine code template system, so I don't have to write all the try-catch-with-a-constant-GUID by myself. However, the KomodoIDE has an easier way to integrate my own weird tools, so I use the 2 in parallel.
The other tool that I wrote as a KomodoIDE "toolbox item", reads in the custom-generated stackt-trace and allows me to move up and down within that stack trace in a way that with every movement, the code region, from where the exception has been thrown, is opened and displayed in the KomodoIDE code editor. As the GUID-s are unique, it's possible to grep the place out and displaying the text region in the IDE was just a matter of scripting.
I found out that that sort of stack-trace navigation by instantly displaying the code region, from where the exception got thrown, really boosted my productivity.
I haven't uploaded the script collections to anywhere, because they're a bit of a mess, but they work and if You want, then I can send them to You. They're a mixture of Ruby, Bash and JavaScript. The KomodoIDE is manipulated by using JavaScript.
I also wrote a little about it in my blog: http://mv-veebilog.blogspot.com/2009/08/about-javascript-line-and-this.html
Post a Comment