sentry-web前端异常堆栈计算功能逻辑解析

3,422 阅读10分钟

什么是sentry

Sentry 是一个实时事件日志记录和汇集的平台。其专注于错误监控以及提取一切事后处理所需信息而不依赖于麻烦的用户反馈。它分为客户端和服务端,客户端(目前客户端有Javascript,Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web页方便查看。Sentry由python编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest等。

sentry的集成与使用,推荐到sentry官网查询与学习,本篇文章只对其前端异常堆栈计算的核心逻辑进行梳理

sentry前端错误收集原理(针对旧版sentry js的SDK raven-js)

sentry实现前端错误监控,通过对window.onerror、window.onunhandledrejection、计时器,延时器,对requestAnimationFrame,对浏览器中可能存在的基于发布订阅模式进行回调处理的函数进行包装重写,将前端未进行异常处理的错误,通过 'vendor/TraceKit/traceKit.js' 进行兼容处理,统一不同浏览器环境下错误对象的差异(chrome,firefox,ie),输出统一的 stacktrace后,重新整理数据结构。再将最后处理过后的信息提交给sentry服务端处理

sentry前端上报核心文件

  1. 'src/raven.js' // 上报数据完整逻辑
  2. 'vendor/TraceKit/traceKit.js' // 堆栈错误计算(统一浏览器差异,是一个npm包,可以直接通过npm安装)
  3. 'src/utils.js' // 工具方法

sentry核心处理逻辑

文件入口

使用raven-js导出的类Raven,调用其install方法初始化sentry

  1. install方法首先进行了防止重复初始化处理,首次初始化时,调用TraceKit.report.subscribe对window.onerror进行了劫持,增加了一个钩子,重写了window.onerror方法,如果原window.onerror方法存在,在原onerror回调前调用了 Raven._handleOnErrorStackInfo方法,然后调用原onerror回调.
    TraceKit.report.subscribe(function() {
        self._handleOnErrorStackInfo.apply(self, arguments);
    });
    
  2. 对未捕获的promise rejection ,添加异常捕获
    if (self._globalOptions.captureUnhandledRejections // 为true) {
        self._attachPromiseRejectionHandler();
    }
    
  3. 对计时器函数、event target进行包装,对这些函数内部出现错误进行异常捕获,主要包装逻辑类似于中间件原理,重写原方法,在执行完错误捕获逻辑后,再调用源代码,捕获逻辑都是将原回调函数使用try catch包裹,对错误进行捕获,捕获到之后,通过captureException处理收集错误,发送给sentry后端服务器. 之后把原错误抛出(注意,在这里,Raven类有一个静态属性_ignoreOnerror,每次对错误进行捕获之后,会改变该状态,通过setTimeout在本次事件循环之后,重置_ignoreOnerror,防止新抛出的错误重复触发错误收集)
    if (self._globalOptions.instrument && self._globalOptions.instrument.tryCatch // true) {
        self._instrumentTryCatch();
    }
    
    关键方法fill方法,取自utils.fill 参数track是Raven类静态属性Raven._wrappedBuiltIns:[],作用是在卸载sentry SDK时,用来还原代码
    function fill(obj, name, replacement, track) {
      if (obj == null) return;
      var orig = obj[name];
      obj[name] = replacement(orig);
      obj[name].__raven__ = true;
      obj[name].__orig__ = orig;
      if (track) {
        track.push([obj, name, orig]);
      }
    }
    
    1. _instrumentTryCatch中对setTimeout的包装
        fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns);
    
    1. _instrumentTryCatch中对setInterval的包装
        fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns);
    
    1. 如果浏览器支持requestAnimationFrame,对requestAnimationFrame进行包装
    if (_window.requestAnimationFrame) {
      fill(
        _window,
        'requestAnimationFrame',
        function(orig) {
          return function(cb) {
            return orig(
              self.wrap(
                {
                  mechanism: {
                    type: 'instrument',
                    data: {
                      function: 'requestAnimationFrame',
                      handler: (orig && orig.name) || '<anonymous>'
                    }
                  }
                },
                cb
              )
            );
          };
        },
        wrappedBuiltIns
      );
    }
    
    1. _instrumentTryCatch会检测全局对象是否有以下属性,并检测以下属性是否有发布订阅接口,如果存在发布订阅接口,将重写对应发布订阅接口(通过检测是否有'addEventlistener属性'和'removeEventListener'),在对应回调调用时,对调用过程中的错误进行监控上报.
    var eventTargets = [
      'EventTarget',
      'Window',
      'Node',
      'ApplicationCache',
      'AudioTrackList',
      'ChannelMergerNode',
      'CryptoOperation',
      'EventSource',
      'FileReader',
      'HTMLUnknownElement',
      'IDBDatabase',
      'IDBRequest',
      'IDBTransaction',
      'KeyOperation',
      'MediaController',
      'MessagePort',
      'ModalWindow',
      'Notification',
      'SVGElementInstance',
      'Screen',
      'TextTrack',
      'TextTrackCue',
      'TextTrackList',
      'WebSocket',
      'WebSocketWorker',
      'Worker',
      'XMLHttpRequest',
      'XMLHttpRequestEventTarget',
      'XMLHttpRequestUpload'
    ];
    for (var i = 0; i < eventTargets.length; i++) {
      wrapEventTarget(eventTargets[i]);
    }
    
    wrapEventTarget详细代码
    function wrapEventTarget(global) {
      var proto = _window[global] && _window[global].prototype;
      if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) {
        fill(
          proto,
          'addEventListener',
          function(orig) {
            return function(evtName, fn, capture, secure) {
              // preserve arity
              try {
                if (fn && fn.handleEvent) {
                  fn.handleEvent = self.wrap(
                    {
                      mechanism: {
                        type: 'instrument',
                        data: {
                          target: global,
                          function: 'handleEvent',
                          handler: (fn && fn.name) || '<anonymous>'
                        }
                      }
                    },
                    fn.handleEvent
                  );
                }
              } catch (err) {
                // can sometimes get 'Permission denied to access property "handle Event'
              }
    
              // More breadcrumb DOM capture ... done here and not in `_instrumentBreadcrumbs`
              // so that we don't have more than one wrapper function
              var before, clickHandler, keypressHandler;
    
              if (
                autoBreadcrumbs &&
                autoBreadcrumbs.dom &&
                (global === 'EventTarget' || global === 'Node')
              ) {
                // NOTE: generating multiple handlers per addEventListener invocation, should
                //       revisit and verify we can just use one (almost certainly)
                clickHandler = self._breadcrumbEventHandler('click');
                keypressHandler = self._keypressEventHandler();
                before = function(evt) {
                  // need to intercept every DOM event in `before` argument, in case that
                  // same wrapped method is re-used for different events (e.g. mousemove THEN click)
                  // see #724
                  if (!evt) return;
    
                  var eventType;
                  try {
                    eventType = evt.type;
                  } catch (e) {
                    // just accessing event properties can throw an exception in some rare circumstances
                    // see: https://github.com/getsentry/raven-js/issues/838
                    return;
                  }
                  if (eventType === 'click') return clickHandler(evt);
                  else if (eventType === 'keypress') return keypressHandler(evt);
                };
              }
              return orig.call(
                this,
                evtName,
                self.wrap(
                  {
                    mechanism: {
                      type: 'instrument',
                      data: {
                        target: global,
                        function: 'addEventListener',
                        handler: (fn && fn.name) || '<anonymous>'
                      }
                    }
                  },
                  fn,
                  before
                ),
                capture,
                secure
              );
            };
          },
          wrappedBuiltIns
        );
        fill(
          proto,
          'removeEventListener',
          function(orig) {
            return function(evt, fn, capture, secure) {
              try {
                fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);
              } catch (e) {
                // ignore, accessing __raven_wrapper__ will throw in some Selenium environments
              }
              return orig.call(this, evt, fn, capture, secure);
            };
          },
          wrappedBuiltIns
        );
      }
    }
    

对捕获到的错误对象进行处理

捕获到的错误通过Raven.captureException方法进行处理,在该方法中会对错误类型进行判断,错误对象的判断通过utils内部的方法进行判断,原理是调用Object.property.toString.call方法,将各错误对象转化为字符串,来确定错误类型

  • 对于 [object ErrorEvent] [object Error] [object Exception] 错误对象,直接使用 TraceKit.computeStackTrace(统一跨浏览器的堆栈跟踪信息)方法 进行异常的堆栈跟踪,对于 [object Object] 非错误对象,进行兼容后再使用 TraceKit.computeStackTrace方法 进行异常的堆栈跟踪.

    else if (isPlainObject(ex)) {
      options = this._getCaptureExceptionOptionsFromPlainObject(options, ex);
      ex = new Error(options.message);
    }
    

    对[object Object]的兼容

    _getCaptureExceptionOptionsFromPlainObject: function(currentOptions, ex) {
        var exKeys = Object.keys(ex).sort();
        var options = objectMerge(currentOptions, {
          message:
            'Non-Error exception captured with keys: ' + serializeKeysForMessage(exKeys),
          fingerprint: [md5(exKeys)],
          extra: currentOptions.extra || {}
        });
        options.extra.__serialized__ = serializeException(ex);
    
        return options;
      }
    

    对异常进行堆栈跟踪计算

    try {
      var stack = TraceKit.computeStackTrace(ex);
      this._handleStackInfo(stack, options);
    } catch (ex1) {
      if (ex !== ex1) {
        throw ex1;
      }
    }
    

    计算结果传递给Raven._handleStackInfo方法再次进行数据处理

    _handleStackInfo: function(stackInfo, options) {
        var frames = this._prepareFrames(stackInfo, options);
    
        this._triggerEvent('handle', {
          stackInfo: stackInfo,
          options: options
        });
    
        this._processException(
          stackInfo.name,
          stackInfo.message,
          stackInfo.url,
          stackInfo.lineno,
          frames,
          options
        );
    },
    
    1. Raven._prepareFrames方法,处理堆栈错误,确认该堆栈错误是否是应用内部错误,并初步处理stacktrace.frames

      _prepareFrames: function(stackInfo, options) {
          var self = this;
          var frames = [];
          if (stackInfo.stack && stackInfo.stack.length) {
            each(stackInfo.stack, function(i, stack) {
              var frame = self._normalizeFrame(stack, stackInfo.url);
              if (frame) {
                frames.push(frame);
              }
            });
      
            // e.g. frames captured via captureMessage throw
            if (options && options.trimHeadFrames) {
              for (var j = 0; j < options.trimHeadFrames && j < frames.length; j++) {
                frames[j].in_app = false;
              }
            }
          }
          frames = frames.slice(0, this._globalOptions.stackTraceLimit);
          return frames;
        },
      
    2. Raven._processException方法将堆栈信息结构重新整理,处理的最终结果就是上报的最终信息,通过Raven._send方法发送给sentry后端服务

      _processException: function(type, message, fileurl, lineno, frames, options) {
          var prefixedMessage = (type ? type + ': ' : '') + (message || '');
          if (
            !!this._globalOptions.ignoreErrors.test &&
            (this._globalOptions.ignoreErrors.test(message) ||
              this._globalOptions.ignoreErrors.test(prefixedMessage))
          ) {
            return;
          }
      
          var stacktrace;
      
          if (frames && frames.length) {
            fileurl = frames[0].filename || fileurl;
            // Sentry expects frames oldest to newest
            // and JS sends them as newest to oldest
            frames.reverse();
            stacktrace = {frames: frames};
          } else if (fileurl) {
            stacktrace = {
              frames: [
                {
                  filename: fileurl,
                  lineno: lineno,
                  in_app: true
                }
              ]
            };
          }
      
          if (
            !!this._globalOptions.ignoreUrls.test &&
            this._globalOptions.ignoreUrls.test(fileurl)
          ) {
            return;
          }
      
          if (
            !!this._globalOptions.whitelistUrls.test &&
            !this._globalOptions.whitelistUrls.test(fileurl)
          ) {
            return;
          }
      
          var data = objectMerge(
            {
              // sentry.interfaces.Exception
              exception: {
                values: [
                  {
                    type: type,
                    value: message,
                    stacktrace: stacktrace
                  }
                ]
              },
              transaction: fileurl
            },
            options
          );
      
          var ex = data.exception.values[0];
          if (ex.type == null && ex.value === '') {
            ex.value = 'Unrecoverable error caught';
          }
      
          // Move mechanism from options to exception interface
          // We do this, as requiring user to pass `{exception:{mechanism:{ ... }}}` would be
          // too much
          if (!data.exception.mechanism && data.mechanism) {
            data.exception.mechanism = data.mechanism;
            delete data.mechanism;
          }
      
          data.exception.mechanism = objectMerge(
            {
              type: 'generic',
              handled: true
            },
            data.exception.mechanism || {}
          );
      
          // Fire away!
          this._send(data); // 发送数据
        },
      
  • 对于 [object DOMError] 和 [object DOMException]错误对象,通过Raven.captureMessage方法进行处理,判断该错误对象是否为需要忽略的错误(是否需要忽略的错误列表在sentry配置时设置),如果不是,再调用 TraceKit.computeStackTrace方法进行堆栈计算,计算结果通过Raven._prepareFrames进行处理然后发送给sentry后端服务

    else if (isDOMError(ex) || isDOMException(ex)) {
      var name = ex.name || (isDOMError(ex) ? 'DOMError' : 'DOMException');
      var message = ex.message ? name + ': ' + ex.message : name;
    
      return this.captureMessage(
        message,
        objectMerge(options, {
          stacktrace: true,
          trimHeadFrames: options.trimHeadFrames + 1
        })
      ); 
    }
    
    captureMessage: function(msg, options) {
        // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an
        // early call; we'll error on the side of logging anything called before configuration since it's
        // probably something you should see:
        if (
          !!this._globalOptions.ignoreErrors.test &&
          this._globalOptions.ignoreErrors.test(msg)
        ) {
          return;
        }
    
        options = options || {};
        msg = msg + ''; // Make sure it's actually a string
    
        var data = objectMerge(
          {
            message: msg
          },
          options
        );
    
        var ex;
        // Generate a "synthetic" stack trace from this point.
        // NOTE: If you are a Sentry user, and you are seeing this stack frame, it is NOT indicative
        //       of a bug with Raven.js. Sentry generates synthetic traces either by configuration,
        //       or if it catches a thrown object without a "stack" property.
        try {
          throw new Error(msg);
        } catch (ex1) {
          ex = ex1;
        }
    
        // null exception name so `Error` isn't prefixed to msg
        ex.name = null;
        var stack = TraceKit.computeStackTrace(ex);
    
        // stack[0] is `throw new Error(msg)` call itself, we are interested in the frame that was just before that, stack[1]
        var initialCall = isArray(stack.stack) && stack.stack[1];
    
        // if stack[1] is `Raven.captureException`, it means that someone passed a string to it and we redirected that call
        // to be handled by `captureMessage`, thus `initialCall` is the 3rd one, not 2nd
        // initialCall => captureException(string) => captureMessage(string)
        if (initialCall && initialCall.func === 'Raven.captureException') {
          initialCall = stack.stack[2];
        }
    
        var fileurl = (initialCall && initialCall.url) || '';
    
        if (
          !!this._globalOptions.ignoreUrls.test &&
          this._globalOptions.ignoreUrls.test(fileurl)
        ) {
          return;
        }
    
        if (
          !!this._globalOptions.whitelistUrls.test &&
          !this._globalOptions.whitelistUrls.test(fileurl)
        ) {
          return;
        }
    
        // Always attempt to get stacktrace if message is empty.
        // It's the only way to provide any helpful information to the user.
        if (this._globalOptions.stacktrace || options.stacktrace || data.message === '') {
          // fingerprint on msg, not stack trace (legacy behavior, could be revisited)
          data.fingerprint = data.fingerprint == null ? msg : data.fingerprint;
    
          options = objectMerge(
            {
              trimHeadFrames: 0
            },
            options
          );
          // Since we know this is a synthetic trace, the top frame (this function call)
          // MUST be from Raven.js, so mark it for trimming
          // We add to the trim counter so that callers can choose to trim extra frames, such
          // as utility functions.
          options.trimHeadFrames += 1;
    
          var frames = this._prepareFrames(stack, options);
          data.stacktrace = {
            // Sentry expects frames oldest to newest
            frames: frames.reverse()
          };
        }
    
        // Make sure that fingerprint is always wrapped in an array
        if (data.fingerprint) {
          data.fingerprint = isArray(data.fingerprint)
            ? data.fingerprint
            : [data.fingerprint];
        }
    
        // Fire away!
        this._send(data); // 最终发送给后端的数据
    
        return this;
      },
    

sentry 后端处理逻辑(python部署)

python核心处理逻辑

上报数据完整结构

{
    "project":"<project>",
    "logger":"javascript",
    "platform":"javascript",
    "request":{
        "headers":{
            "User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
        },
        "url":"http://fast-dev.mypaas.com.cn:8000/performance/api_status"
    },
    "exception":{
        "values":[
            {
                "type":"ReferenceError",
                "value":"a is not defined",
                "stacktrace":{
                    "frames":[
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.js",
                            "lineno":11458,
                            "colno":22,
                            "function":"?",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":274008,
                            "colno":16,
                            "function":"DynamicComponent.umi../node_modules/react/cjs/react.development.js.Component.setState",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":258691,
                            "colno":5,
                            "function":"Object.enqueueSetState",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":264963,
                            "colno":5,
                            "function":"scheduleWork",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":265154,
                            "colno":5,
                            "function":"requestWork",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":265285,
                            "colno":3,
                            "function":"performSyncWork",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":265311,
                            "colno":7,
                            "function":"performWork",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":265399,
                            "colno":7,
                            "function":"performWorkOnRoot",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":264510,
                            "colno":7,
                            "function":"renderRoot",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":264424,
                            "colno":24,
                            "function":"workLoop",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":264384,
                            "colno":12,
                            "function":"performUnitOfWork",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":261569,
                            "colno":16,
                            "function":"beginWork",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":260723,
                            "colno":24,
                            "function":"updateClassComponent",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                            "lineno":260768,
                            "colno":31,
                            "function":"finishClassComponent",
                            "in_app":true
                        },
                        {
                            "filename":"http://fast-dev.mypaas.com.cn:8000/5.async.js",
                            "lineno":460,
                            "colno":12,
                            "function":"Index.render",
                            "in_app":true
                        }
                    ]
                }
            }
        ],
        "mechanism":{
            "type":"onunhandledrejection",
            "handled":false
        }
    },
    "transaction":"http://fast-dev.mypaas.com.cn:8000/5.async.js",
    "trimHeadFrames":0,
    "extra":{
        "session:duration":2768
    },
    "breadcrumbs":{
        "values":[
            {
                "timestamp":1550721477.676,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477448",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721477.729,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477441",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721477.76,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477443",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721477.858,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477456",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721478.015,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477438",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721478.16,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477445",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721478.445,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"POST",
                    "url":"/api/analysis/data?t=1550721477463",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721480.038,
                "category":"navigation",
                "data":{
                    "to":"/performance/api_status",
                    "from":"/overview"
                }
            },
            {
                "timestamp":1550721480.092,
                "category":"ui.click",
                "message":"li.ant-menu-item.ant-menu-item-active.ant-menu-item-selected > a.active"
            },
            {
                "timestamp":1550721480.114,
                "category":"sentry",
                "message":"ReferenceError: a is not defined",
                "event_id":"50931700539c491691c6ddd707cd587c",
                "level":"error"
            },
            {
                "timestamp":1550721480.149,
                "message":"The above error occurred in the <Index> component:
in Index (created by WithAppInfo)
in WithAppInfo (created by Connect(WithAppInfo))
in Connect(WithAppInfo) (created by DynamicComponent)
in DynamicComponent (created by Route)
in Route (created by Route)
in Switch (created by Route)
in Route (created by Route)
in Switch (created by Route)
in div (created by PrimaryContent)
in PrimaryContent (created by PrimaryLayout)
in div (created by PrimaryLayout)
in div (created by PrimaryLayout)
in PrimaryLayout (created by Connect(PrimaryLayout))
in Connect(PrimaryLayout) (created by LoadProfile)
in LoadProfile (created by Connect(LoadProfile))
in Connect(LoadProfile) (created by BaseLayout)
in div (created by BaseLayout)
in BaseLayout (created by Connect(BaseLayout))
in Connect(BaseLayout) (created by DynamicComponent)
in DynamicComponent (created by Route)
in Route (created by RouterWrapper)
in Switch (created by RouterWrapper)
in Router (created by ConnectedRouter)
in ConnectedRouter (created by RouterWrapper)
in RouterWrapper
in Provider (created by DvaContainer)
in DvaContainer

Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://fb.me/react-error-boundaries to learn more about error boundaries.",
                "level":"error",
                "category":"console"
            },
            {
                "timestamp":1550721480.154,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"GET",
                    "url":"http://fast-dev.mypaas.com.cn:8000/5.async.js",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721480.161,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"GET",
                    "url":"http://fast-dev.mypaas.com.cn:8000/umi.dll.js",
                    "status_code":200
                }
            },
            {
                "timestamp":1550721480.164,
                "type":"http",
                "category":"fetch",
                "data":{
                    "method":"GET",
                    "url":"http://fast-dev.mypaas.com.cn:8000/umi.js",
                    "status_code":200
                }
            }
        ]
    },
    "event_id":"a033c918aaec4a06b430e85d7a551ab1"
}

TraceKit包计算的堆栈错误输出 TraceKit.computeStackTrace(err)

{
    "name":"Error",
    "message":"oops",
    "url":"http://localhost:3002/",
    "stack":[
        {
            "url":"webpack:///./vendor/TraceKit/tracekit.js?",
            "line":282,
            "func":"?"
        },
        {
            "url":"webpack:///./src/index.js?",
            "func":"eval",
            "args":[

            ],
            "line":200,
            "column":9
        },
        {
            "url":"http://localhost:3002/bundle.js?dafa8b28e39d5dc07bc8",
            "func":"Module../src/index.js",
            "args":[

            ],
            "line":461,
            "column":1
        },
        {
            "url":"http://localhost:3002/bundle.js?dafa8b28e39d5dc07bc8",
            "func":"__webpack_require__",
            "args":[

            ],
            "line":20,
            "column":30
        },
        {
            "url":"webpack:///multi_(webpack)-dev-server/client?",
            "func":"eval",
            "args":[

            ],
            "line":2,
            "column":18
        },
        {
            "url":"http://localhost:3002/bundle.js?dafa8b28e39d5dc07bc8",
            "func":"Object.0",
            "args":[

            ],
            "line":505,
            "column":1
        },
        {
            "url":"http://localhost:3002/bundle.js?dafa8b28e39d5dc07bc8",
            "func":"__webpack_require__",
            "args":[

            ],
            "line":20,
            "column":30
        },
        {
            "url":"http://localhost:3002/bundle.js?dafa8b28e39d5dc07bc8",
            "func":"?",
            "args":[

            ],
            "line":84,
            "column":18
        },
        {
            "url":"http://localhost:3002/bundle.js?dafa8b28e39d5dc07bc8",
            "func":"?",
            "args":[

            ],
            "line":87,
            "column":10
        }
    ],
    "incomplete":false,
    "partial":true
}