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:
(function () {
'use strict'
angular.module('logToServer', [])
.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);
}
})
.factory('$exceptionHandler', function () {
return function (exception, cause) {
JL('Angular').fatalException(cause, exception);
throw exception;
};
})
.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:
angular.module('ArticlesModule', ['logToServer'])
.controller('ArticlesController', ['$scope', '$http', ArticlesController])
.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:
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:
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:
JL('Angular').fatalException(cause, exception);
to:
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:
.factory('logToServerInterceptor', ['$q', function ($q) {
var myInterceptor = {
'request': function (config) {
config.msBeforeAjaxCall = new Date().getTime();
return config;
},
'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;
},
'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:
if (!window.performance) {
JL().warn('Performance object is not supported');
} else {
var now = new Date().getTime();
var pageLoadTime = now - window.performance.timing.navigationStart;
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!