前端的谬误监控、性能数据往往对业务的稳定性有很重要的影响,即便咱们在开发阶段非常小心,也不免线上会出现异常,并且线上环境的异样咱们往往后知后觉。而页面的性能数据则关系到用户体验,因而采集页面的性能数据也非常的重要。
当初第三方残缺解决方案国外有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;// 取得首字节消耗工夫,也叫TTFBtimingInfo.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...