Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Logging client side errors at case of AngularJS.

0.00/5 (No votes)
24 Oct 2016 1  
How to log client side errors with rich and useful information about them at case of AngularJS.

Problem

There are a lot of means and approaches to log errors with their quite informative description on server side at case of web development. If we use, for examle, ASP.NET MVC we can inherite all our controllers from base one, where common exception handling logic is performed (OnException method is implied) also we can do that at Global.asax via Application_Error method and so on.

But what we will do on client side? We want to log these errors too, supplying them with information about it's native description (what happened), location of code (filename, line number and column number), stack trace and other stuff. This article is dedicated to answer on this question at case of AngularJS, but presented solution can simply be applied to other client side framework or any other custom development approach via number of simple modifications, core idea remains the same.

Solution

As I already said, we use AngularJS framework and want to log errors, which are happen at our custom client side code, I am talk about controllers, directives, services and so on. First of all, we need to subscribe on all exceptions via $exceptionHandler service, that can be simply overridden. I used materials from this article: https://engineering.talis.com/articles/client-side-error-logging/, as starting point, and I will add some modifications further:

(function () {
    angular.module('app').factory(
    "$exceptionHandler", ["$log", "$window", function ($log, $window) {
        function error(exception, cause) {

            // preserve the default behaviour which will log the error
            // to the console, and allow the application to continue running.            
            $log.error.apply($log, arguments);            

            // now try to log the error to the server side.
            try {
                var errorMessage = exception.toString();

                var stackTrace = printStackTrace({ e: exception });                

                // use AJAX (in this example jQuery) and NOT
                // an angular service such as $http
                $.ajax({
                    type: "POST",
                    url: "/logger",
                    contentType: "application/json",
                    data: angular.toJson({
                        url: $window.location.href,
                        message: errorMessage,
                        type: "exception",
                        stackTrace: stackTrace,
                        cause: cause
                    })                
                });                                             
            } catch (loggingError) {
                $log.warn("Error server-side logging failed");
                $log.log(loggingError);
            }
        }
        return (error);
    }]);
}())

At this code we process any error by determining it's stack trace via printStackTrace method, that is available from StackTrace.js library: https://www.stacktracejs.com and then we simply post all collected about error information to server, to store it at database, file or somewhere else. At this point you already may say: "That is all, problem is completely solved!", but there is one important detail. All of these will work very well at development environment, but what about production?

Issue with bundling and minification.

What will be with our solution, presented above, at case of applying bundling and minification stuff, that is often used at production to minimize exchange between client and server sides? Problem will be with stack trace - actually we expect something like this:

0: "{anonymous}()@http://www.mysite.com/Scripts/AngularStuff/myFileName.js:123,34"
1: "{anonymous}()@http://www.mysite.com/Scripts/AngularStuff/anotherFileName.js:423,24"
etc...

but we will get another output:

0: "{anonymous}()@http://www.mysite.com/myBundleName_SomeLongHashedTail:1:2345"
1: "{anonymous}()@http://www.mysite.com/anotherBundleName_SomeLongHashedTail:1:24665"
etc...

And it is the only one, but essential problem  - we have lost actual location of our error. Now we will get it's location within bundle (myBundleName_SomeLongHashedTail at line 1, column 2345;) not actual file, with which we worked at development stage. This information is practically useless, especially, if bundle consists of not only from myFileName.js (like myFileName.min.js), but also from dozens of another files and searching of actual error location becomes very complicated and tedious task. 

To solve this problem we should perform two operations:

  1. Besides bundle we must create corresponding bundle.map projection to produce relation between this bundle and all files from which it consist of (at our case we are interesting in myFileName.js)
  2. Make request to this bundle.map entity with specifying looking for section (myBundleName_SomeLongHashedTail at line 1, column 2345;) to get actual and desired location (myFileName.js at line 123, column 34;)

To create bundle.map we can use this project: https://github.com/benmccallum/AspNetBundling, with it's help let's write code below to BundleConfig.cs:

public static class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
         bundles.Add(new ScriptWithSourceMapBundle("~/myBundleName")
            .IncludeDirectory("~/Scripts/AngularStuff", "*.js", true));
    }
}

ScriptWithSourceMapBundle is class from this project, that allow us to create bundle.map item. Now we can add some additional logic to initial $exceptionHandler code:

(function () {
    angular.module('app').factory(
        "$exceptionHandler", ["$log", "$window", function ($log, $window) {
        function error(exception, cause) {

            $log.error.apply($log, arguments);            

            try {
                var errorMessage = exception.toString();

                var stackTrace = printStackTrace({ e: exception });                

                var locationWithLineAndColumn = stackTrace[0].substr(stackTrace[0].indexOf('http'));
                //stackTrace[0]: 
                //'{anonymous}()@http://www.mysite.com/myBundleName_SomeLongHashedTail:1:2345'

                var array = locationWithLineAndColumn.split(':');
                var columnNumber = array[array.length - 1];
                var lineNumber = array[array.length - 2];                

                var location = locationWithLineAndColumn.substr(0, 
                      locationWithLineAndColumn.length - 2 - lineNumber.length - columnNumber.length);
                //location: 'http://www.mysite.com/myBundleName_SomeLongHashedTail'

                var stackframe = new StackFrame(undefined, [], 
                      location, parseInt(lineNumber), parseInt(columnNumber));

                new StackTraceGPS().pinpoint(stackframe).then(
                    function (answer) {                        
                        //answer == {
                        //    columnNumber:34
                        //    fileName:"http://www.mysite.com/Scripts/AngularStuff/myFileName.js"
                        //    functionName:"someName"
                        //    lineNumber:123
                        //}
                        stackTrace.unshift(answer.fileName + ':' + answer.lineNumber + 
                              ':' + answer.columnNumber);

                        $.ajax({
                            type: "POST",
                            url: "/logger",
                            contentType: "application/json",
                            data: angular.toJson({
                                url: $window.location.href,
                                message: errorMessage,
                                type: "exception",
                                stackTrace: stackTrace, //updated value with answer's stuff
                                functionName: answer.functionName, //new field
                                cause: cause
                            })
                        });   
                    },
                    function (answer) {
                        $log.warn("Error server-side logging failed");
                        console.log(answer);
                });                                             
            } catch (loggingError) {
                $log.warn("Error server-side logging failed");
                $log.log(loggingError);
            }
        }
        return (error);
    }]);
}())

stackTrace actually is array, we take it's first item an then parse it to obtain bundle's url(location variable) and position of error(lineNumber and columnNumber). Then based on these values we construct StackFrame, that is available from library: https://github.com/stacktracejs/stackframe and pass it to pinpoint method of StackTraceGPS class, that you can find as part of already mentioned library: https://www.stacktracejs.com/. This method will return actual location of error with some additional information via argument (answer) passed to callback function and it will contain, what we desire, i.e. "myFileName.js at line 123, column 34;". And that is all, problem is completely solved. Sure, this approach will still work at case of development environment, thought will be redundant.

What to install

  1. AspNetBundling: https://www.nuget.org/packages/AspNetBundling/
  2. StackFrame.js: https://github.com/stacktracejs/stackframe
  3. StackTrace.js: https://www.stacktracejs.com/

Conclusions

Logging errors - is very common task, with which any developer faced. Collecting client side errors is very important and necessary thing, fortunately, we have a lot of hooks for this, like: windows.onerror handler or $exceptionHandler at case of applying AngularJS framework. With the help of StackTrace.js library we have possibility to restore error's stack trace, but it's not enough. At production environment we frequently use bundles with minification, these fact leads to lose actual(expected) error's location and it becomes a real problem. To solve it, we should create .map projection (AspNetBundling will do this work) for our bundle and then call it with the help of StackTrace-gps.js library to find corresponding error's location at actual file.

You can simple use this approach at case of another frameworks via their error's handlers, but creation of stack trace and solving bundle and minification problem will remain the same.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here