Frontend Insight:从故障现象追到可验证的修改
用质量页刷新、缺失分数和重复事件三个场景说明如何定位边界、选择测试、保留数据升级,以及区分模拟和真实链路证据。
“没有数据”可能指三件不同的事:事件没有发出,事件还没入库,或者查询没有选中它。先记下项目、环境、时间和 eventId,再沿同一条事件检查,才知道应该修哪一层。直接重启即使暂时让页面恢复,也未必能解释故障原因。
本文围绕三个可复现的案例组织排障与回归方法。命令在 Frontend Insight 源码仓库根目录执行,需准备独立测试环境;实现版本与阅读顺序见系列入口。
先分清三种验证
| 验证层 | 运行入口 | 能证明什么 | 不能证明什么 |
|---|---|---|---|
| 静态检查、单元与构建 | pnpm check | 格式、工作区边界、生成物、规范名、lint、类型、单元逻辑、构建 | Kafka/数据库/浏览器真实协作 |
| 真实服务集成 | ./scripts/dev smoke、各 verify-r* | 实际管理元数据、事件写入与读取、迁移、权限 | 所有用户点击与浏览器生命周期 |
| 浏览器产品链路 | pnpm test:r8:e2e 及对应阶段 E2E | Chromium/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 对上这些证据,比在多个空列表之间来回刷新更容易定位。
先跟读以下三个文件:
- PageQualityView.vue:
signature、load、filterNavigation、refresh; - quality-refresh.spec.ts:固定时钟、模拟传输与查询参数断言;
- demo-quality.spec.ts:HTTP 202、特定 occurrence 的轮询、环境隔离与 UI 断言。
在独立栈启动后运行:
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 串起观察:
- 打开 demo 项目的页面分析质量页,选择
dev和最近 7 天,记下 URL 的from/to。 - 在实验室触发“模拟 JS 异常”。只在本地查看事件的
appId/env/timestamp/eventId,不要收集登录凭证。 - 观察 HTTP 202。再等待匹配的 occurrence 出现在查询结果中,避免把别的历史错误误当本次结果。
- 点击质量页的“刷新”,比较新的 URL
to;然后切换staging/prod。 - 换成截止于触发错误之前的自定义时间,再次刷新。
预设范围刷新按项目时区推进到当前时间,清理旧 cursor 和派生评分边界;自定义时间保持不变。demo 实际上报 dev,所以这条错误不应出现在其他环境。F5 或分享链接保留 URL 时间快照,与页面按钮不是同一个语义。asOf 是查询快照时间,不能替代事件时间的 to。范围通常按 from ≤ timestamp < to 过滤,因此新快照仍可能查不到范围外的新事件。
把所有刷新都改成“现在”会破坏自定义历史范围、分页与分享链接的可复现性;只切环境又没有修正旧的时间边界。修复需要区分导航、刷新按钮和自定义查询各自的语义,见质量刷新记录。
案例二:已有访问事实,为什么分数仍为空
从一个项目的指标管理打开运营或质量分数,记录版本、绑定范围、事实状态与贡献。按 领域与评分章节 跟到服务,然后读取以下测试:
- score-evaluation.test.ts 与 quality-score-evaluation.test.ts:独立评分与边界;
- score-management.test.ts:版本和管理语义;
- page-usage-binding.test.ts:页面事实和绑定。
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 → view | R5-A/B 或 R6;分母、匿名、env、时区、缺失状态 |
| 改设置/权限/外部接口 | settings/auth/store → controller → view | R7、admin/viewer、项目隔离、撤销与审计 |
| 改导航/刷新/URL | router/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 的应用验收。