<tbody id="5vw1z"></tbody>
<rp id="5vw1z"></rp>
  •  

    前端如何對數據埋點 SDK 呢?

    相信不少人因為項目中沒有接觸過數據埋點相關的內容,而沒有花時間去了解它,總覺得這又是一個自己還不能涉及的方面,然而數據埋點本身并不難理解,只是很難做得好,本文會從 認識數據埋點 SDK、設計前端數據埋點 SDK 兩個核心方面來展開,聊聊前端數據埋點的那些事。

    認識數據埋點 SDK

    SDK?全稱是?Software Development Kit?即 軟件開發工具包,一般都是一些軟件工程師為特定的軟件包、軟件框架、硬件平臺、操作系統等建立應用軟件時的開發工具的集合。

    為什么需要前端數據埋點?

    對產品本身而言,我們需要關注內容包括如下幾個方面:

    • 用戶在產品里 主要做什么操作、停留多久、訪問幾次
    • 用戶點擊率占比如何,會不會出現某些功能設計對于用戶而言是無效的
    • 用戶在核心使用流程上是否順暢,頁面反饋是否正常友好
    • 可能有哪些潛在的用戶的功能需要更新

    總的來說,數據埋點?核心是為了?收集數據有了數據就可以為所欲為),只有通過分析數據,才能更好的評估出整個項目的質量和重要性(數據為王),并且能夠為產品優化指明方向(數據驅動產品)。

    前端數據埋點要考慮哪些方面?

    數據埋點的核心是數據收集,而與數據相關的內容不外乎如下幾個內容:

    • 數據又是基于應用產生的,因為沒有應用就不會有相關的數據
    • 應用本身要提供展示、收集、操作內容,而這是基于平臺的,比如網站就是基于瀏覽器平臺
    • 有應用、有平臺就得有用戶,因為應用本身就是為了給用戶提供好用的功能去解決某些存在的問題
    • 針對開發者而言,應用就是代碼,代碼運行的質量也能決定應用的質量,而顯式質量體現在錯誤或警告上

    總結下來,數據埋點其實要考慮的就是?用戶行為、錯誤警告、頁面性能?三個核心方面。

    用戶行為

    用戶行為就是在網頁應用中進行的一系列操作,但用戶的操作有很多種,都需要記錄下來是不可能的,一般需要記錄用戶的以下幾種行為:

    • 用戶瀏覽頁面次數,PV(Page View)
      • 用戶每次訪問網站中的一個頁面就被記錄為?1?個?PV,多次訪問同一個頁面,訪問量就會累計
    • 頁面瀏覽用戶數,UV(Unique visitor)
      • 通過網絡正常訪問頁面的使用者,通常一臺電腦客戶端或一個用戶賬號為一個訪客,一般同一個客戶端或用戶賬號在?24h?內多次訪問只會被記錄為?1?個?UV,計算策略視具體情況而定
    • 用戶點擊按鈕次數
      • 以上兩種可以認為是?自動式觸發埋點,而點擊按鈕次數就屬于是?互動式觸發埋點,便于去了解這個功能按鈕的使用情況

      錯誤警告

      頁面中代碼運行產生的錯誤,可能會導致用戶核心操作流程被中斷,為了避免大量用戶受到影響,我們需要獲取?生產環境的錯誤數據,這樣才能便于開發者及時進行修復。

    通常來講代碼中的錯誤會包含以下幾大類:

    • 全局錯誤,即未被捕獲的錯誤
    • 局部錯誤,即通過?try...catch、promise.then、promise.catch?等捕獲的錯誤
    • 接口請求錯誤,即在二次封裝請求?API?中進行請求和接收響應時的錯誤
    • 組件級錯誤,即使用?Vue/React?組件時發生的錯誤

    頁面性能

    頁面性能其實也是前端性能優化中一個需要考慮和優化的點,畢竟如果一個網站老是發生?白屏、交互卡頓、頁面資源加載時間長?等問題,肯定是沒辦法留住用戶的,特別是用戶的真實環境各不相同,如?Windows x、MACOS、Android、iOS?等,更加需要統計和收集相關數據,便于進行集中優化處理,提升用戶體驗。

    與頁面性能指標相關的內容,這里大致總結下:

    • 首次繪制(First Paint,FP
      • 在渲染進程確認要渲染當前響應資源后,渲染進程會先創建一個空白頁面,通常把創建空白頁面的這個時間點稱為?First Paint,簡稱?FP
      • 所謂的?白屏時間?其實指的就是創建這個空白頁面到瀏覽器開始渲染非空白內容的時間,比如頁面背景發生變化等
    • 首次內容繪制(First Contentful Paint,FCP
      • 當用戶看見一些 “內容” 元素被繪制在頁面上的時間點,和白屏是不一樣,它可以是?文本?首次繪制,或?SVG?首次出現,或?Canvas?首次繪制等,即當頁面中繪制了第一個?像素?時,這個時間點稱為?First Content Paint,簡稱?FCP
    • 首屏時間 / 最大內容繪制(Largest Contentful Paint, LCP
      • LCP?是一種新的性能度量標準,LCP?側重于用戶體驗的性能度量標準,與現有度量標準相比,更容易理解與推理,當首屏內容完全繪制完成時,這個時間點稱為?Largest Content Paint,簡稱?LCP
      • 最大內容繪制應在?2.5s?內完成
    • 首次輸入延遲(First Input Delay, FID
      • FID?測量的是當用戶第一次在頁面上交互的時候(點擊鏈接、點擊按鈕?或?自定義基于?js?的事件),到瀏覽器實際開始處理這個事件的時間
      • 首次輸入延遲應在?100ms?內完成
    • 累積布局偏移(Cumulative Layout Shift, CLS)
      • CLS?是為了測量?視覺穩定性,以便提供良好的用戶體驗
      • 累積布局偏移應保持在?0.1?或更少
    • 首字節達到時間(Time to First Byte,TTFB
      • 指的是瀏覽器開始收到服務器響應數據的時間(后臺處理時間 + 重定向時間),是反映服務端響應速度的重要指標
      • TTFB?時間如果超過?500ms,用戶在打開網頁的時就會感覺到明顯的等待

    理解了 為什么要做前端數據埋點 和 前端數據埋點所需要統計數據的方方面面,接下來我們就需要設計一個自己的?前端數據埋點 SDK?了。

    設計前端數據埋點 SDK

    這里只我們考慮數據埋點的核心內容,因此不會涉及得肯定沒有那么全面,而一開始也不可能設計得全面,只要保證核心功能,那么在基于核心進行擴展即可。

    確定 options 和 data 內容

    應用的唯一標識 — options.AppId

    數據埋點 SDK?作為一個通用的工具集,是可供多個系統進行使用的,而這就意味著需要去保證每個應用的唯一性,一般來講,在初始化?SDK?的時候是需要接入方提供的當前應用的?ID。

    那這個?ID?從何而來?隨便生成嗎?一般來說需要經過如下步驟:

    • 在對應監控系統上為當前應用生成唯一的?AppId
    • 在對應應用接入?SDK?時作為配置項之一傳入

    其實還會涉及到請求?url?內容,主要用于發送給對應的監控系統,因此?options?核心內容簡單設計如下:

    {
      appId: '', // 當前應用唯一標識
      baseUrl: '', // 數據發送的地址
    }

    數據發送格式 — data

    由于需要收集的數據類型包含多種,最好能夠定義一種比較通用的數據格式,便于更友好地進行數據收集。

    這里簡單定義一下數據格式,大致如下,格式隨需求場景產生差異:

    {
      appId: '', // 當前應用唯一標識
      type: 'action' | 'performance'| 'network' | 'error', // 不同數據類型
      pageUrl: '', // 頁面地址
      apiUrl: '', // 接口地址
      userId: '', // 當前用戶 id
      userName: '', // 當前用戶 name
      time: '',// 觸發記錄的時間
      data: {}, // 接口響應結果 | 性能指標 | 錯誤對象 | 用戶操作相關信息
    }

    確定數據發送方式

    如果要問前端埋點最基本要實現的功能是什么,那必然是?數據發送?的能力,否則即便有應用、有用戶、有數據也只能保存在本地沒法發送給相應的監控系統,意味就沒法進行收集和統計(數據等于白給)。

    那么數據發送都有什么方式呢?針對這個問題把?數據發送?翻譯成?請求發送?就容易多了,轉而問題就變成了 請求發送方式都有哪些?

    一般會包括如下幾種(包括但不限于):

    • XMLHttpRequest
    • fetch
    • form?表單的?action
    • 基于元素?src?屬性的請求
      • img?標簽的?src
      • script?標簽的?src
      • Navigator.sendBeacon()

    這里選擇的是最后一種,因為?Navigator.sendBeacon()?就是專門用于通過?HTTP POST?將統計數據?異步?發送到?Web?服務器上,同時能避免傳統技術發送分析數據的一些問題。

    傳統技術發送統計數據的一些問題,可以直接通過?點擊這里?查看,由于文章篇幅有限不在額外解釋。

    SDK 核心代碼

    這里我們只考慮極簡情況,設計好的?SDK?代碼內容比較簡單,直接上代碼:

    let SDK = null // EasyAgentSDK 實例對象
    const QUEUE = [] // 任務隊列
    cosnt NOOP = (v) => v
    
    // 通過 web-vitals 頁面性能指標
    const reportWebVitals = (onPerfEntry) => {
      if (onPerfEntry && onPerfEntry instanceof Function) {
        import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
          getCLS(onPerfEntry) // 布局偏移量
          getFID(onPerfEntry) // 首次輸入延遲時間
          getFCP(onPerfEntry) // 首次內容渲染時間
          getLCP(onPerfEntry) // 首次最大內容渲染時間
          getTTFB(onPerfEntry) // 首個字節到達時間
        })
      }
    }
    
    export default class EasyAgentSDK {
      appId = ''
      baseUrl = ''
      timeOnPage = 0
      config = {}
      onPageShow = null
      onPagesHide = null
      
      constructor(options = {}) {
        if (SDK) return
    
        SDK = this
        this.appId = options.appId
        this.baseUrl = options.baseUrl || window.location.origin
        this.onPageShow = options.onPageShow || NOOP
        this.onPagesHide = options.onPagesHide || NOOP
    
        // 初始化監聽頁面變化
        this.listenPage()
      }
      
      // 設置 config
      setConfig(congfig){
        this.config = congfig
      }
    
      // 刷新任務隊列
      flushQueue() {
        Promise.resolve().then(() => {
          QUEUE.forEach((fn) => fn())
          QUEUE.length = 0;
        })
      }
    
      // 監聽頁面變化
      listenPage() {
        let pageShowTime = 0
    
        window.addEventListener('pageshow', () => {
          pageShowTime = performance.now()
          
           // 頁面性能指標上報
          reportWebVitals((data) => {
            this.performanceReport({ data })
          })
          
          // 執行 onPageShow
          this.onPageShow();
        })
    
        window.addEventListener('pagehide', () => {
          // 記錄用戶在頁面停留時間
          this.timeOnPage = performance.now() - pageShowTime
          
          // 刷新隊列前執行 onPageShow
          this.onPageShow();
    
          // 刷新任務隊列
          this.flushQueue()
        })
      }
    
      // Json 轉 FormData
      json2FormData(data){
        const formData = new FormData()
    
        Object.keys(data).forEach(key => {
          formData.append(key, data[key])
        });
    
        return formData
      }
    
      // 自定義上報類型
      report(config) {
        QUEUE.push(() => {
          const formData = json2FormData({
            ...this.config,
            ...config,
            time: new Date().toLocaleString(),
            appId: this.appId,
            pageUrl: window.location.href,
          });
          navigator.sendBeacon(`${this.baseUrl}${config.url || ''}`, formData)
        })
      }
    
      // 用戶行為上報
      actionReport(config) {
        this.report({
          ...config,
          type: 'action',
        })
      }
    
      // 網絡狀況上報
      networkReport(config) {
        this.report({
          ...config,
          type: 'network',
        })
      }
    
      // 頁面性能指標上報
      performanceReport(config) {
        this.report({
          ...config,
          type: 'performance',
        })
      }
    
      // 錯誤警告上報
      errorReport(config) {
        this.report({
          ...config,
          type: 'error',
        })
      }
    }

    上報用戶行為

    統計 PV 和 UV — 自動觸發埋點

    關于?PV?和?UV?在上述已經做過介紹了,本質上這兩個數據統計都可在一個上報類型為?action?數據發送中獲得,主要看監控系統是按照怎樣的規則對數據進行分析和統計,這里在?SDK?內部監聽了頁面的?pageshow / pagehide?兩個事件:

    • 在?pageshow?中可以上報與?PV / UV 相關的數據 和 頁面性能相關的數據。
      window.SDK = new EasyAgentSDK({
          appId: 'application_id',
          baseUrl: '//aegis.example.com/collect',
          onPageShow() {
              window.SDK.actionReport({
                  data: {} // 其他必要傳遞的信息
              })
          }
      });
      
      window.SDK.setConfig({
          userId: UserInfo.userId, // 當前用戶 id
          userName: UserInfo.userName, // 當前用戶 name
      });
    • 在 pagehide 中主要用于計算用戶停留在頁面上的時間 timeOnPage 和 刷新任務隊列

    統計用戶點擊按鈕 — 交互式觸發埋點

    假設我們希望記錄某些按鈕的使用次數的數據,可以在?document?上監聽?click?事件,目的利用事件冒泡以便于不需要侵入不同按鈕的?click?事件,比如:

    const TargetElementFilter = ['export_btn']
    
    const findTarget = (filters) => {
     return filters.find((filter) => TargetElementFilter.find((v) => filter === v)));
    }
    
    document.addEventListener('click', (e) => {
      const { id, className, outerHTML } = e.target
      const isTarget = findTarget([id, className])
    
      if (isTarget) {
        SDK.actionReport({
          data: {
            id, 
            className,
            outerHTML
          }, // 其他必要傳遞的信息
        })
      }
    })

    上報頁面性能

    和頁面性能相關的內容,屬于?SDK?自動觸發埋點,不應該讓使用者在手動接入,在上面的實現中,我們在?pageshow?事件中通?reportWebVitals?和?performanceReport?進行數據上報,并且這里選擇了?Google?推出的?web-vitals?來獲取和頁面性能指標相關的具體數據,對應代碼為:

    // 通過 web-vitals 頁面性能指標
    const reportWebVitals = (onPerfEntry) => {
      if (onPerfEntry && onPerfEntry instanceof Function) {
        import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
          getCLS(onPerfEntry) // 布局偏移量
          getFID(onPerfEntry) // 首次輸入延遲時間
          getFCP(onPerfEntry) // 首次內容渲染時間
          getLCP(onPerfEntry) // 首次最大內容渲染時間
          getTTFB(onPerfEntry) // 首個字節到達時間
        })
      }
    }

    獲取得到的數據大致如下:

    獲取得到的數據

    上報錯誤警告

    全局錯誤

    全局錯誤,即未被捕獲的錯誤,可以通過?window.onerror?事件來捕獲,然后進行錯誤數據上報,大致如下:

    window.addEventListener('error', (reason) => {
        const { filename, message, error } = reason;
    
        window.SDK.errorReport({
            data: {
                filename, 
                message, 
                error
            }
        });
    })

    局部錯誤

    局部錯誤,即通過?try...catch、promise.then、promise.catch?等捕獲的錯誤,大致使用如下:

     try {
        throw new Error('error for test')
      } catch(error) {
        window.SDK.errorReport({
          data: {
            error,
          },
        })
      }
    
    
      Promise.reject(new Error('Promise reject for test'))
      .then(
        () => {},
        (reason) => {
          window.SDK.errorReport({
            data: {
                error: reason
            }
        });
        },
      )
      
      Promise.reject(new Error('Promise reject for test'))
      .catch(
        (reason) => {
          window.SDK.errorReport({
            data: {
                error: reason
            }
        });
        },
      )

    接口請求錯誤

    接口請求錯誤,即在二次封裝請求?API?中進行請求和接收響應時的錯誤,為了方便這里以?axios?來舉例子,我們可以在它的?請求攔截?和?響應攔截?的第二個回調參數中去上報對應的錯誤數據信息,大致如下:

    // 創建axios實例
    const service = axios.create({
      baseURL, // api 的 base_url
      timeout: 60000, // 請求超時時間
      responseType: reqConf.responseType,
    });
    
    // 請求攔截
    service.interceptors.request.use(
      (config) => {
        ...
        return config;
      },
      (error) => {
        window.SDK.errorReport({
          apiUrl: config.url,
          data: {
            error,
          },
        })
      },
    );
    
    // 響應攔截
    service.interceptors.response.use(
      (config: any) => {
        ...
        return config;
      },
      (error: any) => {
        window.SDK.errorReport({
          apiUrl: config.url,
          data: {
            error,
          },
        })
    
        return error.response.data;
      },
    );

    組件級錯誤

    組件級錯誤,即使用?Vue / React?框架組件時發生的錯誤,完全可以使用它們在官方文檔中提到的錯誤捕獲方式來捕獲并上報錯誤。

    • Vue?中的?errorHandler?就是用于為應用內拋出的未捕獲錯誤指定一個全局處理函:
      // App.vue
      onMounted(()=>{
        throw new Error('error in onMounted')
      });
      
      // main.ts
      const app = createApp(App)
      
      app.config.errorHandler = (error, instance, info) => {
          window.SDK.errorReport({
              data: {
                  instance,
                  info,
                  error
              }
          });
      }
    • React 中的 ErrorBoundary 錯誤邊界相關的 getDerivedStateFromErrorcomponentDidCatch 鉤子
      // 定義錯誤邊界組件
      class ErrorBoundary extends React.Component {
        constructor(props) {
          super(props);
          this.state = { hasError: false };
        }
      
        static getDerivedStateFromError(error) {    
            // 更新 state 使下一次渲染能夠顯示降級后的 UI    
            return { hasError: true };  
        }
        componentDidCatch(error, info) {    
            // 可以將錯誤日志上報給服務器    
            window.SDK.errorReport({
              data: {
                  info,
                  error
              }
          });
        }
        render() {
          if (this.state.hasError) {      
              // 自定義降級后的 UI 并渲染      、
              return <h1>Something went wrong.</h1>;    
          }
          return this.props.children; 
        }
      }
      
      // 使用錯誤邊界組件
      <ErrorBoundary>
        <MyWidget />
      </ErrorBoundary>

    結語

    現在我們了解了?前端數據埋點 SDK?的二三事,通過上面的例子可能讓你覺得看起來比較簡單,但是真的要做好數據埋點也必然沒有那么容易,比如好需要考慮你的?SDK?數據發送的時間、發送的次數、需不需要將某些數據信息整合在一起只發送一次、怎么避免網絡擁塞等等問題。

    原文鏈接:點擊這里

    「點點贊賞,手留余香」

    0

    給作者打賞,鼓勵TA抓緊創作!

    微信微信 支付寶支付寶

    還沒有人贊賞,快來當第一個贊賞的人吧!

    聲明:
    1. 本站所有文章教程及資源素材均來源于網絡與用戶分享或為本站原創,僅限用于學習和研究。
    2. 如果內容損害你的權益請聯系客服QQ:1642748312給予處理。
    碼云筆記 » 前端如何對數據埋點 SDK 呢?

    發表回復

    IT互聯網行業相關廣告投放 更專業 更精準

    立即查看 聯系我們
    亚洲 自拍 另类小说综合图区