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)
Tom Seldon
  • 1,023
  • 9
  • 10

6 Answers6

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.

Community
  • 1
  • 1
Marc Durdin
  • 1,675
  • 2
  • 20
  • 27
  • 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). – tandrewnichols Jul 29 '16 at 15:17
  • Nice! This works for me, the timeout brings it outside of Angular's control and Chrome does the rest. – Angry Dan Nov 24 '16 at 15:59
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.

Jakub Hampl
  • 39,863
  • 10
  • 77
  • 106
  • 2
    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. – jcoffland Dec 29 '14 at 01:19
  • Works for me in Firefox, fantastic, the angular stack traces have been bugging me for ages. – jazmit Oct 29 '15 at 11:56
  • If you don't mind the exception being logged twice in the console, you can include a [smaller, lighter snippet of code](http://stackoverflow.com/a/33991279/1836776). – Marc Durdin Nov 30 '15 at 04:07
  • "To prevent code minifiers from destroying your angular application, you have to use the array syntax to define controllers." http://stackoverflow.com/a/20266527/1087768 – coni2k Jan 25 '16 at 02:27
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

Community
  • 1
  • 1
jraede
  • 6,846
  • 5
  • 29
  • 32
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.

Adam
  • 28,537
  • 15
  • 60
  • 73
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;
    };
  });
});
Larrifax
  • 408
  • 3
  • 9
  • Works fine in angular 1.3.8 for me http://cl.ly/image/3y0m0J3w3p1J/20150110-190523.png – Scymex Jan 10 '15 at 20:41
  • 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. – Andrew Magee Mar 07 '15 at 10:20
  • While this works in Angular 1.3.8, see my answer below for a cleaner answer, also from the same issue. – Marc Durdin Nov 30 '15 at 03:58
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);
    };
});
cdauth
  • 6,171
  • 3
  • 41
  • 49