而一个完整的前端监控平台至少需要包括三个部分:数据采集与上报、数据整理和存储、数据展示。算上需要监控的项目的话,也就是说,至少需要4个项目才能完整的记录前端监控的内容。
下图是一个完整的前端监控平台需要处理和解决的问题:
这么大一张图,估计大家看着也脑子发晕,而且这张图很多地方由于内容太多,还是省略之后的。
其实要做前端监控,很多时候,我们可以借助现成的平台去做
2、灯塔
4、神策
......
无论如何,我们至少先看看前端监控到底在监控什么内容
通过上面两个现成的框架,大家也大致能看出,我们前端监控到底是要干什么事情
其实完整的监控平台至少分为三大类
而上面总结的那一大堆,主要就是监控SDK的实现,SDK,其实就是SoftwareDevelopmentKit,其实就是提供实现监控的API
无论性能,行为还是异常情况,我们都需要在需要监控的项目代码中去监听这些内容。那么具体监听的手段其实就被称之为前端埋点。
前端埋点还分为手动埋点和无痕埋点。
手动埋点,就是在要监听的项目中的某段代码或者某个事件中加入一段监听SDK代码,然后对监听的内容进行上报,好处就是可以对关键性行为做出具体的跟踪,坏处是具有侵入性
无痕埋点,就是就是对监听的项目进行全部无脑监听,比如点击事件,滚动事件等等,只要触发了就上报。好处就是对代码没有侵入性,坏处当然也很明显无法快速定位关键信息,上报次数多,服务器压力大
虽然在我们开发完成之后,会经历多轮的单元测试、集成测试、人工测试,但是难免漏掉一些边缘的测试场景,甚至还有一些奇奇怪怪的玄学故障出现;而出现报错后,轻则某些数据页面无法访问,重则导致客户数据出错;
因此,我们的前端监控,需要对前端页面的错误进行监控,一个强大完整的错误监控系统,可以帮我们做以下的事情:
当JavaScript运行时产生的错误就属于JS运行异常,比如我们常见的:
TypeError:CannotreadpropertiesofnullTypeError:xxxisnotafunctionReferenceError:xxxisnotdefined像这种运行时异常,我们很少手动去捕获它,当它发生异常之后,js有两种情况都会触发它
这里有一个点需要特别注意,SyntaxError语法错误,除了用eval()执行的脚本以外,一般是不可以被捕获到的。
其实原因很简单,语法错误,在编译解析阶段就已经报错了,而拥有语法错误的脚本不会放入任务队列进行执行,自然也就不会有错误冒泡到我们的捕获代码。
当然,现在代码检查这么好用,早在编写代码时这种语法错误就被避免掉了,一般我们碰不上语法错误的~
window.onerror是一个全局变量,默认值为null。当有js运行时错误触发时,window会触发error事件,并执行window.onerror(),借助这个特性,我们对window.onerror进行重写就可以捕获到代码中的异常
constrawOnError=window.onerror;//监听js错误window.onerror=(msg,url,line,column,error)=>{//处理原有的onerrorif(rawOnError){rawOnError.call(window,msg,url,line,column,error);}console.log("监控中......");console.log(msg,url,line,column,error);}2、window.addEventListener('error')window.addEventListener('error')来捕获JS运行异常;它会比window.onerror先触发;
window.addEventListener('error',e=>{console.log(e);},true)两者的区别和选用更加建议使用第二种addEventListener('error')的方式;原因很简单:不像方法一可以被window.onerror重新覆盖;而且可以同时处理静态资源错误
界面上的link的css、script的js资源、img图片、CDN资源打不开了,其实都会触发window.addEventListener('error')事件
使用addEventListener捕获资源错误时,一定要将第三个选项设为true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。
我们只需要再事件中加入简单的判断,就可以区分是资源加载错误,还是js错误
Promise.resolve().then(()=>console.log(c));Promise.reject(Error('promise'))而当抛出Promise异常时,会触发unhandledrejection事件,所以我们只需要去监听它就可以进行Promise异常的捕获了,不过值得注意的一点是:相比与上面所述的直接获取报错的行号、列号等信息,Promise异常我们只能捕获到一个报错原因而已
window.addEventListener('unhandledrejection',e=>{console.log("---promiseErr监控中---");console.error(e)})Vue2、Vue3错误捕获我们可以利用这两个钩子函数来进行错误捕获,由于是依赖于Vue配置函数的错误捕获,所以我们在初始化时,需要用户将Vue实例传进来;
看到这里,其实有的同学可能会疑惑,我们现在的调用HTTP接口,一般也就是通过async/await这种基于Promise的解决异步的最终方案;那么,假如说请求了一个接口地址报了500,因为是基于Promise调用的接口,我们能够在上文的Promise异常捕获中,获取到一个错误信息(如下图);
但是有一个问题别忘记了,Promise异常捕获没办法获取报错的行列,我们只知道Promise报错了,报错的信息是接口请求500;但是我们根本不知道是哪个接口报错了;
所以说,我们对于Http请求异常的捕获需求就是:全局统一监控、报错的具体接口、请求状态码、请求耗时以及请求参数等等;
而为了实现上述的监控需求,我们需要了解到:现在异步请求的底层原理都是调用的XMLHttpRequest或者Fetch,我们只需要对这两个方法都进行劫持,就可以往接口请求的过程中加入我们所需要的一些参数捕获;
还有一种错误,平常我们较难遇到,那就是跨域脚本错误,简单来说,就是你跨域调用的内容出现的错误。
当跨域加载的脚本中发生语法错误时,浏览器出于安全考虑,不会报告错误的细节,而只报告简单的Scripterror。浏览器只允许同域下的脚本捕获具体错误信息,而其他脚本只知道发生了一个错误,但无法获知错误的具体内容(控制台仍然可以看到,JS脚本无法捕获)
其实对于三方脚本的错误,我们是否捕获都可以,不过我们需要一点处理,如果不需要捕获的话,就不进行上报,如果需要捕获的话,只上报类型;
和Vue不同的是,我们需要自己定义一个类组件暴露给项目使用,我这里就不具体详写了,感兴趣的同学可以自己进行补全:
现在看来好像没什么问题,但是其实通过ajax上报这种方式存在很大的问题。
过去,为了解决这个问题,统计和诊断代码通常要在
上述的所有方法都会迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力。
这就是sendBeacon()方法存在的意义。使用sendBeacon()方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能,这意味着:
navigator.sendBeacon(url);navigator.sendBeacon(url,data);参数返回值当用户代理成功把数据加入传输队列时,sendBeacon()方法将会返回true,否则返回false。
要弄懂requestIdleCallback函数,其实最主要的是要清楚,浏览器在一帧里面做了什么?
我们上传数据,也可以利用这一点,更好的处理上传时机
exportfunctionreport(type,data,isImmediate=false){ //其他代码省略......//立即上传if(isImmediate){sendBeacon(config.reportUrl,reportData)return}//------requestIdleCallback方式上报------if(window.requestIdleCallback){window.requestIdleCallback(()=>{sendBeacon(config.reportUrl,reportData)},{timeout:3000})}else{setTimeout(()=>{sendBeacon(config.reportUrl,reportData)})}}延迟上报还有一种情况,如果大量的问题需要上传,比如用户疯狂点击出现错误情况,那么每次上报这种情况也不太好,因此做一下延迟上报处理,也很有必要。
//utils/cache.jsconstcache=newMap();exportfunctiongetCache(){returncache;}exportfunctionaddCache(type,data){cache.get(type)cache.get(type).push(data):cache.set(type,[data]);}exportfunctionclearCache(){cache.clear()}//report/index.js//其他代码省略lettimer=nullexportfunctionlazyReportCache(type,data,timeout=3000){console.log(data);addCache(type,data)clearTimeout(timer)timer=setTimeout(()=>{constdataMap=getCache()if(dataMap.size){for(const[type,data]ofdataMap){console.log(`${type},${data}`);report(type,data)}clearCache()}},timeout)}然后直接将之前report的调用换成lazyReportCache调用,当然,后端的代码还需要修改,因为现在提交的都是数组了
其实除了传统的ajax方式,以及Navigator.sendBeacon()方式,还可以采用图片打点上报的方式。
这种方式可以避免页面切换阻塞的问题,但是缺点也很明显:
1、由于是url地址传值,所以传值的数据长度有限
2、地址传递需要后端单独做处理
letoImage=newImage();oImage.src=`${url}logs=${data}`;页面性能监控我们都听说过性能的重要性。但当我们谈起性能,以及让网站"速度提升"时,我们具体指的是什么?
其实性能是相对的:
因此,在谈论性能时,重要的是做到精确,并且根据能够进行定量测量的客观标准来论及性能。这些标准就是指标。
前端性能监控,就是要监测页面的性能情况,将各种的性能数据指标量化并收集
Lighthouse是一个网站性能测评工具,它是GoogleChrome推出的一个开源自动化工具。能够对网页多方面的效果指标进行评测,并给出最佳实践的建议以帮助开发者改进网站的质量。它的使用方法也非常简单,我们只需要提供一个要测评的网址,它将针对此页面运行一系列的测试,然后生成一个有关页面性能的报告。通过报告我们就可以知道需要采取哪些措施来改进应用的性能和体验。
在高版本(>=60)的Chrome浏览器中,Lighthouse已经直接集成到了调试工具DevTools中了,因此不需要进行任何安装或下载。
Lighthouse能够生成一份该网站的报告,比如下图:
性能评分的分值区间是0到100,如果出现0分,通常是在运行Lighthouse时发生了错误,满分100分代表了网站已经达到了98分位值的数据,而50分则对应75分位值的数据
Lighthouse会针对当前网站,给出一些Opportunities优化建议
Opportunities指的是优化机会,它提供了详细的建议和文档,来解释低分的原因,帮助我们具体进行实现和改进
Opportunities给出优化建议列表
Diagnostics指的是现在存在的问题,为进一步改善性能的验证和调整给出了指导
Diagnostics诊断问题列表
打开Chrome浏览器控制台,选择Performance选项,点击左侧reload图标
为了帮助开发者更好地衡量和改进前端页面性能,W3C性能小组引入了NavigationTimingAPI,实现了自动、精准的页面性能打点;开发者可以通过window.performance属性获取。
我们可以通过performanceAPI获取下面的内容
w3clevel2扩充了performance的定义,并增加了PerformanceObserver的支持。
newPerformanceObserver((entryList)=>{for(constentryofentryList.getEntriesByName('first-contentful-paint')){console.log('FCPcandidate:',entry.startTime,entry);}}).observe({type:'paint',buffered:true});那么关键点来了,性能指标到底有些啥,每个性能指标有什么作用?
w3c制定了一大堆指标,不过google发布了web-vitals,它是一个开源的用以衡量性能和用户体验的工具,对于我们现在来说,这个开源工具中所提到的指标已经足够用了。而且现在本身也是业界标准
什么叫以用户为中心的性能指标呢?其实就是可以直接的体现出用户的使用体验的指标;目前Google定义了FCP、LCP、CLS等体验指标,
对于用户体验来说,指标可以简单归纳为加载速度、视觉稳定、交互延迟等几个方面;
您会注意到,虽然部分内容已完成渲染,但并非所有内容都已经完成渲染。这是首次内容绘制(FCP)与LargestContentfulPaint最大内容绘制(LCP)(旨在测量页面的主要内容何时完成加载)之间的重要区别。
为了提供良好的用户体验,网站应该努力将首次内容绘制控制在1.8秒或以内。为了确保您能够在大部分用户的访问期间达成建议目标值,一个良好的测量阈值为页面加载的第75个百分位数,且该阈值同时适用于移动和桌面设备。
虽然延迟加载的内容通常比页面上已有的内容更大,但实际情况并非一定如此。接下来的两个示例显示了在页面完全加载之前出现的最大内容绘制。
在第一个示例中,Instagram标志加载得相对较早,即使其他内容随后陆续显示,但标志始终是最大元素。在Google搜索结果页面示例中,最大元素是一段文本,这段文本在所有图像或标志完成加载之前就显示了出来。由于所有单个图像都小于这段文字,因此这段文字在整个加载过程中始终是最大元素。
为了提供良好的用户体验,网站应该努力将CLS分数控制在0.1或以下。为了确保您能够在大部分用户的访问期间达成建议目标值,一个良好的测量阈值为页面加载的第75个百分位数,且该阈值同时适用于移动和桌面设备。
前一帧和当前帧的所有不稳定元素的可见区域集合(占总可视区域的部分)就是当前帧的影响分数。
在上图中,有一个元素在一帧中占据了一半的可视区域。接着,在下一帧中,元素下移了可视区域高度的25%。红色虚线矩形框表示两帧中元素的可见区域集合,在本示例中,该集合占总可视区域的75%,因此其影响分数为0.75。
例如,在对用户交互进行响应前,以下所有HTML元素都需要等待主线程上正在进行的任务完成运行:
虽然任何输入延迟都可能导致糟糕的用户体验,但我们主要建议您测量首次输入延迟,原因如下:
什么叫以技术为中心的性能指标呢?
所谓埋点是数据采集领域(尤其是用户行为数据采集领域)的术语,其实严格来说,我们之前对错误数据的采集,对性能数据的采集,都算是一种埋点。
埋点方案:
PV(pageview)是页面浏览量,UV(Uniquevisitor)用户访问量。PV只要访问一次页面就算一次,UV同一天内多次访问只算一次。
对于前端来说,只要每次进入页面上报一次PV就行,UV的统计可以放在服务端来做
利用addEventListener()监听popstate、hashchange页面跳转事件。需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)。同理,hashchange也一样。