Retain Console History using Angular’s $log
I’ve started wrap up on a substantial, digest already in progress‘ errors on one of the views. Although I’ve taken steps to try and get around those issues, I haven’t been able to pinpoint how the user is causing those errors.
I began trying to devise a way to track a user as they interact with the application, then use this information to help debug the errors. After poking around the internet, I wanted to use Angular’s $log (docs) module to not only write messages to the console, but retain those messages. Then, write them to the server when an error occurs. I figured out that this behavior can be easily added without having to touch the window’s console object by decorating the $log module.
Decorate Angular’s $log
The first step is to add some functionality to the $log service to capture history anytime one of the methods are used. Here’s the full code to decorate the $log service:
angular.module('app', []).config(['$provide', function ($provide) {
$provide.decorator('$log', ['$delegate', function ($delegate) {
function wrapper(fn) {
return function () {
this.history.push({
date: new Date(),
args: [].slice.call(arguments)
});
return fn.apply(this, arguments);
};
};
$delegate.history = $delegate.history || [];
['log', 'info', 'warn', 'error', 'debug'].forEach(function (name) {
if (angular.isFunction($delegate[name])) {
$delegate[name] = wrapper($delegate[name]);
}
});
return $delegate;
});
}]);
The first section I want to touch on is the wrapper function. Anything that would normally be printed to the console, I want to capture on the stack. The wrapper will take the existing functions on the $log service and create a new function out of them. This way, anytime they are called, they push an entry onto the history stack that consists of, a time stamp, and all of the arguments that were passed into the original function. For instance, if I did $log.info(‘My Stuff’, [1, 2, 3], { hello: ‘world’ }); , it saves it for later. This comes incredibly helpful when you need to see what certain object values were during that action.
The next line is creating the history array, if it doesn’t already exist, on the original object and initializing it with an empty array. Not too much happening there.
Next, we are wrapping the necessary functions that are on the $log service. Right now, those are log, info, warn, error, and debug. The function check is a safeguard in case any of those aren’t actually functions or if I somehow mistype something. It’s not completely necessary, but keeps it from creating functions where they aren’t needed. Then we are calling the wrapper to create new functions that will retain the history. Simple!
From there, you can continue to use the $log service the same as you always have. The only difference now is that there’s a history property that has a traceable history of each time any of those functions were called.
Where does this become useful?
Being able to track a user when an error occurs is always great information to have. I altered the $exceptionHandler to log my database-backed system on the server. It makes an AJAX call with the exception and some other information. As stated before, I knew what the error was, but didn’t know how it was caused.
Here’s how I integrated this into the $exceptionHandler :
$provide.decorator('$exceptionHandler', [
'$delegate', '$log', function ($delegate, $log) {
return function (exception, cause) {
var message = ['Error: ', JSON.stringify(exception), ' - Cause: ', cause || '', ' - History: ', JSON.stringify($log.history)].join('');
$.post(appurl + '/api/log/error', { message: message });
$log.history = [];
return $delegate(exception, cause);
};
}
]);
Now my logs become more informative when an error happens on the client. I also make sure to clear out the history after it gets written so that future errors have their own trace. I’m using jQuery here to do the sending of data, but you can also inject Angular’s $http module to do the call.
I would urge you to make sure the history is cleared at other points in your app. Eventually, that array could grow to a point where your app starts to slow down because of memory usage. Just something to be aware of.
Go Forth and Conquer
Hopefully you find this little technique useful. Being able to see the steps a user took to create an error has allowed me to squish a fair number of those ‘digest already in progress’ errors. The time stamps on each step allow me to see how long they took between each step which may help in debugging in its own right.