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