乐趣区

一步一步搭建前端监控系统JS错误监控篇

摘要: 徒手写 JS 错误监控。

  • 作者:一步一个脚印一个坑
  • 原文:搭建前端监控系统(二)JS 错误监控篇

Fundebug 经授权转载,版权归原作者所有。

背景: 市面上的监控系统有很多,大多收费,对于小型前端项目来说,必然是痛点。另一点主要原因是,功能通用,却未必能够满足我们自己的需求, 所以我们自给自足。

这是搭建前端监控系统的第二章,主要是介绍如何统计 js 报错,跟着我一步步做,你也能搭建出一个属于自己的前端监控系统。

请移步线上:前端监控系统

对于前端应用来说,Js 错误的发生直接影响前端应用的质量。对前端异常的监控是整个前端监控系统中的一个重要环节。前端异常包含很多种情况:1. js 编译时异常(开发阶段就能排)2. js 运行时异常;3. 加载静态资源异常(路径写错、资源服务器异常、CDN 异常、跨域)4. 接口请求异常等。这一篇我们只介绍 Js 运行时异常。

监控流程:监控错误 -> 搜集错误 -> 存储错误 -> 分析错误 -> 错误报警 -> 定位错误 -> 解决错误

首先,我们应该对 Js 报错情况有个大致的了解,这样才能够及时的了解前端项目的健康状况。所以我们需要分析出一些必要的数据。

如: 一段时间内,应用 JS 报错的走势 (chart 图表)、JS 错误发生率、JS 错误在 PC 端发生的概率、JS 错误在 IOS 端发生的概率、JS 错误在 Android 端发生的概率,以及 JS 错误的归类。

然后,我们再去其中的 Js 错误进行详细的分析,辅助我们排查出错的位置和发生错误的原因。

如:JS 错误类型、JS 错误信息、JS 错误堆栈、JS 错误发生的位置以及相关位置的代码;JS 错误发生的几率、浏览器的类型,版本号,设备机型等等辅助信息

一、JS Error 监控功能 (数据概览)

为了得到这些数据,我们需要在上传的时候将其分析出来。在众多日志分析中,很多字段及功能是重复通用的,所以应该将其封装起来。

// 设置日志对象类的通用属性
  function setCommonProperty() {this.happenTime = new Date().getTime(); // 日志发生时间
    this.webMonitorId = WEB_MONITOR_ID;     // 用于区分应用的唯一标识(一个项目对应一个)this.simpleUrl =  window.location.href.split('?')[0].replace('#', ''); // 页面的 url
    this.customerKey = utils.getCustomerKey(); // 用于区分用户,所对应唯一的标识,清理本地数据后失效
    this.pageKey = utils.getPageKey();  // 用于区分页面,所对应唯一的标识,每个新页面对应一个值
    this.deviceName = DEVICE_INFO.deviceName;
    this.os = DEVICE_INFO.os + (DEVICE_INFO.osVersion ? "" + DEVICE_INFO.osVersion :"");
    this.browserName = DEVICE_INFO.browserName;
    this.browserVersion = DEVICE_INFO.browserVersion;
    // TODO 位置信息, 待处理
    this.monitorIp = "";  // 用户的 IP 地址
    this.country = "china";  // 用户所在国家
    this.province = "";  // 用户所在省份
    this.city = "";  // 用户所在城市
    // 用户自定义信息,由开发者主动传入,便于对线上进行准确定位
    this.userId = USER_INFO.userId;
    this.firstUserParam = USER_INFO.firstUserParam;
    this.secondUserParam = USER_INFO.secondUserParam;
  }

  // JS 错误日志,继承于日志基类 MonitorBaseInfo
  function JavaScriptErrorInfo(uploadType, errorMsg, errorStack) {setCommonProperty.apply(this);
    this.uploadType = uploadType;
    this.errorMessage = encodeURIComponent(errorMsg);
    this.errorStack = errorStack;
    this.browserInfo = BROWSER_INFO;
  }
  JavaScriptErrorInfo.prototype = new MonitorBaseInfo();

封装了一个 Js 错误对象 JavaScriptErrorInfo,用以保存页面中产生的 Js 错误。其中,setCommonProperty 用以设置所有日志对象的通用属性。

1)重写 window.onerror 方法,大家熟知,监控 JS 错误必然离不开它,有人对他进行了测试测试介绍感觉也是比较用心了

2)重写 console.error 方法,为什么要重写这个方法,我不能够给出明确的答案,如果 App 首次向浏览器注入的 Js 代码报错了,window.onerror 是无法监控到的,所以只能重写 console.error 的方式来进行捕获,也许会有更好的办法。待 window.onerror 成功后,此方法便不再需要用了

3)重写 window.onunhandledrejection 方法。当你用到 Promise 的时候,而你又忘记写 reject 的捕获方法的时候,系统总是会抛出一个叫 Unhandled Promise rejection. 没有堆栈,没有其他信息,特别是在写 fetch 请求的时候很容易发生。所以我们需要重写这个方法,以帮助我们监控此类错误

下边是启动 JS 错误监控代码

/**
   * 页面 JS 错误监控
   */
  function recordJavaScriptError() {
    // 重写 console.error, 可以捕获更全面的报错信息
    var oldError = console.error;
    console.error = function () {
      // arguments 的长度为 2 时,才是 error 上报的时机
      // if (arguments.length < 2) return;
      var errorMsg = arguments[0] && arguments[0].message;
      var url = WEB_LOCATION;
      var lineNumber = 0;
      var columnNumber = 0;
      var errorObj = arguments[0] && arguments[0].stack;
      if (!errorObj) errorObj = arguments[0];
      // 如果 onerror 重写成功,就无需在这里进行上报了
      !jsMonitorStarted && siftAndMakeUpMessage(errorMsg, url, lineNumber, columnNumber, errorObj);
      return oldError.apply(console, arguments);
    };
    // 重写 onerror 进行 jsError 的监听
    window.onerror = function(errorMsg, url, lineNumber, columnNumber, errorObj)
    {
      jsMonitorStarted = true;
      var errorStack = errorObj ? errorObj.stack : null;
      siftAndMakeUpMessage(errorMsg, url, lineNumber, columnNumber, errorStack);
    };

    function siftAndMakeUpMessage(origin_errorMsg, origin_url, origin_lineNumber, origin_columnNumber, origin_errorObj) {
      var errorMsg = origin_errorMsg ? origin_errorMsg : '';
      var errorObj = origin_errorObj ? origin_errorObj : '';
      var errorType = "";
      if (errorMsg) {var errorStackStr = JSON.stringify(errorObj)
        errorType = errorStackStr.split(":")[0].replace('"',"");
      }
      var javaScriptErrorInfo = new JavaScriptErrorInfo(JS_ERROR, errorType + ":" + errorMsg, errorObj);
      javaScriptErrorInfo.handleLogInfo(JS_ERROR, javaScriptErrorInfo);
    };
  };

OK, 错误日志有了,该怎么计算错误率呢?

JS 错误发生率 = JS 错误个数 (一次访问页面中,所有的 js 错误都算一次)/PV (PC,IOS,Android 平台同理)

所以我们需要记下页面的 PV 记录

    /**
       * 添加一个定时器,进行数据的上传
       * 2 秒钟进行一次 URL 是否变化的检测
       * 10 秒钟进行一次数据的检查并上传
       */
      var timeCount = 0;
      setInterval(function () {checkUrlChange();
        // 循环 5 后次进行一次上传
        if (timeCount >= 25) {
          // 如果是本地的 localhost, 就忽略,不进行上传

          var logInfo = (localStorage[ELE_BEHAVIOR] || "") +
            (localStorage[JS_ERROR] || "") +
            (localStorage[HTTP_LOG] || "") +
            (localStorage[SCREEN_SHOT] || "") +
            (localStorage[CUSTOMER_PV] || "") +
            (localStorage[LOAD_PAGE] || "") +
            (localStorage[RESOURCE_LOAD] || "");

          if (logInfo) {localStorage[ELE_BEHAVIOR] = "";
            localStorage[JS_ERROR] = "";
            localStorage[HTTP_LOG] = "";
            localStorage[SCREEN_SHOT] = "";
            localStorage[CUSTOMER_PV] = "";
            localStorage[LOAD_PAGE] = "";
            localStorage[RESOURCE_LOAD] = "";
            utils.ajax("POST", HTTP_UPLOAD_LOG_INFO, {logInfo: logInfo}, function (res) {}, function () {})
          }
          timeCount = 0;
        }
        timeCount ++;
      }, 200);

上边的代码我用了定时器,大概的意思是 200 毫秒进行一次 URL 变化的检查,5 秒进行一次数据的检查,如果有数据就进行上传,并清空上一次的数据。为什么用定时器呢,因为在单页应用中,路由的切换和地址栏的变化是无法被监控的,我确实没有想到特别好的办法来监控,所以用了这种方式, 如果有人有更好的办法,请给我留言,谢谢。

封装简易的 Ajax

为了将这些数据上传到我们的服务器,我们总不能每次都用 xmlHttpRequest 来发送 ajax 请求吧,所以我们需要自己封装一个简单的 Ajax

/**
     *
     * @param method  请求类型 (大写)  GET/POST
     * @param url     请求 URL
     * @param param   请求参数
     * @param successCallback  成功回调方法
     * @param failCallback   失败回调方法
     */
    this.ajax = function(method, url, param, successCallback, failCallback) {var xmlHttp = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
      xmlHttp.open(method, url, true);
      xmlHttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
      xmlHttp.onreadystatechange = function () {if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {var res = JSON.parse(xmlHttp.responseText);
          typeof successCallback == 'function' && successCallback(res);
        } else {typeof failCallback == 'function' && failCallback();
        }
      };
      xmlHttp.send("data=" + JSON.stringify(param));
    }

二、JS Error 详细信息解析

统计 JS Error 的目的,一、是为了了解线上项目的健康状况,二、是为了分析错误,帮助我们查找问题之所在,并且解决它。

所以,如何定位线上的问题,并解决问题,是我们现在要讨论的重点。下面我们需要对几个关键点进行分析:

① 某种错误发生的次数——发生次数跟影响用户是成正比的,如果发生次数跟影响用户数量都很高,那么这是一个比较严重的 bug, 需要立即解决。反之,如果次数很多,影响用户数量很少。说明这种错误只发生在少量设备中,优先级相对较低,可以择时对该类机型设备进行兼容处理。当然,ip 地址访问次数也能说明这个问题

② 页面发生了哪些错误——这个有利于我们缩小问题的范围,方便我们排查,如:

③ 错误堆栈——这点不用说,是定位错误最重要的因素。正常情况下,代码都是被压缩的,所以我在后台解析并截取出错代码附近的一部分代码,进行展示,排查错误。PS: 我看到网上有人利用 jsMap 反向找到代码的具体位置,想法很不错,后期我会加上。另外,代码虽然被压缩,但是依然很轻松定位到出错的位置,如下图所示,所以这个功能暂时作为附加题,不用那么着急加上。

④ 设备信息——当错误发生是,分析出用户当时使用设备的浏览器信息,系统版本,设备机型等等,能够帮我们快速的定位到需要兼容的设备,进而提升解决问题的效率。

⑤ 用户足迹——我个人觉得比较有用,但是代价太高。因为这个需要记录下用户在页面上的所有行为,需要上传非常多的数据,功能待定。

这个功能已经在后边进行完善了,点击 查看足迹 按钮即可查出这个用的行为足迹,在定位线上问题方面,有很大的作用 , 我在后边的篇幅中有介绍 搭建前端监控系统(五)怎样定位线上问题

到此,已经收集到了 JS 错误日志的大部分信息了,并且已经分析出 JS 错误的详细信息了。

三、JS 报错的实时监控与报警

既然我们已经具有了搜集 js 报错和分析报错的能力了,那么我们也可以做到 Js 报错实时监控,以及实时预警了,这样可以防范线上事故于未然,及时的制止线上事故的持续发生, 减少损失。

如上图所示,我展示了从当前时间向前推算 24 小时,每小时报错数量。另外展示了 7 天前同一时间段的报错数量,如果你的项目健康稳定,那么在相同时间段的报错数量应该不会相差太大。如果出现相差太大的情况发生,说明线上出现了问题,此刻应该发出警告,避免线上事故的发生。demo 上暂未加上警告功能,但是原理清楚了,后边自然水到渠成。

关于 Fundebug

Fundebug 专注于 JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js 和 Java 线上应用实时 BUG 监控。自从 2016 年双十一正式上线,Fundebug 累计处理了 10 亿 + 错误事件,付费客户有阳光保险、核桃编程、荔枝 FM、掌门 1 对 1、微脉、青团社等众多品牌企业。欢迎大家免费试用!

退出移动版