AngularJS - Stack trace ignoring source map
Asked Answered
M

6

30

I've written an AngularJS app but it's proving a bit of a nightmare to debug. I'm using Grunt + uglify to concatenate and minify my application code. It also creates a source map alongside the minified JS file.

The source map seems to work properly when there is a JS error in the file, but outside of the AngularJS application. e.g. If I write console.log('a.b'); at the top of one of the files, the error logged in the Chrome debugger displays line + file info for the original file, not the minified one.

The problem occurs when there is a problem with code that Angular runs itself (e.g. in Controller code). I get a nice stack trace from Angular, but it only details the minified file not the original.

Is there anything I can do to get Angular to acknowledge the source map?

Example error below:

TypeError: Cannot call method 'getElement' of undefined
at Object.addMapControls (http://my-site/wp-content/plugins/my-maps/assets/js/app.min.js:1:2848)
at Object.g [as init] (http://my-site/wp-content/plugins/my-maps/assets/js/app.min.js:1:344)
at new a (http://my-site/wp-content/plugins/my-maps/assets/js/app.min.js:1:591)
at d (http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js:29:495)
at Object.instantiate (http://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js:30:123)
Militant answered 17/10, 2013 at 7:24 Comment(0)
P
10

Larrifax's answer is good but there is an improved version of the function documented in the same issue report:

.config(function($provide) {

  // Fix sourcemaps
  // @url https://github.com/angular/angular.js/issues/5217#issuecomment-50993513
  $provide.decorator('$exceptionHandler', function($delegate) {
    return function(exception, cause) {
      $delegate(exception, cause);
      setTimeout(function() {
        throw exception;
      });
    };
  });
})

This will generate two stack traces, as Andrew Magee noted: one formatted by Angular, then a second one formatted by the browser. The second trace will apply sourcemaps. It's probably not a great idea to disable the duplicates, because you may have other Angular modules that also do work with exceptions that could be called after this via the delegation.

Persuasion answered 30/11, 2015 at 4:5 Comment(2)
The setTimeout is critical here. I was trying Larrifax's original hack, but was getting other unrelated errors (because throwing the error interrupts angular, presumably).Toothless
Nice! This works for me, the timeout brings it outside of Angular's control and Chrome does the rest.Dobruja
R
9

The only solution I could find is to bite the bullet and parse the source maps yourself. Here is some code that will do this. First you need to add source-map to your page. Then add this code:

angular.module('Shared').factory('$exceptionHandler', 
function($log, $window, $injector) {
  var getSourceMappedStackTrace = function(exception) {
    var $q = $injector.get('$q'),
        $http = $injector.get('$http'),
        SMConsumer = window.sourceMap.SourceMapConsumer,
        cache = {};

    // Retrieve a SourceMap object for a minified script URL
    var getMapForScript = function(url) {
      if (cache[url]) {
        return cache[url];
      } else {
        var promise = $http.get(url).then(function(response) {
          var m = response.data.match(/\/\/# sourceMappingURL=(.+\.map)/);
          if (m) {
            var path = url.match(/^(.+)\/[^/]+$/);
            path = path && path[1];
            return $http.get(path + '/' + m[1]).then(function(response) {
              return new SMConsumer(response.data);
            });
          } else {
            return $q.reject();
          }
        });
        cache[url] = promise;
        return promise;
      }
    };

    if (exception.stack) { // not all browsers support stack traces
      return $q.all(_.map(exception.stack.split(/\n/), function(stackLine) {
        var match = stackLine.match(/^(.+)(http.+):(\d+):(\d+)/);
        if (match) {
          var prefix = match[1], url = match[2], line = match[3], col = match[4];
          return getMapForScript(url).then(function(map) {
            var pos = map.originalPositionFor({
              line: parseInt(line, 10), 
              column: parseInt(col, 10)
            });
            var mangledName = prefix.match(/\s*(at)?\s*(.*?)\s*(\(|@)/);
            mangledName = (mangledName && mangledName[2]) || '';
            return '    at ' + (pos.name ? pos.name : mangledName) + ' ' + 
              $window.location.origin + pos.source + ':' + pos.line + ':' + 
              pos.column;
          }, function() {
            return stackLine;
          });
        } else {
          return $q.when(stackLine);
        }
      })).then(function (lines) {
        return lines.join('\n');
      });
    } else {
      return $q.when('');
    }
  };

  return function(exception) {
    getSourceMappedStackTrace(exception).then($log.error);
  };
});

This code will download the source, then download the sourcemaps, parse them, and finally attempt to replace the locations in the stack trace the mapped locations. This works perfectly in Chrome, and quite acceptably in Firefox. The disadvantage is that you are adding a fairly large dependency to your code base and that you move from very fast synchronous error reporting to fairly slow async error reporting.

Resee answered 1/8, 2014 at 13:45 Comment(4)
This worked for me, in Chrome. I changed _.map to $.map so that it uses jQuery instead of underscore.js. I already had a jQuery dependency and didn't want to add underscore.js.Obstructionist
Works for me in Firefox, fantastic, the angular stack traces have been bugging me for ages.Predict
If you don't mind the exception being logged twice in the console, you can include a smaller, lighter snippet of code.Persuasion
"To prevent code minifiers from destroying your angular application, you have to use the array syntax to define controllers." https://mcmap.net/q/500819/-minification-is-breaking-my-angularjs-codeDevotee
E
6

I just had the same issue and have been hunting around for a solution - apparently it's a Chrome issue with stack traces in general and happens to apply to Angular because it uses stack traces in error reporting. See:

Will the source mapping in Google Chrome push to Error.stack

Exosmosis answered 9/11, 2013 at 20:28 Comment(1)
And the bug is now fixed in chromium 42 :) Cf code.google.com/p/chromium/issues/detail?id=357958 .Pursuer
O
3

I would take a look at the following project: https://github.com/novocaine/sourcemapped-stacktrace

It does essentially the same thing as the answer from @jakub-hampl but might be useful.

Osmose answered 15/8, 2014 at 18:0 Comment(1)
Have you tried it? It doesn't seem to work for me.. It doesn't work from inside any angular module/directive/etc codeNance
M
0

According to this issue it seems that Angular's $logProvider breaks sourcemapping. A workaround like this is suggested in the issue:

var module = angular.module('source-map-exception-handler', [])

module.config(function($provide) {
  $provide.decorator('$exceptionHandler', function($delegate) {
    return function(exception, cause) {
        $delegate(exception, cause);
        throw exception;
    };
  });
});
Mycorrhiza answered 3/9, 2014 at 11:4 Comment(3)
Works fine in angular 1.3.8 for me cl.ly/image/3y0m0J3w3p1J/20150110-190523.pngToffee
Kind of worked for me. As-is, I will see both the yucky stack trace (sometimes several times) and a good stack trace. If I comment out $delegate(exception, cause) then I just get the good stack trace.Cahan
While this works in Angular 1.3.8, see my answer below for a cleaner answer, also from the same issue.Persuasion
M
0

As the bug has been fixed in Chrome (but the issue persists in Angular), a workaround that doesn’t print out the stack trace twice would be this:

app.factory('$exceptionHandler', function() {
    return function(exception, cause) {
        console.error(exception.stack);
    };
});
Minoru answered 30/10, 2016 at 10:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.