前端實(shí)現(xiàn)監(jiān)控SDK的實(shí)戰(zhàn)指南
監(jiān)控內(nèi)容
- 錯(cuò)誤監(jiān)控
如:瀏覽器兼容問題、代碼bug、后端接口掛掉等問題 - 行為日志
如常用的電商app,通過分析用戶瀏覽時(shí)間較長(zhǎng)頁(yè)面有哪些、常點(diǎn)擊按鈕有哪些等行為,通過分析用戶的行為定制不同策略引導(dǎo)用戶進(jìn)行購(gòu)買 - PV/UV統(tǒng)計(jì)
如:統(tǒng)計(jì)用戶訪問頁(yè)面次數(shù),每天有多少用戶訪問系統(tǒng)
圍繞以上三點(diǎn)進(jìn)行設(shè)計(jì),主要流程如下:

數(shù)據(jù)采集:采集前端監(jiān)控的相關(guān)數(shù)據(jù),包括PV/UV、用戶行為、報(bào)錯(cuò)信息。
日志上報(bào):將采集到的數(shù)據(jù)發(fā)送給服務(wù)端。
日志查詢:在后臺(tái)頁(yè)面中查詢采集到的數(shù)據(jù),進(jìn)行系統(tǒng)分析。
功能拆分

初始化
獲取用戶傳遞的參數(shù),調(diào)用初始化函數(shù),在初始化函數(shù)中可以注入一些監(jiān)聽事件來實(shí)現(xiàn)數(shù)據(jù)統(tǒng)計(jì)的功能。
/**
* 初始化配置
* @param {*} options 配置信息
*/
function init(options) {
// ------- 加載配置 ----------
loadConfig(options);
}/**
* 加載配置
* @param {*} 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)控
前端是直接和用戶打交道的,頁(yè)面報(bào)錯(cuò)是特別影響用戶體驗(yàn)的,即使在測(cè)試充分上線后也會(huì)因用戶操作行為和操作環(huán)境出現(xiàn)各種錯(cuò)誤,所以不光是后端需要加報(bào)警監(jiān)控,前端的錯(cuò)誤監(jiān)控也很重要。
錯(cuò)誤類型
- 語(yǔ)法錯(cuò)誤
語(yǔ)法錯(cuò)誤一般在開發(fā)階段就可以發(fā)現(xiàn),如拼寫錯(cuò)誤、符號(hào)錯(cuò)誤等,語(yǔ)法錯(cuò)誤無法被try{}catch{}捕獲,因?yàn)樵陂_發(fā)階段就能發(fā)現(xiàn),也不會(huì)發(fā)布到線上。try { const name = 'wsjyq; console.log(name); } catch (error) { console.log('--- 語(yǔ)法錯(cuò)誤 --') } - 同步錯(cuò)誤
指在js同步執(zhí)行過程中發(fā)生的錯(cuò)誤,如變量未定義,可被try-catch捕獲try { const name = 'wsjy'; console.log(nam); } catch (error) { // console.log('--- 同步錯(cuò)誤 ---- ') } - 異步錯(cuò)誤
指在setTimeout等函數(shù)中發(fā)生的錯(cuò)誤,無法被try-catch捕獲異步錯(cuò)誤也可以用Window.onerror捕獲處理,比try-catch方便很多
{/* 異步錯(cuò)誤 */} <button style={{ marginRight: 20 }} onClick={() => { // 異步錯(cuò)誤無法被trycatch捕獲 try { setTimeout(() => { let name = 'wsjyq'; name.map(); }) } catch (error) { console.log('--- 異步錯(cuò)誤---- ') } }} >異步錯(cuò)誤</button>// ----- 異步錯(cuò)誤捕獲 -------- /** * @param {String} msg 錯(cuò)誤描述 * @param {String} url 報(bào)錯(cuò)文件 * @param {Number} row 行號(hào) * @param {Number} col 列號(hào) * @param {Object} error 錯(cuò)誤Error對(duì)象 */ window.onerror = function (msg, url, row, col, error) { console.log('---- 捕獲到j(luò)s執(zhí)行錯(cuò)誤 ----'); console.log(msg); console.log(url); console.log(row); console.log(col); console.log(error); return true; }; Promise錯(cuò)誤
在Promise中使用catch可以捕獲到異步錯(cuò)誤,但如果沒寫catch的話在Window.onerror中是捕獲不到錯(cuò)誤的,或者可以在全局加上unhandledrejection監(jiān)聽沒被捕獲到的Promise錯(cuò)誤。{/* promise錯(cuò)誤 */} <button style={{ marginRight: 20 }} onClick={() => { Promise.reject('promise error').catch(err => { console.log('----- promise error -----'); }); Promise.reject('promise error'); }} >promise錯(cuò)誤</button>// ------ promise error ----- window.addEventListener('unhandledrejection', (error) => { console.log('---- 捕獲到promise error ---') }, true);資源加載錯(cuò)誤
指一些資源文件獲取失敗,一般用Window.addEventListener來捕獲。{/* resource錯(cuò)誤 */} <button style={{ marginRight: 20 }} onClick={() => { setShow(true); }} >resource錯(cuò)誤</button> { show && <img src="localhost:8000/images/test.png" /> // 資源不存在 } </div>// ------ resource error ---- window.addEventListener('error', (error) => { console.log('---- 捕獲到resource error ---') }, true);
SDK監(jiān)控錯(cuò)誤就是圍繞這幾種錯(cuò)誤實(shí)現(xiàn)的,try-catch用來在可預(yù)見情況下監(jiān)控特定錯(cuò)誤 ,Window.onerror主要來捕獲預(yù)料之外的錯(cuò)誤,比如異步錯(cuò)誤。但對(duì)于Promise錯(cuò)誤和網(wǎng)絡(luò)錯(cuò)誤是無法進(jìn)行捕獲的,所以需要用到Window.unhandledrejection監(jiān)聽捕獲Promise錯(cuò)誤,通過error監(jiān)聽捕獲資源加載錯(cuò)誤,從而達(dá)到各類型錯(cuò)誤全覆蓋。
用戶埋點(diǎn)統(tǒng)計(jì)
埋點(diǎn)是監(jiān)控用戶在應(yīng)用上的一些動(dòng)作表現(xiàn),如在淘寶某商品頁(yè)面上停留了幾分鐘,就會(huì)有一條某用戶在某段時(shí)間內(nèi)搜索了某商品并停留了幾分鐘的記錄,后臺(tái)根據(jù)這些記錄去分析用戶行為,并在指定之后推送或產(chǎn)品迭代優(yōu)化等,對(duì)于產(chǎn)品后續(xù)的發(fā)展起重要作用。
埋點(diǎn)又分為手動(dòng)埋點(diǎn)和自動(dòng)埋點(diǎn)
手動(dòng)埋點(diǎn)
手動(dòng)在代碼中添加相關(guān)埋點(diǎn)代碼,如用戶點(diǎn)擊某按鈕或者提交一個(gè)表單,會(huì)在按鈕點(diǎn)擊事件中和提交事件中添加相關(guān)埋點(diǎn)代碼。
{/* 手動(dòng)埋點(diǎn) */}
<button
onClick={() => {
tracker('submit', '提交表單');
tracker('click', '用戶點(diǎn)擊');
tracker('visit', '訪問新頁(yè)面');
}}
>按鈕1</button>
{/* 屬性埋點(diǎn) */}
<button data-target="按鈕2被點(diǎn)擊了">按鈕2</button>- 優(yōu)點(diǎn):可控性強(qiáng),可以自定義上報(bào)具體數(shù)據(jù)。
- 缺點(diǎn):對(duì)業(yè)務(wù)代碼入侵性強(qiáng),若需要很多地方進(jìn)行埋點(diǎn)需要一個(gè)個(gè)進(jìn)行添加。
自動(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)控的埋點(diǎn)方式。
{/* 自動(dòng)埋點(diǎn) */}
<button
style={{ marginRight: 20 }}
onClick={(e) => {
//業(yè)務(wù)代碼
}}
>按鈕3</button>/**
* 自動(dòng)上報(bào)
*/
export function autoTrackerReport() {
// 自動(dòng)上報(bào)
document.body.addEventListener('click', function (e) {
const clickedDom = e.target;
// 獲取標(biāo)簽上的data-target屬性的值
let target = clickedDom?.getAttribute('data-target');
// 獲取標(biāo)簽上的data-no屬性的值
let no = clickedDom?.getAttribute('data-no');
// 避免重復(fù)上報(bào)
if (no) {
return;
}
if (target) {
lazyReport('action', {
actionType: 'click',
data: target
});
} else {
// 獲取被點(diǎn)擊元素的dom路徑
const path = getPathTo(clickedDom);
lazyReport('action', {
actionType: 'click',
data: path
});
}
}, false);
}需要注意的是:無痕埋點(diǎn)是通過全局監(jiān)聽click事件的冒泡行為實(shí)現(xiàn)的,如果在click事件中阻止了冒泡行為,是不會(huì)冒泡到click監(jiān)聽里的,所以,對(duì)于加了冒泡行為的click事件需要進(jìn)行手動(dòng)埋點(diǎn)上報(bào),從而保證上報(bào)全覆蓋。
{/* 自動(dòng)埋點(diǎn) */}
<button
style={{ marginRight: 20 }}
onClick={(e) => {
e.stopPropagation(); // 阻止事件冒泡
tracker('submit', '按鈕1被點(diǎn)擊了'); //手動(dòng)上報(bào)
}}
>按鈕3</button>- 優(yōu)點(diǎn):不用入侵代碼就可以實(shí)現(xiàn)全局埋點(diǎn)上報(bào)。
- 缺點(diǎn):只能上報(bào)基本的行為交互信息,無法上報(bào)自定義數(shù)據(jù)。只要在頁(yè)面中點(diǎn)擊了,就會(huì)上報(bào)至服務(wù)器,導(dǎo)致上報(bào)次數(shù)會(huì)太多,服務(wù)器壓力大。
PV統(tǒng)計(jì)
PV即頁(yè)面瀏覽量,表示頁(yè)面的訪問次數(shù)
非SPA頁(yè)面只需通過監(jiān)聽onload事件即可統(tǒng)計(jì)頁(yè)面的PV,在SPA頁(yè)面中,路由的切換主要由前端來實(shí)現(xiàn),而單頁(yè)面切換又分為hash路由和history路由,兩種路由的實(shí)現(xiàn)原理不一樣,本文針對(duì)這兩種路由分別實(shí)現(xiàn)不同的數(shù)據(jù)采集方式
history路由
history路由依賴全局對(duì)象history實(shí)現(xiàn)的
- history.back(): 返回上一頁(yè) (瀏覽器回退)
- history.forward():前進(jìn)一頁(yè) (瀏覽器前進(jìn))
- history.go():跳轉(zhuǎn)歷史中某一頁(yè)
- history.pushState():添加新記錄
- history.replaceState():修改當(dāng)前記錄
history路由的實(shí)現(xiàn)主要由pushState和replaceState實(shí)現(xiàn),但這兩個(gè)方法不能被popstate監(jiān)聽到,所以需要對(duì)這兩個(gè)方法進(jìn)行重寫并進(jìn)行自定義事件監(jiān)聽來實(shí)現(xiàn)數(shù)據(jù)采集。
import { lazyReport } from './report';
/**
* history路由監(jiān)聽
*/
export function historyPageTrackerReport() {
let beforeTime = Date.now(); // 進(jìn)入頁(yè)面的時(shí)間
let beforePage = ''; // 上一個(gè)頁(yè)面
// 獲取在某個(gè)頁(yè)面的停留時(shí)間
function getStayTime() {
let curTime = Date.now();
let stayTime = curTime - beforeTime;
beforeTime = curTime;
return stayTime;
}
/**
* 重寫pushState和replaceState方法
* @param {*} name
* @returns
*/
const createHistoryEvent = function (name) {
// 拿到原來的處理方法
const origin = window.history[name];
return function(event) {
// if (name === 'replaceState') {
// const { current } = event;
// const pathName = location.pathname;
// if (current === pathName) {
// let res = origin.apply(this, arguments);
// return res;
// }
// }
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; // 頁(yè)面路徑
lazyReport('visit', {
stayTime,
page: beforePage,
})
beforePage = currentPage;
}
// 頁(yè)面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()
});
}
hash路由
url中的hash值變化會(huì)引起hashChange的監(jiān)聽,所以只需在全局添加一個(gè)監(jiān)聽函數(shù),在函數(shù)中實(shí)現(xiàn)數(shù)據(jù)采集上報(bào)即可。但在react和vue中hash路由的跳轉(zhuǎn)是通過pushState實(shí)現(xiàn)的,所以還需加上對(duì)pushState的監(jiān)聽。
/**
* hash路由監(jiān)聽
*/
export function hashPageTrackerReport() {
let beforeTime = Date.now(); // 進(jìn)入頁(yè)面的時(shí)間
let beforePage = ''; // 上一個(gè)頁(yè)面
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()
});
// 頁(yè)面load監(jiān)聽
window.addEventListener('load', function () {
listener()
});
const createHistoryEvent = function (name) {
const origin = window.history[name];
return function(event) {
//自定義事件
// if (name === 'replaceState') {
// const { current } = event;
// const pathName = location.pathname;
// if (current === pathName) {
// let res = origin.apply(this, arguments);
// return res;
// }
// }
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ì)
統(tǒng)計(jì)一天內(nèi)訪問網(wǎng)站的用戶數(shù)
UV統(tǒng)計(jì)只需在SDK初始化時(shí)上報(bào)一條消息即可。
/**
* 初始化配置
* @param {*} options 配置信息
*/
function init(options) {
// ------- 加載配置 ----------
// -------- uv統(tǒng)計(jì) -----------
lazyReport('user', '加載應(yīng)用');
}數(shù)據(jù)上報(bào)
- xhr接口請(qǐng)求
采用接口請(qǐng)求的方式,就像其他業(yè)務(wù)請(qǐng)求一樣,知識(shí)傳遞的數(shù)據(jù)是埋點(diǎn)的數(shù)據(jù)。通常情況下,公司里處理埋點(diǎn)的服務(wù)器和處理業(yè)務(wù)邏輯的服務(wù)器不是同一臺(tái),所以需要手動(dòng)解決跨域問題。另一方面,如果在上報(bào)過程中刷新或者重新打開頁(yè)面,可能會(huì)造成埋點(diǎn)數(shù)據(jù)的缺失,所以傳統(tǒng)xhr接口請(qǐng)求方式并不能很好適應(yīng)埋點(diǎn)的需求。 - img標(biāo)簽
img標(biāo)簽的方式是將埋點(diǎn)數(shù)據(jù)偽裝成圖片url的請(qǐng)求方式,避免了跨域問題,但瀏覽器對(duì)url長(zhǎng)度會(huì)有限制,所以不適合大數(shù)據(jù)量上報(bào),也會(huì)存在刷新或重新打開頁(yè)面的數(shù)據(jù)丟失問題。 - sendBeacon
這種方式不會(huì)出現(xiàn)跨域問題,也不糊存在刷新或重新打開頁(yè)面的數(shù)據(jù)丟失問題,缺點(diǎn)是存在兼容性問題。在日常開發(fā)中,通常采用sendBeacon上報(bào)和img標(biāo)簽上報(bào)結(jié)合的方式。
/**
* 上報(bào)
* @param {*} type
* @param {*} params
*/
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();
}總結(jié)
到此這篇關(guān)于前端實(shí)現(xiàn)監(jiān)控SDK的文章就介紹到這了,更多相關(guān)前端監(jiān)控SDK內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一道超經(jīng)典js面試題Foo.getName()的故事
Foo.getName算是一道比較老的面試題了,大致百度了一下在17年就有相關(guān)文章在介紹它,下面這篇文章主要給大家介紹了關(guān)于一道超經(jīng)典js面試題Foo.getName()的相關(guān)資料,需要的朋友可以參考下2022-03-03
JS 添加網(wǎng)頁(yè)桌面快捷方式的代碼詳細(xì)整理
如何添加桌面快捷?很多網(wǎng)友都有這個(gè)疑問;JS 點(diǎn)擊添加網(wǎng)頁(yè)桌面快捷方式的代碼,需要的朋友可以參考下2012-12-12
Javascript腳本獲取form和input內(nèi)容的方法(兩種方法)
隨著js的發(fā)展,許多的網(wǎng)頁(yè)數(shù)據(jù)處理完全可以由js腳本解決,而不需要發(fā)送到服務(wù)器,這里分享兩種Javascript腳本獲取form和input內(nèi)容的方法,感興趣的朋友跟隨小編一起看看吧2023-05-05
JavaScript中省略元素對(duì)數(shù)組長(zhǎng)度的影響
這篇文章主要介紹了JavaScript中省略元素對(duì)數(shù)組長(zhǎng)度的影響,本文給大家介紹的非常詳細(xì)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-10-10
BootStrap智能表單實(shí)戰(zhàn)系列(三)分塊表單配置詳解
這篇文章主要介紹了BootStrap智能表單實(shí)戰(zhàn)系列(三)分塊表單配置詳解的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06
JavaScript獲取當(dāng)前網(wǎng)頁(yè)標(biāo)題(title)的方法
這篇文章主要介紹了JavaScript獲取當(dāng)前網(wǎng)頁(yè)標(biāo)題(title)的方法,涉及javascript中document.title方法的使用,需要的朋友可以參考下2015-04-04

