返回知识中心
指纹

Chrome for Testing:固定构建版本,而非指纹

可用性端点按同一版本发布四个通道和每个平台三个产物。固定该版本仍把字体、GPU 与时钟留在版本之外。

BotBrowser Team

文档中心

想直接进入 指纹 文档吗?

这篇文章属于知识中心的指南库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

固定构建版本是成本最低的可复现性收益,而且它比看起来要小。BotBrowser 提供已声明的配置档案,使页面可读取的各项表面在多次启动之间保持一致,它不负责决定安装哪个二进制文件。直接读取 Chrome for Testing 端点可以看到:四个通道及各自版本,六个平台各发布同一版本的浏览器、driver 与 headless shell,以及版本固定之后仍在变化的一串值。

TL;DR

  • 可用性端点发布四个通道,Stable、Beta、Dev 与 Canary,每个通道都有版本和修订号。
  • 读取当天,Stable 为 157.0.8103.50,修订号 1717349;Canary 为 159.0.8154.2,修订号 1739869。
  • 每个平台在同一版本发布三个产物:浏览器、chromedriver 和 chrome-headless-shell。
  • 覆盖六个平台,linux-arm64、linux64、mac-arm64、mac-x64、win32 与 win64,每个平台三个产物都存在。
  • 每个已发布的 URL 都带着同一个版本字符串,这正是固定版本有意义的原因。
  • 读取时,已知良好列表包含 2626 个版本,覆盖主版本 113 到 159。
  • 固定构建版本同时锁定渲染器与 driver,它不会锁定字体、GPU 驱动、系统库或时钟。
  • 配置档案描述环境对外报告什么,固定构建版本描述实际安装了什么。两者都需要,且互不替代。

目录

三个面板。第一个列出四个通道及其版本与修订号。第二个显示六个平台,每个平台携带同一版本字符串的浏览器、driver 与 headless shell。第三个区分固定构建版本能修的部分与仍在变化的部分:字体、GPU 驱动、系统库与时钟。

端点发布什么

第一个有用的事实是:可用性以数据形式发布,而非以下载页面形式发布,这才让自动化固定成为可能。两个端点覆盖了 pipeline 需要的大部分内容:一个报告各通道最后一个已知良好的构建,另一个报告所有已知良好的版本。

last_known_good_channels: Stable, Beta, Dev, Canary
  Stable: version=157.0.8103.50 revision=1717349
  Beta:   version=158.0.8127.18 revision=1727472
  Dev:    version=159.0.8144.0 revision=1735750
  Canary: version=159.0.8154.2 revision=1739869

两个字段重要,而且回答不同的问题。版本是读者认得的字符串,修订号是 Chromium 仓库中底层的提交位置。按版本写的固定可读;按修订号写的固定没有歧义,即使某个通道重新发布构建。同时记录两者是成本最低的做法,只记录版本是常见错误。

通道结构对既要稳定又不想过时的 pipeline 同样重要。Stable 变化慢,是发布门禁的合适默认值。Beta、Dev 与 Canary 变化更快,适合在改动进入 Stable 之前发现它,这与可复现性要求正好相反。为了确定性而跟踪 Canary 的 pipeline,为需要最慢通道的工作选择了最快的通道。

known_good_count: 2626
oldest: 113.0.5672.0   newest: 159.0.8154.2
majors_present (last six): 154, 155, 156, 157, 158, 159

列表的规模本身就是以程序方式读取它的实际理由。两千六百个版本不是人能扫视的数字,而手工编辑的固定会在有人从博客文章而不是从端点复制版本的那一刻发生漂移。

三个产物,一个版本

第二个事实会击穿大多数只固定浏览器的 pipeline。每个版本发布三个独立产物,可用的自动化环境需要三者一致。

chrome_platforms: linux-arm64, linux64, mac-arm64, mac-x64, win32, win64
mac-arm64: chrome=true driver=true shell=true
linux64:   chrome=true driver=true shell=true
win64:     chrome=true driver=true shell=true
distinct_versions_in_urls: 157.0.8103.50
all_artefacts_agree: true

最后一行是关键。该版本的每个已发布 URL 都带同一个版本字符串,所以固定一个值就固定了整个集合。三者不一致时,失败并不随机:比浏览器更新的 driver 会拒绝附加,而不同构建的 headless shell 渲染结果与它本应代表的浏览器不同。

headless shell 是团队最容易忘记的产物,也是悄悄改变结果的那个。针对 chrome-headless-shell 运行的测试并没有针对浏览器二进制运行,所以固定浏览器而让 shell 浮动,会产生渲染器变化但版本号不变的 pipeline。检查三者报告同一版本只是一行断言,它能抓住只固定浏览器抓不到的情况。

pin = "157.0.8103.50"
assert browser_version == pin
assert driver_version  == pin
assert shell_version   == pin

同样的结构解释了一种看起来像不稳定的失败。容器镜像从一处来源安装浏览器、从包管理器安装 driver,可能得到同一主版本的两个版本,多数情况下能用,只在发生变化的那项能力上间歇失败。上面的断言把这种间歇失败变成启动错误,这个结果值得付出成本。

固定版本能修什么、不能修什么

第三个事实决定固定是否足够,诚实的答案是:它修好的比读者期望的少。固定的构建同时锁定渲染器与 driver,因为它命名的就是这些产物。

pinned by the build:  renderer version, driver version, headless shell version
not pinned:           fonts, GPU driver, system libraries, time zone, clock

字体是最常见的残留。固定了浏览器却没有 CJK 字体的容器会把中文页面渲染成方块,像素结果与同样浏览器版本的开发机不同。产物集合中没有任何东西能修好这一点,因为字体来自操作系统镜像,而不是浏览器包。

GPU 栈是第二项。固定浏览器在软件渲染机器上产生的 canvas 与 WebGL 输出,与同一浏览器在真实适配器机器上的输出不同,而两者版本相同。这种差异可测量,它不是固定的缺陷;它是固定从未覆盖的变量。

时钟与时区是第三项,也最容易被忽略,因为它们根本不属于浏览器。读取格式化日期或依赖语言环境字符串的测试,可能在同一构建上因主机配置而通过或失败,而只记录浏览器版本的报告无法解释这种差异。

实际的解读是:固定是必要的第一步,也是糟糕的终点,这与浏览器自动化的确定性模式从排程侧得出的结论一致。它移除了最大的漂移来源,即在固定测试下变化的渲染器,留下三个较小的来源,记录必须分别命名。只记录构建、别的不记的 pipeline 能回答「浏览器变了吗」,不能回答「环境变了吗」。

第四个观察应当放在这里,因为它把固定变成运维例行工作。各通道按自己的节奏移动,从不复查的固定就变成了停止接收修复的决定。保持固定诚实的例行工作有三步,没有一步是自动的:读取端点,把已发布的 Stable 版本与固定值比较,判断差异是必须接受的安全修复,还是可以等到下一个计划窗口的功能变更。

pinned:   157.0.8103.50
published:157.0.8103.50
delta:    none
action:   none

这个比较成本很低,却正是团队跳过的步骤。固定之后从不比较的 pipeline,最终会运行带已知缺陷的构建,而面板仍显示健康的版本号,因为版本被固定了,看起来像是有意为之。把已发布的值记在固定值旁边,漂移就变得可见,无需任何人记住固定是什么时候写的。

保留比较还有第二个理由。主版本跃迁改变渲染器的方式与补丁跃迁不同,跨越主版本的固定与接受补丁的固定是不同的决定。把两者都当作「一次更新」,就把低风险变更和高风险变更合并成一个动作,随后的评审要么对补丁太重,要么对主版本太轻。

版本字符串本身就携带这些信息,读者不必查更新日志就能使用。四段分别对应 Chromium 在 chrome/VERSION 里记录的字段:MAJOR.MINOR.BUILD.PATCH,所以 157.0.8103.50 读作主版本 157、次版本 0、build 8103、补丁 50。第一段变化值得完整重设基线;最后一段变化通常影响最小。把字符串当作可读结构而不是不透明令牌,才能在任何下载之前作出这个判断。

这些都不需要新工具。端点是 JSON,比较是字符串相等,断言是启动时的三项检查。需要的是决定把构建与配置档案一起记录,因为只命名其中一个的报告无法回答读者真正会问的问题。

实践结论

读取可用性端点而不是博客文章,把版本与修订号一起记录,并在启动时断言浏览器、chromedriver 与 chrome-headless-shell 都报告固定值。发布门禁跟踪 Stable,把更快的通道留给变更探测,因为确定性要求与新鲜度要求方向相反。然后记录固定不覆盖的内容:字体集合、GPU 栈、系统库与时钟。关于这套做法实现的发布契约,见浏览器自动化版本固定与更新;关于同一构建的两台机器为何仍会不一致,见 Selenium Grid 节点指纹差异。

已声明的配置档案与固定的构建回答两个不同问题。配置档案说明环境向页面报告什么;构建说明实际安装了什么。只有其中一个的环境可能在一个维度上一致,在另一个维度上漂移,而同时命名两者的报告才是日后可以核查的那一种。

最后一组观察关于三个产物各自的用途,因为把它们当作可互换的读者会固定错对象。浏览器二进制是渲染器及其派生的一切:布局、随附字体、JavaScript 引擎,以及页面可读取的表面。driver 是协议桥,其版本必须匹配,因为它所说的协议由浏览器自身的协议定义生成。

browser   -> renderer, layout, JS engine, page-readable surfaces
driver    -> protocol bridge; must match the browser it drives
shell     -> headless renderer used instead of the browser binary

中间那行解释了为什么 driver 不匹配会大声失败而不是微妙失败。针对另一代协议编译的 driver 会拒绝会话,或在第一条形态变化的命令上失败,错误命名的是命令而不是版本。看到协议错误和看起来匹配的版本字符串,通常说明这个 driver 来自包管理器而不是产物集合。

第三行产生的是安静差异。headless shell 是独立的二进制,所以针对它运行的测试根本没有针对浏览器运行,即使版本字符串一致。两者渲染同一页面时的细微差别,恰好落在视觉比较会注意的地方:亚像素文本位置、字体回退顺序,以及懒加载图片的解码方式。固定浏览器却让 shell 浮动的 pipeline,固定了它不用的产物,却让真正使用的那个没有固定。

这对指纹报告还有一层后果。从页面读到的值依赖渲染器,所以只命名浏览器版本而不命名 shell 版本的报告,只要运行用了 shell,就命名了错误的产物。实践中这不是细微区别:它决定了报告能否复现。

由此得到的实践规则很短。三者一起固定,启动时断言三者,并记录实际运行的是哪一个。做到这三点的 pipeline 能回答后来的读者会问的问题,那个问题不是「固定了哪个版本」,而是「哪个二进制产生了这个结果」。

关于两个端点本身还有一点,因为名字容易混淆,而这个差别决定固定怎么写。一个端点回答「这个通道当前的构建是什么」,通道每次重新发布它都会变。另一个回答「存在哪些版本」,它只会增长。按第一个端点写的固定是移动值的快照;按第二个写的固定是不可变条目的引用,这才是可复现构建真正需要的。

last-known-good-versions.json -> current per channel; changes on republish
known-good-versions.json      -> every published version; only grows
<version>.json                -> the artefact list for one version; immutable

第三行是 pipeline 在安装时应该读取的内容。它以版本字符串为键,所以一个固定值解析为一份文档,列出每个平台的全部产物,解析过程不依赖当前哪个通道健康。安装时读取第一个端点的 pipeline,下周会安装另一个构建,而配置没有任何变化,这与固定的用途正好相反。

这个区别也解释了一类令人困惑的缺陷报告。昨天通过、今天没有改代码却失败的测试套件,通常在某处准备步骤里读取了移动端点,修法不是追查失败,而是让读取变成不可变。这是一行改动,却能把一整类间歇失败从记录中移除。

对于还需要知道何时移动的团队,第一个端点仍然是应该读取的对象,只是在不同的时间。它属于报告差值的计划检查,而不属于必须可复现的安装路径。按用途分开两次读取,pipeline 才能既可复现又保持最新,这看起来矛盾,直到两者被分开。

产物分工还有一层后果,关于记录在被审计之前必须包含什么。构建标识回答哪个渲染器产生了结果。它不回答当时有哪些字体、存在哪个图形适配器、进程读取的是哪个时钟。这三项变量会把相同的构建变成不同的结果,而按设计它们都不在产物集合里。

record: build, fonts present, graphics adapter, time zone
missing any one -> the result is reproducible only by luck

保持这份记录最便宜的方式是在启动时写入,而不是事后重建。一行启动记录写明构建、字体列表长度、适配器字符串和时区如 America/Toronto,只花几百字节,却能回答读者在结果变化后会问的问题。事后重建意味着对可能已被重建的镜像进行猜测,那是同一项工作昂贵得多的版本。

字体这一项值得单独一句,因为它产生的是错误答案而不是失败。缺少 CJK 字体不会抛错;它替换为回退字体,页面照常渲染,提取的文本依然正确,截图却不同。比较截图的 pipeline 看到一个无法解释的差异,比较提取文本的 pipeline 什么都看不到。记录字体数量把两种情况都变成可观察状态。

这就是整个主题的形态。固定移除了最大的变量,留下一小组其他变量,工作的价值在于精确命名剩余部分,而不是假装固定覆盖了一切。记录四个字段的读者能解释变化;只记录一个字段的读者只能报告某处动了。

最后一条运维说明锁定到底为什么有用。固定构建并不是因为变化不好,而是因为未声明的变化会让失败无法归因。更新后失败可以用更新解释,而没有记录任何变化的失败会逼着人从零开始排查。锁定买的就是归因。

unpinned + failure -> investigate the browser, the fonts, the GPU, the code
pinned   + failure -> compare against the last run on the same build

来源

#Chrome For Testing#Chromedriver#版本固定#自动化#可复现性

本文属于指南: 什么是浏览器指纹?原理、风险、信号与隐私边界

在 BotBrowser 里跑一遍同样的检查

加载 profile,让刚读到的这些信号在浏览器内部保持一致,并在正式上线前先在自己的机器上确认结果。