搭建一個視覺化使用者行為軌跡打點體系SDK

墨跡發表於2022-02-26

一、基本原理

● 1、利用xpath的唯一性,繫結打點元素新增事件進行傳送資料打點
● 2、後臺管理系統搭建一個視覺化選取打點元素的功能並儲存配置
● 3、前端根據頁面URL獲取到打點配置進行初始化(通過xpath繫結事件)
基本流程如圖所示:

二、前端傳送打點資料方式

前端有幾種方案進行傳送打點資料

1、傳統ajax請求

利用傳統的ajax請求進行傳送資料,缺點是容易阻塞請求,對使用者不友好
而且弊端很大,使用者關閉頁面時會截斷請求,也就是傳送會終止掉,用於記錄瀏覽時長不適用
axios.post(url, data); // 以axios為例

2、動態圖片

我們可以通過在 beforeunload 事件處理器中建立一個圖片元素並設定它的 src 屬性的方法來延遲解除安裝以保證資料的傳送,因為絕大多數瀏覽器會延遲解除安裝以保證圖片的載入,所以資料可以在解除安裝事件中傳送。

const sendLog = (url, data) => {
  let img = document.createElement('img');
  const params = [];
  Object.keys(data).forEach((key) => {
    params.push(`${key}=${encodeURIComponent(data[key])}`);
  });
  img.onload = () => img = null;
  img.src = `${url}?${params.join('&')}`;
};

3. sendBeacon

為了解決上述問題,便有了 navigator.sendBeacon 方法,使用該方法傳送請求,可以保證資料有效送達,且不會阻塞頁面的解除安裝或載入,並且編碼比起上述方法更加簡單。

export const sendBeacon = (url, analyticsData) => {
    const apiUrl = config.apiRoot + url
    let data = getParams(analyticsData)
    // 相容不支援sendBeacon的瀏覽器
    if (!navigator.sendBeacon) {
        const client = new XMLHttpRequest()
        // 第三個參數列示同步傳送
        client.open('POST', apiUrl, false)
        client.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
        client.send(data)
        return
    }
    const formData = new FormData()
    Object.keys(analyticsData).forEach((key) => {
        let value = analyticsData[key]
        if (typeof value !== 'string') {
            // formData只能append string 或 Blob
            value = JSON.stringify(value)
        }
        formData.append(key, value)
    })
    navigator.sendBeacon(apiUrl, formData)
}

最後我們使用了動態圖片的方式,因為阿里雲提供的阿里雲-採集-通過WebTracking採集日誌應對大量資料採集不造成網站本身伺服器壓力

三、搭建SDK

利用webpack搭建專案,打包出單個sdk的js檔案包,前端引入sdk即可(此部分不做贅述了,感興趣可以搜尋webpack相關資料)用 webpack 寫一個簡單的 JS SDK
● 視覺化選取xpath-參考外掛
SDK的主要功能:

  1. 暴露出初始化方法,以及打點的方法(為了支援手動打點)
  2. 新增選取xpath功能,並暴露給後臺管理系統使用
  3. 根據連結URL讀取到打點配置列表
  4. 初始化繫結打點事件功能
  5. 進入頁面記一次打點
  6. 記錄瀏覽時長
  7. SDK與父級iframe通訊功能(為了傳遞資料給後臺管理系統)
    記錄遊覽時長示例:
// 統計時長
const viewTime = (data) => {
  let startTime = new Date().getTime() // 瀏覽開始時間
  let endTime = null // 瀏覽結束時間
  // 頁面解除安裝觸發
  window.addEventListener('unload', () => {
    endTime = new Date().getTime()
    let params = {
      viewTime: (endTime - startTime) / 1000,
      eventType: 'view',
      accessId: ACCESS_ID
    }
    params = Object.assign(params, data)
    sendLog(params)
  }, false)
}
// 選取xpath跨域跨頁面通訊
import Postmate from 'postmate'
import Inspector from '../plugins/inspect' // 選取xpath節點外掛

let childIframe = null
const myInspect = new Inspector()
const getXpathForm = function (options) {
    myInspect.setOptions(options, (data) => {
        let params = {
            xpath: data,
            route: window.QWK_ANALYSIS_SDK_OPTIONS?.route || ''
        }
        childIframe.emit('send-data-event', params)
    })
}
export default {
    // 和父級iframe通訊
    initMessage () {
        // 開發模式下啟用選節點除錯
        if (process.env.BUILD_ENV === 'dev') {
            document.querySelector('#selected').onclick = () => {
                myInspect.setOptions({
                    deactivate: true
                }, (data) => {
                    console.log(data)
                })
            }
        }
        const handshake = new Postmate.Model({
            // iframe父級獲取xpath
            getXpath: (options) => {
                getXpathForm(options)
            },
            // 移除選取
            deactivate: () => {
                myInspect.deactivate()
            }
        })
        // When parent <-> child handshake is complete, events may be emitted to the parent
        handshake.then((child) => {
            childIframe = child
        })
    }
}
// 匯出SDK
// main.js入口檔案
import { init } from './lib/init'
import action from './lib/action'
import selectXpath from './lib/select-xpath'
// import { documentReady } from './plugins/common'

// 初始化選取xpath的跨域通訊
selectXpath.initMessage()

// documentReady(() => {
//     // 初始化
//     // init().then(res => res)
// })

// 匯出SDK模組
export {
    init,
    action
}

四、後臺管理系統搭建視覺化選取xpath

第一步:第三方網站引入SDK

在sdk中寫一個選取xpath功能並暴露出來給後臺管理系統呼叫
● 視覺化選取xpath-參考外掛

第二步:搭建管理系統

搭建一個載入網站的iframe,如圖所示:
image.png
我們需要在這裡呼叫SDK中的選取網站xpath功能的方法,這就必須和載入的iframe中的網站通訊
因為是iframe載入的第三方網站,會有跨域問題,所以我們需要一個外掛來實現這一功能 postmate
GitHub連結

<template>
    <div class="iframe-box" ref="content">
    </div>
</template>

<script>
    import Postmate from 'postmate'

    export default {
        name: 'WebIframe',
        props: {
            src: {
                type: String,
                default: ''
            },
            options: {
                type: Object,
                default: () => ({})
            }
        },
        data () {
            return {
                $child: null
            }
        },
        mounted () {
            this.$nextTick(() => this.initMessage())
        },
        methods: {
            initMessage () {
                let handshake = new Postmate({
                    container: this.$refs.content,
                    url: this.src,
                    name: 'web-iframe-name',
                    classListArray: ['iframe-class-content'],
                })
                handshake.then((child) => {
                    this.$child = child
                    child.on('send-data-event', (data) => {
                        this.$emit('selected', data)
                    })
                })
            },
            // 獲取選取xpath
            getXpath () {
                let options = {
                    clipboard: false, // 是否自動複製
                    deactivate: true, // 選擇之後銷燬
                    ...this.options
                }
                try {
                    this.$child.call('getXpath', options)
                } catch (e) {
                    this.$errMsg('載入SDK失敗,請重新載入網站試試~也可能當前網站未引入使用者行為軌跡跟蹤SDK,請聯絡相關人員新增')
                }
            },
            // 移除選取彈層
            remove () {
                this.$child && this.$child.call('deactivate')
            }
        }
    }
</script

選取節點結果,效果如圖:
image.png

五、遇到的問題以及解決方案

1、後臺管理系統iframe載入第三方網站通訊問題

這裡因為是通過iframe來載入第三方網站進行視覺化打點的,所以需要父級iframe和第三方網站進行通訊,但是會有跨域問題,跨域問題解決方案有很多種,這裡使用基於postmessage的第三方外掛postmate來解決

2、動態路由問題

遇到比如文章詳情的頁面,因為文章詳情會有很多的連結 https://www.baidu.com/article... 像這樣的,後面的detail/id跟隨著很多id,這樣的頁面不可能每篇文章都去配置一下的,這樣就需要做動態路由配置統一的動態引數用其他字元標識去載入配置。
方案:
①、在配置中新增動態路由標識,通過後端去讀取資料庫進行匹配動態路由(需要後端去做大量匹配)
②、純前端操作,前端sdk和後臺管理系統互相傳遞動態路由
經過討論,我們選用了第二種,動態路由通過前端初始化sdk的時候去傳入,這時候sdk中接收到傳進來的動態路由,就根據這個路由去載入配置,SDK傳遞給後臺視覺化選取xpath配置,也要通過這個路由去儲存配置
這時候兩邊就可以一一對應上了。我們定義了這樣的路由 https://www.baidu.com/article...{id},其中{id}為動態引數

3、動態節點繫結不到事件問題

由於動態節點是在載入頁面時dom沒有生成,這時候就初始化去繫結事件是查詢不到dom節點的,因此該節點的打點就是失效的。為了解決這個問題,我們可以通過全域性點選事件去查詢這個節點,利用document的點選去查詢這個動態節點,然後得到當前點選的target對比xpath查詢到的節點相等,說明當前點選的節點就是xpath需要繫結事件的節點,此時傳送對應的資料即可

let { data } = await getConfig(route)
let eventList = data.filter((m) => m.eventType !== 'visible')
let viewList = data.filter((m) => m.eventType === 'visible')
let dynamicList = [] // 動態生成的節點
// 點選事件或者其他事件
eventList.forEach((item) => {
  const node = item.xpath && document.evaluate(item.xpath, document).iterateNext()
  if (!node) {
    // 找不到節點,說明有可能是動態生成的節點
    dynamicList.push(item)
    return
  }
  node.addEventListener(item.eventType || 'click', () => {
    action.track(item)
  })
})
// 通過document的點選開查詢動態生成的節點
let dynamicClickList = dynamicList.filter((m) => m.eventType === 'click')
if (dynamicClickList && dynamicClickList.length) {
  document.onclick = (event) => {
    const target = event.target || window.event.target
    const parentNode = target.parentNode
    for (let item of dynamicClickList) {
      // 先把查詢到的節點給存下來
      item.node = document.evaluate(item.xpath, document).iterateNext() || item.node
    }
    let xpathItem = dynamicClickList.find((m) => {
      return m.xpath && (target === m.node || parentNode === m.node)
    })
    // 查詢到節點,傳送打點
    xpathItem && delete xpathItem.node && action.track(xpathItem)
  }
}

4、目標進入可視區域

使用場景:有些橫向滾動切換的元素,需要目標進入到使用者可見的區域時進行打點,於是就有這樣的需求
IntersectionObserver 參考文件連結

let observer = null // 可視區域
let isTrackList = [] // 已經打點過的
if ('IntersectionObserver' in window) {
  // 建立一個監聽節點可視區域
  observer = new IntersectionObserver(entries => {
    const image = entries[0]
    // 進入可視區域
    if (image.isIntersecting) {
      // 當前可視區域的打點配置
      let current = viewList.find((m) => m.xpath && image.target === document.evaluate(m.xpath, document).iterateNext())
      // 已經打點過的
      let trackEd = isTrackList.find((m) => m.id === current.id)
      // 已經打過的點不再打
      if (current && !trackEd) {
        isTrackList.push(current)
        action.track(current)
      }
    }
  })
}
viewList.forEach((item) => {
  const node = item.xpath && document.evaluate(item.xpath, document).iterateNext()
  if (!node) {
    return
  }
  // 監聽節點
  observer.observe(node)
})

相關文章