Thursday, April 5, 2012

Tracking JavaScript exceptions with TraceKit

One of the challenges in modern browsers is trapping all the JavaScript exceptions your users might be experiencing. Internet Explorer (IE) is especially unforgiving about sending invalid JavaScript commands. If you issue $('#myid').text() on an input HTML tag causes, IE will raise a "Object doesn't support this property or method". IE also has some security restrictions that prevent you from submitting a change to an input file HTML tag, causing you to see an "Access Denied" error message but not on other browsers such as Chrome and Firefox. You wouldn't know about these issues unless you started finding ways to report these errors.

The first thing you might consider is to use window.onerror() to perform an Ajax server-side to log the message, url, and line number. This approach works, but provides limited information especially for minified JavaScipt code since the line number would always refer to first few lines of the code! Without a character number, you'd be lost to find the location of the offending code. Since the stack trace in JavaScript is lost once the window.onerror() function is called, you have to intercept the exception before it gets to this function.

An open-source alternative is to use TraceKit, which provides a cross-browser mechanism of capturing stack traces. You can then setup to start receiving stack traces similar to the following:
url: https://myhost.com/static/js/tracekit.js
line: 164
context:
}, (stack.incomplete ? 2000 : 0));

throw ex; // re-throw to propagate to the top level (and cause window.onerror)
}
func:

url: https://myhost.com/static/js/my.js
column: 45
line: 1841
context:

if ($("#mydata").length === 0) {
$('#myid').text(title).attr('href', url);
$('#myid2_url').text(url).attr('href', url);
func: myfunction_name


url: https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js
column: 4351
line: 3
context: None
func: HTMLDocument.

(You may notice that rethrow line at the top. For Internet Explorer, the exception needs to be thrown to the top of the browser. For other browsers, you'll have to live with this extraneous information.)

How does TraceKit work? Since only the error message, URL, and line number is reported once window.onerror() is called, the key to this approach is to wrap jQuery document.ready(), $.event.add, and $.ajax calls around a try/except block, collecting the stack traces, finding the code snippet from the line number/column number, and adding a reporting handler that will be called after all the stack traces have been pulled.

You can read through the rest of the code to see how the stack traces are derived for Internet Explorer, Chrome, Safari, and Firefox. In order to derive the code snippet, TraceKit actually performs an XmlHttpRequest() call (in other words, an Ajax request) to retrieve this file from your localhost. As a result, any document that's not located on your server will be printed as "HTMLDocument." in the final stack trace output. The current version on the GitHub repo also doesn't obey this restriction, so there's a pull request that will attempt to retrieve the .JS files only if they are part of the document.domain. There is also pull request to retrieve files with https:// prefixes and deal with jQuery 1.7+, which introduces an additional parameter in the $.event.add() function.

You can then setup a reporting handler to POST this data back to your server:
TraceKit.report.subscribe(function (stackInfo) {                                                                                                                                                           
$.ajax({
url: '/error_reporting/',
type: 'POST',
data: {
browserUrl: window.location.href,
stackInfo: JSON.stringify(stackInfo)
}
});
return true; //suppress error on client
});
The information that gets passed includes a list of stack traces with each element as a dictionary. You can decode this information and output the information with something like the following code:

def process_js_error(request):
stack_info = request.POST.get('stackInfo', '')
logging.debug("stackInfo: %s" % stack_info)
try:
stack_info = json.loads(stack_info)
except ValueError:
content = "Stack Info: %s\nBrowser URL: %s\nUser Agent: %s\n" % (stack_info, request.POST['browserUrl'], request.META['HTTP_USER_AGENT'])
else:
# Prettier formatting stack trace.
if isinstance(stack_info['stack'], list):
stack_trace = "\n"
for stack_item in stack_info['stack']:
for key, val in stack_item.iteritems():
if isinstance(val, list) and val:
stack_trace += "%s: \n%s\n" % (key, "\n".join(val))
else:
stack_trace += "%s: %s\n" % (key, val)
stack_trace += "\n"

else:
stack_trace = stack_info['stack']

content = """
Error Type: %(error_type)s
Browser URL: %(browser_url)s
User Agent: %(user_agent)s
User Name: %(user_name)s

"" % {'error_type': stack_info['message'],
'browser_url': request.POST['browserUrl'],
'user_agent': request.META['HTTP_USER_AGENT'],
'stack_trace': stack_trace}

There are a bunch of pull requests that you may want to consider when using TraceKit, some of which were discussed in this writeup. Remember that there are a couple of limitations mentioned when using TraceKit. First, Chrome/Firefox/Opera for the most part will output full stack frames with column numbers but Internet Explorer will only report the top stack frame and the column numbers are not always guaranteed. Furthermore, code snippets can only be made available if they are locally hosted on your site (i.e. for files hosted on CDN such as those prefixed with https://ajax.googleapis.com/) Hope you find this tool useful for tracking down your JavaScript exceptions as we did!

No comments:

Post a Comment