Vue 專案宣告式主動埋點

VioletJack發表於2023-03-06

公司系統需求加上埋點功能,用來統計各頁面功能的使用情況。於是,結合網上資料以及之前使用埋點系統的經歷,仔細研究研究。

調研

埋點分類

常見的埋點型別有三種

  • 程式碼埋點

    • 透過 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

參考資料

相關文章