Frontend Insight:从错误计数到可排查的运行上下文
解释错误快照、响应流、显式关联与 SourceMap 如何组成诊断过程,以及采集完整度、业务影响和存储成本之间的取舍。
Frontend Insight 是面向内部 Web 产品的运营与质量分析平台,已完成交付。它包含页面与功能使用、业务工作流、效率和质量分析。这篇文章以运行时诊断为线索,解释系统如何从错误统计走到可用于排障的上下文。
我负责需求、指标口径、产品结构、设计评审和验收反馈,与 AI 协作实现、测试和整理交接材料。验收时发现,页面能显示错误数量,但错误原文、堆栈与请求上下文不足以支持定位,随后继续补齐诊断链路。下面的实现说明对应 refactor 的 aad88a6 版本。
错误计数和排障需要不同的信息
看到错误次数上升,工程师能知道问题还在发生,却还不知道怎样复现。点开其中一次错误后,需要继续找:当时抛了什么异常,用户先做了什么,哪条请求与它有关,页面运行的是哪个构建版本。这些信息决定接下来查哪里,单独增加一个报错字符串还不够。
诊断信息也有成本。原始对象可能很大,响应正文可能是流,日志对象可能在打印后继续变化;保存更完整的上下文会增加浏览器处理、传输、存储和访问控制负担。因此,这部分工作的价值取决于两点:保留下来的信息能否帮助还原当时状态,以及采集行为是否改变业务运行。
因此,系统把“发生了一次错误”与“这次错误有哪些详情”分开保存。基础事件用于统计,详情可以被采样、截断、限流,也可能过期或写入失败。即使详情不再可读,这次错误仍然发生过;否则只要减少诊断采集,报表里的错误率就会跟着下降。
沿一次错误追踪信息怎样形成
浏览器的全局异常、Promise rejection、框架适配和显式上报汇入错误入口。提取器把异常及其上下文转换为受预算约束的快照,再交给采集 API、Kafka 和 consumer。服务端分别保存基础事件与受限详情,页面按事件读取并展示。
这里的“快照”有两个约束。首先,采集对象时不能顺手执行业务 getter 或 toJSON:序列化本身可能抛异常或改变状态。实现通过属性描述符读取数据属性,并限制节点、深度和序列化大小;无法读取的字段保留原因。其次,console 记录需要对应调用时的内容,异常发生时又需要冻结当时的回溯窗口,不能展示对象后来被修改的状态。
例如,应用先记录一个状态对象,再修改它,最后抛出异常。如果仅保存对象引用,详情页中的前置日志就可能显示修改后的值。这会改变工程师对事件顺序的理解。这个例子用于解释快照语义;相关回归还检查并发、跨页面切换和 tracker 销毁后的记录归属。
实现路径见诊断维护说明。
请求正文为什么由应用提供
请求采集容易产生一个看似方便的实现:包装 fetch 后,再读一遍 Response,把内容放入日志。问题在于响应正文具有消费语义,业务可能还要读取它;流式或未知长度的正文,也不能假定能被低成本地完整复制。
当前实现记录受控 fetch 的请求过程,并接收应用提供的已读响应文本。SDK 不再调用业务 Response 的 text() / json(),也不通过 clone() / tee() 在后台读取另一份流。这样会限制自动采集的完整度,但不会为了记录正文再创建一条读取任务。应用自身读取文本的开销仍然存在。
假设请求刚发出去,页面就抛了异常,此时诊断里的 pending 是准确的。几秒后收到响应,应用再读到正文,这些都是后来的事实。SDK 可以更新当前页面缓冲中的请求记录,但已经冻结并发送的异常信封要保留原样,才能还原出错当时的状态。请求发起、收到响应、取得正文和异常发生,因此需要分别记录。
正文预算也不能只按 JavaScript 字符数计算。JSON 转义、UTF-8 编码和状态元数据都会占空间。请求快照实现按序列化大小寻找可保留前缀,并避免截断 UTF-16 代理对;超过预算时标为 truncated。由此,页面能够区分“正文为空”“应用未提供”“无法读取”和“只保存了前缀”,排查者不必从一个空白框猜原因。
textEvidence 的前缀选择核心如下,省略返回的状态元数据。jsonBytes 表示 JSON 序列化后 UTF-8 编码的字节数:
const reserve = 180;
let lo = 0, hi = Math.min(value.length, limit);
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2);
if (jsonBytes(value.slice(0, mid)) <= limit - reserve) lo = mid;
else hi = mid - 1;
}
if (lo && /[\uD800-\uDBFF]/.test(value[lo - 1]!)) lo--;
const kept = value.slice(0, lo);
搜索通过测量候选前缀的序列化大小缩小范围,上界同时受字段预算限制,避免在一份很大的正文上无限扩大搜索范围。这里做保守截取,不把字符数当作字节数,也不承诺找到所有 Unicode 编码边界上的最长前缀。reserve 给字段状态等元数据留空间,末尾代理项检查避免留下半个字符。后续仍需检查单条记录、集合和总信封预算,单字段截断不能保证整个事件一定可接收。这段实现把“限制大小”落实为可检查的编码与预算规则。
时间邻近能提供线索,不能直接确定归属
同一页面可能并发执行多个请求。同一个操作名称也可能被点击两次。如果只按时间或名称关联错误,很容易把另一轮操作的请求放进来。
系统使用显式 request、operation 和 workflow 实例标识建立关联。没有显式依据时只展示时间邻近关系。本地观察 ID 也不冒充服务端请求 ID:两者解决的范围不同,只有业务实际传递并返回关联信息时,才有进一步跨端关联的依据。
工作流分析还需要处理迟到和重复事件。归约实现同时检查事件发生时间和接收时间,再按实例重建状态。举一个演算场景:事件在 10:00 发生,10:05 才被系统接收。查询截至 10:02 时已知的状态,应排除它;只看事件时间则会让后来收到的信息改写“当时已经知道什么”。
相同事件 ID、相同内容的重复投递可以去重;同 ID 内容冲突需要拒绝并记录原因。缺少开始事件会进入拒绝记录;实例上下文或步骤证据不足等情况可形成 unresolved。包含 unresolved 的汇总不返回成功率,避免把无法确定的实例隐含当成失败或忽略后继续给出一个看似精确的百分比。
SourceMap 能否定位,先取决于构建身份
压缩栈里的文件名、行号和列号,只有与实际部署的构建匹配,才适合映射回源码。使用同名文件的另一份 map,可能产生一个有行号、却属于错误版本的位置。
上传流程按项目、release、build 与资源标识组织文件,校验 JavaScript 和 map 的摘要;同一身份对应不同内容时拒绝覆盖。查询时使用匹配的私有 map,由服务端受限解析器处理,不从错误栈里的 URL 自动下载内容。
映射结果和原始编译栈分别保留。版本不符、map 缺失、映射失败、位置已找到但没有 sourcesContent,会得到不同状态。最后一种情况尤其容易混淆:可以定位到文件和行号,并不代表能够展示源码片段。页面给出降级原因,比补读未知版本源码更有助于判断结果是否可用。
SourceMap 实现还包含上传凭证、权限检查、过期和私有文件生命周期。原文读取与导出分别检查权限,前端着色使用文本节点,复制与导出保留原文。
页面需要解释信息停在哪一层
“没有堆栈”可能发生在不同阶段。若所有情况都变成一个空数组,使用者无法决定应该修 SDK 接入、等待存储、上传 map,还是申请权限。
| 层次 | 一种状态 | 对排查的含义 |
|---|---|---|
| 采集 | truncated / unavailable | 源信息不完整或不可读,不能推断原文不存在 |
| 存储 | pending / write_failed / expired | 基础错误已出现,详情可能尚未可读、写入失败或已过期 |
| 解析 | partial / parse_failed | 原始内容仍保留,派生结构未完全获得 |
| 访问 | forbidden | 当前身份不能读取,不代表系统没有保存 |
有了这些状态,使用者看到 pending 可以先查消费进度,看到 forbidden 则应检查当前权限,而不必把所有空白都归因于 SDK 漏采。错误次数仍从基础事实计算,不随详情的采样、过期或解析结果变化。
验证覆盖了什么,还不能说明什么
验证沿浏览器、采集、消费、存储、授权 API 与页面检查同一事件。故障场景包括非标准栈、响应流、跨页面晚回调、权限撤销、错误构建身份和私有文件缺失;恢复验证在隔离环境执行。测试对应到这些行为,才比单独报一个测试数量更能说明交付范围。
D7 记录中的性能测量使用 709db4bb,环境为 4 vCPU、约 16.7 GB 内存的 GitHub Ubuntu runner,依赖服务运行在容器内。异常事件以 200 events/s 持续 10 秒时:
| 指标 | 结果 | 计时含义 |
|---|---|---|
| 接收 p95 | 52.89 ms | 接收请求的响应延迟 |
| 诊断持久化 p95 | 9,562 ms | 经过异步处理到详情持久化的延迟 |
接收快不代表详情立即可查,两项的差距提示要继续观察消费和存储环节。这个短时合成负载能验证当次环境的处理行为,不能证明长期容量或目标生产环境的承载上限。记录还显示基础 SDK gzip 为 12,287 bytes,完整诊断组合为 24,976 bytes;可选模块让基础接入较轻,完整诊断仍有明确的体积成本。
当前项目已交付,具体接入与操作见用户指南。维护时可沿开发者阅读路径继续检查各层实现;新环境的部署参数、容量和保留周期仍需重新验证。
按实现主题继续阅读
项目介绍说明整体取舍;开发者阅读入口按任务组织完整系列。理解数据为什么出现或缺失,可从事件管道和读模型开始;维护错误原文、请求、SourceMap 与策略,继续阅读诊断实现。