前端監控的范圍很廣,如監控性能,監控異常,監控告警等一系列的維度來確保我們的頁面和功能是正常的,在出現問題時研發可以及時做出響應,及時追蹤,定位問題。
圖片
總體來說性能監控是做的比較大的,而且監控對提高頁面的秒開是有實際優化指導意義的,離線包、預請求等都是優化手段。
圖片
這些錯誤類的信息不一定是我們都需要關注的,有些疑難雜癥,但是有不影響頁面展示和功能的報錯,也是可以忽略的,要知道不是所有的錯誤都能被解決的,這個時候我們可以只關注那些影響我們頁面核心功能的部分,針對這部分做一個告警配置,例如:
圖片
針對告警信息,再進一步對錯誤進行分析,找到能解決的問題,達到對頁面穩定性的把控。
圖片
資源加載成功之后頁面渲染繼續運行。
這些階段都有報錯的可能,而前端要做的就是監控后面這階段:資源加載和頁面交互。
做前端監控也有很多其他好處, 例如:
前端監控是很有必要的,通過監控,我們能在線上應用異常時,第一時間收到反饋,并及時止損。對業務的發展是有正向作用的。
錯誤監控包括 JavaScript 代碼錯誤、Promsie 錯誤、接口(XHR,Fetch)錯誤、資源加載錯誤(Script,Link等)等,這些錯誤大多會導致頁面功能異常甚至白屏。
性能監控包括頁面的加載時間、接口響應時間等,側面反應了用戶體驗的好壞。
代碼埋點,就是項目中引入埋點 SDK,手動在業務代碼中標記,觸發埋點事件進行上報。比如頁面中的某一個模塊的點擊事件,會在點擊事件的監聽中加入觸發埋點的代碼 this.$track('事件名', { 需要上傳的業務數據 }),將數據上報到服務器端。
這個方案也是我們實際項目中現有的方案。
通過可視化交互的手段,代替代碼埋點,可以新建、編輯、修改埋點。在組件和頁面的維度進行埋點的設計。
將業務代碼和埋點代碼分離,提供一個可視化交互的頁面,輸入為業務代碼,通過這個可視化系統,可以在業務代碼中自定義的增加埋點事件,最后輸出的代碼耦合了業務代碼和埋點代碼。
這個方案是可以解決第一種代碼埋點的痛點,也是我們目前正準備做的方案。
前端的任意一個事件都被綁定一個標識,所有的事件都被記錄下來,通過定期上傳記錄文件,配合文件解析,解析出來我們想要的數據,并生成可視化報告。
無痕埋點的優點是采集全量數據,不會出現漏埋和誤埋等現象。缺點是給數據傳輸和服務器增加壓力,也無法靈活定制數據結構。針對業務數據的準確性不高。
前端的埋點上報需要存儲起來,這個可以使用阿里云的日志服務,不需要投入開發就可以采集。
新建一個項目比如:xxx-monitor
新建一個存儲日志,根據阿里云的要求發起請求,攜帶需要上報的數據:
http://${project}.${host}/logstores/${logStore}/track
圖片
代碼中調用 Track 上報日志:
日志的上報可以封裝成公共的調用方式, monitor/utils/里面放所有的工具方法;
tracker.js 的實現就是按照阿里云的上報格式發送請求,并帶上處理好的需要上報的業務數據即可,下面的都是固定的,在日志服務建好:
圖片
實現一個 Tracker 類導出類的實例即可,這樣在監控的核心代碼中直接調用 tracker.send(data),核心實現代碼如下:
// monitor/utils/get/track.js...class SendTrackLoger { constructor() { this.url = `http://${project}.${host}/logstores/${logStore}/track` this.xhr = new XMLHttpRequest() } send(data = {}, callback) { const logData = {...logData} for(let key in logs) { if (typeof logs[key] === 'number') { logs[key] = `${logs[key]}` // 這是阿里云的要求,字段不能是數字類型 } } let body = JSON.stringify({ __logs__: [logs] }) this.xhr.open('POST', this.url, true) this.xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) this.xhr.onload = function() { if (this.status >= 200 && this.status <= 300 || this.status === 304) { callback && callback() } } this.xhr.onerror = function(error) { console.log(error) } this.xhr.send(body) }}export default new SendTrackLoger()
這里展示的是自定義要上報的數據字段:
圖片
前端需要監控的錯誤有兩類:
新建一個 fronend-monitor 項目,這個項目就相當于我們的工程項目,監控的核心實現可以寫到項目里面,也可以抽成 SDK 的形式 Import 引入進來,這里先寫到項目中。
webpack.config.js 用來打包項目,做接口數據 Mock,測試 XHR 請求監控接口錯誤等。
const path = require('path')const HtmlWebpackPlugin = xxxmodule.exports = { mode: 'development', context: process.cwd(), entry:'./src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'monitor.js' }, devServer: { contentBase: path.resolve(__dirname, 'dist'), before(router) { router.get('/success', function(req, res) { res.json({ id: 1 }) }) router.post('/error', function(req, res) { res.sendStatus(500) }) }, }, module: {}, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', inject: "head" }) ]}
新建一個 src/index.html 在這個里面寫一些問題代碼,然后測試監控的錯誤捕獲。
// src/index.html<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" cnotallow="width=device-width, initial-scale=1.0"> <title>monitor</title></head><body> <input id="jsErrorBtn" type="button" value="js 代碼錯誤" notallow="btnClick()" /> <input id="promiseErrorBtn" type="button" value="promise 錯誤" notallow="promiseClick()" /> <input id="successBtn" type="button" value="成功 ajax 請求" notallow="successAjax()" /> <input id="errorBtn" type="button" value="失敗 ajax 請求" notallow="errorAjax()" /> <script> function btnClick() { window.goods.type = 2 } function promiseClick() { new Promise((resolve, reject) => { resolve(1) }, () => { console.log(123) }) } function successAjax() { var xhr = new XMLHttpRequest() xhr.open('GET', '/success', true) xhr.responseType = 'json' xhr.onload = function () { console.log(xhr.response) } xhr.send() } function errorAjax() { var xhr = new XMLHttpRequest() xhr.open('POST', '/error', true) xhr.responseType = 'json' xhr.onload = function() { console.log(xhr.response) } xhr.onerror = function(err) { console.log(err) } xhr.send('name=123') }</script></body></html>
Javascript 錯誤分為 2 種:語法錯誤,資源家加載錯誤,這些錯誤都會被 window.addEventListener('error', function(event) {})捕獲,來判斷是否是資源加載錯誤。
window.addEventListener('error', function(event) { // 如果 target 是script link 等資源 if (event.target && (event.target.src || event.target.href)) { const element = getElement(event.target || event.path) tracker.send({ ... title: document.title, url: location.href, timestamp: event.timeStamp, userAgent: navigator.userAgent, type: 'resourceError', ... }) } else { tracker.send({ ... title: document.title, url: location.href, timestamp: event.timeStamp, userAgent: navigator.userAgent, type: 'jsError', ... }) } }, true)
代碼中未被捕獲的 Promise 錯誤,要監聽 unhandledrejection 事件 window.addEventListener('unhandledrejection', function(event) {})。
// 監聽未捕獲的 promise 錯誤 window.addEventListener('unhandledrejection', function(event) { // PromiseRejectionEvent let message = '' let stack = '' const reason = event.reason let filename = '' let lineno = '' let colno = '' if (reason) { message = reason.message stack = reason.stack const match = stack.match(//s+at/s+(.+):(/d+):(/d+).+/) filename = match[1] lineno = match[2] colno = match[3] } tracker.send({ ... title: document.title, url: location.href, timestamp: event.timeStamp, userAgent: navigator.userAgent, type: 'promiseError', ... }) }, true)
接口異常上報主要是攔截請求,攔截 XMLHttpRequest 對象,改寫 XHR 的 Open 和 Send 方法,將需要上報的數據發到阿里云存儲,監聽 Load,Error,Abort 事件,上報數據:
// src/monitor/lib/xhr.jsimport tracker from '../utils/tracker'export default function injectXHR() { // 獲取 window 上的 XMLHttpRequest 對象 const XMLHttpRequest = window.XMLHttpRequest // 保存舊的open, send函數 const prevOpen = XMLHttpRequest.prototype.open const prevSend = XMLHttpRequest.prototype.send // 不可使用箭頭函數,不然會找不到 this 實例 XMLHttpRequest.prototype.open = function (method, url, async, username, password) { // 重寫open,攔截請求 // 不攔截 track 本身以及 socket, 直接放行 if (!url.match(/logstores/) && !url.match(/sockjs/)) { this.logData = { method, url, async, username, password } } return prevOpen.apply(this, arguments) } XMLHttpRequest.prototype.send = function (body) { // 重寫 send,攔截有 logData 的請求,獲取 body 參數 if (this.logData) { this.logData.body = body let startTime = Date.now() function handler(type) { return function (event) { // event: ProgressEvent let duration = Date.now() - startTime let status = this.status let statusText = this.statusText console.log(event) tracker.send({ type: 'xhr', eventType: type, pathname: this.logData.url, status: `${status} ${statusText}`, duration: `${duration}`, // 接口響應時長 response: this.response ? JSON.stringify(this.response) : '', params: body || '', }) } } this.addEventListener('load', handler('load'), false) this.addEventListener('error', handler('error'), false) this.addEventListener('abort', handler('abort'), false) } return prevSend.apply(this, arguments) }}
白屏就是頁面上什么東西也沒有,在頁面加載完成之后,如果頁面上的空白點很多,就說明頁面是白屏的,需要上報,這個上報的時機是:document.readyState === 'complete' 表示文檔和所有的子資源已完成加載,表示load(window.addEventListener('load')狀態事件即將被觸發。
document.readyState 有三個值:loading(document正在加載),interactive(可交互,表示正在加載的狀態結束,但是圖像,樣式和框架之類的子資源仍在加載),complete 就是完成,所以監控白屏需要在文檔都加載完成的情況下觸發。
// src/monitor/utils/onload.jsexport function onload(callback) { if (document.readyState === 'complete') { callback() } else { window.addEventListener('onload', callback) }}
監控白屏的思路主要是:可以將可視區域中心點作為坐標軸的中心,在X、Y軸上各分 10 個點,找出這個 20 個坐標點上最上層的 DOM 元素,如過這些元素是包裹元素,空白點數就加一,包裹元素可以自定義比如 Html Body App Root Container Content 等,空白點數大于 0 就上報白屏日志。
export default function computedBlankScreen() { // 包裹玉元素列表 const wrapperSelectors = ['body', 'html', '#root', '#App'] // 空白節點的個數 let emptyPoints = 0 // 判斷20個點處的元素是否是包裹元素 function isWrapper(element) { const selector = getSelector(element) console.log(selector) if (wrapperSelectors.indexOf(selector) >= 0) { // 表示是在包裹元素里面,空白點就要加一 emptyPoints++ } } onload(function() { let xElements, yElements for (let i = 0; i <=9; i++) { xElements = document.elementFromPoint(window.innerWidth * i / 10, window.innerHeight / 2) yElements = document.elementFromPoint(window.innerHeight * i / 10, window.innerWidth / 2) // document.elementFromPoint 返回的是某一個坐標點的由到外的html元素的集合 isWrapper(xElements[0]) isWrapper(yElements[0]) } if (emptyPoints >= 0) { let centerPoint = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2) tracker.send() } })}
用戶交互的響應時間如果大于某一個時間,用戶就會感覺卡頓。可以定一個時間比如 100 毫秒,就代表響應時間長,會卡頓。
PerformanceObserver 構造函數使用給定的觀察者 Callback 生成新的PerformanceObserver 對象,當通過 Observe() 方法注冊條目類型(需要監控的類型)的性能條目被記錄下來時,會調用該觀察者回調。
所以可以 new PerformanceObserver 來監控 longTask,監控的資源加載如果超過 100 毫秒就表示卡頓,可以瀏覽器空閑(requestIdleCallback)的時候上報數據。
....export default function longTask() { new PerformanceObserver(function(list) { list.getEntries().forEach(function(entry) { if (entry.duration > 100) { // 瀏覽器空閑的時候上報 requestIdleCallback(() => { tracker.send({ type: 'longTask', eventType: lastEvent.type, startTime: formatTime(entry.startTime), duration: formatTime(entry.duration), }); }); } }) }).observe({ entryTypes: ['longtask']})}
PerformanceObserver.observe 方法用于觀察傳入的參數中指定的性能條目類型的集合。當記錄一個指定類型的性能條目時,性能監測對象的回調函數將會被調用。performance.timing 記錄了從輸入 URL 到頁面加載完成的所有的時間,從這些字段中可以提取對對頁面性能的監控,通過分析這些指標來優化頁面的體驗,比如統計 FMP、LCP 等,具體可以查看 MDN。
navigator.connection 對象獲取網絡連接的信息:effectiveType(網絡類型),RTT(估算餓往返時間)等,還能通過監聽 window.addEventListener('unload')事件計算用戶在頁面的停留時間。
import tracker from '../util/tracker';export function pv() { var connection = navigator.connection; tracker.send({ type: 'pv', networkType: connection.effectiveType, // 網絡類型 rtt: connection.rtt, // 往返時間 screen: `${window.screen.width}x${window.screen.height}` // 設備分辨率 }); let startTime = Date.now(); window.addEventListener('unload', () => { let stayTime = Date.now() - startTime; // 頁面停留時間 tracker.send({ type: 'stayTime', stayTime }); }, false);}
本文鏈接:http://www.www897cc.com/showinfo-26-17850-0.html一文搞懂得物前端監控
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 想要微信小程序+Uniapp?XBoot開源項目助你快速開發!
下一篇: 十個Python中的數據類型技巧