7 月 15 日,一名用户进入了监控规则创建页,但在创建规则的过程中,浏览器连续抛出了 3 次相同的异常:
TypeError: Cannot read properties of undefined (reading 'filter')
三次报错发生在不到半分钟内。用户未必会为这种局部异常提交工单,研发也很难仅凭一句“刚才页面好像不太对”稳定复现。
不过,部署在页面里的 RUM SDK 已经把现场留下了。
这里的 RUM,是 Real User Monitoring,也就是真实用户监控。它记录的不是测试环境里页面能否打开,而是真实用户使用网页或 App 时发生了什么。错误发生后,RUM 可以关联当时的页面、版本、设备和访问过程,并把相似错误聚合成一个 Issue。
这条 Issue 告诉我们:错误集中在监控规则创建页,当前样例影响 1 个会话、出现 3 次,来自同一个正式发布版本。会话时间线还保留了用户报错前后的页面访问。
RUM 的第一层价值就在这里。它没有急着猜根因,而是先把一条散落在浏览器控制台里的错误,变成了一件可以查询、可以聚合、可以继续调查的线上问题。
SourceMap 把线上错误带回了源码
前端代码发布后通常会经过打包和压缩。没有 SourceMap 时,浏览器堆栈往往只能指向类似 index-q2_vQxih.js:134:36624 的位置。研发拿到这样的行号,仍然需要花时间寻找它对应哪份源码。
SourceMap 保存了压缩代码与开发源文件之间的映射。只要上传与发布版本匹配的 SourceMap,RUM 就能把线上堆栈重新还原为开发时的文件、函数和行号。
在这条 Issue 中,RUM 把最上层堆栈还原到了:
src/Packages/monit/pages/rule/add/content.tsx:322:16
调用它的位置也被还原到了 content.tsx:287:12。展开堆栈,出错源码直接显示在页面里:
if (queries.filter((i) => i.expr).length === 0) return false;

(截图已裁去 Issue、应用、用户和会话标识)
到这里,问题已经从“某个压缩文件调用 filter 失败”,变成了一个研发可以直接阅读的问题:validateQueryStep 在校验告警查询条件时,对 queries 调用了 .filter()。
但知道哪一行出错,还不等于知道为什么出错。queries 为什么会是 undefined?这份文件属于哪个仓库?应该修改前端基座,还是负责监控规则的微前端?这些工作仍然需要继续完成。
RUM 看见现场,SRE 自动化把现场交给 AI SRE
在我们配置的流程中,RUM Issue 会进入 Flashduty 的 Incident 响应流程,再触发一条 SRE 自动化。
AI SRE 关注线上系统如何更快发现问题、定位问题并恢复,同时用自动化减少重复劳动。这条自动化负责规定触发条件、注入 RUM 上下文、选择排障步骤和工具,以及 AI SRE 可以执行到哪一步。
AI SRE 拿到的不是孤零零的一行报错,而是整组证据:错误类型、出现次数、页面 URL、发布版本、会话分布和 SourceMap 堆栈。
这次排查中还有一个很实际的难点。应用级映射首先指向前端基座 foundation-app,但错误页面是 /monit/rule/add,AI SRE 结合路由和源码路径继续下钻,最终把修改目标定位到了负责监控功能的独立微前端,而不是在基座仓库里盲目搜索。
这一步很重要。大型前端应用经常由基座和多个微前端共同组成。只知道“哪个应用报错”还不够,页面路由、SourceMap 文件路径和仓库映射需要一起使用,才能找到真正该改的代码。
根因藏在一次表单初始化里
SourceMap 还原出的上下文大致如下:
const { rule_configs } = allValues;
if (!rule_configs) return false;
const {
check_threshold,
check_anydata,
check_nodata,
queries,
} = rule_configs;
if (queries.filter((i) => i.expr).length === 0) return false;
代码检查了 rule_configs 是否存在,却直接假定其中的 queries 一定是数组。表单初始校验发生时,rule_configs 已经存在,而查询条件尚未完成初始化,queries 因此可能是 undefined。
这也解释了为什么错误会在同一次访问里连续出现:每次触发表单校验,都可能再次走到相同代码。
修复不需要改接口,也不需要重写表单,只要让“尚无查询条件”的状态按原本意图返回校验不通过:
- if (queries.filter((i) => i.expr).length === 0) return false;
+ if ((queries ?? []).filter((i) => i.expr).length === 0) return false;
改动只有一处。queries 存在时,原有逻辑完全不变;尚未初始化时,空数组会让校验返回 false,而不是抛出异常。
最后一条报错之后 8 分钟,修复进入主分支
这条 Issue 留下了一条很完整的时间线。
- RUM 记录到最后一条错误样例。
- 约 4 分钟后,针对
queries.filter(...)的修复提交完成。 - 约 8 分钟后,修复 PR 合并进入主分支。
PR 描述中写明了线上错误、服务与版本、SourceMap 行号、根因和一行代码差异,并标注由 Agent 生成。
这里的“自动修复”并不等于 AI 可以绕过研发流程直接发布。AI SRE 可以读取证据、修改代码并创建 PR;PR 是否合并、何时发布,仍然由团队决定。在这次案例中,修复随后被合并。RUM 页面显示,最后一条错误正好发生在合并之前;接下来的 7 天里,没有再出现新的同类样例。
一周后,我们又把同一条 Issue 交给 AI SRE 做了一次回放。它重新读取 RUM 证据,沿着页面路由和 SourceMap 找到监控微前端,并确认主分支已经包含上述保护,因此没有必要再次修复。对 SRE 来说,避免重复改动和避免漏修一样重要。
不是每条 RUM Issue 都应该进入代码仓库
另一条 fetchError · Network request failed 也曾触发同一套自动化。它只影响 1 个移动端会话、发生 1 次,没有形成同类错误聚集,也没有足够的页面和堆栈证据。
AI SRE 没有为了“完成任务”而强行改代码,而是记录了当前判断和证据缺口:先观察;如果错误反复出现或影响面扩大,再重新触发调查。

这两条 Issue 放在一起,恰好说明了自动化的价值。明确指向源码缺陷的问题,可以继续推进到 PR;证据不足或更像偶发环境问题的 Issue,则应该停下来,而不是制造一份没有把握的代码改动。
RUM 与 AI SRE,连接之后才形成闭环
RUM 单独使用已经有价值。它让团队知道真实用户遇到了什么,影响了多少会话,集中在哪个版本,并利用 Session 和 SourceMap 缩短人工复现与定位时间。
AI SRE 建立在这些证据之上。它继续完成影响判断、仓库定位、源码分析和受控操作。中间的 SRE 自动化则负责把两者连接起来:什么 Issue 可以触发、需要注入哪些上下文、使用哪些工具、能否创建 PR,以及在哪一步等待人工确认。
工程师仍然保留 Review、合并和发布权限。发布之后,RUM 继续观察新版本里是否还会出现同类错误。

RUM 提供真实用户现场;自动化负责触发、上下文、排障步骤和权限;AI SRE 完成调查与受控动作;工程师负责 Review 与发布。
如果你的团队也想跑通这条链路
如果还没有使用 RUM,可以先选择一个重要的前端应用接入 SDK。除了采集错误,也要配置版本、环境、页面和会话等上下文,并在每次发布时上传匹配版本的 SourceMap。
第一阶段的目标不必定得太大。当一条 Issue 能够回答“什么错误、发生在哪个页面、影响哪些版本和会话、对应哪一行源码”时,RUM 已经开始替团队节省排查时间。
接下来,再补齐应用、页面和代码仓库之间的映射,为 AI SRE 开放读取 RUM 证据、读取代码和创建 PR 的受控权限。建议先选择一条历史上反复出现、SourceMap 已经指向业务源码的 Issue 做回放。
一次好的回放不一定非要产出 PR。它也可能告诉你证据不足、问题已经修复,或者应该继续观察。真正值得追求的是:每一步判断都有证据,修改代码时有明确边界,最后的合并与发布仍然掌握在工程师手中。
如果你手里正好有一条难以复现的前端 Issue,欢迎带着它来跑一次 RUM × AI SRE:从真实用户现场开始,看看它能不能走到一份可 Review 的修复 PR。


