Sentry 是一个实时事件日志记录和聚集的平台。其专一于错误监控以及提取一切过后处理所需信息而不依赖于麻烦的用户反馈。它分为客户端和服务端,客户端(目前客户端有Javascript,Python, PHP,C#, Ruby等多种语言)就嵌入在你的应用程序中间,程序出现异常就向服务端发送消息,服务端将消息记录到数据库中并提供一个web页方便查看。Sentry由python编写,源码开放,性能卓越,易于扩展,目前著名的用户有Disqus, Path, mozilla, Pinterest等。javascript
sentry的集成与使用,推荐到sentry官网查询与学习,本篇文章只对其前端异常堆栈计算的核心逻辑进行梳理前端
sentry实现前端错误监控,经过对window.onerror、window.onunhandledrejection、计时器,延时器,对requestAnimationFrame,对浏览器中可能存在的基于发布订阅模式进行回调处理的函数进行包装重写,将前端未进行异常处理的错误,经过 'vendor/TraceKit/traceKit.js' 进行兼容处理,统一不一样浏览器环境下错误对象的差别(chrome,firefox,ie),输出统一的 stacktrace后,从新整理数据结构。再将最后处理事后的信息提交给sentry服务端处理java
使用raven-js导出的类Raven,调用其install方法初始化sentrynode
TraceKit.report.subscribe(function() {
self._handleOnErrorStackInfo.apply(self, arguments);
});
复制代码
if (self._globalOptions.captureUnhandledRejections // 为true) {
self._attachPromiseRejectionHandler();
}
复制代码
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]);
}
}
复制代码
fill(_window, 'setTimeout', wrapTimeFn, wrappedBuiltIns);
复制代码
fill(_window, 'setInterval', wrapTimeFn, wrappedBuiltIns);
复制代码
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
);
}
复制代码
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方法,将各错误对象转化为字符串,来肯定错误类型python
对于 [object ErrorEvent] [object Error] [object Exception] 错误对象,直接使用 TraceKit.computeStackTrace(统一跨浏览器的堆栈跟踪信息)方法 进行异常的堆栈跟踪,对于 [object Object] 非错误对象,进行兼容后再使用 TraceKit.computeStackTrace方法 进行异常的堆栈跟踪.react
else if (isPlainObject(ex)) {
options = this._getCaptureExceptionOptionsFromPlainObject(options, ex);
ex = new Error(options.message);
}
复制代码
对[object Object]的兼容webpack
_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;
}
复制代码
对异常进行堆栈跟踪计算git
try {
var stack = TraceKit.computeStackTrace(ex);
this._handleStackInfo(stack, options);
} catch (ex1) {
if (ex !== ex1) {
throw ex1;
}
}
复制代码
计算结果传递给Raven._handleStackInfo方法再次进行数据处理github
_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
);
},
复制代码
Raven._prepareFrames方法,处理堆栈错误,确认该堆栈错误是不是应用内部错误,并初步处理stacktrace.framesweb
_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;
},
复制代码
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;
},
复制代码
{
"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"
}
复制代码
{
"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
}
复制代码