前端埋點(diǎn)與監(jiān)控最佳實(shí)踐:從基礎(chǔ)到全流程實(shí)現(xiàn)
大綱
我們會(huì)從以下三個(gè)方向來(lái)講解埋點(diǎn)與監(jiān)控的知識(shí):
- 什么是埋點(diǎn)?什么是監(jiān)控?
- JS 中實(shí)現(xiàn)監(jiān)控的核心方案
- 寫一個(gè)“相對(duì)”完整的監(jiān)控實(shí)例
一、什么是埋點(diǎn)?什么是監(jiān)控?
在日常溝通中,我們經(jīng)常會(huì)把【埋點(diǎn)】和【監(jiān)控】放到一起說(shuō),但是它們?cè)诒举|(zhì)上是有一定的區(qū)別的:
1. 埋點(diǎn)
埋點(diǎn)主要用于收集用戶行為數(shù)據(jù)。在日常開發(fā)中,我們會(huì)通過 在前端代碼中插入代碼或腳本的方式 來(lái)實(shí)現(xiàn)埋點(diǎn)功能。
埋點(diǎn)的主要作用就是:捕獲特定用戶行為(如點(diǎn)擊、瀏覽、提交表單、頁(yè)面跳轉(zhuǎn)等)以及關(guān)鍵業(yè)務(wù)數(shù)據(jù)(如下單金額、商品類別等)
在日常開發(fā)中,埋點(diǎn)的實(shí)現(xiàn)方案大致可以分為以下三大類:
- 手動(dòng)埋點(diǎn):在代碼中手動(dòng)加入記錄代碼來(lái)捕獲特定事件。
- 自動(dòng)埋點(diǎn):利用 DOM 事件代理等技術(shù)來(lái)捕獲頁(yè)面上所有事件,從而減少手動(dòng)配置。
- 可視化埋點(diǎn):通過工具界面標(biāo)記需要采集的元素和事件,可以不用手寫代碼。
2. 監(jiān)控
而監(jiān)控則主要關(guān)注 系統(tǒng)的性能和穩(wěn)定性。在日常開發(fā)中,我們會(huì)通過 采集頁(yè)面加載時(shí)間、資源請(qǐng)求、錯(cuò)誤日志等數(shù)據(jù) 的方式來(lái)實(shí)現(xiàn)前端監(jiān)控。
監(jiān)控的主要作用就是:及時(shí)發(fā)現(xiàn)并定位頁(yè)面性能瓶頸或代碼異常,目的是為了保障系統(tǒng)不出 bug
在日常開發(fā)中,監(jiān)控一般需要完成以下三大部分:
- 性能監(jiān)控:如:首屏加載時(shí)間、頁(yè)面交互耗時(shí)、資源加載耗時(shí)等。
- 錯(cuò)誤監(jiān)控:捕獲 JavaScript 錯(cuò)誤、網(wǎng)絡(luò)請(qǐng)求失敗、資源加載異常等。
- 用戶體驗(yàn)監(jiān)控:收集白屏、卡頓等影響用戶體驗(yàn)的問題等。
區(qū)別總結(jié)
維度 | 前端埋點(diǎn) | 前端監(jiān)控 |
目標(biāo) | 捕獲用戶行為數(shù)據(jù) | 監(jiān)控系統(tǒng)性能、錯(cuò)誤、穩(wěn)定性 |
數(shù)據(jù)類型 | 用戶點(diǎn)擊、表單提交、頁(yè)面跳轉(zhuǎn)等 | 頁(yè)面加載時(shí)間、錯(cuò)誤日志、卡頓情況等 |
實(shí)現(xiàn)方式 | 手動(dòng)埋點(diǎn)、自動(dòng)埋點(diǎn)、可視化埋點(diǎn) | 錯(cuò)誤捕獲、性能指標(biāo)采集 |
核心關(guān)注點(diǎn) | 用戶行為、業(yè)務(wù)數(shù)據(jù) | 系統(tǒng)Bug、性能優(yōu)化 |
二、JS 中實(shí)現(xiàn)監(jiān)控的核心方案
根據(jù)上面所說(shuō),我們知道埋點(diǎn)和監(jiān)控的目的存在不同,但是它們的思路確是有很多一致性的,其核心都是:獲取關(guān)鍵的數(shù)據(jù),發(fā)送(上報(bào))給服務(wù)端,依據(jù)數(shù)據(jù)來(lái)解決其不同的目的。
所以,無(wú)論是埋點(diǎn)也好,還是監(jiān)控也罷,我們都需要 獲取關(guān)鍵位置數(shù)據(jù)。
1. 跟蹤用戶事件(點(diǎn)擊、滾動(dòng)等)
定義通用跟蹤函數(shù)(后續(xù)事件會(huì)通過該函數(shù)完成上報(bào)):trackEvent 函數(shù)接收事件類型和事件詳情,并上報(bào)到服務(wù)端。
// 用于記錄或發(fā)送跟蹤數(shù)據(jù)到服務(wù)器的函數(shù)
function trackEvent(eventType, details) {
console.log(`Event: ${eventType}`, details); // 在控制臺(tái)打印事件類型和詳情
// 上報(bào)到服務(wù)端。
fetch('/測(cè)試接口地址', { method: 'POST', body: JSON.stringify({ eventType, details }) });
}
捕獲按鈕點(diǎn)擊事件:獲取 id 為 myButton 的按鈕,并在其 click 事件上添加監(jiān)聽器。在按鈕被點(diǎn)擊時(shí)調(diào)用 trackEvent 函數(shù),記錄點(diǎn)擊事件的類型(button_click)、按鈕 ID 和時(shí)間戳。
// 跟蹤按鈕點(diǎn)擊事件
const button = document.getElementById('myButton'); // 獲取按鈕元素
button.addEventListener('click', function () {
trackEvent('button_click', { buttonId: 'myButton', timestamp: Date.now() }); // 記錄點(diǎn)擊事件并添加按鈕ID和時(shí)間戳
});
捕獲頁(yè)面滾動(dòng)事件:在全局 scroll 事件上添加監(jiān)聽器,每當(dāng)頁(yè)面發(fā)生滾動(dòng)時(shí)調(diào)用 trackEvent 函數(shù),記錄滾動(dòng)事件的類型(page_scroll)、頁(yè)面垂直滾動(dòng)距離(scrollY)和時(shí)間戳。
// 跟蹤頁(yè)面滾動(dòng)事件
window.addEventListener('scroll', function () {
trackEvent('page_scroll', { scrollY: window.scrollY, timestamp: Date.now() }); // 記錄滾動(dòng)事件并添加滾動(dòng)位置和時(shí)間戳
});
2. 完成性能監(jiān)控指標(biāo)
我們可以使用 PerformanceAPI,來(lái)檢測(cè)某些操作需要多長(zhǎng)時(shí)間。如:頁(yè)面加載時(shí)間和 API 調(diào)用耗時(shí)的監(jiān)控:
頁(yè)面加載時(shí)間監(jiān)控:通過 window.addEventListener('load') 監(jiān)聽頁(yè)面加載完成的事件,在頁(yè)面完全加載后獲取當(dāng)前時(shí)間(使用 performance.now()),計(jì)算出頁(yè)面加載的總耗時(shí)(從頁(yè)面初始化到加載完成的時(shí)間),并通過 trackEvent 函數(shù)將事件類型、耗時(shí)數(shù)據(jù)等記錄下來(lái)。
// 測(cè)量頁(yè)面加載時(shí)間
window.addEventListener('load', function () {
const pageLoadTime = performance.now(); // 獲取頁(yè)面加載完成后的時(shí)間(毫秒)
trackEvent('page_load', { duration: pageLoadTime }); // 記錄頁(yè)面加載事件,并包含加載耗時(shí)數(shù)據(jù)
});
API 調(diào)用耗時(shí)監(jiān)控:在 measureApiCallPerformance 函數(shù)中使用 performance.now() 獲取調(diào)用 API 前的開始時(shí)間,通過 fetch 方法發(fā)起網(wǎng)絡(luò)請(qǐng)求并在響應(yīng)返回后再次獲取時(shí)間差,計(jì)算 API 請(qǐng)求的總耗時(shí)。將 API 耗時(shí)和接口地址等信息通過 trackEvent 函數(shù)記錄下來(lái)。
// 測(cè)量 API 調(diào)用的耗時(shí)
function measureApiCallPerformance() {
const start = performance.now(); // 記錄 API 調(diào)用的開始時(shí)間
fetch('https://api.sunday.com/data')
.then(response => response.json())
.then(data => {
const duration = performance.now() - start; // 計(jì)算 API 調(diào)用的耗時(shí)
trackEvent('api_call', { duration: duration, endpoint: 'https://api.sunday.com/data' }); // 記錄 API 調(diào)用事件,并包含耗時(shí)和接口地址
});
}
3. 進(jìn)行錯(cuò)誤追蹤監(jiān)聽
我們可以利用 window.onerror 回調(diào)或者直接使用一些庫(kù)(如:Sentry)完成錯(cuò)誤監(jiān)聽:
基礎(chǔ)錯(cuò)誤跟蹤:通過 window.onerror 捕獲全局 JavaScript 錯(cuò)誤。當(dāng)錯(cuò)誤發(fā)生時(shí),window.onerror 會(huì)自動(dòng)獲取錯(cuò)誤的詳細(xì)信息(如錯(cuò)誤信息、文件、行號(hào)、列號(hào)及堆棧信息),并將這些信息通過 trackEvent 函數(shù)發(fā)送到后臺(tái),用于后續(xù)的錯(cuò)誤分析和排查。
// 使用 window.onerror 實(shí)現(xiàn)基礎(chǔ)的錯(cuò)誤跟蹤
window.onerror = function (message, source, lineno, colno, error) {
// 捕獲 JavaScript 錯(cuò)誤信息,并通過 trackEvent 函數(shù)記錄
trackEvent('js_error', {
message: message, // 錯(cuò)誤信息
source: source, // 錯(cuò)誤發(fā)生的文件
lineno: lineno, // 錯(cuò)誤所在的行號(hào)
colno: colno, // 錯(cuò)誤所在的列號(hào)
error: error ? error.stack : '' // 錯(cuò)誤的堆棧信息(如果有)
});
};
第三方錯(cuò)誤跟蹤服務(wù)(Sentry):Sentry 是一個(gè)常用的錯(cuò)誤監(jiān)控服務(wù)。通過 dsn 配置唯一的項(xiàng)目標(biāo)識(shí),之后可以使用 Sentry.captureException 方法捕獲并上報(bào)自定義錯(cuò)誤。這種方式適合用于捕獲更多類型的異常并進(jìn)行詳細(xì)的錯(cuò)誤分析。
// 使用第三方服務(wù) Sentry 進(jìn)行錯(cuò)誤跟蹤
Sentry.init({ dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0' }); // 初始化 Sentry
Sentry.captureException(new Error('在這里描述錯(cuò)誤內(nèi)容')); // 捕獲并上報(bào)自定義錯(cuò)誤
4. 自定義的埋點(diǎn)上報(bào)
有時(shí)候我們可能還需要進(jìn)行一些特別要求的數(shù)據(jù)上報(bào),比如:跟蹤用戶在頁(yè)面特定區(qū)域的停留時(shí)間,一共分成三步來(lái)做:
- 當(dāng)用戶的鼠標(biāo)進(jìn)入指定區(qū)域(ID 為 sectionId)時(shí),通過 mouseenter 事件記錄進(jìn)入的時(shí)間戳 sectionStartTime。
- 當(dāng)用戶的鼠標(biāo)離開該區(qū)域時(shí),通過 mouseleave 事件獲取當(dāng)前時(shí)間,計(jì)算用戶在該區(qū)域的停留時(shí)長(zhǎng) timeSpent。
- 將停留時(shí)間和區(qū)域標(biāo)識(shí)一起通過 trackEvent 函數(shù)發(fā)送到分析系統(tǒng),方便后續(xù)分析用戶在頁(yè)面不同區(qū)域的停留時(shí)長(zhǎng)
// 跟蹤用戶在頁(yè)面特定區(qū)域的停留時(shí)間
let sectionStartTime = 0; // 記錄進(jìn)入?yún)^(qū)域的時(shí)間
const sectionElement = document.getElementById('sectionId'); // 獲取目標(biāo)區(qū)域的 DOM 元素
// 當(dāng)用戶鼠標(biāo)進(jìn)入該區(qū)域時(shí)觸發(fā)
sectionElement.addEventListener('mouseenter', function () {
sectionStartTime = Date.now(); // 記錄進(jìn)入?yún)^(qū)域的時(shí)間戳
});
// 當(dāng)用戶鼠標(biāo)離開該區(qū)域時(shí)觸發(fā)
sectionElement.addEventListener('mouseleave', function () {
const timeSpent = Date.now() - sectionStartTime; // 計(jì)算停留時(shí)間
trackEvent('time_spent', { section: 'sectionId', duration: timeSpent }); // 上報(bào)停留時(shí)間和區(qū)域標(biāo)識(shí)
});
5. 局部小總結(jié)
通過以上的幾個(gè)案例,我們可以再次明確:監(jiān)控核心就是獲取關(guān)鍵的數(shù)據(jù),發(fā)送(上報(bào))給服務(wù)端
我們只需要 依照自己的需求,找到對(duì)應(yīng)的 事件節(jié)點(diǎn),獲取 需要上報(bào)的數(shù)據(jù),通過接口傳遞給服務(wù)端即可。
PS:這里需要注意的是 上報(bào)的方式分為:【統(tǒng)一上報(bào)】和 【實(shí)時(shí)上報(bào)】 兩大類,這里不去細(xì)說(shuō)。
因此,想要完成監(jiān)控,那么就需要更加深入的了解關(guān)鍵事件節(jié)點(diǎn),如:瀏覽器窗口事件、鼠標(biāo)事件、鍵盤事件、表單事件 甚至是 DOM 是否可見
三、一個(gè)完整的表單監(jiān)控示例
那么接下來(lái)咱們就完成一個(gè)表單監(jiān)控示例。他可以監(jiān)控到 瀏覽量、按鈕點(diǎn)擊量和表單提交量 等
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>表單行為跟蹤示例</title>
</head>
<body>
<!-- 示例表單 -->
<h1>用戶注冊(cè)表單</h1>
<form id="registrationForm">
<label for="username">用戶名:</label>
<input type="text" id="username" name="username" required />
<br /><br />
<label for="email">郵箱:</label>
<input type="email" id="email" name="email" required />
<br /><br />
<label for="password">密碼:</label>
<input type="password" id="password" name="password" required />
<br /><br />
<button type="button" id="submitButton">注冊(cè)</button>
</form>
<script>
// 通用跟蹤函數(shù):用于記錄事件并發(fā)送到服務(wù)器
function trackEvent(eventType, details) {
console.log(`Event: ${eventType}`, details)
// 將數(shù)據(jù)發(fā)送到分析服務(wù)
fetch('/請(qǐng)求路徑', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ eventType, details })
})
}
// 1. 監(jiān)控頁(yè)面瀏覽量
window.addEventListener('load', function () {
trackEvent('page_view', {
url: window.location.href,
timestamp: Date.now()
})
})
// 2. 監(jiān)控輸入字段聚焦事件
const inputFields = document.querySelectorAll('#registrationForm input')
inputFields.forEach((field) => {
field.addEventListener('focus', function () {
trackEvent('input_focus', {
fieldName: field.name,
timestamp: Date.now()
})
})
})
// 3. 監(jiān)控按鈕點(diǎn)擊量
const submitButton = document.getElementById('submitButton')
submitButton.addEventListener('click', function () {
trackEvent('button_click', {
buttonId: 'submitButton',
timestamp: Date.now()
})
// 模擬提交表單,調(diào)用表單提交處理邏輯
handleSubmit()
})
// 4. 監(jiān)控表單提交量
const form = document.getElementById('registrationForm')
function handleSubmit() {
// 驗(yàn)證表單是否有效(如果需要可以增加更多驗(yàn)證邏輯)
if (form.checkValidity()) {
trackEvent('form_submit', {
formId: 'registrationForm',
formData: {
username: form.username.value,
email: form.email.value,
password: form.password.value // 注意:實(shí)際場(chǎng)景中避免記錄敏感信息
},
timestamp: Date.now()
})
// 模擬發(fā)送表單數(shù)據(jù)到服務(wù)器
fetch('/請(qǐng)求路徑', {
method: 'POST',
body: new FormData(form)
})
.then((response) => response.json())
.then((data) => {
console.log('Form submitted successfully', data)
})
} else {
alert('請(qǐng)?zhí)顚懲暾韱?)
}
}
</script>
</body>
</html>
測(cè)試執(zhí)行結(jié)果如下:
圖片