AngularJS - Stack trace ignoring source map

2019-01-21 22:27发布

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)

6条回答
女痞
2楼-- · 2019-01-21 22:52

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.

查看更多
ゆ 、 Hurt°
3楼-- · 2019-01-21 22:55

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;
    };
  });
});
查看更多
姐就是有狂的资本
4楼-- · 2019-01-21 23:03

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

查看更多
萌系小妹纸
5楼-- · 2019-01-21 23:04

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.

查看更多
【Aperson】
6楼-- · 2019-01-21 23:07

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.

查看更多
仙女界的扛把子
7楼-- · 2019-01-21 23:14

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);
    };
});
查看更多
登录 后发表回答