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
}
複製代碼