前端异常监控治理【记录我一路上的跌宕起伏】

前言

那是一个看似寻常的上线日,我们的项目上线了一个引流的需求,效果非常显著,pv几乎翻了一倍,但是,异常也是几乎翻了一倍。。。

不过在我们看到群里的报错以后,发现几乎都是由于用户的浏览器、网络波动等外部因素导致的报错,所以我们就不是太当一回事了,但是我们都忽略掉了群里的后端老板,他看不懂这些异常,在群里问:"为什么会有这么多的错误?我们必须要找出原因,想办法解决它们。" 尽管我们很努力地去解释,这些大多数都是无效异常,无需过度关注,但我们老板的疑惑并没有消除,甚至定了一个kr:完善异常监控能力,从前端开始做起

于是,既然老板都发话了,再加上咱有一颗勇于探索的心,我便揽下了这个活,这篇文章就此诞生。接下来我将通过类似日记的形式,记录自己在探索异常监控旅程中的每一步。如果在阅读过程中有任何问题,希望得到各位专业大佬的指导,非常感谢~

day01

明确思路

首先,我确定自己要做的就是 过滤掉无效异常,无效异常无需报警,只有那种因为我们业务代码导致的异常,才需要报警。不然的像现在群里天天报警,大家都已经懒得去关注了,怕最后跟狼来了的故事一样,真遇到问题了也没发现。

关于无效异常

  1. 第三方错误:这些错误由第三方脚本或库引起,以代码逻辑无关。例如,来自广告脚本、统计代码、或者第三方插件的错误。这些错误不会对代码产生直接影响,但是会带来很多错误。
  2. 网络错误:一些用户可能会遇到网络问题,导致某些请求失败,或者无法加载某些脚本文件。这可能会引发一些异常,但这些异常往往是暂时的,而且对调试代码没有太大帮助。
  3. 废弃或不支持的浏览器引发的错误:如果用户正在使用一个废弃或者不再被支持的浏览器版本,可能会引发一些错误。这些错误没有太大帮助,因为它们是由浏览器的问题引起的,而非代码问题。

确认当前的监控手段

异常上报

我首先去检查了当前项目是如何完成报警监控的,发现是接入了公司提供的监控SDK,SDK提供异常上报的方式有两种,手动上报和自动上报。

手动上报 的方式指在我们业务代码遇到异常时,通过自己去调用SDK初始化以后生成的实例提供的error方法完成上报。我们是react项目,目前手动调用的地方有两个,一个是通过错误边界(不熟悉的同学可以看这里👉 关于react错误边界),上报组件内的异常;另一个是通过公共的fetch方法,上报API异常(此文章只讨论JS异常,接口问题这就交给后端同学看吧哈哈哈)。

自动上报的方式是SDK通过绑定全局的错误处理函数来实现。

  • window.onerror绑定了一个全局错误处理函数,他会在未被捕获的运行时错误发生时被触发。
  • window.onunhandledrejection: 当 Promise 被 reject 并且在微任务队列结束时没有添加对应的处理函数(例如,.catch()),将会触发该事件。

监控报警

我们的项目在监控报警平台配置了一个JS异常监控(这里有一个大坑,后面让我尴了一个大尬),当监听到有异常上报以后,通过webhook的方式触发报警群里的通知机器人,实现消息通知。

阅读SDK文档-尝试通过SDK配置解决问题

既然已经找到了异常上报的源头,那我就想,是不是直接改改SDK的配置,让无效异常在上报前直接被过滤掉,问题是不是就解决了呢?说干就干,于是我开始阅读SDK文档,很快,我便找到了SDK的异常设置,发现目前提供三种过滤:

  • 异常类型过滤:通过设置异常的类型来实现过滤。但是这个很明显无法实现区分有效与无效异常,就比如他们都可能会报TypeError这种错。
  • 异常消息过滤:对异常的信息进行过滤。这个也不行,我总不能遇到一个异常加一条过滤吧,太低效了。
  • 异常触发URL过滤:对触发异常的URL过滤。只能说通过这个可以过滤掉测试环境的异常,还是不能帮我们解决问题。

就此我确认了SDK的异常过滤能力不能达到我的需求,尝试阻止无效异常上报的方案失败了。

day02

异常聚合-设置阈值

于是我将目光移向了我们项目在监控平台配置的异常监控目,发现目前平台上支持配置报警的阈值,在a时间内,触发了b次报警或者影响了c个用户,再进行报警。 目前的最小报警周期是5分钟,我感觉这个方案可行,于是便向我的小组长讲述了自己的方案。

遇坎

重新理解项目监控的真正目标

在我讲述完自己的方案以后,小组长给出了他的反馈:实施监控的核心目的在于识别并解决项目中存在的问题,而不是等待错误积累到一定程度后才引起关注。我们的重点应该是预防并及时解决问题,而不是简单地对它们进行计数并在达到某一阈值时才报警。这番话也是让我真正认清了此次监控治理的真正目标。

阈值不生效?

虽然确认了阈值的方案不可行,但我还是进行了阈值的配置,想看看到底是什么效果,结果发现,群里依然在报警。

这是咋回事?明明没达到阈值啊,于是我开始一条一条查看监控的配置,终于发现这个监控配置的webhook地址跟群里机器人的webhook地址不一样。也就是说群里的报警消息不是这个平台的监控发的。。。于是我便去问维护这个项目的老同事,才知道,原来群里的报警是之前一个离职的同事写了一个脚本来实现的。

好吧,那我只要找到脚本的代码,然后稍微改改代码应该就可以了吧?果然事情还是想的简单了,我问同事脚本的仓库地址,同事说他不知道,怀疑可能没有搞代码托管,我不相信,先是去我们组的云应用列表里找,看看能不能找到对应的应用;然后又根据群里报警机器人的webhook地址,让托管平台的同学帮忙查一下,有没有项目里的代码涉及到这个地址。结果都没有找到,事实证明,之前的同学确实没有将代码上传到托管平台,本来是前人栽树,后人乘凉。但这次,树却未没有种好,我只能重新开始耕耘。

day03

定时任务脚本

虽然没有找到之前同事的代码,但也算是给了我很大的启发,我完全可以自己通过 写一个脚本来实现异常的过滤和推送,完成非常多定制化的需求,而不再依赖于平台 。于是我开始了对定时任务实现方案的调研,发现公司的云平台支持通过创建无服务器(Serverless)应用来托管任务函数,从而实现定时任务的执行,目前平台支持node12,那么,我只需要 用node写好函数,然后上传,再配置好执行周期,就大功告成。

函数功能拆解

任务整体流程如下:

1.获取异常列表

监控平台已经对外开放了一个查询异常列表的API。由于我的函数设定是每5分钟触发一次,所以我只需要每次查询从五分钟前到当前时刻这个时间段的异常列表即可。

2.异常过滤

这应该是最关键的一步,已知的是我们要过滤掉无效的异常,那么怎么区分有效无效异常呢,目前我想到的是 通过sourcemap是否成功映射到源代码上,我们项目里的监控SDK支持上传sourcemap,如果有成功映射上,会在异常的堆栈信息里,返回对应的文件路径。

如果一个异常能够通过sourcemap成功映射到源代码上,那么我们就可以认为这是一个有效的异常。这是因为它指向了源代码中的具体位置,能够为我们找出问题提供有用的线索。相反,如果一个异常无法被正确地映射回源代码,那么这可能意味着这个异常是由于编译错误、第三方代码或其他我们无法直接解决的因素引起的。在这种情况下,我们可以将这种异常视为无效异常,并选择忽略它。

通过这种方式,我们可以聚焦在真正需要我们注意和解决的有效异常上,提高我们的问题解决效率,换句话说就是,如果sourcemap无法映射上,而且我们无法复现的话,那么这个问题也很难解决。

3.异常信息整理

这步涉及到需要在群里展示的内容,包括触发时间、异常信息、触发页面、文件位置等信息。

4.报警推送

企业群提供了调用通知机器人的API,将之前整理好的异常信息发送即可。

day04

异步任务异常无法映射

在昨天完成脚本的编写与部署以后,然后我便在群里观察情况,发现确实是过滤掉了大部分的异常,报警数量下降了非常多,内心非常的喜悦,但依然没有松懈,依然在观察每一条异常。然后在我回顾历史异常的时候,发现了这么一条报错TypeError: n is undefinend,并且文件位置是我们压缩后的js文件,但是没有映射上sourcemap,这是怎么回事?于是前往了触发页面,复现了这个问题,并且发现这就是我们业务代码导致的报错,但是因为没有影响到页面的展示所以一直没有被发现。业务代码大致是这样的:

js 复制代码
      getInfoById({id}).then(info => {
        // 有些情况下,info为undefined,但是报错
        const name = info.name;
        // ...
      })

这为什么会没有映射上sourcemap啊,于是我又开始问题排查,最后定位原因:异步异常没有完整的堆栈上下文信息,导致平台无法完成映射。

同步异常与异步异常

异常从发生位置来看可以分为同步异常与异步异常两种,如果没有在代码中通过 try/catchcatch() 捕获错误,那么这些错误就会成为未处理的错误。

  • 同步异常 :发生在同步执行的代码块中,被抛出时触发window.onerr事件。
js 复制代码
// util.js
window.onerror = function (msg, url, lineNo, columnNo, error) {
    console.dir(error);
}
export const sendError = (error) => {
    // 发送同步异常
    throw new Error('我是错误');
}

// index.js
sendError()
  • 异步异常 :发生在setTimeout、Promise等异步任务的回调函数中。如果在Promise中产生了一个未捕获的错误,那么这个Promise将会变为"rejected"状态,这个错误也会被传递到下一个 .catch() 处理程序。如果没有 .catch() 处理程序,这个错误就会成为一个未处理的Promise拒绝,被抛出后触发window.onunhandledRejection 事件。
js 复制代码
// util.js
window.onunhandledrejection = function (e) {
    console.log(e);
}
export const sendError = (error) => {
    // 发送异步异常
    Promise.resolve().then(() => {
      throw new Error('我是错误');
    })
}

// index.js
sendError()

为什么异步异常的堆栈信息只有一条

这是因为js的事件循环机制,Promise这类异步任务不会立即执行,而是被放入事件队列中,等待当前执行栈为空后,再进入执行栈完成执行。当一个异步函数抛出异常时,这个异常是在新的堆栈上下文中生成的。这个堆栈上下文是独立于触发这个异步操作的原始堆栈上下文的,因为触发异步操作的原始上下文在执行完成后,就已经从调用堆栈中被移除了。所以,这个异常的堆栈跟踪无法显示启动异步操作的代码路径。

如何修复异步异常

  1. 推送报警通知:因为我目前的过滤条件为是否能映射上sourcemap,但目前异步异常无法映射上sourcemap信息,导致异常会被过滤掉,不过监控平台为异步异常专门搞了一个字段,于是可以用它来做区分。
  2. 添加catch捕获:获取到Error以后,可以通过message信息,可以获取到rejected的内容,如果比较好找的话,那么可以加catch来捕获一下。

day05

异常修复

终于,异常也过滤完了,开始轮到修复了,首先再明确一下,目前还会发出的报警的异常有两种:

  1. 业务代码
  2. 第三方包

其中业务代码的报错分为day04所述的同步/异步异常,同步异常通过sourcemap映射上的源码可以很轻松的修复,异步异常上面说过这里也不再重复。

第三方包的异常

对于第三方包的异常,可以选择过滤掉,只需检查映射完成后的文件路径中是否包含node_modules即可。 不过,如果是一些比较严重的问题,我们也可以选择修复,这里我推荐两种方法:

fork有问题的仓库

我们可以fork有问题的仓库到自己的账号下,在修改提交后,将代码推送到fork的副本仓库内,然后在项目中安装使用自己fork的仓库,确认没问题后,就去发起Pull Request,给第三方包修个bug。

patch-package

如果觉得fork比较麻烦的话,也可以选择使用patch-package这个包,它可以让我们在本地修改一个包时生成一个文件,后续运行patch-package命令,再将这个补丁应用到node_modules里的相应包上。 具体示例如下:

  1. 安装 npm install -D patch-package
  2. 假设react的某段代码有问题,我们便可以先在node_modules里面直接进行改动,等改完以后,执行npx patch-package react,该命令会将我们在本地生成一个diff文件,里面包含了我们对react包的改动内容,将来要随着业务代码一起推送到远程仓库中。
  3. 最后我们还需要在package.json中配置一下postinstall,这样在别的同学拉取到我们的diff文件到本地后,可以通过npm install来应用上我们对第三方包的改动。
json 复制代码
// package.json
{
  "scripts": {
    "postinstall": "patch-package"
  }
}

postinstall是一个在Node包管理器中被用作特殊钩子的脚本名。这个脚本会在每次运行 npm installyarn install 后自动执行。

小结

以上便是我在探索异常监控之路上至今遇到的诸多问题及其解决方案。我知道肯定还有许多未曾触及的细节,在未来的探索中,我将继续遇到新的挑战并持续分享补充。既然您已经阅读到这里,那么能否献给我一个小小的赞呢?这个小小的鼓励对我来说意义非凡,真的非常感谢!o( ̄▽ ̄)ブ

相关推荐
安冬的码畜日常1 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记2 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀3 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端
永远不打烊3 小时前
librtmp 原生API做直播推流
前端