這幾天心血來潮,想了解一下前端監控的相關知識,但是在查看了不少資料以後,發現沒有詳細介紹前端監控的相關文章,都是講個大概,反卻是現成的前端監控工具備很多。javascript
爲了深刻學習前端監控的相關技術原理,這幾天都在查閱相關的資料。如今打算寫一篇文章詳細介紹一下前端監控,對這幾天的研究作一個總結(因而就有了本文)。css
// 前端監控流程
數據採集 --> 數據上報 --> 服務端處理 --> 數據庫存儲 --> 數據監控可視化平臺
複製代碼
不過,本文只講監控中的數據採集和數據上報兩個步驟,後續流程需讀者自行研究探索(這也是一種樂趣)。html
先來了解一下 Web API window.performance 。前端
Performance 接口能夠獲取到當前頁面中與性能相關的信息,它是 High Resolution Time API 的一部分,同時也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。java
這個 API 的屬性 timing
,包含了頁面加載各個階段的起始及結束時間。 爲了方便你們理解 timing
各個屬性的意義,我在知乎找到一位網友對於 timing
寫的簡介,在此轉載一下。ios
timing: {
// 同一個瀏覽器上一個頁面卸載(unload)結束時的時間戳。若是沒有上一個頁面,這個值會和fetchStart相同。
navigationStart: 1543806782096,
// 上一個頁面unload事件拋出時的時間戳。若是沒有上一個頁面,這個值會返回0。
unloadEventStart: 1543806782523,
// 和 unloadEventStart 相對應,unload事件處理完成時的時間戳。若是沒有上一個頁面,這個值會返回0。
unloadEventEnd: 1543806782523,
// 第一個HTTP重定向開始時的時間戳。若是沒有重定向,或者重定向中的一個不一樣源,這個值會返回0。
redirectStart: 0,
// 最後一個HTTP重定向完成時(也就是說是HTTP響應的最後一個比特直接被收到的時間)的時間戳。
// 若是沒有重定向,或者重定向中的一個不一樣源,這個值會返回0.
redirectEnd: 0,
// 瀏覽器準備好使用HTTP請求來獲取(fetch)文檔的時間戳。這個時間點會在檢查任何應用緩存以前。
fetchStart: 1543806782096,
// DNS 域名查詢開始的UNIX時間戳。
//若是使用了持續鏈接(persistent connection),或者這個信息存儲到了緩存或者本地資源上,這個值將和fetchStart一致。
domainLookupStart: 1543806782096,
// DNS 域名查詢完成的時間.
//若是使用了本地緩存(即無 DNS 查詢)或持久鏈接,則與 fetchStart 值相等
domainLookupEnd: 1543806782096,
// HTTP(TCP) 域名查詢結束的時間戳。
//若是使用了持續鏈接(persistent connection),或者這個信息存儲到了緩存或者本地資源上,這個值將和 fetchStart一致。
connectStart: 1543806782099,
// HTTP(TCP) 返回瀏覽器與服務器之間的鏈接創建時的時間戳。
// 若是創建的是持久鏈接,則返回值等同於fetchStart屬性的值。鏈接創建指的是全部握手和認證過程所有結束。
connectEnd: 1543806782227,
// HTTPS 返回瀏覽器與服務器開始安全連接的握手時的時間戳。若是當前網頁不要求安全鏈接,則返回0。
secureConnectionStart: 1543806782162,
// 返回瀏覽器向服務器發出HTTP請求時(或開始讀取本地緩存時)的時間戳。
requestStart: 1543806782241,
// 返回瀏覽器從服務器收到(或從本地緩存讀取)第一個字節時的時間戳。
//若是傳輸層在開始請求以後失敗而且鏈接被重開,該屬性將會被數製成新的請求的相對應的發起時間。
responseStart: 1543806782516,
// 返回瀏覽器從服務器收到(或從本地緩存讀取,或從本地資源讀取)最後一個字節時
//(若是在此以前HTTP鏈接已經關閉,則返回關閉時)的時間戳。
responseEnd: 1543806782537,
// 當前網頁DOM結構開始解析時(即Document.readyState屬性變爲「loading」、相應的 readystatechange事件觸發時)的時間戳。
domLoading: 1543806782573,
// 當前網頁DOM結構結束解析、開始加載內嵌資源時(即Document.readyState屬性變爲「interactive」、相應的readystatechange事件觸發時)的時間戳。
domInteractive: 1543806783203,
// 當解析器發送DOMContentLoaded 事件,即全部須要被執行的腳本已經被解析時的時間戳。
domContentLoadedEventStart: 1543806783203,
// 當全部須要當即執行的腳本已經被執行(不論執行順序)時的時間戳。
domContentLoadedEventEnd: 1543806783216,
// 當前文檔解析完成,即Document.readyState 變爲 'complete'且相對應的readystatechange 被觸發時的時間戳
domComplete: 1543806783796,
// load事件被髮送時的時間戳。若是這個事件還未被髮送,它的值將會是0。
loadEventStart: 1543806783796,
// 當load事件結束,即加載事件完成時的時間戳。若是這個事件還未被髮送,或者還沒有完成,它的值將會是0.
loadEventEnd: 1543806783802
}
複製代碼
經過以上數據,咱們能夠獲得幾個有用的時間git
// 重定向耗時
redirect: timing.redirectEnd - timing.redirectStart,
// 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(),
複製代碼
還有一個比較重要的時間就是白屏時間,它指從輸入網址,到頁面開始顯示內容的時間。github
將如下腳本放在 </head>
前面就能獲取白屏時間。數據庫
<script> whiteScreen = new Date() - performance.timing.navigationStart </script>
複製代碼
經過這幾個時間,就能夠得知頁面首屏加載性能如何了。axios
另外,經過 window.performance.getEntriesByType('resource')
這個方法,咱們還能夠獲取相關資源(js、css、img...)的加載時間,它會返回頁面當前所加載的全部資源。 它通常包括如下幾個類型
咱們只需用到如下幾個信息
// 資源的名稱
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.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
}
複製代碼
經過對性能及資源信息的解讀,咱們能夠判斷出頁面加載慢有如下幾個緣由:
除了用戶網速過慢,咱們沒辦法以外,其餘兩個緣由都是有辦法解決的,性能優化的文章和書籍網上已經有不少了,有興趣可自行查找資料瞭解。
經過這幾天的資料查找,瞭解到如今能捕捉的錯誤有三種。
1 經過 addEventListener('error', callback, true)
在捕獲階段捕捉資源加載失敗錯誤。 2 經過 window.onerror
捕捉 js 錯誤。 3 經過 addEventListener('unhandledrejection', callback)
捕捉 promise 錯誤,可是沒有發生錯誤的行數,列數等信息,只能手動拋出相關錯誤信息。
咱們能夠建一個錯誤數組變量 errors
在錯誤發生時,將錯誤的相關信息添加到數組,而後在某個階段統一上報,具體如何操做請看代碼
// 捕獲資源加載失敗錯誤 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(),
})
}
}, 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(),
})
}
// 監聽 promise 錯誤 缺點是獲取不到行數數據
addEventListener('unhandledrejection', e => {
monitor.errors.push({
type: 'promise',
msg: (e.reason && e.reason.msg) || e.reason || '',
// 錯誤發生的時間
time: new Date().getTime(),
})
})
複製代碼
經過錯誤收集,能夠了解到網站錯誤發生的類型及數量,從而能夠作相應的調整,以減小錯誤發生。 完整代碼和DEMO會在文章末尾放出,你們能夠複製代碼(HTML文件)在本地測試一下。
性能數據能夠在頁面加載完以後上報,儘可能不要對頁面性能形成影響。
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)
}
}
複製代碼
固然,你也能夠設一個定時器,循環上報。不過每次上報最好作一下對比去重再上報,避免一樣的數據重複上報。
我在DEMO裏提供的代碼,是用一個 errors
數組收集全部的錯誤,再在某一階段統一上報(延時上報)。 其實,也能夠改爲在錯誤發生時上報(即時上報)。這樣能夠避免在收集完錯誤延時上報還沒觸發,用戶卻已經關掉網頁致使錯誤數據丟失的問題。
// 監聽 js 錯誤
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, })
}
複製代碼
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)
})
}
複製代碼
除了性能和錯誤監控,其實咱們還能夠作得更多。
使用 window.navigator
能夠收集到用戶的設備信息,操做系統,瀏覽器信息...
是指經過互聯網訪問、瀏覽這個網頁的天然人。訪問您網站的一臺電腦客戶端爲一個訪客。00:00-24:00內相同的客戶端只被計算一次。一天內同個訪客屢次訪問僅計算一個UV。 在用戶訪問網站時,能夠生成一個隨機字符串+時間日期,保存在本地。在網頁發生請求時(若是超過當天24小時,則從新生成),把這些參數傳到後端,後端利用這些信息生成 UV 統計報告。
即頁面瀏覽量或點擊量,用戶每1次對網站中的每一個網頁訪問均被記錄1個PV。用戶對同一頁面的屢次訪問,訪問量累計,用以衡量網站用戶訪問的網頁數量。
傳統網站 用戶在進入 A 頁面時,經過後臺請求把用戶進入頁面的時間捎上。過了 10 分鐘,用戶進入 B 頁面,這時後臺能夠經過接口捎帶的參數能夠判斷出用戶在 A 頁面停留了 10 分鐘。 SPA 能夠利用 router 來獲取用戶停留時間,拿 Vue 舉例,經過 router.beforeEach
destroyed
這兩個鉤子函數來獲取用戶停留該路由組件的時間。
經過 document.documentElement.scrollTop
屬性以及屏幕高度,能夠判斷用戶是否瀏覽完網站內容。
經過 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>
複製代碼