Problem
There are a lot of means and approaches to log errors with their quite informative description on server side at case of web development. If we use, for examle, ASP.NET MVC we can inherite all our controllers from base one, where common exception handling logic is performed (OnException
method is implied) also we can do that at Global.asax
via Application_Error
method and so on.
But what we will do on client side? We want to log these errors too, supplying them with information about it's native description (what happened), location of code (filename, line number and column number), stack trace and other stuff. This article is dedicated to answer on this question at case of AngularJS
, but presented solution can simply be applied to other client side framework or any other custom development approach via number of simple modifications, core idea remains the same.
Solution
As I already said, we use AngularJS
framework and want to log errors, which are happen at our custom client side code, I am talk about controllers, directives, services and so on. First of all, we need to subscribe on all exceptions via $exceptionHandler
service, that can be simply overridden. I used materials from this article: https://engineering.talis.com/articles/client-side-error-logging/, as starting point, and I will add some modifications further:
(function () {
angular.module('app').factory(
"$exceptionHandler", ["$log", "$window", function ($log, $window) {
function error(exception, cause) {
$log.error.apply($log, arguments);
try {
var errorMessage = exception.toString();
var stackTrace = printStackTrace({ e: exception });
$.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);
}]);
}())
At this code we process any error by determining it's stack trace via printStackTrace
method, that is available from StackTrace.js library: https://www.stacktracejs.com and then we simply post all collected about error information to server, to store it at database, file or somewhere else. At this point you already may say: "That is all, problem is completely solved!", but there is one important detail. All of these will work very well at development environment, but what about production?
Issue with bundling and minification.
What will be with our solution, presented above, at case of applying bundling and minification stuff, that is often used at production to minimize exchange between client and server sides? Problem will be with stack trace - actually we expect something like this:
0: "{anonymous}()@http://www.mysite.com/Scripts/AngularStuff/myFileName.js:123,34"
1: "{anonymous}()@http://www.mysite.com/Scripts/AngularStuff/anotherFileName.js:423,24"
etc...
but we will get another output:
0: "{anonymous}()@http://www.mysite.com/myBundleName_SomeLongHashedTail:1:2345"
1: "{anonymous}()@http://www.mysite.com/anotherBundleName_SomeLongHashedTail:1:24665"
etc...
And it is the only one, but essential problem - we have lost actual location of our error. Now we will get it's location within bundle
(myBundleName_SomeLongHashedTail at line 1, column 2345;) not actual file, with which we worked at development stage. This information is practically useless, especially, if bundle
consists of not only from myFileName.js (like myFileName.min.js), but also from dozens of another files and searching of actual error location becomes very complicated and tedious task.
To solve this problem we should perform two operations:
- Besides
bundle
we must create corresponding bundle.map
projection to produce relation between this bundle
and all files from which it consist of (at our case we are interesting in myFileName.js)
- Make request to this
bundle.map
entity with specifying looking for section (myBundleName_SomeLongHashedTail at line 1, column 2345;) to get actual and desired location (myFileName.js at line 123, column 34;)
To create bundle.map
we can use this project: https://github.com/benmccallum/AspNetBundling, with it's help let's write code below to BundleConfig.cs
:
public static class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptWithSourceMapBundle("~/myBundleName")
.IncludeDirectory("~/Scripts/AngularStuff", "*.js", true));
}
}
ScriptWithSourceMapBundle
is class from this project, that allow us to create bundle.map
item. Now we can add some additional logic to initial $exceptionHandler
code:
(function () {
angular.module('app').factory(
"$exceptionHandler", ["$log", "$window", function ($log, $window) {
function error(exception, cause) {
$log.error.apply($log, arguments);
try {
var errorMessage = exception.toString();
var stackTrace = printStackTrace({ e: exception });
var locationWithLineAndColumn = stackTrace[0].substr(stackTrace[0].indexOf('http'));
var array = locationWithLineAndColumn.split(':');
var columnNumber = array[array.length - 1];
var lineNumber = array[array.length - 2];
var location = locationWithLineAndColumn.substr(0,
locationWithLineAndColumn.length - 2 - lineNumber.length - columnNumber.length);
var stackframe = new StackFrame(undefined, [],
location, parseInt(lineNumber), parseInt(columnNumber));
new StackTraceGPS().pinpoint(stackframe).then(
function (answer) {
stackTrace.unshift(answer.fileName + ':' + answer.lineNumber +
':' + answer.columnNumber);
$.ajax({
type: "POST",
url: "/logger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: errorMessage,
type: "exception",
stackTrace: stackTrace, functionName: answer.functionName, cause: cause
})
});
},
function (answer) {
$log.warn("Error server-side logging failed");
console.log(answer);
});
} catch (loggingError) {
$log.warn("Error server-side logging failed");
$log.log(loggingError);
}
}
return (error);
}]);
}())
stackTrace
actually is array, we take it's first item an then parse it to obtain bundle's url(location
variable) and position of error(lineNumber
and columnNumber
). Then based on these values we construct StackFrame
, that is available from library: https://github.com/stacktracejs/stackframe and pass it to pinpoint
method of StackTraceGPS
class, that you can find as part of already mentioned library: https://www.stacktracejs.com/. This method will return actual location of error with some additional information via argument (answer
) passed to callback function and it will contain, what we desire, i.e. "myFileName.js at line 123, column 34;". And that is all, problem is completely solved. Sure, this approach will still work at case of development environment, thought will be redundant.
What to install
- AspNetBundling: https://www.nuget.org/packages/AspNetBundling/
- StackFrame.js: https://github.com/stacktracejs/stackframe
- StackTrace.js: https://www.stacktracejs.com/
-
Conclusions
Logging errors - is very common task, with which any developer faced. Collecting client side errors is very important and necessary thing, fortunately, we have a lot of hooks for this, like: windows.onerror
handler or $exceptionHandler
at case of applying AngularJS
framework. With the help of StackTrace.js library we have possibility to restore error's stack trace, but it's not enough. At production environment we frequently use bundles with minification, these fact leads to lose actual(expected) error's location and it becomes a real problem. To solve it, we should create .map
projection (AspNetBundling will do this work) for our bundle
and then call it with the help of StackTrace-gps.js library to find corresponding error's location at actual file.
You can simple use this approach at case of another frameworks via their error's handlers, but creation of stack trace and solving bundle and minification problem will remain the same.