埋点自动收集方案-埋点提取

avatar
公众号:转转技术

简介

埋点统计问题一直是业务中非常重要的一环。埋点数据是后期业务分析和技术优化的重要基础。

随着业务迭代次数的增多,伴随人员的变动,历史累赘也越来越多,经常 PM 来找开发同学寻问某某功能的埋点是什么。一方面沟通成本高,一方面对开发同学频繁打扰也严重影响开发效率。

针对这些问题,我们开发出了一套将项目中分散埋点自动收集、归类、上报、展示的一套系统,来协助相关业务方查看和分析业务数据。

主要过程分为 路由整理(依赖分析)埋点提取和上报展示平台 三个部分。

这次主要介绍 埋点提取和上报 部分。

埋点标记

首先指出的是我们业务中的埋点多是手动埋点,形如:

this.$log('page_view', pageType, backup);

分布在各个页面和组件中。

一个首要维护问题是很多情况下,时间一长,连开发者本身也难以记得这个埋点的意义,以及附带参数的意思了。

解决这个问题我们很容易想到通过增加注释的方式来增强可维护性。但是怎么加?

这里我们借助于 JSDoc 这种通用的注释规范,来给我们的埋点做说明。

同时通过一个自定义标签来标记这是一个埋点上报。

像这样:

/*
 * @log 页面展现
 * @backup 参数说明
 */
this.$log('page_view', pageType, backup);

之后,就着手准备收集工作了。

埋点处理

预处理

因此我们的收集也是基于 JSDoc 来完成的。但在开始之前需要先对我们的源文件进行处理,和我们大多数工程一样,都需要 Babel 进行转换一下,因为一些高级语法 JSDoc 同样也不支持,会在在处理过程中语法报错而中断。

还使用到了 vue-template-compiler 将 vue 中的 js 代码提取出来。

以及对 TypeScript 的转换。

JSDoc 的插件写法请查阅官方文档

// 一个典型的 JSDoc 插件写法
exports.handlers = {
    beforeParse: function(e) {
        // 对文件预处理
    }
}

标记

接下来是自定义我们的标签@log

exports.defineTags = function (dictionary) {
    // 定义 @log 标签
    dictionary.defineTag('log', {
        // 声明自定义的 tag 可以包含文字 像:这里面的name `@param {string} name - Description`
        canHaveName: true,
        onTagged: function(doclet, tag) {
            // 这里保存埋点文字说明
            doclet.meta.log = {
                name: tag.text
            };
        },
    });
    
    // 定义 @backup 标签
    dictionary.defineTag('backup', {
        onTagged: function (doclet, tag) {
            // 将数 backup 描述存储起来
            doclet.backup = doclet.backup || [];
            if (tag.value || tag.text) {
                doclet.backup.push(tag.value || tag.text);
            }
        }
    });
}

细心的你也看见了上面除了定义了 @log 标签还定义了另外一个 @backup 标签,和上面对应,用于收集埋点的参数说明。

这样我们通过 JSDoc 的自定义标签,将我们的埋点标记了出来。接下如如何把埋点收集成我们可以理解的格式。

解析

不知道你注意到没有,添加的 block comment 其实原本是用在方法定义上的,但我们这里使用在方法调用上,结果就是 JSDoc 工具直接忽略了这中类型的注释。

好在 JSDoc 有对应的标签:@name

在注释中添加了 @name 声明这条注释,指出这条注释具体的含义,以便 JSDoc 对此进行收集。

然后还需要标记出 actionType,pageType,这可以通过已有的 @param 标签来标记。

/**
 * @name 埋点
 * @log 页面展现
 * @param {string} pageType - pageType 说明
 * @param {string} actionType - actionType 说明
 * @backup {string} backup - 参数说明
 */

关键的是如何解析埋点方法中的actionType 以及 pageType

通常我们遇到的埋点方法调用的情形有:

// 一个对象参数
log_method({actionType: '', pageType: '', backup: {}});
// 一个对象参数,一个字符串参数
log_method({actionType: ''}, 'pageType');
// 两个对象参数
log_method({actionType: ''}, {});
// 一个字符串参数
log_method('action_type');
// 两个字符串参数
log_method('action_type', 'page_type');
// 三个参数
log_method('action_type', 'page_type', {});
// 两个参数
log_method('action_type', { foo: 'bar' });
// 三个参数
log_method('action_type', null, {});
// 三个参数
log_method('action_type', void 0, {});

可以看到调用形式很多,而这些使用方式都是项目中已经出现的方式,所以,都要兼容。

这么多情况如何准确提取参数并解析呢?还是 JSDoc 提供了切入点:astNodeVisitor

exports.astNodeVisitor = {
    visitNode: function(node, e, parser, currentSourceName) {
        // do all sorts of crazy things here
    }
};

这里使用到了 AST,前面我们大转转FE(zhuanzhuanfe)公众号文章有介绍过,这里就不再展开讲了,概括地说就是通过 AST 获取到我们需要的actionType,pageType 以及对参数backup的解析。

至此,解析问题基本解决了,紧接着就是处理数据了。

收集

默认情况下 JSDoc 收集到的注释会按照内置的模板输出为 html 文件,但我们这里其实并不需要文件预览,只关心数据,后续我们有自己的数据展示平台。

所以,数据处理我们通过自定义 JSDoc 的模板来完成:

JSDoc 模板的写法:

/**
 * Generate documentation output.
 *
 * @param {TAFFY} data - A TaffyDB collection representing
 *                       all the symbols documented in your code.
 * @param {object} opts - An object with options information.
 * @param {Tutorial} tutorials
 */
exports.publish = function(taffyData, opts, tutorials) {
    // 我们需要的数据都在 taffyData 中
}

我们所需要的数据都在 taffyData 中。

在模板中我们需要根据我们的需求处理以下几件事:

  • 对 PageType 这种变化不大的内容提供自定义方法
  • 按照给定的文件顺序(路由)输出埋点
  • 提供 markdown 文件输出,方便本地预览
  • 重复埋点
  • 上报埋点展示平台

总结过程: 

配置

以上就是埋点收集部分的关键步骤,考虑到不同项目的埋点上报方法的差异,我们提供了配置文件来对埋点方法名、自定义 pageType 方法、自定义 actionType 方法、公共 backup 定义、项目信息等进行定义。详细请参阅使用文档。

总结

通过借助 JSDoc 插件,我们完成了由注释到埋点数据的提取工作,这过程中需要了解 JSDoc 对应的方法,以及对我们本身需求功能点的分解。

这也是一个 AST 和 js 注释的一个实际使用场景,希望对各位有所启发。

references:

  1. 关于 JSDoc 插件 jsdoc.app/about-plugi…
  2. ast explorer astexplorer.net/