Showing posts with label EDD. Show all posts
Showing posts with label EDD. Show all posts

Saturday, April 13, 2013

Report Unhandled Errors from JavaScript

Logging and aggregating error reports is one of the most important things you can do when building software: 80% of customer issues can be solved by fixing 20% of the top-reported bugs.

Almost all websites at least has some form of error logging on their servers, but what about the client side of those websites? People have a tendency to brush over best practices for client side web development because "it's just some scripts." That, is, WRONG! Your JavaScript is your client application, it is how users experience your website, and as such it needs the proper attention and maintenance as any other rich desktop application.

So then, how do you actually know when your users are experiencing errors in their browser? If you are like the vast majority of websites out there...

You don't know about JavaScript errors, and it's time to fix that!

window.onerror

Browsers do offer a way to get notified of all unhandled exceptions, that is the window.onerror event handler. You can wire a listener up to this global event handler and get back three parameters: the error message, the URL of the file in which the script broke, and the line number where the exception was thrown.

window.onerror = function myErrorHandler(errorMsg, url, lineNumber) {
  // TODO: Something with this exception!
  // Just let default handler run.
  return false;
}

StackTrace.js

JavaScript can throw exceptions like any other language; browser debugging tools often show you a full stack trace for unhandled exceptions, but gathering that information programmatically is a bit more tricky. To learn a bit more about the JavaScript language and how to gather this information yourself, I suggest taking a look at this article by Helen Emerson. However in practice I would strongly suggest you use a more robust tool...

StackTrace.js is a very powerful library that will build a fully detailed stack trace from an exception object. It has a simple API, cross browser support, it handles fringe cases, and is very light weight and unobtrusive to your other JS libraries.

try {
    // error producing code
} catch(error) {
   // Returns stacktrace from error!
   var stackTrace = printStackTrace({e: error});
}

Two Big Problems

  1. The window.onerror callback does not contain the actual error object.

This is a big problem because without the error object you cannot rebuild the stack trace. The error message is always useful, but file names and line numbers will be completely useless once you have minified your code in production. Currently, the only way you can bring additional information up to the onerror callback is to try catch any exceptions that you can and store the error object in a closure or global variable.

  1. If you globally try catch event handlers it will be harder to use a debugger.

It would not be ideal to wrap every single piece of code that you write in an individual try catch block, and if you try to wrap your generic event handling methods in try catches then those catch blocks will interrupt your debugger when you are working with code in development.

Currently my suggestion is to go with the latter option, but only deploy those interceptors with your minified or production code.

jQuery Solution

This global error handling implementation for jQuery and ASP.NET MVC is only 91 lines of JavaScript and 62 lines of C#.

Download JavaScriptErrorReporter from GitHub

To get as much information as possible, you need to wire up to three things:
(Again, I suggest that you only include this when your code is minified!)

  1. window.onerror
  2. $.fn.ready
  3. $.event.dispatch

Here is the meat of those wireups:

var lastStackTrace,
    reportUrl = null,
    prevOnError = window.onerror,
    prevReady = $.fn.ready,
    prevDispatch = $.event.dispatch;
 
// Send global methods with our wrappers.
window.onerror = onError;
$.fn.ready = readyHook;
$.event.dispatch = dispatchHook;
 
function onError(error, url, line) {
    var result = false;
    try {
        // If there was a previous onError handler, fire it.
        if (typeof prevOnError == 'function') {
            result = prevOnError(error, url, line);
        }
        // If the report URL is not loaded, load it.
        if (reportUrl === null) {
            reportUrl = $(document.body).attr('data-report-url') || false;
        }
        // If there is a rport URL, send the stack trace there.
        if (reportUrl !== false) {
            var stackTrace = getStackTrace(error, url, line, lastStackTrace);
            report(error, stackTrace);
        }
    } catch (e) {
        // Something went wrong, log it.
        if (console && console.log) {
            console.log(e);
        }
    } finally {
        // Clear the wrapped stack so it does get reused.
        lastStackTrace = null;
    }
    return result;
}
 
function readyHook(fn) {
    // Call the original ready method, but with our wrapped interceptor.
    return prevReady.call(this, fnHook);
 
    function fnHook() {
        try {
            fn.apply(this, arguments);
        } catch (e) {
            lastStackTrace = printStackTrace({ e: e });
            throw e;
        }
    }
}
 
function dispatchHook() {
    // Call the original dispatch method.
    try {
        prevDispatch.apply(this, arguments);
    } catch (e) {
        lastStackTrace = printStackTrace({ e: e });
        throw e;
    }
}

Identifying Duplicate Errors

One last thing to mention is that when your stack trace arrives on the server it will contain file names and line numbers. The inconsistency of these numbers will make it difficult to identify duplicate errors. I suggest that you "clean" the stack traces by removing this extra information when trying to create a unique error hash.

private static readonly Regex LineCleaner
    = new Regex(@"\([^\)]+\)$", RegexOptions.Compiled);
 
private int GetUniqueHash(string[] stackTrace)
{
    var sb = new StringBuilder();
 
    foreach (var stackLine in stackTrace)
    {
        var cleanLine = LineCleaner
            .Replace(stackLine, String.Empty)
            .Trim();
 
        if (!String.IsNullOrWhiteSpace(cleanLine))
            sb.AppendLine(cleanLine);
    }
 
    return sb
        .ToString()
        .ToLowerInvariant()
        .GetHashCode();
}

Integration Steps

This article was meant to be more informational than tutorial; but if you are interested in trying to apply this to your site, here are the steps that you would need to take:

  1. Download JavaScriptErrorReporter from GitHub.
  2. Include StackTrace.js as a resource in your website.
  3. Include ErrorReporter.js as a resource in your website.
    • Again, to prevent it interfering with your JavaScript debugger, I suggest only including this resource when your scripts are being minified.
  4. Add a report error action to an appropriate controller. (Use the ReportError action on the HomeController as an example.)
  5. Add a "data-report-url" attribute with the fully qualified path to your report error action to the body tag of your pages.
  6. Log any errors that your site reports!
Shout it

Enjoy,
Tom

Wednesday, May 19, 2010

Exception Driven Development and CodeSmith Insight

f customer facing issues are your top priority, then you are already adhere to the primary principle of Exception Driven Development.

Exception Driven Development

It's a pretty simple concept: Collect application data, especially regarding errors and crashes, and then focus on fixing those problems that your users are actually experiencing.

Following good development practices such as Test Driven Development, Automated Deployment, and rigorous QA, help us develop more reliable software. While these efforts can cost additional time and money, it is almost universally agreed that the return on investment (happy users working with a solid product) is worth every penny. So given all the things we do to prevent bugs from getting released, when a bug does finally make it into production, not fixing it right away would be negligent, irresponsible, and it would undermine all the work put into preventing bugs in the first place.

A great example of the ROI (return on investment) from using EDD is the Windows Error Reporting service. Microsoft has been recording errors from both their products and others for years now, and after analyzing the data they have come to some compelling conclusions:

"80 percent of customer issues can be solved by fixing 20 percent of the top-reported bugs. Even addressing 1 percent of the top bugs would address 50 percent of the customer issues."

What does all this mean? It means it's a really good idea to log errors, respond to user defect reports, and basically do everything you can to fix bugs that users are experiencing, ASAP!

So, it's good to ship buggy code? No, absolutely not! ...but sooner or later, it is going to happen; and when it does, it is your job to improve your software.

How do we do this? Use some form of exception and error reporting software. You need a central location to aggregate your application's errors and crashes.

While the concepts of EDD are not rocket science, the task can still take a little bit of work to achieve. Having the correct tools readily available makes the job much easier...

1) Error Reporting Services

I am saying that you need something to report errors, and then I'm suggesting that you probably want a preexisting tool to do that. Implementing your own solution to log errors is probably tempting, but it's not often the best idea. A few common flaws include: increasing time to market, a lack of standardization between products and other tools, and the ironic bugs in your bug tracker.

For reference, here are just a few of the better error reporting tools out there:

  • ELMAH
  • Exceptioneer

A major problem with the simplest (and thus most common) implementation of error reporting software, is that often it's just a log...a log that quickly starts to resemble the bottomless pit in the basement of any fantasy dungeon; you know, the one where even the bravest warriors, and savviest developers, fear to tread. (Please note, I am not trying to imply that the tools I mentioned above do this, I'm just saying that logs eventually stack up.)  A good solution to this is to organize captured errors into an issue tracking system...

2) Issue Tracking

Often issue tracker software gets merged with project management software, so without going into it too much I'll just say a few quick things:

  • Different exceptions may be the same bug.
  • Twenty occurrences of the same bug are still just one bug.
  • Two hundred occurrences of one bug does not necessarily make it higher priority than the twenty occurrences bug.

Having a system to help organize and prioritize your bugs is pretty vital to getting them fixed.

So far we have talked about capturing errors and organizing them, but what about bugs that don't crash or throw exceptions?

3) Help Desk

Not all bugs in software cause the application to blow up, nor do they always throw exceptions. Sometimes a human being (not something that we are used to working with in software) actually has to tell you "Hey, X is not working!" This kind of communication can be facilitated by email, feedback services, etc. The bottom line is that you need a way to collect feedback (include defect reports) from users; and then, ideally, you want to sync that information with whatever you are using to track issues/tasks.

Here are a few good help desk systems:

  • FogBugz
  • Smarter Track

So, if you like the concepts behind EDD, but don't like the idea of having to pay-for/integrate-with/learn-how-to-use/login-to/aggregate-data-from three different systems *GASPS FOR AIR*, than I have just the product for you!

CodeSmith Insight

A jack of all aforementioned trades, and a master of feedback aggregation, CodeSmith Insight was designed to unite these types of software systems.

Error Reporting Service

CodeSmith Insight provides a client that integrates with any .NET application. You just drop in one assembly reference, and add a few simple lines into your app.config, and then Insight will be wired up to automatically report all unhandled exceptions. The CodeSmith Insight client also makes it extremely easy to report any other information you want, whether that be creating a new case in the system, or adding additional information to a preexisting case.

Issue Tracker

All cases reported to CodeSmith Insight, whether they be crash reports, user feedback, or emails, are all saved in a central database hosted in the cloud. Insight automatically stacks duplicate errors that it receives, as well as allows you to merge issues together. All cases are completely searchable, and include a slew of information, including a complete history of actions taken on the case.

Help Desk

CodeSmith Insight can send and receive email, and it offers a robust email client built right into it's web UI. After a case is created in the system (whether that be a crash report or an email), you can then respond directly to a user regarding that case, and then go back and fourth from there. You can even send an update to a group of users experiencing the same issue and let them know the issue has been fixed.

I realize that as a CodeSmith employee I probably have a little bit of bias on subject, but I can say with all honesty and sincerity that CodeSmith Insight really is the perfect tool for Exception Driven Development. But you don't have to take my word for it; CodeSmith Insight is currently in beta, and is FREE! Go check it out, and decide for yourself.

Real Time Web Analytics