公司系統需求加上埋點功能,用來統計各頁面功能的使用情況。於是,結合網上資料以及之前使用埋點系統的經歷,仔細研究研究。
調研
埋點分類
常見的埋點型別有三種
程式碼埋點
- 透過 JavaScript 程式碼主動將所需要的資訊上報給伺服器。
- 優點:可以精確的上報所需的資料,對於少量埋點需求較為合適。
- 缺點:程式碼遍佈專案各處,不好維護管理。且埋點只能透過開發人員手動完成。
視覺化埋點
- 需要另外一個視覺化埋點圈選系統來圈選需要埋點的 DOM 元素。然後透過在系統中整合 SDK 來主動上報這些區域的埋點資訊。其實算是另一種意義上的程式碼埋點。
- 優點:有圈選系統可以讓產品、運維同學自行決定埋點區域。
- 缺點:適用範圍有限,如內網系統、移動端 Hybrid 頁面這些就很難用外部的視覺化埋點來做。
無埋點
- 其實也叫全量埋點,即全域性監聽系統事件,把使用者所有行為都進行上報。
- 優點:行為資料記錄全面,無需增加或維護埋點程式碼。
- 缺點:上報資料量大,對伺服器有一定壓力。且無法精確上報某一功能的特停資料。
埋點目標
- 資料監控:透過埋點讓產品運維同學知道專案當前的具體情況,從而有針對性的去最佳化專案。
- 異常監控:從開發角度去收集專案中發生的 JS 報錯、介面報錯等異常情況。發現問題、解決問題、最佳化專案。
- 效能監控:收集專案執行中的各種效能指標,如白屏時間、首屏載入時間、介面請求時間等等。
埋點 SDK 實現猜想
以我之前工作中用到過的埋點系統 GrowingIO 為例。我們可以透過它的 SDK 文件 來驗證上面的理論。
- 它透過全域性引入 JS 程式碼的方式來進行整合,它會在 window 全域性物件下加上一個 gio 函式處理各種埋點行為。
- 由於埋點系統會為很多專案服務,所以需要初始化的時候加上
gio('init', 'your projectId', {})
。 - 它要求在需要圈選的 DOM 元素上
data-growing-container
屬性,這其實是 HTML 元素的 dataset 屬性,可以用來對元素進行自定義資料屬性的讀寫操作。有了圈選標記,埋點事件攔截的時候就可以指哪打哪了。 - 它透過
gio('track', eventId, eventLevelVariables);
函式實現了主動埋點行為,這個自然是必不可少的。總有埋點需求是自動埋點做不了的。 - 它的無埋點記錄的是所有元素的點選量和瀏覽量,應該是全域性監聽了元素的點選和可視事件。
- 它的視覺化圈選是透過 XPath 來唯一定位一個元素的,那麼視覺化圈選其實就是將目標 DOM 的 xPath 儲存起來,在埋點的時候去獲取指定 DOM 元素的點選量和訪問量。(關於 xpath 的使用可以看 Introduction to using XPath in JavaScript - XPath | MDN)
我的埋點
方案選擇
由於專案的埋點只需要記錄一些指定的行為,所以全埋點方案被我 PASS 了。同時也沒有必要另外寫一個頁面去做埋點的圈選,最終,選擇了最簡單粗暴地主動埋點。
主動埋點 1.0
一開始埋點其實很簡單,透過在 JavaScript 程式碼中寫埋點程式碼來進行實現。
定義一個埋點工具物件。
// logger.js
export default {
...,
track(data) {
const configInfo = this.getConfigInfo() // 一些公共配置資訊,如使用者名稱、token、時間、url 等
return fetch.post('/api/v1/web/log', {
...data,
...configInfo,
})
},
}
將 logger 物件綁到 Vue 的原型中。
// main.js
Vue.prototype.$logger = logger
在需要的地方主動埋點。
<template>
<div>
<el-button @click="download">download</el-button>
</div>
</template>
<script>
export default {
name: 'demo',
methods: {
download() {
window.open('file url')
this.trackLogger()
},
trackLogger() {
this.$logger.track({
component_id: '2',
component_name: '下載按鈕',
})
},
},
}
</script>
<style lang="scss" scoped></style>
遇到的問題
其實主動埋點應該就是如此,但隨著埋點程式碼的逐漸增多(已經從起初的 20 條增加到 203 條了……)。看程式碼的時候就非常難受了。描述一個場景:
- 需要檢查同事程式碼中的埋點情況,由於不清楚他的程式碼,就需要一點點找了。
- 全域性搜尋埋點程式碼
$logger.track()
,得到 n 個包含有埋點程式碼的函式。 - 再逐個跟蹤這些包含埋點程式碼的函式的觸發位置(有時候還會是函式嵌函式),最終找到繫結函式的 DOM 元素。
- 如此才算是確定了一個元素擁有埋點行為。
宣告式 vs 命令式
面對上面的場景,我在想有沒有辦法能夠省去逐個查函式的步驟,讓主動埋點程式碼更加直觀呢。這裡就得提到另外一個點了:宣告式程式碼與命令式程式碼的區別了。
- 宣告式程式碼:如 HTML、XML、CSS,它的特點是可讀性更強,描述的時候更符合直覺、更形象。
- 命令式程式碼:如 JavaScript,它的特點是更符合行為步驟的思考模式,適合處理一些邏輯性強的功能。
舉幾個栗子
比如畫一幅畫,用宣告式的方式來描述是“我要畫一幅畫,它有青草、大樹和天空”;而用命令式的方式描述是"我要畫一幅畫,首先需要畫青草,然後再畫大樹,最後加上藍色的天空。"
還有一個例子,在 vue 中有一個 createElement
函式,它可以在 vue 的 render 函式中命令式的建立 DOM 元素。
createElement(
'anchored-heading',
{
props: {
level: 1,
},
},
[createElement('span', 'Hello'), ' world!'],
)
但這種命令式的寫法可讀性很差。vue 官方也發現了這個問題,於是引進了 JSX 來彌補這個缺陷。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
},
})
JSX 的寫法明顯就更偏向於宣告式。
那麼回過來複習下主動埋點的目的:透過程式碼主動上報指定 DOM 元素的行為事件。所以個人感覺用宣告式寫法會更好一些。
主動埋點 2.0
說幹就幹,我試著將命令式埋點改為宣告式埋點。
首先在入口檔案 main.js
中引入全域性註冊邏輯。
// 事件名稱
const COMPONENT_MAP = {
1: '圖表切換',
2: '下載按鈕',
}
// 修復點選子元素不上報埋點資訊的問題
function bindDataset(el, value) {
el.dataset.loggerId = value
// 遞迴繫結 dataset 到所有子集上
el.children.forEach((child) => {
bindDataset(child, value)
})
}
// 全域性註冊指令,在需要埋點的 DOM 上加上 dataset
Vue.directive('logger', {
bind: function (el, binding) {
const { value } = binding
bindDataset(el, value)
},
})
// 全域性監聽元件點選事件,加入防抖是為了避免短時間內快速重複點選
document.addEventListener(
'click',
throttle((e) => {
if (e.target.dataset.loggerId) {
this.$logger.track({
component_id: e.target.dataset.loggerId,
component_name: COMPONENT_MAP[e.target.dataset.loggerId],
})
}
}, 2000),
)
在上面程式碼中,我將埋點透過vue 指令的方式將埋點資訊繫結到目標 DOM 的 dateset 上面。然後透過全域性 click 事件攔截來獲得目標元素的點選行為,並上報埋點資訊。
- 由於沒有找到如何直接在 Vue 元件上直接操作 DOM 的方式(ref 不算,那個需要寫很多的 ref='xxx' 很不划算),所以想到了 Vue 指令。
- 在點選 DOM 元素的時候,如果元素中有子節點那麼全域性 click 事件只能捕獲到子節點的事件,於是我偷懶將子節點都加上了 dataset。(元件的子元素不會太多,偷個懶了)
以上遇到的兩個問題個人感覺不是最佳方案,如果有好的解決方案歡迎討論呀!
使用方式如下,可讀性上強了不少。
<div class="filter-wrap" @click="setFilterPopupVisible(true)" v-logger="1">
<img class="filter-icon" :src="filterIconUrl" />
<img
class="filter-icon-checked"
:src="filterSelectedIconUrl"
v-show="isFilterActive"
/>
</div>
如此,以後在看埋點程式碼的時候只要全域性搜尋 v-logger
就可以很方便的看到有哪些 DOM 元素或者 vue 元件是進行了埋點的了。不需要反覆去查各種事件了。
最後
折騰了一圈,主要就是想解決看主動埋點程式碼太噁心的問題。然後順便複習一些知識點。
- 宣告式程式設計和指令式程式設計
- 埋點相關知識
- dataset
- xpath