Sentry的异样数据处理流程

获取异样

在 Sentry的前端异样监控计划中之前咱们说过,Sentry的全局异样获取形式有2种,window.onerror以及unhandledrejection。

异样数据的解决

以unhandledrejection为例 globalhandlers.ts中

    addInstrumentationHandler({      // eslint-disable-next-line @typescript-eslint/no-explicit-any      callback: (e: any) => {        let error = e;        // dig the object of the rejection out of known event types        try {          // PromiseRejectionEvents store the object of the rejection under 'reason'          // see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent          if ('reason' in e) {            error = e.reason;          }          // something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents          // to CustomEvents, moving the `promise` and `reason` attributes of the PRE into          // the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec          // see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and          // https://github.com/getsentry/sentry-javascript/issues/2380          else if ('detail' in e && 'reason' in e.detail) {            error = e.detail.reason;          }        } catch (_oO) {          // no-empty        }        const currentHub = getCurrentHub();        const hasIntegration = currentHub.getIntegration(GlobalHandlers);        const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;        // addEventListener的事件中间接throw Error的时候,shouldIgnoreOnError会为true.即不会上报        // 起因是在instrumentDOM办法中,用globalDOMEventHandler办法对监听事件包了一层,使得ignoreOnError>0        if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {          return true;        }        const client = currentHub.getClient();        const event = isPrimitive(error)          ? this._eventFromRejectionWithPrimitive(error)          : eventFromUnknownInput(error, undefined, {              attachStacktrace: client && client.getOptions().attachStacktrace,              rejection: true,            });        event.level = Severity.Error;        addExceptionMechanism(event, {          handled: false,          type: 'onunhandledrejection',        });        currentHub.captureEvent(event, {          originalException: error,        });        return;      },      type: 'unhandledrejection',    }); 

整体流程分为以下几个模块解说

兼容解决

        try {          // PromiseRejectionEvents store the object of the rejection under 'reason'          // see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent          if ('reason' in e) {            error = e.reason;          }          // something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents          // to CustomEvents, moving the `promise` and `reason` attributes of the PRE into          // the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec          // see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and          // https://github.com/getsentry/sentry-javascript/issues/2380          else if ('detail' in e && 'reason' in e.detail) {            error = e.detail.reason;          }        } catch (_oO) {          // no-empty        }

这一块都是在做兼容性解决,适配不同浏览器中的谬误类型

过滤条件

          const currentHub = getCurrentHub();        const hasIntegration = currentHub.getIntegration(GlobalHandlers);        const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;        if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {          return true;        }

这里的hasIntergration是在判断是否装置了对应的全局监听函数的插件(初始化的时候就会默认加载)
shouldIgnoreOnError : addEventListener的事件中间接throw Error的时候,shouldIgnoreOnError会为true.即不会上报
起因是在instrumentDOM办法中,用globalDOMEventHandler办法对监听事件包了一层,使得ignoreOnError>0
isFailedOwnDelivery即判断是否为Sentry本身的申请谬误,若是,则不上报。

外围解决逻辑

        const client = currentHub.getClient();        const event = isPrimitive(error)          ? this._eventFromRejectionWithPrimitive(error)          : eventFromUnknownInput(error, undefined, {              attachStacktrace: client && client.getOptions().attachStacktrace,              rejection: true,            });
export function isPrimitive(wat: any): wat is Primitive {  return wat === null || (typeof wat !== 'object' && typeof wat !== 'function');}

isPrimitive(error)即在判断error是否为原始数据类型
若为原始数据类型,则解决比较简单

  private _eventFromRejectionWithPrimitive(reason: Primitive): Event {    return {      exception: {        values: [          {            type: 'UnhandledRejection',            // String() is needed because the Primitive type includes symbols (which can't be automatically stringified)            value: `Non-Error promise rejection captured with value: ${String(reason)}`,          },        ],      },    };  }

若为援用数据类型,外围处理函数在tracekit.ts为

    stack = computeStackTraceFromStacktraceProp(ex);    if (stack) {      return popFrames(stack, popSize);    }

具体就在computeStackTraceFromStacktraceProp,popFrames这2个函数中。

function computeStackTraceFromStackProp(ex: any): StackTrace | null {  if (!ex || !ex.stack) {    return null;  }  const stack = [];  const lines = ex.stack.split('\n');  let isEval;  let submatch;  let parts;  let element;  for (let i = 0; i < lines.length; ++i) {    if ((parts = chrome.exec(lines[i]))) {      const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line      isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line      if (isEval && (submatch = chromeEval.exec(parts[2]))) {        // throw out eval line/column and use top-most line/column number        parts[2] = submatch[1]; // url        parts[3] = submatch[2]; // line        parts[4] = submatch[3]; // column      }      element = {        // working with the regexp above is super painful. it is quite a hack, but just stripping the `address at `        // prefix here seems like the quickest solution for now.        url: parts[2] && parts[2].indexOf('address at ') === 0 ? parts[2].substr('address at '.length) : parts[2],        func: parts[1] || UNKNOWN_FUNCTION,        args: isNative ? [parts[2]] : [],        line: parts[3] ? +parts[3] : null,        column: parts[4] ? +parts[4] : null,      };    } else if ((parts = winjs.exec(lines[i]))) {      element = {        url: parts[2],        func: parts[1] || UNKNOWN_FUNCTION,        args: [],        line: +parts[3],        column: parts[4] ? +parts[4] : null,      };    } else if ((parts = gecko.exec(lines[i]))) {      isEval = parts[3] && parts[3].indexOf(' > eval') > -1;      if (isEval && (submatch = geckoEval.exec(parts[3]))) {        // throw out eval line/column and use top-most line number        parts[1] = parts[1] || `eval`;        parts[3] = submatch[1];        parts[4] = submatch[2];        parts[5] = ''; // no column when eval      } else if (i === 0 && !parts[5] && ex.columnNumber !== void 0) {        // FireFox uses this awesome columnNumber property for its top frame        // Also note, Firefox's column number is 0-based and everything else expects 1-based,        // so adding 1        // NOTE: this hack doesn't work if top-most frame is eval        stack[0].column = (ex.columnNumber as number) + 1;      }      element = {        url: parts[3],        func: parts[1] || UNKNOWN_FUNCTION,        args: parts[2] ? parts[2].split(',') : [],        line: parts[4] ? +parts[4] : null,        column: parts[5] ? +parts[5] : null,      };    } else {      continue;    }    if (!element.func && element.line) {      element.func = UNKNOWN_FUNCTION;    }    stack.push(element);  }  if (!stack.length) {    return null;  }  return {    message: extractMessage(ex),    name: ex.name,    stack,  };}

能够看出,外围流程其实就是在对不同的浏览器做兼容解决,以及将数据组合成堆栈的模式

后续

将异样数据进行捕捉以及解决之后,就是上报流程了。详情能够参看上一篇文档 Sentry的异样数据上报机制

        currentHub.captureEvent(event, {          originalException: error,        });

总结

在大抵理解到了整个异样数据处理流程后,发现重点是在对于不同浏览器的兼容性解决上,整个链路比拟清晰,两头很多繁琐的解决细节也都没有太多的去关注,毕竟工夫和精力绝对无限,想的是在对Sentry的整体脉络有了肯定理解之后,对本人做监控能有很多启发,当然这也是钻研Sentry源码的初衷。