浅析前端异常埋点系统

 

在长期生产 bug 并修复 bug 的循环中,上线产品不可避免出现异常。如何能快速定位到发生错误的代码位置、第一时间通知开发人员异常发生以及报错的堆栈信息、用户OS与浏览器版本等十分重要。

而错误埋点追踪系统的出现就是为了应对上述问题的解决方案。

错误类型

JS执行错误

1. SyntaxError:解析时发生语法错误,window.onerror捕获不到,一般SyntaxError在构建阶段,甚至本地开发阶段就会被发现。

2. TypeError:值不是所期待的类型。

3. ReferenceError:引用未声明的变量。

4. RangeError:当一个值不在其所允许的范围或者集合中

网络错误

  1. ResourceError:资源加载错误
  2. HttpError:Http请求错误

前端异常捕获

js 异常的特点是,出现不会导致 JS 引擎崩溃,最多只会终止当前执行的任务。在Javascript中,我们通常有以下两种异常捕获机制。

1. try…catch 语句能捕捉到的异常,必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的,优点是能够较好地进行异常捕获,不至于使得页面由于一处错误挂掉,缺点是显得过于臃肿,大多代码使用try ... catch包裹,影响代码可读性。以下都是无法被捕获到的情形:

  1. 异步任务抛出的异常(执行时 try catch 已经从执行完了,比如 setTimeout)。
  2. promise,正常情况下异常被 promise 内部捕获到了,并未往上抛异常,使用 promise.catch() 处理或者 promise 前使用用 await 就可以被 try... catch 捕获了。
  3. 语法错误(代码运行前,在编译时就检查出来了的错误)。

2. window.onerror 最大的好处就是同步任务、异步任务都可捕获,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,捕获异常后,统一上报至我们的日志服务器,而且可以全局监听。缺点是无法捕获资源加载错误,同时,跨域脚本无法准确捕获异常,跨域之后 window.onerror捕获不到正确的异常信息,而是统一返回一个Script error,可通过在<script>使用 crossorigin属性来规避这个问题,或者使用 try ... catch ... 进行捕获。

当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,图片、script、css加载错误,都能被捕获。

window.addEventListener('error', function() {
  console.log(error);
  // ...
  // 异常上报
});

或者是劫持原生方法:

const nativeAddEventListener = EventTarget.prototype.addEventListener; // 先将原生方法保存起来。
EventTarget.prototype.addEventListener = function (type, func, options) { // 重写原生方法。
    const wrappedFunc = function (...args) { // 将回调函数包裹一层try catch
        try { 
            return func.apply(this, args);
        } catch (e) {
            const errorObj = {
                ...
                error_name: e.name || '',
                error_msg: e.message || '',
                error_stack: e.stack || (e.error && e.error.stack),
                error_native: e,
                ...
            };
            // 接下来可以将errorObj统一进行处理。
        }
    }
    return nativeAddEventListener.call(this, type, wrappedFunc, options); // 调用原生的方法,保证addEventListener正确执行

 

3. Promise 内部异常,如果遗漏处理,最好是添加一个 Promise 全局异常捕获事件 unhandledrejection :

window.addEventListener("unhandledrejection", e => {
 console.log('unhandledrejection',e)
});

4. vue工程异常 ,window.onerror 并不能捕获.vue文件发生的获取。使用Vue.config.errorHandler这样的Vue全局配置处理函数,被调用时,可获取错误信息和Vue 实例:

//main.js
import { createApp } from "vue";
import App from "./App.vue";

let app = createApp(App);
app.config.errorHandler = function(e) {
  console.log(e);
  //错误上报...
};
app.mount("#app");

综合以上所述的前端异常捕获方式:

import { createApp } from "vue";
import App from "./App.vue";

let app = createApp(App);

window.addEventListener(
  "error",
  (e) => {
    console.log(e);
    /** TODO:上报逻辑 */
    return true;
  },
  true
);
/** 处理未捕获的异常,主要是promise内部异常,统一抛给 onerror */
window.addEventListener("unhandledrejection", (e) => {
  throw e.reason;
});
/** 框架异常统一捕获 */
app.config.errorHandler = function(err, vm, info) {
  /** TODO:上报逻辑 */
  console.log(err, vm, info);
};
app.mount("#app");

5. React 异常捕获,react 通过componentDidCatch,声明一个错误边界的组件

sourcemap

通常在生产环境下的代码是经过 webpack 打包后压缩混淆的,否则源代码泄漏易造成安全问题。在该环境下,代码被压缩成了一行。而webpack 打包后会生成一份.map的脚本文件,浏览器利用它对错误位置进行追踪,但这种做法并不可取。

更为推荐的是在服务端使用 Node.js 接收到的日志信息时使用 source-map 对其进行解析,以避免源代码的泄露造成风险。

vue.config.js配置里通过属性 productionSourceMap: true来控制 webpack 是否生成 map 文件。

编写一个插件让 webpack 在打包完成后触发一个钩子实现 sourcemap 文件上传

首先,在vue.config.js中进行配置:

import SourceMapUploader from "./source-map-upload";
module.exports = {
    configureWebpack: {
        resolve: {
            alias: {
                "@": resolve("src"),
            },
        },
        plugins: [
             new SourceMapUploader({url: "http://localhost:3000/upload"})
        ],
    }
    //   chainWebpack: (config) => {},
}

sourcemap 文件上传实现(source-map-upload.js ):

const fs = require("fs");
const http = require("http");
const path = require("path");

class SourceMapUploader {
  constructor(options) {
    this.options = options;
  }
  /**
   * 用到了hooks,done表示在打包完成之后
   * status.compilation.outputOptions就是打包的dist文件
   */
  apply(compiler) {
    if (process.env.NODE_ENV == "production") {
      compiler.hooks.done.tap("sourcemap-uploader", async (status) => {
        /** 读取目录下的 map 后缀的文件 */
        let dir = path.join(status.compilation.outputOptions.path, "/js/");
        let chunks = fs.readdirSync(dir);
        let map_file = chunks.filter((item) => {
          return item.match(/\.js\.map$/) !== null;
        });
        /** 上传 sourcemap */
        while (map_file.length > 0) {
          let file = map_file.shift();
          await this.upload(this.options.url, path.join(dir, file));
        }
      });
    }
  }
  
  /** 调用upload接口,上传文件 */
  upload(url, file) {
    return new Promise((resolve) => {
      let req = http.request(`${url}?name=${path.basename(file)}`, {
        method: "POST",
        headers: {
          "Content-Type": "application/octet-stream",
          Connection: "keep-alive",
        },
      });

      let fileStream = fs.createReadStream(file);
      fileStream.pipe(req, { end: false });
      fileStream.on("end", function() {
        req.end();
        resolve();
      });
    });
  }
}
module.exports = SourceMapUploader;

行为搜集

通过搜集用户的操作,可以明显发现错误为什么产生。 用户的操作分类如下:

  • UI行为: 点击、滚动、聚焦/失焦、长按
  • 浏览器行为:请求、前进/后退、跳转、新开页面、关闭
  • 控制台行为:log、warn、error

如何搜集?

  • 点击行为:使用addEventListener监听全局上的click事件,将事件和DOM元素名字收集。与错误信息一起上报。
  • 发送请求:监听XMLHttpRequest的onreadystatechange回调函数。
  • 页面跳转:监听window.onpopstate,页面进行跳转时会触发。
  • 控制台行为:重写console对象的info等方法。

错误上报

两种方式:

  1. 通过动态创建一个img,这种方式无需加载任何库,而且页面是无需刷新的,将需要上报的错误数据放在url中,相当于 get 请求,没有跨域问题。缺点是有url长度限制,一般够用;
  2. ajax 与正常的接口请求无异,可以用 post。

确定上报的内容,应该包含异常位置(行号,列号),异常信息,在错误堆栈中包含了绝大多数调试有关的信息,请求的时候只能以字符串方式传输,因此需要将对象进行序列化处理。

  1. 将异常数据从属性中解构出来,存入一个JSON对象
  2. 将JSON对象转换为字符串
  3. 将字符串转换为Base64
function uploadErr({ lineno, colno, error: { stack }, message, filename }) {
  let str = window.btoa(
    JSON.stringify({
      lineno,
      colno,
      error: { stack },
      message,
      filename,
    })
  );
  let front_ip = "http://localhost:3000/error";
  new Image().src = `${front_ip}?info=${str}`;
}

使用 POST 一个  1 x 1 的 gif 对 错误、环境、行为信息 进行上报。原因是: 

  • 没有跨域问题
  • 发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
  • 不会携带当前域名 cookie!
  • 不会阻塞页面加载,影响用户的体验,只需 new Image 对象
  • 相比于 BMP/PNG 体积最小,可以节约 41% / 35% 的网络资源小

一句话总结:监听 / 劫持 原始方法,获取需要上报的数据,在错误发生时 触发 函数使用 gif 上报。

后端或者监控平台接收到信息后进行对应的反向操作,就可以在日志中记录。

接收到的错误记录转化为有效的数据入库,核心功能需要对数据进行清洗,生成标识错误的errorKey,进行过滤。

读取到监控平台时,先读取对应的 map文件(按 filename 对应),然后只需传入压缩后的JS报错行号列号即可,就会返回压缩前JS的错误信息。

而且,在上报的时候增加报错时间,用户浏览器信息,对错误类型区分,自定义错误类型统计,引入图表可视化展示,更加直观地追踪。

同时对上报频率做限制。如类似mouseover事件中的报错应该考虑防抖般的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定谔的猫96

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值