ELL3N HSU

Available in Chinese only.

← 文章

Frontend Insight:从故障现象追到可验证的修改

用质量页刷新、缺失分数和重复事件三个场景说明如何定位边界、选择测试、保留数据升级,以及区分模拟和真实链路证据。

“没有数据”可能指三件不同的事:事件没有发出,事件还没入库,或者查询没有选中它。先记下项目、环境、时间和 eventId,再沿同一条事件检查,才知道应该修哪一层。直接重启即使暂时让页面恢复,也未必能解释故障原因。

本文围绕三个可复现的案例组织排障与回归方法。命令在 Frontend Insight 源码仓库根目录执行,需准备独立测试环境;实现版本与阅读顺序见系列入口。

先分清三种验证

验证层运行入口能证明什么不能证明什么
静态检查、单元与构建pnpm check格式、工作区边界、生成物、规范名、lint、类型、单元逻辑、构建Kafka/数据库/浏览器真实协作
真实服务集成./scripts/dev smoke、各 verify-r*实际管理元数据、事件写入与读取、迁移、权限所有用户点击与浏览器生命周期
浏览器产品链路pnpm test:r8:e2e 及对应阶段 E2EChromium/WebKit 下 UI、SDK 和服务链路真实部署机器容量、最终业务批准

不是所有 Playwright 用例都使用真实后端。R8 的 quality-refresh.spec.ts 拦截 API,专门验证时间与 URL;demo-quality.spec.ts 才从真实 demo 经 ingestion 和存储验证 occurrence。阅读测试开头、fixture 和配置后,再解释它的证明范围。

R8 工作流 覆盖原产品回归;诊断扩展的完整候选验证还包括 runtime-diagnostics-d7.yml 与 runtime-diagnostics-d7-products.yml;生产工作流 使用独立生产 Compose;文档工作流 只运行格式及规范名检查。绿色文档 CI 不能当作应用回归成功。

建立可复现的隔离环境

先记录 git status --short 与 git rev-parse HEAD,不要把已有工作覆盖。源码检查使用 Node ≥24、仓库锁定的 pnpm 11.15.1,版本来自根 package.json,依赖来自 lockfile:

node --version
pnpm --version
pnpm install --frozen-lockfile
pnpm check

依赖安装完成不等于服务已启动。目标本地环境是 Apple Silicon + Docker Desktop/Compose v2;首次创建独立本地栈:

./scripts/dev doctor
./scripts/dev bootstrap
./scripts/dev up
./scripts/dev smoke
./scripts/dev status
pnpm exec playwright install chromium webkit

管理端默认 4173,demo 默认 4174。账号与完整步骤见 本地验收指南。up 会运行本地 seed;已有配置和数据需要保留时使用 ./scripts/dev up-preserve,不要为排障重复初始化。down 保留卷;reset --confirm-local-data-loss 删除该本地栈数据,不是普通重启。

scripts/dev 的 doctor 包含 arm64、Docker、内存和磁盘检查。CI 的 M5_ALLOW_NON_ARM64=1 是隔离 runner 的显式配置,不代表任意机器都满足正式环境要求。生产无需在宿主安装 Node/pnpm,使用镜像内工具链和 scripts/production,不要用本地栈替代生产入口。

案例一:已入库的新错误为什么没有显示

先从最近触发的一条错误开始。SDK 的记录可以确认它是否产生,存储和 consumer 可以确认它是否落库,页面请求则说明当前范围有没有包含它。用同一个 eventId 对上这些证据,比在多个空列表之间来回刷新更容易定位。

先跟读以下三个文件:

在独立栈启动后运行:

pnpm exec playwright test --config tests/config/playwright.r8.config.ts quality-refresh.spec.ts
pnpm exec playwright test --config tests/config/playwright.r8.config.ts demo-quality.spec.ts

两个命令默认各跑 Chromium/WebKit。第一项只需可访问的管理端静态页面,API 被模拟;第二项需要 seed 项目、demo、API、Kafka、consumer 与 ClickHouse 全部运行。不要只运行第一项便声称链路已通过。

在隔离栈中复现时,按同一 eventId 串起观察:

  1. 打开 demo 项目的页面分析质量页,选择 dev 和最近 7 天,记下 URL 的 from/to。
  2. 在实验室触发“模拟 JS 异常”。只在本地查看事件的 appId/env/timestamp/eventId,不要收集登录凭证。
  3. 观察 HTTP 202。再等待匹配的 occurrence 出现在查询结果中,避免把别的历史错误误当本次结果。
  4. 点击质量页的“刷新”,比较新的 URL to;然后切换 staging/prod。
  5. 换成截止于触发错误之前的自定义时间,再次刷新。

预设范围刷新按项目时区推进到当前时间,清理旧 cursor 和派生评分边界;自定义时间保持不变。demo 实际上报 dev,所以这条错误不应出现在其他环境。F5 或分享链接保留 URL 时间快照,与页面按钮不是同一个语义。asOf 是查询快照时间,不能替代事件时间的 to。范围通常按 from ≤ timestamp < to 过滤,因此新快照仍可能查不到范围外的新事件。

把所有刷新都改成“现在”会破坏自定义历史范围、分页与分享链接的可复现性;只切环境又没有修正旧的时间边界。修复需要区分导航、刷新按钮和自定义查询各自的语义,见质量刷新记录。

案例二:已有访问事实,为什么分数仍为空

从一个项目的指标管理打开运营或质量分数,记录版本、绑定范围、事实状态与贡献。按 领域与评分章节 跟到服务,然后读取以下测试:

pnpm test:r1c
pnpm test:r6

可以先用固定 fixture 手算一遍分子、分母、门槛、方向、目标和权重,再与断言比较。若根本没有目标或必要事实,空正式值就是应有结果。此时若直接把 golden/snapshot 改成新的输出,测试虽然变绿,原先保护的业务口径却可能被一起删掉。

一份可复核的定位记录应把页面状态对应到事实来源、版本、时间范围和保护它的测试。若需要改变 fixture,在独立分支完成;观察问题不要求发布指标或改变正式配置。

案例三:事件重复与工作流乱序

先阅读 workflow-reducer.test.ts、workflow.test.ts、durable-offsets.test.ts。然后执行:

pnpm test:r4b
pnpm exec vitest run apps/consumer/test/durable-offsets.test.ts

设一个实例的开始、步骤与终态按不同顺序到达,再重放其中一条相同事件,可以分别验证两类身份:eventId 去重控制同一事实只参与一次计算,workflowInstanceId 关联控制多个事实属于同一业务执行。两种 ID 不能互换。

ClickHouse 已写入但 offset 尚未确认时退出 consumer,恢复后可能重写原事件。正确结果依赖后续读侧去重,不能从最终数字正确推断原始表没有重复行。首个终态、超时和缺步骤按 reducer 的规则解释,不使用“最后到达覆盖一切”。完整处理路径见事件管道。

面对故障,沿最短证据链定位

现象优先检查分层判断及下一步
点击操作后没有事件SDK 是否初始化、功能是否启用、是否显式报告终态、是否 flush查 SDK/业务调用;按钮点击不等于业务完成
/v1/events 被拒绝响应拒绝码、精确 Origin、项目、v3 契约查 ingestion;不要先重建数据库
202 但查不到项目/env/时间、consumer 状态、offset/写入失败、链路延迟202 是接收阶段证据;查实际写入和读模型
质量页仍是旧错误显式刷新、URL to、类别/页面/组/cursor区分历史窗口与新快照;不要关闭环境过滤
工作流数量异常实例 ID、首个终态、超时、重复及缺步骤查 reducer fixture,再查原始受控事实
指标空白但存在访问分母、目标、样本、版本绑定、事实采集状态“有 PV”不代表所有指标可计算
403 或找不到项目登录状态、全局角色、项目成员角色查 API 授权;UI 隐藏不是安全边界
快速切项目后显示旧结果请求 scope、AbortController、generation 和卸载查对应 view;不要假设所有 view 都通过同一 hook 实现
新版本部署无法读 secret文件权限、实际容器 UID/GID、挂载用生产 doctor/指南排查,不重新生成旧数据库密码
CI 浏览器依赖失败失败步骤是否安装浏览器/系统包而非断言区分 runner 网络故障与代码缺陷,保留同 SHA 重跑证据

先使用 ./scripts/dev status、./scripts/dev logs consumer 等有限日志入口。错误码、请求 ID、时间、env、项目和提交可以连接各层元数据;需要检查诊断原文时走受限详情通道。普通运行日志、审计与 DLQ 不应另存一份完整 payload,否则会绕开诊断授权、保留与删除语义。

常见修改落点与最小回归

修改源码与契约起点优先回归
新增/调整事件字段event-contract 的规范字典/生成脚本 → Schema 和类型 → SDK → ingestion/consumer → 存储迁移contract:check、canonical:check、契约 fixture、SDK、真实链路
改分析对象或工作流配置server-core 分析对象服务 → API controller → 管理端R1-A、R4-B;已有实例和版本语义
改公式或分数公式 AST/DAG、评分 evaluator、metric/score 管理与绑定R1-B/C golden、真实数据库与页面
改页面质量/运营对应 facts/read model → controller → viewR5-A/B 或 R6;分母、匿名、env、时区、缺失状态
改设置/权限/外部接口settings/auth/store → controller → viewR7、admin/viewer、项目隔离、撤销与审计
改导航/刷新/URLrouter/project context/view 请求状态R8;深链接、返回、并发旧响应、刷新范围
改数据库结构新增 migration,保留已应用文件空库 + 保留数据升级;相关查询与备份恢复

命令是否存在以 根 scripts 为准;集成入口看 scripts/dev。不要从旧 M0–M5 文档复制已删除的路由或 seed:r0 命令。局部修改先跑能暴露该风险的测试,再按仓库发布门进行完整回归;纯文档改动不需要重跑故障注入和生产部署。

诊断修改还要验证哪些失败分支

诊断链路增加了基础事实之外的受限详情、私有 SourceMap 文件和删除墓碑。改动相关功能时,除了正常读取,还需要选取能暴露风险的反例:

改动需要观察的失败分支
SDK 重试短时断网后按原 eventId 回队;永久 4xx 不回队;关页后不承诺恢复
对象快照getter/toJSON 不执行;日志后的对象修改不回写历史错误
请求关联并发同名操作不串实例;晚回调不污染换页后的缓冲
详情读取撤权、过期、切项目时拒绝旧响应;复制重新授权
SourceMap错构建、缺文件、无 sourcesContent 分别降级,原始编译栈仍在
删除与恢复墓碑阻止晚到写入复活;恢复点后的撤权和删除需要再次执行

恢复验证应在可丢弃的隔离环境进行。备份不仅需要 MySQL 的 receipt、策略、授权和墓碑,还需要匹配的 ClickHouse 详情与私有 map;恢复旧备份可能带回旧权限或已删内容,不能仅因进程启动就恢复对外访问。具体步骤应使用该版本的生产与诊断指南。

修复完成后,把触发输入、原本应保持的规则、实际修改的位置和验证范围放在一起记录。后来再遇到相似问题,就能判断是否属于同一原因。模拟 API 可以证明 URL 和页面请求行为,真实链路可以证明同一事件经过了存储,隔离恢复可以证明指定恢复点的一致性;这些证据各自回答不同问题。本文提供复现与维护方法,没有重新执行 Frontend Insight 的应用验收。