?? ? ? ? 随着产品的用户数量的不断增长,对于站点体验衡量的的需求也日益紧迫,用户会将产品和他们每天使用的体验最好的 Web 站点进行比较。想着手优化,则必须先有相关的监控数据,才能对症下药。
性能是留住用户的关键 。 大量的研究报告已经表明了性能和商业成绩的关系,糟糕的性能会让您的站点损失用户数、转化率和口碑。错误监控则能够让开发者第一时间发现并修复问题 ,单靠用户遇到问题并反馈是不现实的,当用户遇到白屏或者接口错误时,更多的人可能会重试几次、失去耐心然后直接关掉您的网站。
为什么要做前端监控
- 更快的发现问题和解决问题
- 做产品的决策依据
- 为业务扩展提供更多的可能性
- 根据指标优化产品
监控哪些东西?
稳定性(报错相关):
错误名称 | 说明 |
---|
js错误 | js执行错误,promise异常 | 资源异常 | script,link,img,css等资源的加载异常 | 接口错误 | ajax,fetch请求接口异常 | 白屏 | 页面空白 |
用户体验(性能相关):
性能指标名称 | 说明 |
---|
加载时间 | 各个阶段的加载时间 | TTFB(time to first byte 首字节时间) | 是指浏览器发起第一个请求到数据返回第一个字节所消耗时间 | FP(First Paint 首次绘制时间) | 首次渲染的时间,是第一个像素点绘制到屏幕的时间 | FCP(First Contentful Paint 首次内容绘制时间) | 首次有内容绘制渲染的时间,指浏览器将第一个dom渲染到屏幕的时间 | FMP(First Meaningful paint 首次有意义绘制) | 首次有意义绘制时间 | FID(First Input Delay 首次输入延迟) | 用户首次和页面交互到页面响应交互的时间 | 卡顿 | 超过150ms的长任务 |
业务:
指标 | 说明 |
---|
PV | page view 即页面浏览量和点击量 | UV | 指访某个站点的不同ip地址的人数 | 页面的停留时间 | 用户在每一个页面的停留时间 |
前端监控的流程
前端埋点 --> 数据采集 --> 数据建模和存储 --> 数据传输(实时/批量) --> 数据统计(分析) --> 数据可视化 --> 报告和报警(短信)
常见埋点方案:
方案 | 说明 |
---|
代码埋点 | 代码埋点就是以嵌入式代码的形式进行埋点,比如需要监控用户的点击事件,会选择在用户点击时,插入一段代码,保存这个监听行为或者直接将监听行为以某种数据格式直接传递给服务器端 -优点:可以在任意时刻,精确的发送或保存所需要的数据信息 -缺点: 工作量很大 | 可视化埋点 | 通过可视化交互的手段代替代码埋点,将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码耦合业务代码和埋点代码 | 无痕埋点 | 前端的任意一个事件都被绑定一个标识,所有事件都被记录下来,通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告提供给专业人员分析 -优点:采集全量数据,不会出现漏埋和误埋的现象 -缺点:给数据传输和服务器增加压力,也无法灵活定制数据结构 |
稳定性(报错相关)代码实践:
定义数据 采集和上报的数据结构 :
{
kind: 'xxx',
type: 'xxx',
errorType: 'xxx',
url: '',
message: event.message,
filename: event.filename,
position: `${event.lineno}:${event.colno}`,
stack: getStack(event.error.stack),
selector: selector
}
准备工具函数和前置需要: 为了能拿到报错前最后操作的一个dom元素是谁,我们可以在click,keydown事件的捕获阶段 ,每次把值替换存起来, 因为冒泡阶段一般都会由开发者操控,可能会阻止时间冒泡,而捕获阶段就没有这种担忧
const getLastEvent = (function () {
let lastEvent
['click', 'keydown'].forEach(eventType => {
document.addEventListener(eventType, (e) => {
lastEvent = event
}, {
passive: true,
capture: true
})
})
function getLastEvent() {
return lastEvent
}
return getLastEvent
})()
getStack 函数:用于提取报错的堆栈中的信息
function getStack(stack) {
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g, '')).join("^")
}
getSelector 函数:从getLastEvent() 执行后返回的最后一个交互dom的事件对象中提取出css 选择器
function getSelector(path) {
if (Array.isArray(path)) {
return path.reverse().filter(ele => {
return ele !== document && ele !== window
}).map(ele => {
let selector = ''
if (ele.id) {
return `${ele.nodeName.toLowerCase()}#${ele.id}`
} else if (ele.className && typeof ele.className === 'string') {
return `${ele.nodeName.toLowerCase()}.${ele.className}`
} else {
selector = ele.nodeName.toLowerCase()
}
return selector
}).join(' ')
}
}
解析后会变成:
监听全局未捕获的js错误和全局资源加载异常
注意:对于全局资源加载异常的监听,addEventListener的第三个参数需要指定为true
window.addEventListener("error", (event) => {
let lastEvent = getLastEvent()
let selector = lastEvent && getSelector(lastEvent.path)
if (event.target && (event.target.src || event.target.href)) {
const log = {
kind: 'stability',
type: 'error',
errorType: 'resourceError',
filename: event.target.src || event.target.href,
tagName: event.target.tagName,
selector: selector
}
console.log(log)
return
}
const log = {
kind: 'stability',
type: 'error',
errorType: 'jsError',
url: '',
message: event.message,
filename: event.filename,
position: `${event.lineno}:${event.colno}`,
stack: getStack(event.error.stack),
selector: selector
}
console.log(log)
sendTracker.send(log)
}, true)
监听全局未捕获的promise错误
promise的报错信息比较特殊,需要特殊处理 一下
window.addEventListener("unhandledrejection", (event) => {
let lastEvent = getLastEvent()
let selector = lastEvent && getSelector(lastEvent.path)
let message = ''
let filename = ''
let line = 0
let column = 0
let stack = ''
let reason = event.reason
if (typeof reason === 'string') {
message = reason
} else if (typeof reason === 'object') {
if (reason.stack) {
let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/)
console.log(matchResult)
filename = matchResult[1]
line = matchResult[2]
column = matchResult[3]
}
message = reason.message
stack = getStack(reason.stack)
}
const log = {
kind: 'stability',
type: 'error',
errorType: 'promiseError',
message,
filename,
position: `${line}:${column}`,
stack,
selector: selector
}
console.log(log)
})
监听xhr的请求,上报数据
fetch和ajax监听方式实现一样,都是重写prototype上的方法 ,加入自己的采集逻辑,这里以xhr为例
; (function () {
let XMLHttpRequest = window.XMLHttpRequest
let oldOpen = XMLHttpRequest.prototype.open
XMLHttpRequest.prototype.open = function (method, url, async) {
if (!url.match(/xxxxxx/)) {
this.logData = {
method,
url,
async
}
}
return oldOpen.apply(this, arguments)
}
let oldSend = XMLHttpRequest.prototype.send
XMLHttpRequest.prototype.send = function (body) {
if (this.logData) {
let startTime = Date.now()
const handler = (type) => (event) => {
let duration = Date.now() - startTime
let status = this.status
let statusText = this.statusText
const log = {
kind: 'stability',
type: 'xhr',
errorType: type,
pathname: this.logData.url,
status: status + '-' + statusText,
duration,
response: this.response ? JSON.stringify(this.response) : '',
params: body || ''
}
console.log('xhr', log)
}
this.addEventListener('load', handler('load'), false)
this.addEventListener('error', handler('error'), false)
this.addEventListener('abort', handler('abort'), false)
}
return oldSend.apply(this, arguments)
}
})()
监听白屏上报
怎么判断是否白屏,根据主流的方案,在屏幕的中线上,xy轴分成9份 ,根据坐标通过elementsFromPoint 取18个点,判断是否有元素, 如果出现16个点以上取不到元素 ,则说明页面现在处于白屏状态,上报数据
; (function () {
function getCssSelector(ele) {
if (ele.id) {
return "#" + id
} else if (ele.className) {
return "." + ele.className.split(" ").filter(item => !!item).join(".")
} else {
return ele.nodeName.toLowerCase()
}
}
let wrapper = ['html', 'body']
let emptyPoints = ''
function isWrapper(ele) {
let selector = getCssSelector(ele)
if (wrapper.indexOf(selector) != -1) {
emptyPoints++
}
}
function isBlank() {
for (let i = 1; i <= 9; i++) {
let xElements = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)
let yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)
isWrapper(xElements[0])
isWrapper(yElements[0])
}
if (emptyPoints > 16) {
let centerEle = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2)
const log = {
kind: 'stability',
type: 'blank',
emptyPoints,
screen: window.screen.width + " x " + window.screen.height,
viewPoint: window.innerWidth + " x " + window.innerHeight,
selector: getCssSelector(centerEle[0])
}
console.log(log)
}
}
isBlank()
})()
白屏上报的数据格式如下:
监听js框架的错误:
vue,react 都有提供一定的错误捕获能力,比如vue 中有提供errorHandler
app.config.errorHandler = (err, vm, info) => {
}
特别地: 跨域script error
浏览器只允许同域下的脚本捕获具体的错误信息,别的域名下的报错信息不允许具体打出 如何解决:crossorigin="anonymous"
<script type="text/javascript" src="http://a.com"></script>
<script type="text/javascript" src="http://b.com" crossorigin="anonymous"></script>
比如: a.com是本域名下的,b.com是第三方的域名,引用第三方脚本的时候需要加上 crossorigin="anonymous" 属性,这样的话,该脚本报错,就能接收到具体的堆栈
用户体验(性能相关)性能监控
前端性能监控主要分为两种方式
合成监控: 什么叫合成监控?就是在一个模拟场景里,去提交一个需要做性能审计的页面,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个审计报告。
合成监控中最近比较流行的是 Google 的 Lighthouse,下面我们就以 Lighthouse 为例(在chrome开发者工具中就有提供 )。 根据报告中指出的问题,我们才去推荐的方案解决 特别的: Lighthouse工具还可以nodejs中安装包的方式去测分,我们可以批量的多次的去跑指定的网站,测出一个平均分出来,减少偶然性
真实用户监控: 所谓真实用户监控,就是用户在我们的页面上访问,访问之后就会产生各种各样的性能指标,我们在用户访问结束的时候,把这些性能指标上传到我们的日志服务器上,进行数据的提取清洗加工,最后在我们的监控平台上进行展示的一个过程。
那问题来了,使用什么东西去收集什么数据呢?
使用标准的使用 PerformanceTimeline API (在复杂场景,亦可考虑优先使用PerformanceObserver ),收集在整个页面渲染的过程中,各个时间段相减得出来的业界标准
有一张很经典的图 通过上图中各个时间点相减得出来的差值被业界赋予了不同的意义,具体看下表:
指标名 | 描述 | 计算方式 |
---|
DNS查询 | DNS 阶段耗时 | domainLookupEnd - domainLookupStart | TCP连接 | TCP 阶段耗时 | connectEnd - connectStart | SSL建连 | SSL 连接时间 | connectEnd - secureConnectionStart | 首字节网络请求 | 首字节响应时间(ttfb) | responseStart - requestStart | 内容传输 | 内容传输,Response阶段耗时 | responseEnd - responseStart | DOM解析 | Dom解析时间 | domInteractive - responseEnd | 资源加载 | 资源加载 | loadEventStart - domContentLoadedEventEnd | 首字节 | 首字节 | responseStart - fetchStart | DOM Ready | dom ready | domContentLoadedEventEnd - fetchStart |
代码实践:
; (function () {
setTimeout(() => {
const {
connectEnd,
connectStart,
domContentLoadedEventEnd,
domContentLoadedEventStart,
domInteractiv,
fetchStart,
domLoading,
loadEventStart,
requestStart,
responseStart,
responseEnd
} = performance.timing
const log = {
kind: "experience",
type: 'timing',
connectTime: connectEnd - connectStart,
ttfbTime: responseStart - requestStart,
responseTime: responseEnd - responseEnd,
parseDOMTime: loadEventStart - domLoading,
domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,
timeToInteractive: domInteractiv - fetchStart,
loadTime: loadEventStart - fetchStart,
}
}, 1000);
})()
总结与补充:
异常监控 1.前端有哪些异常? 2.如何捕获异常 3.异常如何上报
前端异常三大类:
js运行错误:
- evalError:已在eva()函数中发生的错误
- internalError:内部错误
- rangeError:超出数字范围的错误
- referenceError:非法引用
- syntaxError:语法错误
- typeError:类型错误
- urlError:在encodeURL中发生的错误
- jsonError:JSON.parse解析发生的错误
…
异步异常:
- setTimeout
- setInterval
- Promise
- requestAnimation
function asyncError() {
try {
setTimeout(() => {
console.log(name.age.a)
}, 1000);
} catch (e) {
console.log(e)
}
}
asyncError()
- 网络加载错误/加载失败(link,script,img,css)
- http请求错误(XMLHttpRequest,fetch)
异常捕获
try,catch window.onerror 全局捕获promise 异常(unhandledrejection) - 重写
addEventListener ,更方便统一的进行异常捕获
const originAddEventListener = EventTarget.prototype.addEventListener
EventTarget.prototype.addEventListener = function (type, listener, options) {
const listen = function (...args) {
try {
return listener.apply(this, args)
} catch (e) {
throw e
}
}
return originAddEventListener.call(this, type, listen, options)
}
window.addEventListener('error', function (e) {
if (e.target.tagName.toUpperCase() === 'IMG') {
console.log('img error')
}
})
劫持fetch,XMLHttpRequest 重写prototype 上的方法
- 网页奔溃捕获(
service worker 拦截监听)
错误上报
- XMLHttpRequest
- navigator.sendBeacon
- IndexDB缓存,异步上报
- 页面截图(html2canvas,dom-to-image)
==特别的:==对于上传的错误频繁且庞大的时候,可以稀释错误上报 ,减轻服务端压力,去重收集 的错误
errorReporter.send = function(data){
if(Math.random>0.5){
send(data)
}
}
开源成熟现成的监控平台: Sentry
是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。其专注于错误监控以及提取一切事后处理所需的信息;支持几乎所有主流开发语言( JS/Java/Python/php )和平台, 并提供了web界面来展示输出错误。Sentry 分为服务端和客户端 SDK,前者可以直接使用它家提供的在线服务,也可以本地自行搭建;后者提供了对多种主流语言和框架的支持,包括 React、Angular、Node、Django、RoR、PHP、Laravel、Android、.NET、JAVA 等。同时它可提供了和其他流行服务集成的方案,例如 GitHub、GitLab、bitbuck、heroku、slack、Trello 等。
Sentry官网: https://sentry.io/
Github项目地址: https://github.com/getsentry/onpremise
|