测试说:“支付页点了没反应。”
开发打开自己的手机,操作一遍,一切正常。于是问题单里只剩下一句:“我这里没问题,麻烦再试一下。”
这句话通常不是结论,只能说明开发当前使用的设备、版本、账号和网络没有复现。真正的问题现场,应该让另一个人不需要猜测,就能回答:哪一版、什么环境、怎么操作、发生了什么。
核心结论:可用的问题现场不是一张报错截图,而是一组能连接“用户操作、应用状态和程序证据”的信息。目标不是证明问题存在,而是让问题能够复现、定位和验证。
先确认是不是同一个应用
很多“你有问题、我没问题”,最后发现双方运行的根本不是同一份代码。
至少要确认:
- App 版本号和构建号;
- Debug、Profile 还是 Release;
- 测试、预发还是生产环境;
- Android 或 iOS,以及系统版本;
- 设备型号、语言、时区和字体缩放;
- 当前账号、权限和功能开关;
- 能对应到源码的提交、流水线编号或发布标识。
版本号相同也不一定代表构建产物相同。灰度配置、远程开关、渠道包和后端环境都可能改变运行结果。
一条完整的复现步骤应该怎么写
“进入订单页后闪退”信息太少。更有用的写法是:
- 使用测试账号登录预发环境;
- 将系统语言切换为英文;
- 打开待支付订单;
- 关闭网络后点击“立即支付”;
- 恢复网络,再次点击按钮;
- 页面停留约 3 秒后退出。
同时记录:
- 预期结果:显示网络错误,可以重新提交;
- 实际结果:按钮一直 Loading,返回后应用退出;
- 复现频率:5 次出现 3 次;
- 首次发生时间:精确到分钟,并写明时区。
步骤要包含必要的前置状态。账号数据、权限、缓存、弱网和前后台切换,往往正是问题出现的条件。
截图只能说明结果,录屏还能说明过程
截图适合记录错误文案、布局异常和最终页面。涉及点击无响应、页面跳转、闪烁、键盘、手势或动画时,录屏通常更有价值。
录屏最好包含问题发生前的几步操作,并保留状态栏时间。这样可以把画面时间与日志时间对齐。
但不要在录屏中暴露手机号、验证码、支付信息、Token、助记词或内部服务器地址。提交前先脱敏。
日志不是越多越好
几万行无关日志会让真正的错误更难找。有效日志至少需要:
- 精确时间和时区;
- 日志级别与业务模块;
- 页面、操作名称和请求标识;
- 错误对象与完整堆栈;
- 能对应发布版本的构建标识。
Flutter 中可以用 dart:developer 保留错误和堆栈:
import 'dart:developer' as developer;
Future<void> submitOrder(String requestId) async {
try {
await orderRepository.submit();
} catch (error, stackTrace) {
developer.log(
'submit order failed, requestId=$requestId',
name: 'checkout',
error: error,
stackTrace: stackTrace,
);
rethrow;
}
}requestId 或 Trace ID 可以把客户端日志、网关日志和服务端日志串起来。不要记录密码、访问令牌、完整身份证号或未经脱敏的请求体。
不同问题需要不同证据
| 问题类型 | 优先保留的证据 |
|---|---|
| 崩溃 | 完整堆栈、构建标识、符号文件、崩溃前操作路径 |
| 接口异常 | 请求标识、状态码、耗时、环境、脱敏后的关键参数 |
| 页面卡顿 | 目标设备、刷新率、Profile Trace、触发步骤 |
| 布局问题 | 截图、屏幕尺寸、语言、字体缩放、输入文案 |
| 偶现状态错误 | 录屏、状态变更日志、请求顺序、账号与前置数据 |
崩溃堆栈如果没有匹配当前构建的符号文件,定位价值会大幅降低。使用代码混淆时,也要保存与该构建对应的符号化材料。
性能问题则不能只交一段 Debug 模式录屏。应在目标设备的 Profile 或 Release 模式下采集 Trace,并说明设备刷新率和复现操作。
本地排查时常用的信息
环境差异明显时,可以先收集:
flutter --version
flutter doctor -v连接设备后,可根据场景查看 flutter logs、Android Logcat 或 Xcode 设备日志。命令输出很长时,只保留问题时间窗口,并附上原始文件,避免在问题单中粘贴无法阅读的大段文本。
线上问题无法临时连接调试工具,更需要提前建设:
- 发布版本和构建标识;
- 崩溃与非致命异常上报;
- 页面与关键操作轨迹;
- 网络请求标识和耗时;
- 功能开关、环境和必要设备信息。
采集范围要遵守隐私政策和最小必要原则。为了排障而无限收集用户数据,同样是工程问题。
一份可以直接使用的问题模板
## 问题描述
一句话说明现象和影响。
## 构建与环境
- App 版本 / 构建号:
- 提交或流水线编号:
- 环境 / Flavor:
- 设备 / 系统:
- 账号、权限、功能开关:
## 复现步骤
1.
2.
3.
## 预期结果
## 实际结果
## 复现频率与时间
## 证据
- 截图或录屏:
- 日志时间范围:
- Crash / Trace / requestId:模板的价值不在于字段填满,而在于减少来回追问。与问题无关的字段可以省略,关键条件不能靠猜。
一套实用的处理流程
修复后要回到原设备、原账号或等价环境验证。只在开发自己的正常环境里验证,无法证明原问题已经解决。
最后记住这几点
- “我这里没问题”只能说明当前环境没有复现,不代表问题不存在。
- 先确认构建身份,再讨论代码行为。
- 复现步骤要同时记录预期、实际、频率和前置状态。
- 不同问题需要不同证据,截图不能代替堆栈或 Trace。
- 日志要能关联请求和版本,同时遵守脱敏与最小采集原则。
- 修复必须在原问题条件下回归验证。
问答复盘
Q1:有错误截图,为什么还不能开始定位?
答: 截图通常只有最终结果,缺少构建版本、操作过程、状态变化和程序堆栈,无法判断问题从哪里开始。
Q2:版本号相同,能否确认双方运行的是同一份代码?
答: 不能。还要核对构建号、提交或流水线标识、环境、渠道和远程功能开关。
Q3:日志是不是记录得越详细越好?
答: 不是。应记录定位所需的时间、模块、请求标识、错误和堆栈,同时避免敏感信息与无关噪声。
Q4:卡顿问题为什么不能只看 Debug 模式录屏?
答: Debug 模式有额外调试开销,不能代表线上表现。应在目标设备的 Profile 或 Release 模式下采集帧数据和 Trace。
Q5:偶现问题最值得补充什么信息?
答: 精确时间、复现频率、操作录屏、账号前置数据、请求顺序和关联标识,这些信息能帮助还原发生条件。
Q6:问题修复后,在开发手机上验证通过就可以关闭吗?
答: 不够。应尽量使用原设备、原账号、原环境和原操作路径回归,确认导致问题的条件已经被覆盖。