首頁 > 軟體

JS前端監控採集使用者行為的N種姿勢

2022-07-22 14:00:44

引言

上一篇我們詳細介紹了前端如何採集異常資料。採集異常資料是為了隨時監測線上專案的執行情況,發現問題及時修復。在很多場景下,除了異常監控有用,收集使用者的行為資料同樣有意義。

怎麼定義行為資料?顧名思義,就是使用者在使用產品過程中產生的行為軌跡。比如去過哪幾個頁面,點過哪幾個按鈕,甚至在某個頁面停留了多長時間,某個按鈕點選了多少次,如果有需求都可以記錄下來。

但是記錄行為資料是一個和業務緊密關聯的事情,不可能把每個使用者每一步操作都極其詳細的記錄下來,這樣會產生極其龐大的資料,很顯然不現實。

合理的做法是,根據產品的實際情況評估,哪個模組哪個按鈕需要重點記錄,則可以採集的詳細一些;哪些模組不需要重點關注,則簡單記錄一下基本資訊。

根據這個邏輯,我們可以把行為資料分為兩類:

  • 通用資料
  • 特定資料

下面分別介紹這兩類資料該如何收集。

通用資料

在一個產品中,使用者最基本的行為就是切換頁面。使用者使用了哪些功能,也能從切換頁面中體現出來。因此通用資料一般是在頁面切換時產生,表示某個使用者存取了某個頁面。

頁面切換對應到前端就是路由切換,可以通過監聽路由變化來拿到新頁面的資料。Vue 在全域性路由守衛中監聽路由變化,任意路由切換都能執行這裡的回撥函數。

// Vue3 路由寫法
const router = createRouter({ ... })
router.beforeEach(to => {
  // to 代表新頁面的路由物件
  recordBehaviors(to)
})

React 在元件的 useEffect 中實現相同的功能。不過要注意一點,監聽所有路由變化,則需要所有路由都經過這個元件,監聽才有效果。具體的方法是設定路由時加 * 設定:

import HomePage from '@/pages/Home'
<Route path="*" component={HomePage} />,

然後在這個元件的的 useEffect 中監聽路由變化:

// HomePage.jsx
const { pathname } = useLocation();
useEffect(() => {
  // 路由切換這個函數觸發
  recordBehaviors(pathname);
}, [pathname]);

上面程式碼中,在路由切換時都呼叫了 recordBehaviors() 方法並傳入了引數。Vue 傳的是一個路由物件,React 傳的是路由地址,接下來就可以在這個函數內收集資料了。

明確了在哪裡收集資料,我們還要知道收集哪些資料。收集行為資料最基本的欄位如下:

app:應用的名稱/標識

env:應用環境,一般是開發,測試,生產

version:應用的版本號

user_id:當前使用者 ID

user_name:當前使用者名稱

page_route:頁面路由

page_title:頁面名稱

start_at:進入時間

end_at:離開時間

上面的欄位中,應用標識、環境、版本號統稱應用欄位,用於標誌資料的來源。其他欄位主要分為 使用者頁面時間三類,通過這三類資料就可以簡單的判斷出一件事:誰到過哪個頁面,並停留了多長時間。

應用欄位的設定和獲取方式我們在上一節 搭建前端監控,如何採集異常資料? 中講過,就不做多餘介紹了,獲取欄位的方式都是通用的。

下面介紹其他的幾類資料如何獲取。

獲取使用者資訊

現代前端應用儲存使用者資訊的方式基本都是一樣的,localStorage 存一份,狀態管理裡存一份。因此獲取使用者資訊從這兩處的任意一處獲得即可。這裡簡單介紹下如何從狀態管理中獲取。

最簡單的方法,在函數 recordBehaviors() 所處的 js 檔案中,直接匯入使用者狀態:

// 從狀態管理裡中匯出使用者資料
import { UserStore } from '@/stores';
let { user_id, user_name } = UserStore;

這裡的 @/stores 指向我專案中的檔案 src/stores/index.ts,表示狀態管理的入口檔案,使用時替換成自己專案的實際位置。實際情況中還會有使用者資料為空的問題,這裡需要單獨處理一下,方便我們在後續的資料檢視中能看出分別:

import { UserStore } from '@/stores';
// 收集行為函數
const recordBehaviors = ()=> {
  let report_date = {
    ...
  }
  if(UserStore) {
    let { user_id, user_name} = UserStore
    report_date.user_id = user_id || 0
    report_date.user_name = user_name || '未命名'
  } else {
    report_date.user_id = user_id || -1
    report_date.user_name = user_name || '未獲取'
  }
}

上面程式碼中,首先判斷了狀態管理中是否有使用者資料,如果有則獲取,沒有則指定預設值。這裡指定預設值的細節要注意,不是隨便指定的,比如 user_id 的預設值有如下意義:

  • user_id 為 0:表示有使用者資料,但沒有 user_id 欄位或該欄位為空
  • user_id 為 -1:表示沒有使用者資料,因而 user_id 欄位獲取不到

使用者資料是經常容易出錯的地方,因為涉及到登入狀態和許可權等複雜問題。指定了上述預設值後,就可以從收集到的行為資料中判斷出某個頁面使用者狀態是否正常。

獲取頁面資訊

前面我們在監聽路由變化的地方呼叫了 recordBehaviors 函數並傳入了引數,頁面資訊可以從引數中拿到,我們先看在 Vue 中怎麼獲取:

// 路由設定
{
  path: '/test',
  meta: {
    title: '測試頁面'
  },
  component: () => import('@/views/test/Index.vue')
}
// 獲取設定
const recordBehaviors = (to)=> {
  let page_route = to.path
  let page_title = to.meta.title
}

Vue 中比較簡單,可以直接從引數中拿到頁面資料。相比之下,React 的引數只是一個路由地址,想拿到頁面名稱還需要做單獨處理。

一般在設計許可權時,我們會在伺服器端會維護一套路由資料,包含路由地址和名稱。路由資料在登入後獲取,存在狀態管理中,那麼有了 pathname 就可以從路由資料中找到對應的路由名稱。

// React 中
import { RouteStore } from '@/stores';
const recordBehaviors = (pathname) => {
  let { routers } = RouteStore; // 取出路由資料
  let route = routers.find((row) => (row.path = pathname));
  if (route) {
    let page_route = route.path;
    let page_title = route.title;
  }
};

這樣,頁面資訊的 page_route、page_title 兩個欄位也拿到了。

設定時間

行為資料中用兩個欄位 start_atend_at 分別表示使用者進入頁面和離開頁面的時間。這兩個欄位非常重要,我們在後續使用資料的時候可以判斷出很多資訊,比如:

  • 某個使用者在某個頁面停留了多久?
  • 某個段時間內,某個使用者停留在哪幾個頁面?
  • 某個時間段內,哪個頁面的使用者停留時間最長?
  • 某個頁面,哪些使用者的使用率最高?

還有很多資訊,都能根據這兩個時間欄位判斷。開始時間很好辦,函數觸發時直接獲取當前時間:

var start_at = new Date();

結束時間這裡需要考慮的情況比較多。首先要確定資料什麼時候上報?使用者進入頁面後上報,還是離開頁面時上報?

如果進入頁面時上報,可以保證行為資料一定會被記錄,不會丟失,但此時 end_at 欄位必然為空。這樣的話,就需要在離開頁面時再調介面,將這條記錄的 end_time 更新,這種方式的實現比較麻煩一些:

// 進入頁面時呼叫
const recordBehaviors = () => {
  let report_date = {...} // 此時 end_at 為空
  http.post('/behaviors/insert', report_date).then(res=> {
    let id = res.id // 資料 id
    localStorage.setItem('CURRENT_BEHAVIOR_ID', id)
  })
}
// 離開頁面時呼叫:
const updateBehaviors = ()=> {
  let id = localStorage.getItem('CURRENT_BEHAVIOR_ID')
  let end_at = new Date()
  http.post('/behaviors/update/'+id, end_at) // 根據 id 更新結束時間
  localStorage.removeItem('CURRENT_BEHAVIOR_ID')
}

上面程式碼中,進入頁面先上報資料,並儲存下 id,離開頁面再根據 id 更新這條資料的結束時間。

如果在離開頁面時上報,那麼就要保證離開頁面前上報介面已經觸發,否則會導致資料丟失。在滿足這個前提條件下,上報邏輯會變成這樣:

// 進入頁面時呼叫
const recordBehaviors = () => {
  let report_date = {...} // 此時 end_at 為空
  localStorage.setItem('CURRENT_BEHAVIOR', JSON.stringify(report_date));
}
// 離開頁面時呼叫
const reportBehaviors = () => {
  let end_at = new Date()
  let report_str = localStorage.getItem('CURRENT_BEHAVIOR')
  if(report_str) {
    let report_date = JSON.parse(report_str)
    report_date.end_at = end_at
    http.post('/behaviors/insert', report_date)
  } else {
    console.log('無行為資料')
  }
}

對比一下這兩種方案,第一種的弊端是介面需要調兩次,這會使介面請求量倍增。第二種方案只呼叫一次,但是需要特別注意可靠性處理,總體來說第二種方案更好些。

特定資料

除了通用資料,大部分情況我們還要在具體的頁面中收集某些特定的行為。比如某個關鍵的按鈕有沒有點選,點了多少次;或者某個關鍵區域使用者有沒有看到,看到(曝光)了多少次等等。

收集資料還有一個更專業的叫法 ———— 埋點。直觀理解是,哪裡需要上報資料,就埋一個上報函數進去。

通用資料針對所有頁面自動收集,特定資料就需要根據每個頁面的實際需求手動新增。以一個按鈕為例:

<button onClick={onClick}>點選</button>;
const onClick = (e) => {
  // console.log(e);
  repoerEvents(e);
};

上面程式碼中,我們想記錄這個按鈕的點選情況,所以做了一個簡單的埋點 ———— 在按鈕點選事件中呼叫 repoerEvents() 方法,這個方法內部會收集資料並上報。

這是最原始的埋點方式,直接將上報方法放到事件函數中。repoerEvents() 方法接收一個事件物件引數,在引數中獲取需要上報的事件資料。

特定資料與通用資料的許多欄位是一樣的,收集特定資料需要的基本欄位如下:

app:應用的名稱/標識

env:應用環境,一般是開發,測試,生產

version:應用的版本號

user_id:當前使用者 ID

user_name:當前使用者名稱

page_route:頁面路由

page_title:頁面名稱

created_at:觸發時間

event_type:事件型別

action_tag:行為標識

action_label:行為描述

這些基本欄位中,前 7 個欄位與前面通用資料的獲取完全一樣,這裡就不贅述了。實際上特定資料需要獲取的專有欄位只有 3 個:

event_type:事件型別

action_tag:行為標識

action_label:行為描述

這三個欄位也非常容易獲取。event_type 表示事件觸發的型別,比如點選、捲動、拖動等,可以在事件物件中拿到。action_tag 和 action_label 是必須指定的屬性,表示本次埋點的標識和文字描述,用於在後續的資料處理時方便查閱和統計。

瞭解了採集特定資料是怎麼回事,接下來我們用程式碼實現。

手動埋點上報

假設要為登入按鈕做埋點,按照上面的資料採集方式,我們書寫程式碼如下:

<button data-tag="user_login" data-label="使用者登入" onClick={onClick}>
  登入
</button>;
const onClick = (e) => {
  // console.log(e);
  repoerEvents(e);
};

程式碼中,我們通過元素的自定義屬性傳遞了 tag 和 label 兩個標識,用於在上報函數中獲取。

上報函數 repoerEvents() 程式碼邏輯如下:

// 埋點上報函數
const repoerEvents = (e)=> {
  let report_date = {...}
  let { tag, label } = e.target.dataset
  if(!tag || !label) {
    return new Error('上報元素屬性缺失')
  }
  report_date.event_type = e.type
  report_date.action_tag = tag
  report_date.action_label = label
  // 上報資料
  http.post('/events/insert', report_date)
}

這樣就實現了一個基本的特定資料埋點上報功能。

全域性自動上報

現在我們回過頭來梳理一下這個上報流程,雖然基本功能實現了,但是還有些不合理之處,比如:

  • 必須為元素指定事件處理常式
  • 必須為元素新增自定義屬性
  • 在原有事件處理常式中手動新增埋點,侵入性高

首先我們的埋點方式是基於事件的,也就是說,不管元素本身是否需要事件處理,我們都要給他加上,並在函數內部呼叫 repoerEvents() 方法。如果一個專案需要埋點的地方非常多,這種方式的接入成本就會非常高。

參考之前做異常監控的邏輯,我們換一個思路:能否全域性監聽事件自動上報呢?

思考一下,如果要做全域性監聽事件,那麼只能監聽需要埋點的元素的事件。那麼如何判斷哪些元素需要埋點呢?

上面我們為埋點的元素指定了 data-tag 和 data-label 兩個自定義屬性,那是不是根據這兩個自定義屬性判斷就可以?我們來試驗一下:

window.addEventListener('click', (event) => {
  let { tag, label, trigger } = event.target.dataset;
  if (tag && label && trigger == 'click') {
    // 說明該元素需要埋點
    repoerEvents(event);
  }
});

上面程式碼還多判斷了一個自定義屬性 dataset.trigger,表示元素在哪種事件觸發時需要上報。全域性監聽事件需要這個標識,這樣可避免事件衝突。

新增全域性監聽後,收集某個元素的特定資料就簡單了,方法如下:

<button data-tag="form_save" data-label="表單儲存" data-trigger="click">
  儲存
</button>

試驗證明,上述全域性處理的方式是可行的,這樣的話就不需要在每一個元素上新增或修改事件處理常式了,只需要在元素中新增三個自定義屬性 data-tagdata-labeldata-trigger 就能自動實現資料埋點上報。

元件上報

上面全域性監聽事件上報的方式已經比手動埋點高效了許多,現在我們再換一個場景。

一般情況下當埋點功能成熟之後,會封裝成一個 SDK 供其他專案使用。如果我們將採集資料按照 SDK 的思路實現,讓開發者在全域性監聽事件,是不是一個好的方式呢?

顯然是不太友好的。如果是一個 SDK,那麼最好的方式是將所有內容聚合成一個元件,在元件內實現上報的所有功能,而不是讓使用者在專案中新增監聽事件。

封裝元件的話,那麼元件的功能最好是將要新增埋點的元素包裹,這樣自定義元素也就不需要指定了,而轉為元件的屬性,然後在元件內實現事件監聽。

以 React 為例,我們看一下如何將上面的採集功能封裝為元件:

import { useEffect, useRef } from 'react';
const CusReport = (props) => {
  const dom = useRef(null);
  const handelEvent = () => {
    console.log(props); // {tag:xx, label:xx, trigger:xx}
    repoerEvents(props);
  };
  useEffect(() => {
    if (dom.current instanceof HTMLElement) {
      dom.current.addEventListener(props.trigger, handelEvent);
    }
  }, []);
  return (
    <span ref={dom} className="custom-report">
      {props.children}
    </span>
  );
};
export default CusReport;

元件使用方式如下:

<CusReport tag="test" label="功能測試" trigger="click">
  <button>測試</button>
</CusReport>

這樣就比較優雅了,不需要修改目標元素,只要把元件包裹在目標元素之外即可。

總結

本文介紹了搭建前端監控如何採集行為資料,將資料分為 通用資料 和 特定資料 兩個大類分別處理。同時也介紹了多種上報資料的方式,不同的場景可以選擇不同的方式。

其中的資料部分只介紹了實現功能的基礎欄位,實際情況中可以根據自己的業務需求新增。

許多小夥伴留言這套前端監控能否開源,肯定是要開源的,不過內容比較多我還在做,等到基本完善了我會發一個版本,感謝小夥伴們的關注。

本系列文章如下

以上就是JS前端監控採集使用者行為的N種姿勢的詳細內容,更多關於JS前端監控採集使用者行為的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com