8

搭建前端监控系统(二)JS错误日志收集篇

 2 years ago
source link: https://zhuanlan.zhihu.com/p/35771536
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

  这是搭建前端监控系统的第二章,主要是介绍如何统计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 监控功能 (数据概览)

v2-984b79ae69c5b0b31a30e3156c715f29_b.jpgv2-984b79ae69c5b0b31a30e3156c715f29_1440w.jpg

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

  // 设置日志对象类的通用属性
  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错误监控代码:

一、重写window.onerror 方法:

    // 重写 onerror 进行jsError的监听
    window.onerror = function(errorMsg, url, lineNumber, columnNumber, errorObj) {
      jsMonitorStarted = true;
      var errorStack = errorObj ? errorObj.stack : null;
      siftAndMakeUpMessage(errorMsg, url, lineNumber, columnNumber, errorStack);
    };

二、重写console.error方法:

    var oldError = console.error;
    console.error = function (errorMsg) {
      siftAndMakeUpMessage(errorMsg, WEB_LOCATION, 0, 0, {"CustomizeError": "CustomizeError: No error stack"});
      return oldError.apply(console, arguments);
    };

三、重写window.onunhandledrejection方法:

    window.onunhandledrejection = function(e) {
      var errorMsg = "";
      if (typeof e.reason === "object") {
        errorMsg = JSON.stringify(e.reason);
      } else {
        errorMsg = e.reason;
      }
      siftAndMakeUpMessage(errorMsg, WEB_LOCATION, 0, 0, {"CustomizeError": "CustomizeError: No error stack"});
    }

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 详细信息解析

v2-1d43f8e568ab277cf1ad70d9fecea9d0_b.jpgv2-1d43f8e568ab277cf1ad70d9fecea9d0_1440w.jpg

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

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

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

v2-76746ebd688ff22a33583eb19b9e3a00_b.jpgv2-76746ebd688ff22a33583eb19b9e3a00_1440w.jpg


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

v2-87e9ad7eb5b1bd898d1d69b889c7d967_b.jpgv2-87e9ad7eb5b1bd898d1d69b889c7d967_1440w.jpg


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

v2-1f3b3e99590bede9e8f523f39664c895_b.jpgv2-1f3b3e99590bede9e8f523f39664c895_1440w.jpg


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

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

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

v2-6a9598190d3b21bba32b26150ca19641_b.jpgv2-6a9598190d3b21bba32b26150ca19641_1440w.jpg


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

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

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

v2-bb5bcf288a6c7fb18f75e75dde84828f_b.jpgv2-bb5bcf288a6c7fb18f75e75dde84828f_1440w.jpg


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

上一章:

下一章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK