溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JS前端監(jiān)控采集用戶行為的方法有哪些

發(fā)布時(shí)間:2022-07-22 11:42:08 來源:億速云 閱讀:102 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容介紹了“JS前端監(jiān)控采集用戶行為的方法有哪些”的有關(guān)知識(shí),在實(shí)際案例的操作過程中,不少人都會(huì)遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

    通用數(shù)據(jù)

    在一個(gè)產(chǎn)品中,用戶最基本的行為就是切換頁面。用戶使用了哪些功能,也能從切換頁面中體現(xiàn)出來。因此通用數(shù)據(jù)一般是在頁面切換時(shí)產(chǎn)生,表示某個(gè)用戶訪問了某個(gè)頁面。

    頁面切換對(duì)應(yīng)到前端就是路由切換,可以通過監(jiān)聽路由變化來拿到新頁面的數(shù)據(jù)。Vue 在全局路由守衛(wèi)中監(jiān)聽路由變化,任意路由切換都能執(zhí)行這里的回調(diào)函數(shù)。

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

    React 在組件的 useEffect 中實(shí)現(xiàn)相同的功能。不過要注意一點(diǎn),監(jiān)聽所有路由變化,則需要所有路由都經(jīng)過這個(gè)組件,監(jiān)聽才有效果。具體的方法是配置路由時(shí)加 * 配置:

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

    然后在這個(gè)組件的的 useEffect 中監(jiān)聽路由變化:

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

    上面代碼中,在路由切換時(shí)都調(diào)用了 recordBehaviors() 方法并傳入了參數(shù)。Vue 傳的是一個(gè)路由對(duì)象,React 傳的是路由地址,接下來就可以在這個(gè)函數(shù)內(nèi)收集數(shù)據(jù)了。

    明確了在哪里收集數(shù)據(jù),我們還要知道收集哪些數(shù)據(jù)。收集行為數(shù)據(jù)最基本的字段如下:

    app:應(yīng)用的名稱/標(biāo)識(shí)

    env:應(yīng)用環(huán)境,一般是開發(fā),測(cè)試,生產(chǎn)

    version:應(yīng)用的版本號(hào)

    user_id:當(dāng)前用戶 ID

    user_name:當(dāng)前用戶名

    page_route:頁面路由

    page_title:頁面名稱

    start_at:進(jìn)入時(shí)間

    end_at:離開時(shí)間

    上面的字段中,應(yīng)用標(biāo)識(shí)、環(huán)境、版本號(hào)統(tǒng)稱應(yīng)用字段,用于標(biāo)志數(shù)據(jù)的來源。其他字段主要分為 用戶,頁面,時(shí)間三類,通過這三類數(shù)據(jù)就可以簡(jiǎn)單的判斷出一件事:誰到過哪個(gè)頁面,并停留了多長時(shí)間。

    應(yīng)用字段的配置和獲取方式我們?cè)谏弦还?jié) 搭建前端監(jiān)控,如何采集異常數(shù)據(jù)? 中講過,就不做多余介紹了,獲取字段的方式都是通用的。

    下面介紹其他的幾類數(shù)據(jù)如何獲取。

    獲取用戶信息

    現(xiàn)代前端應(yīng)用存儲(chǔ)用戶信息的方式基本都是一樣的,localStorage 存一份,狀態(tài)管理里存一份。因此獲取用戶信息從這兩處的任意一處獲得即可。這里簡(jiǎn)單介紹下如何從狀態(tài)管理中獲取。

    最簡(jiǎn)單的方法,在函數(shù) recordBehaviors() 所處的 js 文件中,直接導(dǎo)入用戶狀態(tài):

    // 從狀態(tài)管理里中導(dǎo)出用戶數(shù)據(jù)
    import { UserStore } from '@/stores';
    let { user_id, user_name } = UserStore;

    這里的 @/stores 指向我項(xiàng)目中的文件 src/stores/index.ts,表示狀態(tài)管理的入口文件,使用時(shí)替換成自己項(xiàng)目的實(shí)際位置。實(shí)際情況中還會(huì)有用戶數(shù)據(jù)為空的問題,這里需要單獨(dú)處理一下,方便我們?cè)诤罄m(xù)的數(shù)據(jù)查看中能看出分別:

    import { UserStore } from '@/stores';
    // 收集行為函數(shù)
    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 || '未獲取'
      }
    }

    上面代碼中,首先判斷了狀態(tài)管理中是否有用戶數(shù)據(jù),如果有則獲取,沒有則指定默認(rèn)值。這里指定默認(rèn)值的細(xì)節(jié)要注意,不是隨便指定的,比如 user_id 的默認(rèn)值有如下意義:

    • user_id 為 0:表示有用戶數(shù)據(jù),但沒有 user_id 字段或該字段為空

    • user_id 為 -1:表示沒有用戶數(shù)據(jù),因而 user_id 字段獲取不到

    用戶數(shù)據(jù)是經(jīng)常容易出錯(cuò)的地方,因?yàn)樯婕暗降卿洜顟B(tài)和權(quán)限等復(fù)雜問題。指定了上述默認(rèn)值后,就可以從收集到的行為數(shù)據(jù)中判斷出某個(gè)頁面用戶狀態(tài)是否正常。

    獲取頁面信息

    前面我們?cè)诒O(jiān)聽路由變化的地方調(diào)用了 recordBehaviors 函數(shù)并傳入了參數(shù),頁面信息可以從參數(shù)中拿到,我們先看在 Vue 中怎么獲?。?/p>

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

    Vue 中比較簡(jiǎn)單,可以直接從參數(shù)中拿到頁面數(shù)據(jù)。相比之下,React 的參數(shù)只是一個(gè)路由地址,想拿到頁面名稱還需要做單獨(dú)處理。

    一般在設(shè)計(jì)權(quán)限時(shí),我們會(huì)在服務(wù)端會(huì)維護(hù)一套路由數(shù)據(jù),包含路由地址和名稱。路由數(shù)據(jù)在登錄后獲取,存在狀態(tài)管理中,那么有了 pathname 就可以從路由數(shù)據(jù)中找到對(duì)應(yīng)的路由名稱。

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

    這樣,頁面信息的 page_route、page_title 兩個(gè)字段也拿到了。

    設(shè)置時(shí)間

    行為數(shù)據(jù)中用兩個(gè)字段 start_at、end_at 分別表示用戶進(jìn)入頁面和離開頁面的時(shí)間。這兩個(gè)字段非常重要,我們?cè)诤罄m(xù)使用數(shù)據(jù)的時(shí)候可以判斷出很多信息,比如:

    • 某個(gè)用戶在某個(gè)頁面停留了多久?

    • 某個(gè)段時(shí)間內(nèi),某個(gè)用戶停留在哪幾個(gè)頁面?

    • 某個(gè)時(shí)間段內(nèi),哪個(gè)頁面的用戶停留時(shí)間最長?

    • 某個(gè)頁面,哪些用戶的使用率最高?

    還有很多信息,都能根據(jù)這兩個(gè)時(shí)間字段判斷。開始時(shí)間很好辦,函數(shù)觸發(fā)時(shí)直接獲取當(dāng)前時(shí)間:

    var start_at = new Date();

    結(jié)束時(shí)間這里需要考慮的情況比較多。首先要確定數(shù)據(jù)什么時(shí)候上報(bào)?用戶進(jìn)入頁面后上報(bào),還是離開頁面時(shí)上報(bào)?

    如果進(jìn)入頁面時(shí)上報(bào),可以保證行為數(shù)據(jù)一定會(huì)被記錄,不會(huì)丟失,但此時(shí) end_at 字段必然為空。這樣的話,就需要在離開頁面時(shí)再調(diào)接口,將這條記錄的 end_time 更新,這種方式的實(shí)現(xiàn)比較麻煩一些:

    // 進(jìn)入頁面時(shí)調(diào)用
    const recordBehaviors = () => {
      let report_date = {...} // 此時(shí) end_at 為空
      http.post('/behaviors/insert', report_date).then(res=> {
        let id = res.id // 數(shù)據(jù) id
        localStorage.setItem('CURRENT_BEHAVIOR_ID', id)
      })
    }
    // 離開頁面時(shí)調(diào)用:
    const updateBehaviors = ()=> {
      let id = localStorage.getItem('CURRENT_BEHAVIOR_ID')
      let end_at = new Date()
      http.post('/behaviors/update/'+id, end_at) // 根據(jù) id 更新結(jié)束時(shí)間
      localStorage.removeItem('CURRENT_BEHAVIOR_ID')
    }

    上面代碼中,進(jìn)入頁面先上報(bào)數(shù)據(jù),并保存下 id,離開頁面再根據(jù) id 更新這條數(shù)據(jù)的結(jié)束時(shí)間。

    如果在離開頁面時(shí)上報(bào),那么就要保證離開頁面前上報(bào)接口已經(jīng)觸發(fā),否則會(huì)導(dǎo)致數(shù)據(jù)丟失。在滿足這個(gè)前提條件下,上報(bào)邏輯會(huì)變成這樣:

    // 進(jìn)入頁面時(shí)調(diào)用
    const recordBehaviors = () => {
      let report_date = {...} // 此時(shí) end_at 為空
      localStorage.setItem('CURRENT_BEHAVIOR', JSON.stringify(report_date));
    }
    // 離開頁面時(shí)調(diào)用
    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('無行為數(shù)據(jù)')
      }
    }

    對(duì)比一下這兩種方案,第一種的弊端是接口需要調(diào)兩次,這會(huì)使接口請(qǐng)求量倍增。第二種方案只調(diào)用一次,但是需要特別注意可靠性處理,總體來說第二種方案更好些。

    特定數(shù)據(jù)

    除了通用數(shù)據(jù),大部分情況我們還要在具體的頁面中收集某些特定的行為。比如某個(gè)關(guān)鍵的按鈕有沒有點(diǎn)擊,點(diǎn)了多少次;或者某個(gè)關(guān)鍵區(qū)域用戶有沒有看到,看到(曝光)了多少次等等。

    收集數(shù)據(jù)還有一個(gè)更專業(yè)的叫法 &mdash;&mdash;&mdash;&mdash; 埋點(diǎn)。直觀理解是,哪里需要上報(bào)數(shù)據(jù),就埋一個(gè)上報(bào)函數(shù)進(jìn)去。

    通用數(shù)據(jù)針對(duì)所有頁面自動(dòng)收集,特定數(shù)據(jù)就需要根據(jù)每個(gè)頁面的實(shí)際需求手動(dòng)添加。以一個(gè)按鈕為例:

    <button onClick={onClick}>點(diǎn)擊</button>;
    const onClick = (e) => {
      // console.log(e);
      repoerEvents(e);
    };

    上面代碼中,我們想記錄這個(gè)按鈕的點(diǎn)擊情況,所以做了一個(gè)簡(jiǎn)單的埋點(diǎn) &mdash;&mdash;&mdash;&mdash; 在按鈕點(diǎn)擊事件中調(diào)用 repoerEvents() 方法,這個(gè)方法內(nèi)部會(huì)收集數(shù)據(jù)并上報(bào)。

    這是最原始的埋點(diǎn)方式,直接將上報(bào)方法放到事件函數(shù)中。repoerEvents() 方法接收一個(gè)事件對(duì)象參數(shù),在參數(shù)中獲取需要上報(bào)的事件數(shù)據(jù)。

    特定數(shù)據(jù)與通用數(shù)據(jù)的許多字段是一樣的,收集特定數(shù)據(jù)需要的基本字段如下:

    app:應(yīng)用的名稱/標(biāo)識(shí)

    env:應(yīng)用環(huán)境,一般是開發(fā),測(cè)試,生產(chǎn)

    version:應(yīng)用的版本號(hào)

    user_id:當(dāng)前用戶 ID

    user_name:當(dāng)前用戶名

    page_route:頁面路由

    page_title:頁面名稱

    created_at:觸發(fā)時(shí)間

    event_type:事件類型

    action_tag:行為標(biāo)識(shí)

    action_label:行為描述

    這些基本字段中,前 7 個(gè)字段與前面通用數(shù)據(jù)的獲取完全一樣,這里就不贅述了。實(shí)際上特定數(shù)據(jù)需要獲取的專有字段只有 3 個(gè):

    event_type:事件類型

    action_tag:行為標(biāo)識(shí)

    action_label:行為描述

    這三個(gè)字段也非常容易獲取。event_type 表示事件觸發(fā)的類型,比如點(diǎn)擊、滾動(dòng)、拖動(dòng)等,可以在事件對(duì)象中拿到。action_tag 和 action_label 是必須指定的屬性,表示本次埋點(diǎn)的標(biāo)識(shí)和文字描述,用于在后續(xù)的數(shù)據(jù)處理時(shí)方便查閱和統(tǒng)計(jì)。

    了解了采集特定數(shù)據(jù)是怎么回事,接下來我們用代碼實(shí)現(xiàn)。

    手動(dòng)埋點(diǎn)上報(bào)

    假設(shè)要為登錄按鈕做埋點(diǎn),按照上面的數(shù)據(jù)采集方式,我們書寫代碼如下:

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

    代碼中,我們通過元素的自定義屬性傳遞了 tag 和 label 兩個(gè)標(biāo)識(shí),用于在上報(bào)函數(shù)中獲取。

    上報(bào)函數(shù) repoerEvents() 代碼邏輯如下:

    // 埋點(diǎn)上報(bào)函數(shù)
    const repoerEvents = (e)=> {
      let report_date = {...}
      let { tag, label } = e.target.dataset
      if(!tag || !label) {
        return new Error('上報(bào)元素屬性缺失')
      }
      report_date.event_type = e.type
      report_date.action_tag = tag
      report_date.action_label = label
      // 上報(bào)數(shù)據(jù)
      http.post('/events/insert', report_date)
    }

    這樣就實(shí)現(xiàn)了一個(gè)基本的特定數(shù)據(jù)埋點(diǎn)上報(bào)功能。

    全局自動(dòng)上報(bào)

    現(xiàn)在我們回過頭來梳理一下這個(gè)上報(bào)流程,雖然基本功能實(shí)現(xiàn)了,但是還有些不合理之處,比如:

    • 必須為元素指定事件處理函數(shù)

    • 必須為元素添加自定義屬性

    • 在原有事件處理函數(shù)中手動(dòng)添加埋點(diǎn),侵入性高

    首先我們的埋點(diǎn)方式是基于事件的,也就是說,不管元素本身是否需要事件處理,我們都要給他加上,并在函數(shù)內(nèi)部調(diào)用 repoerEvents() 方法。如果一個(gè)項(xiàng)目需要埋點(diǎn)的地方非常多,這種方式的接入成本就會(huì)非常高。

    參考之前做異常監(jiān)控的邏輯,我們換一個(gè)思路:能否全局監(jiān)聽事件自動(dòng)上報(bào)呢?

    思考一下,如果要做全局監(jiān)聽事件,那么只能監(jiān)聽需要埋點(diǎn)的元素的事件。那么如何判斷哪些元素需要埋點(diǎn)呢?

    上面我們?yōu)槁顸c(diǎn)的元素指定了 data-tag 和 data-label 兩個(gè)自定義屬性,那是不是根據(jù)這兩個(gè)自定義屬性判斷就可以?我們來試驗(yàn)一下:

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

    上面代碼還多判斷了一個(gè)自定義屬性 dataset.trigger,表示元素在哪種事件觸發(fā)時(shí)需要上報(bào)。全局監(jiān)聽事件需要這個(gè)標(biāo)識(shí),這樣可避免事件沖突。

    添加全局監(jiān)聽后,收集某個(gè)元素的特定數(shù)據(jù)就簡(jiǎn)單了,方法如下:

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

    試驗(yàn)證明,上述全局處理的方式是可行的,這樣的話就不需要在每一個(gè)元素上添加或修改事件處理函數(shù)了,只需要在元素中添加三個(gè)自定義屬性 data-tag,data-labeldata-trigger 就能自動(dòng)實(shí)現(xiàn)數(shù)據(jù)埋點(diǎn)上報(bào)。

    組件上報(bào)

    上面全局監(jiān)聽事件上報(bào)的方式已經(jīng)比手動(dòng)埋點(diǎn)高效了許多,現(xiàn)在我們?cè)贀Q一個(gè)場(chǎng)景。

    一般情況下當(dāng)埋點(diǎn)功能成熟之后,會(huì)封裝成一個(gè) SDK 供其他項(xiàng)目使用。如果我們將采集數(shù)據(jù)按照 SDK 的思路實(shí)現(xiàn),讓開發(fā)者在全局監(jiān)聽事件,是不是一個(gè)好的方式呢?

    顯然是不太友好的。如果是一個(gè) SDK,那么最好的方式是將所有內(nèi)容聚合成一個(gè)組件,在組件內(nèi)實(shí)現(xiàn)上報(bào)的所有功能,而不是讓使用者在項(xiàng)目中添加監(jiān)聽事件。

    封裝組件的話,那么組件的功能最好是將要添加埋點(diǎn)的元素包裹,這樣自定義元素也就不需要指定了,而轉(zhuǎn)為組件的屬性,然后在組件內(nèi)實(shí)現(xiàn)事件監(jiān)聽。

    以 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="功能測(cè)試" trigger="click">
      <button>測(cè)試</button>
    </CusReport>

    這樣就比較優(yōu)雅了,不需要修改目標(biāo)元素,只要把組件包裹在目標(biāo)元素之外即可。

    “JS前端監(jiān)控采集用戶行為的方法有哪些”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    js
    AI