老大喊我做一个项目埋点方案,我给他这么搞,做前端的都用得上!

994 阅读8分钟

啰里八嗦的话

众所周知,做C端产品对用户体验的要求是极高的。同时产品实施是否到位,投入回报是否成正比,成了一个团队是否有奖励的关键因素(这是一个最实际的软件团队的真实诉求“之一“哈哈哈)。尽管前期会做众多的用户调研,收集收据等等,明确哪些是用户基础需求,哪些是用户兴奋需求,但还是需要一个具体的可以量化的数据支撑,来评估某项业务是否满足了用户真实的需求,以及为产品后期迭代做参考等等。

那么基于以上的实际情况,对于整个产品线而言,做一个埋点业务收集是非常必要的。当然做埋点还有更多的意义,在这里暂不展开讨论。

以下将对该方案的实施过程进行完整的分析,同时也将提供一个简单的合理的可复用的方案输出,感兴趣的客官请留步~✌

当前现状

  • 目前3个产品业务线产品,对于前端项目而言是分开管理的,现有业务也会有源源不断的需求增加,后面还会增加新的产品线。
  • 主要使用Vue.js@2.0版本,但有一个项目是采用3.0版本开发的。
  • 项目已开发迭代历经8个月,主要产品业务代码已超17万行。

整体分析

  • 对接第三方没有可以既可以满足当下需求,又可以满足强扩展性的关键要素的现成方案,同时对于隐私信息的收集不能交给第三方。
  • 现有代码量不具备可精准埋点的条件。
  • pv类型的数据收集可暂时交给某度统计,这种大而笼统的数据只有运营会关注,埋点对用户分析的粒度更小。
  • 埋点尽量不影响具体业务功能及代码,甚至绝对的不影响。

实际需求

  • 收集粒度到按钮级别的用户。
  • 页面进入时间,停留时间,也就是要拿到enterTimeleaveTime
  • 要具备可对某页面使用量,某按钮使用量,某用户使用轨迹,某时间段按钮使用量,用户浏览器基本信息,后台最好具备前端页面功能点击量的可视化还原。

把需求转换成数据

以下是大致的需要收集到的数据

userAgent // 浏览器信息,包含设备信息,分辨率等

path // 访问页面 可以用router也可以用location.href

pageInfo // 页面信息 enterTime和leaveTime

userInfo // 用户信息 当前登录用户个人信息

eventData // 操作事件信息 事件类型,操作DOM节点,操作时间,节点名称(html节点名称),节点文本

代码怎么下手

  • 不影响现有业务,抽离成公共类是必须的。
  • 用户信息以及其他后期可能记录的属于扩展性信息,应可以外部传入。
  • 目前使用开源UI框架,项目整体按钮节点都是button标签(部分不是的可以改过来)。
  • 数据记录和发送,发送不能太频繁,需要一个记录数据的队列,设定一个阈值,到达阈值后进行发送并清空。

代码大概的雏形

基本结构

class Monitor {
  constructor() {
    
  }
  /**
   * @description 初始化方法:
   *	extendData 用于传入基于业务的数据信息,
   *	router 是vue-router对象,这里既可以通过init传入,也可以在当前类模块直接引入,
   *	config 是配置信息
   * @param {*} { extendData = null, router = null, config = {} }
   * @memberof Monitor
   */
  init({ extendData = null, router = null, config = {} }) {
		//TODO
    // 这里可以做一些覆盖默认配置,初始化监听事件等操作
    let { vpt } = config
    this.vpt = vpt ? vpt : this.vpt
    this.uaHandler()
    
    // 这里下面会讲
    this.eventHandler = this.eventCallback.bind(this) // 关键
    document.addEventListener('click', this.eventHandler, true) 
  }
}

使用方式

// 这里建议在项目封装的router管理模块使用
import Monitor from './monitor'

const monitor = new Monitor()
monitor.init({
  router: router,
  extendData: {
    userInfo: {
      userName:"张三",
      account:"13666666666"
    }
  }
})

Monitor类应具备的数据

 constructor() {
    // 发送队列的阈值
    this.vpt = 10

    // 事件节点类型限制
    this.limitNodeType = ['button']

    // 用户浏览器信息
    this.uaInfo = {}

    // 页面级别的数据队列
    this.pageDataQueue = []

    // 当前操作队列ID
    this.currentQueueId = null

    // 此属性用于保存bind返回的匿名函数
    this.eventHandler = null
 }

埋点功能结构

浏览器信息记录

这里可以自定义获取更多的信息,后面如果对首屏加载,网速检测等数据也有收集需求也可以加进去。

 /**
  * @description ua信息记录
  * @memberof Monitor
 */
uaHandler() {
  this.uaInfo = {
    userAgent: navigator.userAgent,
    dpiWidth: window.screen.width,
    dpiHeight: window.screen.height
  }
}

记录页面维度信息

由于最终上传数据是一个整合的数组对象,而单项数据又是跟着页面走的,同一个页面可能在阈值范围内反复访问,所以需要一个guid用于标识”当前页面“确保唯一性,guid生成方法如下,一般在阈值内可保证唯一性,没必要搞得太复杂。

/**
* @description 生成guid,当前操作队列的唯一标识
* @returns {*}
* @memberof Monitor
*/
guid() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = (Math.random() * 16) | 0,
        v = c == 'x' ? r : (r & 0x3) | 0x8
    return v.toString(16)
  })
}

到达阈值需要上传现有数据并清空数据,上传数据应该是外部自行处理的,所以应向外部暴露一个get()方法用于获取数据,clear()用于清空数据

获取数据

/**
   * @description 用于外部获取操作队列
   * @returns {*} Array[]
   * @memberof Monitor
   */
get() {
  return this.pageDataQueue
}

清空数据

 /**
   * @description 用于清空队列
   * @memberof Monitor
   */
clear() {
  //TODO
  this.pageDataQueue = []
}

当然这里也可以采用内部上传数据的方法,就是需要引入接口请求方法,把这项功能交给Monitor处理,这里仅需在内部同时监听队列即可,如是可以有vptHandler方法

/**
   * @description 阈值监听,达到阈值就发送数据
   * @memberof Monitor
   */
vptHandler() {
  if (this.pageDataQueue.length >= this.vpt) {
    this.sendData()  // 这里用于走上传数据业务,上传完毕后清空数据,可根据实际情况进行处理
  }
}

记录页面访问的基本信息

router.afterEach(async (to) => {
  // 离开监听
  this.updateLeaveTime()
  // 阈值监听
  this.vptHandler()
	// 当前操作页面的唯一标识
  this.currentQueueId = this.guid()
  let initPageData = [
    {
      id: this.currentQueueId,
      path: to.path, // 因为当前项目目录结构和菜单路由命名是高度一致的,所以可以采用path进行记录
      uaInfo: this.uaInfo,
      pageInfo: {
        entryTime: this.getTime() //注意这里只有enterTime,更新leaveTime的时机交给👆上面的updateLeaveTime方法
      },
      ...extendData,
      eventData: [] // 事件记录置空
    }
  ]
  this.pageDataQueue = this.pageDataQueue.concat(initPageData) // 将当前页面数据追加到操作队列
})

离开页面监听

此时是完成一个页面数据的闭环的关键要素

/**
   * @description 页面离开时间更新
   * @memberof Monitor
   */
updateLeaveTime() {
  let index = this.pageDataQueue.findIndex(
    (el) => el.id == this.currentQueueId
  )
  if (index >= 0) {
    this.pageDataQueue[index].pageInfo.leaveTime = this.getTime()
  }
}

事件的监听

事件的监听需要一个中转,对于eventCallback应该具备针对不同事件类型方法的转发,当然目前只考虑点击事件。

/**
   * @description 事件回调中转
   * @param {*} e
   * @memberof Monitor
   */
  eventCallback(e) {
    if (e.type == 'click') {
      this.clickEventHandler(e)
    }
  }

点击事件的监听

上面提到由于项目整体采用统一的UI框架,几乎所有的按钮都是采用的标准button标签,故可以根据DOM节点获取到如下数据进行记录

const { innerText, localName, formAction, type } = ele.target

 /**
   * @description 页面点击事件收集
   * @param {*} ele 事件节点
   * @memberof Monitor
   */
clickEventHandler(ele) {
  const { innerText, localName, formAction, type } = ele.target
  let isEv = this.limitNodeType.includes(localName) 
  if (isEv) {
    let eventData = [
      {
        innerText,
        localName,
        formAction,
        eleType: type,
        eventType: 'click',
        clickTime: this.getTime()
      }
    ]
		
    // 找到当前页面中的eventData,将当前操作追加进去
    let index = this.pageDataQueue.findIndex(
      (el) => el.id == this.currentQueueId
    )
    if (index >= 0) {
      this.pageDataQueue[index].eventData = this.pageDataQueue[
        index
      ].eventData.concat(eventData)
    }
  }
}

监听事件销毁

针对特定场景我们不需要记录用户的行为,可以对Monitor的监听事件进行销毁,以避免多余的性能消耗

// 销毁监听事件
destroy() {
  document.removeEventListener('click', this.eventHandler, true)
}

结果输出

至此我们可以获取到如下结构的数据,根据阈值设定的时机进行上传。

W3TBdO.png

总结成果

以上整体方案根据各项实际情况对需求进行了分析,从整体分析到代码落地,结合项目整体特点,同时兼顾低成本开发与接入,满足后期扩展性,甚至该思路可以满足其他单页面框架。

像这种类似的需求,在团队规模不大的情况下,可能不会被产品关注到,有些没有类似经验的产品也无从下手,不知道怎么收集,怎样展示。所以往往是项目经理去向开发直接提出的需求,也没有具体的原型输出,这时候就需要开发去从各种维度思考问题,将这种“一句话需求”转化成可实施的代码方案应用到工作当中。

此文章涉及到的代码某些部分考虑的可能不算周全,同时对问题分析的角度可能还有众多其他的看法和讨论,更多的本意还是将这种方法分享给诸多同行(肯定不是最好,哈哈哈哈),欢迎路过的XDM随时滴我。

不要吝啬留下你的脚印哈👇

2021-07-20文后补充,出于尝试想借此会回用一下rollup做一个相对完整的纯js插件,故已将插件发布至npm,没想到已经有下载量了,也不知道哪来的哈哈,感兴趣可以试一下🤣(纯属交流学习),代码地址在homepage可以去看😳😳😳😳😳

image.png