社区精选 | 搭建前端监控,采集用户行为的 N 种姿势

优采云 发布时间: 2022-07-27 21:31

  社区精选 | 搭建前端监控,采集用户行为的 N 种姿势

  今天为各位带来的是社区作者杨成功的文章,在这篇文章中他为大家介绍了搭建前端监控,采集用户行为的 N 种姿势。

  让我们一起来了解吧~

  大家好,我是杨成功。

  上一篇我们详细介绍了前端如何采集异常数据。采集异常数据是为了随时监测线上项目的运行情况,发现问题及时修复。在很多场景下,除了异常监控有用,收集用户的行为数据同样有意义。

  怎么定义行为数据?顾名思义,就是用户在使用产品过程中产生的行为轨迹。比如去过哪几个页面,点过哪几个按钮,甚至在某个页面停留了多长时间,某个按钮点击了多少次,如果有需求都可以记录下来。

  但是记录行为数据是一个和业务紧密关联的事情,不可能把每个用户每一步操作都极其详细的记录下来,这样会产生极其庞大的数据,很显然不现实。

  合理的做法是,根据产品的实际情况评估,哪个模块哪个按钮需要重点记录,则可以采集的详细一些;哪些模块不需要重点关注,则简单记录一下基本信息。

  根据这个逻辑,我们可以把行为数据分为两类:

  通用数据特定数据

  下面分别介绍这两类数据该如何收集。

  通用数据

  在一个产品中,用户最基本的行为就是切换页面。用户使用了哪些功能,也能从切换页面中体现出来。因此通用数据一般是在页面切换时产生,表示某个用户访问了某个页面。

  页面切换对应到前端就是路由切换,可以通过*敏*感*词*路由变化来拿到新页面的数据。Vue 在全局路由守卫中*敏*感*词*路由变化,任意路由切换都能执行这里的回调函数。

  // Vue3 路由写法<br />const router = createRouter({ ... })<br />router.beforeEach(to => {<br />  // to 代表新页面的路由对象<br />  recordBehaviors(to)<br />})<br />

  React 在组件的 useEffect 中实现相同的功能。不过要注意一点,*敏*感*词*所有路由变化,则需要所有路由都经过这个组件,*敏*感*词*才有效果。具体的方法是配置路由时加*配置:

  import HomePage from '@/pages/Home'<br />,<br />

  然后在这个组件的的 useEffect 中*敏*感*词*路由变化:

  // HomePage.jsx<br />const { pathname } = useLocation();<br />useEffect(() => {<br />  // 路由切换这个函数触发<br />  recordBehaviors(pathname);<br />}, [pathname]);<br />

  上面代码中,在路由切换时都调用了recordBehaviors()方法并传入了参数。

  Vue 传的是一个路由对象,React 传的是路由地址,接下来就可以在这个函数内收集数据了。

  明确了在哪里收集数据,我们还要知道收集哪些数据。收集行为数据最基本的字段如下:

  上面的字段中,应用标识、环境、版本号统称应用字段,用于标志数据的来源。其他字段主要分为用户,页面,时间三类,通过这三类数据就可以简单的判断出一件事:谁到过哪个页面,并停留了多长时间。

  应用字段的配置和获取方式我们在上一节中讲过,就不做多余介绍了,获取字段的方式都是通用的。

  下面介绍其他的几类数据如何获取。

  获取用户信息

  现代前端应用存储用户信息的方式基本都是一样的,localStorage 存一份,状态管理里存一份。因此获取用户信息从这两处的任意一处获得即可。这里简单介绍下如何从状态管理中获取。

  最简单的方法,在函数recordBehaviors()所处的 js 文件中,直接导入用户状态:

  // 从状态管理里中导出用户数据<br />import { UserStore } from '@/stores';<br />let { user_id, user_name } = UserStore;<br />

  这里的@/stores 指向我项目中的文件 src/stores/index.ts,表示状态管理的入口文件,使用时替换成自己项目的实际位置。实际情况中还会有用户数据为空的问题,这里需要单独处理一下,方便我们在后续的数据查看中能看出分别:

  import { UserStore } from '@/stores';<br /><br />// 收集行为函数<br />const recordBehaviors = ()=> {<br />  let report_date = {<br />    ...<br />  }<br />  if(UserStore) {<br />    let { user_id, user_name} = UserStore<br />    report_date.user_id = user_id || 0<br />    report_date.user_name = user_name || '未命名'<br />  } else {<br />    report_date.user_id = user_id || -1<br />    report_date.user_name = user_name || '未获取'<br />  }<br />}<br />

  上面代码中,首先判断了状态管理中是否有用户数据,如果有则获取,没有则指定默认值。这里指定默认值的细节要注意,不是随便指定的,比如 user_id 的默认值有如下意义:

  用户数据是经常容易出错的地方,因为涉及到登录状态和权限等复杂问题。指定了上述默认值后,就可以从收集到的行为数据中判断出某个页面用户状态是否正常。

  

  获取页面信息

  前面我们在*敏*感*词*路由变化的地方调用了 recordBehaviors 函数并传入了参数,页面信息可以从参数中拿到,我们先看在 Vue 中怎么获取:

  // 路由配置<br />{<br />  path: '/test',<br />  meta: {<br />    title: '测试页面'<br />  },<br />  component: () => import('@/views/test/Index.vue')<br />}<br /><br />// 获取配置<br />const recordBehaviors = (to)=> {<br />  let page_route = to.path<br />  let page_title = to.meta.title<br />}<br />

  Vue 中比较简单,可以直接从参数中拿到页面数据。相比之下,React 的参数只是一个路由地址,想拿到页面名称还需要做单独处理。

  一般在设计权限时,我们会在服务端会维护一套路由数据,包含路由地址和名称。路由数据在登录后获取,存在状态管理中,那么有了 pathname 就可以从路由数据中找到对应的路由名称。

  // React 中<br />import { RouteStore } from '@/stores';<br /><br />const recordBehaviors = (pathname) => {<br />  let { routers } = RouteStore; // 取出路由数据<br />  let route = routers.find((row) => (row.path = pathname));<br />  if (route) {<br />    let page_route = route.path;<br />    let page_title = route.title;<br />  }<br />};<br />

  这样,页面信息的 page_route、page_title 两个字段也拿到了。

  设置时间

  行为数据中用两个字段start_at、end_at分别表示用户进入页面和离开页面的时间。这两个字段非常重要,我们在后续使用数据的时候可以判断出很多信息,比如:

  还有很多信息,都能根据这两个时间字段判断。开始时间很好办,函数触发时直接获取当前时间:

  <br />var start_at = new Date();<br />

  结束时间这里需要考虑的情况比较多。首先要确定数据什么时候上报?用户进入页面后上报,还是离开页面时上报?

  如果进入页面时上报,可以保证行为数据一定会被记录,不会丢失,但此时 end_at 字段必然为空。这样的话,就需要在离开页面时再调接口,将这条记录的 end_time 更新,这种方式的实现比较麻烦一些:

  // 进入页面时调用<br />const recordBehaviors = () => {<br />  let report_date = {...} // 此时 end_at 为空<br />  http.post('/behaviors/insert', report_date).then(res=> {<br />    let id = res.id // 数据 id<br />    localStorage.setItem('CURRENT_BEHAVIOR_ID', id)<br />  })<br />}<br /><br />// 离开页面时调用:<br />const updateBehaviors = ()=> {<br />  let id = localStorage.getItem('CURRENT_BEHAVIOR_ID')<br />  let end_at = new Date()<br />  http.post('/behaviors/update/'+id, end_at) // 根据 id 更新结束时间<br />  localStorage.removeItem('CURRENT_BEHAVIOR_ID')<br />}<br />

  上面代码中,进入页面先上报数据,并保存下 id,离开页面再根据 id 更新这条数据的结束时间。

  如果在离开页面时上报,那么就要保证离开页面前上报接口已经触发,否则会导致数据丢失。在满足这个前提条件下,上报逻辑会变成这样:

  // 进入页面时调用<br />const recordBehaviors = () => {<br />  let report_date = {...} // 此时 end_at 为空<br />  localStorage.setItem('CURRENT_BEHAVIOR', JSON.stringify(report_date));<br />}<br /><br />// 离开页面时调用<br />const reportBehaviors = () => {<br />  let end_at = new Date()<br />  let report_str = localStorage.getItem('CURRENT_BEHAVIOR')<br />  if(report_str) {<br />    let report_date = JSON.parse(report_str)<br />    report_date.end_at = end_at<br />    http.post('/behaviors/insert', report_date)<br />  } else {<br />    console.log('无行为数据')<br />  }<br />}<br />

  对比一下这两种方案,第一种的弊端是接口需要调两次,这会使接口请求量倍增。第二种方案只调用一次,但是需要特别注意可靠性处理,总体来说第二种方案更好些。

  特定数据

  除了通用数据,大部分情况我们还要在具体的页面中收集某些特定的行为。比如某个关键的按钮有没有点击,点了多少次;或者某个关键区域用户有没有看到,看到(曝光)了多少次等等。

  收集数据还有一个更专业的叫法 ———— 埋点。直观理解是,哪里需要上报数据,就埋一个上报函数进去。

  通用数据针对所有页面自动收集,特定数据就需要根据每个页面的实际需求手动添加。以一个按钮为例:

  点击;<br />const onClick = (e) => {<br />  // console.log(e);<br />  repoerEvents(e);<br />};<br />

  上面代码中,我们想记录这个按钮的点击情况,所以做了一个简单的埋点 ———— 在按钮点击事件中调用 repoerEvents()方法,这个方法内部会收集数据并上报。

  这是最原始的埋点方式,直接将上报方法放到事件函数中。repoerEvents() 方法接收一个事件对象参数,在参数中获取需要上报的事件数据。

  特定数据与通用数据的许多字段是一样的,收集特定数据需要的基本字段如下:

  这些基本字段中,前 7 个字段与前面通用数据的获取完全一样,这里就不赘述了。实际上特定数据需要获取的专有字段只有 3 个:

  这三个字段也非常容易获取。event_type 表示事件触发的类型,比如点击、滚动、拖动等,可以在事件对象中拿到。action_tag 和 action_label 是必须指定的属性,表示本次埋点的标识和文字描述,用于在后续的数据处理时方便查阅和统计。

  了解了采集特定数据是怎么回事,接下来我们用代码实现。

  手动埋点上报

  假设要为登录按钮做埋点,按照上面的数据采集方式,我们书写代码如下:

  <br />  登录<br />;<br />const onClick = (e) => {<br />  // console.log(e);<br />  repoerEvents(e);<br />};<br />

  代码中,我们通过元素的自定义属性传递了 tag 和 label 两个标识,用于在上报函数中获取。

  上报函数 repoerEvents() 代码逻辑如下:

  

  // 埋点上报函数<br />const repoerEvents = (e)=> {<br />  let report_date = {...}<br />  let { tag, label } = e.target.dataset<br />  if(!tag || !label) {<br />    return new Error('上报元素属性缺失')<br />  }<br />  report_date.event_type = e.type<br />  report_date.action_tag = tag<br />  report_date.action_label = label<br /><br />  // 上报数据<br />  http.post('/events/insert', report_date)<br />}<br />

  这样就实现了一个基本的特定数据埋点上报功能。

  全局自动上报

  现在我们回过头来梳理一下这个上报流程,虽然基本功能实现了,但是还有些不合理之处,比如:

  首先我们的埋点方式是基于事件的,也就是说,不管元素本身是否需要事件处理,我们都要给他加上,并在函数内部调用 repoerEvents() 方法。如果一个项目需要埋点的地方非常多,这种方式的接入成本就会非常高。

  参考之前做异常监控的逻辑,我们换一个思路:能否全局*敏*感*词*事件自动上报呢?

  思考一下,如果要做全局*敏*感*词*事件,那么只能*敏*感*词*需要埋点的元素的事件。那么如何判断哪些元素需要埋点呢?

  上面我们为埋点的元素指定了data-tag和data-label两个自定义属性,那是不是根据这两个自定义属性判断就可以?我们来试验一下:

  window.addEventListener('click', (event) => {<br />  let { tag, label, trigger } = event.target.dataset;<br />  if (tag && label && trigger == 'click') {<br />    // 说明该元素需要埋点<br />    repoerEvents(event);<br />  }<br />});<br />

  上面代码还多判断了一个自定义属性 dataset.trigger,表示元素在哪种事件触发时需要上报。全局*敏*感*词*事件需要这个标识,这样可避免事件冲突。

  添加全局*敏*感*词*后,收集某个元素的特定数据就简单了,方法如下:

  <br />  保存<br /><br />

  试验证明,上述全局处理的方式是可行的,这样的话就不需要在每一个元素上添加或修改事件处理函数了,只需要在元素中添加三个自定义属性 data-tag,data-label,data-trigger 就能自动实现数据埋点上报。

  组件上报

  上面全局*敏*感*词*事件上报的方式已经比手动埋点高效了许多,现在我们再换一个场景。

  一般情况下当埋点功能成熟之后,会封装成一个 SDK 供其他项目使用。如果我们将采集数据按照 SDK 的思路实现,让开发者在全局*敏*感*词*事件,是不是一个好的方式呢?

  显然是不太友好的。如果是一个 SDK,那么最好的方式是将所有内容聚合成一个组件,在组件内实现上报的所有功能,而不是让使用者在项目中添加*敏*感*词*事件。

  封装组件的话,那么组件的功能最好是将要添加埋点的元素包裹,这样自定义元素也就不需要指定了,而转为组件的属性,然后在组件内实现事件*敏*感*词*。

  以 React 为例,我们看一下如何将上面的采集功能封装为组件:

  import { useEffect, useRef } from 'react';<br /><br />const CusReport = (props) => {<br />  const dom = useRef(null);<br />  const handelEvent = () => {<br />    console.log(props); // {tag:xx, label:xx, trigger:xx}<br />    repoerEvents(props);<br />  };<br />  useEffect(() => {<br />    if (dom.current instanceof HTMLElement) {<br />      dom.current.addEventListener(props.trigger, handelEvent);<br />    }<br />  }, []);<br />  return (<br />    <br />      {props.children}<br />    <br />  );<br />};<br /><br />export default CusReport;<br />

  组件使用方式如下:

  <br />  测试<br /><br />

  这样就比较优雅了,不需要修改目标元素,只要把组件包裹在目标元素之外即可。

  总结

  本文介绍了搭建前端监控如何采集行为数据,将数据分为通用数据和特定数据两个大类分别处理。同时也介绍了多种上报数据的方式,不同的场景可以选择不同的方式。

  其中的数据部分只介绍了实现功能的基础字段,实际情况中可以根据自己的业务需求添加。

  许多小伙伴留言这套前端监控能否开源,肯定是要开源的,不过内容比较多我还在做,等到基本完善了我会发一个版本,感谢小伙伴们的关注。

  SegmentFault 思否社区小编说

  自 2022-07-01 起 SegmentFault 思否公众号改版啦!之后将陆续推出新的栏目和大家见面!(请拭目以待呀~❤)

  在「社区精选」栏目中,我们将为广大开发者推荐来自 SegmentFault 思否开发者社区的优质技术文章,这些文章全部出自社区中充满智慧的技术创作者哦!

  希望通过这一栏目,大家可以共同学习技术干货,GET 新技能和各种花式技术小 Tips。

  欢迎越来越多的开发者加入创作者的行列,我们将持续甄选出社区中优质的内容推介给更多人,让闪闪发光的技术创作者们走到聚光灯下,被更多人认识。

  「社区精选」投稿邮箱:

  投稿请附上社区文章地址

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线