春节快乐,干货来袭。QAPM小程序质量套件服务于包括银行等公司内外多个产品,本文对小程序的SDK做技术揭秘。
目前公司内外存在多个小程序的监控方案,包括微信提供的原生方案、Aegis、Fundebug和听云等,那么对比这些的监控方案,QAPM的小程序质量套件有什么不同呢?
(1)定位不同:相比专注于做一个优秀的工具,QAPM的小程序质量套件定位于数字体验监控(DEM)。利用关联分析、可视化和机器学习等方法,实现性能、用户行为的监控、观察和分析,核心聚焦于用户体验。
(2)问题域不同:手中有锤子,眼前都是钉子。传统的小程序监控工具,都是在描述问题上做研究,目的是如何把问题描述得更清晰;QAPM的小程序质量套件,在复现和解决问题上具有天然的优势,内置用户行为点击事件的监控能力,可以清晰的得知用户操作路径,通过回放用户操作,解决研发不易复现问题的痛点,提高修bug的速度。
(3)技术不同:使用全新hook技术,完美实现对只读的wx下的方法的hook操作;对Page参数进行解析,获取用户点击的相对坐标和绝对坐标,完美拿到用户的点击操作。
(4)稳定性优势:QAPM的小程序质量套件可靠性高,具备银行标准,目前服务于广州农村商业银行和长沙银行。
一、QAPM小程序SDK做了什么事情?
小程序SDK采用无埋点方案,通过hook一些关键的小程序api,例如wx.request、App.onError等,在不影响业务的正常运行的情况下,获取到这些api的入参、执行时间等信息,并在合适的时机做数据组装和上报,达到获取性能数据的目的。
二、hook小程序的关键api
1.监控的关键api有哪些?
请求监控:wx.request
页面性能监控:Page.onReady、Page.onLoad、Page.onShow、Page.onHide、Page.onUnload
启动监控:App.onLaunch
jserror监控:App.onError
2.如何hook?
(1)直接替换法
Page和App下的方法可读可写,所以直接替换就可以完成hook了。以App.onLoad为例,先把原来的onLoad复制一份,接着用新的函数替换掉原来的onLoad,并在新函数中执行自定义的代码:
Page和App下的其它方法都是用这种直接替换的形式完成hook。要完成正常的记录时间的功能,就需要在执行原来的方法前后,分别执行一次获取时间戳的函数。要完成记录错误堆栈的功能,就得截获wx.onError的参数,并做好堆栈信息的处理和上报。
(2)使用Object.defineProperty
wx这个对象下的方法是只读的,使用上述的“直接替换法”走不通,因此需要使用其它办法,经过一系列探索,最终决定采用Object.defineProperty这个方法。MDN上对这个方法的描述是这样的:
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
下面是如何使用Object.defineProperty对wx.request进行hook操作的源码。这里的关键其实就是Object.defineProperty(wx,"request",{……}),其中value里面定义了hook之后执行的我们的代码,主要包括4个步骤:
(1)在请求开始的时候,记录请求的方法和url
(2)开启正常的请求
(3)为请求结束的回调函数config.complete注入我们的代码,主要是记录请求的方法、url、请求状态码res.statusCode、请求内容config和响应内容res
(4)如果hook失败了,则把wx.reqeust替换成原来的wx.request,以免影响正常的业务逻辑
源码如下:
于是我们就完成了对微信小程序关键的api的hook操作,并对页面的各个生命周期的开始和结束时间戳做了记录,对小程序发请求的接口做了请求耗时、请求和响应内容的记录。
三、获取setData的性能数据
setData在整个小程序的数据更新中起到重要作用,获取setData的性能数据对优化渲染耗时有一定的帮助。setData的性能数据包括使用次数、执行时间和更新数据的大小。有2个方法可以获取这些东西:
1.基础库在2.12.0以下的版本,需要采用上述的“直接替换法”进行hook操作,在调用setData的时候和它的回调函数中分别执行一次获取时间戳的操作,从而得到耗时的性能数据。
2..基础库在2.12.0以上的版本,微信提供了一个接口可以直接获取setData的性能数据,这个接口是setUpdatePerformanceListener,提供3个时间戳:
(1)此次更新进入等待队列的时间戳
(2)更新运算开始的时间戳
(3)更新运算结束的时间戳
sdk以(1)和(3)的差值作为setData的耗时,包括了等待队列的时间和实际做更新运算的时间。
具体的接口参数信息可以参考微信小程序的这篇官方文档:setUpdatePerformanceListener文档
调用这个封装好函数的时机是在hook Page的时候,需要把page作为参数传入,才能够获得当前的page的setData信息,代码逻辑主要有这几块:
(1)对原始的setData做好备份
(2)对基础库2.12.0以下的版本,使用方法1进行hook,并分别在setData开始的时候、和在回调中做好时间戳的获取和存储
(3)对基础库大于2.12.0的版本,使用方法2,用setUpdatePerformanceListener获取setData的精确耗时数据
(4)出错了就执行备份的setData,保证不影响正常的业务逻辑
这两个方法的合并后的源码如下:
四、用户行为
小程序中,使用一些技巧可以获取到用户点击页面的x和y轴的坐标点,
原理:sdk在对Page的所有方法进行hook的时候,解析page的参数,其中page的第一个参数中,有一个叫做touches的字段,里面包含有pageX、pageY、clientX和clientY这些字段,含义就是相对于整个页面的绝对坐标点和在当前窗口中的坐标点。
由此我们就可以获取到点击的坐标信息了,坐标点信息配合点击的页面名称和时间戳等基础信息,就可以做用户行为的回放功能。
截取一段对page的hook的源码如下:
五、jserror堆栈信息的行列号和文件名的翻译
1.这块其实比较简单,分3个步骤实现翻译功能:
(1)获取错误堆栈信息,方法是hook wx.onError
(2)获得当前版本的sourcemap文件,在微信开发者工具点击上传小程序,就可以得到这个文件了,如果是线上版本,则可以直接去微信小程序管理平台下载
(3)使用现有的sourcemap转换工具库实现翻译功能
下面以nodejs的source-map库举个例子:
2.选取map文件的技巧:
(1)当sourcemap文件夹下面存在APP文件下时,则直接选取这个文件夹下的map文件来翻译
(2)否则选取FULL文件夹下的map文件来翻译
(3)正常来讲,需要先选对map文件,接着运行以上代码之后,就可以将看不懂的行列号和文件名翻译成看得懂的行列号和文件名了
3.框架开发的小程序如何翻译jserror的堆栈?
当使用框架来开发小程序时,如果要正确翻译,则需要遵循以下步骤:
(1)注意将webpack打包的devtool属性设置成source-map,这样打包后的文件就会给每个js文件都对应生成一个map文件
(2)走正常的上传小程序的流程,获取sourcemap文件。
微信小程序有一点做得比较好的就是这里了:我们生成了那么多的map文件,是不占用小程序宝贵的2MB的大小的,正常上传上去,就会带上刚才给每个js文件生成的map文件,小程序这边会将这一系列的map文件做好合并处理。经过这一系列的操作之后,就能够完美的将大部分的jserror的堆栈信息翻译成可读性较高的信息了。
QAPM小程序的sdk中,比较重要的一些技术细节就是上面说到的5大类了。