Qiankun Sentry 監(jiān)控異常上報(bào)無(wú)法自動(dòng)區(qū)分項(xiàng)目解決
前言
最近項(xiàng)目組決定將前端異常監(jiān)控由 Fundebug 切換為 Sentry。整個(gè)切換過(guò)程可以說(shuō)非常簡(jiǎn)單,部署一個(gè)后臺(tái)服務(wù),然后將 Sentry SDK 集成到前端應(yīng)用中就完事兒了。在之后的使用過(guò)程中,小編遇到了一個(gè)問(wèn)題。由于我們的項(xiàng)目采用的是基于 qiankun 的微前端架構(gòu),在應(yīng)用使用過(guò)程中,常常會(huì)出現(xiàn)發(fā)生異常應(yīng)用和上報(bào)應(yīng)用不匹配的情況。
為了解決這個(gè)問(wèn)題,小編先去 qiankun 的 issue 下翻了翻,看有沒(méi)有好的解決方案。雖然也有不少人遇到了同樣的問(wèn)題 - 求教一下 主子應(yīng)用的sentry應(yīng)該如何實(shí)踐 #1088,但是社區(qū)里并沒(méi)有一個(gè)好的解決方案。于是乎小編決定自己去閱讀 Sentry 源碼和官方文檔,期望能找到一種合理并通用的解決方案。
經(jīng)過(guò)一番梳理,小編如愿找到了解決方案,并且效果還不錯(cuò)。接下來(lái)小編就帶著大家一起了解一下整個(gè)解決方案的具體情況。
使用 Sentry 上報(bào)異常
在正式介紹解決方案之前,小編先帶大家簡(jiǎn)單回顧一下一個(gè)前端應(yīng)用是如何接入 Sentry 的。
第一步,在 Sentry 監(jiān)控平臺(tái)構(gòu)建一個(gè)項(xiàng)目

項(xiàng)目創(chuàng)建好以后,會(huì)自動(dòng)生成一個(gè) dsn,這個(gè) dsn 會(huì)在前端項(xiàng)目接入 Sentry 時(shí)作為必填項(xiàng)傳入。
第二步,前端應(yīng)用接入 Sentry
前端應(yīng)用接入 Sentry 也非常簡(jiǎn)單,只要使用 Sentry 提供的 init api,傳入必傳的 dsn 就可以了。
import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import App from "./App";
Sentry.init({
dsn: "https://90eb5fc98bf447a3bdc38713cc253933@sentry.byai.com/66",
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 1.0,
});
ReactDOM.render(<App />, document.getElementById("root"));
經(jīng)過(guò)這兩步,前端應(yīng)用的異常監(jiān)控接入就完成了。當(dāng)應(yīng)用在使用時(shí),如果發(fā)生異常,Sentry 會(huì)自動(dòng)捕獲異常,然后上報(bào)到監(jiān)控平臺(tái)。上報(bào)完成以后,我們就可以在項(xiàng)目的 issues 中查看異常并著手修復(fù)。
單個(gè)的 Spa 應(yīng)用接入 Sentry 時(shí)按照上面的步驟無(wú)腦操作就可以了,但如果應(yīng)用是基于 qiankun 的微前端架構(gòu),那就需要解決異常上報(bào)不匹配的問(wèn)題了。
小編手上的項(xiàng)目就是采用了基于 qiankun 的微前端架構(gòu),一個(gè)頁(yè)面會(huì)至少同時(shí)存在兩個(gè)應(yīng)用,有時(shí)甚至?xí)?3 到 4 個(gè)應(yīng)用。在應(yīng)用使用過(guò)程中,常常會(huì)出現(xiàn)異常上報(bào)不匹配的問(wèn)題。

如上圖所示,主應(yīng)用、cc sdk 應(yīng)用中的異常都會(huì)上報(bào)到 aicc 項(xiàng)目中,這給異常處理帶來(lái)很大的困擾。
出現(xiàn)這個(gè)問(wèn)題的原因也非常好理解。
Sentry 在執(zhí)行 init 方法時(shí)會(huì)通過(guò)覆寫(xiě) window.onerror、window.unhandledrejection 的方式初始化異常捕獲邏輯。之后不管是哪個(gè)應(yīng)用發(fā)生異常,都最終會(huì)觸發(fā) onerror、unhandledrejection 的 callback 而被 Sentry 感知,然后上報(bào)到 dsn 指定的項(xiàng)目中。而且 Sentry 的 init 代碼不管是放在主應(yīng)用中,還是放在子應(yīng)用里面,都沒(méi)有質(zhì)的改變,所有被捕獲的異常還是會(huì)一股腦的上報(bào)到某個(gè)項(xiàng)目中,無(wú)法自動(dòng)區(qū)分。
了解了異常上報(bào)無(wú)法自動(dòng)區(qū)分的問(wèn)題,接下來(lái)小編就給大家講一下自己是如何解決這個(gè)問(wèn)題的。
解決方案
想要解決這個(gè)問(wèn)題,我們必須要先找到問(wèn)題的切入點(diǎn),而異常上報(bào)時(shí)的接口調(diào)用就是這個(gè)切入點(diǎn)。
當(dāng) Sentry 捕獲到應(yīng)用產(chǎn)生的異常時(shí),會(huì)調(diào)用一個(gè)接口來(lái)上報(bào)異常,如下:

對(duì)比這個(gè)接口的 url 和上報(bào)應(yīng)用的 dsn,我們可以發(fā)現(xiàn)異常上報(bào)接口的 url 其實(shí)是由上報(bào)應(yīng)用的 dsn 轉(zhuǎn)化來(lái)的,轉(zhuǎn)化過(guò)程如下:
// https://62187b367e474822bb9cb733c8a89814@sentry.byai.com/56
dsn - https://{param1}@{param2}/{param3}
|
|
v
url - https://{param2}/api/{param3}/store/?sentry_key={param1}&sentry_version=7
我們?cè)賮?lái)看看這個(gè)上報(bào)接口攜帶的參數(shù):

在接口參數(shù)中,exceptions.values[0].stacktrace.frames 是異常的追蹤棧信息。通過(guò)棧信息中的 filename 字段,我們可以知道發(fā)生異常的 js 文件的 url。通常情況下,微前端中各個(gè)子應(yīng)用的 js 的 url 前綴是不相同的(各個(gè)子應(yīng)用靜態(tài)文件的位置是分離的),那么根據(jù)發(fā)生異常的 js 的 url 就可以判斷該異常屬于哪個(gè)應(yīng)用。
有了上面兩個(gè)信息,異常上報(bào)自動(dòng)區(qū)分的解決方案就清晰明了了:
- 第一步攔截異常上報(bào)接口,拿到異常詳情,根據(jù)追蹤棧中的
filename判斷異常屬于哪個(gè)應(yīng)用; - 第二步,根據(jù)匹配應(yīng)用的
dsn重新構(gòu)建url; - 第三步,使用新的
url上報(bào)異常;
在這個(gè)方案中,最關(guān)鍵的是攔截異常上報(bào)接口。為了能實(shí)現(xiàn)這一步,小編進(jìn)行了各種嘗試。
失敗的方案一
由于 Sentry 異常上報(bào)是通過(guò) window.fetch(url, options) 來(lái)實(shí)現(xiàn)的,所以我們可以通過(guò)覆寫(xiě) window.fetch 的方式去攔截異常上報(bào)。
代碼實(shí)現(xiàn)如下:
const originFetch = window.fetch;
window.fetch = (url, options) => {
// 根據(jù) options 中的異常信息,返回新的 url 和 options
const [newUrl, newOptions] = sentryFilter(url, options);
// 使用原生的 fetch
return originFetch(newUrl, newOptions);
}
該方案看起來(lái)很靠譜,然而在實(shí)際使用的時(shí)候并未發(fā)揮作用,原因是 Sentry 內(nèi)部只會(huì)使用原生的 fetch。如果發(fā)現(xiàn) fetch 方法被覆寫(xiě),那么 Sentry 會(huì)通過(guò)自己的方式重新去獲取原生的 fetch。
小編截取了 Sentry 的部分源碼給大家看一下:
// FetchTransport 是一個(gè)構(gòu)造函數(shù)
// Sentry 在執(zhí)行 init 方法時(shí)會(huì)構(gòu)建一個(gè) FetchTransport 實(shí)例,然后通過(guò)這個(gè) FetchTransport 實(shí)例調(diào)用 window.fetch 方法去做異常上報(bào)
function FetchTransport(options, fetchImpl) {
if (fetchImpl === void 0) { fetchImpl = getNativeFetchImplementation(); }
var _this = _super.call(this, options) || this;
_this._fetch = fetchImpl;
return _this;
}
// 使用原生的 window.fetch 實(shí)現(xiàn) FetchTransport
function getNativeFetchImplementation() {
if (cachedFetchImpl) {
return cachedFetchImpl;
}
// 根據(jù) isNativeFetch 來(lái)判斷 window.fetch 是否被覆寫(xiě)
if (isNativeFetch(global$7.fetch)) {
return (cachedFetchImpl = global$7.fetch.bind(global$7));
}
var document = global$7.document;
var fetchImpl = global$7.fetch;
// 如果被覆寫(xiě),借助 iframe 獲取原生的 window.fetch
if (document && typeof document.createElement === 'function') {
try {
var sandbox = document.createElement('iframe');
sandbox.hidden = true;
document.head.appendChild(sandbox);
var contentWindow = sandbox.contentWindow;
if (contentWindow && contentWindow.fetch) {
fetchImpl = contentWindow.fetch;
}
document.head.removeChild(sandbox);
}
catch (e) {
logger.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', e);
}
}
return (cachedFetchImpl = fetchImpl.bind(global$7));
}
// 判斷 window.fetch 是否已經(jīng)被覆寫(xiě)
function isNativeFetch(func) {
return func && /^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString());
}
由于 Sentry 內(nèi)部有一套邏輯來(lái)保證 fetch 必須為原生方法,所以覆寫(xiě) window.fetch 的方案失敗, pass !
不通用的方案二
既然覆寫(xiě) window.fetch 的方案行不通,那我們就重新想辦法。
觀察上面的 FetchTransport 的入?yún)?。如果沒(méi)有指定 fetchImpl,Sentry 會(huì)通過(guò) getNativeFetchImplementation 來(lái)實(shí)現(xiàn)一個(gè) fetchImpl。那我們主動(dòng)給 FetchTransport 傳遞覆寫(xiě)以后的 fetch 方法,不就可以做到攔截 fetch 調(diào)用了嗎?
這個(gè)方案看起來(lái)也很靠譜,趕緊試一下,??。
從 FetchTransport 追本溯源,小編找到了 FetchTransport 方法調(diào)用的位置:
BrowserBackend.prototype._setupTransport = function () {
if (!this._options.dsn) {
return _super.prototype._setupTransport.call(this);
}
var transportOptions = __assign(__assign({}, this._options.transportOptions), { dsn: this._options.dsn, tunnel: this._options.tunnel, sendClientReports: this._options.sendClientReports, _metadata: this._options._metadata });
var api = initAPIDetails(transportOptions.dsn, transportOptions._metadata, transportOptions.tunnel);
var url = getEnvelopeEndpointWithUrlEncodedAuth(api.dsn, api.tunnel);
if (this._options.transport) {
return new this._options.transport(transportOptions);
}
if (supportsFetch()) {
var requestOptions = __assign({}, transportOptions.fetchParameters);
this._newTransport = makeNewFetchTransport({ requestOptions: requestOptions, url: url });
// 使用 FetchTransport 的位置
return new FetchTransport(transportOptions);
}
this._newTransport = makeNewXHRTransport({
url: url,
headers: transportOptions.headers,
});
return new XHRTransport(transportOptions);
};
在上面的這段代碼中, this._options 就是我們執(zhí)行 Sentry.init 時(shí)的入?yún)?。觀看 FetchTransport 調(diào)用的地方,由于沒(méi)有 fetchImpl 的入?yún)?,所?Sentry 會(huì)通過(guò) getNativeFetchImplementation 來(lái)實(shí)現(xiàn) fetchImpl。
既然這樣,那我們可以在 Sentry.init 方法執(zhí)行的時(shí)候添加一個(gè) fetchImpl 入?yún)?,然后在調(diào)用 FetchTransport 方法時(shí)傳入。
改造后的代碼如下:
// 改動(dòng) Sentry 源碼
BrowserBackend.prototype._setupTransport = function () {
...
if (supportsFetch()) {
var requestOptions = __assign({}, transportOptions.fetchParameters);
this._newTransport = makeNewFetchTransport({ requestOptions: requestOptions, url: url });
return new FetchTransport(transportOptions, this._options.fetchImpl);
}
...
}
// 業(yè)務(wù)代碼
const originFetch = window.fetch;
// Sentry.init 執(zhí)行
Sentry.init({
dsn: 'xxx',
...
fetchImpl: (url, options) => {
// 根據(jù) options 中的異常信息,返回新的 url 和 options
const [newUrl, newOptions] = sentryFilter(url, options);
// 使用原生的 fetch
return originFetch(newUrl, newOptions);
}
...
});
經(jīng)驗(yàn)證,該方案可正常工作,捕獲的異常都可自動(dòng)上報(bào)到對(duì)應(yīng)的應(yīng)用中,問(wèn)題解決,happy ??。
不過(guò)興奮過(guò)后,再回過(guò)頭來(lái)看看這個(gè)方案,發(fā)現(xiàn)其實(shí)槽點(diǎn)還是蠻多的:
- 要修改
Sentry源碼,重新生成一個(gè)內(nèi)部npm包; - 如果
Sentry版本升級(jí),必須再次修改源碼, 很不方便;
總體來(lái)說(shuō),這個(gè)方案雖然能解決問(wèn)題,但是不夠通用,不夠優(yōu)雅。作為一名有追求的 ??????,小編當(dāng)然不能僅僅止步于實(shí)現(xiàn)功能,還得想辦法實(shí)現(xiàn)的更好,于是就有了接下來(lái)的方案三。
合理、優(yōu)雅的方案三
還是看上面 BrowserBackend.prototype._setupTransport 源碼,有這樣一段邏輯:
...
if (this._options.transport) {
return new this._options.transport(transportOptions);
}
...
如果在 Sentry.init 執(zhí)行時(shí),配置了 transport,那么就會(huì)使用該 transport 方法來(lái)初始化上報(bào)異常需要的 transport 實(shí)例。
既然這樣,那我們自己定義一個(gè) CustomeTransport 構(gòu)造函數(shù)不就可以了么。另外,小編在 Sentry 暴露給外面的 exports 中發(fā)現(xiàn)了 FetchTransport,那么 CustomeTransport 就可以通過(guò)繼承 FetchTransport 來(lái)實(shí)現(xiàn)。
具體方案如下:
import { Transports, init } from '@sentry/browser';
const fetchImpl = (url, options) => {
// 根據(jù) options 中的異常信息,返回新的 url 和 options
const [newUrl, newOptions] = sentryFilter(url, options);
// 使用原生的 fetch
return originFetch(newUrl, newOptions);
}
class CustomerTransport extends Transports.FetchTransport {
constructor(options) {
super(options, fetchImpl)
}
}
init({
dsn: 'xxxx',
...
transport: CustomerTransport
});
經(jīng)驗(yàn)證,該方案可正常工作,捕獲的異常都可自動(dòng)上報(bào)到對(duì)應(yīng)的應(yīng)用中,而且不用像方案二那樣修改 Sentry 源碼,優(yōu)雅、通用,問(wèn)題真正解決,perfect ??。
7.x 版本解決方案
上面的方案三只針對(duì) 6.x 版本。如果大家使用的 Sentry 是最新的 7.x 版本,小編也設(shè)計(jì)了相應(yīng)的解決方案。
import { init, makeFetchTransport } from '@sentry/browser';
const CustomeTransport = (options) => {
const fetchImpl = (url, options) => {
const [newUrl, newOptions] = sentryFilter(url, options);
return window.fetch(newUrl, newOptions);
};
return makeFetchTransport(options, fetchImpl);
};
init({
dsn: 'https://525053cc037e42bcb981670e97a0a821@sentry.byai.com/52',
...
transport: CustomeTransport,
});
親測(cè)可用哦!
結(jié)束語(yǔ)
也許有小伙伴會(huì)問(wèn),如果瀏覽器不支持 fetch 的話,那上面說(shuō)的方案不就沒(méi)有用了嗎?
其實(shí)大可不必?fù)?dān)心這一點(diǎn)。
首先,當(dāng)前主流瀏覽器,除了比較老的版本,已經(jīng)實(shí)現(xiàn)了對(duì) fetch 的支持。如果有些小伙伴的瀏覽器實(shí)在不支持 fetch,那也沒(méi)有關(guān)系。由于 Sentry 內(nèi)部沒(méi)有要求 xhr 必須使用原生的 send 方法,所以我們可以通過(guò)覆寫(xiě) XMLHttpRequest 原型鏈上的 send 方法來(lái)實(shí)現(xiàn)對(duì)異常上報(bào)的攔截,具體的實(shí)現(xiàn)過(guò)程就由小伙伴們自行研究了
以上就是Qiankun Sentry 監(jiān)控異常上報(bào)無(wú)法自動(dòng)區(qū)分項(xiàng)目解決的詳細(xì)內(nèi)容,更多關(guān)于Qiankun Sentry 監(jiān)控異常區(qū)分的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
微信小程序中頁(yè)面FOR循環(huán)和嵌套循環(huán)
這篇文章主要介紹了微信小程序中頁(yè)面FOR循環(huán)和嵌套循環(huán)的相關(guān)資料,需要的朋友可以參考下2017-06-06
js 實(shí)現(xiàn)驗(yàn)證碼輸入框示例詳解
這篇文章主要為大家介紹了js 實(shí)現(xiàn)驗(yàn)證碼輸入框示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
微信小程序開(kāi)發(fā)之相冊(cè)選擇和拍照詳解及實(shí)例代碼
這篇文章主要介紹了微信小程序開(kāi)發(fā)之相冊(cè)選擇和拍照詳解及實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-02-02
競(jìng)態(tài)條件Race condition及如何避免的三種方案詳解
這篇文章主要為大家介紹了競(jìng)態(tài)條件Race condition及如何避免的三種方案詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
JavaScript?執(zhí)行上下文的視角詳解this使用
這篇文章主要為以JavaScript?執(zhí)行上下文的視角為大家講清楚?this使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
Svelte嵌套組件preventDefault構(gòu)建Web應(yīng)用
這篇文章主要介紹了Svelte嵌套組件preventDefault構(gòu)建Web應(yīng)用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

