前端监控原理

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

数据采集

性能数据采集

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

https://developer.mozilla.org/zh-CN/docs/Web/API/Performance

Performance 接口可以获取到当前页面中与性能相关的信息,它类属于是Hight Resolution Time API,还融合了Performance Timeline API ,navigation Timing API ,User Timing API 和 Resource Timing API。

window.performance
相关属性

timing中各个属性的意思

timimg:{
  //同一个浏览器上页面卸载(unload)结束时的时间戳,如果没有上一个页面,这个值会和fetchStart相同,
  navigationStart:1611191820290,

  //上一个页面unload事件抛出时间的时间戳,如果没有上一个页面那就会返回0
  unloadEventStart:0,

  //与unloadEventStart 相对应,unload事件处理完成时的时间戳,如果没有以上一个页面,值会返回为0
  unloadEventEnd:0,

  //第一个HTTP重定向开始的时间戳,如果没有重定向,或者重定向中的额一个不同源,值会返回为0
  redirectStart:0,

  //最后一个HTTP请求重定向时(也就是说HTTP响应的最后一个比特直接被接受到的时间)的时间戳。
  //如果没有重定向,或者重定向中的一个不同源,会返回一个值为0
  redirectEnd:0,

  //浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳,这个时间点会在检查任何应用缓存之前。
  fetchStart:1611191820292

  //DNS域名查询开始的UNIX时间戳。
  //如果使用了持续连接(persistent contion),或者这个信息存储大了缓存或者本地资源上,值将会和fetchStart一致
  domainLookupStart:1611191820292,

  //DNS域名查询完成的时间
  //如果使用本地缓存(即无 DNS 查询)或持久连接,则与fetchStart值相等
   domainLookupEnd:1611191820292,

  //HTTP(TCP) 域名查询结束的时间戳。
  //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。
  connectStart:1611191820292,

  // HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。
  // 如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结
束。
  connectEnd:1611191820292,

  // HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。
 secureConnectionStart: 0,

 // 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。
 requestStart: 1611191820294,

 // 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。
 //如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。
 responseStart: 1611191820650,

 // 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时
 //(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。
 responseEnd: 1611191820860,

 // 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。
 domLoading: 1611191820656,

 // 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。
 domInteractive: 1611191820861,

 // 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。
 domContentLoadedEventStart: 1611191820878,

 // 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。
 domContentLoadedEventEnd: 1611191820878,

 // 当前文档解析完成,即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳
 domComplete: 1611191820913,

 // load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。
 loadEventStart: 1611191820913,

 // 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.
 loadEventEnd: 1611191820913
}
根据以上数据的分析得到我们需要的时间戳
//重定向
redirect:timing.redirectEnd - timing.redirectStart,

//DOM 渲染耗时
dom:timing.domComplete - timing.domLoading,

//页面加载耗时
load:timing.loadEventEnd - timing.navigationStart,

//页面卸载耗时
unload:timing.unloadEvent - timing.unloadEventStart,

//请求耗时
request:timing.responeEnd - timing.requestStart,

//获取性能信息时当前时间
time:new Date().getTime(),

还有一个白屏时间,指的是从输入网址,到页面开始显示内容的时间。将脚本放入</head>前面就可以获取白屏时间了。

<script>
    whiteScreen = new Date() - performance.timing.navigationStart
    //通过domLing和 navigationStart 也可以
    whiteScreen = performance.timing.domLoading - performance.timing.navigationStart
</script>

这样就可以得知首页加载性能如何了。

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

一般包括以下几种类型:

  • script
  • link
  • img
  • 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,
    //DOM渲染耗时
    dom: timing.domComplete - timing.domLoading,
    //页面加载耗时
    load:timing.loadEventEnd - timing.navigationStart,
    //页面卸载耗时
    unload: timing.unloadEventEnd - timing.unloadEventStart,
    //请求耗时
    request: timing.responesEnd - timing.requestStart,
    // 获取性能信息时当前时间
    time:new Date().getTime(),
  }
  return performance
}
//获取资源信息
  const getResources = () => {
  if(!window.performance) return
  const data = window.performance.getEntriesByType('resource')
  const resource = {
    xmlhttprequset:[],
    css:[],
    other:[],
    script:[],
    img:[],
    link:[],
    fetch:[]
    //获取资源信息当前时间
    time: new Date().getTime(),
  }
  data.forEach(item =>{
    const arry = resource[item.initiatorType]
    arry && arry.push({
        //资源的名称
        name:item.name,
        //资源加载耗时,
        durating: item.duration.toFixed(2),
        //资源大小
        size: item.transferSize,
        //资源所用协议
        protocol: item.nextHopProtocol,
    })
  })
return resource
}
小结

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

  • 资源过大,过多
  • 网速过慢
  • DOM 元素过多

除了用户网速过慢之外,其他原因都有解决办法(性能优化)
页面加载过慢还有其他原因,例如:没有使用按需加载,没有使用CDN等等,

错误数据采集

目前所能捕捉的错误有三种:
1.资源加载错误,通过 addEventListener('error',callback,true)在捕获阶段捕捉资源加载失败错误。
2.js执行错误,通过window.onerror捕捉js错误。
3.promise错误,通过addEventListener('unhandledrejection',callback)捕捉promise错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。
可以建一个错误数组变量errors在错误发生时,将错误的相关信息添加到数组,在阶段统一上报,具体操作请看下面:

addEventListener('error',e =>{
  const target =e.target
  if(target != window){
    monitor.errors.push({
      type:target.locaName,
      url:target.src || target.href,
      msg:(target.src || target.href) + 'is load error',
      //错误发生的时间
      time: new Date().getTime(),
    })    
  }
},true)

//监听js错误
window.onerror = function(msg,url,row,col,error){
  montior.errors.push({
    type:'javascript',
    row:row,
    col:col,
    msg:error && error.stack ? error.stack:msg,
    url:url,
    //错误发生的时间
    time: new Date().getTime(),
  })
}

//监听promise 错误缺点是获取不到行数数据
addEventListener('unhandledrejection',e =>{
  monitor.errors.push({
    type: 'promise',
    msg: (e.reason && e.reason.msg) || e.reason || '',
    //错误发生的时间
    time: new Date().getTime(),
  })
})
小结

通过错误收集,可以了解到网站发生错误的类型及数量,从而做出相应的调整,以减少错误发生

数据上报

性能数据上报

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

window.onload = () =>{
  //在浏览器空闲时间获取性能及资源信息
  //https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
  if(window.requestIdleCallback){
    window.requestIdleCallback(() =>{
      monitor.performance = getPerformance()
      monitor.resources = getResources()
    })
  }else {
    setTimeout(() =>{
      monitor.performance = getPerformance()
      monitor.resources = getResources()  
    },0)
  }
}

可以设置一个定时器,循环上报。不过每次上报最好做下对比去重再上报,避免同样的诗句重复上报。

错误数据上报

用一个errors数组收集所有的错误,再在某一阶段统一上报(延时上报)。
也可以改成错误发生上报(即时上报)。这样可以避免收集完错误,但延时上报还没触发,用户却已经关掉网页导致错误数据丢失的问题。

//监听js错误
window.onerror = function (msg,url,row,col,error){
   const data = {
    type:'javascript',
    row:row,
    msg:error && error.stack ? error.stack : msg,
    url: url,
    //错误发生的时间
    time: new Date().getTime(),
  } 
    //即时上报
    axios.post({
      url:'xxx',
      data
    })
}

也可以使用navigator.sendBeacon()来进行上报。

window.addEventListener('unload',logData,false);
function logData(){
  navigator.sendBeacon("/log",analyticsData);
}

技术特点:

使用sendBeacon()方法会使用用户代理(浏览器)在有机会时一步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载人性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。

扩展

SPA

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

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

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

用户信息收集

navigator

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

UV(Unque visitor)

是指通过互联网浏览这个网页的访客,00:00-24:00内相同的设备访问只被计算一次。一天内同个访客访问仅计算一个UV。
在用户范围网站时,可以生成一个随机字符串+时间日期,保存在本地。在网页发生请求时(如果超过当天24小时,则重新生成),把这些参数传到后端,后端利用这些信息生成UV统计报告。

PV(Page View)

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

页面停留时间
传统网站

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

SPA

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

浏览深度

通过document.documentElement.scrollTop属性,以及屏幕高度,可以判断用户是否浏览完网站内容
页面跳转来源通过document.referre属性,可以知道用户从哪个网站跳转而来

页面跳转来源

通过document.referrer属性,可以获得用户是从哪个网站跳转而来的

小结

通过分析用户数据,我们可以了解到用户的浏览习惯,爱好等等信息,

案例

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <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)
                },
                // 重置 monitor 对象
                reset() {
                    window.performance && window.performance.clearResourceTimings()
                    monitor.performance = getPerformance()
                    monitor.resources = getResources()
                    monitor.errors = []
                },
                // 清空 error 信息
                clearError() {
                    monitor.errors = []
                },
                // 上传监控数据
                upload() {
                    // 自定义上传
                    // axios.post({
                    //     url: monitor.url,
                    //     data: {
                    //         performance,
                    //         resources,
                    //         errors,
                    //         user,
                    //     }
                    // })
                },
                // 设置数据上传地址
                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,
                    // DOM 渲染耗时
                    dom: timing.domComplete - timing.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: [],
                    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 = () => {
                // 在浏览器空闲时间获取性能及资源信息 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
                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)
                }
            }

            // 捕获资源加载失败错误 js css img...
            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)

            // 监听 js 错误
            window.onerror = function(msg, url, row, col, error) {
                monitor.errors.push({
                    type: 'javascript', // 错误类型
                    row: row, // 发生错误时的代码行数
                    col: col, // 发生错误时的代码列数
                    msg: error && error.stack? error.stack : msg, // 错误信息
                    url: url, // 错误文件
                    time: new Date().getTime(), // 错误发生的时间
                })

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

            // 监听 promise 错误 缺点是获取不到行数数据
            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.css">
    <title>Document</title>
</head>
<body>
    <button class="btn1">错误测试按钮1</button>
    <button class="btn2">错误测试按钮2</button>
    <button class="btn3">错误测试按钮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.js"></script>
<script>
document.querySelector('.btn1').onclick = () => {
    setTimeout(() => {
        console.log(button)
    }, 0)
}

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

document.querySelector('.btn3').onclick = () => {
    throw ('这是一个手动扔出的错误')
}
</script>
</body>
</html>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容