IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 前端监控原理及实践 -> 正文阅读

[JavaScript知识库]前端监控原理及实践


?? ? ? ? 随着产品的用户数量的不断增长,对于站点体验衡量的的需求也日益紧迫,用户会将产品和他们每天使用的体验最好的 Web 站点进行比较。想着手优化,则必须先有相关的监控数据,才能对症下药。

性能是留住用户的关键。 大量的研究报告已经表明了性能和商业成绩的关系,糟糕的性能会让您的站点损失用户数、转化率和口碑。错误监控则能够让开发者第一时间发现并修复问题单靠用户遇到问题并反馈是不现实的,当用户遇到白屏或者接口错误时,更多的人可能会重试几次、失去耐心然后直接关掉您的网站。

为什么要做前端监控

  1. 更快的发现问题和解决问题
  2. 做产品的决策依据
  3. 为业务扩展提供更多的可能性
  4. 根据指标优化产品

监控哪些东西?

稳定性(报错相关):

错误名称说明
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的长任务

业务:

指标说明
PVpage 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(' ')
        }
    }

在这里插入图片描述
解析后会变成:
在这里插入图片描述

监听全局未捕获的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', //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错误

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)
        //上报数据 
    })

监听xhr的请求,上报数据

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()
        })()

白屏上报的数据格式如下:
在这里插入图片描述

监听js框架的错误:

vue,react都有提供一定的错误捕获能力,比如vue中有提供errorHandler

app.config.errorHandler = (err, vm, info) => {
  // 处理错误
  // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
}

特别地: 跨域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" 属性,这样的话,该脚本报错,就能接收到具体的堆栈


用户体验(性能相关)性能监控

前端性能监控主要分为两种方式

  • 一种叫做合成监控(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 Readydom readydomContentLoadedEventEnd - 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运行错误
  • 网络加载错误
  • http请求错误

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')
        }
    })
  • http请求错误

劫持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

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-07 13:45:45  更:2021-10-07 13:45:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年4日历 -2024/4/20 5:01:01-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码