关于快速定位线上 Bug 的一些理论知识

1,206 阅读9分钟

why

生产线上偶尔会出现一些问题,妨碍了客户操作,待反馈到我们技术的时候,有时候可以通过快速排查代码来定位问题,但人力有时穷,有时候我们会束手无策,这时候我们会请求客户主动录屏,通过录屏视频,我们可以看到客户的操作路径和页面显示,这时候定位问题就会变得相对简单点了。但,客户的体验感会变得更差。我们可以为客户的体验感提升做一些努力吗?

How

如果我们可以用我们的手段快速定位到线上的问题,不需要客户提供录屏视频,那客户的体验感不就 UpUpUp 了吗?

如果我们可以用我们的手段在客户反馈问题之前,就定位到了问题,那客户来反馈的时候,我们直接告诉客户我们正在修复,且安排了上线时间,那客户的体验感和信任感不就 UpUpUp 了吗?

那怎么来实现我们的提升客户体验感和信任感呢?

我们可以很容易地发现,解决方案聚焦在两个点上:如何发现问题和如何快速定位问题。

如何发现问题,一般有以下两个解决方案:

  1. 自建前端监控,
  2. 使用前端监控服务,如 sentry,fundebug等。

如何快速定位问题,常见三把斧:用 sourceMap 定位发生问题的代码,监听收集客户行为和操作路径,自主录屏。

当然,自主录屏其实不是常见的操作(但作业很大啊),甚至我们常用的 sentry,也是后面才加上的(sentry SDK 版本 >= 7.24.0),没错,现在 sentry 是支持录屏回放了(暂时还是 beta,详情请看sentry 的 replay 库)。

等等,sentry 可以用 sourceMap ,也有发生错误时的 timeline(有收集到部分客户操作的行为),现在也可以录屏回放了,那我们直接用 sentry 就 ok 啦

so,全文完~

等等,其实我还没水完!

先看一下 sentry 吧。

sentry

基础设置

请看 这里 !有详细的教程!

注意,上传 sourceMap 到 sentry.io 上,在复杂的大项目中,其实是会有一些小限制的:

  1. sourceMap 太大,上传 sentry.io 可能速度会比较慢,导致打包项目时间很长,如果有 hotfix 的情况的话,就有点尴尬
  2. sourceMap 在 CI(Jenkins)打包,有可能会受到公司网络安全限制,导致无法联通 sentry.io ,从而导致无法上传 sourceMap

新功能 Session Replay

当前(2023.01.10)该功能处于公测,可以尝试申请加入测试。可预见的是,该功能开放时应是付费功能,但也不妨碍我们现在去了解它。

可以在 这里 看到 React 的 session replay 的设置,在页面的右侧,还有其他的库的设置方法,点击对应的库跳转到对应页面即可。

虽然新功能很强,但它底层使用了 rrweb 库(请看下一小节),使它生成的视频的数据量有点大,对服务器的带宽会造成一定的压力(当用户量大的时候),虽然它提供了方法压缩数据,但对于服务器来说,仍会造成一定资源的占用。当然,sentry 也对它做了一些努力,比如使用了 pako 库来进一步提升压缩比例。

以上我们了解了一些关于 sentry 新功能的知识,我们进一步了解深一些的知识吧。

sentry 的 Session Replay 拆解

看一下它的 实现库 的 package.json 里的开发依赖:

{
  "devDependencies": {
    "@babel/core": "^7.17.5",
    "@types/pako": "^2.0.0",
    "jsdom-worker": "^0.2.1",
    "pako": "^2.0.4",
    "rrweb": "1.1.3",
    "tslib": "^1.9.3"
  }
}

它的实现核心库就是 rrwebrrweb 可是一个好东西啊,想了解一下它是怎样的一个好东西,可以看这篇文章 rrweb:打开 web 页面录制与回放的黑盒子 ,它讲述了 rrweb 研发初衷和研发时遇到的一些问题和解决方案,可以给想自研页面回放框架的人一些思路吧。

自建前端监控系统(类 sentry)

首先,我们要知道,自建前端系统的话,我们要做一些什么样的工作,难度如何,工作量如何,自建的优势是啥。下面我讲述一下这几个方面。

why

自由度高,系统可优化程度高,高度贴合公司系统等,这些既是自建前端监控系统的优势,也是为什么要自建的原因。

How

通过观察 sentry,总结一下大概要做以下几方面的工作:

  1. 采集:发现错误并上报(需要兼容报错情况)
  2. 储存:将上报信息保存到对应版本(如果有做版本区分的话)去
  3. 分析:可定位具体错误(最好有用户行为日志/操作路径)
  4. 分析:前端录屏回放(可以使用 rrweb 库)
  5. 报警:通过 email,企微等通知

简单分析一下采集步骤。

除了框架(React, Vue 等)内部的错误和 xhr 报错处理外,我们一般会接触到异常以及捕获方法如下:

异常类型同步方法异步方法资源加载Promiseasync/await
try/catch
Window.onerror
error
unhandledrejection

so,只要 error + unhandledrejection,就可以覆盖这些异常情况了!

React,优雅的捕获异常

有一个库 react-error-catch 是基于ErrorBoudary,error与unhandledrejection封装的一个组件。其核心如下:

ErrorBoundary.prototype.componentDidMount = function () {
  // event catch
  window.addEventListener('error', this.catchError, true);
  // async code
  window.addEventListener('unhandledrejection', this.catchRejectEvent, true);
};

使用:

import ErrorCatch from 'react-error-catch'

const App = () => {
  return (
  <ErrorCatch
      app="react-catch"
      user="cxyuns"
      delay={5000}
      max={1}
      filters={[]}
      onCatch={(errors) => {
        console.log('报错咯');
        // 上报异常信息到后端,动态创建标签方式
        new Image().src = `http://localhost:3000/log/report?info=${JSON.stringify(errors)}`
      }}
    >
      <Main />
    </ErrorCatch>)
}

当然,这里只是获取到了对应的错误,上报一般是使用 gif 来携带信息来上报的。

为什么大厂前端监控都在用GIF做埋点?

使用GIF上报的原因

向服务器端上报数据,可以通过请求接口,请求普通文件,或者请求图片资源的方式进行。**只要能上报数据,无论是请求GIF文件还是请求其他普通文件(JS)或者是请求接口,服务器端其实并不关心具体的上报方式。**那为什么所有系统都统一使用了请求GIF图片的方式上报数据呢?

####「防止跨域」

一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。而跨域请求很容易出现由于配置不当被浏览器拦截并报错,这是不能接受的。但图片的src属性并不会跨域,并且同样可以发起请求。(排除接口上报)

####「防止阻塞页面加载,影响用户体验」

通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。

但是图片请求例外。构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点,这是其他类型的资源请求所做不到的。(排除文件方式)

####「相比PNG/JPG,GIF的体积最小」

最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节。

同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。

####「并且大多采用的是1*1像素的透明GIF来上报」

1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。

好,数据已经有了,接下来是数据处理了

React,优雅的捕获异常

利用error捕获的错误,其最主要的是提供了错误堆栈信息,对于分析错误相当不友好,尤其打包之后。

so,应对这个问题,可以用 source-map-js 来获取对应的源码信息,然后用 error-stack-parser 来获取对应错误的原始文件名,行和列信息。

前端录屏+定位源码,帮你快速定位线上bug

source-map 的还原流程: 1、从服务器获取指定.map 的文件内容 2、new 一个 SourceMapConsumer 的实例,表示一个已解析的源映射,给它一个文件位置来查询有关原始文件位置的信息 3、输入报错发生的行和列,可以得到源码对应原始文件名、行和列信息 4、从源文件的 sourcesContent 字段中,获取对应的源码信息

如何优雅地查看 JS 错误堆栈?

Error-stack-parser 原理:

  • 拿到原始堆栈字符串,使用 error-stack-parser 解析为堆栈帧,每个堆栈帧包含三个最重要的字段:

    url - 源码的 URL 地址 line - 堆栈位置行号 col - 堆栈位置列号

  • 对于 url,我们可以用于加载源码内容,得到 source

  • source 使用 UglifyJs 反向美化成多行的代码 prettysource,并且同时生成 sourcemap

  • 堆栈帧中的 line 和 col 通过 sourcemap 反查,得到美化后对应的 prettyline 和 prettycol

  • 将 prettysource、prettyline、prettycol 给到 Monaco Editor 渲染,就可以得到上述截图的效果

这时候我们应该会有一个比较准确的的错误信息了。但用户操作/行为路径并没有记录。这个只能自己写对应的方法或采用全纪录的方式去记录并在上报错误时,携带这部分的信息才行了。

so,用户操作/行为路径部分的分析,略过!

现在我们来到了前端录屏回放这个小节了

从上文得知, sentry 也是用 rrweb 库来实现,证明这个库应该是比较成熟的了。所以录屏和回放也可以使用 rrweb 来实现。实现不难,多看文档。不如来看下可能会面对的问题吧:

  1. rrweb 生成的数据量有点大
  2. 什么时候上报录屏数据
  3. 鼠标轨迹显示

这些个问题,可以在 前端录屏+定位源码,帮你快速定位线上bug 这篇文章得到解答。如果遇到更多问题,会及时补充的。

参考

github.com/getsentry/s…

docs.sentry.io/product/sen…

sentry.io/for/session…

docs.sentry.io/platforms/j…

rrweb:打开 web 页面录制与回放的黑盒子

前端录屏+定位源码,帮你快速定位线上bug

如何优雅地查看 JS 错误堆栈?

React,优雅的捕获异常

为什么大厂前端监控都在用GIF做埋点?