Click here to Skip to main content
15,881,424 members
Articles / Web Development / HTML

Log the Errors in Angular Application with JSNLog

Rate me:
Please Sign up or sign in to vote.
4.79/5 (6 votes)
18 Jan 2015CPOL4 min read 32.4K   354   17   3
Shows how to configure your Angular application with JSNLog, a popular JavaScript logging library

Introduction

Debugging client-side errors is not easy when you don't know what a user is doing and seeing. In addition, when the errors happen, even customers don't see them. All they see is that the application is either showing something strange or nothing at all. AngularJS handles these errors very well; it will catch client-side errors and show them in the browser console allowing your application to continue. The problem is that even a user will not know that an error happened and the details of the error unless the user is an advanced one who knows what a browser console is and how to open it.

What is the solution? Log the errors, user actions, and context information like a page name somewhere on the server side where you can easily see and review them. The goal of this article is to show you how to do it in AngularJS application using one of the multiple client-side logging libraries—JSNLog, written by Matt Perdeck. You can find a few articles by Matt explaining what JSNLog is and how to use it. I suggest you read his article first; they are very informative and have a lot of details on how to use JSNLog and how to integrate it with the server-side logging framework like NLog, Log4Net or Elmah.

JSNLog documentation is very extensive. You can find it at http://js.jsnlog.com/Documentation/JSNLogJs.

Another problem which this article will help with is when you have your own custom server-side logging solution and don't want to switch to some third party libraries such as those mentioned above.

For demo purposes, I wrote a small application which will download articles from CodeProject.com using the provided API.

Let's Get Started

First, go to http://js.jsnlog.com, download a standalone jsnlog.min.js version and add it to our project.

Second, navigate to http://js.jsnlog.com/Documentation/GetStartedLogging/AngularJsErrorHandling and copy all code from this page into a separate file.

This is what the code will look like:

JavaScript
(function () {
    'use strict'

    // Create new module logToServer with new $log service
    angular.module('logToServer', [])
        // Make AngularJS do JavaScript logging through JSNLog so we can log to the server 
        // by replacing the $log service
        .service('$log', function () {
            this.log = function (msg) {
                JL('Angular').trace(msg);
            }
            this.debug = function (msg) {
                JL('Angular').debug(msg);
            }
            this.info = function (msg) {
                JL('Angular').info(msg);
            }
            this.warn = function (msg) {
                JL('Angular').warn(msg);
            }
            this.error = function (msg) {
                JL('Angular').error(msg);
            }
        })
        // Replace the factory that creates the standard $exceptionHandler service
        .factory('$exceptionHandler', function () {
            return function (exception, cause) {
                JL('Angular').fatalException(cause, exception);
                throw exception;
            };
        })
        // Add a factory to create the interceptor to the logToServer module
        .factory('logToServerInterceptor', ['$q', function ($q) {
            var myInterceptor = {
                'request': function (config) {
                    config.msBeforeAjaxCall = new Date().getTime();
                    return config;
                },
                'response': function (response) {
                    if (response.config.warningAfter) {
                        var msAfterAjaxCall = new Date().getTime();
                        var timeTakenInMs = msAfterAjaxCall - response.config.msBeforeAjaxCall;
                        if (timeTakenInMs > response.config.warningAfter) {
                            JL('Angular.Ajax').warn({
                                timeTakenInMs: timeTakenInMs,
                                config: response.config,
                                data: response.data
                            });
                        }
                    }
                    return response;
                },
                'responseError': function (rejection) {
                    var errorMessage = "timeout";
                    if (rejection.status != 0) {
                        errorMessage = rejection.data.ExceptionMessage;
                    }
                    JL('Angular.Ajax').fatalException({
                        errorMessage: errorMessage,
                        status: rejection.status,
                        config: rejection.config
                    }, rejection.data);
                    return $q.reject(rejection);
                }
            };
            return myInterceptor;
        }]);
})();

Then, add (import) the logToServer module to your main module and add the new interceptor to the interceptor pipeline of the main module:

JavaScript
// Create module
angular.module('ArticlesModule', ['logToServer'])
    // Register the controller
    .controller('ArticlesController', ['$scope', '$http', ArticlesController])
    // Add the new interceptor to the interceptor pipeline of the main module
    .config(['$httpProvider', function ($httpProvider) {
        $httpProvider.interceptors.push('logToServerInterceptor');
    }]);

And as the final step, we need to tell JSNLog what default URL to send messages to. In reality, it's probably better to use a RESTful web service but in the demo application, a plain .aspx page will do just fine. I added the below code to the logToServer module itself so the same URL will be used for all application pages:

JavaScript
JL.setOptions({
    'defaultAjaxUrl': 'LogDetails.aspx'
})

In the server side code, be it an .aspx page or a web service, you'd use your own logging code to save the errors. I'll just save the data to the text file for simplicity.

That's it! You are all set to log client-side errors.

Let's test our code. I added 'use strict' so all variables have to be defined, otherwise an exception will be thrown. I added myVariable to the application module and this is what was logged:

{"r":"","lg":[{"l":6000,
"m":"{\"stack\":\"ReferenceError: myVariable is not defined\\n    at new ArticlesController
(http://localhost:4283/app.js:32:9)\\n    at Object.e [as invoke]
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:36:365)\\n    at F.instance
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:75:91)\\n    at
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:58:287\\n    at s
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:7:408)\\n    at G
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:58:270)\\n    at g
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:51:172)\\n    at g
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:51:189)\\n    at
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:50:280\\n    at
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:18:8\",
\"message\":\"myVariable is not
defined\",\"name\":\"ReferenceError\"}","n":"Angular","t":1421533414109}]}

This is useful but let's try to improve it. One missing thing is that we don't know what page logged the error. We can use a so called request id—see "r":"" above. The request id is used to uniquely identify each request and it's set to a random number in .NET edition of JSNLog. In my opinion, a random number to identify which log messages belong to which user may not be a solution for some sites, because it does not tell you anything about the user. Sometimes, it's important to know a type of the user - regular or admin/superuser, etc. We can put any arbitrary information in the request id field but I'll store a page name:

JavaScript
JL.setOptions({
    'requestId': window.location.pathname.split("/").pop()
});

Now the request id has the page name:"r":"index.htm". The rest is the same.

What if we don't want to log the whole exception? Then, we can change the line below:

JavaScript
JL('Angular').fatalException(cause, exception);

to:

JavaScript
JL().log(4000, { 'stack': exception.stack, 'error': exception.message });

And the following will be logged:

{"r":"Index.html","lg":[{"l":4000,
"m":"{\"stack\":\"ReferenceError: myVariable is not defined\\n    at new
ArticlesController (http://localhost:4283/app.js:32:9)\\n    at Object.e [as invoke]
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:36:365)\\n    at F.instance
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:75:91)\\n    at
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:58:287\\n    at s
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:7:408)\\n    at G
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:58:270)\\n    at g
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:51:172)\\n    at g
(https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:51:189)\\n    at
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:50:280\\n    at
https://ajax.googleapis.com/ajax/libs/angularjs/1.3.1/angular.min.js:18:8\",
\"error\":\"myVariable is not
defined\"}","n":"","t":1421539948122}]}

Another area where we can improve is AJAX calls. In modern JavaScript applications, AJAX calls are used a lot and it's important to log AJAX errors and their durations. Let's change the interceptor to be like this:

JavaScript
.factory('logToServerInterceptor', ['$q', function ($q) {
    var myInterceptor = {
        // The request function is called before the AJAX request is sent
        'request': function (config) {
            config.msBeforeAjaxCall = new Date().getTime();
            return config;
        },
        // The response function is called after receiving a good response from the server
        'response': function (response) {
            var msAfterAjaxCall = new Date().getTime();
            var timeTakenInMs = msAfterAjaxCall - response.config.msBeforeAjaxCall;
            JL('Angular.Ajax').info({
                url: response.config.url,
                timeTakenInMs: timeTakenInMs
            });
            return response;
        },
        // The responseError function is called when an error response was received, 
        // or when a timeout happened.
        'responseError': function (rejection) {
            var errorMessage = "unknown";
            JL('Angular.Ajax').fatalException({
                status: rejection.status,
                url: rejection.config.url,
                errorMessage: rejection.data.error
            });
            return $q.reject(rejection);
        }
    };
    return myInterceptor;
}]);

Now if the AJAX call was successful, the following will be logged:

{"r":"index.htm","lg":[{"l":3000,"m":"{\"url\":\"https://api.codeproject.com/v1/Articles?page=1
\",\"timeTakenInMs\":606}","n":"Angular.Ajax","t":1421543626142}]}

And if the AJAX call failed:

{"r":"index.htm","lg":
[{"l":6000,"m":"{\"status\":400,\"url\":\"https://api.codeproject.com/token\",
\"errorMessage\":\"invalid_client\"}","n":"Angular.Ajax","t":1421543588139}]}

You may be wondering how you can log something such as user actions, add some trace information, etc. This is easy. You can add logging code anywhere. The page load time can be logged like this:

JavaScript
if (!window.performance) {
    // IE 8 and below is not supported
    JL().warn('Performance object is not supported');
} else {
    var now = new Date().getTime();
    var pageLoadTime = now - window.performance.timing.navigationStart;

    // Log the page load time
    JL().info(window.location.pathname.split("/").pop() + ' load time-' + pageLoadTime + ' ms');
}

This is what you will see in the log file:

{"r":"index.htm","lg":[{"l":3000,"m":"index.htm load time-384 ms","n":"","t":1421544517551}]}

Conclusion

As you can see, JavaScript logging is very easy with JSNLog library and, hopefully, this article will help anyone to get started right away. Happy logging!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
BugError: [$rootScope:inprog] $digest already in progress Pin
Member 1211694412-Oct-16 2:52
Member 1211694412-Oct-16 2:52 
QuestionEndpoint for .Net, Request Ids Pin
Matt Perdeck17-Jan-15 22:20
Matt Perdeck17-Jan-15 22:20 
AnswerRe: Endpoint for .Net, Request Ids Pin
Igor Vigdorchik18-Jan-15 4:53
Igor Vigdorchik18-Jan-15 4:53 
Thank you, Matt. I appreciate your comment.

I did not want to use JSNLog for .Net Smile | :) . My article is targeting the sites which use web services to communicate with the backend and have to use their own server side error logging code (maybe because too much is already invested and there is no time to re-write it). This was a reason JSNLog was selected. The standalone library has a small footprint and meets this requirement.

As to the request id. In my opinion, a random number to identify which log messages belong to which user may not be a solution for some sites, because it does not tell you anything about the user. Sometimes it's important to know a type of the user - regular or admin/superuser, etc. My point was that we can put any arbitrary information in the request id field. I should've made it clearer; I'll update the article accordingly.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.