前端埋點資料收集及上報方案
什麼是埋點
埋點,它的學名是事件追蹤(Event Tracking),主要是針對特定使用者行為或業務過程進行捕獲、處理和傳送的相關技術及實施過程。埋點是資料領域的一個專業術語,也是網際網路領域的一個俗稱。
埋點是產品資料分析的基礎,一般用於推薦系統的反饋、使用者行為的監控和分析、新功能或者運營活動效果的統計分析等。
埋點包含兩個重要概念:事件(event),屬性(param)
- 事件(event):應用中發生了什麼,例如使用者操作、系統事件或系統錯誤。以你拍一產品為例,包含以下事件:enter_page(進入頁面)、leave_page(離開頁面)。
- 屬性(param):為了描述使用者群細分而定義的屬性,例如語言偏好或地理位置。以“進入課後練習”事件為例,它包含如下事件屬性:enter_from(從哪個頁面來),class_id(課程id)等。
- 屬性值(value):屬性的維度,即行為觸發時的具體維度。例如:enter_from:home(主頁)、system(系統)等。
主流方案
- 無痕埋點(全埋點),利用瀏覽器或APP自帶的監聽方式,對使用者的瀏覽頁面、點選等行為進行收集,一般用於粗顆粒度的資料分析,例如公司的slardar
-
- 資料噪聲大,不管有用沒有,資料都會被收集
- 無法定製化埋點,無法採集到指定事件和業務屬性
- 可供DA使用的資訊較少
- 接入簡單,幾乎無侵入,不需要額外的開發成本
- 使用者操作行為收集非常完整,幾乎不會遺漏
- 優點:
- 缺點:
- 程式碼埋點,前端開發人員在程式碼中自定義監聽和收集
-
- 工作量大,而且對程式碼侵入性很大,後期維護也不是很方便
- 可以精確埋點,具備明確的事件標識
- 業務屬性非常豐富
- 埋點觸發方式可以靈活定義
- DA使用更方便和精確
- 優點:
- 缺點:
- 埋點sdk,sdk向外暴露上報埋點的介面,監聽和收集過程開發人員無感知。例如公司的tea
-
- 暫時想不到
- 業務開發只需關注事件標識、業務屬性等
- 兼顧無痕埋點優點和程式碼埋點的優勢
- 優點:
- 缺點:
常見埋點屬性
通常前端是按照頁面維度統計埋點的,常見的事件屬性如下:
屬性 | 描述 |
---|---|
uid | 使用者id,若使用者未登陸,則返回特定標識id |
url | 當前事件觸發頁面的url |
eventTime | 觸發埋點的時間戳 |
localTime | 觸發埋點時的使用者本地時間,使用標準YYYY-MM-DD HH:mm:ss格式表示,方便後期直接使用字串查詢 |
deviceType | 當前使用者使用的裝置型別,比如apple、三星、chrome等 |
deviceId | 當前使用者使用的裝置id |
osType | 當前使用者使用的系統型別,比如windows、macos、ios、android等 |
osVersion | 當前使用者使用的系統版本 |
appVersion | 當前應用版本 |
appId | 當前應用id |
extra | 自定義資料,一般是序列化的字串,且資料結構應保持穩定 |
常見埋點事件
事件 | 上報時機 | 描述 |
---|---|---|
頁面停留 | 當前頁面切換或者頁面解除安裝時 | 記錄前一頁瀏覽時間 |
pv | 進入頁面時 | 頁面訪問次數,uv只需要根據deviceId過濾 |
互動事件 | 使用者互動事件觸發時 | 比如點選、長按等 |
邏輯事件 | 符合邏輯條件時 | 比如登陸、跳轉頁面等 |
效能資料採集方案
目前效能指標資料大部分來源於 window.performance API。
Performance.timing
引數名 | 描述 |
---|---|
connectEnd | HTTP(TCP) 返回瀏覽器與伺服器之間的連線建立時的時間戳。如果建立的是持久連線,則返回值等同於fetchStart屬性的值。連線建立指的是所有握手和認證過程全部結束。 |
connectStart | HTTP(TCP) 域名查詢結束的時間戳。如果使用了持續連線(persistent connection),或者這個資訊儲存到了快取或者本地資源上,這個值將和 fetchStart一致。 |
domComplete | 當前文件解析完成,即Document.readyState 變為 'complete'且相對應的readystatechange 被觸發時的時間戳 |
domContentLoadedEventEnd | 當所有需要立即執行的指令碼已經被執行(不論執行順序)時的時間戳。 |
domContentLoadedEventStart | 當解析器傳送DOMContentLoaded 事件,即所有需要被執行的指令碼已經被解析時的時間戳。 |
domInteractive | 當前網頁DOM結構結束解析、開始載入內嵌資源時(即Document.readyState屬性變為“interactive”、相應的readystatechange事件觸發時)的時間戳。 |
domLoading | 當前網頁DOM結構開始解析時(即Document.readyState屬性變為“loading”、相應的 readystatechange事件觸發時)的時間戳。 |
domainLookupEnd | DNS 域名查詢完成的時間。如果使用了本地快取(即無 DNS 查詢)或持久連線,則與 fetchStart 值相等 |
domainLookupStart | DNS 域名查詢開始的UNIX時間戳。如果使用了持續連線(persistent connection),或者這個資訊儲存到了快取或者本地資源上,這個值將和fetchStart一致。 |
fetchStart | 瀏覽器準備好使用HTTP請求來獲取(fetch)文件的時間戳。這個時間點會在檢查任何應用快取之前。 |
loadEventEnd | 當load事件結束,即載入事件完成時的時間戳。如果這個事件還未被髮送,或者尚未完成,它的值將會是0. |
loadEventStart | load事件被髮送時的時間戳。如果這個事件還未被髮送,它的值將會是0。 |
navigationStart | 同一個瀏覽器上一個頁面解除安裝(unload)結束時的時間戳。如果沒有上一個頁面,這個值會和fetchStart相同。 |
redirectEnd | 最後一個HTTP重定向完成時(也就是說是HTTP響應的最後一個位元直接被收到的時間)的時間戳。如果沒有重定向,或者重定向中的一個不同源,這個值會返回0. |
redirectStart | 第一個HTTP重定向開始時的時間戳。如果沒有重定向,或者重定向中的一個不同源,這個值會返回0。 |
requestStart | 返回瀏覽器向伺服器發出HTTP請求時(或開始讀取本地快取時)的時間戳。 |
responseEnd | 返回瀏覽器從伺服器收到(或從本地快取讀取,或從本地資源讀取)最後一個位元組時(如果在此之前HTTP連線已經關閉,則返回關閉時)的時間戳。 |
responseStart | 返回瀏覽器從伺服器收到(或從本地快取讀取)第一個位元組時的時間戳。如果傳輸層在開始請求之後失敗並且連線被重開,該屬性將會被數製成新的請求的相對應的發起時間 |
secureConnectionStart | HTTPS 返回瀏覽器與伺服器開始安全連結的握手時的時間戳。如果當前網頁不要求安全連線,則返回0。 |
unloadEventEnd | 和 unloadEventStart 相對應,unload事件處理完成時的時間戳。如果沒有上一個頁面,這個值會返回0。 |
unloadEventStart | 上一個頁面unload事件丟擲時的時間戳。如果沒有上一個頁面,這個值會返回0。 |
常見效能指標
指標名 | 描述 |
---|---|
FP | 頁面首次繪製時間 |
FCP | 頁面首次有內容繪製的時間 |
FMP | 頁面首次有效繪製時間,FMP >= FCP |
TTI | 頁面完全可互動時間 |
FID | 頁面載入階段,使用者首次互動操作的延時時間 |
MPFID | 頁面載入階段,使用者互動操作可能遇到的最大延時時間 |
LOAD | 頁面完全載入的時間(load 事件發生的時間) |
FP
FP (First Paint)指標通常會反映頁面的白屏時間,而白屏時間會反映當前 Web 頁面的網路載入效能情況,當載入效能非常良好的情況下,白屏的時間就會越短,使用者等待內容的時間就會越短,流失的概率就會降低。
該指標可以通過 performance.getEntriesByType('paint')
方法獲取 PerformancePaintTiming API
提供的打點資訊,找到 name 為 first-paint 的物件,描述的即為 FP 的指標資料,如下圖所示:
FCP
FCP (First Contentful Paint) 為首次有內容渲染的時間點,在效能統計指標中,從使用者開始訪問 Web 頁面的時間點到 FCP 的時間點這段時間可以被視為無內容時間,一般 FCP >= FP。
該指標可以通過 performance.getEntriesByType('paint')
方法獲取 PerformancePaintTiming API 提供的打點資訊,找到 name 為 first-contentful-paint 的物件,描述的即為 FCP 的指標資料,如下圖所示:
FMP
FMP(First Meaningful Paint),即首次繪製有意義內容的時間,當整體頁面的佈局和文字內容全部渲染完成後,即可認為是完成了首次有意義內容的繪製。所以 FMP 衡量了使用者看到網頁的主要內容的時間,是使用者體驗角度的一種重要的衡量指標。
前端業界現在比較認可的一個計算 FMP 的方式就是「 頁面在載入和渲染過程中最大布局變動之後的那個繪製時間 」。可通過 MutationObserver 監聽每一次頁面整體的 DOM 變化,觸發 MutationObserver 的回撥,在回撥計算出當前 DOM 樹的變動分數,分數變化最劇烈的時刻,即為 FMP 的時間點。
TTI
TTI(Time To Interactive),即 從頁面載入開始到頁面處於完全可互動狀態所花費的時間 。頁面處於完全可互動狀態時,滿足以下 3 個條件:
- 頁面已經顯示有用內容。
- 頁面上的可見元素關聯的事件響應函式已經完成註冊。
- 事件響應函式可以在事件發生後的 50ms 內開始執行。
資源載入指標
window.performance.getEntriesByType('resource')
會返回當前頁面載入的所有資源(js、css、img...)的各類效能指標,可用於靜態資源效能資料採集。
主要型別有:script、link、img、css、xmlhttprequest、beacon、fetch、other。PerformanceResourceTiming - Web APIs | MDN
引數名 | 描述 |
---|---|
connectEnd | 一個 DOMHighResTimeStamp,表示瀏覽器完成建立與伺服器的連線以檢索資源之後的時間。 |
connectStart | 一個 DOMHighResTimeStamp,表示瀏覽器開始建立與伺服器的連線以檢索資源之前的時間。 |
decodedBodySize | 一個 number,表示在刪除任何應用的內容編碼之後,從訊息主體的請求(HTTP 或快取)中接收到的大小(以八位位元組為單位)。 |
domainLookupEnd | 一個 DOMHighResTimeStamp,表示瀏覽器完成資源的域名查詢之後的時間。 |
domainLookupStart | 一個 DOMHighResTimeStamp,表示在瀏覽器立即開始資源的域名查詢之前的時間 |
duration | 返回一個 timestamp,即 responseEnd 和 startTime 屬性的差值。 |
encodedBodySize | 一個 number,表示在刪除任何應用的內容編碼之前,從有效內容主體的請求(HTTP 或快取)中接收到的大小(以八位位元組為單位)。 |
entryType | 返回 "resource"。 |
fetchStart | 一個 DOMHighResTimeStamp,表示瀏覽器即將開始獲取資源之前的時間。 |
initiatorType | 一個 string,代表啟動效能條目的資源的型別 |
name | 返回資源 URL。 |
nextHopProtocol | 一個 string,代表用於獲取資源的網路協議,由 ALPN 協議 ID(RFC7301) 定義。 |
redirectEnd | 一個 DOMHighResTimeStamp,表示收到上一次重定向響應的傳送最後一個位元組時的時間。 |
redirectStart | 一個 DOMHighResTimeStamp 代表啟動重定向的請求開始之前的時間。 |
requestStart | 一個 DOMHighResTimeStamp,表示瀏覽器開始向伺服器請求資源之前的時間。 |
responseEnd | 一個 DOMHighResTimeStamp,表示在瀏覽器接收到資源的最後一個位元組之後或在傳輸連線關閉之前(以先到者為準)的時間。 |
responseStart | 一個 DOMHighResTimeStamp,表示瀏覽器從伺服器接收到響應的第一個位元組後的時間。 |
secureConnectionStart | 一個 DOMHighResTimeStamp,表示瀏覽器即將開始握手過程以保護當前連線之前的時間。 |
serverTiming | 一個 PerformanceServerTiming 陣列,包含伺服器計時指標的 PerformanceServerTiming 條目。 |
startTime | 返回一個 timestamp,表示資源獲取開始的時間。該值等效於 fetchStart。 |
transferSize | 一個 number 代表所獲取資源的大小(以八位位元組為單位)。該大小包括響應標頭欄位以及響應有效內容主體。 |
workerStart | 一個 DOMHighResTimeStamp, 如果服務 Worker 執行緒已經在執行,則返回在分派 FetchEvent 之前的時間戳,如果尚未執行,則返回在啟動 Service Worker 執行緒之前的時間戳。如果服務 Worker 未攔截該資源,則該屬性將始終返回 0。 |
其他指標計算方式
指標名 | 描述 | 計算方式 |
---|---|---|
DNS查詢 | DNS 階段耗時 | domainLookupEnd - domainLookupStart |
TCP連線 | TCP 階段耗時 | connectEnd - connectStart |
SSL建連 | SSL 連線時間 | connectEnd - secureConnectionStart |
首位元組網路請求 | 首位元組響應時間(ttfb) | responseStart - requestStart |
內容傳輸 | 內容傳輸,Response階段耗時 | responseEnd - responseStart |
DOM解析 | Dom解析時間 | domInteractive - responseEnd |
資源載入 | 資源載入 | loadEventStart - domContentLoadedEventEnd |
首位元組 | 首位元組 | responseStart - fetchStart |
DOM Ready | dom ready | domContentLoadedEventEnd - fetchStart |
redirect時間 | 重定向時間 | redirectEnd - redirectStart |
DOM render | dom渲染耗時 | domComplete - domLoading |
load | 頁面載入耗時 | loadEventEnd - navigationStart |
unload | 頁面解除安裝耗時 | unloadEventEnd - unloadEventStart |
請求耗時 | 請求耗時 | responseEnd - requestStart |
白屏時間 | 白屏時間 | domLoading - navigationStart |
錯誤資料採集方案
目前所能捕捉的錯誤有三種:
-
資源載入錯誤,通過
addEventListener('error', callback, true)
在捕獲階段捕捉資源載入失敗錯誤。 -
js 執行錯誤,通過
window.onerror
捕捉 js 錯誤。 -
-
跨域的指令碼會給出 "Script Error." 提示,拿不到具體的錯誤資訊和堆疊資訊。此時需要在script標籤增加
crossorigin="anonymous"
屬性,同時資源伺服器需要增加CORS相關配置,比如Access-Control-Allow-Origin: *
-
跨域的指令碼會給出 "Script Error." 提示,拿不到具體的錯誤資訊和堆疊資訊。此時需要在script標籤增加
-
promise 錯誤,通過
addEventListener('unhandledrejection', callback)
捕捉 promise 錯誤,但是沒有發生錯誤的行數,列數等資訊,只能手動丟擲相關錯誤資訊。
// 在捕獲階段,捕獲資源載入失敗錯誤 addEventListener('error', e => { const target = e.target if (target != window) { monitor.errors.push({ type: target.localName, url: target.src || target.href, msg: (target.src || target.href) + ' is load error', time: Date.now() }) } }, true) // 監聽 js 錯誤 window.onerror = function(msg, url, row, col, error) { monitor.errors.push({ type: 'javascript', row: row, col: col, msg: error && error.stack? error.stack : msg, url: url, time: Date.now() }) } // 監聽 promise 錯誤 缺點是獲取不到行數資料 addEventListener('unhandledrejection', e => { monitor.errors.push({ type: 'promise', msg: (e.reason && e.reason.msg) || e.reason || '', time: Date.now() }) })
資料上報方案
在這個場景中,需要考慮兩個問題:
- 如果資料上報介面與業務系統使用同一域名,瀏覽器對請求併發量有限制,所以存在網路資源競爭的可能性。
- 瀏覽器通常在頁面解除安裝時會忽略非同步ajax請求,如果需要必須進行資料請求,一般在unload或者beforeunload事件中建立同步ajax請求,以此延遲頁面解除安裝。從使用者側角度,就是頁面跳轉變慢。
Beacon
可以看到,除開ie瀏覽器,目前主流現代瀏覽器對beacon的支援率非常高。Beacon - MDN文件
Beacon 介面用來排程向 Web 伺服器傳送的非同步非阻塞請求。
POST
通俗的講就是,Beacon可將資料非同步傳送至服務端,且能夠保證在頁面解除安裝完成前傳送請求(解決ajax頁面解除安裝會終止請求的問題)。使用方法如下:
navigator.sendBeacon(url, data);
其中 data 引數是可選的,它的型別可以為 ArrayBufferView
, Blob
, DOMString
或者 FormData
。如果瀏覽器成功地將 beacon 請求加入到待發送的佇列裡,這個方法將會返回 true ,否則將會返回 false
使用Beacon時需要後臺需要使用 post
方法接收引數,考慮到跨域問題,後臺還需要改造介面配置CORS。同時請求頭必須滿足CORS-safelisted request-header,其中content-type的型別必須為 application/x-www-form-urlencoded
, multipart/form-data
, 或者 text/plain
。
type ContentType = 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain'; const serilizeParams = (params: object) => { return window.btoa(JSON.stringify(params)) } function sendBeacon(url: string, params: object) { const formData = new FormData() formData.append('params', serilizeParams(params)) navigator.sendBeacon(url, formData) }
Image
sendBeacon的相容性問題是不可避免的,不過可以充分利用大部分瀏覽器會在頁面解除安裝前完成圖片的載入的特性,通過在頁面新增img的方式上報資料。
function sendImage(url: string, params: object) { const img = new Image() img.style.display = 'none' const removeImage = function() { img.parentNode.removeChild(img) } img.onload = removeImage img.onerror = removeImage img.src = `${url}?params=${serilizeParams(params)}` document.body.appendChild(img) }
由於img圖片為get請求方式,不同伺服器針對uri的長度有限制,長度超過限制時會出現HTTP 414錯誤,所以還要注意上報頻率,減少一次性上傳的屬性過多。
HTTP 1.1 defines Status Code 414 Request-URI Too Long for the cases where a server-defined limit is reached. You can see further details on RFC 2616. For the case of client-defined limits, there is no sense on the server returning something, because the server won't receive the request at all.
相容方案
優先使用sendBeacon的方式,Image方式作為fallback。
function sendLog(url: string, params: object) { if(navigator.sendBeacon) { sendBeacon(url, params) } else { sendImage(url, params) } }
- 設計模式【4】-- 建造者模式詳解
- 分散式鎖及其實現
- TDSQL | 國產化浪潮下,資料庫 雲如何跑上核心業務?
- 為了生成唯一id,React18專門引入了新Hook:useId
- Java SPI機制從原理到實戰
- Rust 稽核團隊“一夜之間”集體辭職:開源社群治理話題再被熱議
- PHP 基金會,是個好事 (PHP Foundation)
- 0.99M,150FPS,移動端超輕量目標檢測演算法PP-PicoDet來了!
- Shell 指令碼避坑指南(一)
- KVO原理分析
- Markdown語法基礎
- 升級到Java 17沒這麼簡單
- 無處不在的 Kubernetes,難用的問題解決了嗎?
- Spark 架構設計與原理思想
- 如何用 Flutter開發一個直播應用
- 訊息佇列RocketMQ應對雙十一流量洪峰的“六大武器”
- 面試官問我JVM記憶體結構,我真的是
- Hermes將成為React Native預設的JS引擎
- 做一名真正的軟體工程師
- CDP客戶資料管理平臺體系化搭建