Read TraceKit
XXHolic opened this issue · 0 comments
目录
引子
前端异常研究时,发现了 TraceKit 这个库, sentry 里面部分功能也基于这个库再改造了,就去看了下源码。
TraceKit 版本: v0.4.6 。
简介
TraceKit 对浏览器堆栈进行解析追踪,对市场上主要的浏览器都做了测试。在浏览器异常一些方面做了比较详尽的处理。
思路
源码中的一些思路:
- 通过订阅的方式向外部抛出处理后的异常。
- 主要对
onerror
和onunhandledrejection
事件进行了包装,支持撤销包装。 - 异常信息处理,结合了正则匹配。
下面针对主要的逻辑进行介绍。
具体实现
捕获异常并解析主要有三种途径: onerror 事件、 onunhandledrejection 事件、 TraceKit.report(ex) 。
onerror 事件
源码
/**
* Ensures all global unhandled exceptions are recorded.
* Supported by Gecko and IE.
* @param {string} message Error message.
* @param {string} url URL of script that generated the exception.
* @param {(number|string)} lineNo The line number at which the error occurred.
* @param {(number|string)=} columnNo The column number at which the error occurred.
* @param {Error=} errorObj The actual Error object.
* @memberof TraceKit.report
*/
function traceKitWindowOnError(message, url, lineNo, columnNo, errorObj) {
var stack = null;
if (lastExceptionStack) {
TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message);
processLastException();
} else if (errorObj) {
stack = TraceKit.computeStackTrace(errorObj);
notifyHandlers(stack, true, errorObj);
} else {
var location = {
'url': url,
'line': lineNo,
'column': columnNo
};
var name;
var msg = message; // must be new var or will modify original `arguments`
if ({}.toString.call(message) === '[object String]') {
var groups = message.match(ERROR_TYPES_RE);
if (groups) {
name = groups[1];
msg = groups[2];
}
}
location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line);
location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line);
stack = {
'name': name,
'message': msg,
'mode': 'onerror',
'stack': [location]
};
notifyHandlers(stack, true, null);
}
if (_oldOnerrorHandler) {
return _oldOnerrorHandler.apply(this, arguments);
}
return false;
}
封装后的 onerror
事件处理程序中将异常分为了三类:
- 优先处理
lastExceptionStack
记录的值; - 不符合条件 1 就处理
errorObj
有值的情况; - 不符合 1 和 2 ,构造一个异常返回。
比较多的异常会在第二类中进行处理,执行 TraceKit.computeStackTrace(ex, depth)
方法。
源码
/**
* Computes a stack trace for an exception.
* @param {Error} ex
* @param {(string|number)=} depth
* @memberof TraceKit.computeStackTrace
*/
function computeStackTrace(ex, depth) {
var stack = null;
depth = (depth == null ? 0 : +depth);
try {
// This must be tried first because Opera 10 *destroys*
// its stacktrace property if you try to access the stack
// property first!!
stack = computeStackTraceFromStacktraceProp(ex);
if (stack) {
return stack;
}
} catch (e) {
if (debug) {
throw e;
}
}
try {
stack = computeStackTraceFromStackProp(ex);
if (stack) {
return stack;
}
} catch (e) {
if (debug) {
throw e;
}
}
try {
stack = computeStackTraceFromOperaMultiLineMessage(ex);
if (stack) {
return stack;
}
} catch (e) {
if (debug) {
throw e;
}
}
try {
stack = computeStackTraceByWalkingCallerChain(ex, depth + 1);
if (stack) {
return stack;
}
} catch (e) {
if (debug) {
throw e;
}
}
return {
'name': ex.name,
'message': ex.message,
'mode': 'failed'
};
}
该方法中对异常分为 4 种类型进行处理:
computeStackTraceFromStacktraceProp(ex)
针对 Opera 10+ 中抛出的异常进行处理;computeStackTraceFromStackProp(ex)
针对 Chrome、 Gecko 中抛出的异常进行处理;computeStackTraceFromOperaMultiLineMessage(ex)
针对 Opera 9 及其更早版本抛出的异常进行处理;computeStackTraceByWalkingCallerChain(ex)
针对 Safari 、 IE 中抛出的异常进行处理;
看看使用比较多的 Chrome 中的处理 computeStackTraceFromStackProp(ex)
方法。
源码
/**
* Computes stack trace information from the stack property.
* Chrome and Gecko use this property.
* @param {Error} ex
* @return {?TraceKit.StackTrace} Stack trace information.
* @memberof TraceKit.computeStackTrace
*/
function computeStackTraceFromStackProp(ex) {
if (!ex.stack) {
return null;
}
var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,
gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i,
winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,
// Used to additionally parse URL/line/column from eval frames
isEval,
geckoEval = /(\S+) line (\d+)(?: > eval line \d+)* > eval/i,
chromeEval = /\((\S*)(?::(\d+))(?::(\d+))\)/,
lines = ex.stack.split('\n'),
stack = [],
submatch,
parts,
element,
reference = /^(.*) is undefined$/.exec(ex.message);
for (var i = 0, j = lines.length; i < j; ++i) {
if ((parts = chrome.exec(lines[i]))) {
var 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 = {
'url': !isNative ? parts[2] : null,
'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[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = null; // no column when eval
} else if (i === 0 && !parts[5] && !_isUndefined(ex.columnNumber)) {
// 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 = guessFunctionName(element.url, element.line);
}
element.context = element.line ? gatherContext(element.url, element.line) : null;
stack.push(element);
}
if (!stack.length) {
return null;
}
if (stack[0] && stack[0].line && !stack[0].column && reference) {
stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line);
}
return {
'mode': 'stack',
'name': ex.name,
'message': ex.message,
'stack': stack
};
}
对异常信息中 stack
处理的思路:
- 对
stack
中字符串信息,以\n
进行分割得到数组,然后进行遍历进行正则匹配,提供了 3 种匹配规则,分别是chrome
、gecko
、winjs
; - 遍历过程中,优先进行
chrome
匹配,如果不符合,再进行winjs
,如果不符合 ,再进行gecko
匹配; - 如果以上 3 种匹配都不符合,就重新进行下个循环,如果符合其中之一,接着推测是否有函数并组装;
- 接着对异常所处的上下环境进行猜测并组装;
- 以上步骤处理完后,放入数组中,开始下一个循环。
这里需要提一下针对 Safari、 IE 中的处理,添加一个额外的参数 incomplete
参数,表示是否完成了异常的处理。在另外一个地方会用到。
onunhandledrejection 事件
源码
function installGlobalUnhandledRejectionHandler() {
if (_onUnhandledRejectionHandlerInstalled === true) {
return;
}
_oldOnunhandledrejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = traceKitWindowOnUnhandledRejection;
_onUnhandledRejectionHandlerInstalled = true;
}
function traceKitWindowOnUnhandledRejection(e) {
var stack = TraceKit.computeStackTrace(e.reason);
notifyHandlers(stack, true, e.reason);
}
onunhandledrejection
事件处理程序同样是使用了 TraceKit.computeStackTrace(ex, depth)
方法。
TraceKit.report(ex)
这是一种主动的上报方式,也是对外暴露的一个方法。
源码
/**
* Cross-browser processing of unhandled exceptions
*
* Syntax:
* ```js
* TraceKit.report.subscribe(function(stackInfo) { ... })
* TraceKit.report.unsubscribe(function(stackInfo) { ... })
* TraceKit.report(exception)
* try { ...code... } catch(ex) { TraceKit.report(ex); }
* ```
*
* Supports:
* - Firefox: full stack trace with line numbers, plus column number
* on top frame; column number is not guaranteed
* - Opera: full stack trace with line and column numbers
* - Chrome: full stack trace with line and column numbers
* - Safari: line and column number for the top frame only; some frames
* may be missing, and column number is not guaranteed
* - IE: line and column number for the top frame only; some frames
* may be missing, and column number is not guaranteed
*
* In theory, TraceKit should work on all of the following versions:
* - IE5.5+ (only 8.0 tested)
* - Firefox 0.9+ (only 3.5+ tested)
* - Opera 7+ (only 10.50 tested; versions 9 and earlier may require
* Exceptions Have Stacktrace to be enabled in opera:config)
* - Safari 3+ (only 4+ tested)
* - Chrome 1+ (only 5+ tested)
* - Konqueror 3.5+ (untested)
*
* Requires TraceKit.computeStackTrace.
*
* Tries to catch all unhandled exceptions and report them to the
* subscribed handlers. Please note that TraceKit.report will rethrow the
* exception. This is REQUIRED in order to get a useful stack trace in IE.
* If the exception does not reach the top of the browser, you will only
* get a stack trace from the point where TraceKit.report was called.
*
* Handlers receive a TraceKit.StackTrace object as described in the
* TraceKit.computeStackTrace docs.
*
* @memberof TraceKit
* @namespace
*/
TraceKit.report = (function reportModuleWrapper() {
var handlers = [],
lastException = null,
lastExceptionStack = null;
/**
* Add a crash handler.
* @param {Function} handler
* @memberof TraceKit.report
*/
function subscribe(handler) {
installGlobalHandler();
installGlobalUnhandledRejectionHandler();
handlers.push(handler);
}
/**
* Remove a crash handler.
* @param {Function} handler
* @memberof TraceKit.report
*/
function unsubscribe(handler) {
for (var i = handlers.length - 1; i >= 0; --i) {
if (handlers[i] === handler) {
handlers.splice(i, 1);
}
}
if (handlers.length === 0) {
uninstallGlobalHandler();
uninstallGlobalUnhandledRejectionHandler();
}
}
/**
* Dispatch stack information to all handlers.
* @param {TraceKit.StackTrace} stack
* @param {boolean} isWindowError Is this a top-level window error?
* @param {Error=} error The error that's being handled (if available, null otherwise)
* @memberof TraceKit.report
* @throws An exception if an error occurs while calling an handler.
*/
function notifyHandlers(stack, isWindowError, error) {
var exception = null;
if (isWindowError && !TraceKit.collectWindowErrors) {
return;
}
for (var i in handlers) {
if (_has(handlers, i)) {
try {
handlers[i](stack, isWindowError, error);
} catch (inner) {
exception = inner;
}
}
}
if (exception) {
throw exception;
}
}
var _oldOnerrorHandler, _onErrorHandlerInstalled;
var _oldOnunhandledrejectionHandler, _onUnhandledRejectionHandlerInstalled;
/**
* Ensures all global unhandled exceptions are recorded.
* Supported by Gecko and IE.
* @param {string} message Error message.
* @param {string} url URL of script that generated the exception.
* @param {(number|string)} lineNo The line number at which the error occurred.
* @param {(number|string)=} columnNo The column number at which the error occurred.
* @param {Error=} errorObj The actual Error object.
* @memberof TraceKit.report
*/
function traceKitWindowOnError(message, url, lineNo, columnNo, errorObj) {
var stack = null;
if (lastExceptionStack) {
TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message);
processLastException();
} else if (errorObj) {
stack = TraceKit.computeStackTrace(errorObj);
notifyHandlers(stack, true, errorObj);
} else {
var location = {
'url': url,
'line': lineNo,
'column': columnNo
};
var name;
var msg = message; // must be new var or will modify original `arguments`
if ({}.toString.call(message) === '[object String]') {
var groups = message.match(ERROR_TYPES_RE);
if (groups) {
name = groups[1];
msg = groups[2];
}
}
location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line);
location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line);
stack = {
'name': name,
'message': msg,
'mode': 'onerror',
'stack': [location]
};
notifyHandlers(stack, true, null);
}
if (_oldOnerrorHandler) {
return _oldOnerrorHandler.apply(this, arguments);
}
return false;
}
/**
* Ensures all unhandled rejections are recorded.
* @param {PromiseRejectionEvent} e event.
* @memberof TraceKit.report
* @see https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunhandledrejection
* @see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
*/
function traceKitWindowOnUnhandledRejection(e) {
var stack = TraceKit.computeStackTrace(e.reason);
notifyHandlers(stack, true, e.reason);
}
/**
* Install a global onerror handler
* @memberof TraceKit.report
*/
function installGlobalHandler() {
if (_onErrorHandlerInstalled === true) {
return;
}
_oldOnerrorHandler = window.onerror;
window.onerror = traceKitWindowOnError;
_onErrorHandlerInstalled = true;
}
/**
* Uninstall the global onerror handler
* @memberof TraceKit.report
*/
function uninstallGlobalHandler() {
if (_onErrorHandlerInstalled) {
window.onerror = _oldOnerrorHandler;
_onErrorHandlerInstalled = false;
}
}
/**
* Install a global onunhandledrejection handler
* @memberof TraceKit.report
*/
function installGlobalUnhandledRejectionHandler() {
if (_onUnhandledRejectionHandlerInstalled === true) {
return;
}
_oldOnunhandledrejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = traceKitWindowOnUnhandledRejection;
_onUnhandledRejectionHandlerInstalled = true;
}
/**
* Uninstall the global onunhandledrejection handler
* @memberof TraceKit.report
*/
function uninstallGlobalUnhandledRejectionHandler() {
if (_onUnhandledRejectionHandlerInstalled) {
window.onunhandledrejection = _oldOnunhandledrejectionHandler;
_onUnhandledRejectionHandlerInstalled = false;
}
}
/**
* Process the most recent exception
* @memberof TraceKit.report
*/
function processLastException() {
var _lastExceptionStack = lastExceptionStack,
_lastException = lastException;
lastExceptionStack = null;
lastException = null;
notifyHandlers(_lastExceptionStack, false, _lastException);
}
/**
* Reports an unhandled Error to TraceKit.
* @param {Error} ex
* @memberof TraceKit.report
* @throws An exception if an incomplete stack trace is detected (old IE browsers).
*/
function report(ex) {
if (lastExceptionStack) {
if (lastException === ex) {
return; // already caught by an inner catch block, ignore
} else {
processLastException();
}
}
var stack = TraceKit.computeStackTrace(ex);
lastExceptionStack = stack;
lastException = ex;
// If the stack trace is incomplete, wait for 2 seconds for
// slow slow IE to see if onerror occurs or not before reporting
// this exception; otherwise, we will end up with an incomplete
// stack trace
setTimeout(function () {
if (lastException === ex) {
processLastException();
}
}, (stack.incomplete ? 2000 : 0));
throw ex; // re-throw to propagate to the top level (and cause window.onerror)
}
report.subscribe = subscribe;
report.unsubscribe = unsubscribe;
return report;
}());
源码中是一个立即执行函数,返回了 report(ex)
函数,并给这个函数增加了 subscribe
和 unsubscribe
属性,分支指向了 subscribe(handler)
函数和 unsubscribe(handler)
函数。
接下来看看执行 report 第一次上报的时候做了什么:
- 调用
TraceKit.computeStackTrace
方法,处理后结果赋给lastExceptionStack
,源异常ex
赋给lastException
。 setTimeout
执行了一个逻辑:如果lastException === ex
,则执行processLastException()
方法,该方法会重置lastException
和lastExceptionStack
,把已处理的异常通知给订阅者。延时的时间由上面提过的参数incomplete
决定。- 最终会
throw
给上一层,触发window.onerror
。 - 在
onerror
中,lastExceptionStack
有值会优先处理,并会添加了incomplete
参数,最终也会执行processLastException()
方法。
数据格式
处理之后数据格式中可能有的属性:
{
'incomplete': false,
'mode': 'stack',
'name': 'name',
'message': 'message',
'partial': true,
'stack': []
}
- incomplete : 信息是否不完整。
- mode : 解析异常信息的方法途径。
stack
表示从异常ex.stack
中解析;stacktrace
表示从ex.stacktrace
中解析,针对的是 Opera 10+ ;multiline
表示从ex.message
中进行解析,针对的是 Opera 9 及更早版本 ;callers
表示根据arguments.caller
进行解析,主要针对 Safari 和 IE;onerror
表示onerror
事件中处理特殊一类异常;failed
表示解析失败。 - name : 异常名称。
- message : 异常描述信息。
- partial : 抛出的异常中能获取到
url
(导致异常的脚本路径) 和lineNo
(导致异常的脚本行数),才会有partial
属性。 - stack : 存储解析后栈帧。
其中 stack 的存放对象格式:
{
'url': '', // 脚本或 html 路径
'func': '', // 函数名称,匿名函数可能为空
'args': '', // 函数参数
'line': 12, // 所处行数
'column': 32, // 所处列数
'context': [] // 猜测的相关源码
}
特殊情况
上面是分析正常情况下的逻辑,比较极端的情况,需要同时结合几种处理逻辑进行判断。
情况 1
情景: 没有使用 TraceKit.report(ex)
方法,应用程序中出现了死循环,一直抛出异常。
预计: 全局的异常捕获,会跟随死循环不停的上报异常,这也是一个风险点。
情况 2
情景: 使用 TraceKit.report(ex)
方法,异常 A 进入 ,刚执行完,这时异常 B 进入了。
预计: 这时 lastExceptionStack
可能仍有值,那么就会进入到 processLastException()
中,但 A 异常抛到 onerror
,在 onerror
处理程序中,这时 lastExceptionStack
有值,就会处理,最终也会执行 processLastException()
。如果捕获 A 异常的 onerror
事件处理程序先执行了,那么 B 异常可以按照正常逻辑处理;如果 B 异常处理先执行,那么 A 异常的处理就会少一步,这样就会导致同类上报的信息产生了差异。尝试模拟这样的情况没有达到成功,是否有这样的情况,不太确定。