性能是留住用户的关键
。 大量的研究报告已经表明了性能和商业成绩的关系,糟糕的性能会让您的站点损失用户数、转化率和口碑。错误监控则能够让开发者第一时间发现并修复问题
,单靠用户遇到问题并反馈是不现实的,当用户遇到白屏或者接口错误时,更多的人可能会重试几次、失去耐心然后直接关掉您的网站。
稳定性(报错相关):
错误名称 | 说明 |
---|---|
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', //js执行错误,如:jsError
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(' ')
}
}
注意:对于全局资源加载异常的监听,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', //js或css加载错误
filename: event.target.src || event.target.href, //哪个文件报错了
tagName: event.target.tagName,
selector: selector //最后一个操作的元素
}
console.log(log)
return
}
// console.log('lastEvent', lastEvent)
const log = {
kind: 'stability', //大类
type: 'error', //小类
errorType: 'jsError', //js执行错误
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的报错信息比较特殊,需要特殊处理
一下
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', //promise错误
message,
filename, //哪个文件报错了
position: `${line}:${column}`,
stack, //报错堆栈,能知道报错前调用了谁
selector: selector //最后一个操作的元素
}
console.log(log)
//上报数据
})
fetch和ajax监听方式实现一样,都是重写prototype上的方法
,加入自己的采集逻辑,这里以xhr为例
//监听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, //xhr错误类型
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()
})()
vue,react
都有提供一定的错误捕获能力,比如vue
中有提供errorHandler
app.config.errorHandler = (err, vm, info) => {
// 处理错误
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
浏览器只允许同域下的脚本捕获具体的错误信息,别的域名下的报错信息不允许具体打出
如何解决: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"
属性,这样的话,该脚本报错,就能接收到具体的堆栈
前端性能监控主要分为两种方式
一种叫做合成监控(Synthetic Monitoring,SYN)
另一种是真实用户监控(Real User Monitoring,RUM)
合成监控:
什么叫合成监控?就是在一个模拟场景里,去提交一个需要做性能审计的页面,通过一系列的工具、规则去运行你的页面,提取一些性能指标,得出一个审计报告。
合成监控中最近比较流行的是 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,//2
connectStart,//2
domContentLoadedEventEnd,//4
domContentLoadedEventStart,//4
domInteractiv,//1
fetchStart,//1
domLoading, //5
loadEventStart,//5
requestStart,//3
responseStart,//3
responseEnd//3
} = performance.timing
const log = {
kind: "experience",
type: 'timing',
connectTime: connectEnd - connectStart, //连接时间
ttfbTime: responseStart - requestStart,//首字节到达时间
responseTime: responseEnd - responseEnd, //响应的读取时间
parseDOMTime: loadEventStart - domLoading,//dom解析时间
domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,
timeToInteractive: domInteractiv - fetchStart,//首次可交互时间
loadTime: loadEventStart - fetchStart,//完整的加载时间
}
}, 1000);
})()
异常监控
1.前端有哪些异常?
2.如何捕获异常
3.异常如何上报
前端异常三大类:
js运行错误:
异步异常:
function asyncError() {
try {
setTimeout(() => {
console.log(name.age.a)
}, 1000);
} catch (e) {
console.log(e)
}
}
asyncError()
异常捕获
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
拦截监听)错误上报
特别的: 对于上传的错误频繁且庞大的时候,可以稀释错误上报
,减轻服务端压力,去重收集
的错误
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