前端监控尝试

背景

公司举行黑客马拉松其中一个赛题就是进行前端错误和性能监控,以前一直想过作这么一件事情,当线上发生代码错误或者资源引用错误时不是经过用户反馈得知,而是开发人员能在第一时间知晓,此次终于有机会进行尝试,与另外两名前端同事共同进行了一天一晚上的代码长跑。javascript

设计方案

  • 监控哪些

前端监控通常分为错误监控和性能监控,而咱们这次也是主要从这两方面进行监控。html

  • 与项目集成方式

集成方式考虑到有非侵入式集成(SDK)和侵入式集成,分析二者利弊后决定采用非侵入式集成,但愿能够像JQuery同样,经过一行代码直接引入,在项目中直接使用,最后的理想状态就是:一行代码,开箱即用。前端

类型 优势 缺点
非侵入式 主动监测,指标齐全 没法监控复杂应用、监控的数据可能较少,没法捕获已经try-catch的数据
侵入式 能够监控复杂应用,进行细致监控 须要侵入源代码
  • 指标的上报、存储与展现

监控sdk收到的信息经过指定接口(性能和错误接口分开)上报给后台,后台使用MongoDB进行存储和数据的处理,前台页面进行数据的直观显示。vue

监控数据收集方式

错误处理

  • js错误监控方式

咱们能够经过try/catch方式进行错误的捕获,可是考虑到无侵入式的原则,最后选择使用全局的error事件进行捕获错误。html5

var handleWindowError = function (_window, config) {
  _oldWindowError = _window.onerror;
  _window.onerror = function (msg, url, line, col, error) {
    // console.log(error);
    var eventId = `${url}${line}${col}`
    config.sendError({
      title: msg,
      msg: {
        resourceUrl: url,
        rowNum: line,
        colNum: col,
        info: error,
        filename: url,
        eventId: eventId,
      },
      category: 'js',
      level: 'error'
    })
    if (_oldWindowError && isFunction(_oldWindowError)) {
      windowError && windowError.apply(window, arguments);
    }
  }
}
复制代码
  • 异步代码错误监控

经过监听全局'unhandledrejection'事件能够捕获未处理的 reject 。java

var handleRejectPromise = function (_window, config) {
  _window.addEventListener('unhandledrejection', function (event) {
    if (event) {
      var reason = event.reason;
      config.sendError({
        title: '不捕获Promise异步错误',
        msg: reason,
        category: 'js',
        level: 'error'
      });
    }
  }, true);
}

复制代码
  • 资源请求错误

当页面中如【img src='./404.png'】引入不存在的资源时,会出现未找到资源的错误,若是在引入未找到的js将会形成页面严重错误。 所以能够经过监控监听到的错误是否含有html标签,而且标签中是否有src或者href属性进行资源请求的监控。node

var handleResourceError = function (_window, config) {
  _window.addEventListener('error', function (event) {
    if (event) {
      var target = event.target || event.srcElement;
      var isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
      if (!isElementTarget) return; // js error再也不处理

      var url = target.src || target.href;
      config.sendError({
        title: target.nodeName,
        msg: {
          url: url,
          eventId: url,
        },
        category: 'resource',
        level: 'error',
      });
    }
  }, true);
}

复制代码
  • 请求错误

经过监控原生XMLHttpRequest属性进行ajax请求的监控。git

var handleAjaxError = function (_window, config) {
  var protocol = _window.location.protocol;
  if (protocol === 'file:') return;

  // 处理fetch
  _handleFetchError(_window, config);

  // 处理XMLHttpRequest
  if (!_window.XMLHttpRequest) {
    return;
  }
  var xmlhttp = _window.XMLHttpRequest;

  var _oldSend = xmlhttp.prototype.send;
  var _handleEvent = function (event) {
    if (event && event.currentTarget && event.currentTarget.status !== 200) {
      config.sendError({
        title: event.target.responseURL,
        msg: {
          response: event.target.response,
          responseURL: event.target.responseURL,
          status: event.target.status,
          statusText: event.target.statusText,
          eventId: event.target.responseURL,
        },
        category: 'ajax',
        level: 'error'
      });
    }
  }
}

复制代码
  • 控制台错误监控

经过监控浏览器控制台输出的错误日志进行错误的捕获。github

var handleConsoleError = function (_window, config) {
  if (!_window.console || !_window.console.error) return;

  var _oldConsoleError = _window.console.error;
  _window.console.error = function () {
    config.sendError({
      title: 'consoleError',
      msg: JSON.stringify(arguments),
      category: 'js',
      level: 'error'
    });
    _oldConsoleError && _oldConsoleError.apply(_window, arguments);
  };
}

复制代码
  • vue经过指定钩子函数进行错误监控

vue中有指定的钩子函数errorHandler进行错误的监控,能够经过监控errorHandler收集的错误进行错误的监控。ajax

var handleVueError = function (_window, config) {
  var vue = _window.Vue;
  if (!vue || !vue.config) return; // 没有找到vue实例
  
  var _oldVueError = vue.config.errorHandler;

  Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
    var metaData = {};
    if (Object.prototype.toString.call(vm) === '[object Object]') {
      metaData.componentName = vm._isVue ? vm.$options.name || vm.$options._componentTag : vm.name;
      metaData.propsData = vm.$options.propsData;
    }
    config.sendError({
      title: 'vue Error',
      msg: {
        meta: metaData,
        info,
      },
      category: 'js',
      level: 'error'
    });

    if (_oldVueError && isFunction(_oldVueError)) {
      _oldOnError.call(this, error, vm, info);
    }
  };
}

复制代码

性能监控

  • 性能监控

    主要监测当前页面

    1. 页面彻底加载时间
    2. HTTP请求响应完成时间
    3. 脚本加载时间
    4. DOM加载完成时间
    5. onload事件时间 ...

    主要经过window.performance进行页面性能监控。

使用

直接在项目首页进行引入,会在建立项目时生成如下代码,一行代码,开箱即用。(示例代码仅供参考)

<script src="https://kylenxu.github.io/monitor-td/index.js?errorMonitor=true&performanceMonitor=true&projectId=82b12c829722d727e6ca40b8aa166e43&name=test&errorUrl=http://172.30.104.166:8038/api/errors&performanceError=http://172.30.104.166:8038/api/performance&vue=true&js=true"></script>
复制代码

字段说明

对应上面URL地址中参数:

属性 说明 类型 默认值
projectId 项目ID String
name 项目名称 String
errorUrl 错误监控数据上报地址 String
performanceError 性能监控数据上报地址 String
errorMonitor 是否进行错误监控 Boolean true
performanceMonitor 是否进行性能监控 Boolean true
vue 是否进行VUE项目监控 Boolean true
js 是否进行JS项目监控 Boolean true

特色

  • js语法报错

    主要经过window.onerror()进行页面错误监控。

  • 异步代码运行报错

    主要经过window.addEventListener('unhandledrejection',fn());进行页面异步代码错误监控。

  • 资源加载报错

    主要经过window.addEventListener('error',fn());进行页面异步代码错误监控。

  • 接口请求报错

    主要经过window.fetch();进行接口请求错误监控。

  • ajax请求报错

    主要经过对window.XMLHttpRequest监控,进行ajax接口请求错误监控。

  • 控制台错误信息

    主要经过window.console.error();进行控制台错误监控。

    注:html5: console.error会打印日志,显示红色的错误信息,但不会阻挡对下面js的执行。

    windows: onsole.error会阻挡程序执行,js异常就是语法或逻辑错误,好比 this._btn1 = abc; //abc是不存在这样会阻挡后面语句的执行,不单阻挡了本函数,函数的外边的后面代码也不执行了,也就是本次调用堆栈就报废了。

  • VUE错误监控

    主要经过Vue.config.errorHandler();钩子函数进行VUE错误监控。

  • 性能监控

    主要检测当前页面彻底加载时间、HTTP请求响应完成时间、脚本加载时间等信息。

    主要经过window.performance进行页面性能监控。

总结

最终实现了一个最小可用的前端监控系统,经过一行代码直接引入,在项目中直接使用,实现了最终的理想状态:一行代码,开箱即用。

致敬

  • 一块儿熬夜的兄弟们
相关文章
相关标签/搜索