埋点自动收集方案

·  阅读 1830

埋点自动收集方案-概述

image

引言

埋点自动收集方案,由于涉及内容较多,考虑到篇幅原因,将分为三篇文章分别阐述

  • 《埋点自动收集方案-概述》
  • 《埋点自动收集方案-路由依赖分析》
  • 《埋点自动收集方案-埋点提取》

其余两篇请在【大转转FE】 公众号2020年11月5日和9日的文章中查看

本篇文章篇幅较长,对最终效果好奇的同学,可以优先查看“后台预览”部分的截图

痛点

埋点,作为跟踪业务上线效果的核心手段

其说明文档也是重中之重,但在很多公司或团队都在以最原始的方式维护

有的团队即便有统一的埋点平台,但整体运营成本也较高 image

1)维护成本

每次提需求,pm需要花费大量时间维护埋点文档

新增埋点还好,直接加上就可以了。

如果有涉及老埋点删除或修改,还要先找开发同学确认,然后再更新文档

如果怕麻烦,文档只增不删。。时间长了,你懂的~

2)到底谁来维护埋点文档

前面提到,新增还好,主要是删除或者更新埋点,谁来推动埋点文档更新?

开发同学推动pm?

pm找开发要埋点更新list?

开发直接更新埋点文档?

开发同学在开发同时还要记录埋点更新list?

开发和pm会不会相互扯皮?

3)无阻断性流程

整个流程,单靠一方角色确实难以达成目标

最终无论谁来维护,但凡没有阻断性流程,就变成全靠自觉了。

靠自觉,那要是能持久,就见鬼了~

4)埋点文档不准确

由于前面的原因,也就造成了埋点文档和实际上报代码越来越远

时间长了自然没法看,尤其碰到人员变动或者业务调整

新同学接手后,脑门直接三道黑线。。

5)开发同学抵触情绪强

PM:“帮我查下xx项目的埋点吧,我急用”

PM:“之前做的xx活动的埋点能帮我查一下么”

PM:“上次需求迭代,埋点更新list给我一份”

...

尤其赶上倒排期的项目。。

image

作为FE的你,是否有同感。。

引发的思考

无论怎样,线上数据通过埋点反馈,而且业务调整的依据就是数据

这是客观事实。

所以埋点文档这个痛点必须解决。

可能有同学会问,为什么不用第三方的埋点方案(比如:growingIO、神策等)

因为,无论采用哪种方案,目前主流上报埋点的方式就两种:

  1. 全埋点上报(含区域上报)
  2. sdk主动上报(单埋点、多埋点上报)

全埋点上报需要pm根据页面结构进行配置(主要维护方在pm),一旦页面结构变更,需要重新配置。

另外公司的现状就是有自己的数据平台,但如果采用全埋点上报,在数据分析层面会有非常大的负担,而且各业务线本身采用的是代码主动上报方式。

sdk主动上报,就会遇到前面提到的问题。

可能还有同学说:“这本来就是pm该干的事,我只负责我这部分就好。”

这本身也没毛病,但我们本质上还是想提升整体团队的效率

在做这个方案的时候,我们也和很多团队的pm沟通过,维护一个可用的埋点文档确实会消耗相当大的精力

这也让我更加坚定了决心!

image

方案思路

前面所有的问题,核心就两个:

  • 没有统一埋点平台
  • 没有科学可用的埋点协作模式

经过组内多次讨论,又将问题细分,以及对应解决方案如下:

问题解决方案
维护角色模糊pm只提供需要统计的埋点,更新由代码层面自动维护
维护成本高通过jsdoc代码注释,利用代码在编译时,从文件中抽离埋点及解释
文档可用性埋点抽离完成,直接上报埋点后台
老项目接入提供统一自动注释添加工具
新项目接入将该方案集成到脚手架中,同时提供检查loader,硬性检查完整性
业务提效建立埋点后台,提供埋点及数据预览

看表格可能还是一脸懵,我们先看看整体思路图 image

解释一下流程:

  1. pm编写埋点文档(仅开发用)
  2. fe依据文档编写上报逻辑
  3. 代码在上线时要执行build, 此时触发webpack 埋点抽离插件
  4. 埋点抽离插件按页面,收集其所有埋点
  5. 收集完后,上报埋点服务
  6. pm可以通过埋点平台查看埋点及对应的数据了

看流程很清晰,但肯定会有一些问题,那么我们分别解释下:

问题1:如何收集埋点

我们以vue项目为例,在Vue.prototype下挂了公共上报埋点方法:

Vue.prototype.$log = function (actionType, backup = null) {...}

  • actionType 必选,行为埋点参数
  • backup 可选,埋点的附加参数

如上报某个页面pv,附加参数为渠道号channel,即:

this.$log('PAGE_SHOW', { channel: 'xxx' })
复制代码

解决方案:利用自定义jsdoc插件和自定义标签收集注释

/**
 * @log 页面展现
 * @backup channel: 渠道号
 */
this.$log('PAGE_SHOW', { channel: 'xxx' })
复制代码

其中,@log和@backup 是jsdoc的自定义标签,让jsdoc只监控$log方法也是需要明确指定的

所以,为了让整个插件行为保持一致,需要引入一个公共的配置文件 js_doc.conf.js

js_doc.conf.js

module.exports = {
  // 让jsdoc识别 @log
  tag: 'log',
  // 告知插件this.$log是发送埋点的方法,定义成数组是因为有的项目可能存在多个
  method: ['$log'],
  // pageType前缀,即会给项目pageType加前缀来区分不同项目
  prefix: 'H5BOOK',
  // pageType具体生成规则,不同项目可能规则不同
  pageTypeGen: function({ prefix, routeName, dir, path, fileName }) {
    return (prefix + routeName).toUpperCase();
  },
  // 公共的backup说明,最终单个埋点backup由公共的和私有的拼接而成
  backup: 'uid:用户id',
  // 项目信息,埋点上报用
  projectInfo: {
    projectId: '工程id,一般用项目名称',
    projectName: '中文名称',
    projectDesc: '项目描述',
    projectIsShowOldMark: false  // 是否展示老数据
  }
}
复制代码

该配置文件,是js_doc插件,连同后文提到的文件依赖分析插件、自动添加埋点注释工具所共用的

具体js_doc插件的实现方式 ,==在随后的《埋点自动收集方案-埋点提取》中会专门讲解==,本文主要讲解整体方案原理

通过这一步,就可以顺利收集到每个行为埋点,以及相应的注释和参数说明了,即actionType和对应的注释

当然,确定一个埋点,需要有2个核心因素

  • actionType:行为埋点
  • pageType:页面标识(有的团队叫pageId),用来定位具体是哪个页面

pageType通常是在页面运行时以当前页面路由为参照得来的,那问题来了

问题2:如何在==编译时==获取pageType

而且还有一种常见情况,公共组件,如下图

image

A组件被多个页面引用

或者A组件被二级组件B引用,然后又被直接或间接引入页面。

那么组件A中的埋点,肯定要被收集到对应的页面1和页面2当中。

这怎么办?

这问题等于就变成了:

  • 如何确定页面路由(即获取pageType)
  • 如何分析文件依赖(即收集公共组件中的埋点)

解决方案:

  • 通过编写文件依赖分析插件,分析每个文件的相互引用关系
  • 利用ast语法分析路由文件,拿到所有页面路由,即拿到页面的根组件
  • 利用webpack的编译分析出整个项目的文件依赖关系,最终,你拿到每个页面引用了哪些组件

==该部分具体实现方案会在 《埋点自动收集方案-路由依赖分析》中会专门阐述==

样例:

// 附近的人
export default {
  routes: [
    // 首页
    {
      path: '/page1',
      name: 'page1',
      meta: {
        title: '转转活动页',
        desc: 'xx首页'
      },
      component: () => import('../views/page1/index.vue')
    },
    ...
  ]
}
复制代码

当然实际情况会很复杂:

  • 路由入口文件包含多个子路由文件。
  • 路由引入组件的方式有多种(静态引入、动态引入)
  • 引入组件的书写方式多样
  • ...

写法复杂的情况,通过ast语法分析,都可以进行兼容,不过确实需要耐心~

到此,核心信息收集完成。

前面jsdoc部分:拿到了每个文件所有的actionType及对应的注释

而这一步又拿到了:项目所有的页面路由引用及对应的组件文件依赖关系

对于页面说明的取值方式:meta.desc || meta.title || name || path

因为页面的描述和真实title不一定相同,所以单独加了个desc字段

想一下,拿到这些是不是可以以页面为单位,组织行为埋点数据了!

在接入过程中,我们还遇到了一个问题,就是

各业务pageType生成规则不统一

考虑到这个问题,我们在配置文件中定义了下面内容:

module.exports = {
  ...
  // pageType前缀,即会给项目pageType加前缀来区分不同项目
  prefix: 'H5BOOK',
  // pageType具体生成规则,不同项目可能规则不同
  pageTypeGen: function({ prefix, routeName, dir, path, fileName }) {
    return (prefix + routeName).toUpperCase();
  },
  ...
}
复制代码

prefix:作为项目个性化前缀(主要用于区分不同项目,因为不同项目可能会定义同样的路由)

pageTypeGen:该方法用于业务自己定义生成pageType的规则,我们把prefix(项目前缀),routeName(路由名称),dir(文件目录),path(路由),fileName(文件名)通通返回给用户,有了这些参数,基本上已经可以覆盖全部的路由生成方式了。

OK,最终数据结构形式:

{
  // 项目所有页面
  pageList: [
    // 单个页面
    {
      // 该页面的路由信息
      routeInfo: {
        routeName: 'page1',
        description: 'xx首页'
      },
      // 该页面对应所有的行为埋点
      actionList: [
        {
          actionType: 'PAGE_SHOW',
          pageType: 'H5BOOK_page1',
          backup: 'channel: 渠道号',
          description: '页面展现'
        },
        ...
      ]
    },
    ...
  ],
  // 项目信息
  projectInfo: {
    projectId: '项目id',
    projectName: '项目名称',
    projectDesc: '项目描述信息',
    projectMark: '项目标记',
    projectLogsMark: '埋点标记',
    projectType: '项目类型',
    projectIsShowOldMark: false, // 是否展示老的埋点数据,默认false
    projectDefBackup: '默认参数说明'
  }
}
复制代码

埋点收集完成,再加入项目相关信息, 然后通过接口批量上报到埋点后台保存。

埋点后台

我们用eggjs + mongoose搭建的后台服务

用react + antd + bizChart(图表库)搭建的后台系统

后台预览

显示所有自动埋点收集项目列表 image 点击,显示该项目下所有页面(路由)名称 image 点击,展示该页面所有埋点 image 同时刻查看单个埋点近七日数据走向 image

上面是方案的核心部分,但毕竟整个方案解题思路是技术驱动

所以我们肯定还要关心:如何让整个方案可持续性的运转起来?

问题3:如何保证埋点及时更新?

这个问题就比较简单了,既然做成了webpack插件,那么

解决方案:更新build命令,在上线时执行收集和上报插件

埋点提取插件,在webpack编译后,收集完埋点自动上报。

保证每次上线时都对埋点文档进行更新。让文档和代码实时同步

问题4:老项目如何快速接入埋点方案?

老项目接入埋点是每个开发最头疼的事情,尤其补埋点注释

为此,我们也提出了一套便捷方案

解决方案:通过命令行工具自动补齐注释

我们编写了一个命令行工具,叫做autocomment,全局安装

在项目根目录执行该命令,工具会自动将src目录下全部的.vue、.js、.ts文件进行自动添加

该命令工具和前面插件使用共同的配置文件js_doc.conf.js

module.exports = {
  // 让jsdoc识别 @log
  tag: 'log',
  // 告知插件this.$log是发送埋点的方法,定义成数组是因为有的项目可能存在多个
  method: ['$log'],
  ...
}
复制代码

读取里面tag和method属性。

对形如this.$log('xxxx', {...}),且没有注释方法进行自动注释添加

添加前:

this.$log('PAGE_CLOSE')
复制代码

添加后

/**
 * @log autocomment-PAGE_CLOSE
 */
this.$log('PAGE_CLOSE')
复制代码

添加注释的逻辑很简单:

  • 依旧通过ast分析,提取this.$log()方法中的actionType参数
  • 加上固定前缀 'autocomment-' 作为最终默认注释

为什么加固定前缀?

因为可以便让开发同学快速找出,哪些注释是通过工具自动添加的。

全局搜索 'autocomment-'

image

同时,为了消除开发同学的心理障碍(毕竟工具直接改源码,心里还是不踏实的)

为此,我们还做了添加汇总页面,工具添加完注释,会自动弹出

image

用仿git-history的样式,告知开发同学,我们改了这些地方

问题5:开发时,如何保证遵循埋点添加注释的规范

因为是注释形式收集,开发同学很容易忘记写,那么

解决方案:提供一个监控loader,对开发实时检测

开发时,一旦发现有this.$log()方法调用,且前面没有注释,直接控制台报错

实现方式同样是利用ast语法分析,复用前面的算法就可以了,大同小异。

注意:埋点规范

细心的同学可能也发现了,这个方案确实有几个限制:

==actionType必须是字符串==

如果是变量或者表达式,ast就无法正常收集。

所以如果需要上报接口返回的内容,可以把值放到backup里。通过参数说明上报接口

不支持场景:

// 错误演示1:
const resp = {...}
this.$log(resp.actionType)

// 错误演示2
this.$log(type === 1 ? 'actionType1' : 'actionTyp2')
复制代码

如果遇到这种场景,需要变更书写方式

// 错误演示1改进:
const resp = {...}
/**
 * @log 内容上报
 * @backup type: 数据类型 
 */
this.$log('respData', { type: resp.actionType })

// 错误演示2改进:
if (type === 1) {
  /**
   * @log 行为埋点1
   */
  this.$log('actionType1')
} else {
  /**
   * @log 行为埋点2
   */
  this.$log('actionType2')
}
复制代码

PS:从代码美观上讲,确实不好看。。。但,毕竟功能最重要嘛

另外该方案也 ==不支持动态添加路由==

当然也不是不能支持,有必要的话,可以单独处理。

不过,通常ToC的项目,一般的路由定义方式已经足够了。

ok, 这就是埋点自动收集方案的核心内容。

结语

当初这个方案也和很多业务方的开发同学深入讨论过

其中关注度最高的几个问题:

  • 埋点文档本本就该由pm维护,这等于把pm的工作转嫁给开发了
  • 因为要写注释,导致开发效率降低,业务能否接受
  • 如果出了问题,责任该由谁承担

大家所面临的处境与合作模式千差万别,确实没办法一概而论

但我们的初衷还是希望能够提升整体的协作效率。

最起码可以把团队维护文档的精力节省出来,去思考更有价值的事,不是么~

这个方案目前在已经接入的团队中,广受好评,也确实解决了核心痛点。

我们相信:技术就是效率,技术与产品本来就该相互促进!

分类:
前端
标签:
分类:
前端
标签: