Qiankun Sentry 监控异常上报无法自动区分项目解决

目录
  • 前言
  • 使用 Sentry 上报异常
  • 解决方案
    • 失败的方案一
    • 不通用的方案二
    • 合理、优雅的方案三
    • 7.x 版本解决方案
  • 结束语

前言

最近项目组决定将前端异常监控由 Fundebug 切换为 Sentry。整个切换过程可以说非常简单,部署一个后台服务,然后将 Sentry SDK 集成到前端应用中就完事儿了。在之后的使用过程中,小编遇到了一个问题。由于我们的项目采用的是基于 qiankun 的微前端架构,在应用使用过程中,常常会出现发生异常应用和上报应用不匹配的情况。

为了解决这个问题,小编先去 qiankunissue 下翻了翻,看有没有好的解决方案。虽然也有不少人遇到了同样的问题 - 求教一下 主子应用的sentry应该如何实践 #1088,但是社区里并没有一个好的解决方案。于是乎小编决定自己去阅读 Sentry 源码和官方文档,期望能找到一种合理并通用的解决方案。

经过一番梳理,小编如愿找到了解决方案,并且效果还不错。接下来小编就带着大家一起了解一下整个解决方案的具体情况。

使用 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.onerrorwindow.unhandledrejection 的方式初始化异常捕获逻辑。之后不管是哪个应用发生异常,都最终会触发 onerrorunhandledrejectioncallback 而被 Sentry 感知,然后上报到 dsn 指定的项目中。而且 Sentryinit 代码不管是放在主应用中,还是放在子应用里面,都没有质的改变,所有被捕获的异常还是会一股脑的上报到某个项目中,无法自动区分。

了解了异常上报无法自动区分的问题,接下来小编就给大家讲一下自己是如何解决这个问题的。

解决方案

想要解决这个问题,我们必须要先找到问题的切入点,而异常上报时的接口调用就是这个切入点。

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。通常情况下,微前端中各个子应用的 jsurl 前缀是不相同的(各个子应用静态文件的位置是分离的),那么根据发生异常的 jsurl 就可以判断该异常属于哪个应用。

有了上面两个信息,异常上报自动区分的解决方案就清晰明了了:

  • 第一步拦截异常上报接口,拿到异常详情,根据追踪栈中的 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 的入参。如果没有指定 fetchImplSentry 会通过 getNativeFetchImplementation 来实现一个 fetchImpl。那我们主动给 FetchTransport 传递覆写以后的 fetch 方法,不就可以做到拦截 fetch 调用了吗?

这个方案看起来也很靠谱,赶紧试一下,。

FetchTransport 追本溯源,小编找到了 FetchTransport 方法调用的位置:

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 就是我们执行 Sentry.init 时的入参。观看 FetchTransport 调用的地方,由于没有 fetchImpl 的入参,所以 Sentry 会通过 getNativeFetchImplementation 来实现 fetchImpl

既然这样,那我们可以在 Sentry.init 方法执行的时候添加一个 fetchImpl 入参,然后在调用 FetchTransport 方法时传入。

改造后的代码如下:

// 改动 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);
    }
    ...
}
// 业务代码
const originFetch = window.fetch;
// Sentry.init 执行
Sentry.init({
    dsn: 'xxx',
    ...
    fetchImpl: (url, options) => {
        // 根据 options 中的异常信息,返回新的 url 和 options
        const [newUrl, newOptions] = sentryFilter(url, options);
        // 使用原生的 fetch
        return originFetch(newUrl, newOptions);
    }
    ...
});

经验证,该方案可正常工作,捕获的异常都可自动上报到对应的应用中,问题解决,happy 。

不过兴奋过后,再回过头来看看这个方案,发现其实槽点还是蛮多的:

  • 要修改 Sentry 源码,重新生成一个内部 npm 包;
  • 如果 Sentry 版本升级,必须再次修改源码, 很不方便;

总体来说,这个方案虽然能解决问题,但是不够通用,不够优雅。作为一名有追求的

(0)

相关推荐

  • 如何使用Sentry 监控你的Spring Boot应用

    Sentry是一个应用监控系统,可以用于前后端各种技术栈的线上监控和错误分析,这次我们用它来进行Spring Boot项目的线上日志分析实践. 创建项目 首先需要根据提示在Sentry中创建好项目,项目平台(platform)可以选择Spring Boot或者Java. 引入依赖&配置 Sentry提供了官方的Spring Boot Starter. <dependency> <groupId>io.sentry</groupId> <artifactId

  • 微前端qiankun沙箱实现源码解读

    目录 前言 LegacySandbox单实例沙箱 ProxySandbox多实例沙箱 SapshotSandbox 快照沙箱 结束语 前言 上篇我们介绍了微前端实现沙箱的几种方式,没看过的可以下看下JS沙箱这篇内容,扫盲一下.接下来我们通过源 码详细分析下qiankun沙箱实现,我们clone下qiankun代码,代码主要在sandbox文件夹下,目录结构为 ├── common.ts ├── index.ts // 入口文件 ├── legacy │ └── sandbox.ts // 代理沙

  • qiankun 找不到入口问题彻底解决

    目录 前言 为什么要找生命周期 如何找入口 兜底找入口 微应用的 Webpack 配置 主应用的兜底逻辑 总结 前言 嗨害嗨,好久不见,我是海怪. 有一阵子没写文章了,今天来更一期关于 qiankun 找不到生命周期的问题. 刚开始给项目接入 qiankun 的时候,时不时就会报 Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry: 开发

  • Sentry错误日志监控使用方法解析

    无论作为新手还是老手程序员在程序的开发过程中,代码运行时难免会抛出异常,而且项目在部署到测试.生产环境后,我们便不可能像在开发时那样容易的及时发现处理错误了.一般我们都是在错误发生一段时间后,错误信息才会传递到开发人员那里,然后一顿操作查看程序运行的日志,就熟练使用awk和grep去分析日志,但是往往我们会因为日志中缺少上下文关系,导致很难分析真正的错误是什么. Sentry由此应运而生成为了解决这个问题的一个很好的工具,设计了诸多特性帮助开发者更快.更方面.更直观的监控错误信息. 关于日志管理

  • 微前端qiankun改造日渐庞大的项目教程

    项目背景 很多小伙伴在工作中都碰到过和我一样的场景,手上的某个项目越来越大,眼看着每次build时间越来越长,吐了.在杭州某独角兽我碰到了这样的一个项目,他叫运营后台,听名字就知道,他的主要用户是运营人员.问题就是随着公司业务的越来越多,这个运营后台承担的已经不是某一块业务了,而是所有业务的运营操作的中后台都在这上面.你可以这样理解,这个系统的每个一级菜单都是一块独立的业务,相互之间没有任何瓜葛:按常规的理解,这应该是单独的每一个project比较合理,但是正因为他的用户又都是公司的同一群人,他

  • Qiankun Sentry 监控异常上报无法自动区分项目解决

    目录 前言 使用 Sentry 上报异常 解决方案 失败的方案一 不通用的方案二 合理.优雅的方案三 7.x 版本解决方案 结束语 前言 最近项目组决定将前端异常监控由 Fundebug 切换为 Sentry.整个切换过程可以说非常简单,部署一个后台服务,然后将 Sentry SDK 集成到前端应用中就完事儿了.在之后的使用过程中,小编遇到了一个问题.由于我们的项目采用的是基于 qiankun 的微前端架构,在应用使用过程中,常常会出现发生异常应用和上报应用不匹配的情况. 为了解决这个问题,小编

  • SpringBoot WebSocket实时监控异常的详细流程

    目录 写在前面 实现: 前端: 后端: 测试 写在前面 此异常非彼异常,标题所说的异常是业务上的异常. 最近做了一个需求,消防的设备巡检,如果巡检发现异常,通过手机端提交,后台的实时监控页面实时获取到该设备的信息及位置,然后安排员工去处理. 因为需要服务端主动向客户端发送消息,所以很容易的就想到了用WebSocket来实现这一功能. WebSocket就不做介绍了,上链接:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket 前端略

  • C#监控文件夹并自动给图片文件打水印的方法

    本文实例讲述了C#监控文件夹并自动给图片文件打水印的方法.分享给大家供大家参考.具体分析如下: 个人私心的缘故,经常写一些博客之类的文章,由于看到网络上面好多同志转载后不标明出处,所以特地写了这么一个小程序,这个小程序的功能是当我在页面上通过QQ截图之后,把截到的图片保存到一个指定的路径,然后工具自动帮我把图片上面加上水印. 下面是全部代码: using System; using System.Collections.Generic; using System.ComponentModel;

  • PostgreSQL更新表时时间戳不会自动更新的解决方法

    PostgreSQL更新表时时间戳不会自动更新的解决方法,具体如下 操作系统:CentOS7.3.1611_x64 PostgreSQL版本:9.6 问题描述 PostgreSQL执行Insert语句时,自动填入时间的功能可以在创建表时实现,但更新表时时间戳不会自动自动更新. 在mysql中可以在创建表时定义自动更新字段,比如 : create table ab ( id int, changetimestamp timestamp NOT NULL default CURRENT_TIMEST

  • ThinkPHP提交表单时默认自动转义的解决方法

    本文实例讲述了ThinkPHP提交表单时默认自动转义的解决方法.分享给大家供大家参考.具体方法如下: 一.问题: 在ThinkPHP中提交表单插入数据的时候,单引号和双引号是会被自动转义的,就是会自动的加上反斜线,但是我不想给单引号和双引号加上反斜线. 在ThinkPHP中提交表单插入数据的时候,单引号和双引号是会被自动转义的,就是会自动的加上反斜线,但是我不想给单引号和双引号加上反斜线,在ThinkPHP中提交表单插入数据的时候,单引号和双引号是会被自动转义的,就是会自动的加上反斜线,但是我不

  • 详解nodejs的express如何自动生成项目框架

    本文主要介绍了nodejs的express如何自动生成项目框架,这里整理了详细的代码,有需要的小伙伴可以参考下. nodejs版本为:4.X,express版本为4.X 1.全局安装2个模块 express.express-generator 在命令行输入: npm install -g express npm install -g express-generator 如果模块下载很慢可以在后面 空格加上淘宝的镜像 --registry=https://registry.npm.taobao.o

  • Vue CLI 3.x 自动部署项目至服务器的方法

    前言 平时部署前端项目流程是:先部署到测试环境ok后再发布到生产环境上,部署到测试环境用 xshell 连上服务器,然后用 xftp 连接服务器,然后本地 build 项目,接着把 build 好的文件通过 xftp 上传到服务器上,整个流程感觉稍有繁琐,重复. 本教程讲解的是 Vue-CLI 3.x 脚手架搭建的vue项目, 利用scp2自动化部署到静态文件服务器 Nginx 一 安装scp2 scp2是一个基于ssh2增强实现,纯粹使用JavaScript编写. 而ssh2就是一个使用nod

  • Pycharm2017版本设置启动时默认自动打开项目的方法

    最新版本不同于旧版本的设置,网上检索一番方法都不适用. 新版的设置位置在"configure->setting->Appearance&Behavior->System Setting->Reopen Last project on startup"这里勾选是否默认启动最近一次关闭的项目. 以上这篇Pycharm2017版本设置启动时默认自动打开项目的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • 关于layui表单中按钮自动提交的解决方法

    layui表单中的按钮会自动提交,这是一个很麻烦的事情. 这几天项目中多次用到表单按钮,仔细研究了下,找到了解决方法: 1.如果不需要放在表单中的按钮,最好不要放在表单中,不在layui的form中的按钮就不会进行自动提交了: 2.放在表单中的按钮可以通过js中的回调函数里添加 return false制止.filter中的参数指的是按钮中的lay-filter属性中的值,这个可以自己随便设. 比如: form.on('submit(filter)',function(data){ ......

随机推荐