前端监控SDK(三):深入行为监控

167 阅读4分钟

前端SDK系列文章:

本文是前端监控SDK系列的第三篇,主要介绍用户访问页面时,最常见的几个指标:

  • pv
  • uv
  • 曝光

PV

pv 上报通常有以下几种场景:

  • 刷新页面
  • A 页面跳转到 B 页面
  • 页面前进、后退:如点击浏览器的前进、后退按钮,或者调用 history.go(-1), history.go(1)

spa 应用在跳转后都会刷新页面,监听 windowload 事件即可

window.addEventListener('load',() => {
	const href = window.location.href
	report({
    eventType: 'pv',
    action: 'reload',
    from: href,
    to: href
  })
})

spa 应用中,通过以下俩种方法来触发跳转:

  • hash 模式:更改页面 hash
  • history 模式:调用 pushStatereplaceState 更改页面 url

但是,这俩种方法都不会刷新页面

对于 hash 模式,可以通过监听 hashchange 事件来进行上报:

listen(window, 'hashchange', (ev: HashChangeEvent) => {
	// 在 hashchange之前触发过 popstate 事件,此时在 hashchange 事件中不用再次上报
  if (popstateFired) {
    popstateFired = false
  } else {
    const { oldURL, newURL } = ev
    report({
      eventType: 'pv',
      action: 'navigation',
      from: oldURL,
      to: newURL
    })
    from = newURL
  }
})

对于 history 模式,可以重写 pushStatereplaceState 函数,在重写函数中上报页面信息:

let from = getLocationHref()
let popstateFired = false
const historyReplacer = (origin: Function) => {
  return function (this: Window['history'], state: object, unused: string, url?: string) {
    popstateFired = false
    const result = origin.call(this, state, unused, url)
    const to = getLocationHref()
    report({
      eventType: 'pv',
      action: 'navigation',
      from,
      to
    })
    from = to
    return result
  }
}
replaceAop(window.history, 'pushState', historyReplacer)

replaceAop(window.history, 'replaceState', historyReplacer)

history 模式在前进、后退时会触发 popstate 事件,所以在 popstate 事件中上报前进、后退引发的跳转:

listen(window, 'popstate', () => {
  popstateFired = true
  const to = getLocationHref()
  report({
    eventType: 'pv',
    action: 'navigation',
    from,
    to
  })
  from = to
})

需要注意的是, popstate 不仅会在 history 模式的前进、后退情况下触发,在 hash 值更改时也会触发,并且**会在 hashchange 前触发,**这样便会在 hash 模式下上报俩次。

为了避免上述的这种情况,我设置了 popstateFired 的变量来判断 popstate 事件是否已经被触发,如果是的话就不会在 hashchange 中再次上报了:

https://cdn.jsdelivr.net/gh/wangkaiwd/drawing-bed/images/202410201825636.png

相关的文档可以看这里:

UV

uv 的统计由服务端进行计算,通过上报 pvvisitorId 来得到 uv 的数据

前端可以通过浏览器指纹库github.com/jackspirou/… 来生成 visitorId 传给服务端,用来区分不同用户。如果用户上传了 userId ,则使用 userId 来区分不同用户

元素曝光

元素的曝光主要有以下几种场景:

  • 元素显示、隐藏
  • 新增、删除元素
  • 元素从屏幕外进入屏幕内

浏览器提供了 Intersection Observer API 来检测元素是否在屏幕可见,详细的介绍可以阅读下面的文章:

我也写了一个简单的 demo ,可以结合文章一起阅读,加深理解:stackblitz.com/edit/vitejs…

当监听的元素在屏幕中的可见性发生变化时,会触发传递给 Intersection Observer API的回调函数。回调函数接收 IntersectionObserverEntry 列表作为参数,IntersectionObserverEntry 中的 intersectionRatio 属性,表示元素和屏幕的交叉比例,取值范围为 [0, 1]

intersectionRatio > 0 时,便认为元素出现在了屏幕中。当 intersectionRatio === 0 时, 表示元素在屏幕中消失:

export const createIntersection = (
  el: Element, params?: Record<string, any>, options?: IntersectionObserverInit) => {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
	    // 元素出现在屏幕中,上报曝光信息
      if (entry.intersectionRatio > 0) {
        const target = getTarget(entry as any)
        const reportParams: Omit<EventInfo, 'triggerTime'> = { eventType: 'expose', target }
        if (params) {
          reportParams.params = params
        }
        report(reportParams)
      } else {
	      // 元素在屏幕中消失
      }
    })
  }, options)
  observer.observe(el)
  return observer
}

在原生 js 中,需要手动为需要曝光的元素调用 createIntersection 。在动态新增曝光元素时,也要为新增的曝光元素调用 createIntersection

const box = document.querySelector('.box')!
const expose = document.querySelector('.expose')!
const cancelExpose = document.querySelector('.cancel-expose')!

let boxIntersectionObserver: IntersectionObserver | null = null
expose.addEventListener('click', () => {
  if (boxIntersectionObserver) {
    boxIntersectionObserver.unobserve(box)
  }
  boxIntersectionObserver = createIntersection(box)
})

cancelExpose.addEventListener('click', () => {
  boxIntersectionObserver?.unobserve(box)
})

如果使用的是 react 框架,可以封装 IntersectionContainer 组件来曝光想要曝光的元素:

import React, { useEffect, useRef } from 'react'
import { createIntersection } from '@monitor-fe/core'

interface IntersectionContainerProps {
  children: React.ReactNode;
  className?: string;
  style?: React.CSSProperties;
  exposeParams?: Record<string, any>
}

const IntersectionContainer = (props: IntersectionContainerProps) => {
  const { children, exposeParams, ...rest } = props
  const containerRef = useRef<HTMLDivElement | null>(null)
  useEffect(() => {
    if (!containerRef.current) { return }
    const observer = createIntersection(containerRef.current, exposeParams)
    return () => {
      if (containerRef.current) {
        observer.unobserve(containerRef.current)
      }
    }
  }, [])
  return (
    <div ref={containerRef} {...rest}>
      {children}
    </div>
  )
}
export default IntersectionContainer
import { IntersectionContainer } from '@monitor-fe/react'

const ExposeDemo = () => {
  return (
    <div>
      <div style={{ height: 2000 }}>
        Placeholder
      </div>
      <IntersectionContainer
        style={{
          height: 100,
          width: 100,
          backgroundColor: 'pink',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center'
        }}
      >
        box
      </IntersectionContainer>
    </div>
  )
}

export default ExposeDemo

在创建 intersectionObserver 时,支持传入 threshold 参数,当监听元素与屏幕视口的交叉比例达到 threshold 设置的比例时,触发监听函数。通过 threshold 参数,我们便可以根据需求来自己定义监听元素与屏幕的交叉比例来判断元素是否满足曝光条件。

如设置 shreshold: 0.5 ,表示监听元素有超过一半出现在屏幕中时,才会认为其满足曝光条件

小结

要实现 pv 上报,要了解浏览器页面导航的相关事件:

  • load
  • hashchange
  • popstate
  • pushState , replaceState

页面的访问 uv 要结合 userId 以及浏览器指纹来处理

对于元素曝光,浏览器提供了 Intersection Observer API 可以很方便的判断元素是否在屏幕中,并且提供了 threshold 属性控制回调函数触发时,元素与屏幕的交叉比例