测试说:“支付页点了没反应。”

开发打开自己的手机,操作一遍,一切正常。于是问题单里只剩下一句:“我这里没问题,麻烦再试一下。”

这句话通常不是结论,只能说明开发当前使用的设备、版本、账号和网络没有复现。真正的问题现场,应该让另一个人不需要猜测,就能回答:哪一版、什么环境、怎么操作、发生了什么。

核心结论:可用的问题现场不是一张报错截图,而是一组能连接“用户操作、应用状态和程序证据”的信息。目标不是证明问题存在,而是让问题能够复现、定位和验证。

先确认是不是同一个应用

很多“你有问题、我没问题”,最后发现双方运行的根本不是同一份代码。

至少要确认:

  • App 版本号和构建号;
  • Debug、Profile 还是 Release;
  • 测试、预发还是生产环境;
  • Android 或 iOS,以及系统版本;
  • 设备型号、语言、时区和字体缩放;
  • 当前账号、权限和功能开关;
  • 能对应到源码的提交、流水线编号或发布标识。

版本号相同也不一定代表构建产物相同。灰度配置、远程开关、渠道包和后端环境都可能改变运行结果。

一条完整的复现步骤应该怎么写

“进入订单页后闪退”信息太少。更有用的写法是:

  1. 使用测试账号登录预发环境;
  2. 将系统语言切换为英文;
  3. 打开待支付订单;
  4. 关闭网络后点击“立即支付”;
  5. 恢复网络,再次点击按钮;
  6. 页面停留约 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:问题修复后,在开发手机上验证通过就可以关闭吗?

答: 不够。应尽量使用原设备、原账号、原环境和原操作路径回归,确认导致问题的条件已经被覆盖。