<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
上一篇我們詳細介紹了前端如何採集異常資料。採集異常資料是為了隨時監測線上專案的執行情況,發現問題及時修復。在很多場景下,除了異常監控有用,收集使用者的行為資料同樣有意義。
怎麼定義行為資料?顧名思義,就是使用者在使用產品過程中產生的行為軌跡。比如去過哪幾個頁面,點過哪幾個按鈕,甚至在某個頁面停留了多長時間,某個按鈕點選了多少次,如果有需求都可以記錄下來。
但是記錄行為資料是一個和業務緊密關聯的事情,不可能把每個使用者每一步操作都極其詳細的記錄下來,這樣會產生極其龐大的資料,很顯然不現實。
合理的做法是,根據產品的實際情況評估,哪個模組哪個按鈕需要重點記錄,則可以採集的詳細一些;哪些模組不需要重點關注,則簡單記錄一下基本資訊。
根據這個邏輯,我們可以把行為資料分為兩類:
下面分別介紹這兩類資料該如何收集。
在一個產品中,使用者最基本的行為就是切換頁面。使用者使用了哪些功能,也能從切換頁面中體現出來。因此通用資料一般是在頁面切換時產生,表示某個使用者存取了某個頁面。
頁面切換對應到前端就是路由切換,可以通過監聽路由變化來拿到新頁面的資料。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_at
、end_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-tag
,data-label
,data-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其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45