成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

前端錯誤監控-Sentry自動捕獲前端應用異常原理

開發 前端
當應用代碼中調用 open 方法時,實際使用的是覆寫以后的 open 方法。在新的 open 方法內部,又覆寫了 onreadystatechange,這樣就可以收集到接口請求返回的結果。新的 open 方法內部會使用調用原生的 open 方法。

常見的前端異常及其捕獲方式

前端異常通常可以分為以下幾種類型:

  • js 代碼執行時異常;
  • promise 類型異常;
  • 資源加載類型異常;
  • 網絡請求類型異常;
  • 跨域腳本執行異常;
  • 不同類型的異常,捕獲方式不同。

js 代碼執行時異常

js 代碼執行異常,是我們經常遇到異常。這一類型的異常,又可以具體細分為:

  • Error,最基本的錯誤類型,其他的錯誤類型都繼承自該類型。通過 Error,我們可以自定義 Error 類型。
  • RangeError: 范圍錯誤。當出現堆棧溢出(遞歸沒有終止條件)、數值超出范圍(new Array 傳入負數或者一個特別大的整數)情況時會拋出這個異常。
  • ReferenceError,引用錯誤。當一個不存在的對象被引用時發生的異常。
  • SyntaxError,語法錯誤。如變量以數字開頭;花括號沒有閉合等。
  • TypeError,類型錯誤。如把 number 當 str 使用。
  • URIError,向全局 URI 處理函數傳遞一個不合法的 URI 時,就會拋出這個異常。如使用 decodeURI('%')、decodeURIComponent('%')。
  • EvalError, 一個關于 eval 的異常,不會被 javascript 拋出。

具體詳見: Error - JavaScript - MDN Web Docs - Mozilla

通常,我們會通過 try...catch 語句塊來捕獲這一類型異常。如果不使用 try...catch,我們也可以通過 window.onerror = callback 或者 window.addEventListener('error', callback) 的方式進行全局捕獲。

promise 類異常

在使用 promise 時,如果 promise 被 reject 但沒有做 catch 處理時,就會拋出 promise 類異常。

Promise.reject(); // Uncaught (in promise) undefined

promise 類型的異常無法被 try...catch 捕獲,也無法被 window.onerror = callback 或者 window.addEventListener('error', callback) 的方式全局捕獲。針對這一類型的異常, 我們需要通過 window.onrejectionhandled = callback 或者 window.addListener('rejectionhandled', callback) 的方式去全局捕獲。

靜態資源加載類型異常

如果我們頁面的img、js、css 等資源鏈接失效,就會提示資源類型加載如異常。

<img src="localhost:3000/data.png" /> // Get localhost:3000/data.png net::ERR_FILE_NOT_FOUND

針對這一類的異常,我們可以通過 window.addEventListener('error', callback, true) 的方式進行全局捕獲。

這里要注意一點,使用 window.onerror = callback 的方式是無法捕獲靜態資源類異常的。

原因是資源類型錯誤沒有冒泡,只能在捕獲階段捕獲,而 window.onerror 是通過在冒泡階段捕獲錯誤,對靜態資源加載類型異常無效,所以只能借助 window.addEventListener('error', callback, true) 的方式捕獲。

接口請求類型異常

在瀏覽器端發起一個接口請求時,如果請求的 url 的有問題,也會拋出異常。

不同的請求方式,異常捕獲方式也不相同:

  • 接口調用是通過 fetch 發起的

我們可以通過 fetch(url).then(callback).catch(callback) 的方式去捕獲異常。

  • 接口調用通過 xhr 實例發起

如果是 xhr.open 方法執行時出現異常,可以通過 window.addEventListener('error', callback) 或者 window.onerror 的方式捕獲異常。

xhr.open('GET', "https://")  // Uncaught DOMException: Failed to execute 'open' on 'XMLHttpRequest': Invalid URL
at ....

如果是 xhr.send 方法執行時出現異常,可以通過 xhr.onerror 或者 xhr.addEventListener('error', callback) 的方式捕獲異常。

xhr.open('get''/user/userInfo');
xhr.send(); // send localhost:3000/user/userinfo net::ERR_FAILED

跨域腳本執行異常

當項目中引用的第三方腳本執行發生錯誤時,會拋出一類特殊的異常。這類型異常和我們剛才講過的異常都不同,它的 msg 只有 'Script error' 信息,沒有具體的行、列、類型信息。

之以會這樣,是因為瀏覽器的安全機制: 瀏覽器只允許同域下的腳本捕獲具體異常信息,跨域腳本中的異常,不會報告錯誤的細節。

針對這類型的異常,我們可以通過 window.addEventListener('error', callback) 或者 window.onerror 的方式捕獲異常。

如果我們想獲取這類異常的詳情,需要做以下兩個操作:

  • 在發起請求的 script 標簽上添加 crossorigin="anonymous";
  • 請求響應頭中添加 Access-Control-Allow-Origin: *;

這樣就可以獲取到跨域異常的細節信息了。

Sentry 異常監控原理

有效的異常監控需要哪些必備要素

異常監控的核心作用就是通過上報的異常,幫開發人員及時發現線上問題并快速修復。

要達到這個目的,異常監控需要做到以下 3 點:

線上應用出現異常時,可以及時推送給開發人員,安排相關人員去處理。

上報的異常,含有異常類型、發生異常的源文件及行列信息、異常的追蹤棧信息等詳細信息,可以幫助開發人員快速定位問題。

可以獲取發生異常的用戶行為,幫助開發人員、測試人員重現問題和測試回歸。

這三點,分別對應異常自動推送、異常詳情獲取、用戶行為獲取。

異常詳情獲取

為了能自動捕獲應用異常,Sentry 劫持覆寫了 window.onerror 和 window.unhandledrejection 這兩個 api。

劫持覆寫 window.onerror 的代碼如下:

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.unhandledrejection 的代碼如下:

oldOnUnhandledRejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = function (e) {
// 收集異常信息并上報
triggerHandlers('unhandledrejection', e);
if (oldOnUnhandledRejectionHandler) {
return oldOnUnhandledRejectionHandler.apply(this, arguments);
}
return true;
};

雖然通過劫持覆寫 window.onerror 和 window.unhandledrejection 已足以完成異常自動捕獲,但為了能獲取更詳盡的異常信息, Sentry 在內部做了一些更細微的異常捕獲。

具體來說,就是 Sentry 內部對異常發生的特殊上下文,做了標記。這些特殊上下文包括: dom 節點事件回調、setTimeout / setInterval 回調、xhr 接口調用、requestAnimationFrame 回調等。

舉個 ,如果是 click 事件的 handler 中發生了異常, Sentry 會捕獲這個異常,并將異常發生時的事件 name、dom 節點描述、handler 函數名等信息上報。

具體處理邏輯如下:

  • 標記 setTimeout / setInterval / requestAnimationFrame
  • 為了標記 setTimeout / setInterval / requestAnimationFrame 類型的異常,Sentry 劫持覆寫了原生的 setTimout / setInterval / requestAnimationFrame 方法。新的 setTimeout / setInterval / requestAnimationFrame 方法調用時,會使用 try ... catch 語句塊包裹 callback。

具體實現如下:

var 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、requestAnimationFrame 的劫持覆寫邏輯和 setTimeout 基本一樣,這里就不再重復說明了,感興趣的小伙伴們可自行實現。
  • 標記 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 等信息。

其實,除了標記 dom 事件回調上下文,Sentry 還可以標記 Notification、WebSocket、XMLHttpRequest 等對象的事件回調上下文。可以這么說,只要一個對象有 addEventListener 方法并且可以被劫持覆寫,那么對應的回調上下文會可以被標記。

標記 xhr 接口回調

為了標記 xhr 接口回調,Sentry 先對 XMLHttpRequest.prototype.send 方法劫持覆寫, 等 xhr 實例使用覆寫以后的 send 方法時,再對 xhr 對象的 onload、onerror、onprogress、onreadystatechange 方法進行了劫持覆寫, 使用 try ... catch 語句塊包裹傳入的 callback。

具體代碼如下:

fill(XMLHttpRequest.prototype, 'send', _wrapXHR);

function _wrapXHR(originalSend) {
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var xhr = this;
var xmlHttpRequestProps = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
// 劫持覆寫
xmlHttpRequestProps.forEach(function (prop) {
if (prop in xhr && typeof xhr[prop] === 'function') {
// 覆寫
fill(xhr, prop, function (original) {
var wrapOptions = {
mechanism: {
data: {
// 回調觸發的階段
function: prop,
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
};
var originalFunction = getOriginalFunction(original);
if (originalFunction) {
wrapOptions.mechanism.data.handler = getFunctionName(originalFunction);
}
return wrap$1(original, wrapOptions);
});
}
});
return originalSend.apply(this, args);
};

  • 當 callback 內部發生異常時,會被 catch 捕獲,捕獲的異常會被標記對應的請求階段。

有了這些回調上下文信息的幫助,定位異常就更加方便快捷了。

用戶行為獲取

常見的用戶行為,可以歸納為頁面跳轉、鼠標 click 行為、鍵盤 keypress 行為、 fetch / xhr 接口請求、console 打印信息。

Sentry 接入應用以后,會在用戶使用應用的過程中,將上述行為一一收集起來。等到捕獲到異常時,會將收集到的用戶行為和異常信息一起上報。

那 Sentry 是怎么實現收集用戶行為的呢?答案: 劫持覆寫上述操作涉及的 api。

具體實現過程如下:

收集頁面跳轉行為

為了可以收集用戶頁面跳轉行為,Sentry 劫持并覆寫了原生 history 的 pushState、replaceState 方法和 window 的 onpopstate。

劫持覆寫 onpopstate

// 使用 oldPopState 變量保存原生的 onpopstatevar oldPopState = window.onpopstate;
var lastHref;
// 覆寫 onpopstatewindow.onpopstate = function() {
...
var to = window.location.href;
var from = lastHref;
lastHref = to;
// 將頁面跳轉行為收集起來triggerHandlers('history', {
from: from,
to: to,
});
if (oldOnPopState) {
try {
// 使用原生的 popstate return oldOnPopState.apply(this, args);
} catch (e) {
...
}
}
...
}
復制代碼

劫持覆寫 pushState、replaceState

// 保存原生的 pushState 方法
var originPushState = window.history.pushState;
// 保存原生的 replaceState 方法
var originReplaceState = window.history.replaceState;

// 劫持覆寫 pushState
window.history.pushState = function() {
var args = [];
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
var url = args.length > 2 ? args[2] : undefined;
if (url) {
var from = lastHref;
var to = String(url);lastHref = to;
// 將頁面跳轉行為收集起來
triggerHandlers('history', {
from: from,
to: to,
});
}
// 使用原生的 pushState 做頁面跳轉
return originPushState.apply(this, args);
}

// 劫持覆寫 replaceState
window.history.replaceState = function() {
var args = [];
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i];
}
var url = args.length > 2 ? args[2] : undefined;
if (url) {
var from = lastHref;
var to = String(url);lastHref = to;
// 將頁面跳轉行為收集起來
triggerHandlers('history', {
from: from,
to: to,
});
}
// 使用原生的 replaceState 做頁面跳轉
return originReplaceState.apply(this, args);
}

收集鼠標 click / 鍵盤 keypress 行為

  • 為了收集用戶鼠標 click 和鍵盤 keypress 行為, Sentry 做了雙保險操作:通過 document 代理 click、keypress 事件來收集 click、keypress 行為;通過劫持 addEventListener 方法來收集 click、keypress 行為;

相關代碼實現如下:

function instrumentDOM() {
...
// triggerDOMHandler 用來收集用戶 click / keypress 行為var triggerDOMHandler = triggerHandlers.bind(null, 'dom');
var globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true);

// 通過 document 代理 click、keypress 事件的方式收集 click、keypress 行為document.addEventListener('click', globalDOMEventHandler, false);
document.addEventListener('keypress', globalDOMEventHandler, false);

['EventTarget', 'Node'].forEach(function (target) {
var proto = window[target] && window[target].prototype;
if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) {
return;
}

// 劫持覆寫 Node.prototype.addEventListener 和 EventTarget.prototype.addEventListenerfill(proto, 'addEventListener', function (originalAddEventListener) {

// 返回新的 addEventListener 覆寫原生的 addEventListenerreturn function (type, listener, options) {

// click、keypress 事件,要做特殊處理,if (type === 'click' || type == 'keypress') {
try {
var el = this;
var handlers_1 = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {});
var handlerForType = (handlers_1[type] = handlers_1[type] || { refCount: 0 });
// 如果沒有收集過 click、keypress 行為if (!handlerForType.handler) {
var handler = makeDOMEventHandler(triggerDOMHandler);
handlerForType.handler = handler;
originalAddEventListener.call(this, type, handler, options);
}
handlerForType.refCount += 1;
}
catch (e) {
// Accessing dom properties is always fragile.// Also allows us to skip `addEventListenrs` calls with no proper `this` context.
}
}
// 使用原生的 addEventListener 方法注冊事件return originalAddEventListener.call(this, type, listener, options);
};
});
...
});
}

整個實現過程還是非常巧妙的,很值得拿來細細說明。

首先, Sentry 使用 document 代理了 click、keypress 事件。通過這種方式,用戶的 click、keypress 行為可以被感知,然后被 Sentry 收集。

但這種方式有一個問題,如果應用的 dom 節點是通過 addEventListener 注冊了 click、keypress 事件,并且在事件回調中做了阻止事件冒泡的操作,那么就無法通過代理的方式監控到 click、keypress 事件了。

針對這一種情況, Sentry 采用了覆寫Node.prototype.addEventListener 的方式來監控用戶的 click、keypress 行為。

由于所有的 dom 節點都繼承自 Node 對象,Sentry 劫持覆寫了Node.prototype.addEventListener。當應用代碼通過 addEventListener 訂閱事件時,會使用覆寫以后的 addEventListener 方法。

新的 addEventListener 方法,內部里面也有很巧妙的實現。如果不是 click、keypress 事件,會直接使用原生的 addEventListener 方法注冊應用提供的 listener。但如果是 click、keypress 事件,除了使用原生的 addEventListener 方法注冊應用提供的 listener 外,還使用原生 addEventListener 注冊了一個 handler,這個 handler 執行的時候會將用戶 click、keypress 行為收集起來。

也就是說,如果是 click、keypress 事件,應用程序在調用 addEventListener 的時候,實際上是調用了兩次原生的 addEventListener。

另外,在收集 click、keypress 行為時,Sentry 還會把 target 節點的的父節點信息收集起來,幫助我們快速定位節點位置

收集 fetch / xhr 接口請求行為

同理,為了收集應用的接口請求行為,Sentry 對原生的 fetch 和 xhr 做了劫持覆寫。

劫持覆寫 fetch

var originFetch = window.fetch;

window.fetch = function() {

var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// 獲取接口 url、method 類型、參數、接口調用時間信息var handlerData = {
args: args,
fetchData: {
method: getFetchMethod(args),
url: getFetchUrl(args),
},
startTimestamp: Date.now(),
};
// 收集接口調用信息triggerHandlers('fetch', __assign({}, handlerData));
return originalFetch.apply(window, 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;
});
}
復制代碼

應用中使用 fetch 發起請求時,實際使用的是新的 fetch 方法。新的 fetch 內部,會使用原生的 fetch 發起請求,并收集接口請求數據和返回結果。

劫持覆寫 xhr

function instrumentXHR() {
...
var xhrproto = XMLHttpRequest.prototype;
// 覆寫 XMLHttpRequest.prototype.openfill(xhrproto, 'open', function (originalOpen) {
return function () {
...
var onreadystatechangeHandler = function () {
if (xhr.readyState === 4) {
...

// 收集接口調用結果triggerHandlers('xhr', {
args: args,
endTimestamp: Date.now(),
startTimestamp: Date.now(),
xhr: xhr,
});
}
};
// 覆寫 onreadystatechangeif ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
fill(xhr, 'onreadystatechange', function (original) {
return function () {
var readyStateArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
readyStateArgs[_i] = arguments[_i];
}
onreadystatechangeHandler();
return original.apply(xhr, readyStateArgs);
};
});
}
else {
xhr.addEventListener('readystatechange', onreadystatechangeHandler);
}
return originalOpen.apply(xhr, args);
};
});

// 覆寫 XMLHttpRequest.prototype.sendfill(xhrproto, 'send', function (originalSend) {
return function () {
...
// 收集接口調用行為triggerHandlers('xhr', {
args: args,
startTimestamp: Date.now(),
xhr: this,
});
return originalSend.apply(this, args);
};
});
}

復制代碼

Sentry 是通過劫持覆寫 XMLHttpRequest 原型上的 open、send 方法的方式來實現收集接口請求行為的。

當應用代碼中調用 open 方法時,實際使用的是覆寫以后的 open 方法。在新的 open 方法內部,又覆寫了 onreadystatechange,這樣就可以收集到接口請求返回的結果。新的 open 方法內部會使用調用原生的 open 方法。

同樣的,當應用代碼中調用 send 方法時,實際使用的是覆寫以后的 send 方法。新的 send 方法內部先收集接口調用信息,然后調用原生的 send 方法。

收集 console 打印行為

有了前面的鋪墊,console 行為的收集機制理解起來就非常簡單了,實際就是對 console 的 debug、info、warn、error、log、assert 這借個 api 進行劫持覆寫。

代碼如下:

var originConsoleLog = console.log;console.log = function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
// 收集 console.log 行為
triggerHandlers('console', { args: args, level: 'log' });
if (originConsoleLog) {
originConsoleLog.apply(console, args);
}
}

文章出自:??前端餐廳ReTech??,如有轉載本文請聯系前端餐廳ReTech今日頭條號。

github:https://github.com/zuopf769

責任編輯:武曉燕 來源: 今日頭條
相關推薦

2022-08-16 10:44:11

Sentry前端異常

2022-11-16 09:03:35

Sentry前端監控

2017-05-04 21:30:32

前端異常監控捕獲方案

2023-08-10 13:46:48

前端資源優化

2023-03-01 09:07:44

前端監控異常

2018-09-14 16:20:37

2021-01-19 12:00:39

前端監控代碼

2012-11-05 13:59:12

WebFdSafeJS

2015-08-20 10:23:23

前端代碼日志收集

2024-11-06 11:15:59

2020-09-04 13:50:35

前端異常監控代碼

2022-06-10 14:09:18

前端監控異常數據

2019-07-15 07:58:10

前端開發技術

2022-03-15 21:38:29

sentry微服務監控

2021-12-15 20:06:48

ReactJSSentry開發者

2021-06-30 19:48:21

前端自動化測試Vue 應用

2020-11-10 09:19:23

Spring BootJava開發

2015-02-03 14:45:55

android全局異常

2021-09-14 23:50:17

Sentry后端監控

2021-06-26 07:40:21

前端自動化測試Jest
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 久久高清 | 国产剧情一区 | www.日本国产 | 久久国产欧美日韩精品 | 久久久久国产精品 | 午夜精品久久久久久久久久久久久 | 久久999 | 国产精品视频网 | 久草免费在线视频 | 免费同性女女aaa免费网站 | 亚洲精品黄色 | 北条麻妃一区二区三区在线观看 | 一区二区三区在线 | 欧 | 精品香蕉一区二区三区 | 日韩午夜一区二区三区 | 日本一区二区影视 | 日本精品在线播放 | 毛片国产| 成人在线免费av | 欧美视频 亚洲视频 | 日本网站在线看 | 在线色网站 | 日韩av成人 | 日韩在线播放视频 | 一区二区三区免费 | 国产精品成人一区二区三区 | 男插女下体视频 | 九九热在线免费视频 | 综合第一页 | 九九亚洲| 无码日韩精品一区二区免费 | 久久伊人精品一区二区三区 | 国产一区久久久 | 国产精品久久久久久久久久久久 | 91超碰在线观看 | 国产精品一区二区电影 | 99久久免费精品 | 久久久久久久久久影视 | 午夜欧美一区二区三区在线播放 | 亚洲免费网 | 国产精品自拍视频 |