一文搞懂得物前端監控

架構師修行手冊發表於2023-11-10


來源:得物技術

目錄

一、背景

二、監控型別

    1. 效能監控

    2. 異常告警監控

    3. 日常巡檢監控

三、監控知識梳理

    1. 前端監控的必要性

    2. 前端監控目標

        2.1 保證穩定性(錯誤監控)

        2.2 提升使用者體驗(效能監控)

        2.3 針對業務進行統計

    3. 前端監控的流程

        3.1 前端埋點方案

        3.2 監控指令碼

四、結語

背景

得物的服務端監控是比較全面和有效的,除了上報原始日誌資料,還透過資料分析制定線上告警機制,呼叫鏈路分析,而針對前端專案這一塊,還是不夠全面的。對前端線上問題感應不及時,靠人肉發現,沒有告警機制等問題,所以就有個前端監控這個專案。前端監控也確實很有必要,我們需要對線上的頁面有個全面的把控,而至於怎麼做監控,做資料上報,以及資料分析,如何針對監控資料分析出有用的核心鏈路的告警等也能有個全面的認識。本文主要是介紹得物針對監控做了哪些事情以及對前端底層監控手段做個總結。

監控型別

前端監控的範圍很廣,如監控效能,監控異常,監控告警等一系列的維度來確保我們的頁面和功能是正常的,在出現問題時研發可以及時做出響應,及時追蹤,定位問題。

效能監控

目前前端平臺在效能監控和異常監控是分開的,效能監控有專門 SDK,去做上報和分析,聯合得物 App 端上一起做了效能最佳化和資料分析,這塊是獨立的。主要包括效能的上報,以及效能的最佳化手段。下圖是前端效能監控方面的資料展示效果。

一文搞懂得物前端監控

總體來說效能監控是做的比較大的,而且監控對提高頁面的秒開是有實際最佳化指導意義的,離線包、預請求等都是最佳化手段。

異常告警監控

針對異常監控也有單獨的平臺和 SDK 去承接和上報,下圖來展示收集一些異常資訊,對異常資訊進行分類,再透過設定告警機制來通知開發人員及時發現問題:

一文搞懂得物前端監控

這些錯誤類的資訊不一定是我們都需要關注的,有些疑難雜症,但是有不影響頁面展示和功能的報錯,也是可以忽略的,要知道不是所有的錯誤都能被解決的,這個時候我們可以只關注那些影響我們頁面核心功能的部分,針對這部分做一個告警配置,例如:

一文搞懂得物前端監控

針對告警資訊,再進一步對錯誤進行分析,找到能解決的問題,達到對頁面穩定性的把控。

日常巡檢監控

還有一種比較特殊的場景,針對運營活動做的營銷會場,在各個配置的坑位去做巡檢,提前發現會場是否正常,有沒有白屏、API 異常等提前發現,然後聯絡相應的人去人工處理。這種監控模式對於要投放出去的頁面做提前檢查是很有效果的,事實證明也確實如此,提前避免了很多線上問題,很厲害。

一文搞懂得物前端監控

監控知識梳理

上面幾個大型別的監控都是前端同學在監控方面做的努力和成果,涉及到的知識點也很多,同樣對於業務的把控和理解也很深入,很值得學習並瞭解,後面的內容就是針對監控做個梳理總結,瞭解下大致的實現方式。

前端監控的必要性

使用者在訪問頁面的時候大致會經歷下面的階段:

  • 向服務端請求獲取靜態資源;

  • 瀏覽器載入資源;

  • 資源載入成功之後頁面渲染繼續執行。

這些階段都有報錯的可能,而前端要做的就是監控後面這階段:資源載入和頁面互動。

做前端監控也有很多其他好處, 例如:

  • 第一時間上報異常,解決問題;

  • 能夠比較完整的重現問題使用者的操作全流程路徑,方便開發者復現問題,定位問題;

  • 針對頁面的 PV、UV 等資訊可以為產品和運營做推廣決策提供資料依據。

前端監控是很有必要的,透過監控,我們能線上上應用異常時,第一時間收到反饋,並及時止損。對業務的發展是有正向作用的。

前端監控目標

保證穩定性(錯誤監控)

錯誤監控包括 JavaScript 程式碼錯誤、Promsie 錯誤、介面(XHR,Fetch)錯誤、資源載入錯誤(Script,Link等)等,這些錯誤大多會導致頁面功能異常甚至白屏。

提升使用者體驗(效能監控)

效能監控包括頁面的載入時間、介面響應時間等,側面反應了使用者體驗的好壞。

  • 載入時間:頁面執行時各個階段的載入時間;

  • TTFB(Time To First Byte)(首位元組時間):瀏覽器發起第一個請求到資料返回第一個位元組所消耗的時間,這個時間包含了網路請求時間、後端處理時間;

  • FP(First Paint)(首次繪製):首次繪製包括了任何使用者自定義的背景繪製,它是將第一個畫素點繪製到螢幕的時刻;

  • FCP(First Content Paint)(首次內容繪製):首次內容繪製是瀏覽器將第一個 DOM 渲染到螢幕的時間,可以是任何文字、影像、SVG 等的時間;

  • FMP(First Meaningful paint)(首次有意義繪製):首次有意義繪製是頁面可用性的量度標準;

  • LCP(Largest Contentful Paint):視窗內最大的圖片或者文字渲染的時間,當最大的內容塊渲染完的時候,基本上主內容都載入完了,與現有的頁面載入指標相比,與使用者體驗的相關性更好;

  • FID(First Input Delay)(首次輸入延遲):使用者首次和頁面互動到頁面響應互動的時間;

  • 卡頓:指超過 50ms 的長任務,具體的指標可以根據頁面的內容進行調節,一般 50ms 人眼就能感覺到卡頓。

針對業務進行統計

  • PV:Page View 即頁面瀏覽量或點選量;

  • UV:指訪問某個站點的不同 IP 地址的人數;

  • 頁面的停留時間:使用者在每一個頁面的停留時間。

前端監控的流程

  • 前端埋點(透過 SDK 給頁面的 DOM 都加上標記)

  • 資料上報(收集,儲存)

  • 分析和計算(將採集到的資料進行加工彙總)

  • 視覺化展示(按照緯度將資料展示)

  • 監控報警(發現異常後按一定的條件觸發報警)

前端埋點方案

程式碼埋點

程式碼埋點,就是專案中引入埋點 SDK,手動在業務程式碼中標記,觸發埋點事件進行上報。比如頁面中的某一個模組的點選事件,會在點選事件的監聽中加入觸發埋點的程式碼 this.$track('事件名', { 需要上傳的業務資料 }),將資料上報到伺服器端。

  • 優點:能夠在任何時刻,更精確的傳送需要的資料資訊,上報資料更靈活;

  • 缺點:工作量大,程式碼侵入太強,過於耦合業務程式碼,一次埋點的更改就要引起發版之類的操作。

這個方案也是我們實際專案中現有的方案。

視覺化埋點

透過視覺化互動的手段,代替程式碼埋點,可以新建、編輯、修改埋點。在元件和頁面的維度進行埋點的設計。

將業務程式碼和埋點程式碼分離,提供一個視覺化互動的頁面,輸入為業務程式碼,透過這個視覺化系統,可以在業務程式碼中自定義的增加埋點事件,最後輸出的程式碼耦合了業務程式碼和埋點程式碼。

這個方案是可以解決第一種程式碼埋點的痛點,也是我們目前正準備做的方案。

無痕埋點

前端的任意一個事件都被繫結一個標識,所有的事件都被記錄下來,透過定期上傳記錄檔案,配合檔案解析,解析出來我們想要的資料,並生成視覺化報告。

無痕埋點的優點是採集全量資料,不會出現漏埋和誤埋等現象。缺點是給資料傳輸和伺服器增加壓力,也無法靈活定製資料結構。針對業務資料的準確性不高。

監控指令碼

日誌儲存

前端的埋點上報需要儲存起來,這個可以使用阿里雲的日誌服務,不需要投入開發就可以採集。

新建一個專案比如:xxx-monitor

新建一個儲存日誌,根據阿里雲的要求發起請求,攜帶需要上報的資料:

{project}.${host}/logstores/${logStore}/track

一文搞懂得物前端監控

程式碼中呼叫 Track 上報日誌:

日誌的上報可以封裝成公共的呼叫方式, monitor/utils/裡面放所有的工具方法;

tracker.js 的實現就是按照阿里雲的上報格式傳送請求,並帶上處理好的需要上報的業務資料即可,下面的都是固定的,在日誌服務建好:

一文搞懂得物前端監控

實現一個 Tracker 類匯出類的例項即可,這樣在監控的核心程式碼中直接呼叫 tracker.send(data),核心實現程式碼如下:





































// monitor/utils/get/track.js...class SendTrackLoger {  constructor() {    this.url = `{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()

這裡展示的是自定義要上報的資料欄位:

一文搞懂得物前端監控

監控錯誤

前端需要監控的錯誤有兩類:

  • Javascript 錯誤(JS 錯誤,Promise 異常)

  • 監聽 Error 錯誤(資源載入錯誤)

指令碼實現

新建一個 fronend-monitor  專案,這個專案就相當於我們的工程專案,監控的核心實現可以寫到專案裡面,也可以抽成 SDK 的形式 Import 引入進來,這裡先寫到專案中。

webpack.config.js 用來打包專案,做介面資料 Mock,測試 XHR 請求監控介面錯誤等。































const path = require('path')const HtmlWebpackPlugin = xxx
module.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" content="width=device-width, initial-scale=1.0">  <title>monitor</title></head><body>  <input id="jsErrorBtn" type="button" value="js 程式碼錯誤" onclick="btnClick()" />  <input id="promiseErrorBtn" type="button" value="promise 錯誤" onclick="promiseClick()" />  <input id="successBtn" type="button" value="成功 ajax 請求" onclick="successAjax()" />  <input id="errorBtn" type="button" value="失敗 ajax 請求" onclick="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 錯誤

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。

統計pv (頁面的停留時間)

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);}

總結

前端監控是一個成熟業務線的標配,目前最多的場景是監控 JS 錯誤,介面請求和效能最佳化,然後根據日誌資訊進行分析分類的視覺化展示,在發生異常的時候通知到相應的業務開發,監控的效能指標給頁面的體驗最佳化提供資料對比和最佳化的方向。


參考文章:

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

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

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

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70027824/viewspace-2994618/,如需轉載,請註明出處,否則將追究法律責任。