本文使用「署名 4.0 国际 (CC BY 4.0)」 许可协议,欢迎转载、或重新修改使用,但需要注明来源。
作者: 百应前端团队 @0o华仔o0 首发于 如何优雅的解决 Qiankun 下 Sentry 异常上报无法自动区分项目的问题 ?
前言
最近项目组决定将前端异常监控由 Fundebug
切换为 Sentry
。整个切换过程可以说非常简单,部署一个后台服务,然后将 Sentry SDK
集成到前端应用中就完事儿了。在之后的使用过程中,小编遇到了一个问题。由于我们的项目采用的是基于 qiankun
的微前端架构,在应用使用过程中,常常会出现发生异常应用和上报应用不匹配的情况。
为了解决这个问题,小编先去 qiankun
的 issue
下翻了翻,看有没有好的解决方案。虽然也有不少人遇到了同样的问题 - 求教一下 主子应用的sentry应该如何实践 #1088,但是社区里并没有一个好的解决方案。于是乎小编决定自己去阅读 Sentry
源码和官方文档,期望能找到一种合理并通用的解决方案。
经过一番梳理,小编如愿找到了解决方案,并且效果还不错。接下来小编就带着大家一起了解一下整个解决方案的具体情况。
本文的目录结构如下:
-
使用 Sentry 上报异常
-
解决方案
-
失败的方案一
-
不通用的方案二
-
合理、优雅的方案三
-
7.x 版本解决方案
-
-
结束语
使用 Sentry 上报异常
在正式介绍解决方案之前,小编先带大家简单回顾一下一个前端应用是如何接入 Sentry
的。
-
第一步,在
Sentry
管理后台构建一个项目项目创建好以后,会自动生成一个
dsn
,这个dsn
会在前端项目接入 Sentry 时作为必填项传入。 -
第二步,前端应用接入
Sentry
前端应用接入
Sentry
也非常简单,只要使用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"));
经过这两步,前端应用的异常监控接入就完成了。当应用在使用时,如果发生异常,Sentry
会自动捕获异常,然后上报到后台管理系统。上报完成以后,我们就可以在项目的 issues
中查看异常并着手修复。
单个的 Spa
应用接入 Sentry
时按照上面的步骤无脑操作就可以了,但如果应用是基于 qiankun
的微前端架构,那就需要解决异常上报不匹配的问题了。
小编手上的项目就是采用了基于 qiankun
的微前端架构,一个页面会至少同时存在两个应用,有时甚至会有 3 到 4 个应用。在应用使用过程中,常常会出现异常上报不匹配的问题。
如上图所示,主应用、cc sdk 应用中的异常都会上报到 aicc 项目中,这给异常处理带来很大的困扰。
出现这个问题的原因也非常好理解。
Sentry
在执行 init
方法时会通过覆写 window.onerror
、window.unhandledrejection
的方式初始化异常捕获逻辑。之后不管是哪个应用发生异常,都最终会触发 onerror
、unhandledrejection
的 callback
而被 Sentry
感知,然后上报到 dsn
指定的项目中。而且 Sentry
的 init
代码不管是放在主应用中,还是放在子应用里面,都没有质的改变,所有被捕获的异常还是会一股脑的上报到某个项目中,无法自动区分。
了解了异常上报无法自动区分的问题,接下来小编就给大家讲一下自己是如何解决这个问题的。
解决方案
想要解决这个问题,我们必须要先找到问题的切入点,而异常上报时的接口调用就是这个切入点。
当 Sentry
捕获到应用产生的异常时,会调用一个接口来上报异常,如下:
对比这个接口的 url
和上报应用的 dsn
,我们可以发现异常上报接口的 url
其实是由上报应用的 dsn
转化来的,转化过程如下:
// https://62187b367e474822bb9cb733c8a89814@sentry.byai.com/56
dsn - https://{param1}@{param2}/{param3}
|
|
v
url - https://{param2}/api/{param3}/store/?sentry_key={param1}&sentry_version=7
我们再来看看这个上报接口携带的参数:
在接口参数中,exceptions.values[0].stacktrace.frames
是异常的追踪栈信息。通过栈信息中的 filename
字段,我们可以知道发生异常的 js
文件的 url
。通常情况下,微前端中各个子应用的 js
的 url
前缀是不相同的(各个子应用静态文件的位置是分离的),那么根据发生异常的 js
的 url
就可以判断该异常属于哪个应用。
有了上面两个信息,异常上报自动区分的解决方案就清晰明了了:
-
第一步拦截异常上报接口,拿到异常详情,根据追踪栈中的
filename
判断异常属于哪个应用; -
第二步,根据匹配应用的
dsn
重新构建url
; -
第三步,使用新的
url
上报异常;
在这个方案中,最关键的是拦截异常上报接口。为了能实现这一步,小编进行了各种尝试。
失败的方案一
由于 Sentry
异常上报是通过 window.fetch(url, options)
来实现的,所以我们可以通过覆写 window.fetch
的方式去拦截异常上报。
代码实现如下:
const originFetch = window.fetch;
window.fetch = (url, options) => {
// 根据 options 中的异常信息,返回新的 url 和 options
const [newUrl, newOptions] = sentryFilter(url, options);
// 使用原生的 fetch
return originFetch(newUrl, newOptions);
}
该方案看起来很靠谱,然而在实际使用的时候并未发挥作用,原因是 Sentry
内部只会使用原生的 fetch
。如果发现 fetch
方法被覆写,那么 Sentry
会通过自己的方式重新去获取原生的 fetch
。
小编截取了 Sentry 的部分源码给大家看一下:
// FetchTransport 是一个构造函数
// Sentry 在执行 init 方法时会构建一个 FetchTransport 实例,然后通过这个 FetchTransport 实例调用 window.fetch 方法去做异常上报
function FetchTransport(options, fetchImpl) {
if (fetchImpl === void 0) { fetchImpl = getNativeFetchImplementation(); }
var _this = _super.call(this, options) || this;
_this._fetch = fetchImpl;
return _this;
}
// 使用原生的 window.fetch 实现 FetchTransport
function getNativeFetchImplementation() {
if (cachedFetchImpl) {
return cachedFetchImpl;
}
// 根据 isNativeFetch 来判断 window.fetch 是否被覆写
if (isNativeFetch(global$7.fetch)) {
return (cachedFetchImpl = global$7.fetch.bind(global$7));
}
var document = global$7.document;
var fetchImpl = global$7.fetch;
// 如果被覆写,借助 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 是否已经被覆写
function isNativeFetch(func) {
return func && /^function fetch\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString());
}
由于 Sentry
内部有一套逻辑来保证 fetch
必须为原生方法,所以覆写 window.fetch
的方案失败, pass
!
不通用的方案二
既然覆写 window.fetch
的方案行不通,那我们就重新想办法。
观察上面的 FetchTransport
的入参。如果没有指定 fetchImpl
,Sentry
会通过 getNativeFetchImplementation
来实现一个 fetchImpl
。那我们主动给 FetchTransport
传递覆写以后的 fetch
方法,不就可以做到拦截 fetch
调用了吗?
这个方案看起来也很靠谱,赶紧试一下,