背景
作为程序猿,想必大家有过这样的经验。有报错信息,能必现的问题,调试修改起来一般都比较顺利。而那种没有报错的问题,难以发现、难以调试。很可能查了大半天,最后发现只需要改几行代码。付出与收益不成比。
举个例子:
某个小程序上调用某个 API,开发测试时候可以的,但后面因为一些原因突然没达到预期的效果。排查时候发现这个 API 因为一些原因走到了 fail 回调,但代码里只写了 success 回调,并没有写 fail 回调。
在实际开发中一般不会对每个 API 调用和其回调都打日志,多个 API,我们很难知道其调用过程,冷不丁就会出现意料外的情况而不知。
举个例子:
小程序上调用某个广告 API,但发现点广告关闭按钮,onClose 的回调一直没被调用。排查时候发现开发者在 page 的 onHide 回调里调用了 offClose,而进入广告会触发 page 的 onHide。
如果在小程序开发中能收集 API 调用与 API 回调的信息,对这些异常情况进行判断,在需要的时候能知道哪些 API 被执行,以及相互间的关系,那么在出现这类问题时能省掉大量排查时间。
快速开始
安装
在项目中
npm install --save proxy-monitor
在小程序中会用到 npm,参考文档
使用
以微信小程序为例 在小程序的 app.js 中
const ProxyMonitor = default;const monitor = wx apiPrefix: 'wx' { console; };wx = monitorproxy; //修改全局变量
调用某个 api,没有传 fail 回调,却应该走到 fail 回调,如:
wx;
那么能收到打印信息:
建议一般情况只打印、上报 ret 不为 ok 的日志,要了解多个 API 及回调的调用顺序与依赖关系再打印更多信息。
原理说明
用Proxy
和defineProperty
可以监听对象属性的set
、get
,然后对函数进行包装,可以收集到参数、结果等信息。
Proxy
和defineProperty
不同点是:
Proxy
可以在对象的属性get
时进行处理,可以理解为懒处理。对于api
调用缺少fail
回调的情况,我们可以补一个空函数。如:
{
"apiId": "4",
"apiName": "wx.showToast:args[0].fail:autoadd:callback",
"property": "fail",
"pid": "3",
"parentApiName": "wx.showToast",
"traceId": "3",
"traceApiName": "wx.showToast",
"args": [
{
"errMsg": "showToast:fail parameter error: parameter.title should be String instead of Number;"
}
],
"ret": "fail",
"isAsync": false,
"time": 1589532839302
}
这样能发现api
失败而漏传fail
回调的情况。
defineProperty
需要事先对对象属性进行改写监听,如果对每个缺少fail
的api
补充fail
,那么可能会造成内存溢出的问题。
defineProperty
性能会比Proxy
好些,但不是高频调用,可忽略不计。
默认用Proxy
进行处理,对不支持Proxy
或用参数控制可以用defineProperty
处理。
对api
的结果进行递归处理,可以得到多层api
调用链,如:wx.getFileSystemManager.saveFile
。
综合处理api
和其回调,可以分析判断一些异常情况,参考下面详细介绍。
详细介绍
ProxyMonitor 类
new ProxyMonitor(target[,options])
target
监听目标对象
options
选项
参数 | 类型 |
---|---|
shouldHandleMethod | function |
shouldHandleResult | function |
shouldReportPropertySet | function |
report | function |
getApiId | function |
hookSetTimeout | function |
hookSetInterval | function |
shouldHookConsoleError | function |
apiCbkTimeout | number |
eventCbkTimeout | number |
dealType | string |
shouldHandleMethod(target,rootProperty,property):boolean
判断方法调用是否监听,缩小监听范围,避免大量无用信息
参数 | 类型 | 说明 |
---|---|---|
target | object | 目标对,如 FileSystemManager 对象 |
rootProperty | string | 调用链,如 wx.getFileSystemManager.saveFile |
property | string | 方法名,如 FileSystemManager 下的saveFile |
shouldHandleResult(target,rootProperty,property):boolean
判断是否对方法结果进行监听,如果该结果下的 api 属于高频调用,监听意义又不大,可以不做监听,如Canvas.getContext
参数 | 类型 | 说明 |
---|---|---|
target | object | 目标对象 |
rootProperty | string | 调用链 |
property | string | 方法名 |
shouldReportPropertySet(target,rootProperty,property):boolean
判断是否监听对象的 set 操作,如多媒体对象的src
的 set 操作可以监听,但其他的可以不做监听
参数 | 类型 | 说明 |
---|---|---|
target | object | 目标对象 |
rootProperty | string | 调用链 |
property | string | 属性名 |
report(reportData)
接收处理 api 调用产生的数据
reportData
对象
属性 | 类型 | 说明 |
---|---|---|
apiId | string | id |
apiName | string | 调用链 |
property | string | 属性名 |
pid | string | 父 id,如saveFile 在downloadFile 的回调中运行,那么其父 id 就是 downloadFile 回调对应的 id |
parentApiName | string | 父调用链 |
traceId | string | 链路 id,既根api 的 id |
traceApiName | string | 根api |
args | array | 方法参数 |
res | array | 方法结果 |
ret | string | 运行标识,一般情况下可以打印、上报不为ok 的数据,需要分析排查问题时上报更多内容 |
time | number | 执行时间 |
isAsync | boolean | api 的回调是否是异步执行 |
例子:
{
"apiId": "7",
"apiName": "wx.getFileSystemManager.saveFile:args[0].fail:callback",
"property": "fail",
"pid": "5",
"parentApiName": "wx.getFileSystemManager.saveFile",
"traceId": "3",
"traceApiName": "wx.downloadFile",
"args": [
{ "errMsg": "saveFile:fail permission denied, open \"./test.txt\"" }
],
"ret": "fail",
"isAsync": true,
"time": 1589524829196
}
对应代码是:
wx.downloadFile({
url: 'http://down.qq.com/qzone/c.txt',
success(res) {
console.log('download success', res)
fileSystemManager.saveFile({
tempFilePath: res.tempFilePath,
filePath: './test.txt',
success(res) {
console.log('save success')
},
fail(res) {
console.log('save fail', res)
}
})
},
fail(res) {
console.log('download fail', res)
}
})
parantApiName
和traceApiName
能关联离散的日志,帮助理解。借助zipkin这样的全链路日志系统,可以可视化观察 API 调用上下文,帮助开发者分析日志,排查问题。
ret
的说明
名称 | 说明 |
---|---|
ok | 正常 |
fail | fail 回调执行 |
onError | 事件监听onError 、onSocketError 、onUpdateFailed 的回调执行 |
exception | api 或回调执行出错。开发者可能会catch 住错误但不打印,导致出问题而不知 |
fail | fail 回调执行 |
timeout | success 、fail 回调超时 |
timeout_maybe | 事件监听的回调超时 |
errorLog | 构造参数shouldHookConsoleError 传true 时console.error 的日志 |
cbkMultiWarn | success 、fail 被多次调用的警告(一般是平台侧 api 实现的问题) |
cbkOrderWarn | complete 在success 、fail 调用前被调用(一般是平台侧 api 实现的问题) |
cbkAsyncWarn | 可能你期望api 的success 、fail 回调是异步的,但平台侧实现上是同步的,导致你的代码在时序上出现问题,这个会帮到你 |
getApiId():string
id 生成器
如果上报,需要生成唯一 id
hookSetTimeout(setTimeout)
包装setTimeout
因为setTimeout
是异步的,setTimeout
外的api
调用和setTimeout
回调里的api
调用会失去关联关系。对setTimeout
进行处理可以将上下关联起来。
一般情况下不用传
用法示例:
//app.js
const ProxyMonitor = require('proxy-monitor').default
const monitor = new ProxyMonitor(wx, {
apiPrefix: 'wx',
report(data) {
console.log('report data', JSON.stringify(data))
},
hookSetTimeout: setTimeout
});
wx = monitor.proxy //修改全局变量
App({
onLaunch: function () {
let fileSystemManager = wx.getFileSystemManager();
wx.downloadFile({
url: 'http://down.qq.com/qzone/c.txt',
success(res) {
console.log('download success', res)
monitor.setTimeout(function () {
fileSystemManager.saveFile({
tempFilePath: res.tempFilePath,
filePath: './test.txt',
success(res) {
console.log('save success')
},
fail(res) {
console.log('save fail', res)
}
})
})
},
fail(res) {
console.log('download fail', res)
}
})
}
})
上报数据示例:
{
"apiId": "8",
"apiName": "wx.getFileSystemManager.saveFile",
"property": "saveFile",
"pid": "7",
"parentApiName": "wx.setTimeout:callback",
"traceId": "3",
"traceApiName": "wx.downloadFile",
"args": [
{
"tempFilePath": "http://tmp/wx69f996eabbd34128.o6zAJs-_DIy1PGG3-yf5SNVll3yY.bpVYCxdHxs3P5d793fc5b00a2348c3fb9ab59e5ca98a.txt",
"filePath": "./test.txt",
"success": "function",
"fail": "function"
}
],
"ret": "ok",
"isAsync": null,
"time": 1589530097006
}
hookSetInterval(setInterval)
同上
shouldHookConsoleError:boolean
是否监听console.error
,没有其他错误处理上报,可以设为 true
上报数据示例:
{
"apiId": "7",
"apiName": "wx.ConsoleError",
"pid": "6",
"parentApiName": "wx.getFileSystemManager.saveFile:args[0].fail:callback",
"traceId": "2",
"traceApiName": "wx.downloadFile",
"args": ["save file error"],
"ret": "errorLog",
"isAsync": null,
"time": 1589531227058
}
shouldHookConsoleError():boolean
是否监听console.error
apiCbkTimeout:number
判断success
、fail
回调超时时间,默认 3 秒
eventCbkTimeout:number
事件监听的回调超时时间,默认 30 秒
dealType:string
监听处理类型,Proxy
或defineProperty
。默认Proxy