本文由團隊成員蕭凡撰寫,已受權塗鴉大前端獨家使用,包括但不限於編輯、標註原創等權益。css
前端開發攻城獅開開心心的 coding,很是自豪的進行了業務、UI 分離開發,各類設計模式、算法優化輪番上陣,代碼寫的 Perfect(勞資代碼天下第一),沒有 BUG,程序完美,兼容性 No.1,代碼能打能抗質量高。下班輕鬆打卡,回家看娃。前端
實際上,開發環境與生產環境並不能等同,而且測試的過程再完善,依然會有漏測的狀況存在。考慮到用戶使用客戶端環境、網絡環境等等一系列的不肯定因素存在。ajax
因此在開發過程當中必定要記得三大原則(我胡謅的)算法
埋點就像城市中的攝像頭,從產品的角度考慮,它能夠監控到用戶在咱們產品裏的行爲軌跡,爲產品的迭代、項目的穩定提供依據,WHO、WHEN、WHERE、HOW、WHAT 是埋點採集數據的基礎維度。後端
對前端開發而言,能夠監控頁面資源加載性能,異常等等,提供了頁面體驗和健康指數,爲後續性能優化提供依據,及時上報異常和發生場景。從而可以及時修正問題,提升項目質量等。設計模式
埋點能夠大概分爲三類:跨域
代碼埋點 | 可視化埋點 | 無痕埋點 | |
---|---|---|---|
典型場景 | 無痕埋點沒法覆蓋到,好比須要業務數據 | 簡單規範的頁面場景 | 簡單規範的頁面場景, |
優點 | 業務數據明確 | 開發成本低,運營人員可直接進行相關埋點配置 | 無需配置,數據可回溯 |
不足 | 數據不可回溯,開發成本高 | 不能關聯業務數據,數據不可回溯 | 數據量較大,不能關聯業務數據 |
大部分狀況,咱們能夠經過無痕埋點收集到全部的信息數據,再配合可視化埋點,可以具體定位到某一個點位,這樣大部分的埋點信息都據此分析出來。瀏覽器
在特殊狀況下,能夠多加上業務代碼手動埋點,處理一下特別的場景(大部分狀況是走強業務與正常的點擊,刷新事件無關須要上報的信息)性能優化
上面的數據經過 3 個維度來定義埋點事件babel
LEVEL
: 描述埋點數據的日誌級別
INFO
:一些用戶操做,請求成功,資源加載等等正常的數據記錄ERROR
: JS報錯,接口報錯等等錯誤類型的數據記錄DEBUG
: 預留開發人員經過手動調用的方式回傳排除bug的數據記錄WARN
: 預留開發人員經過手動調用的方式回傳非正經常使用戶行爲的的數據記錄CATEGORY
:描述埋點數據的分類
TRACK
: 埋點SDK對象的生命週期管理整個埋點數據。
WILL_MOUNT
:sdk對象即將初始化加載,生成一個默認ID,跟蹤所有相關事件DID_MOUNTED
:sdk對象初始化完成,主要獲取設備指紋等等的異步操做完成AJAX
: AJAX相關數據ERROR
:頁面中的異常相關數據PERFORMANCE
: 關於性能相關數據OPERATION
: 用戶操做相關數據EVENT_NAME
:具體的事件名稱根據上述的維度,咱們能夠簡單設計以下的架構
根據上圖的架構,再進行下面的具體代碼開發
在瀏覽器中如今主要有 2 種請求方式,一個是 XMLHttpRequest
, 一個是 Fetch
。
function NewXHR() {
var realXHR: any = new OldXHR(); // 代理模式裏面有提到過
realXHR.id = guid()
const oldSend = realXHR.send;
realXHR.send = function (body) {
oldSend.call(this, body)
//記錄埋點
}
realXHR.addEventListener('load', function () {
//記錄埋點
}, false);
realXHR.addEventListener('abort', function () {
//記錄埋點
}, false);
realXHR.addEventListener('error', function () {
//記錄埋點
}, false);
realXHR.addEventListener('timeout', function () {
//記錄埋點
}, false);
return realXHR;
}
複製代碼
const oldFetch = window.fetch;
function newFetch(url, init) {
const fetchObj = {
url: url,
method: method,
body: body,
}
ajaxEventTrigger.call(fetchObj, AJAX_START);
return oldFetch.apply(this, arguments).then(function (response) {
if (response.ok) {
//記錄埋點
} else {
//上報錯誤
}
return response
}).catch(function (error) {
fetchObj.error = error
//記錄埋點
throw error
})
}
複製代碼
PV
,UV
在進入頁面時,咱們經過算法生成一個惟一 session id
,做爲此次埋點行爲的全局 id,上報用戶 id,設備指紋,設備信息。在用戶未登陸的狀況下,經過設備指紋來計算 UV
,經過 session id
計算 PV
。
異常就是干擾程序的正常流程的不尋常事故
在JS
中能夠經過 window.onerror
和window.addEventListener('error', callback)
捕捉運行時異常,通常使用window.onerror
,它兼容性更好。
window.onerror = function(message, url, lineno, columnNo, error) {
const lowCashMessage = message.toLowerCase()
if(lowCashMessage.indexOf('script error') > -1) {
return
}
const detail = {
url: url
filename: filename,
columnNo: columnNo,
lineno: lineno,
stack: error.stack,
message: message
}
//記錄埋點
}
複製代碼
在這裏咱們過濾了 Script Error
, 它產生的緣由主要是頁面中加載的第三方跨域腳本報錯,好比託管在第三方 CDN 中的 js
腳本。這類問題比較難以排查。解決的方法有:
CORS
(Cross Origin Resource Sharing,跨域資源共享),以下步驟
<srcipt src="another domain/main.js" cossorigin="anonymous"></script>
Access-Control-Allow-Origin: * | 指定域名
try catch
<script scr="crgt.js"></script> //加載crgt腳本,window.crgt = {getUser: () => string} try{ window.crgt.getUser(); }catch(error) { throw error // 輸出正確的錯誤堆棧 } 複製代碼
js
在異步異常時沒法經過 onerror
方法捕獲 ,在 Promise 對象在 reject 時,同時並無進行處理時 會拋出一個 unhandledrejection
的錯誤,並不會被上述的方法所捕獲,因此須要添加單獨的處理事件。
window.addEventListener("unhandledrejection", event => {
throw event.reason
});
複製代碼
在瀏覽器中,能夠經過 window.addEventListener('error', callback)
的方式監聽資源加載異常,好比 js
或者 css
腳本文件丟失。
window.addEventListener('error', (event) => {
if (event.target instanceof HTMLElement) {
const target = parseDom(event.target, ['src']);
const detail = {
target: target,
path: parseXPath(target),
}
// 記錄埋點
}
}, true)
複製代碼
經過 addEventListener click
監聽 click
事件
window.addEventListener('click', (event) => {
//記錄埋點
}, true)
複製代碼
在這裏經過組件的 displaName
來定位元素的位置,displaName
表示組件的文件目錄,好比 src/components/Form.js
文件導出的組件 FormItem
經過 babel plugin
自動添加屬性 @components/Form.FormItem
,或者使用者主動給組件添加 static
屬性 displayName
。
監聽頁面hash變化,對hash進行解析
window.addEventListener('hashchange', event => {
const { oldURL, newURL } = event;
const oldURLObj = url.parseUrl(oldURL);
const newURLObj = url.parseUrl(newURL);
const from = oldURLObj.hash && url.parseHash(oldURLObj.hash);
const to = newURLObj.hash && url.parseHash(newURLObj.hash);
if(!from && !to ) return;
// 記錄埋點
})
複製代碼
經過 addEventListener beforeunload
監聽離開頁面事件
window.addEventListener('beforeunload', (event) => {
//記錄埋點
})
複製代碼
class Observable {
constructor(observer) {
observer(this.emit)
}
emit = (data) => {
this.listeners.forEach(listener => {
listener(data)
})
}
listeners = [];
subscribe = (listener) => {
this.listeners.push(listeners);
return () => {
const index = this.listeners.indexOf(listener);
if(index === -1) {
return false
}
this.listeners.splice(index, 1);
return true;
}
}
}
複製代碼
const clickObservable = new Observable((emit) => {
window.addEventListener('click', emit)
})
複製代碼
然而在處理 ajax
,須要將多種數據組合在一塊兒,須要進行 merg 操做,則顯得沒有那麼優雅,也很難適應後續複雜的數據流的操做。
const ajaxErrorObservable = new Observable((emit) => {
window.addEventListener(AJAX_ERROR, emit)
})
const ajaxSuccessObservable = new Observable((emit) => {
window.addEventListener(AJAX_SUCCESS, emit)
})
const ajaxTimeoutObservable = new Observable((emit) => {
window.addEventListener(AJAX_TIMEOUT, emit)
})
複製代碼
能夠選擇 RxJS 來優化代碼
export const ajaxError$ = fromEvent(window, 'AJAX_ERROR', true)
export const ajaxSuccess$ = fromEvent(window, 'AJAX_SUCCESS', true)
export const ajaxTimeout$ = fromEvent(window, 'AJAX_TIMEOUT', true)
複製代碼
ajaxError$.pipe(
merge(ajaxSuccess$, ajaxTimeout$),
map(data=> (data) => ({category: 'ajax', data; data}))
subscribe(data => console.log(data))
複製代碼
經過 merge
, map
兩個操做符完成對數據的合併和處理。
core
event$
數據流合併snapshot
獲取當前設備快照,例如url
,userID
,router
track
埋點類,組合數據流和日誌。logger
logger
日誌類
info
warn
debug
error
observable
ajax
beforeUpload
opeartion
routerChange
logger
track
自建埋點系統是一個須要先後端一塊兒合做的事情,若是人力不足的狀況下,建議使用第三方分析插件,例如 Sentry 就能足夠知足大部分平常使用
但仍是建議多瞭解,在第三方插件出現不能知足業務需求的時候,能夠頂上。