前端异常监控金沙官网线上:

=====================================================================

为什么要做前端异常监控

  • 有些问题只存在于线上特定的环境
  • 后端错误有监控,前端错误没有监控

*前端性能监控系统:* 预览地址

基本实现

参考我们nodejs服务层和app的错误上报处理,基本流程为:
触发错误->捕获错误->错误上报->记录日志文件->存入ELK方便查询

  • 错误捕获:

    1. 接口请求error
    2. 全局监听异常
    3. 主动捕获
  • 错误上报:
    这里是需要我们参照app的地方,我们有一个日志服务器专门接收记录日志。

    这里需要我们的上报数据格式和日志服务器一致。

  • 记录日志文件和存入ELK
    我们根据上报的日志数据中两个key生成不同的日志文件,然后将ELK和这个日志文件绑定能够方便我们在kibana上查询。

根据基本流程,可以确定我们每个部分的职责:

  • 前端

    • 报错事件监听
    • 报错处理上报
  • 后端

    • 提供接口收集报错
    • 存储入ELK方便查询

=====================================================================

预期问题

  • 前端错误分类
  • 如何查明出错位置
  • 错误如何上报
  • 错误消息数据结构
  • 如何平滑的应用在业务项目中

  对于前端应用来说,Js错误的发生直接影响前端应用的质量。对前端异常的监控是整个前端监控系统中的一个重要环节。 那么如何做到对Js错误的监控呢?对搜集到的js错误,我们该如何去分析呢?分析的结果该如何展示呢?这些问题将直接关系到是否能够监控和分出去有价值的数据。

错误分类

  1. javascript异常
    • 语法错误
    • 运行时错误
    • script文件内错误(跨域和未跨域)
  2. JS文件、CSS文件、img图片等(资源)的404错误(其实是有onerror事件的dom)
  3. promise的异常捕获
  4. ajax请求错误

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

错误捕获

  1. 主动捕获(try catch / promise catch)
  2. 全局捕获(onerror / addEventListener)

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

主动捕获

我们在一些运算之后,得到一个期望的结果,然而结果不是我们想要的,这时可以上报一下错误。
基本上主动捕获也就是要求我们调用Logger.error(error, tag, message)(这个是前端监控js文件提供的一个方法)方法主动上报

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

try catch 捕获

try {} catch(err) {}无法捕捉到异步错误和语法错误

自动添加try catch仅能对js文件生效,无法对html文件进行操作。(可以在catch中上报关于代码位置)

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

全局捕获

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

金沙官网线上 1

 

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

// 设置日志对象类的通用属性
  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();

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

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

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

  下边是启动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记录

 // 用户访问行为日志(PV)
    function CustomerPV(uploadType, loadType, loadTime) {
      setCommonProperty.apply(this);
      this.uploadType = uploadType;
      this.loadType = loadType;  // 用以区分首次加载
      this.loadTime = loadTime; // 加载时间
    }
    CustomerPV.prototype = new MonitorBaseInfo();
    /**
     * 添加一个定时器,进行数据的上传
     * 3秒钟进行一次URL是否变化的检测
     * 15秒钟进行一次数据的检查并上传
     */
    var defaultLocation = window.location.href.split('?')[0].replace('#', '');
    var timeCount = 0;
    setInterval(function () {
      // 如果是单页应用, 只更改url
      var webLocation = window.location.href.split('?')[0].replace('#', '');
      // 如果url变化了, 就把更新的url记录为 defaultLocation, 重新设置pageKey
      if (defaultLocation != webLocation) {
        recordPV();
        defaultLocation = webLocation;
      }
      // 循环5后次进行一次上传
      if (timeCount >= 5) {
        var logInfo = localStorage[ELE_BEHAVIOR] || "" +
          localStorage[JS_ERROR] || "" +
          localStorage[CUSTOMER_PV] || "";
        if (logInfo) {
          utils.ajax("POST", HTTP_UPLOAD_LOG_INFO, {logInfo: logInfo}, function (res) {
            // 上传完成后,清空本地记录
            if (res.code === 200) {
              localStorage[ELE_BEHAVIOR] = "";
              localStorage[JS_ERROR] = "";
              localStorage[CUSTOMER_PV] = "";
            }
          })
        }
        timeCount = 0;
      }
      timeCount ++;
    }, 3000);

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

 

onerror事件
/**
  * @param {String}  msg    错误信息
  * @param {String}  url    出错文件
  * @param {Number}  row    行号
  * @param {Number}  col    列号
  * @param {Object}  error  错误详细信息
  */
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了');
  console.log({
    msg,  url,  row, col, error
  })
  return true; // 注意,在返回 true 的时候,异常才不会继续向上抛出error;
};

打印如下:

我知道错误了
{
    msg: "Uncaught ReferenceError: error is not defined", 
    url: "file:///Users/beifeng/Desktop/test.html", 
    row: 25, 
    col: 5, 
    error: ReferenceError: error is not defined at 
}

通过为页面上的 script 标签添加 crossOrigin 属性完成跨域上报,别忘了服务器也设置 Access-Control-Allow-Origin 的响应头。(解决跨域的js脚本错误上报)

通常我们使用window.onerror来捕获js脚本的错误信息。但是对于跨域调用的js脚本,onerror事件只会给出很少的报错信息:error: Script error.这个简单的信息很明显不足以看出脚本的具体错误,所以我们可以使用crossorigin属性,使得加载的跨域脚本可以得出跟同域脚本同样的报错信息:
<script crossorigin src="http://www.lmj.com/demo/crossoriginAttribute/error.js"></script>
如果是这样,www.lmj.com的服务器必须给出一个Access-Control-Allow-Origin的header,否则无法访问此脚本。

// 举个例子
// http://localhost:8080/index.html
<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log('我知道错误了,也知道错误信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<script src="http://localhost:8081/test.js" crossorigin></script>

// http://localhost:8081/test.js
setTimeout(() => {
  console.log(error);
});

onerror事件 是无法捕获到网络异常的错误(资源加载失败,裸奔,图片显示异常等)。当我们遇到 <img src="./404.png"> 报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。

二、JS Error 详细信息解析

 

金沙官网线上 2  

  统计JS Error的目的,一是为了了解线上项目的健康状况,二是为了分析错误,帮助我们查找问题之所在,并且解决它。所以,为了查找并解决问题,我们需要对几个关键点进行分析。

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

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

  金沙官网线上 3

  ③  错误堆栈——这点不用说,是定位错误最重要的因素。正常情况下,代码都是被压缩的,所以我在后台解析并截取出错代码附近的一部分代码,进行展示,排查错误。 金沙官网线上 4

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

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

  金沙官网线上 5

  到此,已经收集到了JS错误日志的大部分信息了,并且已经分析出JS错误的详细信息了。只需要将其上传,入库,再进行分析展现,就可以看到JS错误信息的预览效果和详情。所以,我们再去部署一下后台代码。

 

  上一章: 搭建前端监控系统(一)阿里云服务器搭建篇

  下一章: 搭建前端监控系统(三)NodeJs服务器部署篇

 


 

  

  为了将这些数据上传到我们的服务器,我们总不能每次都用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));
    }

 

window.addEventListener监听error事件

由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

<script>
/**
  * @param {String}    event         监听事件
  * @param {function}  function      出错文件
  * @param {Boolean}   useCapture    指定事件是否在捕获或冒泡阶段执行。
  *        true - 事件句柄在捕获阶段执行
  *        false- false- 默认。事件句柄在冒泡阶段执行
  */
window.addEventListener('error', (error) => {
  console.log('我知道 404 错误了');
  console.log(
    error
  );
  return true;
}, true);
</script>
<img src="./404.png" alt="金沙官网线上 6">


打印如下:

我知道 404 错误了
<!--资源加载错误-->
bubbles:false
cancelBubble:false
cancelable:false
composed:false
currentTarget:null
defaultPrevented:false
falseeventPhase:0
isTrusted:true
path:Array(5) // dom树
  0:img
  1:div
  2:body
  3:html
  4:document
  5:Window
returnValue:true
srcElement:img // 发生错误的dom
target:img
timeStamp:5.545000000000001 // 错误发生的时间(按页面加载时间为0)
type:"error"

<!--运行时错误-->
bubbles:false
cancelBubble:false
cancelable:true
colno:13 // 列号
composed:false
currentTarget:Window
defaultPrevented:true
error:TypeError: hahahah at file:///home/jhjr/Desktop/%E5%89%8D%E7%AB%AF%E5%BC%82%E5%B8%B8%E7%9B%91%E6%8E%A7/test.html:22:13
  message:"Unexpected identifier" // 错误信息
  stack:"SyntaxError: Unexpected identifier" // 错误栈
eventPhase:0
filename:"file:///home/jhjr/Desktop/%E5%89%8D%E7%AB%AF%E5%BC%82%E5%B8%B8%E7%9B%91%E6%8E%A7/test.html" // 错误文件
isTrusted:true
lineno:22 // 行号
message:"Uncaught TypeError: hahahah"
path:Array(1) // DOM树
returnValue:false
srcElement:Window // 发生错误的dom
target:Window
timeStamp:1005.4350000000001 // 错误发生的时间(按页面加载时间为0)
type:"error"

网上说addEventListener的浏览器兼容性不太好,去Can I use查了一下其实还好.具体看这里addEventListener.

还有一个问题是它的error对象在不同浏览器会有不同的一个体现,这里需要注意下.

本文由金沙官网线上发布于Web前端,转载请注明出处:前端异常监控金沙官网线上:

您可能还会对下面的文章感兴趣: