白屏首屏异常定位:把 source map 和用户回放喂给调试助手后,我看到了什么
生产环境白屏的根因往往不在报错本身,而在“报错发生前用户到底做了什么”。过去我们靠堆日志、靠猜、靠灰度回滚,现在把 source map 和用户操作回放一起喂给调试助手,定位效率的提升不是线性的,而是从“小时级”直接掉到“分钟级”。这篇文章我想拆解一下这种工作流的实际效果、边界和几个反直觉的发现。
回放比报错本身更有信息量
白屏类问题最恶心的地方在于:报错栈经常指向一个“受害者”而非“凶手”。比如 React 渲染崩溃,栈顶可能是某个第三方库的内部函数,但真正的触发条件是用户在一个特定表单里粘贴了一段特殊字符,或者从一个缓存失效的页面跳转过来。
用户操作回放(比如 rrweb 录制的 DOM 快照序列 + 鼠标/输入事件)补上了这个缺失的因果链。调试助手拿到回放后,能做的事情比单纯读栈要具体得多:它能识别出“用户在崩溃前 3 秒修改了下拉框的值,从‘按月’切换到了‘自定义区间’,然后点击查询按钮,下一秒根节点被清空”。
一个真实的案例:我们有个报表页在生产环境偶发白屏,Sentry 报错栈指向一个图表库的 setOption 内部异常,但本地无法复现。把回放喂给助手后,它注意到崩溃前用户把浏览器窗口从 1920px 拖窄到了 980px 左右,而回放里能看到图表容器宽度变成了负数。这直接定位到我们自己的响应式计算逻辑在某个断点区间产出了非法尺寸,图表库只是“忠实”地抛了错。没有回放,这个栈我看一百遍也想不到是拖窗口触发的。
Source map 还原后的上下文比栈本身关键
Source map 的价值大家知道是还原混淆后的行列号,但在调试助手的工作流里,它更重要的作用是提供源码上下文。助手拿到还原后的文件和行列号,会去读附近 20-50 行的源码,结合回放里的操作序列做推断。
这里有个实操细节:source map 必须包含 sourcesContent,否则助手拿到的是还原后的文件路径和行列号,却没有源码文本可读。很多团队为了安全把 source map 的 sourcesContent 去掉了,或者只上传了不含源码内容的 map 文件,这在传统调试里够用(人自己对着本地代码看),但对 AI 助手几乎是废的——它没法去读你本地的代码。
我们的做法是:构建时生成完整 source map,上传到 Sentry 或自建的错误追踪服务时保留 sourcesContent,但在 CDN 上不公开 .map 文件。这样线上用户拿不到源码,调试助手通过 API 能拿到完整上下文。权限上用一个单独的 token 控制,只给调试流程用。
把两者结合后,助手能给出的不只是“可能原因”
单给报错栈,助手输出的是“可能是 A,也可能是 B,建议排查 C”。这种输出对老手来说价值有限,因为你自己看栈也能得出差不多的结论。
但把 source map 和回放一起喂进去,输出会变成另一种形态:按时间线重建的事件序列,并标注每个关键节点对应的源码位置和可疑状态变化。比如:
11:23:04.812 用户在 /report 页面点击「导出」按钮 (源码: ReportToolbar.tsx:142)
11:23:04.907 Redux action 'EXPORT_REPORT_REQUEST' 触发
11:23:05.231 API 返回 200,payload 大小 4.7MB
11:23:05.489 ReportTable 组件重新渲染,行数从 50 跳变到 4800
11:23:05.512 useMemo 依赖项 [data] 变化,重新计算列宽
11:23:05.583 Error: Cannot read properties of undefined (reading 'width')
at calculateColumnWidth (ReportTable.tsx:89)
11:23:05.590 React 卸载根节点,页面白屏
这个时间线不是助手凭空生成的,它把回放里的事件时间戳和源码里的关键点对应起来,中间穿插了 Redux DevTools 或类似工具导出的 action 日志(如果有的话)。重点是它标出了 API 返回 payload 大小 4.7MB 这个数据——这是回放里看不到的,是助手从网络请求日志里关联出来的。这个 4.7MB 的响应导致表格行数从 50 跳到 4800,进而触发了列宽计算里一个对 undefined 的访问。
这种输出质量,已经接近一个资深工程师花半小时手动排查后写的复盘了。但助手做到这一步只需要 30 秒左右。
反直觉的发现:助手最擅长定位的是“状态污染”类白屏
我们分析了过去半年用这套流程定位的 40 多个白屏案例,发现一个模式:纯渲染错误(比如某组件 props 类型不对)用传统栈就能定位,助手带来的增益不大;但状态污染类白屏——前一个页面的全局状态、缓存的旧版本数据、事件监听未清理、localStorage 里过期的字段——这类问题用传统方法极难定位,而助手结合回放和源码后表现非常好。
原因很简单:状态污染类问题的根因在时间上离崩溃点很远,可能在几十秒前甚至上一个页面。人类的排查习惯是从崩溃点往回追,但回放让助手能从会话起点往前推,两个方向在中间汇合。
一个典型的例子:用户从 A 页面跳转到 B 页面后白屏。报错栈指向 B 页面某个组件读取 user.preferences 失败。传统思路是查 B 页面为什么没拿到 user 数据。但助手从回放里发现,用户在 A 页面时触发了一个“保存草稿”操作,这个操作往 Redux store 里写入了一个不完整的 user 对象(只有 id 和 draft 字段),而 B 页面的组件假设 store 里的 user 一定是完整的。这个写入和崩溃之间隔了 47 秒,中间用户还做了三次无关的操作。
这类问题如果没有助手,我可能最后也能定位到,但中间会绕很多弯路。助手的优势是它不会“想当然”——它不会假设 store 里的 user 一定是完整的,它会去对比“回放里写入的数据结构”和“源码里读取的字段”。
边界:不是所有白屏都适合这套流程
得说清楚这套方法不擅长什么。
首次加载白屏(用户打开页面就白,没有操作)回放的价值很低,因为没有操作序列可分析。这种情况更依赖 source map 还原报错栈,以及构建产物分析。
Web Worker / WASM 内部的崩溃:回放只能录主线程的 DOM 和事件,Worker 里的错误链路是断的。助手能拿到报错栈,但回放无法提供操作上下文。
内存泄漏导致的渐进式卡顿后白屏:回放能记录 DOM 变化,但内存占用是采样数据,且采样频率通常很低(rrweb 默认不记录性能指标)。助手可能能发现“页面元素数量持续增长”这个线索,但根因定位还是要靠 heap snapshot。
iframe 内的白屏:跨域 iframe 的回放是黑盒,事件和 DOM 都拿不到。
这些边界不是说不该用,而是要知道什么时候这套流程能发挥最大价值:用户在页面上有明确操作序列、崩溃发生在交互之后、报错栈能还原到源码。这三条满足,基本就是这套方法的最佳适用场景。
实操上的一些具体建议
如果要把这套流程搭起来,几个具体的配置项值得注意:
回放录制:用 rrweb 的话,建议开启 recordCanvas: false(Canvas 内容录下来体积暴增且对白屏定位帮助不大),sampling 里的 mousemove 可以设低一些,但 input 和 scroll 要保留。blockClass 把敏感信息遮掉,避免回放里出现用户手机号、身份证号之类的内容——这既是合规要求,也避免助手被无关信息干扰。
Source map 上传:构建时把 .map 文件上传到错误追踪服务,而不是放在 CDN 上。webpack 的 devtool: 'hidden-source-map' 会生成 map 但不往产物里加 sourceMappingURL,这样线上代码不会暴露 map 地址。map 文件单独走 API 上传,和 release 版本号绑定。
回放和报错的关联:错误追踪服务(Sentry、Bugsnag 等)通常支持在报错事件里附带 session 标识。把 rrweb 的 session ID 写入报错的 tag,这样看到报错后能一键跳到对应的回放。调试助手需要能同时访问这两个数据源。
给助手的 prompt 结构:我现在的做法是把信息分成三个块喂给助手——报错栈(还原后的源码路径 + 行列号 + 错误信息)、回放摘要(时间戳 + 事件类型 + 关键 DOM 变化 + 输入内容)、源码片段(报错位置附近 50 行 + 相关组件的关键代码)。最后加一句约束:“先列出时间线,再标注每个可疑节点对应的源码位置,最后给出根因假设和验证步骤。”这个结构比直接扔一堆 JSON 过去效果好得多。
常见问题
回放文件太大怎么办?
rrweb 录制的回放在长会话下可能到几十 MB。建议按会话切片,每 5 分钟或每 500 个事件切一段,只保留报错前 2 分钟到报错后 30 秒的片段。另外开启 gzip 压缩(回放数据是 JSON,压缩比很高,通常能到 10:1 左右)。
source map 里没有 sourcesContent 还有救吗?
可以让助手根据还原后的文件路径和行列号,去你的 git 仓库里找对应版本的源码(通过 release 版本号关联 commit hash)。这需要调试助手能访问代码仓库,但效果不如直接在 map 里嵌 sourcesContent 稳定——因为本地代码可能和线上构建时的版本有细微差异。
调试助手误判了怎么办?
助手的输出应该当作“高优先级的排查方向”而不是最终结论。我现在的习惯是让助手给出根因假设后,必须附带“验证步骤”——比如“在 ReportTable.tsx:89 处加一个防御性判断,本地模拟 payload 4.7MB 的响应,看是否复现”。如果验证步骤跑不通,再把失败信息喂回去让助手修正。这种迭代通常两三轮就能收敛。
不用 AI 助手,这套流程还有价值吗?
有。回放和 source map 的关联本身就能大幅提升人工排查效率,AI 只是把这个过程自动化了。我们最早做这套关联时还没有引入调试助手,光是把回放和报错栈放一起看,定位时间就从平均 2 小时降到了 40 分钟左右。助手是把这个 40 分钟再压缩到 5 分钟以内。
这套流程真正改变的不是“多了一个工具”,而是排查思路的翻转:以前是从报错出发反推原因,现在是从用户行为出发正向追踪。白屏那一刻不是起点,而是整个链条的终点。