线上小程序出错没日志:wx.getRealtimeLogManager 实时日志的接入与排查
适用读者:小程序已经上线、排查线上问题主要靠用户反馈和截图的前端同学;想把异常上报从被动等待变成主动掌握的团队也值得一读。全文按真实项目的接入顺序推进,代码可以直接搬走改改就用。
上周三晚上九点多,运营在群里甩了一句「有用户反馈小程序点不开,点哪儿都没反应」,过了两分钟又补一句「安卓机,具体型号还在问」。我把开发者工具打开,每个页面挨个点了一遍,什么问题都没有。版本一样、代码一样、接口也正常,就是复现不了。这种用户点不开、本地怎么试都正常的场景,做小程序的迟早要撞上一回,而撞上之后最要命的往往不是 bug 本身,是手里没有任何日志。
传统做法是让用户自己开调试:右上角胶囊菜单里连点几下开启 vConsole,再让用户操作一遍、截图发过来。这个流程跑下来,愿意配合的用户十个里挑不出两个,截图还经常糊得看不清报错。用户白费劲,你也白费劲。这篇文章记录的就是我们当时怎么用 wx.getRealtimeLogManager 把这事儿变省事的:客户端写日志,开发者在小程序管理后台直接按用户查,用户什么都不用做。
实时日志和 console.log 到底差在哪
console.log 写出来的东西只存在于当前调试环境。开发者工具里你能看见,真机调试窗口里你也能看见,但普通用户正常使用小程序的时候,这些输出既不存储也不上传,程序一退就没了。等于你在用户手机上埋了一堆探针,探针的数据只有你本人去到那台手机上才读得到。

wx.getRealtimeLogManager 是微信官方给的实时日志能力,基础库 2.7.1 开始支持。调用方式长得和 console 一模一样,也是 info、warn、error 三个方法,区别是写入的日志会被客户端缓存并上报到微信后台,开发者在 mp.weixin.qq.com 的运维中心里就能按用户维度检索。关键的一点:它不占小程序包体积,也不用引任何第三方 SDK。
| 对比维度 | console.log | 实时日志 |
|---|---|---|
| 数据去向 | 本地调试面板,不落库 | 微信后台,可检索 |
| 谁能看 | 只有当前设备的调试者 | 开发者在后台按 openID 查 |
| 用户是否配合 | 开调试模式才可见 | 不需要,全程无感 |
| 保留时间 | 程序退出即丢 | 后台留存一段时间 |
| 适合场景 | 本地开发调试 | 线上问题定位 |
本地开发该用 console 还是用 console,实时日志是给线上那部分流量准备的。两者不冲突,我们封装时干脆同时写,一套调用两个出口。
logger 模块:封装一次,全项目复用
直接在业务代码里到处写 wx.getRealtimeLogManager() 不太好维护,接口兼容判断、页面标记这些重复逻辑会散落一地。我们项目里的做法是封一个 utils/logger.js,全项目只认它。
下面代码的依赖与环境:微信小程序基础库 2.7.1 及以上,无第三方依赖;开发工具里写入的日志只在本地可见,真机线上日志才会在后台出现。
// utils/logger.js
// 依赖:微信小程序基础库 2.7.1 及以上,无第三方依赖
// 环境说明:开发工具里写的日志只在本地,真机线上日志才进后台
// 判空:低版本基础库拿不到实例,这里静默降级成纯 console
const rt = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null
// 当前筛选标记,随每条日志一起上报,后台检索靠它
let currentFilter = ''
// 取页面栈顶路由作为日志来源标签,一眼看出发生在哪个页面
function tag() {
const pages = getCurrentPages()
const top = pages[pages.length - 1]
return top ? top.route : 'app'
}
// 对外只暴露分级方法和两个标记方法,调用方不用关心上报细节
const logger = {
info(...args) {
console.info(...args)
// 本地与线上行为保持一致,双写不冲突
rt && rt.info(tag(), currentFilter, ...args)
},
warn(...args) {
console.warn(...args)
// warn 留给异常但可自动恢复的场景
rt && rt.warn(tag(), currentFilter, ...args)
},
error(...args) {
console.error(...args)
rt && rt.error(tag(), currentFilter, ...args)
},
// 进入关键流程前调用,比如支付页、提交订单页
setFilter(msg) {
currentFilter = msg
rt && rt.setFilterMsg(msg)
},
// 追加标记而非覆盖,适合把一次流程的多个环节串起来
addFilter(msg) {
rt && rt.addFilterMsg(msg)
}
}
module.exports = logger
封装里几个设计点说一下。tag 函数取当前页面栈顶的路由路径,这样每条日志自带页面来源,后台看日志时一眼就知道发生在哪个页面,省得再翻代码对行为。兼容判断只做一次,放在模块顶层,老基础库的用户调用时静默降级成纯 console,不会报错也不会崩。
页面埋点不是每个函数都要打,打多了反而把有限的上报条数挤掉。原则是关键动作前后打 info,失败分支打 error:
// pages/order/submit.js
// 依赖:上面封装的 utils/logger 模块
const logger = require('../../utils/logger')
Page({
onSubmit() {
// 关键动作前打 info,形成行为轨迹
logger.info('提交订单开始', this.data.skuId)
// 拉起收银台是用户反馈最多的高危环节,埋点别漏
wx.requestPayment({
// 支付失败必须落 error,这是排查时的关键线索
fail: (res) => logger.error('支付失败', res.errMsg)
})
}
})
App 级的全局兜底是整个体系里性价比最高的部分。onError 能抓住同步异常和大部分异步回调里的异常,onUnhandledRejection 补上 Promise 没人接的情况。这两个钩子不接,用户手机上崩了你就什么都不知道:
// app.js
// 全局兜底:onError 抓同步与异步回调异常,onUnhandledRejection 抓没处理的 Promise
const logger = require('./utils/logger')
App({
onError(msg) {
// 线上未捕获异常几乎都从这里出去,error 级别必打
// msg 是拼接后的字符串,堆栈和消息混在一起
logger.error('App.onError', msg)
},
onUnhandledRejection(res) {
// reason 可能是对象,序列化后写入更易读
logger.error('unhandledRejection', JSON.stringify(res.reason))
}
})
setFilterMsg 与 addFilterMsg,别用混了
这两个方法名字像、作用也像,都是给当前用户的日志打筛选标记,方便后台检索,但行为差一个字:setFilterMsg 是覆盖,addFilterMsg 是追加。
| 方法 | 行为 | 典型用法 | 常见坑 |
|---|---|---|---|
| setFilterMsg | 覆盖之前设置的标记 | 进入关键流程前设置场景名,比如「支付页」 | 多个页面都调用时互相顶掉,只剩最后一个 |
| addFilterMsg | 在现有标记后追加 | 一次下单流程依次追加浏览、下单、支付 | 追加有总长度上限,无限追加会被截断 |
我们的用法是页面 onShow 里 setFilterMsg 设当前场景,流程节点用 addFilterMsg 串联。比如一次支付,标记串起来是「支付页-创建订单-拉起收银台」,后台按标记一搜,这个用户整个支付链路的日志全出来了。要注意标记的总长度有上限,具体数值以官方文档为准,反正别把接口 URL 整段塞进去,截断之后检索就用不了了。
info、warn、error 的分级不是摆设
后台查询时可以按日志级别过滤,而且客户端上报有条数配额——单个用户单位时间内能上报的条数有限,超额之后 info 级别的会被优先丢弃,warn 和 error 的保留优先级更高,具体配额数值随基础库版本调整,以官方文档当前说明为准。这意味着分级策略直接决定排查时你能不能看到关键日志。
| 级别 | 打什么 | 我们项目里的实例 |
|---|---|---|
| info | 关键节点的行为轨迹 | 进入支付页、提交订单开始、触发一次重试 |
| warn | 自动恢复的异常 | 请求超时后重试成功、缓存读取失败走了降级 |
| error | 需要人来看的异常 | 支付失败、登录态失效、App.onError 兜底捕获 |
早期项目里有人图省事全打 error,结果后台 error 日志被无意义信息淹没,真出问题时反而漏。后来定了规矩:error 只给需要立刻定位的异常,warn 给异常但已恢复的,info 只打影响排查走向的节点。条数一下降下来,配额也够用了。
mp 后台怎么查:按 openID 和按 filterMsg 两条路
查询入口在小程序管理后台:「开发」→「开发管理」→「运维中心」→「实时日志」。查询方式两条路:按用户查需要对方的 openID,按标记查就填 filterMsg。
实际用下来按 openID 查是主路径,所以拿 openID 的能力要提前铺好。我们的做法是登录成功后把 openID 一起存到自己的用户表,用户来反馈时运营先问一句注册手机号,后台一查 openID 就有了。也有团队在「意见反馈」页面顺带把 openID 展示给用户让其复制,思路都一样:反馈通道里必须带上能稳定定位用户的标识,否则日志再多也对不上号。
后台里还能再按时间范围、日志级别筛。拿到具体用户后先看 error,再看这条日志前后的 info 轨迹,基本能还原用户出问题前做了什么。当时那个「点不开」的反馈,就是这样五分钟定位到的:用户的 error 日志里躺着一条支付失败的记录,前面 filterMsg 显示卡在拉起收银台这一步,最后查明是某个旧版本基础库的兼容问题,加了个判断就收掉了。
机制剖析:一条日志从手机到后台的完整链路
理解上报机制,才知道为什么有的日志会丢、为什么后台查不到刚打的日志。
flowchart LR
A[用户操作触发埋点] --> B[写入客户端内存缓冲]
B --> C{网络可用且满足上报时机}
C -- 满足 --> D[批量上报到微信后台]
C -- 不满足 --> B
D --> E[后台按 openID 聚合存储]
E --> F[开发者在管理后台按用户或标记检索]
链路上每个环节都有取舍。客户端这边,日志先写内存缓冲,攒够量或者等到合适时机批量上报,而不是每写一条就发一次请求——这给用户省了流量,代价是极端情况下,比如用户打完日志立刻杀进程,最后一批可能没来得及传上去。所以排查时别只盯着最后一秒的日志,往前后多看一段。
后台这边按 openID 做聚合,同一个用户的日志归到一起,检索时再按 filterMsg 筛选。这就是为什么标记要设计成覆盖和追加两个方法:它本质上是这条日志流上的索引字段,你打日志时写进去的内容,决定了后台能不能快速把它捞出来。
和 console 的区别也在这里:console 是纯本地的输出通道,什么都不落;实时日志是一条带缓冲、带上报、带后台存储和检索的完整链路。理解了这一点,「为什么我本地打的日志后台看不到」「为什么用户手机上的日志缺了几条」这类问题都能自己推出来。
排查流程也可以标准化下来:
flowchart TD
A[收到用户反馈] --> B[通过注册信息换到 openID]
B --> C[管理后台按 openID 查实时日志]
C --> D[先看 error 再看前后 info 轨迹]
D --> E{能否定位到具体页面或接口}
E -- 能 --> F[修复并发新版本]
E -- 不能 --> G[补充埋点等用户再次触发]
踩坑记录:这些坑我们一个个踩过
对象直接传进日志方法,后台展示时可能变成不好读的形式,复杂对象我习惯 JSON.stringify 之后再写,顺便只留关键字段而不是整个对象。onError 的入参是一大段拼接好的字符串,堆栈和消息混在一起,打日志前按行拆一下更清爽,不然后台一屏都放不下。基础库 2.7.1 以下的用户拿到的 wx.getRealtimeLogManager 是 undefined,封装里必须判空,这个坑在封装层处理一次就够了。另外真机上日志从写入到后台可见有延迟,通常几分钟以内,别打完立刻去后台刷,等一等再查,省得怀疑自己打错了。
收尾之前,把两个误区说清楚
误区一是接入实时日志之后就觉得异常监控齐活了。实时日志的定位是「出问题之后你来查」,它不会主动推送告警,error 量暴涨也不会有人通知你。真要做监控告警,onError 里同步接一层自己的上报接口仍然是需要的,两件事不冲突。误区二是日志打得越多越保险。前面说了条数配额的存在,无节制的 info 会把你真正需要的那条挤掉,克制地打、在关键节点打,才是这个机制下正确的姿势。
工具装好了,链路铺通了,下一次用户再来说「点不开」,你手里的回应就从「麻烦配合复现一下」变成「稍等,我看看日志」。这事儿省下来的不只是排查时间,还有用户的耐心。条数配额和 filterMsg 截断这两个点,不同时期的实测数据不太一样,欢迎在评论区补充你那边的观察。
参考与延伸
- 微信官方实时日志文档:https://developers.weixin.qq.com/miniprogram/dev/framework/realtimelog/
- wx.getRealtimeLogManager 接口文档:https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.getRealtimeLogManager.html
- App 生命周期与 onError 说明:https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html
- 小程序开发框架总览:https://developers.weixin.qq.com/miniprogram/dev/framework/
微信小程序、实时日志、wx.getRealtimeLogManager、线上排查、异常上报、前端监控