实现
一、认识
基于 Sentry
异常采集方案,将一些方法进行重写。Sentry
异常监控原理是如何做的呢?
-
为了能自动捕获应用异常,
Sentry
劫持覆写了window.onerror
和window.unhandledrejection
这两个api
-
Sentry
内部对异常发生的特殊上下文,做了标记。这些特殊上下文包括:dom
节点事件回调、setTimeout
/setInterval
回调、xhr
接口调用、requestAnimationFrame
回调等
1.1 全局捕获
1.2 单点捕获
二、window.onerror
为了能自 动捕获应用异常,Sentry
劫持覆写了 window.onerror
, 重写逻辑如下所示:
const oldErrorHandler = window.onerror;
window.onerror = function (msg, url, line, column, error) {
// 收集异常信息并上报
triggerHandlers('error', {
column: column,
error: error,
line: line,
msg: msg,
url: url,
});
if (oldErrorHandler) {
return oldErrorHandler.apply(this, arguments);
}
return false;
};
三、window.onunhandledrejection
为了能自动捕获应用异常,Sentry
劫持覆写了 window.onunhandledrejection
, 重写逻辑如下所示:
const oldOnUnhandledRejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = function (e) {
// 收集异常信息并上报
triggerHandlers('unhandledrejection', e);
if (oldOnUnhandledRejectionHandler) {
return oldOnUnhandledRejectionHandler.apply(this, arguments);
}
return true;
};
四、setTimeout
标记 setTimeout
, 为了标记 setTimeout
类型的异常,Sentry
劫持覆写了原生的 setTimout
方法。新的 setTimeout
方法调用时,会使用 try ... catch
语句块包裹 callback
。
重写逻辑如下
const originSetTimeout = window.setTimeout;
window.setTimeout = function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var originalCallback = args[0];
// wrap$1 会对 setTimeout 的入参 callback 使用 try...catch 进行包装
// 并在 catch 中上报异常
args[0] = wrap$1(originalCallback, {
mechanism: {
data: { function: getFunctionName(original) },
handled: true,
// 异常的上下文是 setTimeout
type: 'setTimeout',
},
});
return original.apply(this, args);
}
当 callback
内部发生异常时,会被 catch
捕获,捕获的异常会标记 setTimeout
。
五、setInterval
标记 setInterval
, 为了标记 setInterval
类型的异常,Sentry
劫持覆写了原生的 setInterval
方法。新的 setInterval
方法调用时,会使用 try ... catch
语句块包裹 callback
。
六、requestAnimationFrame
标记 requestAnimationFrame
, 为了标记 requestAnimationFrame
类型的异常,Sentry
劫持覆写了原生的 requestAnimationFrame
方法。新的 requestAnimationFrame
方法调用时,会使用 try ... catch
语句块包裹 callback
。
七、dom event handler
标记 dom
事件 handler
, 所有的 dom
节点都继承自 window.Node
对象,dom
对象的 addEventListener
方法来自 Node
的 prototype
对象。为了标记 dom
事件 handler
,Sentry
对 Node.prototype.addEventListener
进行了劫持覆写。新的 addEventListener
方法调用时,同样会使用 try ... catch
语句块包裹传入的 handler
。
重写逻辑如下
function xxx() {
var proto = window.Node.prototype;
...
// 覆写 addEventListener 方法
fill(proto, 'addEventListener', function (original) {
return function (eventName, fn, options) {
try {
if (typeof fn.handleEvent === 'function') {
// 使用 try...catch 包括 handle
fn.handleEvent = wrap$1(fn.handleEvent.bind(fn), {
mechanism: {
data: {
function: 'handleEvent',
handler: getFunctionName(fn),
target: target,
},
handled: true,
type: 'instrument',
},
});
}
}
catch (err) {}
return original.apply(this, [
eventName,
wrap$1(fn, {
mechanism: {
data: {
function: 'addEventListener',
handler: getFunctionName(fn),
target: target,
},
handled: true,
type: 'instrument',
},
}),
options,
]);
};
});
}
当 handler
内部发生异常时,会被 catch
捕获,捕获的异常会被标记 handleEvent
, 并携带 event name
、event target
等信息。
八、xhr open、send
**XHR
**通过重写(拦截)send
和open
。为了标记 xhr
接口回调,Sentry
先对 XMLHttpRequest.prototype.send
方法劫持覆写, 等 xhr
实例使用覆写以后的 send
方法时,再对 xhr
对象的 onload
、onerror
、onprogress
、onreadystatechange
方法进行了劫持覆写, 使用 try ... catch
语句块包裹传入的 callback
。
重写逻辑如下
function fill(source, name, replacementFactory) {
var original = source[name];
var wrapped = replacementFactory(original);
source[name] = wrapped;
}
// xhr
function instrumentXHR(): void {
// 保存真实的xhr的原型
const xhrproto = XMLHttpRequest.prototype;
// 拦截open方法
fill(xhrproto, 'open', function (originalOpen: () => void): () => void {
return function (this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
const xhr = this;
const onreadystatechangeHandler = function (): void {
if (xhr.readyState === 4) {
if (xhr.__sentry_xhr__) {
xhr.__sentry_xhr__.status_code = xhr.status;
}
// // 上报sentry
triggerHandlers('xhr', {
args,
endTimestamp: Date.now(),
startTimestamp: Date.now(),
xhr,
});
}
};
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
// 拦截onreadystatechange方法
fill(xhr, 'onreadystatechange', function (original: WrappedFunction): Function {
return function (...readyStateArgs: any[]): void {
onreadystatechangeHandler();
// 返回原来的方法
return original.apply(xhr, readyStateArgs);
};
});
} else {
xhr.addEventListener('readystatechange', onreadystatechangeHandler);
}
// 调用原来的方法
return originalOpen.apply(xhr, args);
};
});
// fill其实就是拦截的一个封装originalSend就是原来的send方法
fill(xhrproto, 'send', function (originalSend: () => void): () => void {
return function (this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
// 上报sentry
triggerHandlers('xhr', {
args,
startTimestamp: Date.now(),
xhr: this,
});
// 返回原来方法
return originalSend.apply(this, args);
};
});
}
当 callback
内部发生异常时,会被 catch
捕获,捕获的异常会被标记对应的请求阶段。
九、fetch
fetch
通过拦截整个方法
// 重写fetch
function instrumentFetch() {
if (!supportsNativeFetch()) {
return;
}
fill(global$2, 'fetch', function (originalFetch) {
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var handlerData = {
args: args,
fetchData: {
method: getFetchMethod(args),
url: getFetchUrl(args),
},
startTimestamp: Date.now(),
};
triggerHandlers('fetch', __assign({}, handlerData));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return originalFetch.apply(global$2, args).then(function (response) {
triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), response: response }));
return response;
}, function (error) {
triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), error: error }));
throw error;
});
};
});
}
十、console.log
function instrumentConsole() {
if (!('console' in global$2)) {
return;
}
['debug', 'info', 'warn', 'error', 'log', 'assert'].forEach(function (level) {
if (!(level in global$2.console)) {
return;
}
fill(global$2.console, level, function (originalConsoleLevel) {
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// 上报sentry
triggerHandlers('console', { args: args, level: level });
// this fails for some browsers. :(
if (originalConsoleLevel) {
Function.prototype.apply.call(originalConsoleLevel, global$2.console, args);
}
};
});
});
}
十一、Vue.config.errorHandler
// sentry中对Vue errorHandler的处理
function vuePlugin(Raven, Vue) {
var _oldOnError = Vue.config.errorHandler;
Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
// 上报
Raven.captureException(error, {
extra: metaData
});
if (typeof _oldOnError === 'function') {
// 为什么这么做?
_oldOnError.call(this, error, vm, info);
}
};
}
module.exports = vuePlugin;
十二、React ErrorBoundary
ErrorBoundary
的定义: 如果一个class
组件中定义了 static getDerivedStateFromError()
或componentDidCatch()
这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用static getDerivedStateFromError()
渲染备用 UI
,使用componentDidCatch()
打印错误信息
// ErrorBoundary的示例
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
this.setState({ hasError: true });
// 在这里可以做异常的上报
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
那么Sentry
是怎么实现的呢?
// ts声明的类型,可以看到sentry大概实现的方法
/**
* A ErrorBoundary component that logs errors to Sentry.
* Requires React >= 16
*/
declare class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
state: ErrorBoundaryState;
componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void;
componentDidMount(): void;
componentWillUnmount(): void;
resetErrorBoundary: () => void;
render(): React.ReactNode;
}
// 真实上报的地方
ErrorBoundary.prototype.componentDidCatch = function (error, _a) {
var _this = this;
var componentStack = _a.componentStack;
// 获取到配置的props
var _b = this.props, beforeCapture = _b.beforeCapture, onError = _b.onError, showDialog = _b.showDialog, dialogOptions = _b.dialogOptions;
withScope(function (scope) {
// 上报之前做一些处理,相当于axios的请求拦截器
if (beforeCapture) {
beforeCapture(scope, error, componentStack);
}
// 上报
var eventId = captureException(error, { contexts: { react: { componentStack: componentStack } } });
// 开发者的回调
if (onError) {
onError(error, componentStack, eventId);
}
// 是否显示sentry的错误反馈组件(也是一种收集错误的方式)
if (showDialog) {
showReportDialog(__assign(__assign({}, dialogOptions), { eventId: eventId }));
}
// componentDidCatch is used over getDerivedStateFromError
// so that componentStack is accessible through state.
_this.setState({ error: error, componentStack: componentStack, eventId: eventId });
});
};
十三、Axios
axios
通过请求/响应拦截器 注意:sentry
支持自动收集和手动收集两种错误收集方法,但是还不能捕捉到异步操作、接口请求中的错误,比如接口返回404
、500
等信息,此时我们可以通过Sentry.caputureException()
进行主动上报。