Logging client side errors server-side in AngularJS
If builders built buildings the way programmers wrote programs, the first wood-pecker that came along would destroy civilization ~ Gerald Weinberg
Tracking down a problem in a rich client application is hard when you can’t see what your user is seeing and experiencing. Worse, when the errors are raised they exist only in the context of the users browser, where we, as the developers of the code can’t get to them. This post describes how we get around this problem at Talis in our AngularJS apps using stacktrace.js to help us.
We need to be logging client side errors to our servers where we can see them, and if need be with other contextual information to help us diagnose the problem (e.g. the dom, user details, $scopes
etc. ). If we don’t do this, good luck trying to find out what causes an Angular app to die on user X’s version of browser Y.
How does stracktrace.js help
Before describing how to wire up an AngularJS app to automatically post errors/exceptions to a server I want to talk about stacktrace.js by Eric Wendelin, which attempts to solve an important problem. Getting the right error information out of a JavaScript exception object is not easy especially across different browsers because not all browsers give you informative stacktraces, and those that do, give you the information in different ways.
stacktrace.js
hides you from those implementation details and provides a simple consistent way to get at them. It does this by providing a method called printStackTrace()
which is globally available and can be called from within any function and returns the call stack at that point as shown in the example below:
<script type="text/javascript" src="https://rawgithub.com/stacktracejs/stacktrace.js/master/stacktrace.js"></script>
<script type="text/javascript">
var trace = printStackTrace();
alert(trace.join('\n\n'));
// or alternatively pass an error to it:
try {
// error producing code
} catch(e) {
var trace = printStackTrace({e: e});
alert('Error!\n' + 'Message: ' + e.message + '\nStack trace:\n' + trace.join('\n'));
}
</script>
Logging client-side in AngularJS
AngularJS has pretty good error handling and, within its context, will catch client-side errors and log them to the console allowing your application to continue. The problem, as outlined at the beginning of this article, is that the only person who knows the error occurred is the user sitting in front of the screen.
So let’s fix this. As a broad overview what we are going to do is create a new AngularJS module that we’ll use for all our logging. We’ll add services to this module that will augment the default error handling in AngularJS, and we’ll use stacktrace.js to get at the browser specific stack trace, and make a post request to a server side API to store the error.
Make sure that you have included the stacktrace.js library, by adding the script tag to your html page as shown in the example above.
Let’s start by defining a new AngularJS module that we’ll use for logging:
var loggingModule = angular.module('talis.services.logging', []);
;
Now, let’s add a simple service to this module that will act as a wrapper onto stacktrace.js and expose the printStackTrace()
method. We do this because we don’t want to be referencing global objects inside AngularJS components ~ thats just wrong!
var loggingModule = angular.module('talis.services.logging', []);
/**
* Service that gives us a nice Angular-esque wrapper around the
* stackTrace.js pintStackTrace() method.
*/
loggingModule.factory(
"traceService",
function(){
return({
print: printStackTrace
});
}
);
;
With our traceService
implemented, let’s add an exceptionLoggingService
which we can use to augment the default logging behaviour in AngularJS:
var loggingModule = angular.module('talis.services.logging', []);
/**
* Service that gives us a nice Angular-esque wrapper around the
* stackTrace.js pintStackTrace() method.
*/
loggingModule.factory(
"traceService",
function(){
return({
print: printStackTrace
});
}
);
/**
* Override Angular's built in exception handler, and tell it to
* use our new exceptionLoggingService which is defined below
*/
loggingModule.provider(
"$exceptionHandler",{
$get: function(exceptionLoggingService){
return(exceptionLoggingService);
}
}
);
/**
* Exception Logging Service, currently only used by the $exceptionHandler
* it preserves the default behaviour ( logging to the console) but
* also posts the error server side after generating a stacktrace.
*/
loggingModule.factory(
"exceptionLoggingService",
["$log","$window", "traceService",
function($log, $window, traceService){
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();
// use our traceService to generate a stack trace
var stackTrace = traceService.print({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);
}]
);
;
That’s basically it. Any unhandled error within the Angular app will now be posted server-side automatically. As the comment I’ve placed in the code above highlights it is very important that, when posting the log statement to your server, you do NOT use the AngularJS $http
service. For two reasons, firstly it will create a circular dependency which you really want to avoid, and secondly if the AngularJS app is fubar’d you still have a chance of logging the error to your server. As you can see I’ve used jQuery in the example above but you could just as easily use a XMLHTTPRequest.
The POST request is made to a server-side endpoint /logger
which sits outside the single page AngularJS app. This is a simple service that receives a json
object. What you do with it is entirely up to you, you could store it in a database, or simply write it to a log file. In our case we log to our standard application server logs which are aggregated into our centralised Logstash server near realtime, allowing us to see whats happening immediately:
But there’s more …
Now there’s a few other things we might want to do. For example it would be nice to be able to introduce a service we could explicitly call to log errors that don’t raise exceptions.
/**
* Application Logging Service to give us a way of logging
* error / debug statements from the client to the server.
*/
loggingModule.factory(
"applicationLoggingService",
["$log","$window",function($log, $window){
return({
error: function(message){
// preserve default behaviour
$log.error.apply($log, arguments);
// send server side
$.ajax({
type: "POST",
url: "/logger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: message,
type: "error"
})
});
},
debug: function(message){
$log.log.apply($log, arguments);
$.ajax({
type: "POST",
url: "/clientlogger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: message,
type: "debug"
})
});
}
});
}]
);
You can use this by injecting the applicationLoggingService
into any module that needs it and call error()
or debug()
whenever you want to, as shown in this very simple example:
angular.module('example', []).factory("example", ['$rootScope', '$http', 'applicationLoggingService',
function ($rootScope, $http) {
var instance = function () {};
instance.get = function (url, callback) {
$http.get(url).success(function (response) {
applicationLoggingService.debug({
message: "retrieved data successfully",
});
callback(null, response);
}).error(function (response) {
applicationLoggingService.error({
message: "error retrieving data",
data: response.body
});
callback(response, null);
});
};
return instance;
}
])
Whilst the example above is very simple, it actually helps illustrate a different problem which is the final thing I’d like to focus on in this post.
Whilst it makes sense to want to log an error when the $http
service fails, do we really want to add that line to every $http.get().error()
callback? On the face of it, it might sound like a reasonable thing to do. You definitely want to know if your client app is requesting data from a service and that service is returning errors, but is there a better way?
The answer is yes, we can combine our applicationLoggingService
with an $http
Interceptor. As Chris mentioned in one of our earlier posts, interceptors are functions which are called between the response of the http call being received, and it being handed off to your application code.
In our case we can create an interceptor that specifically checks if any $http
request has failed and if so automatically log the error without the need for us to litter our code with explicit error()
log statements. Here’s how:
'example', []).config(['$httpProvider', function($httpProvider) {
/**
* this interceptor uses the application logging service to
* log server-side any errors from $http requests
*/
$httpProvider.responseInterceptors.push([
'$rootScope', '$q', '$injector','$location','applicationLoggingService',
function($rootScope, $q, $injector, $location, applicationLoggingService){
return function(promise){
return promise.then(function(response){
// http on success
return response;
}, function (response) {
// http on failure
// in this example im just looking for 500, in production
// you'd obviously need to be more discerning
if(response.status === null || response.status === 500) {
var error = {
method: response.config.method,
url: response.config.url,
message: response.data,
status: response.status
};
applicationLoggingService.error(JSON.stringify(error));
}
return $q.reject(response);
});
};
}
]);
}]);
;
angular.module(
Summary
In this post I’ve touched on the inherent difficulties of retrieving stack traces from browsers and how stacktrace.js can help us. I’ve covered how to create an AngularJS module that you can use to augment the default error handling behaviour in AngularJS ensuring unhandled errors are logged server-side. I’ve also illustrated how a similar approach can be used to explicitly log error()
or debug()
statements from the client to the server. I’ve also demonstrated how to create an $http
interceptor that will automatically log errors returned from any $http
request.
Finally, I hope this post helps highlight the importance of logging client side errors to your server and that it isn’t very difficult to achieve.