ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PostHog 稳定化 Flaky 测试完整指南:复现、根因定位、修复与 N 轮验证方法论

PostHog 稳定化 Flaky 测试完整指南:复现、根因定位、修复与 N 轮验证方法论 PostHog 稳定化 Flaky 测试完整指南复现、根因定位、修复与 N 轮验证方法论【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog本文基于 .agents/skills/fixing-flaky-tests/SKILL.md 编写该文档是 PostHog 单仓库monorepo中用于指导 Agent 与开发者处理 CI 间歇性失败flaky test的官方技能规范。文中所有命令、判定标准与仓库路径均以当前仓库为准。本篇导读PostHog 前端使用 Jestkea 状态层 MSW 接口模拟、后端使用 pytestDjango ClickHouse、端到端使用 Playwright是一个典型的大型 JS/Python 单仓库。测试一旦在 CI 上间歇性失败rerun 即绿、本地通过就很容易陷入重跑、放宽超时、加 sleep的恶性循环。本文以 PostHog 官方技能为骨架讲述一套完整方法论先量化失败率再最小化本地复现定位根因而不是掩盖症状用按失败率规模化的 N 轮验证证明修复最后明确修复 / 降级re-level/ 删除三种合法结局并输出结构化报告。读完你既能复现并根治 PostHog 的 flaky Jest/pytest 测试也能把这套纪律迁移到任何大型 CI 项目。纪律红线动手前的三条铁律与正确分工在提出任何改变 CI 运行方式的改动之前先查阅 docs/internal/ci-things-already-tried.md。该文件用可测量的 verdictrejected/reverted/superseded/abandoned/open记录了 PostHog 在测试并行pytest-xdist、Playwright shard、testmon 覆盖率选择、Snob 导入图选择等、CI 编排与 runner 选型上已经试过且被否决的路线。一个被判过rejected的方案不应被无脑重造只有当否决理由如 runner 规格、价格、工具能力已经变化时才允许在 PR 里写明原因后重新尝试。处理 flaky test 之前先用.agents/skills/debugging-ci-failures/SKILL.md中定义的流程做 CI 红跑的分诊与分类代码回归、lint、typecheck、快照差异、migration、codegen drift、infra/runner 等只有在失败被确认为 flaky test 之后才由本文档接管后续的复现、根因与修复。写新 Playwright 测试用.agents/skills/playwright-test/SKILL.mdproducts/engineering_analytics/skills/下的investigating-ci-failures绿/红边界分析与diagnosing-ci-and-merge-bottlenecks工程分析工具缺陷说明属于产品技能不可在 Agent 场景直接调用但其SKILL.md可供阅读参考。三条不可妥协的纪律按顺序执行先复现再修复。针对一个你从未观察到的失败去写修复本质是猜测。只有当下面第 3 节的升级阶梯escalation ladder全部走完仍无法复现时才允许退而求其次采用基于分析的修复。修复根因绝不掩盖。sleep、调大超时、加重试、放宽断言都是在隐藏flaky而不是修复它。用 N 轮循环验证。一次绿跑对间歇性失败不构成任何证据N 的取值必须根据观察到的失败率来规模化见第 7 节。需要特别强调的是稳定化不是唯一合法结局。一旦知道它为什么 flaky第 5 节会反问这个测试是否配存在一个抓不住任何真实回归的测试值得删除因为运行层级level过高而 flaky 的测试值得向下挪一级。此外在任何动作之前先测量而不是臆断。是否 flaky、失败率是多少必须从可验证的 GitHub 运行数据第 1 节中确立——绝不能继承自 Slack 告警、同事猜测或ci:insights的一行标签。1. 从 GitHub 度量失败率运行数据是唯一事实来源GitHub Actions API或 GitHub MCP是事实来源。hogli ci:insights只是一份摘要digest不是 oracle——它可能误标 flaky 与确定性失败之间的边界它滞后于 GitHub API要等 webhook 稳定它根本给不出失败率因为它只报告绝对次数CI 只上报失败、不上报通过的普通运行因此没有分母。ci:insights只能用来验证假设或拉取历史背景永远不能作为第一步或分类的权威。用原始运行数据自己建立时间线# 可疑分支上该工作流的通过/失败历史此处以 master 为例 gh run list --repo PostHog/posthog --workflowci-backend.yml --branch master \ --status completed --limit 60 --json conclusion,headSha,createdAt,databaseId运行级结论还不够——一个红跑可能是在另一个job/测试上失败的。每次都要确认失败的是同一个测试去读失败 shard 的日志对绿跑要确认该测试在该 shard 里确实运行并通过了它可能被 shard 到了别处而不是被修复gh api repos/PostHog/posthog/actions/jobs/{job_id}/logs \ | grep -E exact::test::id|short test summary读时间线后再分类相邻 commit 上交错的通过/失败——同一份未改动的测试有时过、有时挂 → 才是真正的flaky继续往下走超长的连续失败串例如 30 次连续失败在统计上与 flake 不相容——在任何单次失败率低于约 95% 的前提下p^30 ≈ 0。这是确定性回归转去debugging-ci-failures技能找引入 commit其第 4 步两者同时出现很常见一个潜伏的 flake 失败率跳到了接近 100%。这时去找转变点最后一次绿 → 第一次红是那个边界而不是摘要的一行结论告诉你是什么导致了突变。若失败已到达 masterproducts/engineering_analytics/skills/investigating-ci-failures/的references/中提供了边界查询。如果一个失败被报告为或你怀疑是确定性consistent不要串行处理——并行地去度量失败率并尝试复现。记录测得的失败率失败次数 / 总运行次数来自运行数据。第 7 节要用它来规模化验证循环的 N。然后确认它还没被人处理过git log --oneline -10 -- test_file_path # 是否近期已被修复 hogli ci:insights search test name or error # 跨运行历史 —— 与运行数据互相印证不要盲信search报告两个维度要读懂各自的真实含义broken tests近 2 天的失败指纹。potentially_resolved状态只表示该 job 最近一次默认分支运行又绿了——这是修复可能已落地的弱证据不是证明。在报告已修复之前要用运行数据对照确认它确实覆盖了这次失败。test health按爆炸半径排序与engineering-analytics-flaky-testsMCP 工具读取的是同一份数据。其中confirmed_flake是唯一有证明背书的分类同一个 commit 在同一 matrix job 里既失败又通过通过一次重跑尝试转绿或 job 内重试。在另一个 matrix 分支上通过不算恢复。suspected_regression表示没有记录到恢复——这是没有证据不是回归的证据在你的运行数据给出相反结论前先把它当作真实回归对待。用 Trunk Flaky Tests 佐证CI 把测试结果上传到 Trunk Flaky TestsPostHog 的实例覆盖master与 PR 运行的逐测试失败历史。PR 上trunk-io机器人的 Trunk Test Analytics 评论会链接该 PR 专属的切片。仓库根目录 .mcp.json 中配置的trunkMCP server 可以查询它Trunk 将这些工具标记为 experimentalsearch-testrepoName: PostHog/posthogtestNameSearch: 测试名不带文件路径→ 拿到测试用例 IDfix-flaky-testrepoNametestCaseId→ 失败历史、首次出现的 commit、git blame 以及 Trunk 的根因调查createNewInvestigation: true触发一次全新分析最多需要一分钟。认证方式通过/mcp→trunk做一次浏览器 OAuth无头headless环境则在.mcp.json的 server 条目里加Authorization: Bearer头携带TRUNK_API_TOKEN组织 token。与ci:insights一样这只是佐证与历史不是分类权威——是否 flaky、失败率多少仍然来自上面的运行数据。2. 从 CI 抽取失败现场gh run view run-id --log-failed # 如果 job 被重跑过且最新一次通过了flaky 失败其实躺在更早的 attempt 里 gh api repos/PostHog/posthog/actions/runs/{run_id}/attempts/1/jobs gh api repos/PostHog/posthog/actions/jobs/{job_id}/logs继续之前务必采集齐精确的测试 ID文件路径 测试名与失败的断言或错误周围环境的告警——[MSW] Unhandled、async leak 告警、teardown 错误——它们往往才是真正的原因只是打印在症状之前同一个 worker/shard 里在此之前运行了哪些测试顺序怀疑对象ordering suspects。3. 本地复现触碰任何代码之前沿着下面的条件逐级升级直到失败出现。在第一个能复现的层级停下——那一层就是第 7 节的验证环境。单次运行hogli test path::test——确认测试至少能跑起来重复循环默认 N20捕捉概率型 flakeCI 化条件CI 用低 worker 数、在资源争用的 runner 上以分片方式跑 Jest——动手前先读 frontend/package.json 中test脚本和 .github/workflows/ci-frontend.yml 了解当前 CI 标志。当前写法示例与该 flaky 测试所在的 shard 邻居一起跑pnpm --filterposthog/frontend jest test_file neighbor_file --maxWorkers2 --forceExitpytest 则整文件或整个 class 跑而不是单个测试让模块级 fixture 与执行顺序和 CI 一致调整顺序把怀疑的污染测试polluter排在被影响者victim之前再把文件内顺序反过来跑。孤立时消失的 flake 多半是顺序 bug资源争用重跑循环的同时在另一个 shell 里跑 CPU 密集型任务比如并行跑一个全文件的 jest。超时类 flake 往往只在这种情况下现身。循环外壳——用退出码判定不要用 grep 输出N20; PASS0; FAIL0 for i in $(seq 1 $N); do if test command /tmp/flake-run.log 21; then PASS$((PASS1)) else FAIL$((FAIL1)); cp /tmp/flake-run.log /tmp/flake-fail-$i.log; echo run $i: FAIL fi done echo $PASS passed, $FAIL failed out of $N两条成本提示复现阶段在第一次失败后break——捕获一份失败日志就够只有度量失败率或第 7 节验证时才需要跑满 N 次pnpm --filterposthog/frontend jest脚本在每次调用前都会跑pnpm build:products。循环内部先构建一次然后用pnpm --filterposthog/frontend exec jest ...迭代跳过重复构建。如果爬完整个阶梯仍无法复现说明 flake 是 CI 环境特异的。这时可以基于 CI 证据与根因分析来修复但必须在报告中明确说明——此时第 7 节的验证是分析性的而非经验性的。4. 定位根因对症状分类绝不修补症状先把症状对号入座症状大致原因类别等待 promise/listener/element 超时未被 await 的异步工作、缺失的 mock、隐藏的 pending 请求单独跑通过与邻居一起跑失败反之亦然共享状态模块缓存、DB 行、全局配置、顺序在午夜/UTC 边界附近失败或在慢 runner 上失败使用了真实时钟——缺少freeze_time/ fake timers对列表顺序或生成的 ID 做断言把非确定性顺序/ID 断言成确定性查询看不到刚写入的数据最终一致性ClickHouse缺少显式 flush/commit只在--maxWorkers2/ 争用条件下失败调度暴露的竞态条件、超时设置过紧原因不显然时二分定位bisect如果症状表对不上明确的根因且测试文件本身没改过git log -- test_file是旧的那触发器就在别处——邻居测试、依赖升级、或产品代码变更。与其猜不如找它从什么时候开始先 bisect CI 运行历史便宜、无需本地构建取第 1 节时间线上最后一次绿 → 第一次红的边界diff 该窗口内的 commitgit log good..bad。这份短清单常常直接点名凶手能本地复现且失败近乎确定时git bisect代码git bisect start bad-sha good-sha git bisect run bash -c repro command # exit 0 good非零 bad注意对间歇性 flake某一步的侥幸通过会把git bisect带上错路。只有当失败是确定性的才信任代码 bisect否则每步把复现命令跑 N 次任何一次失败即判 bad或干脆用 CI 运行历史的边界。PostHog 特有模式前端Jest kea缺失 MSW mock带afterMountloaders 的 kea logic 会经由connect()链发出 API 调用——三层深的 logic 可能触发一次未 mock 的 fetch。当前未处理请求会以良性的空分页 200来解析所以症状是loader 以空数据或错误数据成功返回而不是网络错误。日志里的[MSW] Unhandled GET ...告警会点名缺失的 mock——补上useMocks条目即可。注意这个未处理请求行为历史上变过它以前是挂起若症状对不上去读 frontend/src/mocks/jest.ts 了解未 mock 请求今天到底做什么该文件注释详细记录了 Jest-only 默认 handler、不可 passthrough 的originalResponse.clone崩溃、以及用空分页 200 兜底而不是挂起/拒绝的理由toFinishAllListeners()超时它要等所有已挂载 logic 的所有kea listener promise默认 3sLISTENER_FINISH_WAIT_TIMEOUT定义于kea-test-utils。任何一个挂着 pending loader 的关联 logic 都会阻塞它。修复 pending 的工作不要调大超时Mock URL 不匹配mocksToHandlers会去掉末尾斜杠但 query 参数、current风格的片段和:param模式必须与真实请求 URL 一致。对照[MSW] Unhandled那行检查逐测试 handler 重置frontend/src/mocks/jest.ts 注册了全局afterEach(() mswServer.resetHandlers())。由于it.each的每个 case 都是独立测试运行时 mock 必须在beforeEach中重新注册泄漏的挂载在beforeEach挂载、却从未卸载的 logic会把异步工作泄漏到后面的测试里。PostHog 特有模式后端pytestDB 状态泄漏测试间共享行数据而缺乏隔离——检查 fixture 的 scope判断该测试是否需要pytest.mark.django_db(transactionTrue)真实时间使用freeze_time绝不对now()派生值做断言ClickHouse 最终一致性查询可能看不到刚插入的数据——在测试 setup 里显式 flush而不是 sleep。5. 决定结局——修复只是三种结局之一一旦知道为什么flake在投入精力稳定化之前先问一句这个测试到底该不该存在flaky test 是唯一一种成本已被证明而价值尚未被证明的情形它已经实打实地消耗了重跑、墙钟时间与注意力。因此要用writing-tests见 .agents/skills/writing-tests/SKILL.md的价值闸门做追溯式审查而且比对待新测试更严格这个测试到底抓得住哪个现有测试都抓不住的、真实的回归三种结局都合法要刻意选择不要默认第一个。结局适用场景下一步修复它Fix it以大致合理的成本守护了一个真实回归第 6 节降级它Re-level it值得守护但 flake 根植于它当前运行所在的层级见下文删除它Delete it你说不出它抓的回归或已有其他测试在抓见下文删除它经常过不了闸门的几种典型形态同义反复的冒烟测试断言一个文件里许多兄弟测试要跑起来本就依赖的前置条件比如图表渲染了、列表非空。后面每个测试都是它的更强版本重复覆盖对某条已被现有测试走过的路径再加一个更薄的第二测试。应该折进参数化 case或直接去掉第三方断言因为它调用的是供应商的最终一致性、调度器或 API 而 flaky——那不是我们的逻辑。那是供应商该写的测试而且基于 mock 的兄弟测试通常已经覆盖了我们这边被永久门控除 CI 外处处 skip缺凭据、opt-in 标记于是没人针对它开发只有 CI 在为它买单。删除是不可逆的所以它比修复有着更高的门槛必须获得用户的显式批准与 quarantine 相同的门槛。提出删除方案后交还控制权不要自己动手交代覆盖缺口说出file:line还有谁在抓这个行为或者直说失去了什么覆盖、为什么可接受。大概别处有覆盖不是答案——去读兄弟测试绝不为了在时间压力下把红构建变绿而删除。那是带更多步骤的 quarantine也正是真实回归得以发布的方式。删除后直接跳到第 8 节没有东西需要跑循环了。降级它把测试沿writing-tests的成本阶梯向下挪一层而不是在原位加固。为了证明一次直接调用就能证明的逻辑却来回折腾真实的 broker、浏览器或供应商 API——这是在测传输层而传输层正是非确定性所在。降级是从构造上消除 flake因此优先于越来越繁琐的等待。替代品是一枚新测试对它跑writing-tests的闸门并在它的新层级上验证而不是对着旧复现条件——那些条件已不存在。成本阶梯cost ladder由 .agents/skills/writing-tests/SKILL.md 定义纯函数/单元测试 → kea logic 测试 → Django TestCase → 基于 ClickHouse 的测试 → Playwright e2e每上一级约慢一个数量级且更易 flaky。6. 修复根因——绝不掩盖诱人的掩盖动作应该改为断言前加sleep(2)/setTimeout等待具体条件waitFor、expectLogic、显式 flush调大测试/listener 超时找出什么在挂起超时只是信使加重试pytest-rerunfailures--reruns、jest.retryTimes只留给真正非确定性的外部基础设施且要带注释和关联 issue——绝不用于被测的产品代码Skip / quarantine 该测试仅在用户显式批准、并带关联 issue 时放宽断言让数据确定性化排序、冻结时间、固定 seed保持断言严格加固一个本不该存在的测试回到第 5 节——删除或降级它才是更便宜的修复修复要尽量小且尽可能留在测试或它的 fixture 内部。如果竞态在产品代码里说明这个 flake 揪出了一个真实 bug——修复产品代码并在报告里写明。7. 用 N 轮循环验证在与复现失败相同的条件同样的邻居测试、worker 数、争用程度下对修复后的代码跑第 3 节的外壳。N 的取值由修复前观察到的失败率决定若大约每 k 次失败一次则约需要 N ≥ 3k 次连续通过才能获得约 95% 的置信度认为 flake 已消失——(1 - 1/k)^(3k) ≈ 5%。所以用N max(3k, 20)。如果没有可用的失败率估计跑 50 次并在报告中注明置信度降低。如果 flake 始终无法在本地复现跑 N20 作为回归检查并把验证标注为分析性analytical。循环中任何一次失败→ 回到第 4 节根因错了或没找全。最后把所在文件/套件正常跑一遍确认修复没有破坏兄弟测试。选择了降级旧的复现条件已不再适用。在新层级上循环替代测试并确认 flake 消失是因为旧测试没了、而不是因为它变快了。选择了删除没有东西需要循环。把所在文件/套件跑一遍确认没有东西依赖它并把覆盖论证带进报告。8. 报告Test: file path::test name Observed in CI: measured rate from run data, e.g. 8/45 runs over 3h (gh run list); ci:insights state corroborates Local repro: command conditions, e.g. 3/20 failures with neighbor X, maxWorkers2 | not reproducible locally Root cause: one or two sentences Outcome: fixed | re-leveled (from → to) | deleted Change: what changed and why it removes the cause; for a deletion, what still covers the behavior (file:line) and what coverage is genuinely lost Validation: N/N passes under repro conditions | N/N at the new level | analytical only (CI-specific) | n/a, deleted Follow-ups: product bug found, related tests with the same pattern, or none报告里的每个字段都要求可验证的、具体的证据Observed in CI必须写测得的分子/分母如8/45 runs over 3hLocal repro必须写命令与条件Validation必须写循环次数与通过率。这正是整套方法论测量优先的收口。边界Boundaries不要重跑 CI job、不要 push、不要为了验证修复而往 GitHub 发帖——在本地验证不要为了修 flake 而编辑.github/workflows/CI 基础设施改动需要人工评审见debugging-ci-failures技能的安全规则不要为了把 flake 变绿而接受或更新快照如果同一根因模式显然也影响兄弟测试仅当修法是机械性时才在同一次改动里一起修否则列为 follow-up未经用户显式批准不得删除测试且必须附上a对覆盖的具名替代或b对失去覆盖的直白说明。删除是合法结局但绝不是通往绿色的捷径。技能规范之外的仓库实现佐证为了让上述方法论落在实处可以在当前仓库中核对下列实现细节frontend/src/mocks/jest.ts 完整呈现了 Jest 环境的 MSW 装配setupServer(...handlers, ...jestOnlyDefaultHandlers)、useMocks的server.use(...)语义、全局afterEach(() mswServer.resetHandlers())以及 jsdom 下未处理请求兜底为可克隆的空分页 200 的理由——是缺失 MSW mock / 逐测试 handler 重置 / 顺序泄漏三类 frontend flake 的第一手证据frontend/package.json 的test脚本含pnpm build:products jest --testPathPattern... --forceExit --shard$SHARD_IDX/$SHARD_TOTAL与jest脚本是第 3 节先读 CI 跑法再复现的直接依据.github/workflows/ci-frontend.yml 定义了前端测试的分片与 runner 规格复现 CI 化条件时以此为准docs/internal/ci-things-already-tried.md 记录了包括pytest-xdistshard 内并行、Playwright E2E 分片、Bazel/testmon/Snob 测试选择在内的各项 CI 方案实测结论——任何改 CI 让套件别这么 flaky的念头动手前都应在此检索相关技能与仓库根配置.agents/skills/debugging-ci-failures/SKILL.md红跑分诊与 flaky 判定、.agents/skills/writing-tests/SKILL.md成本阶梯与价值闸门、.mcp.jsontrunkMCP server 的查询工具入口。以上证据均出自当前仓库足以支撑你在本地以相同条件复现 flake、并以仓库内一致的配置执行验证循环。整套方法的终点不是测试变绿了而是对根因的确认性修复 覆盖决策的自觉选择 可复核的报告。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表