前端监控原理,深入浅出,从上手到实战

news/2024/7/19 12:58:14 标签: js, javascript, css, dom, html
htmledit_views">
html" title=js>js_content">

前端监控分为性能监控和错误监控。其中监控又分为两个环节:数据采集和数据上报。本文主要讲的就是如何进行数据采集和数据上报。

数据采集

性能数据采集

性能数据采集需要使用 window.performance API。

Performance 接口可以获取到当前页面中与性能相关的信息,它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

从 MDN 的文档可以看出,window.performance.timing 包含了页面加载各个阶段的起始及结束时间。

这些属性需要结合下图一起看,更好理解:

为了方便大家理解 timing 各个属性的意义,我在知乎找到一位网友对于 timing 写的简介,在此转载一下。

timing: {
        
 navigationStart: 1543806782096, 
 unloadEventStart: 1543806782523, 
 unloadEventEnd: 1543806782523, 
 redirectStart: 0,
 redirectEnd: 0, 
 fetchStart: 1543806782096,        
 html" title=dom>domainLookupStart: 1543806782096, 
 html" title=dom>domainLookupEnd: 1543806782096,         
 connectStart: 1543806782099,        
 connectEnd: 1543806782227,
 secureConnectionStart: 1543806782162,
 requestStart: 1543806782241,        
 responseStart: 1543806782516,        
 responseEnd: 1543806782537, 
 html" title=dom>domLoading: 1543806782573, 
 html" title=dom>domInteractive: 1543806783203, 
 html" title=dom>domContentLoadedEventStart: 1543806783203, 
 html" title=dom>domContentLoadedEventEnd: 1543806783216, 
 html" title=dom>domComplete: 1543806783796, 
 loadEventStart: 1543806783796, 
 loadEventEnd: 1543806783802
}

通过以上数据,我们可以得到几个有用的时间

redirect: timing.redirectEnd - timing.redirectStart,

html" title=dom>dom: timing.html" title=dom>domComplete - timing.html" title=dom>domLoading,

load: timing.loadEventEnd - timing.navigationStart,

unload: timing.unloadEventEnd - timing.unloadEventStart,

request: timing.responseEnd - timing.requestStart,

time: new Date().getTime(),

还有一个比较重要的时间就是白屏时间,它指从输入网址,到页面开始显示内容的时间。

将以下脚本放在 </head> 前面就能获取白屏时间。

<script>
    whiteScreen = new Date() - performance.timing.navigationStart
    
    whiteScreen = performance.timing.html" title=dom>domLoading - performance.timing.navigationStart
</script>

通过这几个时间,就可以得知页面首屏加载性能如何了。

另外,通过 window.performance.getEntriesByType('resource') 这个方法,我们还可以获取相关资源(html" title=js>js、html" title=css>css、img...)的加载时间,它会返回页面当前所加载的所有资源。

它一般包括以下几个类型:

  • sciprt

  • link

  • img

  • html" title=css>css

  • fetch

  • other

  • xmlhttprequest

我们只需用到以下几个信息:

name: item.name,

duration: item.duration.toFixed(2),

size: item.transferSize,

protocol: item.nextHopProtocol,

现在,写几行代码来收集这些数据。

const getPerformance = () => {
    if (!window.performance) return
    const timing = window.performance.timing
    const performance = {
        
        redirect: timing.redirectEnd - timing.redirectStart,
        
        whiteScreen: whiteScreen,
        
        html" title=dom>dom: timing.html" title=dom>domComplete - timing.html" title=dom>domLoading,
        
        load: timing.loadEventEnd - timing.navigationStart,
        
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        
        request: timing.responseEnd - timing.requestStart,
        
        time: new Date().getTime(),
    }

    return performance
}


const getResources = () => {
    if (!window.performance) return
    const data = window.performance.getEntriesByType('resource')
    const resource = {
        xmlhttprequest: [],
        html" title=css>css: [],
        other: [],
        script: [],
        img: [],
        link: [],
        fetch: [],
        
        time: new Date().getTime(),
    }

    data.forEach(item => {
        const arry = resource[item.initiatorType]
        arry && arry.push({            
            name: item.name,            
            duration: item.duration.toFixed(2),            
            size: item.transferSize,            
            protocol: item.nextHopProtocol,
        })
    })

    return resource
}

小结

通过对性能及资源信息的解读,我们可以判断出页面加载慢有以下几个原因:

  1. 资源过多、过大

  2. 网速过慢

  3. DOM 元素过多

除了用户网速过慢,我们没办法之外,其他两个原因都是有办法解决的,性能优化的文章和书籍网上已经有很多了,有兴趣可自行查找资料了解。

PS:其实页面加载慢还有其他原因,例如没有使用按需加载、没有使用 CDN 等等。不过这里我们强调的仅通过对性能和资源信息的解读来获取原因。

错误数据采集

目前所能捕捉的错误有三种:

  1. 资源加载错误,通过 addEventListener('error', callback, true) 在捕获阶段捕捉资源加载失败错误。

  2. html" title=js>js 执行错误,通过 window.onerror 捕捉 html" title=js>js 错误。

  3. promise 错误,通过 addEventListener('unhandledrejection', callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。

我们可以建一个错误数组变量 errors 在错误发生时,将错误的相关信息添加到数组,然后在某个阶段统一上报,具体如何操作请看下面的代码:

addEventListener('error', e => {
    const target = e.target
    if (target != window) {
        monitor.errors.push({
            type: target.localName,
            url: target.src || target.href,
            msg: (target.src || target.href) + ' is load error',
            
            time: new Date().getTime(),
        })
    }
}, true)


window.onerror = function(msg, url, row, col, error) {
    monitor.errors.push({
        type: 'html" title=javascript>javascript',
        row: row,
        col: col,
        msg: error && error.stack? error.stack : msg,
        url: url,
        
        time: new Date().getTime(),
    })
}


addEventListener('unhandledrejection', e => {
    monitor.errors.push({
        type: 'promise',
        msg: (e.reason && e.reason.msg) || e.reason || '',
        
        time: new Date().getTime(),
    })
})

小结

通过错误收集,可以了解到网站发生错误的类型及数量,从而做出相应的调整,以减少错误发生。完整代码和 DEMO 会在文章末尾放出,大家可以复制代码(HTML 文件)在本地测试一下。

数据上报

性能数据上报

性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。

window.onload = () => {
    
    
    if (window.requestIdleCallback) {
        window.requestIdleCallback(() => {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        })
    } else {
        setTimeout(() => {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        }, 0)
    }
}

当然,你也可以设一个定时器,循环上报。不过每次上报最好做一下对比去重再上报,避免同样的数据重复上报。

错误数据上报

我在 DEMO 里提供的代码,是用一个 errors 数组收集所有的错误,再在某一阶段统一上报(延时上报)。

其实,也可以改成在错误发生时上报(即时上报)。这样可以避免 “收集完错误,但延时上报还没触发,用户却已经关掉网页导致错误数据丢失” 的问题。

window.onerror = function(msg, url, row, col, error) {
    const data = {
        type: 'html" title=javascript>javascript',
        row: row,
        col: col,
        msg: error && error.stack? error.stack : msg,
        url: url,
        
        time: new Date().getTime(),
    }
    
    
    axios.post({ url: 'xxx', data, })
}

扩展

SPA

window.performance API 是有缺点的,在 SPA 切换路由时,window.performance.timing 的数据不会更新。所以我们需要另想办法来统计切换路由到加载完成的时间。拿 Vue 举例,一个可行的办法就是切换路由时,在路由的全局前置守卫 beforeEach 里获取开始时间,在组件的 mounted 钩子里执行 vm.$nextTick 函数来获取组件的渲染完毕时间。

router.beforeEach((to, from, next) => {
 store.commit('setPageLoadedStartTime', new Date())
})
mounted() {
 this.$nextTick(() => {
  this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
 })
}

除了性能和错误监控,其实我们还可以收集更多的信息。

用户信息收集

navigator

使用 window.navigator 可以收集到用户的设备信息,操作系统,浏览器信息...

UV(Unique visitor)

是指通过互联网浏览这个网页的访客,00:00-24:00 内相同的设备访问只被计算一次。一天内同个访客多次访问仅计算一个 UV。

在用户访问网站时,可以生成一个随机字符串 + 时间日期,保存在本地。在网页发生请求时(如果超过当天 24 小时,则重新生成),把这些参数传到后端,后端利用这些信息生成 UV 统计报告。

PV(Page View)

即页面浏览量或点击量,用户每 1 次对网站中的每个网页访问均被记录 1 个 PV。用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量。

页面停留时间

传统网站

用户在进入 A 页面时,通过后台请求把用户进入页面的时间捎上。过了 10 分钟,用户进入 B 页面,这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。

SPA

可以利用 router 来获取用户停留时间,拿 Vue 举例,通过 router.beforeEachdestroyed 这两个钩子函数来获取用户停留该路由组件的时间。

浏览深度

通过 document.documentElement.scrollTop 属性以及屏幕高度,可以判断用户是否浏览完网站内容。

页面跳转来源

通过 document.referrer 属性,可以知道用户是从哪个网站跳转而来。

小结

通过分析用户数据,我们可以了解到用户的浏览习惯、爱好等等信息,想想真是恐怖,毫无隐私可言。

DEMO

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta >
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script>
        function monitorInit() {
            const monitor = {
                
                url: '',
                
                performance: {},
                
                resources: {},
                
                errors: [],
                
                user: {
                    
                    screen: screen.width,
                    
                    height: screen.height,
                    
                    platform: navigator.platform,
                    
                    userAgent: navigator.userAgent,
                    
                    language: navigator.language,
                },
                
                addError(error) {
                    const obj = {}
                    const { type, msg, url, row, col } = error
                    if (type) obj.type = type
                    if (msg) obj.msg = msg
                    if (url) obj.url = url
                    if (row) obj.row = row
                    if (col) obj.col = col
                    obj.time = new Date().getTime()
                    monitor.errors.push(obj)
                },
                
                reset() {
                    window.performance && window.performance.clearResourceTimings()
                    monitor.performance = getPerformance()
                    monitor.resources = getResources()
                    monitor.errors = []
                },
                
                clearError() {
                    monitor.errors = []
                },
                
                upload() {
                    
                },
                
                setURL(url) {
                    monitor.url = url
                },
            }

            
            const getPerformance = () => {
                if (!window.performance) return
                const timing = window.performance.timing
                const performance = {
                    
                    redirect: timing.redirectEnd - timing.redirectStart,
                    
                    whiteScreen: whiteScreen,
                    
                    html" title=dom>dom: timing.html" title=dom>domComplete - timing.html" title=dom>domLoading,
                    
                    load: timing.loadEventEnd - timing.navigationStart,
                    
                    unload: timing.unloadEventEnd - timing.unloadEventStart,
                    
                    request: timing.responseEnd - timing.requestStart,
                    
                    time: new Date().getTime(),
                }

                return performance
            }

            
            const getResources = () => {
                if (!window.performance) return
                const data = window.performance.getEntriesByType('resource')
                const resource = {
                    xmlhttprequest: [],
                    html" title=css>css: [],
                    other: [],
                    script: [],
                    img: [],
                    link: [],
                    fetch: [],
                    
                    time: new Date().getTime(),
                }

                data.forEach(item => {
                    const arry = resource[item.initiatorType]
                    arry && arry.push({
                        
                        name: item.name,
                        
                        duration: item.duration.toFixed(2),
                        
                        size: item.transferSize,
                        
                        protocol: item.nextHopProtocol,
                    })
                })

                return resource
            }

            window.onload = () => {
                
                if (window.requestIdleCallback) {
                    window.requestIdleCallback(() => {
                        monitor.performance = getPerformance()
                        monitor.resources = getResources()
                        console.log('页面性能信息')
                        console.log(monitor.performance)
                        console.log('页面资源信息')
                        console.log(monitor.resources)
                    })
                } else {
                    setTimeout(() => {
                        monitor.performance = getPerformance()
                        monitor.resources = getResources()
                        console.log('页面性能信息')
                        console.log(monitor.performance)
                        console.log('页面资源信息')
                        console.log(monitor.resources)
                    }, 0)
                }
            }

            
            addEventListener('error', e => {
                const target = e.target
                if (target != window) {
                    monitor.errors.push({
                        type: target.localName,
                        url: target.src || target.href,
                        msg: (target.src || target.href) + ' is load error',
                        
                        time: new Date().getTime(),
                    })

                    console.log('所有的错误信息')
                    console.log(monitor.errors)
                }
            }, true)

            
            window.onerror = function(msg, url, row, col, error) {
                monitor.errors.push({
                    type: 'html" title=javascript>javascript', 
                    row: row, 
                    col: col, 
                    msg: error && error.stack? error.stack : msg, 
                    url: url, 
                    time: new Date().getTime(), 
                })

                console.log('所有的错误信息')
                console.log(monitor.errors)
            }

            
            addEventListener('unhandledrejection', e => {
                monitor.errors.push({
                    type: 'promise',
                    msg: (e.reason && e.reason.msg) || e.reason || '',
                    
                    time: new Date().getTime(),
                })

                console.log('所有的错误信息')
                console.log(monitor.errors)
            })

            return monitor
        }

        const monitor = monitorInit()
    </script>
    <link rel="stylesheet" href="test.html" title=css>css">
    <title>Document</title>
</head>
<body>
    <button>错误测试按钮1</button>
    <button>错误测试按钮2</button>
    <button>错误测试按钮3</button>
    <img src="https://avatars3.githubusercontent.com/u/22117876?s=460&v=4" alt="">
    <img src="test.png" alt="">
<script src="192.168.10.15/test.html" title=js>js"></script>
<script>
document.querySelector('.btn1').onclick = () => {
    setTimeout(() => {
        console.log(button)
    }, 0)
}

document.querySelector('.btn2').onclick = () => {
    new Promise((resolve, reject) => {
        reject({
            msg: 'test.html" title=js>js promise is error'
        })
    })
}

document.querySelector('.btn3').onclick = () => {
    throw ('这是一个手动扔出的错误')
}
</script>
</body>
</html>

参考资料

  • 7 天打造前端性能监控系统

  • zanePerfor

最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

点个在看支持我吧,转发就更好了


http://www.niftyadmin.cn/n/717505.html

相关文章

转载关于视觉SCI期刊

ChanLee_1整理的计算机视觉领域稍微容易中的期刊 模式识别&#xff0c;计算机视觉领域&#xff0c;期刊 (1)pattern recognition letters, 从投稿到发表&#xff0c;一年半时间 (2)Pattern recognition 不好中&#xff0c;时间长 (3)IEICE Transactions on Information and Sys…

iOS:网络编程解析协议一:HTTP超文本传输协议

HTTP传输数据有四种方式&#xff1a;Get方式、Post方式、同步请求方式、异步请求方式。具体的介绍&#xff0c;前面已经有过系统的讲解&#xff0c;这次主要进行具体的举。 说明&#xff1a;同步和异步请求方式在创建链接对象和创建请求对象时&#xff0c;用Get方式或Post方式中…

ruby中判断数字类型+一道算法题

data.is_a?(Integer) 整数的判断data.is_a?(Float)浮点数的判断 data.is_a?(Numeric)是否数字的判断data.class Fixnum数字类型题目&#xff1a;一个整数&#xff0c;它加上100后是一个完全平方数&#xff0c;再加上168又是一个完全平方数&#xff0c;请问该数是多少&#…

微信小程序工程化之持续集成方案(学到了!)

本文作者&#xff1a;韩永刚&#xff0c;360奇舞团 WEB前端开发高级工程师。本文将简单介绍一下持续集成的概念&#xff0c;并手把手带你在你的微信小程序项目中完成属于你的持续集成方案。什么是前端工程化所有能降低成本&#xff0c;并且能提高效率的事情总称为工程化。在前端…

centos Percona-XtraDB-Cluster-56 安装

Percona-XtraDB-Cluster环境【单个物理server上部署的3个节点】一、相关软件安装#安装前确认源安装mysql已经卸载#rpm -qa |grep mysql*#rpm -e --nodeps mysql-server-5.1.73-5.el6_6 mysql-libs-5.1.73-5.el6_6 mysql-5.1.73-5.el6_6 mysql-devel-5.1.73-5.el6_6yum -y insta…

一个多层HMM模型

来源&#xff1a;http://en.wikipedia.org/wiki/Layered_hidden_Markov_model

ISA中站点到站点的×××连接-----PPTP和L2TP

ISA中站点到站点的连接-----PPTP和L2TP在ISA中站点到站点的中有两种形式&#xff0c;一种基于pptp,一种基于l2tp.下面我们来部署第一种基于pptp的连接。首先&#xff0c;我们在ISA服务器上定义地址分配。输入地址的范围。接着&#xff0c;点击客户端属性-------常规----选择启用…

java一切皆对象的理解,以及new关键字的理解

2019独角兽企业重金招聘Python工程师标准>>> java将一切视为对象&#xff0c;在实际操纵中实际上对象是一个引用&#xff0c;可以将这一情形想象成遥控器&#xff08;引用&#xff09;来操纵电视机&#xff08;对象&#xff09;&#xff0c;只要握住这个遥控器&…