菜单

我们的第一张图表上线前错在三处

2026-10-17MCP Checkup

本文译自 2026-09-24 的英文原文。

目录统计页每天计算一次,用几个面板展示我们跟踪的 server 的情况。其中一个面板有一张图,只有一条线:近 90 天每日签名结果数。它是一个宽 320、高 32 个单位的 SVG。合并之前,它经历了三轮自动审查,跨了两个 pull request,每一轮都找出它在另一个方面没有说真话。

1. 「近 90 天」其实是「最近 90 行」

图表背后的查询把每日表按日期倒序排列,取 90 行。写它的那天,这张表才建了一周,所以 90 行和 90 天是一回事。每日计算第一次没跑的时候,它们就不再是一回事了。漏一天,窗口就悄悄变成 91 天;漏十天,就是 100 天。

x 轴让情况更糟。各点按它在结果里的序号等间距排放,所以缺失的一天不会显示成缺口。折线会从前一天直接走到后一天,好像什么都没漏。

审查意见说这个标签是一句假陈述,确实如此。修法有三部分。查询现在有了下界 day >= today − 89 days,时钟由外部注入,测试可以把它钉住。行返回之后,再按同一个截止日过滤一遍,作为第二层,因为测试里模拟的客户端并不执行查询,而以后对查询的改动可能会在没有任何测试察觉的情况下放宽窗口。每个点的 x 位置现在是它距窗口第一天的天数偏移,所以缺失的一天会显示为看得见的缺口。缺口从不用零填补:零等于声称那天什么都没签名,而真实情况是那天我们没有计算。

limit(90) 还在。有了下界之后,它是一个上限,不再是定义。

2. 没有时区的时间戳

另一个面板列出最近的工具集变化,并给出观测到它们的那一分钟。格式化这些时间的辅助函数返回 YYYY-MM-DD HH:MM,而且按它自己写明的约定,不附加 UTC:每个已有的调用方都在自己的文案里加上这个后缀。新面板的那次调用没有加,它渲染出了一个光秃秃的 2026-09-22 08:18。UTC+8 的读者会把它读成自己当地的早上,差了八个小时。

改动只在一个调用点:像其余三个调用方一样,在这里补上 UTC。改辅助函数反而是错的:那样三个已有的调用方就会打印出 UTC UTC。一条新测试断言这个面板里每个渲染出来的时间都带后缀;为了演示它确实会失败,我们导出修复前的那个 commit,对它跑了同一条测试。这里没有任何含糊之处,修复只花了几分钟。要点在于:之前把后缀交给调用方,这个决定本身是对的,但它造出了一份新调用方可以悄无声息地违反的约定,而约定只有在有测试守着时才真正存在。

3. 折线没有对准它的标签

这个 SVG 的 viewBox 是 320 × 32,宽度 100%。它下方的日期标签铺满容器的整个宽度。在窄屏上两者一致。在大约 1,120 像素宽的桌面容器里,浏览器默认保持 SVG 的宽高比,把折线画成 320 像素宽并居中,而标签横跨整个宽度。第一个日期标签的正上方是一片空白,折线的第一个点在它右边几百像素处。

修法是两个属性:SVG 上的 preserveAspectRatio="none",让图形拉伸到容器宽度;path 上的 vector-effect="non-scaling-stroke",让描边不随之被拉伸成一条横向抹开的线。因为这个诊断是从属性推理出来的,而不是亲眼看到的,审查者的条件是推送前先测量:在 1,120 像素的容器里渲染,确认 SVG 的渲染宽度等于容器宽度。结果确实相等。

这说明了什么

三处问题都不会破坏任何数据。每日表、签名结果和观测到的时间都是对的;错的只是它们的呈现,三种互不相关的错法,出现在一个几百行、已经通过自身测试的组件里。图表是对数据的一种声称,而声称要像网站上任何其他句子一样接受审查。三个修复各带一条测试,在原来的代码上会失败;pull request 的行数因此超过了我们平常的上限,而当多出来的行是在修正一句假陈述时,这是允许的。

事实从哪里来

三项发现及其修复,都在统计页的 pull request 历史和审查意见里。渲染宽度的测量是在本地浏览器测试环境里以 1,120 像素进行的,并在第三次推送前附进了完成报告。图表的数据,也就是每日表,在统计页上有说明,并注明了所展示那一行的计算时间。