点击蓝字关注我们,了解更多转转实践
埋点统计问题一直是业务中非常重要的一环。埋点数据是后期业务分析和技术优化的重要基础。
随着业务迭代次数的增多,伴随人员的变动,历史累赘也越来越多,经常 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 方法、自定义 actionType 方法、公共 backup 定义、项目信息等进行定义。详细请参阅使用文档。
通过借助 JSDoc 插件,我们完成了由注释到埋点数据的提取工作,这过程中需要了解 JSDoc 对应的方法,以及对我们本身需求功能点的分解。
这也是一个 AST 和 JavaScript 注释的一个实际使用场景,希望对各位有所启发。
references:
回顾本系列文章:
喜欢本篇内容请给我们点个“在看”