從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

目錄

    • 前言
    • 為什么要做前端監控
    • 前端監控目標
      • 穩定性
      • 用戶體驗
      • 業務
    • 前端監控流程
      • 常見埋點方案
        • 代碼埋點
        • 可視化埋點
        • 無痕埋點
    • 創建項目
      • 第一步、創建`monitor`文件,`cmd`進入文件進行`npm init -y` 項目初始化
      • 第二步、創建`src/index.js`和`src/index.html`文件
      • 第三步、創建`webpack.config.js`文件
      • 第四步、安裝所需模塊
      • 第五步、配置package.json
    • js報錯,資源加載報錯,promise報錯采集上報腳本
      • 監控錯誤和錯誤分類
      • 錯誤異常上報數據結構設計
        • js報錯上報數據結構
        • promise報錯上報數據結構
        • 資源加載報錯上報數據結構
      • js異常監聽上報
        • 第一步、編寫index.html
        • 第二步、創建`/src/monitor/index.js`文件
        • 第三步,入口文件中引入`src/index.js`
        • 第四步、創建`/src/monitor/lib/jsError.js`
        • 第五步、`/src/monitor/index.js`導入`jsError.js`
        • 第六步、查看是否打印成功
        • 第七步、新建`/src/monitor/utils/getLastEvent.js`工具文件
        • 第八步、新建`/src/monitor/utils/getSelector.js` 獲取報錯元素路徑文件
        • 第八步、創建上報文件`/src/monitor/utils/getSelector.js`
        • 第八步、編寫`jsError.js`文件
        • 第九步、上報效果查看
      • Promise異常監聽上報
        • 第一步、promise監聽異常上報方法實現
        • 第二步、上報效果查看
      • 資源加載異常監聽上報
        • 第一步、改造`/src/monitor/jsError.js文件`
        • 第二步、修改`/src/monitor/utils/getSelector.js`方法
        • 第三步、上報效果查看
    • 接口異常采集上報腳本
      • 接口異常上報數據結構設計
      • 接口監聽上報
        • 第一步、模擬接口請求
        • 第二步、創建`xhr.js`監聽接口上報文件
        • 第三步、引入并使用上報監聽
        • 第四步、查看接口監聽上報效果
    • 白屏采集上報腳本
      • 白屏采集上報數據結構
      • 思路
      • 頁面關鍵點采樣對比技術實現白屏上報
        • 第一步、創建onload.js文件方法
        • 第二步、關鍵點采樣對比監控白屏上報
        • 第三步、編寫可監聽到白屏和非白屏的HTML文件
        • 第四步、監測白屏上報和非白屏效果
    • 加載時間采集上報
      • 瀏覽器加載一個網頁的整個流程
        • 流程中字段描述
        • 其他輔助階段字段描述
        • 需要上報字段描述
      • 瀏覽器加載和渲染網頁的整個過程
      • 瀏覽器加載和渲染時間計算上報實現
        • 第一步、創建`/src/monitor/lib/timing.js`文件
        • 第二步、模擬DOM加載完成延遲inde.html文件
        • 第三步、瀏覽器加載和渲染時間上報效果
    • 性能指標采集上報
      • 性能指標描述
      • 各個性能指標獲取
        • 觸發用例index.html
        • FMP 首次有意義繪制
        • LCP 最大內容渲染
        • FID 首次輸入延遲
        • FP 首次繪制
        • FCP 首次內容繪制
        • 性能指標上報整體代碼
        • 查看各項性能指標采集上報效果
    • 卡頓監聽上報
      • 監聽方案
      • 數據結構
      • 借助Long Tasks API實現卡頓上報
        • 第一步、創建longTask.js文件
        • 第二步、循環模仿卡頓效果
        • 第三步、查看卡頓上報效果
    • PV、UV、頁面停留時間采集上報
      • 實現pv和頁面停留時間監聽
        • 第一步、創建pv.js
        • 第二步、查看上報結果
    • 完結~

前言

????????在當今快速發展的Web開發領域中,無論是面試還是與同行交流時,“高大上”的技術術語層出不窮。還記得我第一次了解到前端埋點監控是在一家知名大廠的面試過程中。當面試官問及如何實現前端埋點監控時,我感到一陣茫然——腦海中一片模糊,各種概念交織卻難以理清頭緒。不出所料,那次面試的第三輪我沒有通過。

????????這次經歷成為我深入學習前端埋點監控的起點。回到家中,我開始查閱大量資料、觀看教學視頻,逐步摸索出了一套學習路徑。本文便是基于這段時間的學習和實踐總結而成,旨在為那些剛開始接觸前端埋點監控的朋友提供一個詳盡的指南。

????????文中涵蓋了從PV統計、頁面加載性能指標(如FMPLCP、FID、FP、FCP)到Promise異常監控、錯誤監控以及資源加載監控等多方面的內容。為了確保每個細節都能被清晰理解,并且避免將來遺忘關鍵知識點,我在編寫時盡可能地詳盡描述了每一步驟和每一個概念。

????????無論你是剛踏入前端領域的新人,還是希望進一步提升技能的開發者,相信這篇文章都能為你提供有價值的參考。讓我們一起揭開前端埋點監控的神秘面紗,掌握這一提升網站性能和用戶體驗的重要工具。

????????覺得內容有用?別忘了點個贊+收藏+關注,三連走一波!你的支持是我持續輸出的動力 💪

gitee倉庫地址:地址

為什么要做前端監控

珠峰架構公開課地址【下文根據公開課邊學邊做邊實現,加上自己的個人想法】:珠峰公開課地址

更快發現問題和解決問題。
做產品的決策依據。
提升前端工程師的技術深度和廣度。
為業務擴展提供更多可能性。

前端監控目標

穩定性

錯誤名稱備注
JS錯誤JS執行錯誤或者promise異常
資源異常scriptlink資源加載異常
接口錯誤ajaxfetch接口請求異常
白屏頁面白屏

用戶體驗

錯誤名稱備注
加載時間各個階段的加載時間
TTFB首字節時間是指瀏覽器發起第一個請求到數據返回第一個字節所消耗的時間,這個時間包含了網絡請求時間,后端處理時間。
FP首次繪制首次繪制包括了任何用戶自定義的背景繪制,它是將第一個像素點繪制到屏幕的時刻。
FCP首次內容繪制首次內容繪制是瀏覽器將第一個DOM渲染到屏幕的時間,可以是任何文本圖像和SVG等的時間。
FMP首次有意義繪制首次有意義繪制是頁面可用性的亮度標準。
FID首次輸入延遲用戶首次和頁面交互到頁面響應交互的時間
卡頓超過50ms的長任務

業務

錯誤名稱備注
PVpage view 即頁面瀏覽量或點擊量
UV指訪問某個站點不同IP地址的人數
頁面停留時間用戶在每一個頁面停留的時間

前端監控流程

  • 前端埋點
  • 數據上報
  • 分析和計算將采集到的數據進行加工匯總
  • 可視化展示將數據按各個維度進行展示
  • 監控報警,發現問題后按一定條件觸發報警
    從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
    上圖中,前端主要關注埋點和數據采集兩個階段即可。

常見埋點方案

代碼埋點

代碼埋點就是以嵌入代碼的形式進行埋點,比如需要監控用戶的點擊事件,會選擇在用戶點擊時插入一段代碼,保存這個監聽行為或者直接將監聽行為以某一種數據格式直接傳遞給服務端。

優點: 可以在任意時刻,精確的發送或保存所需要的數據信息。
缺點: 工作量比較大

可視化埋點

通過可視化交互的手段,代替代碼埋點,將業務代碼和埋點代碼分離,提供一個可視化交互的頁面,輸入業務代碼,通過這個可視化系統可以在業務代碼中自定義的增加埋點時間等等,最后輸出的代碼耦合了業務代碼和埋點代碼,可視化埋點其實是用系統來代替手工插入埋點代碼。

無痕埋點

前端的任意一個事件都被綁定一個標識,所有的事件都被記錄下來,通過定期上傳記錄文件,配合文件解析,解析出來我們想要的數據,并生成可視化報告供專業人員分析.

缺點: 數據傳輸和服務器壓力增加,無法靈活定制數據結構。

創建項目

第一步、創建monitor文件,cmd進入文件進行npm init -y 項目初始化

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第二步、創建src/index.jssrc/index.html文件

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第三步、創建webpack.config.js文件

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

const path = require('path')
// webpack打包項目的,HtmlWebpackPlugin生成產出HTML文件,user-agent 把瀏覽器的userAgent變成一個對象 
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {entry: "./src/index.js", // 入口文件 context: process.cwd(), // 上下文目錄 mode: "development", // 開發模式 output: {path: path.resolve(__dirname, "dist"), // 輸出目錄 filename: "monitor.js" // 文件名 },devServer: {static: {directory: path.resolve(__dirname, "dist"), // devServer靜態文件目錄,替換contentBase },}, plugins: [new HtmlWebpackPlugin({ // 自動打包出HTML文件 template: "./src/index.html",inject: "head"})]
}

第四步、安裝所需模塊

npm install webpack webpack-cli html-webpack-plugin user-agent -D

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

npm i webpack-dev-server -D

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第五步、配置package.json

{"name": "monitor","version": "1.0.0","description": "","main": "index.js","scripts": {"build": "webpack","dev": "webpack-dev-server"},"keywords": [],"author": "","license": "ISC","devDependencies": {"html-webpack-plugin": "^5.6.3","user-agent": "^1.0.4","webpack": "^5.99.7","webpack-cli": "^6.0.1","webpack-dev-server": "^5.2.1"}
}

執行npm run build,執行后會出現兩個文件
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
啟動服務器: npm run dev
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

js報錯,資源加載報錯,promise報錯采集上報腳本

監控錯誤和錯誤分類

  • JS錯誤:js報錯。
  • promise異常 資源異常:監聽error

錯誤異常上報數據結構設計

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

js報錯上報數據結構
{errorType: "jsError"filename: "http://localhost:8080/"kind: "stability"message: "Uncaught TypeError: Cannot set properties of undefined (setting 'error')"position: "25:30"selector: "html body div#container div.content input"stack: "TypeError: Cannot set properties of undefined (setting 'error')^errorClick (http://localhost:8080/:25:30)^HTMLInputElement.onclick (http://localhost:8080/:14:72)"timestamp: "1746587404145"title: "前端監控SDK"type: "error"url: "http://localhost:8080/"
}
promise報錯上報數據結構
	errorType: "PromiseError"  // 錯誤類型filename: "http://localhost:8080/" // 訪問的文件名kind: "stability" // 大類message: "Cannot set properties of undefined (setting 'error')"position: "30:32" // 報錯行列selector: "html body div#container div.content input"stack: "TypeError: Cannot set properties of undefined (setting 'error')^http://localhost:8080/:30:32^new Promise (<anonymous>)^promiseErrirClick (http://localhost:8080/:29:9)^HTMLInputElement.onclick (http://localhost:8080/:19:11)" timestamp: "1746587310847"title: "前端監控SDK" // 頁面標題type: "error"  // 小類url: "http://localhost:8080/" // urluserAgent:"chrome 135.0.0.0"  // 用戶瀏覽器信息
資源加載報錯上報數據結構
{errorType: "resourceError", // 錯誤類型filename: "http://localhost:8080/monitor.css", // 訪問的文件名kind: "stability", // 大類selector: "html head link" "selector", // 選擇器tagName: "LINK",  // 標簽名timestamp: "1746587169153",  // 時間戳title: "前端監控SDK",  // 頁面標題type: "error", // 小類url: "http://localhost:8080/", // 頁面URLuserAgent:"chrome 135.0.0.0" // 用戶瀏覽器信息
}

js異常監聽上報

第一步、編寫index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content"><input type="button" value="點擊報錯" onclick="errorClick()" /><inputtype="button"value="點擊拋出Promise錯誤"onclick="promiseErrirClick()"/></div></div><script>function errorClick() {window.someVar.error = "error";}</script></body>
</html>
第二步、創建/src/monitor/index.js文件

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第三步,入口文件中引入src/index.js

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第四步、創建/src/monitor/lib/jsError.js

創建/src/monitor/lib/jsError.js并導出export function injectJsError() { }
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第五步、/src/monitor/index.js導入jsError.js
import { injectJsError } from './lib/jsError'
injectJsError()
第六步、查看是否打印成功

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第七步、新建/src/monitor/utils/getLastEvent.js工具文件
let lastEvent;
['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(eventType => {document.addEventListener(eventType, function (e) {lastEvent = e}, {capture: true, // 捕獲階段監聽passive: true, // 默認不阻止默認事件})
})
第八步、新建/src/monitor/utils/getSelector.js 獲取報錯元素路徑文件
function getSelectors(path) {// 反轉輸入的元素和路徑數組,并過濾掉document和window對象return path.reverse().filter(element => {return element !== document && element !== window}).map(element => {let selector = ""; // 初始化選擇器字符串if (element.id) { // 如果元素有id屬性,則使用id創建更具體的選擇器return `${element.nodeName.toLowerCase()}#${element.id}`} else if (element.className && typeof element.className === "string") {// 如果元素沒有id屬性,但是有className屬性,并且是個字符串,則嘗試使用class來創建選擇器return `${element.nodeName.toLowerCase()}.${element.className}`} else {// 如果既沒有id也沒有class則進返回元素的標簽名作為選擇器return selector = `${element.nodeName.toLowerCase()}`}}).join(" ") // 將所有選擇器用空格拼接
}
export default function (path) {if (Array.isArray(path)) {return getSelectors(path)}
}
第八步、創建上報文件/src/monitor/utils/getSelector.js

監聽錯誤后,調用send(data)方法,調用上報接口,將日志上報到阿里云日志中,當然如下的aliyun鏈接已經失效,可以換成自己的服務接口即可。

let host = 'en-beijing-log.aliyuncs.com';
let project = 'zhufengmonitor';
let logStore = 'zhufengmonitor-store';
let userAgent = require('user-agent');
function getExtraData() {return {title: document.title,url: location.url,timestamp: Date.now(),userAgent: userAgent.parse(navigator.userAgent)// 用戶ID}
}
class SendTracker {constructor() {// 上報路徑 : 項目名.主機名/logstores/存儲的名字/trackthis.url = `${project}.${host}/logstores/${logStore}/track` // 上報路徑this.xhr = new XMLHttpRequest();}send(data = {}) {let extraData = getExtraData();let log = { ...extraData, ...data }// 對象的值不能是數字for (let key in log) {if (typeof log[key] === 'number') {log[key] = `${log[key]}`;}}console.log("send log:", log)let body = JSON.stringify(log);this.xhr.open('POST', this.url, true);this.xhr.setRequestHeader('Content-Type', 'application/json') // 請求體類型this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') // 版本號this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // 請求體的大小this.xhr.onload = function () {console.log(this.xhr.response);}this.xhr.onerror = function (error) {console.log(error);}this.xhr.send(body);}
}
export default new SendTracker();
第八步、編寫jsError.js文件

需要注意,要按照阿里云日志接口格式進行傳輸

let host = 'cn-beijing.log.aliyuncs.com';
let project = 'zhufengmonitor';
let logStore = 'zhufengmonitor-store';
let userAgent = require('user-agent');
function getExtraData() {return {title: document.title,url: location.url,timestamp: Date.now(),userAgent: userAgent.parse(navigator.userAgent)// 用戶ID}
}
class SendTracker {constructor() {// 上報路徑 : 項目名.主機名/logstores/存儲的名字/trackthis.url = `https://${project}.${host}/logstores/${logStore}/track` // 上報路徑this.xhr = new XMLHttpRequest();}send(data = {}) {let extraData = getExtraData();let log = { ...extraData, ...data }// 對象的值不能是數字for (let key in log) {if (typeof log[key] === 'number') {log[key] = `${log[key]}`;}}let body = JSON.stringify({__logs__: [log]})this.xhr.open('POST', this.url, true);this.xhr.setRequestHeader('Content-Type', 'application/json') // 請求體類型this.xhr.setRequestHeader('x-log-apiversion', '0.6.0') // 版本號this.xhr.setRequestHeader('x-log-bodyrawsize', body.length) // 請求體的大小this.xhr.onload = function () {// console.log(this.xhr.response);}this.xhr.onerror = function (error) {// console.log(error);}this.xhr.send(body);}
}
export default new SendTracker();
第九步、上報效果查看

鏈接地址已經失效,如果實現換成自己的服務接口即可。
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
傳輸數據格式
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

Promise異常監聽上報

第一步、promise監聽異常上報方法實現

src/monitor/lib/jsError.js文件新增promise拋出異常監聽代碼:

import getLastEvent from '../utils/getLastEvent.js'
import getSelector from '../utils/getSelector.js'
import tracker from '../utils/tracker.js'
export function injectJsError() {// 監聽全局未捕獲的錯誤window.addEventListener('error', function (event) {let lastEvent = getLastEvent() // 獲取最后一個交互事件// 獲取冒泡向上的dom路徑var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let log = {kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'jsError', // JS執行錯誤filename: event.filename, // 錯誤所在的文件position: event.lineno + ':' + event.colno, // 錯誤所在的行和列的位置message: event.message, // 錯誤信息stack: getLines(event.error.stack), // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素}tracker.send(log)  // 上報數據})
// 監聽全局promise錯誤異常window.addEventListener('unhandledrejection', function (event) {console.log("promise error:", event)let lastEvent = getLastEvent(); // 最后一個事件對象var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let message;let filename;let line = 0;let column = 0;let stack = '';let reason = event.reason;if (typeof reason === 'string') {message = reason;} else if (typeof reason === 'object') {  // 說明是一個錯誤對象 if (reason.stack) {let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);filename = matchResult[1];line = matchResult[2];column = matchResult[3];}stack = getLines(reason.stack)}message = reason.message;tracker.send({kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'PromiseError', // promise執行錯誤filename, // 錯誤所在的文件position: line + ':' + column, // 錯誤所在的行和列的位置message, // 錯誤信息stack, // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素})  // 上報數據}, true)// 拼接at報錯方法一“^”拼接function getLines(stack) {return stack.split('\n').map(item => item.replace(/^\s+at\s+/g, '')).join('^');}
}
第二步、上報效果查看

接口換為公司上報接口即可:
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

資源加載異常監聽上報

第一步、改造/src/monitor/jsError.js文件

如果 (JavaScript 文件CSS 文件)如果加載失敗都會觸發window.addEventListener('error', (event) => {})的監聽事件

import getLastEvent from '../utils/getLastEvent.js'
import getSelector from '../utils/getSelector.js'
import tracker from '../utils/tracker.js'
export function injectJsError() {// 監聽全局未捕獲的錯誤 window.addEventListener('error', (event) => {let lastEvent = getLastEvent() // 獲取最后一個交互事件// 這是一個腳本加載錯誤if (event.target && (event.target.src || event.target.href)) {let log = {kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'resourceError', // JS或者CS資源加載錯誤filename: event.target.src || event.target.href, // 哪個文件報錯了tagName: event.target.tagName, // 錯誤所在標簽// stack: getLines(event.error.stack), // 錯誤堆棧selector: getSelector(event.target) // 代表最后一個操作的元素}tracker.send(log)  // 上報數據} else {// 獲取冒泡向上的dom路徑var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let log = {kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'jsError', // JS執行錯誤filename: event.filename, // 錯誤所在的文件position: event.lineno + ':' + event.colno, // 錯誤所在的行和列的位置message: event.message, // 錯誤信息stack: getLines(event.error.stack), // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素}tracker.send(log)  // 上報數據}}, true)window.addEventListener('unhandledrejection', function (event) {console.log("異常錯誤");let lastEvent = getLastEvent(); // 最后一個事件對象var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);let message;let filename;let line = 0;let column = 0;let stack = '';let reason = event.reason;if (typeof reason === 'string') {message = reason;} else if (typeof reason === 'object') {  // 說明是一個錯誤對象 if (reason.stack) {let matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);filename = matchResult[1];line = matchResult[2];column = matchResult[3];}stack = getLines(reason.stack)}message = reason.message;tracker.send({kind: 'stability', // 監控指標的大類type: 'error', // 小類型錯誤errorType: 'PromiseError', // JS執行錯誤filename, // 錯誤所在的文件position: line + ':' + column, // 錯誤所在的行和列的位置message, // 錯誤信息stack, // 錯誤堆棧selector: lastEvent ? getSelector(path) : "" // 代表最后一個操作的元素})  // 上報數據}, true)// 拼接at報錯方法一“^”拼接function getLines(stack) {return stack.split('\n').map(item => item.replace(/^\s+at\s+/g, '')).join('^');}
}
第二步、修改/src/monitor/utils/getSelector.js方法
function getSelectors(path) {// 反轉輸入的元素和路徑數組,并過濾掉document和window對象return path.reverse().filter(element => {return element !== document && element !== window}).map(element => {let selector = ""; // 初始化選擇器字符串if (element.id) { // 如果元素有id屬性,則使用id創建更具體的選擇器return `${element.nodeName.toLowerCase()}#${element.id}`} else if (element.className && typeof element.className === "string") {// 如果元素沒有id屬性,但是有className屬性,并且是個字符串,則嘗試使用class來創建選擇器return `${element.nodeName.toLowerCase()}.${element.className}`} else {// 如果既沒有id也沒有class則進返回元素的標簽名作為選擇器return selector = `${element.nodeName.toLowerCase()}`}}).join(" ") // 將所有選擇器用空格拼接
}
export default function (pathOrTarget) {if (Array.isArray(pathOrTarget)) { // 可能是一個數組,也可能是一個對象 return getSelectors(pathOrTarget)} else {let path = []while (pathOrTarget) {path.push(pathOrTarget);pathOrTarget = pathOrTarget.parentNode;}return getSelectors(path)}
}
第三步、上報效果查看

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
上面換成公司服務上報接口即可

接口異常采集上報腳本

接口異常上報數據結構設計

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

接口監聽上報

第一步、模擬接口請求

編寫/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>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content"><inputid="successBtn"type="button"value="ajax成功請求"onclick="sendSuccess()"/><inputid="errorBtn"type="button"value="ajax失敗請求"onclick="sendError()"/></div></div><script>//function sendSuccess() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/checkParamConfig/getDetails",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.send();}// 發送錯誤報錯function sendError() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/systemConfig/getDetail",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.onerror = function (error) {console.log("error", error);};xhr.send();}</script></body>
</html>
第二步、創建xhr.js監聽接口上報文件

創建/src/monitor/lib/xhr.js文件,編寫增強 XMLHttpRequest 對象的功能,以監控和記錄所有的 AJAX 請求及其狀態。通過重寫 XMLHttpRequest.prototype.openXMLHttpRequest.prototype.send 方法,可以在請求發出前、請求完成時以及請求失敗或被取消時收集相關數據,并使用 tracker.send 方法將這些數據上報給某個監控系統。

import tracker from '../utils/tracker.js';
export default function injectXHR() {// 保存原始的 open 和 send 方法const originalXhrOpen = window.XMLHttpRequest.prototype.open;window.XMLHttpRequest.prototype.open = function (method, url, async) {console.log("url", url)// 上報請求不需要返回if (!url.match(/logstores/) && !url.match(/sockjs/)) {console.log("logstores")this.logData = {method,url,async,startTime: Date.now()};}return originalXhrOpen.apply(this, arguments);};const originalXhrSend = window.XMLHttpRequest.prototype.send;window.XMLHttpRequest.prototype.send = function (body) {if (this.logData) {const startTime = Date.now();const handler = (type) => (event) => {const duration = Date.now() - startTime;const status = this.status; // 狀態碼const statusText = this.response.error; // OK Server Errortracker.send({kind: 'stability',type: 'xhr',eventType: event.type,pathname: this.logData.url, // 請求路徑status: `${status}-${statusText}`, // 狀態碼duration, // 持續時間response: JSON.stringify(this.response),requestData: body || '',params: this.logData.params, // 響應體timestamp: Date.now()});};this.addEventListener('load', handler('load'), false);this.addEventListener('error', handler('error'), false);this.addEventListener('abort', handler('abort'), false);}return originalXhrSend.apply(this, arguments);};
}
第三步、引入并使用上報監聽

src/monitor/index.js引入,并使用

import { injectJsError } from './lib/jsError'
import injectXHR from './lib/xhr'
injectJsError()
injectXHR()
第四步、查看接口監聽上報效果

接口請求成功:(忽略上報接口)
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
接口請求失敗:(忽略上報接口)
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
請求成功上報效果:
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

請求失敗上報效果:
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

白屏采集上報腳本

白屏采集上報數據結構

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

思路

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
在頁面中垂直交叉選取多個采樣點,使用elementsFromPointAPI獲取采樣點下的HTML元素,判斷采樣點元素是否與容器元素相同,遍歷采樣點,設置采樣點與容器元素相同的個數從而判斷是否出現擺屏,這種方法精確度比較高,技術棧無關,通用性能好,但是開發成本比較高。

頁面關鍵點采樣對比技術實現白屏上報

第一步、創建onload.js文件方法

????????新建src/monitor/utils/onload.js文件
????????如果在頁面完全沒有加載完畢的時候【元素,樣式表,iframe等外部資源】,dom可能還未穩定,就會導致document.elementsFromPoint() 獲取到的元素并不是最終用戶看到的那些。如果在這種情況下執行白屏檢測,可能會錯誤地判斷某些點為空白點,因為這些點對應的元素尚未加載或渲染完成。
????????在頁面加載過程中,JavaScript 可能會在 DOMContentLoaded 事件觸發后立即執行,這時雖然HTML文檔已經被完全加載和解析,但是頁面上的圖片、樣式表等外部資源可能還沒有加載完畢。因此,在這種狀態下直接進行白屏檢測,有可能會錯過一些重要的視覺變化,導致誤判。
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

export default function (callback) {if (document.readyState === 'complete') {callback()} else {window.addEventListener('load', () => {callback()})}
}
第二步、關鍵點采樣對比監控白屏上報

創建/src/monitor/lib/xhr.js文件,頁面關鍵點采樣對比實現白屏上報功能:

import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
// src/monitor/utils/onload.jsexport default function blankScreen() {let warpperElements = ['html', 'body', '#container', ".content"]let enptyPoints = 0function getSelector(element) {if (element && element.id) {return "#" + element.id} else if (element && element.className) {//  a b c => .a .b .creturn "." + element.className.split(' ').filter(item => !!item).join('.')} else {return element && element.nodeName.toLowerCase()}}//  是包裹元素++function isWrapper(element) {let selector = getSelector(element)if (warpperElements.indexOf(selector) != -1) {enptyPoints++}}// 當整個頁面渲染完成了才去判斷是否是白屏onload(function () {for (let i = 1; i <= 18; i++) {let xElement = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)let yElement = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10)isWrapper(xElement[0])isWrapper(yElement[0])}console.log("enptyPoints", enptyPoints)if (enptyPoints >= 16) {let centerElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2)tracker.send({kind: "stability",type: "blank",blankCount: enptyPoints, // 空白點screen: window.screen.width + "*" + window.screen.height, // 屏幕尺寸viewPoint: window.innerWidth + "*" + window.innerHeight, // 視口尺寸selector: getSelector(centerElements[0]),page: window.location.href, // 頁面地址message: "頁面空白點大于16個"})}});
}
第三步、編寫可監聽到白屏和非白屏的HTML文件

不觸發白屏監聽上報html文件,

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content" style="width: 100%; word-wrap: break-word"></div></div><script>let content = document.getElementsByClassName("content")[0];content.innerHTML = "<span>aaa</span>".repeat(10000);</script></body>
</html>

觸發白屏監聽上報html文件:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content"><inputid="successBtn"type="button"value="ajax成功請求"onclick="sendSuccess()"/><inputid="errorBtn"type="button"value="ajax失敗請求"onclick="sendError()"/></div></div><script>//function sendSuccess() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/systemConfig/getDetail",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.send();}// 發送錯誤報錯function sendError() {let xhr = new XMLHttpRequest();xhr.open("POST","http://192.168.60.38:32753/visiondevice/checkParamConfig/getDetailsAAAA",true);xhr.responseType = "json";xhr.onload = function () {console.log(xhr.response);};xhr.onerror = function (error) {console.log("error", error);};xhr.send();}</script></body>
</html>
第四步、監測白屏上報和非白屏效果

填滿<span>標簽,就不會出現白屏,沒有打印上報日志
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
不填滿<span>標簽,就會出現白屏空襲,就會打印上報日志并且發送給服務端
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
發送服務端數據
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

加載時間采集上報

瀏覽器加載一個網頁的整個流程

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
上面一張圖展示了瀏覽器加載一個網頁的整個流程,從開始時間到頁面完全加載完成。

流程中字段描述
字段描述
startTime(開始時間)這是整個加載過程的起點
Prompt for unload(準備卸載舊頁面)如果當前頁面是從另一個頁面導航過來的,瀏覽器會觸發卸載事件,準備卸載舊頁面。
redirectStart(開始重定向)如果存在重定向,瀏覽器會在此時開始重定向過程。
redirectEnd(結束重定向)重定向過程結束。
fetchStart(開始獲取文檔)瀏覽器開始獲取文檔的時間點。
domainLookupStart(開始域名解析)瀏覽器開始進行DNS查詢以解析域名。
domainLookupEnd(結束域名解析)DNS查詢完成。
connectStart(開始鏈接)瀏覽器開始建立與服務器的TCP連接。
secureConnectionStart(開始安全連接)如果使用HTTPS,瀏覽器開始TLS握手過程。
connectEnd(結束連接)TCP連接建立完成。
requestStart(開始請求)瀏覽器開始發送HTTP請求到服務器。
Time to First Byte(TTFB,首次字節時間)從發送請求到接收到第一個字節響應的時間。
responseStart(響應開始)服務器開始發送響應數據。
responseEnd(響應結束)服務器完成響應數據的發送。
unloadEventStart(卸載事件開始)在前一個頁面的卸載過程中,卸載事件開始。
unloadEventEnd(卸載事件結束)卸載事件結束。
domLoading(開始解析DOM)瀏覽器開始解析HTML文檔并構建DOM樹。
domInteractive(DOM結構基本解析完畢)DOM樹的基本結構已經構建完成,可以進行交互操作。
domContentLoadedEventStart(DOMContentLoaded事件開始)DOM完全加載且解析完成,但不包括樣式表、圖片等外部資源。
domContentLoadedEventEnd(DOMContentLoaded事件結束)DOMContentLoaded事件處理程序執行完畢。
domComplete(DOM和資源解析都完成)所有資源(如圖片、腳本等)都已加載完成。
loadEventStart(開始load回調函數)瀏覽器開始執行load事件的回調函數。
onLoad(加載事件)頁面完全加載完成,觸發load事件。
loadEventEnd(結束load回調函數)load事件的回調函數執行完畢。
其他輔助階段字段描述
字段描述
appCache(緩存存儲)應用緩存的處理過程
DNS(域名緩存)DNS緩存的處理過程
TCP(網絡連接)TCP鏈接的處理過程
Request(請求)請求的處理過程
Response(相應)響應的處理過程
Procsssing(處理)對響應數據的處理過程
Load(加載)頁面加載的最終階段
需要上報字段描述
字段描述計算方式意義
unload前一個頁面卸載耗時unloadEventEnd - unloadEventStart-
redirect重定向耗時redirectEnd - redirectStart重定向耗時
appCache緩存耗時domainLookupStart - fetchStart讀取緩存的時間
dnsdns解析耗時domainLookupEnd - domainLookupStart可觀察域名解析服務是否正常
tcptcp鏈接耗時connectEnd - cibbectStart建立連接耗時
sslSSL安全連接耗時connectEnd - secureConnectionStart反應數據安全連接建立耗時
ttfbTime to First Byte(TTFB)網絡請求耗時responseStart - requestStartTTFB是發出頁面請求到接收到應答數據第一個字節所花費的毫秒數
response相應數據傳輸耗時responseEnd - responseStart觀察網絡是否正常
domDOM解析耗時domInteractive - responseEnd觀察DOM結構是否合理,是否有JS阻塞頁面解析
dclDOMContentLoaded時間耗時domContentLoadedEventEnd - domContentLoadedEventStart當HTML文檔被完全加載和解析完成之后,DOMContentLoaded事件等待樣式表,圖像和子框架完成加載
resources資源加載耗時domComplete - domContentLoadedEventEnd可觀文檔流量是否過大
domReadyDOM 階段渲染耗時responseEnd - fetchStartDOM樹和頁面資源加載完成時間,會觸發domContentLoaded事件
首次渲染耗時首次渲染耗時responseEnd - fetchStart加載文檔到看到第一幀非空圖像的時間,也叫做白屏時間
首次可交互事件首次可交互時間domInteractive - fetchStartDOM樹解析完成時間,此時document.readyStart為interactive
首包時間耗時首包耗時responseStart - domainLookupStartDNS解析到響應返回給瀏覽器第一個字節的時間
頁面完全加載時間頁面完全加載時間loadEventSart - fetchStart-
onLoadonLoad事件耗時LoadEventEnd - loadEventStart-

瀏覽器加載和渲染網頁的整個過程

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
這張圖展示了瀏覽器加載和渲染網頁的整個過程,從請求HTML文件開始,到最終頁面渲染完成。
大致上圖流程為是一個階段,下面是對上圖中十一個階段的解讀:

  1. 第一階段,開始請求HTML文件,瀏覽器接收到用戶輸入的URL或者點擊鏈接等操作后,開始向服務器發送HTTP請求,請求獲取HTML文件。
  2. 第二階段,響應HTML文件,服務器接收到請求后,返回HTML文件給瀏覽器。
  3. 第三階段,開始加載,瀏覽器接收到HTML文件后,開始加載過程。
  4. 第四階段,構建DOM文檔對象模型,瀏覽器使用HTML解析器解析HTML文件構建DOM樹,DOM樹是HTML結構的表示形式,用于描述頁面的層次結構,在構建DOM樹的過程中,如果遇到<link>標簽(CSS文件引用)<script>標簽(js文件引入)瀏覽器會進行預解析,并發起對CSS文件和JS文件的請求。
  5. 第五階段,請求CSS文件和JS文件,瀏覽器在預解析過程中發現需要的CSSJS文件后,分別向服務器發送請求,獲取這些資源文件。
  6. 第六階段,返回CSS數據和JavaScript,服務器接收到請求后,返回相應的CSS文件和JS文件給瀏覽器。
  7. 第七階段,構建CSSOM也就是CSS對象模型,瀏覽器使用CSS解析器解析返回的CSS數據,構建CSSOM樹,CSSOM樹包含了所有樣式信息,用于描述頁面元素的央視。
  8. 第八階段,執行JavaScript,瀏覽器使用V8解析器解析并執行返回的JavaScript代碼,javaScript可以修改DOMCSSOM,因此在這個階段可能會繼續構建DOM樹。
  9. 第九階段,繼續構建DOM,如果JS代碼中包含了對DOM的修改操作,瀏覽器會繼續構建和更新DOM樹。
    10.第十階段,構建布局樹,當DOM樹和CSSOM樹都構建完成后,瀏覽器會將他們合并成一個布局樹也成為渲染樹,布局樹包含了所需要的渲染的節點及其央視信息
  10. 第十一階段,渲染階段,最后瀏覽器會根據布局樹進行頁面渲染,將頁面內容展示給用戶。

瀏覽器加載和渲染時間計算上報實現

????????在討論瀏覽器加載網頁的過程時候,Performance(性能)通常指的是web性能,Performance也是只瀏覽器提供的一個內置對象,window.performance通過這個API,開發者可以獲取到詳細的頁面加載時間和資源加載時間等信息,從而進行性能優化,windong.performance提供了多個屬性和方法來幫助分析網頁性能。

屬性描述
navigation包含了相關的導航信息,比如頁面是如何被加載的,以及設計重定向的次數。
timing提供了從開始導航到當前頁面完全加載過程中各個關鍵時間的時間點,如重定向時間,DNS查詢時間,TCP鏈接時間,請求發送時間,響應接收時間等。

打印Performance如下
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

第一步、創建/src/monitor/lib/timing.js文件

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
export default function timing() {onload(function () {setTimeout(() => {const {fetchStart,connectStart,connectEnd,requestStart,responseStart,responseEnd,domLoading,domInteractive,domContentLoadedEventStart,domContentLoadedEventEnd,loadEventStart,} = performance.timing;tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'timing', // 統計每個階段的時間connectTime: connectEnd - connectStart, // 連接時間ttfbTime: responseStart - requestStart, // 首字節到達時間responseTime: responseEnd - responseStart, // 響應時間parseDOMTime: loadEventStart - domLoading, // dom解析時間domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // dom加載完成時間timeToInteractive: domInteractive - fetchStart, // 首次可交互時間loadTime: loadEventStart - fetchStart, // 完整的加載時間});}, 3000)});
}
第二步、模擬DOM加載完成延遲inde.html文件
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><div class="content" style="width: 100%; word-wrap: break-word"></div></div><script>// Dom解析完成后,即使依賴的資源沒有加載完成,也會觸發這個事件 document.addEventListener("DOMContentLoaded", function () {let Start = Date.now();while (date.now() - Start < 5000) {}});</script></body>
</html>
第三步、瀏覽器加載和渲染時間上報效果

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

性能指標采集上報

性能指標描述

字段全稱描述
FPFirst Paint(首次繪制)包括了任何用戶自定義的背景繪制,它是首先將像素會知道屏幕的時刻
FCPFirst Content Paint(首次內容繪制)是瀏覽器將第一個DOM元素渲染到屏幕的時間,可能是文本、圖像、SVG等,這其實就是白屏的時間
FMPFirst Meaningfun Paint(首次有意義繪制)頁面有意義的內容渲染時間
LCPLargest ContentFul Paint (最大內容渲染)代表在viewport中最大的頁面元素加載的時間
DCLDomContentLoaded (DOM加載完成)當HTML文檔被完全加載和解析完成之后,DOMContentLoaded時間被觸發,無需等待樣式表,圖像和子框架的完成加載
LonLoad當以來的資源全部加載完畢之后才會觸發。
TTITime to Interactive (可交互時間)用于標記應用以進行視覺渲染并能可靠相應用戶輸入的時間點。
FIDFirst Input Delay(首次輸入延遲)用戶首次和頁面交互(單機鏈接,點擊按鈕等)到頁面響應交互的時間

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

FMP有意義繪制是根據自己給定判斷,如: h1.setAttribute("elementtiming", "meaningful");
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

各個性能指標獲取

觸發用例index.html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>前端監控SDK</title><script src="monitor.js"></script></head><body><div id="container"><p style="color: red">hello</p><div class="content" style="width: 100%; word-wrap: break-word"><button id="CLICKBTN" onclick="clickMe()">Click Me</button></div></div><script>function clickMe() {let start = Date.now();while (Date.now() - start < 1000) {}}setTimeout(() => {let content = document.getElementsByClassName("content")[0];let h1 = document.createElement("h1");h1.innerHTML = "我是這個頁面中最有意義的內容";h1.setAttribute("elementtiming", "meaningful");content.appendChild(h1);}, 2000);</script></body>
</html>
FMP 首次有意義繪制
// 增加一個性能條目的觀察者new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();FMP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['element'] }) // 觀察頁面中有意義的元素
LCP 最大內容渲染
  new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();LCP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['largest-contentful-paint'] }) // 觀察頁面中最大的元素
FID 首次輸入延遲
  new PerformanceObserver((entryList, observer) => {let lastEvent = getLastEvent();let firstInput = entryList.getEntries()[0]; // 渠道第一個條目console.log("FID", firstInput);if (firstInput) {// 開始處理的時間 - 開始點擊的時間,的差值就是處理的延遲let inputDelay = firstInput.processingStart - firstInput.startTime;let duration = firstInput.duration; // 處理時長if (inputDelay > 0 || duration) {tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'firstInputDeay', // 首次輸入延遲inputDelay, // 輸入延遲duration,  // 處理的時間startTime: firstInput.startTime,selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''});}}observer.disconnect(); // 不需要觀察}).observe({ type: 'first-input', buffered: true }) // 用戶第一次交互,點擊頁面

首次點擊效果:
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

FP 首次繪制
 let FP = performance.getEntriesByName('first-paint')[0]
FCP 首次內容繪制
 let FCP = performance.getEntriesByName('first-contentful-paint')[0]
性能指標上報整體代碼
import tracker from '../utils/tracker.js';
import onload from '../utils/onload.js';
import getLastEvent from '../utils/getLastEvent.js';
import getSelector from '../utils/getSelector.js';
export default function timing() {let FMP, LCP;// 增加一個性能條目的觀察者new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();FMP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['element'] }) // 觀察頁面中有意義的元素new PerformanceObserver((entryList, observer) => {let perfEntriens = entryList.getEntries();LCP = perfEntriens[0];observer.disconnect(); // 不需要觀察}).observe({ entryTypes: ['largest-contentful-paint'] }) // 觀察頁面中最大的元素new PerformanceObserver((entryList, observer) => {let lastEvent = getLastEvent();let firstInput = entryList.getEntries()[0]; // 渠道第一個條目console.log("FID", firstInput);if (firstInput) {// 開始處理的時間 - 開始點擊的時間,的差值就是處理的延遲let inputDelay = firstInput.processingStart - firstInput.startTime;let duration = firstInput.duration; // 處理時長if (inputDelay > 0 || duration) {tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'firstInputDeay', // 首次輸入延遲inputDelay, // 輸入延遲duration,  // 處理的時間startTime: firstInput.startTime,selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''});}}observer.disconnect(); // 不需要觀察}).observe({ type: 'first-input', buffered: true }) // 用戶第一次交互,點擊頁面onload(function () {setTimeout(() => {const {fetchStart,connectStart,connectEnd,requestStart,responseStart,responseEnd,domLoading,domInteractive,domContentLoadedEventStart,domContentLoadedEventEnd,loadEventStart,} = performance.timing;// console.log("performance timing:", performance)tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'timing', // 統計每個階段的時間connectTime: connectEnd - connectStart, // 連接時間ttfbTime: responseStart - requestStart, // 首字節到達時間responseTime: responseEnd - responseStart, // 響應時間parseDOMTime: loadEventStart - domLoading, // dom解析時間domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart, // dom加載完成時間timeToInteractive: domInteractive - fetchStart, // 首次可交互時間loadTime: loadEventStart - fetchStart, // 完整的加載時間});let FP = performance.getEntriesByName('first-paint')[0]let FCP = performance.getEntriesByName('first-contentful-paint')[0]// 開始發送性能指標console.log("FP", FP);console.log("FCP", FCP);console.log("FMP", FMP);console.log("LCP", LCP);tracker.send({kind: 'exeprience', // 用戶體驗指標type: 'paint', // 統計每個階段的時間firstPant: FP.startTime, // 首次繪制firstContentFulPant: FCP.startTime,firstMeaningfulPant: FMP.startTime,largestContentFulPaint: LCP.startTime,  // 最大內容繪制});}, 3000)});
}
查看各項性能指標采集上報效果

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

卡頓監聽上報

監聽方案

當任務阻塞主線程達到 100 ms或更長時間時,將引發諸多問題,例如,可交互時間延遲、嚴重不穩定的交互行為 (輕擊、單擊、滾動、滾輪等) 延遲、嚴重不穩定的事件回調延遲、紊亂的動畫和滾動等。我們將任何主 UI 線程連續不間斷繁忙 1000毫秒及以上的時間區間定義為長任務(Long task)。目前,瀏覽器中存在一個 Long Tasks API,借助該 APIPerformanceObserver 的結合使用,我們能夠精準地定位長任務,從而有效實現對卡頓現象的檢測,當然還有心跳檢測,fps幀率檢測卡頓等方法,先從最簡單的實現,借助new PerformanceObserver的回調方法參數entry.duration判斷是否大于100ms,如果大于則可以認為卡頓。

數據結構

{"title":"前端監控","url":"192.168.60.32:8080/", // 卡頓頁面"timestamp":"158654654845", // 時間戳"userAgent":"", // 瀏覽器信息"kind":"experience","type":"longTask","eventType":"mouseover","startTime":"9331","duration":"150","selector":"HTML BODY#container .content",
}

借助Long Tasks API實現卡頓上報

第一步、創建longTask.js文件
import tracker from '../utils/tracker.js';
import getLastEvent from '../utils/getLastEvent.js';
import getSelector from '../utils/getSelector.js';
export default function longTask() {new PerformanceObserver((entryList, observer) => {entryList.getEntries().forEach(entry => {if (entry.duration > 100) {let lastEvent = getLastEvent();var path = lastEvent.path || (function (evt) {var path = [];var el = evt.target;while (el) {path.push(el);el = el.parentElement;}return path;})(lastEvent);console.log("longTask:", getSelector(path))tracker.send({kind: 'experience',type: 'longTask',eventType: lastEvent.type,startTime: entry.startTime, // 開始時間duration: entry.duration,  // 持續時間selector: lastEvent ? getSelector(path) : "",})}})}).observe({ entryTypes: ['longtask'] })
}
第二步、循環模仿卡頓效果
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><div id="container"><div class="content" style="width: 100%; word-wrap: break-word"><button id="button">點擊卡頓</button></div></div><script>document.getElementById("button").addEventListener("click", function () {// 模擬頁面卡頓for (let i = 0; i < 1000000000; i++) {// do nothing}});</script></body>
</html>
第三步、查看卡頓上報效果

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

PV、UV、頁面停留時間采集上報

PV(page view) 是頁面瀏覽量。
UV(Unique visitor)用戶訪問量。
PV 只要訪問一次頁面就算一次。
UV同一天內多次訪問只算一次。

對于前端來說,只要每次進入頁面上報一次 PV 就行,UV 的統計放在服務端來做,主要是分析上報的數據來統計得出 UV。

實現pv和頁面停留時間監聽

第一步、創建pv.js
import tracker from '../utils/tracker.js';
// utils.js 或者直接放在你的文件中
function getPageURL() {return window.location.href;
}function getUUID() {return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {var r = Math.random() * 16 | 0,v = c == 'x' ? r : (r & 0x3 | 0x8);return v.toString(16);});
}export default function pv() {tracker.send({kind: "business",type: "pv",startTime: performance.now(),pageURL: getPageURL(),referrer: document.referrer,uuid: getUUID(),});let startTime = Date.now();window.addEventListener("beforeunload",() => {let stayTime = Date.now() - startTime;tracker.send({kind: "business",type: "stayTime",stayTime,pageURL: getPageURL(),uuid: getUUID(),});},false);
}
第二步、查看上報結果

從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現
從0到1構建前端監控系統:錯誤捕獲、性能采集、用戶體驗全鏈路追蹤實戰指南SDK實現

完結~

覺得內容有用?別忘了點個贊+收藏+關注,三連走一波!你的支持是我持續輸出的動力 💪

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/82570.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/82570.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/82570.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

前端瀏覽器判斷設備類型的方法

前端瀏覽器判斷設備類型的方法 在前端開發中&#xff0c;判斷設備類型&#xff08;如手機、平板、桌面電腦&#xff09;有多種方法&#xff0c;以下是常用的幾種方式&#xff1a; 1. 使用 User Agent 檢測 通過 navigator.userAgent 獲取用戶代理字符串進行判斷&#xff1a;…

MNIST 手寫數字分類

轉自我的個人博客: https://shar-pen.github.io/2025/05/04/torch-distributed-series/1.MNIST/ 基礎的單卡訓練 本筆記本演示了訓練一個卷積神經網絡&#xff08;CNN&#xff09;來對 MNIST 數據集中的手寫數字進行分類的過程。工作流程包括&#xff1a; 數據準備&#xff…

數據庫中的 Segment、Extent、Page、Row 詳解

在關系型數據庫的底層存儲架構中&#xff0c;數據并不是隨意寫入磁盤&#xff0c;而是按照一定的結構分層管理的。理解這些存儲單位對于優化數據庫性能、理解 SQL 執行過程以及排查性能問題都具有重要意義。 我將從宏觀到微觀&#xff0c;依次介紹數據庫存儲中的四個核心概念&…

DAMA車輪圖

DAMA車輪圖是國際數據管理協會&#xff08;DAMA International&#xff09;提出的數據管理知識體系&#xff08;DMBOK&#xff09;的圖形化表示&#xff0c;它以車輪&#xff08;同心圓&#xff09;的形式展示了數據管理的核心領域及其相互關系。以下是基于用戶提供的關鍵詞對D…

《QDebug 2025年4月》

一、Qt Widgets 問題交流 1. 二、Qt Quick 問題交流 1.QML單例動態創建的對象&#xff0c;訪問外部id提示undefined 先定義一個窗口組件&#xff0c;打印外部的id&#xff1a; // MyWindow.qml import QtQuick 2.15 import QtQuick.Window 2.15Window {id: controlwidth: …

JS | 正則 · 常用正則表達式速查表

以下是前端開發中常用的正則表達式速查表&#xff0c;包含驗證規則、用途說明與示例&#xff1a; &#x1f4cc; 常用正則表達式速查表 名稱正則表達式描述 / 用途示例手機號/^1[3-9]\d{9}$/中國大陸手機號13812345678 ?座機號/^0\d{2,3}-?\d{7,8}$/固定電話010-12345678 ?…

系統思考:個人與團隊成長

四年前&#xff0c;我交付的系統思考項目&#xff0c;今天學員的反饋依然深深觸動了我。 我常常感嘆&#xff0c;系統思考不僅僅是一場培訓&#xff0c;更像是一場持續的“修煉”。在這條修煉之路上&#xff0c;最珍貴的&#xff0c;便是有志同道合的伙伴們一路同行&#xff0…

寫屏障和讀屏障的區別是什么?

寫屏障&#xff08;Write Barrier&#xff09;與讀屏障&#xff08;Read Barrier&#xff09;的區別 在計算機科學中&#xff0c;寫屏障和讀屏障是兩種關鍵的內存同步機制&#xff0c;主要用于解決并發編程中的可見性、有序性問題&#xff0c;或在垃圾回收&#xff08;GC&…

ssh -T git@github.com 測試失敗解決方案:修改hosts文件

問題描述 通過SSH方式測試&#xff0c;使用該方法測試連接可能會遇到連接超時、端口占用的情況&#xff0c;原因是因為DNS配置及其解析的問題 ssh -T gitgithub.com我們可以詳細看看建立 ssh 連接的過程中發生了什么&#xff0c;可以使用 ssh -v命令&#xff0c;-v表示 verbo…

大疆無人機搭載樹莓派進行目標旋轉檢測

環境部署 首先是環境創建&#xff0c;創建虛擬環境&#xff0c;名字叫 pengxiang python -m venv pengxiang隨后激活環境 source pengxiang/bin/activate接下來便是依賴包安裝過程了: pip install onnxruntime #推理框架 pip install fastapi uvicorn[standard] #網絡請求…

00 Ansible簡介和安裝

1. Ansible概述與基本概念 1.1. 什么是Ansible&#xff1f; Ansible 是一款用 Python 編寫的開源 IT 自動化工具&#xff0c;主要用于配置管理、軟件部署及高級工作流編排。它能夠簡化應用程序部署、系統更新等操作&#xff0c;并且支持自動化管理大規模的計算機系統。Ansibl…

Linxu實驗五——NFS服務器

一.NFS服務器介紹 NFS服務器&#xff08;Network File System&#xff09;是一種基于網絡的分布式文件系統協議&#xff0c;允許不同操作系統的主機通過網絡共享文件和目錄3。其核心作用在于實現跨平臺的資源透明訪問&#xff0c;例如在Linux和Unix系統之間共享靜態數據&#…

『 測試 』測試基礎

文章目錄 1. 調試與測試的區別2. 開發過程中的需求3. 開發模型3.1 軟件的生命周期3.2 瀑布模型3.2.1 瀑布模型的特點/缺點 3.3 螺旋模型3.3.1 螺旋模型的特點/缺點 3.4 增量模型與迭代模型3.5 敏捷模型3.5.1 Scrum模型3.5.2 敏捷模型中的測試 4 測試模型4.1 V模型4.2 W模型(雙V…

紅外遙控鍵

紅外 本章節旨在讓用戶自定義紅外遙控功能&#xff0c;需要有板載紅外接收的板卡。 12.1. 獲取紅外遙控鍵值 由于不同遙控器廠家定義的按鍵鍵值不一樣&#xff0c;所以配置不通用&#xff0c;需要獲取實際按鍵對應的鍵值。 1 2 3 4 5 6 #設置輸出等級 echo 7 4 1 7> /pr…

同一個虛擬環境中conda和pip安裝的文件存儲位置解析

文章目錄 存儲位置的基本區別conda安裝的包pip安裝的包 看似相同實則不同的機制實際路徑示例這種差異帶來的問題如何檢查包安裝來源最佳實踐建議 總結 存儲位置的基本區別 conda安裝的包 存儲在Anaconda(或Miniconda)目錄下的pkgs和envs子目錄中&#xff1a; ~/anaconda3/en…

機器學習極簡入門:從基礎概念到行業應用

有監督學習&#xff08;supervised learning&#xff09; 讓模型學習的數據包含正確答案&#xff08;標簽&#xff09;的方法&#xff0c;最終模型可以對無標簽的數據進行正確處理和預測&#xff0c;可以分為分類與回歸兩大類 分類問題主要是為了“盡可能分開整個數據而畫線”…

split和join的區別?

split和join是Python中用于處理字符串的兩種方法&#xff0c;它們的主要區別在于功能和使用場景。? split()方法 ?split()方法用于將字符串按照指定的分隔符分割成多個子串&#xff0c;并返回這些子串組成的列表?。如果不指定分隔符&#xff0c;則默認分割所有的空白字符&am…

MySQL從入門到精通(二):Windows和Mac版本MySQL安裝教程

目錄 MySQL安裝流程 &#xff08;一&#xff09;、進入MySQL官網 &#xff08;二&#xff09;、點擊下載&#xff08;Download&#xff09; &#xff08;三&#xff09;、Windows和Mac版本下載 下載Windows版本 下載Mac版本 &#xff08;四&#xff09;、驗證并啟動MySQL …

LeetCode 解題思路 45(分割等和子集、最長有效括號)

解題思路&#xff1a; dp 數組的含義&#xff1a; 在數組中是否存在一個子集&#xff0c;其和為 i。遞推公式&#xff1a; dp[i] | dp[i - num]。dp 數組初始化&#xff1a; dp[0] true。遍歷順序&#xff1a; 從大到小去遍歷&#xff0c;從 i target 開始&#xff0c;直到 …

電影感戶外啞光人像自拍攝影Lr調色預設,手機濾鏡PS+Lightroom預設下載!

調色詳情 電影感戶外啞光人像自拍攝影 Lr 調色&#xff0c;是借助 Lightroom 軟件&#xff0c;針對戶外環境下拍攝的人像自拍進行后期處理。旨在模擬電影畫面的氛圍與質感&#xff0c;通過調色賦予照片獨特的藝術氣息。強調打造啞光效果&#xff0c;使畫面色彩不過于濃烈刺眼&a…