Logging Client-Side Errors With AngularJS And Stacktrace.js
Last year, at cf.Objective(), I was watching Elliott Sprehn give a presentation on Production Ready JavaScript. In part of the presentation, he was talked about client-side errors and recommended that everyone log client-side errors to the server. To be honest, before he mentioned it, I don't think that it had ever occurred to me! Before that, I had only ever logged server-side errors. But, now that I am building single-page applications with AngularJS, client-side error logging has become an essential tool in creating a stable application. Getting the right error information isn't always easy, though; so, I thought I'd share what I do in my AngularJS applications.
View this demo in my JavaScript-Demos project on GitHub.
AngularJS has excellent error handling! As long as you are inside of an AngularJS context or, you are executing code inside an $apply() callback, AngularJS will catch client-side errors, log them gracefully to the console, and then let your JavaScript application continue to run. The only problem with this is that the current user is the only one who knows that the error occurred.
To help me and my team iron our JavaScript errors, we needed to intercept the core AngularJS error handling and add a server-side communication aspect to it. To do this, we had to override the $exceptionHandler provider and replace it with a custom one that POST'ed the error to the server using AJAX (Asynchronous JavaScript and XML).
Posting the error to the server was only half the battle; it turns out that getting the right error information out of a JavaScript exception object is not super easy, especially across multiple browsers. Luckily, I found Stacktrace.js by Eric Wendelin. Stacktrace.js can take an error object and produce a stacktrace that works in every browser that we support. It has been an invaluable library!
In the following code, I've tried to isolate all of the error handling aspects of my AngularJS application. Really, the only part that I've left out is the debouncing. This code will blindly post every error that occurs on the client. In reality, however, this approach adds too much noise to the error log. As such, in production, I only post unique errors within a given time span. This way, if some directive on an ngRepeat, for example, throws an error, I don't end up making an HTTP request for every single ngRepeat item.
<!doctype html>
<html ng-app="Demo" ng-controller="AppController">
<head>
<meta charset="utf-8" />
<title>
Logging Client-Side Errors With AngularJS And Stacktrace.js
</title>
<style type="text/css">
a[ ng-click ] {
cursor: pointer ;
text-decoration: underline ;
}
</style>
</head>
<body>
<h1>
Logging Client-Side Errors With AngularJS And Stacktrace.js
</h1>
<p>
<a ng-click="causeError()">Cause Error</a>...
</p>
<p>
<em>
<strong>Note:</strong> Look at the JavaScript console to
see the errors being reported.
</em>
</p>
<!-- Load jQuery and AngularJS from the CDN. -->
<script
type="text/javascript"
src="../../vendor/jquery/jquery-2.0.3.min.js">
</script>
<script
type="text/javascript"
src="../../vendor/angularjs/angular-1.0.7.min.js">
</script>
<script
type="text/javascript"
src="../../vendor/stacktrace/stacktrace-min-0.4.js">
</script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// The "stacktrace" library that we included in the Scripts
// is now in the Global scope; but, we don't want to reference
// global objects inside the AngularJS components - that's
// not how AngularJS rolls; as such, we want to wrap the
// stacktrace feature in a proper AngularJS service that
// formally exposes the print method.
app.factory(
"stacktraceService",
function() {
// "printStackTrace" is a global object.
return({
print: printStackTrace
});
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// By default, AngularJS will catch errors and log them to
// the Console. We want to keep that behavior; however, we
// want to intercept it so that we can also log the errors
// to the server for later analysis.
app.provider(
"$exceptionHandler",
{
$get: function( errorLogService ) {
return( errorLogService );
}
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// The error log service is our wrapper around the core error
// handling ability of AngularJS. Notice that we pass off to
// the native "$log" method and then handle our additional
// server-side logging.
app.factory(
"errorLogService",
function( $log, $window, stacktraceService ) {
// I log the given error to the remote server.
function log( exception, cause ) {
// Pass off the error to the default error handler
// on the AngualrJS logger. This will output the
// error to the console (and let the application
// keep running normally for the user).
$log.error.apply( $log, arguments );
// Now, we need to try and log the error the server.
// --
// NOTE: In production, I have some debouncing
// logic here to prevent the same client from
// logging the same error over and over again! All
// that would do is add noise to the log.
try {
var errorMessage = exception.toString();
var stackTrace = stacktraceService.print({ e: exception });
// Log the JavaScript error to the server.
// --
// NOTE: In this demo, the POST URL doesn't
// exists and will simply return a 404.
$.ajax({
type: "POST",
url: "./javascript-errors",
contentType: "application/json",
data: angular.toJson({
errorUrl: $window.location.href,
errorMessage: errorMessage,
stackTrace: stackTrace,
cause: ( cause || "" )
})
});
} catch ( loggingError ) {
// For Developers - log the log-failure.
$log.warn( "Error logging failed" );
$log.log( loggingError );
}
}
// Return the logging function.
return( log );
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// ---
// PUBLIC METHODS.
// ---
// I cause an error to be thrown in nested functions.
$scope.causeError = function() {
foo();
};
// ---
// PRIVATE METHODS.
// ---
function bar() {
// NOTE: "y" is undefined.
var x = y;
}
function foo() {
bar();
}
}
);
</script>
</body>
</html>
We aren't overriding the core AngularJS error handling so much as we are simply augmenting it. As you can see in the log() function, the very first thing we actually do is hand off the exception to AngularJS's native error() method. This way, the error is always logged to the Console even if our HTTP post fails.
You may also notice that the actual HTTP post to the server is executed using jQuery's $.ajax() method and not AngularJS's $http service. This is done on purpose; the $http service uses the errorLogService, so any attempt to use the $http service inside of our exception handler will cause a circular dependency:
Error: Circular dependency: $http <- errorLogService <- $exceptionHandler <- $rootScope
When I added this code to my AngularJS applications, I'm embarrassed to say that I was shocked - completely shocked - at how many JavaScript errors were being generated. Many of the errors turn out to be cross-browser quirk; some turn out to be real bugs; and, some turn out to be complete mysteries that nobody on the team can produce. Slowly, however, we're trying to solve every client-side error that gets logged to the server.
Want to use code from this post? Check out the license.
Reader Comments
Recently we've been using Sentry - https://app.getsentry.com/ - for JS error logging in all our apps.
Like you, we're trying to solve 1 error at a time. The only ones we can't seem to solve are those that originate from browser extensions.
@Christian,
That looks pretty cool - I haven't seen that before. How do you pipe errors into it? Does it expose some local api, like sentry.log()? The public docs don't seem to have any actual code in them.
Very nice! One thing I'd suggest is that you use the version of stacktrace.js on master because it has the very latest browser support (the version on the website is sometimes out-of-date). I plan on doing more frequent releases in the future, but the version on master is heavily tested and should be considered production-ready.
Have you seen this?
http://www.aliaspooryorik.com/blog/index.cfm/e/posts.details/post/logging-client-side-errors-with-hoth-367
You can also look at BugLogHQ (http://www.bugloghq.com), it comes with a javascript client to log errors which has stacktrace.js embedded inside.
https://github.com/oarevalo/BugLogHQ/blob/master/client/bugLogClient.js
@Eric,
Ah, good to know! Actually, I do have one question for you. It looks like your code makes an HTTP request *sometimes* to re-get the Script source. What are the conditions for that? Is that for a particular browser?
@Sami,
While I haven't used Hoth personally, Aaron Greenlee has been working with me on some Amazon S3 stuff and he's talked about it. I'll have to dig into it a bit more. There's a lot that I'd love to clean up about my app.
@Oscar,
Sounds cool! I'll take a look.
@Ben,
If I get an anonymous function on the stack, and the file it's in is on the same domain as the current domain I get the script source so I can parse the line and perhaps guess the function name.
This is used in the case where you define a function like this: var foo = function() {..} because the function "name" is anonymous according to the runtime.
@Eric,
Ahh, very clever! For future reference, I should probably name my functions, even if they are simply being passed into other methods, ex:
thing.do( function myCallback() { .. } );
That would probably make the debugging easier in general!
@Ben,
That's what I recommend :)
If you want to keep the default logging of $exceptionHandler while extending it with custom functionality, you can use a decorator:
angular.module('myApp').config(['$provide', function ($provide) {
$provide.decorator('$exceptionHandler', ['$delegate', function ($delegate) {
return function(exception, cause) {
// Calls the original $exceptionHandler.
$delegate(exception, cause);
//Custom error handling code here.
};
}]);
}]);
@Joe,
Very interesting! I have not used decorators in AngularJS before. I'll definitely have to check that out. Thanks for the insight!
I've been tackling this issue with a non-Angular application I'm developing. I am always interested in how people solve front-end error logging.
I believe logging these errors isn't enough on their own, you need to use it alongside some analytical tracking. My personal tracking JS file keeps a record of document.referrer and window.location.href (amongst other things) and by using all this data I can trace the user's steps through my app to reproduce bugs.
@Sisb,
I definitely agree with that. I wish I had a better system for it at the moment. Right now, I have a "log" that has a really simple UI that I can just page through. Ultimately, I hope to have zero errors; but for now, I just eye-ball it and see which ones are happening more often than not.
The great part of AngularJS is that many errors happen "behind the scenes" due to the way that AngularJS handles client-side errors.
Thanks for this post Ben! Something you should consider adding to your log is a throttling function. If the JavaScript code ends up in a loop condition, clients could quickly DoS your logging endpoint.
I have an example of a couple throttling functions here:
https://github.com/TrackJs/Tech-Demo/blob/master/src/TrackJs.Demo/Content/js/transmission.js
We have also just launched a JavaScript error tracking service called {Track:js} http://trackjs.com/ that attempts to provide some of this. I'd love to do something like this as an Angular plugin to our tracker!
{Track:js} approaches this problem a little different--rather than trying to wring more data out of the very-unhelpful JavaScript error, we instead capture analytics about what the user, the network, the console, and the environment were doing that led up to the error. I'd love to get your feedback on it. Shoot me an email and I'll give you an invite code.
@Todd,
I 100% agree. When I first implemented the client-side error logging, I didn't have any throttling. But, I quickly saw that if a client's code wasn't working, I could quickly get hundreds of errors from the same user in a matter of seconds.
Imagine an AngularJS directive that wasn't working on an ngRepeat that rendered 100 items (each causing a single error).
When I saw this happening, I did add throttling based on the error message and the time (a user can only log a given error once in a given time period). I included a note about "debouncing" in the demo code above; but, I felt that the actual implementation was probably beyond the scope of the blog post.
That said, I love the idea of trying to gather more information about what the user was actually doing at the time of the error. Right now, we do really just rely on the error itself and stacktrace (so that we can at least see which line of code caused the error). Of course, we do get times where the stacktrace is either unavailable; OR, that the error itself makes no sense and can't be reproduced on our end :(
I love the design of your site, btw. Very clean and clear! I'll definitely check it out.
Great post Ben! Did you consider using JSNLog (js.jsnlog.com) to take care of the logging itself?
It has functionality to send your log data to the server and also to throttle log data so you don't get overwelmed. Plus named loggers, etc. Log4J style.
You could use Atatus to log error from AngularJS.
http://www.atatus.com/
In the docs, under exceptions you can see how it can hook into AngularJS
angular.module('app').factory('$exceptionHandler', function () {
return function (exception, cause) {
throw exception;
};
});
We successfully use Atatus itself to monitor www.atatus.com website.
I feel that falling back on jQuery to do a AJAX request is overkill. So instead, to avoid circular dependency problems I inject the services upon the first retrieval of the error handler.
By slightly changing the error log service you can use $http without any problems.
function( $log, $window, $injector, stacktraceService ) {
var $http = null;
// I log the given error to the remote server.
function log( exception, cause ) {
if (!$http)
try { $http = $injector.get('$http'); }
catch (e) {
$http = null; // To assure a retry on the next error
$log.warn('Retrieving $http service for error logging failed.');
$log.log(e);
}
You can use the same technique to load any other service you might need (such as ones that depend on $http). I use that to retrieve some information from other services that allow me to better trace errors. (For example you could retrieve the state of routing instead of just the URL.)
There are lots of ready-to-use tools to monitor js errors our there, it may be more convenient to use one of them.
I am biased, we build js error monitoring tool, Qbaka, focusing on filtering noise from lots of event and providing only valuable information to developers.
I would recommend to check out available services and choose the one that is best: most of tools provide basic features for free, might be better solution than just developing and running your own tool.
Thanks for this post. Client side logging is a must in my opinion.
I too used the jQuery ajax approach. However, on my current project I wanted to remove the jQuery dependency. The mailing list suggested I could use the $httpBackend directly.
Here is the post: http://stackoverflow.com/questions/22696767/how-can-i-call-angulars-httpbackend-directly/22696844#22696844
<pre>
$httpBackend('POST', '/some/url', //method and url
JSON.stringify(buildLogInfo()), //request body
function(status,resp,headerString){ //response call back
console.log('manual backend call',status,resp,headerString);
},
{"Content-Type": "application/json"} //request headers
);
</pre>
Thanks for the post! This is a great tool combination.
However, pardon my stupid question as I just started web programming, I am wondering if the tool still as valuable if the javascript files are optimized, i.e. uglified and minimized. I assume it will send out the traces for whatever optimized javascript file the error will almost always be at line 0.
So my real question is, is there an effective way to track remote javascript errors when the .js file is optimized? Or is it even possible?
Thanks!
@Aaven,
You could do something like that with a tool that supports source maps. stacktrace.js doesn't support source maps yet, and I'm not sure of another way to do this.
@Eric,
Oh I see. Thanks!
Is there a way to figure out which controller is throwing the exception at $exceptionHandler?
How would you implement this in a scenario where you are using require.js and manually boostrapping your app?
When i try to implememt this code in my proejct i am getting below error
$ is not defined
Error: [$rootScope:inprog] $digest already in progress
at the line
$.ajax({
type: "POST",
url: window.apirooturl + "/logger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: errorMessage,
type: "exception",
stackTrace: stackTrace,
cause: ( cause || "")
})
});
can anyone help in this ?/
This is a great and I was able to get this implemented, however it is breaking jasmine tests with expect(fn).toThrow() matcher, I get test FAILED 'Error: Expected function to throw an exception'.
I've spent hours now and I cannot figure out why, as an exception is getting thrown in application as expected, any ideas?
@Simplesthing,
A co-worker fixed the test for me in no time, he added
throw exception;
to the errorLogService.
Another vote for Sentry, the guys over at Disqus know what they're doing.
@ben, the documentation for the JS client isn't on the Sentry readthedocs, because Sentry is the Backend to what ever client you use. The javascript client is called RavenJs. http://raven-js.readthedocs.org/en/1.0.0/usage/index.html
@ben: thx for the article :)
Did you find a solution to use the $http service and stacktrace.js at the same time? That will be great!
Hi Ben,
Can you share the code for how you handle the error message in your server side, where you have posted the message through jQuery....
So, When the log function is invoked, there is no scope. I think this is part of the security restrictions of stacktrace. For instance, in the log(exception,cause){...} if you alert(this) it says "undefined". Having no scope means we can't display our custom modal to the user (the only thing we can do is alert).
Any workaround or hooks back into the original scope?
Hi Ben,
Very helpful post. I was wondering if you would be willing to share your debouncing code also?
Thanks
Hey Ben,
Great stuff. I'm incorporating this into our app now and I ran into a problem unit testing when you use JQueries $.ajax() method to post to the server.
The problem (obviously) is it's hard to test that it's working without actually posting during a unit test. Also, I don't want all my unit tests posting errors (some of which are intentional) to the log.
I found this solution and thought I might share it.
You can use the $injector service to get $http manually at runtime and avoid the circular dependency problem. Then in your unit tests, you can use $httpBackend to assert it's working as normal without actually posting.
module.factory("errorLogService", ['$log', 'stacktraceService', '$injector',
function ($log, stacktraceService, $injector) {
var $http;
// Return the logging function.
return {
log: log
};
// I log the given error to the remote server.
function log(exception, cause) {
// call the injector manually at runtime to get $http and avoid a circular dependency
$http = $http || $injector.get('$http');
// ... continue with posting to server now with $http....
}
}
}
Hi,
Great post, thanks.
Instead of stacktraceJs, I use JSN Log (jsnlog.com) which combines very well with Log4Net. It requires no setup serverside if you have Log4Net already installed. Which is awesome! No need to take care of handling the requests or anything.
Your post pointed me in the right direction on how to implement this with AngularJS.
it is not working for me , to log the the error on client Side , can any one help me to get this example.
Curious to know how you throttled or debounced the error calls. My main concern is if there are two distinct errors:
For example:
Error A
error b
error b
error b
error b
error b
I would like to catch "error A" and "error b" only once. I will update my Gist if I come up with something decent.
https://gist.github.com/coryasilva/edf077df29fe8f9341dd
I'm assuming there's really no way to prevent abuse/unintended POSTs to the logging endpoint? I suppose CSRF could be used to atleast lock it down a little bit more
Works great in Chrome. But nothing works in IE9. Do we have any thing by which we can log angular error in IE9. Get basic information like : errMessage, Line, fileName and stacktrace.
Demo link http://bennadel.github.io/JavaScript-Demos/demos/error-logging-angularjs/ also not works in IE9
For IE9 there is nothing that i get :-( .
Only stacktrace i get in IE9 is error in stacktrace itself.
{anonymous}("<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">...</s:Envelope>")
printStackTrace()
Ben -- we're currently discussing client side logging solutions.
Is this the best approach you've come across? We're thinking of adopting this idea and adding in some approaches to prevent abuse, but also wondering about existing third party integrations that may provide as an existing solution.
Hi Atticus,
Just as a tip: what I did was use Log4Net for serverside logging and add JSNLog as clientside extension of that framework.
I used Bens code and replaced his use of Stacktrace.js with JSNLog.
Guys,
I used decorator and was able to make it work with $http service (that way request travels with all usual auth tokens and info). Here is the code (I used parts of original code from this post) :
//begin of code
angular.module('MyExceptionHandler', []);
angular.module('MyExceptionHandler').factory('myExceptionHandler', ['$injector', '$log', '$window', function($injector, $log, $window) {
return function($delegate) {
return function (exception, cause) {
// Lazy load to avoid circular dependency
var $http = $injector.get('$http');
try {
var errorMessage = exception.toString();
StackTrace.fromError(exception).then(function(stackframes) {
var stringifiedStack = stackframes.map(function(sf) {
return sf.toString();
}).join(' \n');
$http.post('/log',
{
errorUrl: $window.location.href,
errorMessage: errorMessage,
stackTrace: stringifiedStack,
cause: ( cause || "" )
}
).then(function(response) {
//...
}, function(err) {
//...
});
});
} catch ( loggingError ) {
// for Developers - log the log-failure.
$log.warn( "Error logging failed" );
$log.log( loggingError );
}
// Pass through to original handler
$delegate(exception, cause);
};
};
}]);
angular.module('MyExceptionHandler').config(['$provide', function($provide) {
$provide.decorator('$exceptionHandler', ['$delegate', 'exceptionHandlerFactory', function ($delegate, exceptionHandlerFactory) {
return exceptionHandlerFactory($delegate);
}]);
}]);
//end of code
Hi,
You mark in your post "only post unique errors within a given time span" on your production state.
How are you do that ?
Thanks,
Great post, I love your articles. Simple but useful (well most useful things are simple but based on good ideas).
I'm going to apply this technique in my Ionic app.
One could use Google Analytics or for instance Ionic Analytics as the backend server to send the data to, right? Should definitely be possible.
Hello together,
I found this nicearticle about how to to correctly use Bootstrap and Angular together:
https://scotch.io/tutorials/how-to-correctly-use-bootstrapjs-and-angularjs-together
It's explained that we should not use jQuery when using AngularJs because the app will have a different behaviour,
But to implement the authentication or use the http-auth-interceptor I find myself forced to use jQuery :-(
Can anyone give us a solution about integrating http-auth-interceptor in an angularJs app without using jQuery? That will be great :-)