【前端技術(shù)】前端實(shí)現(xiàn)監(jiān)控 SDK 的全面解析(二)
三、功能拆分
(一)初始化
初始化階段需要獲取用戶傳遞過來的相關(guān)參數(shù),接著調(diào)用初始化函數(shù)。在這個(gè)初始化函數(shù)里,能夠注入一些監(jiān)聽事件,以此來達(dá)成數(shù)據(jù)統(tǒng)計(jì)的功能。以下是具體的代碼實(shí)現(xiàn)示例及解析:
javascript
// 初始化配置
function init(options) {
// ------- 加載配置 ----------
loadConfig(options);
}
// 加載配置
export function loadConfig(options) {
const {
appId, // 系統(tǒng)id
userId, // 用戶id
reportUrl, // 后端url
autoTracker, // 自動(dòng)埋點(diǎn)
delay, // 延遲和合并上報(bào)的功能
hashPage, // 是否hash錄有
errorReport // 是否開啟錯(cuò)誤監(jiān)控
} = options;
// --------- appId ----------------
if (appId) {
window['_monitor_app_id_'] = appId;
}
// --------- userId ----------------
if (userId) {
window['_monitor_user_id_'] = userId;
}
// --------- 服務(wù)端地址 ----------------
if (reportUrl) {
window['_monitor_report_url_'] = reportUrl;
}
// -------- 合并上報(bào)的間隔 ------------
if (delay) {
window['_monitor_delay_'] = delay;
}
// --------- 是否開啟錯(cuò)誤監(jiān)控 ------------
if (errorReport) {
errorTrackerReport();
}
// --------- 是否開啟無痕埋點(diǎn) ----------
if (autoTracker) {
autoTrackerReport();
}
// ----------- 路由監(jiān)聽 --------------
if (hashPage) {
hashPageTrackerReport(); // hash路由上報(bào)
} else {
historyPageTrackerReport(); // history路由上報(bào)
}
}
(二)錯(cuò)誤監(jiān)控
前端錯(cuò)誤類型多樣,不同類型的錯(cuò)誤需要采用不同的捕獲方式來實(shí)現(xiàn)全面監(jiān)控。
1. 語法錯(cuò)誤
這類錯(cuò)誤通常在開發(fā)階段就能被發(fā)現(xiàn),例如拼寫錯(cuò)誤、符號(hào)使用錯(cuò)誤等情況。語法錯(cuò)誤沒辦法通過 try{}catch{} 進(jìn)行捕獲,因?yàn)檎T陂_發(fā)流程中就會(huì)被排查出來,基本不會(huì)發(fā)布到線上環(huán)境。
2. 同步錯(cuò)誤
在 JavaScript 同步執(zhí)行過程中產(chǎn)生的錯(cuò)誤屬于同步錯(cuò)誤,像變量未定義這類情況,此類錯(cuò)誤是可以被 try-catch 語句捕獲到的。
3. 異步錯(cuò)誤
在諸如 setTimeout 等函數(shù)執(zhí)行中出現(xiàn)的錯(cuò)誤就是異步錯(cuò)誤,它無法被 try-catch 捕獲,但可以利用 Window.onerror 來進(jìn)行捕獲處理,相較而言這種方式要比 try-catch 方便許多。
4. Promise 錯(cuò)誤
在 Promise 中,如果使用 catch 語句是可以捕獲到異步錯(cuò)誤的,然而要是沒寫 catch 的話,在 Window.onerror 里是沒辦法捕獲到這類錯(cuò)誤的。對(duì)此,可以在全局添加 unhandledrejection 監(jiān)聽來捕獲那些沒被捕獲到的 Promise 錯(cuò)誤。
5. 資源加載錯(cuò)誤
指的是一些資源文件獲取失敗的情況,一般通過 Window.addEventListener 來進(jìn)行捕獲操作。
綜合來看,SDK 監(jiān)控錯(cuò)誤主要圍繞以上這些類型來實(shí)現(xiàn),try-catch 用于在可預(yù)見的情形下監(jiān)控特定錯(cuò)誤,Window.onerror 主要負(fù)責(zé)捕獲那些預(yù)料之外的錯(cuò)誤(比如異步錯(cuò)誤),但對(duì)于 Promise 錯(cuò)誤和網(wǎng)絡(luò)錯(cuò)誤無法單純依靠它們捕獲,所以需要借助 Window.unhandledrejection 監(jiān)聽捕獲 Promise 錯(cuò)誤,通過 error 監(jiān)聽捕獲資源加載錯(cuò)誤,以此達(dá)成對(duì)各類型錯(cuò)誤的全面覆蓋。
(三)用戶埋點(diǎn)統(tǒng)計(jì)
埋點(diǎn)是用于監(jiān)控用戶在應(yīng)用上的各種動(dòng)作表現(xiàn)的一種手段。
1. 手動(dòng)埋點(diǎn)
需要手動(dòng)在代碼中添加相關(guān)的埋點(diǎn)代碼,比如當(dāng)用戶點(diǎn)擊某個(gè)按鈕或者提交一個(gè)表單時(shí),就在按鈕點(diǎn)擊事件以及提交事件中添加對(duì)應(yīng)的埋點(diǎn)代碼。其優(yōu)點(diǎn)在于可控性較強(qiáng),能夠根據(jù)需求自定義上報(bào)具體的數(shù)據(jù)內(nèi)容;但缺點(diǎn)也很明顯,就是對(duì)業(yè)務(wù)代碼的侵入性較大,如果需要在很多地方進(jìn)行埋點(diǎn)操作,那就得逐個(gè)去添加代碼了。
2. 自動(dòng)埋點(diǎn)
自動(dòng)埋點(diǎn)很好地解決了手動(dòng)埋點(diǎn)的缺點(diǎn),實(shí)現(xiàn)了無需侵入業(yè)務(wù)代碼就能在應(yīng)用里添加埋點(diǎn)監(jiān)控的功能。不過它也存在不足,只能上報(bào)基本的行為交互信息,沒辦法上報(bào)自定義的數(shù)據(jù),而且只要頁面中有點(diǎn)擊操作,就會(huì)向服務(wù)器上報(bào),這可能導(dǎo)致上報(bào)次數(shù)過多,給服務(wù)器帶來較大壓力。同時(shí)需要注意,如果在 click 事件中阻止了冒泡行為,自動(dòng)埋點(diǎn)是無法捕獲到的,這種情況下就需要進(jìn)行手動(dòng)埋點(diǎn)上報(bào),以確保上報(bào)的全面覆蓋。
(四)PV 統(tǒng)計(jì)
PV 即頁面瀏覽量,表示頁面被訪問的次數(shù)。對(duì)于非 SPA 頁面,只需通過監(jiān)聽 onload 事件就能統(tǒng)計(jì)頁面的 PV 了。但在 SPA 頁面中,路由的切換主要依靠前端來實(shí)現(xiàn),而且單頁面切換又分為 hash 路由和 history 路由,這兩種路由的實(shí)現(xiàn)原理不一樣,所以要分別針對(duì)它們實(shí)現(xiàn)不同的數(shù)據(jù)采集方式。
1. history 路由
history 路由是依賴全局對(duì)象 history 來實(shí)現(xiàn)的,它包含諸如 history.back()(返回上一頁,對(duì)應(yīng)瀏覽器回退操作)、history.forward()(前進(jìn)一頁,即瀏覽器前進(jìn)操作)、history.go()(跳轉(zhuǎn)歷史中某一頁)、history.pushState()(添加新記錄)、history.replaceState()(修改當(dāng)前記錄)等方法。其中 pushState 和 replaceState 這兩個(gè)方法不能被 popstate 監(jiān)聽到,因此需要對(duì)這兩個(gè)方法進(jìn)行重寫,并添加自定義事件監(jiān)聽來實(shí)現(xiàn)數(shù)據(jù)采集,以下是具體的代碼實(shí)現(xiàn)示例及解析:
javascript
import { lazyReport } from './report';
// history路由監(jiān)聽
export function historyPageTrackerReport() {
let beforeTime = Date.now(); // 進(jìn)入頁面的時(shí)間
let beforePage = ''; // 上一個(gè)頁面
// 獲取在某個(gè)頁面的停留時(shí)間
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime;
beforeTime = curTime;
return stayTime;
}
// 重寫pushState和replaceState方法
const createHistoryEvent = function (name) {
// 拿到原來的處理方法
const origin = window.history[name];
return function(event) {
let res = origin.apply(this, arguments);
let e = new Event(name);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
// history.replaceState
window.addEventListener('replaceState', function () {
listener()
});
window.history.pushState = createHistoryEvent('pushState');
window.history.replaceState = createHistoryEvent('replaceState');
function listener() {
const stayTime = getStayTime(); // 停留時(shí)間
const currentPage = window.location.href; // 頁面路徑
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// 頁面load監(jiān)聽
window.addEventListener('load', function () {
// beforePage = location.href;
listener()
});
// unload監(jiān)聽
window.addEventListener('unload', function () {
listener()
});
// history.go()、history.back()、history.forward() 監(jiān)聽
window.addEventListener('popstate', function () {
listener()
});
}
2. hash 路由
在 hash 路由中,url 里的 hash 值發(fā)生變化時(shí)會(huì)觸發(fā) hashChange 的監(jiān)聽,所以只需在全局添加一個(gè)監(jiān)聽函數(shù),然后在這個(gè)函數(shù)里實(shí)現(xiàn)數(shù)據(jù)采集上報(bào)就可以了。不過在 React 和 Vue 等框架中,hash 路由的跳轉(zhuǎn)有時(shí)候是通過 pushState 實(shí)現(xiàn)的,所以還需要加上對(duì) pushState 的監(jiān)聽,以下是具體代碼示例及解析:
javascript
// hash路由監(jiān)聽
export function hashPageTrackerReport() {
let beforeTime = Date.now(); // 進(jìn)入頁面的時(shí)間
let beforePage = ''; // 上一個(gè)頁面
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime; //當(dāng)前時(shí)間 - 進(jìn)入時(shí)間
beforeTime = curTime;
return stayTime;
}
function listener() {
const stayTime = getStayTime();
const currentPage = window.location.href;
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// hash路由監(jiān)聽
window.addEventListener('hashchange', function () {
listener()
});
// 頁面load監(jiān)聽
window.addEventListener('load', function () {
listener()
});
const createHistoryEvent = function (name) {
const origin = window.history[name];
return function(event) {
//自定義事件
let res = origin.apply(this, arguments);
let e = new Event(name);
e.arguments = arguments;
window.dispatchEvent(e);
return res;
};
};
window.history.pushState = createHistoryEvent('pushState');
// history.pushState
window.addEventListener('pushState', function () {
listener()
});
}
(五)UV 統(tǒng)計(jì)
UV 統(tǒng)計(jì)相對(duì)來說較為簡(jiǎn)單,只需在 SDK 初始化時(shí)上報(bào)一條消息即可完成相關(guān)數(shù)據(jù)的收集。
四、數(shù)據(jù)上報(bào)方式
(一)xhr 接口請(qǐng)求
采用接口請(qǐng)求的方式來上報(bào)數(shù)據(jù),其原理和其他業(yè)務(wù)請(qǐng)求類似,只不過傳遞的數(shù)據(jù)是埋點(diǎn)相關(guān)的數(shù)據(jù)。但這種方式存在一些問題,一方面,通常公司里處理埋點(diǎn)的服務(wù)器和處理業(yè)務(wù)邏輯的服務(wù)器并非同一臺(tái),所以往往需要手動(dòng)去解決跨域問題;另一方面,如果在上報(bào)過程中出現(xiàn)頁面刷新或者重新打開頁面的情況,很可能會(huì)造成埋點(diǎn)數(shù)據(jù)的缺失,因此傳統(tǒng)的 xhr 接口請(qǐng)求方式在適應(yīng)埋點(diǎn)需求方面存在一定局限性。
(二)img 標(biāo)簽
利用 img 標(biāo)簽上報(bào)數(shù)據(jù),是將埋點(diǎn)數(shù)據(jù)偽裝成圖片 url 的請(qǐng)求形式,這樣做的好處是能夠避免跨域問題。然而,瀏覽器對(duì) url 的長(zhǎng)度是有限制的,所以這種方式不太適合大數(shù)據(jù)量的上報(bào),而且同樣存在刷新或重新打開頁面時(shí)數(shù)據(jù)丟失的問題。
(三)sendBeacon
這種上報(bào)方式不會(huì)出現(xiàn)跨域問題,也不存在刷新或重新打開頁面導(dǎo)致的數(shù)據(jù)丟失情況,但它有兼容性方面的問題。在日常開發(fā)中,通常會(huì)采用 sendBeacon 上報(bào)和 img 標(biāo)簽上報(bào)相結(jié)合的方式,以此來兼顧各種情況,確保數(shù)據(jù)上報(bào)的有效性和穩(wěn)定性。以下是具體的上報(bào)函數(shù)代碼示例:
javascript
// 上報(bào)
export function report(data) {
const url = window['_monitor_report_url_'];
// ------- fetch方式上報(bào) -------
// 跨域問題
// fetch(url, {
// method: 'POST',
// body: JSON.stringify(data),
// headers: {
// 'Content-Type': 'application/json',
// },
// }).then(res => {
// console.log(res);
// }).catch(err => {
// console.error(err);
// })
// ------- navigator/img方式上報(bào) -------
// 不會(huì)有跨域問題
if (navigator.sendBeacon) { // 支持sendBeacon的瀏覽器
navigator.sendBeacon(url, JSON.stringify(data));
} else { // 不支持sendBeacon的瀏覽器
let oImage = new Image();
oImage.src = `${url}?logs=${data}`;
}
clearCache();
}
通過上述對(duì)前端實(shí)現(xiàn)監(jiān)控 SDK 的介紹,涵蓋了能拆分到數(shù)據(jù)上報(bào)環(huán)節(jié),旨在幫助開發(fā)人員更好地構(gòu)建和運(yùn)用監(jiān)控系統(tǒng),以保障前端應(yīng)用的穩(wěn)定運(yùn)行以及對(duì)用戶行為等方面的有效洞察。