一、前言
最近对前端监控很有兴趣,所以去使用了前端监控里优秀的开源库Sentry,并且研究了下Sentry源码,整理出这一系列的文章,希望能帮助大家更好地了解前端监控原理。
这一系列的文章我将结合官网api与流程图的方式,通俗易懂地讲解整个流程。
以下是我已完成和计划下一篇文章的主题:
- 搞懂Sentry初始化
- Sentry如何处理错误数据(本文)
- Sentry拿到错误数据后,又是如何上报呢?
- 计划:Sentry如何集成面包屑Breadcrumbs
- 计划:Sentry如何处理session
- ...
这里给觉得看源码很困难的小伙伴一些建议:
- 推荐先看一些文章或者官网文档,并且相应的api要熟悉。然后根据主线走,支线可以大概了解是做什么的,别深究。
- 有时候可能分不清主次,这个时候可以画流程图,这也是我快速了解源码的方法。你画着画着,原本一开始看不懂的源码,可能在后续的某个地方就关联了起来。
- 最后就是要去多思考,比如:从源码中我学到了哪些优秀的写法,用到了什么设计模式,我如何应用到实际项目中,碰到bug是不是能够通过自己知道的运行流程快速定位到bug大概可能出现的位置等等。
而关于Sentry源码如何调试大家可以按照如下:
- git clone
git@github.com:getsentry/sentry-javascript.git
- 进入到 packages/browser 进行npm i 下载依赖
- 进入到 packages/browser/examples 打开index.html就可以开心调试了(这里建议用Live Server打开)
- 说明:packages/browser/examples 下的bundle.js就是打包后的源码,他指向了packages/browser/build/bundle.js。这时候你会发现build目录下还有bundle.es6.js,如果你想使用es6的方式去阅读可以将文件替换成bundle.es6.js
二、导读
我们先来了解一下前端错误的基本内容:
2.1 解前端常见的错误类型有几种:
(1) ECMAScript Execeptions
当脚本代码运行时发生的错误,Error的实例对象会被抛出,除了通用的Error构造函数外,JavaScript还有7个其他类型的错误构造函数
- SyntaxError: 语法错误指的使用了未定义或错误的语法而引发的异常。语法错误会在编译或者解析源码的过程中被检测出来。
- ReferenceError: 引用错误代表当一个不存在的变量被引用时发生的错误。
- RangeError:范围错误代表当一个值不在其所允许的范围或者集合中。
- TypeError:类型错误用来表示值的类型非预期类型时发生的错误。
- URIError: URL错误用来表示以一种错误的方式使用全局URI处理函数而产生的错误。
- EvalError:eval函数的错误代表了一个关于eval函数的错误.此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性。
- InternalError:引擎错误表示出现在JavaScript引擎内部的错误(例如:"InternalError: too much recursion"内部错误:递归过深)。
(2)DOMException
最新的DOM规范定义的错误类型集,兼容旧浏览的DOMError接口, 完善和规范化DOM错误类型。
- IndexSizeError: 索引不在允许的范围内
- HierarchyRequestError: 节点树层次结构有误。
- WrongDocumentError: 对象在错误的
Document
中。 - InvalidCharacterError*:字符串包含无效字符。
- NoModificationAllowedError: 对象不能被修改。
- NotFoundError: 找不到对象。
- NotSupportedError: 不支持的操作
- InvalidStateError: 对象是一个无效的状态。
- SyntaxError: 字符串不匹配预期的模式。
- InvalidModificationError: 对象不能被这种方式修改。
- NamespaceError: 操作在XML命名空间内是不被允许的。
- InvalidAccessError: 对象不支持这种操作或参数。
- TypeMismatchError:对象的类型不匹配预期的类型。
- SecurityError: 此操作不安全的。
- NetworkError: 发生网络错误。
- AbortError: 操作被中止。
- URLMismatchError: 给定的URL不匹配另一个URL。
- QuotaExceededError:给定配额已经超过了。
- TimeoutError: 操作超时。
- InvalidNodeTypeError: 这个操作的节点是不正确的或祖先是不正确的。
- DataCloneError: 对象不能克隆。
2.2 前端错误异常按照捕获方式分类
(1)脚本错误
脚本错误可以分为编译时错误以及运行时错误
脚本错误的捕获方式有:
-
try catch
优点:
- 通过try...catch我们能够知道出错的信息,并且也有堆栈信息可以知道在哪个文件第几行第几列发生错误。
缺点:
- 只能捕获同步代码的异常
- 捕获错误是侵入式的,作为一个监控系统是不合适的
-
window.onerror
优点:
-
能全局捕获错误
-
一样可以拿到出错的信息以及文件名、行号、列号等信息,还可以在window.onerror最后return true让浏览器不输出错误信息到控制台
/* * @param msg{String}:错误消息 * @param url{String}:引发错误的脚本的URL * @param line{Number}:发生错误的代码行 * @param colunm{Number}:发生错误的代码列 * @param error{object}:错误对象 */ window.onerror = function (msg, url, line, colunm, error) { return true; }
缺点:
- 只能捕获即时运行错误,不能捕获资源加载错误(
原理:资源加载错误,没有冒泡所以并不会向上冒泡,object.onerror捕获后就会终止,所以window.onerror并不能捕获资源加载错误
)
-
Script跨域脚本错误捕获:
为了性能方面的考虑,我们一般会将脚本文件放到 CDN ,这种方法会大大加快首屏时间。但是,如果脚本报错,此时,浏览器出于于安全方面的考虑,对于不同源的脚本报错,无法捕获到详细错误信息,只会显示 Script Error
解决方案:
- 可以在
script
标签中,添加crossorigin
属性(推荐使用webpack
插件自动添加)。 - 服务端设置js资源响应头Access-Control-Origin:*(或者是域名)
(2)资源加载错误
资源加载错误包括:img、script、link、audio、video、iframe ...
资源加载与脚本错误异同点:
相同点:
- 监控资源错误本质上和监控常规脚本错误一样,都是监控错误事件实现错误捕获。
不同点:
- 脚本错误参数对象
instanceof
ErrorEvent
,而资源错误的参数对象instanceof
Event
。(由于ErrorEvent 继承于 Event
,所以不管是脚本错误还是资源错误的参数对象,它们都 instanceof Event
,所以,需要先判断脚本错误)。 - 脚本错误的参数对象中包含
message
,而资源错误没有 。
资源加载捕获方式:
-
object.onerror:如:img标签、script标签都可以添加onerror事件,用来捕获资源加载错误
-
performance.getEntries:通过performance.getEntries可以获取网站成功加载的资源数量信息
所以通过
allIms
和loadedImgs
对比即可找出图片资源未加载项目const allImgs = document.getElementsByTagName('image') const loadedImgs = performance.getEntries().filter(i => i.initiatorType === 'img')
(3) Promise 错误
上述的try catch和window.onerror是无法捕捉Promise错误的(因为是异步)
而当 Promise
被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection
事件
当 Promise
被 reject 且有 reject 处理器的时候,会触发 rejectionhandled
事件。
说明:Sentry这边只收集没有被reject的错误即window.unhandledrejection
2.3 上报错误方式
-
采用
Ajax
通信的方式上报(Sentry采用的方式) -
img请求上报, url参数带上错误信息
比如:(new Image()).src = 'https://docs.sentry.io/error?error=error'
三、如何监听错误
我们可以先看下图,本章节将重点如讲解在Sentry初始化的时候,是如何监听错误的,对window.onerror和window.unhandledrejection都做了哪些处理
关于Sentry初始化集成内容可以参考玩转前端监控,全面解析Sentry源码(一)| 搞懂Sentry初始化
监听错误步骤如下:
3.1 初始化
用户使用Sentry.init初始化,传入参数
3.2 绑定控制中心hub
init方法会调用initAndBind去初始化了 client,并且把 client 绑定到了 hub 上,initAndBind中的bindClient方法就是把 client 绑定到了 hub 控制中心上
3.3 集成install
关注bindClient中的setupIntegrations方法
bindClient(client) {
const top = this.getStackTop();
top.client = client;
if (client && client.setupIntegrations) {
client.setupIntegrations();
}
}
setupIntegrations
setupIntegrations() {
if (this._isEnabled()) {
this._integrations = setupIntegrations(this._options);
}
}
function setupIntegrations(options) {
const integrations = {};
getIntegrationsToSetup(options).forEach(integration => {
integrations[integration.name] = integration;
setupIntegration(integration);
});
return integrations;
}
分析:
- setupIntegrations 是对集成integrations进行遍历,把默认集成或者自定义集成进行install
- getIntegrationsToSetup就是获取integrations
注意:_isEnabled是用户传入的options不能把enabled为false和dsn为空,否则不会采集发送
3.4 setupOnce
接着来看看setupIntegration
/** Setup given integration */
function setupIntegration(integration) {
if (installedIntegrations.indexOf(integration.name) !== -1) {
return;
}
integration.setupOnce(addGlobalEventProcessor, getCurrentHub);
installedIntegrations.push(integration.name);
logger.log(`Integration installed: ${integration.name}`);
}
分析:
- 这里会对集成进行遍历,执行setupOnce方法
- 默认集成有7个,我们只要关注其中的new GlobalHandlers
- 当对GlobalHandlers install的时候,会执行其setupOnce方法
setupOnce
setupOnce() {
Error.stackTraceLimit = 50;
if (this._options.onerror) {
logger.log('Global Handler attached: onerror');
this._installGlobalOnErrorHandler();
}
if (this._options.onunhandledrejection) {
logger.log('Global Handler attached: onunhandledrejection');
this._installGlobalOnUnhandledRejectionHandler();
}
}
分析:
- 在new GlobalHandlers()的时候会给_options设置
onerror和onunhandledrejection默认值为true
到此就开始就可以看到Sentry对于运行错误和Promise错误是分别做不同的处理
3.5 window.onerror
_installGlobalOnErrorHandler对应window.onerror
类型
_installGlobalOnErrorHandler() {
if (this._onErrorHandlerInstalled) {
return;
}
addInstrumentationHandler({
callback: (data) => {
// ...
},
type: 'error',
});
this._onErrorHandlerInstalled = true;
}
分析:
- _installGlobalOnErrorHandler为addInstrumentationHandler方法传入
callback
和type
- 其中里面的callback方法这里与初始化的内容无关,我们会在下一章重点讲解。
- type类型是为了
区分当前类型
,在_installGlobalOnUnhandledRejectionHandler会传入type:'unhandledrejection'
3.6 window.unhandledrejection
_installGlobalOnUnhandledRejectionHandler对应window.unhandledrejection
_installGlobalOnUnhandledRejectionHandler() {
if (this._onUnhandledRejectionHandlerInstalled) {
return;
}
addInstrumentationHandler({
callback: (e) => {
// ...
},
type: 'unhandledrejection',
});
this._onUnhandledRejectionHandlerInstalled = true;
}
与_installGlobalOnErrorHandler同理
3.7 addInstrumentationHandler
来看看通用的addInstrumentationHandler方法
/**
* Add handler that will be called when given type of instrumentation triggers.
* Use at your own risk, this might break without changelog notice, only used internally.
* @hidden
*/
const handlers = {};
function addInstrumentationHandler(handler) {
if (!handler || typeof handler.type !== 'string' || typeof handler.callback !== 'function') {
return;
}
handlers[handler.type] = handlers[handler.type] || [];
handlers[handler.type].push(handler.callback);
instrument(handler.type);
}
分析:
- addInstrumentationHandler 会把type与callback传入到handlers中,
当发生错误的时候,就会执行对应callback方法
。 - addInstrumentationHandler最后会
根据type类型去调用instrument方法
,从而执行各种不同的方法。
3.8 instrument
通过instrument可以知道Sentry都处理了哪些情况。如果有兴趣其他方面,可以自己查看对应的源码,里面部分内容我也会之后开一个专题讲解。
function instrument(type) {
if (instrumented[type]) {
return;
}
instrumented[type] = true;
switch (type) {
case 'console':
instrumentConsole();
break;
case 'dom':
instrumentDOM();
break;
case 'xhr':
instrumentXHR();
break;
case 'fetch':
instrumentFetch();
break;
case 'history':
instrumentHistory();
break;
case 'error':
instrumentError();
break;
case 'unhandledrejection':
instrumentUnhandledRejection();
break;
default:
logger.warn('unknown instrumentation type:', type);
}
}
3.9 重点:instrumentError
let _oldOnErrorHandler = null;
/** JSDoc */
function instrumentError() {
_oldOnErrorHandler = global$2.onerror;
global$2.onerror = function (msg, url, line, column, error) {
triggerHandlers('error', {
column,
error,
line,
msg,
url,
});
if (_oldOnErrorHandler) {
return _oldOnErrorHandler.apply(this, arguments);
}
return false;
};
}
分析:
- 如果熟悉
aop(面向切面编程)
的小伙伴对这段代码不会陌生,这里对winodw.onerror 进行劫持,添加了triggerHandlers方法。 - 而当监听到onerror的时候,会调用triggerHandlers 方法根据类型’error'会到
handlers
中找到对应类型的callback方法,也就是_installGlobalOnErrorHandler的callback方法
3.10 重点:instrumentUnhandledRejection。
function instrumentUnhandledRejection() {
_oldOnUnhandledRejectionHandler = global$2.onunhandledrejection;
global$2.onunhandledrejection = function (e) {
triggerHandlers('unhandledrejection', e);
if (_oldOnUnhandledRejectionHandler) {
// eslint-disable-next-line prefer-rest-params
return _oldOnUnhandledRejectionHandler.apply(this, arguments);
}
return true;
};
}
与instrumentError同理
到此,已经为完成整个初始化的步骤了。接下来就是重点关注发生各种类型错误的不同输出。
四、Sentry都对错误信息做了哪些处理
现在我们来先看看_installGlobalOnErrorHandler和_installGlobalOnUnhandledRejectionHandler中的callback
_installGlobalOnErrorHandler:
callback: data => {
const error = data.error;
const currentHub = getCurrentHub();
const hasIntegration = currentHub.getIntegration(GlobalHandlers);
const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;
if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {
return;
}
const client = currentHub.getClient();
const event = isPrimitive(error)
? this._eventFromIncompleteOnError(data.msg, data.url, data.line, data.column)
: this._enhanceEventWithInitialFrame(
eventFromUnknownInput(error, undefined, {
attachStacktrace: client && client.getOptions().attachStacktrace,
rejection: false,
}),
data.url,
data.line,
data.column,
);
addExceptionMechanism(event, {
handled: false,
type: 'onerror',
});
currentHub.captureEvent(event, {
originalException: error,
});
},
_installGlobalOnUnhandledRejectionHandler
callback: e => {
let error = e;
// ...
const currentHub = getCurrentHub();
const hasIntegration = currentHub.getIntegration(GlobalHandlers);
const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;
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 = exports.Severity.Error;
addExceptionMechanism(event, {
handled: false,
type: 'onunhandledrejection',
});
currentHub.captureEvent(event, {
originalException: error,
});
return;
},
从源码看起来_installGlobalOnErrorHandler和_installGlobalOnUnhandledRejectionHandler是很类似的,我们统一一起来分析。
4.1 shouldIgnoreOnError
请注意
这一个函数,我们先看看源码:
let ignoreOnError = 0;
function shouldIgnoreOnError() {
return ignoreOnError > 0;
}
分析:
-
如果你是直接抛出错误throw new Error的时候(实战那边也会有例子),这时候shouldIgnoreOnError会返回true
-
因为我们回过头看[instrument](#3.8 instrument)方法,如果type=‘dom’,会调用instrumentDOM方法
-
所以在初始化的时候已经为为函数function包裹一层
wrap
方法,于是当直接报错的时候ignoreOnError会+1
,这也就是导致会直接跳出callback,不上报错误的原因try{ ... }catch{ ignoreNextOnError() } ------------------------------------------ function ignoreNextOnError() { // onerror should trigger before setTimeout ignoreOnError += 1; setTimeout(() => { ignoreOnError -= 1; }); }
4.2 _installGlobalOnErrorHandler
与_installGlobalOnUnhandledRejectionHandler
的不同点
虽然这两个函数看起来很多,但其实都是差不多的,只有在对错误处理上有些差别:
_installGlobalOnErrorHandler
const event = isPrimitive(error)
? this._eventFromIncompleteOnError(...)
: this._enhanceEventWithInitialFrame(
eventFromUnknownInput(...))
_installGlobalOnUnhandledRejectionHandler
const event = isPrimitive(error)
? this._eventFromRejectionWithPrimitive(...)
: eventFromUnknownInput(...);
分析:
- 通过
isPrimitive
去判断错误类型是基本呢类型还是引用类型而走不同的操作
所以我们就分成4个模块:
eventFromUnknownInput
_enhanceEventWithInitialFrame
,_eventFromIncompleteOnError
,_eventFromRejectionWithPrimitive
分别来研究都对错误做了哪些处理:
4.3 重点:eventFromUnknownInput
eventFromUnknownInput(error, undefined, {
attachStacktrace: client && client.getOptions().attachStacktrace,
rejection: false,
})
--------------------------------------------------------------------------------------------
function eventFromUnknownInput(exception, syntheticException, options = {}) {
let event;
if (isErrorEvent(exception) && exception.error) {
// If it is an ErrorEvent with `error` property, extract it to get actual Error
const errorEvent = exception;
// eslint-disable-next-line no-param-reassign
exception = errorEvent.error;
event = eventFromStacktrace(computeStackTrace(exception));
return event;
}
if (isDOMError(exception) || isDOMException(exception)) {
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
// then we just extract the name, code, and message, as they don't provide anything else
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
// https://developer.mozilla.org/en-US/docs/Web/API/DOMException
const domException = exception;
const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException');
const message = domException.message ? `${name}: ${domException.message}` : name;
event = eventFromString(message, syntheticException, options);
addExceptionTypeValue(event, message);
if ('code' in domException) {
event.tags = Object.assign(Object.assign({}, event.tags), { 'DOMException.code': `${domException.code}` });
}
return event;
}
if (isError(exception)) {
// we have a real Error object, do nothing
event = eventFromStacktrace(computeStackTrace(exception));
return event;
}
if (isPlainObject(exception) || isEvent(exception)) {
// If it is plain Object or Event, serialize it manually and extract options
// This will allow us to group events based on top-level keys
// which is much better than creating new group when any key/value change
const objectException = exception;
event = eventFromPlainObject(objectException, syntheticException, options.rejection);
addExceptionMechanism(event, {
synthetic: true,
});
return event;
}
// If none of previous checks were valid, then it means that it's not:
// - an instance of DOMError
// - an instance of DOMException
// - an instance of Event
// - an instance of Error
// - a valid ErrorEvent (one with an error property)
// - a plain Object
//
// So bail out and capture it as a simple message:
event = eventFromString(exception, syntheticException, options);
addExceptionTypeValue(event, `${exception}`, undefined);
addExceptionMechanism(event, {
synthetic: true,
});
return event;
}
分析:
-
参数说明:
- exception就是错误信息
注意第二个参数为undefind
(于是在eventFromPlainObject和eventFromString中传入的syntheticException此时就是undefined!!也就不会生成错误堆栈)- 第三个参数中的attachStacktrace这边是
用户在Sentry.init中设置的attachStacktrace
,代表需要追踪错误堆栈
-
isErrorEvent如果错误类型是ErrorEvent 会走
eventFromStacktrace(computeStackTrace(exception))
function isErrorEvent(wat) { return Object.prototype.toString.call(wat) === '[object ErrorEvent]'; }
-
isDOMError代表DOMError(已经废弃),isDOMException代表DOMException,调用
eventFromString(注意第二个参数syntheticException为null)
function isDOMError(wat) { return Object.prototype.toString.call(wat) === '[object DOMError]'; } function isDOMException(wat) { return Object.prototype.toString.call(wat) === '[object DOMException]'; }
-
isError 里是Error 或者 Exception 或者DOMException会走这里,
会走eventFromStacktrace(computeStackTrace(exception))
function isError(wat) { switch (Object.prototype.toString.call(wat)) { case '[object Error]': return true; case '[object Exception]': return true; case '[object DOMException]': return true; default: return isInstanceOf(wat, Error); } }
-
isPlainObject或者isEvent针对普通消息,
会走eventFromPlainObject(注意第二个参数syntheticException为null)
function isEvent(wat) { return typeof Event !== 'undefined' && isInstanceOf(wat, Event); } function isPlainObject(wat) { return Object.prototype.toString.call(wat) === '[object Object]'; }
-
其他的就当成简单消息处理了
其实总的看起来,处理eventFromStacktrace,eventFromPlainObject,eventFromString 都是拿到错误消息进行更进一步的数据处理。
其中computeStackTrace是抹平差异,生成错误堆栈的关键
(1) 重点:computeStackTrace 获取错误堆栈
computeStackTrace基于 TraceKit
中的处理方法进行一些改造,主要是抹平各个浏览器间对于错误堆栈的差异
function computeStackTrace(ex) {
let stack = null;
let popSize = 0;
// ...
try {
stack = computeStackTraceFromStackProp(ex);
if (stack) {
return popFrames(stack, popSize);
}
}
catch (e) {
// no-empty
}
return {
message: extractMessage(ex),
name: ex && ex.name,
stack: [],
failed: true,
};
}
computeStackTraceFromStackProp
抹平浏览器差异的具体实现
function computeStackTraceFromStackProp(ex) {
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 + 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,
};
}
分析:
-
通过
ex.stack
可以获取当前的错误堆栈。这里我举一个错误例子方便理解。获取到的错误堆栈为:
Error: externalLibrary method broken: 1610359373199 at externalLibrary (https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:2:9) at https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:5:1"
-
通过ex.stack.split('\n’)转成数组,这里需要对每一个错误栈遍历是因为比如:'Error: externalLibrary method broken: 1610359373199'这种浏览器都是没有区别的可以直接跳过
[ 'Error: externalLibrary method broken: 1610359373199', 'at externalLibrary (https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:2:9)', 'at https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:5:1" ' ]
-
然后通过正则对当前的错误判断属于哪种
浏览器内核
,然后做不同的处理,抹平差异 -
最后返回stack是带有
args,line,func,url
的数据格式args: [] column: 9 func: "externalLibrary" line: 2 url: "https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js"
(2)eventFromStacktrace、eventFromString和 eventFromPlainObject
这三个统一讲解是因为原理都是一样的,都是拿到错误堆栈等信息进行进一步处理,
注意这里的eventFromString和eventFromPlainObject传入的syntheticException都是null所以是不会产生错误堆栈
的
来看看他们的源码:
function eventFromStacktrace(stacktrace) {
const exception = exceptionFromStacktrace(stacktrace);
return {
exception: {
values: [exception],
},
};
}
--------------------------------------------------------------------------------------------
function eventFromString(input, syntheticException, options = {}) {
const event = {
message: input,
};
if (options.attachStacktrace && syntheticException) {
const stacktrace = computeStackTrace(syntheticException);
const frames = prepareFramesForEvent(stacktrace.stack);
event.stacktrace = {
frames,
};
}
return event;
}
-------------------------------------------------------------------------------------------
function eventFromPlainObject(exception, syntheticException, rejection) {
const event = {
exception: {
values: [
{
type: isEvent(exception) ? exception.constructor.name : rejection ? 'UnhandledRejection' : 'Error',
value: `Non-Error ${
rejection ? 'promise rejection' : 'exception'
} captured with keys: ${extractExceptionKeysForMessage(exception)}`,
},
],
},
extra: {
__serialized__: normalizeToSize(exception),
},
};
if (syntheticException) {
const stacktrace = computeStackTrace(syntheticException);
const frames = prepareFramesForEvent(stacktrace.stack);
event.stacktrace = {
frames,
};
}
return event;
}
分析:
-
computeStackTrace 就是我们上面讲过的抹平差异,获取错误堆栈
-
exception包含了错误类型和信息
-
exceptionFromStacktrace实际上是调用prepareFramesForEvent获取错误堆栈
-
看看最终返回的数据吧
exception: { stacktrace: [ { colno: 1, filename: 'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js', function: '?', in_app: true, lineno: 5, }, { colno: 9, filename: 'https://rawgit.com/kamilogorek/cfbe9f92196c6c610535e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js', function: 'externalLibrary', in_app: true, lineno: 2, }, ]; type: 'Error'; value: 'externalLibrary method broken: 1610364003791'; }
(3) 小结
eventFromUnknownInput 虽然代码很多,但是很清晰。
根据错误消息的类型去走eventFromStacktrace、eventFromString和 eventFromPlainObject其中的一个方法。像错误类型为DOMError、DOMException、普通对象的此时是没有错误堆栈的消息
的,而其他的会通过computeStackTrace
去抹平不同浏览器的差异,获取错误堆栈,最后对错误数据处理返回统一的结构
。
4.4 _enhanceEventWithInitialFrame
_enhanceEventWithInitialFrame(event, url, line, column) {
event.exception = event.exception || {};
event.exception.values = event.exception.values || [];
event.exception.values[0] = event.exception.values[0] || {};
event.exception.values[0].stacktrace = event.exception.values[0].stacktrace || {};
event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames || [];
const colno = isNaN(parseInt(column, 10)) ? undefined : column;
const lineno = isNaN(parseInt(line, 10)) ? undefined : line;
const filename = isString(url) && url.length > 0 ? url : getLocationHref();
if (event.exception.values[0].stacktrace.frames.length === 0) {
event.exception.values[0].stacktrace.frames.push({
colno,
filename,
function: '?',
in_app: true,
lineno,
});
}
return event;
}
}
分析:
- 通过eventFromUnknownInput拿到event,url,line,colum后调用_enhanceEventWithInitialFrame
- _enhanceEventWithInitialFrame确保exception,values,stacktrace,frames设置有默认值
4.5 _eventFromRejectionWithPrimitive
_eventFromRejectionWithPrimitive(reason) {
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)}`,
},
],
},
};
}
分析:
- 代码很简单就是返回了默认格式
4.6 _eventFromIncompleteOnError
this._eventFromIncompleteOnError(data.msg, data.url, data.line, data.column)
--------------------------------------------------------------------------------------
/**
* This function creates a stack from an old, error-less onerror handler.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_eventFromIncompleteOnError(msg, url, line, column) {
const ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/i;
// If 'message' is ErrorEvent, get real message from inside
let message = isErrorEvent(msg) ? msg.message : msg;
let name;
if (isString(message)) {
const groups = message.match(ERROR_TYPES_RE);
if (groups) {
name = groups[1];
message = groups[2];
}
}
const event = {
exception: {
values: [
{
type: name || 'Error',
value: message,
},
],
},
};
return this._enhanceEventWithInitialFrame(event, url, line, column);
}
--------------------------------------------------------------------------------------
_enhanceEventWithInitialFrame(event, url, line, column) {
event.exception = event.exception || {};
event.exception.values = event.exception.values || [];
event.exception.values[0] = event.exception.values[0] || {};
event.exception.values[0].stacktrace = event.exception.values[0].stacktrace || {};
event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames || [];
const colno = isNaN(parseInt(column, 10)) ? undefined : column;
const lineno = isNaN(parseInt(line, 10)) ? undefined : line;
const filename = isString(url) && url.length > 0 ? url : getLocationHref();
if (event.exception.values[0].stacktrace.frames.length === 0) {
event.exception.values[0].stacktrace.frames.push({
colno,
filename,
function: '?',
in_app: true,
lineno,
});
}
return event;
}
}
分析:
- _eventFromIncompleteOnError 对于基本类型的错误,可以直接获取错误信息,所以参数里就直接传入 url, line, column
- _eventFromIncompleteOnError 就是
添加错误类型和信息
- _enhanceEventWithInitialFrame就是
添加错误堆栈
到此,window.onerror和window.unhandrerejection 对于错误的不同处理已经研究完毕,接下来就是最后对错误数据进行统一处理。
4.7 addExceptionMechanis
addExceptionMechanism为event事件添加mechanism属性
比如上面的例子经过addExceptionMechanism处理后
addExceptionMechanism(event, {
handled: false,
type: 'onerror',
});
得出如下数据格式:
exception: {
stacktrace: [
// ...
],
type: 'Error',
value: 'externalLibrary method broken: 1610364003791',
mechanism:{
type:'onerror',
handled:false
}
}
4.8 currentHub.captureEvent
到这里,已经把错误信息转换成我们想要的数据格式了。
currentHub.captureEvent 这边就是进行上报数据了,这一块内容将在下一篇文章玩转前端监控,全面解析Sentry源码(三)| 数据上报讲解。
4.9 补充:主动上报的错误信息如何处理
除了靠监听window.onerror和window.unhandledrejection,用户还可以采用captureException
主动上报错误数据
接下来我们看看这与监听时处理数据有什么不同
function captureException(exception, hint, scope) {
// ...
this._process(
this._getBackend()
.eventFromException(exception, hint)
// ...
);
return eventId;
}
----------------------------------------------------------------------------------
function eventFromException(options, exception, hint) {
const syntheticException = (hint && hint.syntheticException) || undefined;
const event = eventFromUnknownInput(exception, syntheticException, {
attachStacktrace: options.attachStacktrace,
});
addExceptionMechanism(event, {
handled: true,
type: 'generic',
});
// ...
}
分析:
- 可以看到主动上报走的是eventFromException方法
- eventFromException实际上就是调用
4.3 eventFromUnknownInput
注意,这里syntheticException不是undefind,是可以去追踪错误堆栈的
4.10 总结
最后我们通过一张流程图来理一理当发生错误的时候,来看看Sentry都做了什么。
五、实战
这一章节主要通过几个例子,让大家直观地看到各种类型的错误经过Sentry处理后会对应返回的数据类型
Sentry.init配置
Sentry.init({
dsn: '..',
attachStacktrace: true,
debug: true,
enabled: true,
release: '..',
});
5.1 window.error | 错误类型是基本类型
这里会走window.onerror中的4.6 _eventFromIncompleteOnError
方法去处理数据
测试用例:
<button id="error-string">window.error | string Error</button>
<script>
document.querySelector('#error-string').addEventListener('click', () => {
const script = document.createElement('script');
script.src = './errorString.js';
document.body.appendChild(script);
});
</script>
errorString.js:
function externalLibrary(date) {
throw `string Error${date}`;
}
externalLibrary(Date.now());
展示结果:
对应错误数据:
exception: {
values: {
mechanism: {
handled: false,
type: 'onerror',
},
stacktrace: {
frames: [
{
colno: 3,
filename: 'http://127.0.0.1:5500/packages/browser/examples/errorString.js',
function: '?',
in_app: true,
lineno: 2,
},
],
},
type: 'Error',
value: 'string Error1610594068361',
},
},
5.2 window.error | 错误类型是引用类型
这里会走window.onerror中的 4.4 _enhanceEventWithInitialFrame
方法去处理数据,由于走的eventFromUnknownInput中的eventFromPlainObject,所以这里面是没有错误栈的消息的。
测试用例:
<button id="error-throw-new-error">window.error | throw new Error</button>
<script>
document.querySelector('#error-throw-new-error').addEventListener('click', () => {
console.log('click');
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = 'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js';
document.body.appendChild(script);
});
</script>
external-lib.js
function externalLibrary (date) {
throw new Error(`externalLibrary method broken: ${date}`);
}
externalLibrary(Date.now());
展示结果:
对应错误数据:
{
exception: {
values: [
{
stacktrace: {
mechanism: {
handled: false,
type: 'onunhandledrejection',
},
frames: [
{
colno: 23,
filename: 'http://127.0.0.1:5500/packages/browser/examples/app.js',
function: '?',
in_app: true,
lineno: 171,
},
],
},
},
];
}
type: 'Error';
value: 'promise-throw-error';
}
5.3 window.error | 不上报错误
这里会走 4.1 shouldIgnoreOnError
直接跳出,不进行错误上报,也就没有错误信息了
测试代码
<button id="ignore-message">ignoreError message example</button>
<script>
document.querySelector('#ignore-message').addEventListener('click', () => {
fun()
});
</script>
展示结果:
5.4 window.unhandledrejection |错误类型是基本类型
这里会走window.unhandledrejection中的4.5 _eventFromRejectionWithPrimitive
方法去处理数据,这里面是没有错误栈的消息的。
测试代码
<button id="unhandledrejection-string">window.unhandledrejection | string-error</button>
<script>
document.querySelector('#unhandledrejection-string').addEventListener('click', () => {
new Promise(function(resolve, reject) {
setTimeout(function() {
return reject('oh string error');
}, 200);
});
});
</script>
展示结果:
对应错误数据:
exception: {
values: [
{
mechanism: { handled: false, type: 'onunhandledrejection' },
},
],
type: 'UnhandledRejection',
value: 'Non-Error promise rejection captured with value: oh string error',
level: 'error',
}
5.5 window.unhandledrejection |错误类型是引用类型
这里会走window.unhandledrejection中的 4.3 重点:eventFromUnknownInput
中的eventFromPlainObject,所以这里面是没有错误栈的消息的。
测试代码
<button id="unhandledrejection-plainObject">window.unhandledrejection | plainObject</button>
<script>
document.querySelector('#unhandledrejection-plainObject').addEventListener('click', () => {
new Promise(function(resolve, reject) {
setTimeout(function() {
const obj = {
msg: 'plainObject',
testObj: {
message: '这是个测试数据',
},
date: new Date(),
reg: new RegExp(),
testFun: () => {
console.log('testFun');
},
};
return reject(obj);
}, 200);
});
});
</script>
展示结果:
对应错误数据:
exception: {
values: [
{
type: 'UnhandledRejection',
value: '"Non-Error promise rejection captured with keys: msg, testFun, testObj"',
},
],
extra: {
__serialized__: {
msg: 'plainObject',
testFun: '[Function: testFun]',
testObj: {
message: '这是个测试数据',
},
},
},
5.5 window.unhandledrejection |错误类型是引用类型并且是DOMException
这里会走window.unhandledrejection中的 4.3 重点:eventFromUnknownInput
方法去处理数据,由于走的eventFromUnknownInput中的eventFromString,所以这里面是没有错误栈的消息的。
测试代码
<button id="DOMException">DOMException</button>
<script>
document.querySelector('#DOMException').addEventListener('click', () => {
screen.orientation.lock('portrait');
});
</script>
展示结果:
对应错误数据:
exception: {
values: [
{
type: 'Error',
value: 'NotSupportedError: screen.orientation.lock() is not available on this device.',
},
],
},
message: 'NotSupportedError: screen.orientation.lock() is not available on this device.',
tags: {
'DOMException.code': '9',
}
5.5 主动上报
这里走的是主动上报的错误信息如何处理
测试代码
<button id="DOMException">DOMException</button>
<script>
document.querySelector('#capture-exception').addEventListener('click', () => {
Sentry.captureException(new Error(`captureException call no. ${Date.now()}`));
});
</script>
展示结果:
控制台没有任何错误,因为Sentry.captureException是向客户端发送一个错误数据
对应错误数据:
exception: {
event_id: '0e7c9eb8fa3c42b786543317943e9d0d',
exception: {
values: [
{
mechanism: {
handled: true,
type: 'generic',
},
stacktrace: {
frames: [
{
colno: 29,
filename: 'http://127.0.0.1:5500/packages/browser/examples/app.js',
function: 'HTMLButtonElement.<anonymous>',
in_app: true,
lineno: 142,
},
],
},
},
],
type: 'Error',
value: 'captureException call no. 1610599823126',
},
}
六、总结
Sentry在初始化的时候对window.onerror和window.unhandledrejection进行劫持,当错误发生的时候,为了抹平各个浏览器内核对于错误堆栈返回的信息不同用computeStackTrace进行抹平差异获取错误堆栈。对于不同类型的错误消息都有自己对应的处理方式,最后输出统一的错误数据。
接着是本文没有讲解的内容:Sentr之后的工作就是获取非错误的数据,如: user-agent
、浏览器信息
、系统信息
、自定义信息
等信息,然后交给Sentry
的生命周期函数,最后在把数据发送到Sentry
服务端,进行错误信息展示。
七、参考资料
- Sentry官网
- Sentry仓库
- Error MDN
- ErrorEvent MDN
- DOMException MDN
- 前端异常监控-看这篇就够了
- 聊聊前端监控——错误监控篇
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!