前端監(jiān)控原理

前端監(jiān)控分為性能監(jiān)控和錯(cuò)誤監(jiān)控。其中監(jiān)控又分為兩個(gè)環(huán)節(jié):數(shù)據(jù)采集和數(shù)據(jù)上報(bào)。本文主要講的就是如何進(jìn)行數(shù)據(jù)采集和數(shù)據(jù)上報(bào)。

數(shù)據(jù)采集

性能數(shù)據(jù)采集

性能數(shù)據(jù)采集需要使用 window.performance API。

Performance 接口可以獲取到當(dāng)前頁面中與性能相關(guān)的信息,它是 High Resolution Time API 的一部分,同時(shí)也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

從 MDN 的文檔可以看出,window.performance.timing 包含了頁面加載各個(gè)階段的起始及結(jié)束時(shí)間。

001.jpg

為了方便大家理解 timing 各個(gè)屬性的意義,我在知乎找到一位網(wǎng)友對(duì)于 timing 寫的簡介,在此轉(zhuǎn)載一下。

timing: {
        
 navigationStart: 1543806782096,

 
 unloadEventStart: 1543806782523,

 
 unloadEventEnd: 1543806782523,

 
 redirectStart: 0,

 
 
 redirectEnd: 0,

 
 fetchStart: 1543806782096,

 
        
 domainLookupStart: 1543806782096,

 
 
 domainLookupEnd: 1543806782096,

 
        
 connectStart: 1543806782099,

 
        
 connectEnd: 1543806782227,

 
 secureConnectionStart: 1543806782162,

 
 requestStart: 1543806782241,

 
        
 responseStart: 1543806782516,

 
        
 responseEnd: 1543806782537,

 
 domLoading: 1543806782573,

 
 domInteractive: 1543806783203,

 
 domContentLoadedEventStart: 1543806783203,

 
 domContentLoadedEventEnd: 1543806783216,

 
 domComplete: 1543806783796,

 
 loadEventStart: 1543806783796,

 
 loadEventEnd: 1543806783802
}

通過以上數(shù)據(jù),我們可以得到幾個(gè)有用的時(shí)間

redirect: timing.redirectEnd - timing.redirectStart,

dom: timing.domComplete - timing.domLoading,

load: timing.loadEventEnd - timing.navigationStart,

unload: timing.unloadEventEnd - timing.unloadEventStart,

request: timing.responseEnd - timing.requestStart,

time: new Date().getTime(),

還有一個(gè)比較重要的時(shí)間就是白屏?xí)r間,它指從輸入網(wǎng)址,到頁面開始顯示內(nèi)容的時(shí)間。
將以下腳本放在 </head> 前面就能獲取白屏?xí)r間。

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

通過這幾個(gè)時(shí)間,就可以得知頁面首屏加載性能如何了。
另外,通過 window.performance.getEntriesByType('resource') 這個(gè)方法,我們還可以獲取相關(guān)資源(js、css、img...)的加載時(shí)間,它會(huì)返回頁面當(dāng)前所加載的所有資源。

002.jpg

它一般包括以下幾個(gè)類型:

  • sciprt
  • link
  • img
  • css
  • fetch
  • other
  • xmlhttprequest

我們只需用到以下幾個(gè)信息:

name: item.name,

duration: item.duration.toFixed(2),

size: item.transferSize,

protocol: item.nextHopProtocol,

現(xiàn)在,寫幾行代碼來收集這些數(shù)據(jù)。

const getPerformance = () => {
    if (!window.performance) return
    const timing = window.performance.timing
    const performance = {
        
        redirect: timing.redirectEnd - timing.redirectStart,
        
        whiteScreen: whiteScreen,
        
        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
}

通過對(duì)性能及資源信息的解讀,我們可以判斷出頁面加載慢有以下幾個(gè)原因:

  1. 資源過多、過大
  2. 網(wǎng)速過慢
  3. DOM 元素過多

除了用戶網(wǎng)速過慢,我們沒辦法之外,其他兩個(gè)原因都是有辦法解決的,性能優(yōu)化的文章和書籍網(wǎng)上已經(jīng)有很多了,有興趣可自行查找資料了解。
PS:其實(shí)頁面加載慢還有其他原因,例如沒有使用按需加載、沒有使用 CDN 等等。不過這里我們強(qiáng)調(diào)的僅通過對(duì)性能和資源信息的解讀來獲取原因。

錯(cuò)誤數(shù)據(jù)采集

目前所能捕捉的錯(cuò)誤有三種:

  1. 資源加載錯(cuò)誤,通過 addEventListener('error', callback, true) 在捕獲階段捕捉資源加載失敗錯(cuò)誤。
  2. js 執(zhí)行錯(cuò)誤,通過 window.onerror 捕捉 js 錯(cuò)誤。
  3. promise 錯(cuò)誤,通過 addEventListener('unhandledrejection', callback)捕捉 promise 錯(cuò)誤,但是沒有發(fā)生錯(cuò)誤的行數(shù),列數(shù)等信息,只能手動(dòng)拋出相關(guān)錯(cuò)誤信息。

我們可以建一個(gè)錯(cuò)誤數(shù)組變量 errors 在錯(cuò)誤發(fā)生時(shí),將錯(cuò)誤的相關(guān)信息添加到數(shù)組,然后在某個(gè)階段統(tǒng)一上報(bào),具體如何操作請(qǐng)看下面的代碼:

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: '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(),
    })
})

通過錯(cuò)誤收集,可以了解到網(wǎng)站發(fā)生錯(cuò)誤的類型及數(shù)量,從而做出相應(yīng)的調(diào)整,以減少錯(cuò)誤發(fā)生。完整代碼和 DEMO 會(huì)在文章末尾放出,大家可以復(fù)制代碼(HTML 文件)在本地測試一下。

數(shù)據(jù)上報(bào)

性能數(shù)據(jù)上報(bào)

性能數(shù)據(jù)可以在頁面加載完之后上報(bào),盡量不要對(duì)頁面性能造成影響。

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

當(dāng)然,你也可以設(shè)一個(gè)定時(shí)器,循環(huán)上報(bào)。不過每次上報(bào)最好做一下對(duì)比去重再上報(bào),避免同樣的數(shù)據(jù)重復(fù)上報(bào)。

錯(cuò)誤數(shù)據(jù)上報(bào)

我在 DEMO 里提供的代碼,是用一個(gè) errors 數(shù)組收集所有的錯(cuò)誤,再在某一階段統(tǒng)一上報(bào)(延時(shí)上報(bào))。
其實(shí),也可以改成在錯(cuò)誤發(fā)生時(shí)上報(bào)(即時(shí)上報(bào))。這樣可以避免 “收集完錯(cuò)誤,但延時(shí)上報(bào)還沒觸發(fā),用戶卻已經(jīng)關(guān)掉網(wǎng)頁導(dǎo)致錯(cuò)誤數(shù)據(jù)丟失” 的問題。

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

擴(kuò)展

SPA

window.performance API 是有缺點(diǎn)的,在 SPA 切換路由時(shí),window.performance.timing 的數(shù)據(jù)不會(huì)更新。所以我們需要另想辦法來統(tǒng)計(jì)切換路由到加載完成的時(shí)間。拿 Vue 舉例,一個(gè)可行的辦法就是切換路由時(shí),在路由的全局前置守衛(wèi) beforeEach 里獲取開始時(shí)間,在組件的 mounted 鉤子里執(zhí)行 vm.$nextTick 函數(shù)來獲取組件的渲染完畢時(shí)間。

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

除了性能和錯(cuò)誤監(jiān)控,其實(shí)我們還可以收集更多的信息。

用戶信息收集

navigator

使用 window.navigator 可以收集到用戶的設(shè)備信息,操作系統(tǒng),瀏覽器信息...

UV(Unique visitor)

是指通過互聯(lián)網(wǎng)瀏覽這個(gè)網(wǎng)頁的訪客,00:00-24:00 內(nèi)相同的設(shè)備訪問只被計(jì)算一次。一天內(nèi)同個(gè)訪客多次訪問僅計(jì)算一個(gè) UV。
在用戶訪問網(wǎng)站時(shí),可以生成一個(gè)隨機(jī)字符串 + 時(shí)間日期,保存在本地。在網(wǎng)頁發(fā)生請(qǐng)求時(shí)(如果超過當(dāng)天 24 小時(shí),則重新生成),把這些參數(shù)傳到后端,后端利用這些信息生成 UV 統(tǒng)計(jì)報(bào)告。

PV(Page View)

即頁面瀏覽量或點(diǎn)擊量,用戶每 1 次對(duì)網(wǎng)站中的每個(gè)網(wǎng)頁訪問均被記錄 1 個(gè) PV。用戶對(duì)同一頁面的多次訪問,訪問量累計(jì),用以衡量網(wǎng)站用戶訪問的網(wǎng)頁數(shù)量。

頁面停留時(shí)間

傳統(tǒng)網(wǎng)站

用戶在進(jìn)入 A 頁面時(shí),通過后臺(tái)請(qǐng)求把用戶進(jìn)入頁面的時(shí)間捎上。過了 10 分鐘,用戶進(jìn)入 B 頁面,這時(shí)后臺(tái)可以通過接口捎帶的參數(shù)可以判斷出用戶在 A 頁面停留了 10 分鐘。

SPA

可以利用 router 來獲取用戶停留時(shí)間,拿 Vue 舉例,通過 router.beforeEach、destroyed 這兩個(gè)鉤子函數(shù)來獲取用戶停留該路由組件的時(shí)間。

瀏覽深度

通過 document.documentElement.scrollTop 屬性以及屏幕高度,可以判斷用戶是否瀏覽完網(wǎng)站內(nèi)容。

頁面跳轉(zhuǎn)來源

通過 document.referrer 屬性,可以知道用戶是從哪個(gè)網(wǎng)站跳轉(zhuǎn)而來。

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,
                    
                    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 = () => {
                
                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('所有的錯(cuò)誤信息')
                    console.log(monitor.errors)
                }
            }, true)

            
            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('所有的錯(cuò)誤信息')
                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('所有的錯(cuò)誤信息')
                console.log(monitor.errors)
            })

            return monitor
        }

        const monitor = monitorInit()
    </script>
    <link rel="stylesheet" href="test.css">
    <title>Document</title>
</head>
<body>
    <button>錯(cuò)誤測試按鈕1</button>
    <button>錯(cuò)誤測試按鈕2</button>
    <button>錯(cuò)誤測試按鈕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 ('這是一個(gè)手動(dòng)扔出的錯(cuò)誤')
}
</script>
</body>
</html>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容