乐趣区

关于前端:基于typescript开发前端错误及性能监控SDK

前端的谬误监控、性能数据往往对业务的稳定性有很重要的影响,即便咱们在开发阶段非常小心,也不免线上会出现异常,并且线上环境的异样咱们往往后知后觉。而页面的性能数据则关系到用户体验,因而采集页面的性能数据也非常的重要。

当初第三方残缺解决方案国外有 sentry,国内有 fundebug、frontjs,他们提供前端接入的 SDK 和数据服务,而后有肯定的收费额度,超出就须要应用付费计划。前端的 SDK 用户监控用户端异样和性能,后端服务用户能够创立利用,每个利用调配一个 APPKEY,而后 SDK 实现主动上报。

本文不思考数据服务,只对前端监控进行剖析,讲下 web 如何进行监控和采集这些数据,并且通过 TS 集成这些性能做出一套前端监控 SDK。

既然须要采集数据,咱们要明确下可能须要哪些数据,目前来看有如下一些数据:

  • 页面谬误数据
  • 页面资源加载状况
  • 页面性能数据
  • 接口数据
  • 手机、浏览器数据
  • 页面拜访数据
  • 用户行为数据

上面剖析一下这些数据如何获取:

页面谬误数据

  • window.onerror AOP 捕捉异样能力无论是异步还是非异步谬误,onerror 都能捕捉到运行时谬误。
  • window.onerror不能捕捉页面资源的加载谬误,但资源加载谬误能被 window.addEventListener 在捕捉阶段捕捉。因为 addEventListener 也可能捕捉 js 谬误,因而须要过滤防止反复触发事件钩子
  • window.onerror无奈捕捉 Promise 工作中未被解决的异样,通过 unhandledrejection 能够捕捉

页面资源加载异样

window.addEventListener(
  "error",
  function (event) {
    const target: any = event.target || event.srcElement;
    const isElementTarget =
      target instanceof HTMLScriptElement ||
      target instanceof HTMLLinkElement ||
      target instanceof HTMLImageElement;
    if (!isElementTarget) return false;

    const url = target.src || target.href;
    onResourceError?.call(this, url);
  },
  true
);

页面逻辑和未 catch 的 promise 异样

 const oldOnError = window.onerror;
 const oldUnHandleRejection = window.onunhandledrejection;

 window.onerror = function (...args) {if (oldOnError) {oldOnError(...args);
   }

   const [msg, url, line, column, error] = args;
   onError?.call(this, {
     msg,
     url,
     line,
     column,
     error
   });
 };

 window.onunhandledrejection = function (e: PromiseRejectionEvent) {if (oldUnHandleRejection) {oldUnHandleRejection.call(window, e);
   }

   onUnHandleRejection && onUnHandleRejection(e);
 };

在 Vue 中,咱们应该通过 Vue.config.errorHandler = function(err, vm, info) {}; 进行异样捕捉,这样能够获取到更多的上下文信息。

对于 React,React 16 提供了一个内置函数 componentDidCatch,应用它能够非常简单的获取到 react 下的错误信息

componentDidCatch(error, info) {console.log(error, info);
}

页面性能数据

通常咱们会关注以下性能指标:

  • 白屏工夫:从浏览器输出地址并回车后到页面开始有内容的工夫;
  • 首屏工夫:从浏览器输出地址并回车后到首屏内容渲染结束的工夫;
  • 用户可操作工夫节点:domready 触发节点,点击事件有反馈;
  • 总下载工夫:window.onload 的触发节点。

白屏工夫

白屏工夫节点指的是从用户进入网站(输出 url、刷新、跳转等形式)的时刻开始计算,始终到页面有内容展现进去的工夫节点。
这个过程包含 dns 查问、建设 tcp 连贯、发送首个 http 申请(如果应用 https 还要染指 TLS 的验证工夫)、返回 html 文档、html 文档 head 解析结束。

首屏工夫

首屏工夫的统计比较复杂,因为波及图片等多种元素及异步渲染等形式。察看加载视图可发现,影响首屏的次要因素的 图片的加载。通过统计首屏内图片的加载工夫便能够获取首屏渲染实现的工夫。

  • 页面存在 iframe 的状况下也须要判断加载工夫
  • gif 图片在 IE 上可能反复触发 load 事件需排除
  • 异步渲染的状况下应在异步获取数据插入之后再计算首屏
  • css 重要背景图片能够通过 JS 申请图片 url 来统计(浏览器不会反复加载)
  • 没有图片则以统计 JS 执行工夫为首屏,即认为文字呈现工夫

用户可操作工夫

DOM 解析结束工夫,可统计 DomReady 工夫,因为通常会在这个工夫点绑定事件

对于 web 端获取性能数据办法很简略,只须要应用浏览器自带的 Performance 接口

页面性能数据采集

Performance 接口能够获取到以后页面中与性能相干的信息,它是 High Resolution Time API 的一部分,同时也交融了 Performance Timeline API、Navigation Timing API、User Timing API 和 Resource Timing API。

从图中能够看到很多指标都是成对呈现,这里咱们间接求差值,就能够求出对应页面加载过程中要害节点的耗时,这里咱们介绍几个比拟罕用的,比方:

const timingInfo = window.performance.timing;

// DNS 解析,DNS 查问耗时
timingInfo.domainLookupEnd - timingInfo.domainLookupStart;

// TCP 连贯耗时
timingInfo.connectEnd - timingInfo.connectStart;

// 取得首字节消耗工夫,也叫 TTFB
timingInfo.responseStart - timingInfo.navigationStart;

// *: domReady 工夫(与 DomContentLoad 事件对应)
timingInfo.domContentLoadedEventStart - timingInfo.navigationStart;

// DOM 资源下载
timingInfo.responseEnd - timingInfo.responseStart;

// 筹备新页面工夫耗时
timingInfo.fetchStart - timingInfo.navigationStart;

// 重定向耗时
timingInfo.redirectEnd - timingInfo.redirectStart;

// Appcache 耗时
timingInfo.domainLookupStart - timingInfo.fetchStart;

// unload 前文档耗时
timingInfo.unloadEventEnd - timingInfo.unloadEventStart;

// request 申请耗时
timingInfo.responseEnd - timingInfo.requestStart;

// 申请结束至 DOM 加载
timingInfo.domInteractive - timingInfo.responseEnd;

// 解释 dom 树耗时
timingInfo.domComplete - timingInfo.domInteractive;

// *:从开始至 load 总耗时
timingInfo.loadEventEnd - timingInfo.navigationStart;

// *: 白屏工夫
timingInfo.responseStart - timingInfo.fetchStart;

// *: 首屏工夫
timingInfo.domComplete - timingInfo.fetchStart;

接口数据

接口数据次要包含接口耗时、接口申请异样,耗时能够通过对 XmlHttpRequest 和 fetch 申请的拦挡过程中进行工夫统计,异样通过 xhr 的 readyState 和 status 属性判断。

XmlHttpRequest 拦挡:批改 XMLHttpRequest 的原型,在发送申请时开启事件监听,注入 SDK 钩子
XMLHttpRequest.readyState 的五种就绪状态:

  • 0:申请未初始化(还没有调用 open())。
  • 1:申请曾经建设,然而还没有发送(还没有调用 send())。
  • 2:申请已发送,正在解决中(通常当初能够从响应中获取内容头)。
  • 3:申请在解决中;通常响应中已有局部数据可用了,然而服务器还没有实现响应的生成。
  • 4:响应已实现;您能够获取并应用服务器的响应了。
XMLHttpRequest.prototype.open = function (method: string, url: string) {
  // ... 省略
  return open.call(this, method, url, true);
};
XMLHttpRequest.prototype.send = function (...rest: any[]) {
  // ... 省略
  const body = rest[0];

  this.addEventListener("readystatechange", function () {if (this.readyState === 4) {if (this.status >= 200 && this.status < 300) {// ... 省略} else {// ... 省略}
    }
  });
  return send.call(this, body);
};

Fetch 拦挡:Object.defineProperty

Object.defineProperty(window, "fetch", {
  configurable: true,
  enumerable: true,
  get() {return (url: string, options: any = {}) => {return originFetch(url, options)
        .then((res) => {// ...})
    };
  }
});

手机、浏览器数据

通过 navigatorAPI 获取在进行解析,应用第三方包 mobile-detect 帮忙咱们获取解析

页面拜访数据

全局数据减少 url、页面题目、用户标识,SDK 能够主动为网页 session 调配一个随机用户 label 作为标识,以此标识单个用户

用户行为数据

次要蕴含用户点击页面元素、控制台信息、用户鼠标挪动轨迹。

  • 用户点击元素:window 事件代理
  • 控制台信息:重写 console
  • 用户鼠标挪动轨迹:第三方库 rrweb

上面是针对这些数据进行对立的监控 SDK 设计

SDK 开发

为更好的解耦模块,我决定应用基于事件订阅的形式,整个 SDK 分成几个外围的模块,因为应用 ts 开发并且代码会保持良好的命名标准和语义化,只有在要害的中央才会有正文,残缺的代码实现见文末 Github 仓库。

  • class: WebMonitor:外围监控类
  • class:AjaxInterceptor:拦挡 ajax 申请
  • class:ErrorObserver:监控全局谬误
  • class:FetchInterceptor:拦挡 fetch 申请
  • class:Reporter:上报
  • class:Performance:监控性能数据
  • class:RrwebObserver:接入 rrweb 获取用户行为轨迹
  • class:SpaHandler:针对 SPA 利用做解决
  • util: DeviceUtil:设施信息获取辅助函数
  • event: 事件核心

SDK 提供的事件

对外裸露事件,_结尾为框架外部事件

export enum TrackerEvents {
  // 对外裸露事件
  performanceInfoReady = "performanceInfoReady",  // 页面性能数据获取结束
  reqStart = "reqStart",  // 接口申请开始
  reqEnd = "reqEnd",   // 接口申请实现
  reqError = "reqError",  // 申请谬误
  jsError = "jsError",  // 页面逻辑异样
  vuejsError = "vuejsError",  // vue 谬误监控事件
  unHandleRejection = "unHandleRejection",  // 未解决 promise 异样
  resourceError = "resourceError",  // 资源加载谬误
  batchErrors = "batchErrors",  // 谬误合并上报事件,用户合并上报申请节俭申请数量
  mouseTrack = "mouseTrack",  //  用户鼠标行为追踪
}

应用形式

import {WebMonitor} from "femonitor-web";
const monitor = Monitor.init();
/* Listen single event */
monitor.on([event], (emitData) => {});
/* Or Listen all event */
monitor.on("event", (eventName, emitData) => {})

外围模块解析

WebMonitor、errorObserver、ajaxInterceptor、fetchInterceptor、performance

WebMonitor

集成了框架的其余类,对传入配置和默认配置进行 deepmerge,依据配置进行初始化

this.initOptions(options);

this.getDeviceInfo();
this.getNetworkType();
this.getUserAgent();

this.initGlobalData(); // 设置一些全局的数据,在所有事件中 globalData 中都会带上
this.initInstances();
this.initEventListeners();

API

反对链式操作

  • on:监听事件
  • off:移除事件
  • useVueErrorListener:应用 Vue 谬误监控,获取更具体的组件数据
  • changeOptions:批改配置
  • configData:设置全局数据

errorObserver

监听 window.onerror 和 window.onunhandledrejection,并且对 err.message 进行解析,获取想要 emit 的谬误数据。

window.onerror = function (...args) {
  // 调用原始办法
  if (oldOnError) {oldOnError(...args);
  }

  const [msg, url, line, column, error] = args;

  const stackTrace = error ? ErrorStackParser.parse(error) : [];
  const msgText = typeof msg === "string" ? msg : msg.type;
  const errorObj: IError = {};

  myEmitter.customEmit(TrackerEvents.jsError, errorObj);
};

window.onunhandledrejection = function (error: PromiseRejectionEvent) {if (oldUnHandleRejection) {oldUnHandleRejection.call(window, error);
  }

  const errorObj: IUnHandleRejectionError = {};
  myEmitter.customEmit(TrackerEvents.unHandleRejection, errorObj);
};

window.addEventListener(
  "error",
  function (event) {
    const target: any = event.target || event.srcElement;
    const isElementTarget =
      target instanceof HTMLScriptElement ||
      target instanceof HTMLLinkElement ||
      target instanceof HTMLImageElement;
    if (!isElementTarget) return false;

    const url = target.src || target.href;

    const errorObj: BaseError = {};
    myEmitter.customEmit(TrackerEvents.resourceError, errorObj);
  },
  true
);

ajaxInterceptor

拦挡 ajax 申请,并触发自定义的事件。对 XMLHttpRequest 的 open 和 send 办法进行重写

XMLHttpRequest.prototype.open = function (method: string, url: string) {const reqStartRes: IAjaxReqStartRes = {};

  myEmitter.customEmit(TrackerEvents.reqStart, reqStartRes);
  return open.call(this, method, url, true);
};

XMLHttpRequest.prototype.send = function (...rest: any[]) {const body = rest[0];
  const requestData: string = body;
  const startTime = Date.now();

  this.addEventListener("readystatechange", function () {if (this.readyState === 4) {if (this.status >= 200 && this.status < 300) {const reqEndRes: IReqEndRes = {};

        myEmitter.customEmit(TrackerEvents.reqEnd, reqEndRes);
      } else {const reqErrorObj: IHttpReqErrorRes = {};
        
        myEmitter.customEmit(TrackerEvents.reqError, reqErrorObj);
      }
    }
  });
  return send.call(this, body);
};

fetchInterceptor

对 fetch 进行拦挡,并且触发自定义的事件。

Object.defineProperty(window, "fetch", {
  configurable: true,
  enumerable: true,
  get() {return (url: string, options: any = {}) => {const reqStartRes: IFetchReqStartRes = {};
      myEmitter.customEmit(TrackerEvents.reqStart, reqStartRes);

      return originFetch(url, options)
        .then((res) => {
          const status = res.status;
          const reqEndRes: IReqEndRes = {};

          const reqErrorRes: IHttpReqErrorRes = {};

          if (status >= 200 && status < 300) {myEmitter.customEmit(TrackerEvents.reqEnd, reqEndRes);
          } else {if (this._url !== self._options.reportUrl) {myEmitter.customEmit(TrackerEvents.reqError, reqErrorRes);
            }
          }

          return Promise.resolve(res);
        })
        .catch((e: Error) => {const reqErrorRes: IHttpReqErrorRes = {};
          myEmitter.customEmit(TrackerEvents.reqError, reqErrorRes);
        });
    };
  }
});

performance

通过 Performance 获取页面性能,在性能数据齐备后 emit 事件

const {
  domainLookupEnd,
  domainLookupStart,
  connectEnd,
  connectStart,
  responseEnd,
  requestStart,
  domComplete,
  domInteractive,
  domContentLoadedEventEnd,
  loadEventEnd,
  navigationStart,
  responseStart,
  fetchStart
} = this.timingInfo;

const dnsLkTime = domainLookupEnd - domainLookupStart;
const tcpConTime = connectEnd - connectStart;
const reqTime = responseEnd - requestStart;
const domParseTime = domComplete - domInteractive;
const domReadyTime = domContentLoadedEventEnd - fetchStart;
const loadTime = loadEventEnd - navigationStart;
const fpTime = responseStart - fetchStart;
const fcpTime = domComplete - fetchStart;

const performanceInfo: IPerformanceInfo<number> = {
  dnsLkTime,
  tcpConTime,
  reqTime,
  domParseTime,
  domReadyTime,
  loadTime,
  fpTime,
  fcpTime
};

myEmitter.emit(TrackerEvents.performanceInfoReady, performanceInfo);

残缺 SDK 实现见下方 Github 仓库地址,欢送 star、fork、issue。

web 前端监控 SDK:https://github.com/alex1504/f…

退出移动版