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