Cypress Cloud 捕获协议(Capture Protocol)本地开发指南:CYPRESS_LOCAL_PROTOCOL_PATH 原理与调试实战
当使用 --record 将测试上传到 Cypress Cloud 时,运行器背后有一段"捕获代码"(capture code)在浏览器中采集测试回放数据。官方开发指南说明了:生产环境中这段代码从 Cloud 下发,而本地开发时开发者需要用环境变量将其替换为本地构建产物。本文基于该指南,结合仓库中 packages/server/lib/cloud/ 下的真实源码,讲清这套协议的加载链、两个关键环境变量的行为差异、本地调试模式下源码故意保留的"不删库、直接抛错"特性,以及生产构建中如何剥离本地调试后门。读完后你能够完整走通"本地 watch 构建捕获代码 → 源码运行 Cypress record → 数据落库并上传"的闭环,并能从源码定位协议各生命周期钩子的执行位置。
一、为什么需要本地开发捕获协议
在 record 模式下,Cypress 并非把所有测试数据都打包上传,而是通过 Cloud 下发的 AppCaptureProtocol 脚本在浏览器侧完成捕获。从源码看,Cloud 在创建 run 的响应中会返回 capture.url(或旧字段 captureProtocolUrl),见 packages/server/lib/cloud/api/index.ts:
const protocolUrl = captureProtocolUrl || process.env.CYPRESS_LOCAL_PROTOCOL_PATH
if (protocolUrl) {
script = await this.getCaptureProtocolScript(protocolUrl)
}
注意这个优先级:云端 URL 优先,本地路径是兜底。getCaptureProtocolScript 的实现则揭示了本地开发的真正入口(packages/server/lib/cloud/api/index.ts):
async getCaptureProtocolScript (url: string, ...) {
// TODO(protocol): Ensure this is removed in production
if (process.env.CYPRESS_LOCAL_PROTOCOL_PATH) {
debugProtocol(`Loading protocol via script at local path %s`, process.env.CYPRESS_LOCAL_PROTOCOL_PATH)
return fs.promises.readFile(process.env.CYPRESS_LOCAL_PROTOCOL_PATH, 'utf8')
}
const res = await retryWithBackoff(...) // 走云端下载 + 签名校验
一旦设置了 CYPRESS_LOCAL_PROTOCOL_PATH,服务端就跳过云端下载、跳过签名校验,直接从本地磁盘读取 JS 文件作为协议脚本。云端路径则会用 enc.verifySignature(res.body, res.headers['x-cypress-signature']) 校验签名,校验失败直接抛 Unable to verify protocol signature(api/index.ts)。这也解释了指南中"生产环境从 Cloud 获取"与本地开发的分叉点。
二、指南中的完整操作步骤(逐条对照)
guides/protocol-development.md 给出的完整流程如下,两个仓库需要同时处于工作状态:
第一步:克隆并构建捕获协议仓库
- 克隆
cypress-services仓库(需要是 Cypress 组织成员,属于内部仓库); - 运行
yarn安装依赖; - 在
app/packages/capture-protocol目录下运行yarn watch,让构建产物dist/index.js随源码改动持续更新——这是本地热调试的基础。
第二步:克隆 Cypress 主仓库并从源码运行
- 克隆
cypress仓库; - 运行
yarn安装依赖; - 在测试项目上执行 record 命令:
CYPRESS_LOCAL_PROTOCOL_PATH=path/to/cypress-services/app/packages/capture-protocol/dist/index.js \
CYPRESS_INTERNAL_ENV=staging \
yarn cypress:run --record --key <record_key> --project <path/to/project>
参数说明:
| 变量/参数 | 作用 | 源码依据 |
|---|---|---|
CYPRESS_LOCAL_PROTOCOL_PATH |
指向本地捕获协议构建产物 dist/index.js 的绝对/相对路径,触发"从本地读脚本"分支 |
api/index.ts#L695 |
CYPRESS_INTERNAL_ENV=staging |
保留变量,把 Cloud 请求指向 staging 环境,避免本地实验污染生产数据 | cli/lib/util.ts#L118 |
--record --key <record_key> |
进入 record 模式并携带 Cloud 项目录制密钥 | 同 api/index.ts 中 createRun 流程 |
--project <path> |
指定测试项目根目录 | — |
关于 CYPRESS_INTERNAL_ENV,源码在 cli/lib/util.ts 中明确了合法取值为 ['development', 'test', 'staging', 'production'](见 packages/server/config/app.json 中的环境名),非法值会以错误码 11 直接退出(cli/lib/errors.ts)。cli/lib/cli.ts 还显示:非 production 的取值会被 CLI 打印一条"W arning: It looks like you're passing CYPRESS_INTERNAL_ENV=..."的保留变量警告后继续运行。因此指南选择 staging 是为了在真实(非生产)Cloud 后端上完整跑通协议加载、上传链路。
三、协议脚本如何被加载执行:requireScript 与 ProtocolManager
读到脚本字符串后,ProtocolManager.prepareProtocol 负责"编译 + 实例化"(packages/server/lib/cloud/protocol.ts):
async prepareProtocol (script: string, options: ProtocolManagerOptions) {
this._captureHash = base64url.fromBase64(
crypto.createHash('SHA256').update(script).digest('base64')
)
...
const { AppCaptureProtocol } = requireScript<{ AppCaptureProtocol: AppCaptureProtocolConstructor }>(script)
this.AppCaptureProtocol = AppCaptureProtocol
}
三个值得注意的实现细节:
- captureHash:对脚本内容做 SHA256 并 base64url 编码,作为该捕获版本的身份标识,后续错误上报(
dispatchErrors)会带上它,方便服务端区分是哪一版捕获代码出了问题; - requireScript:捕获协议是一段字符串而非文件,require_script.ts 用
new Module('id', module)+mod._compile(script, '')把它当作 Node 模块现场编译,取出其exports.AppCaptureProtocol; - 构造参数:
createRun成功后的prepareAndSetupProtocol调用传入runId、projectId、testingType、cloudApi(重试与请求封装)、projectConfig(devServerPublicPathRoute、port、proxyUrl、namespace)、mountVersion与mode: 'record'(api/index.ts#L461-L478)。其中mountVersion来自客户端自报的runnerCapabilities.protocolMountVersion(当前为2,见 api/index.ts#L73),供协议代码判断运行器能力版本。
四、一次 spec 的捕获生命周期与本地调试行为差异
ProtocolManager 把捕获协议的生命周期钩子与测试执行绑定(protocol.ts):
beforeSpec(spec):为每个 spec 实例创建 SQLite 库。落盘位置是os.tmpdir()/cypress/protocol/,文件名为${spec.instanceId}.db,归档文件${spec.instanceId}.tar(_beforeSpec)。注意这里直接用了better-sqlite3的原生绑定路径;connectToBrowser(cdpClient):包一层 CDP 客户端,把on的监听器替换为会捕获异常的 wrapper,再invokeAsync('connectToBrowser', ...)交给协议接管浏览器;beforeTest/afterTest/preAfterTest:按测试粒度同步/异步回调;commandLogAdded/urlChanged/pageLoading/responseStreamReceived等:把运行器事件转发给捕获代码;afterSpec():结束后uploadCaptureArtifact将.tar归档 PUT 到 Cloud 下发的uploadUrl,由 put_protocol_artifact.ts 以application/x-tar流式上传,最多重试 3 次、500ms 线性退避,且受 5GB 的DB_SIZE_LIMIT约束(CYPRESS_INTERNAL_SYSTEM_TESTS=1时降为 200 字节,见 protocol.ts#L25-L32)。
本地开发模式的三个行为开关是理解指南那条命令的关键,全部在文件头部声明(protocol.ts#L22-L23):
const CAPTURE_ERRORS = !process.env.CYPRESS_LOCAL_PROTOCOL_PATH && !process.env.CYPRESS_LOCAL_STUDIO_PATH
const DELETE_DB = !process.env.CYPRESS_LOCAL_PROTOCOL_PATH
- 错误策略:生产(
CAPTURE_ERRORS=true)时,协议内部抛出的错误不会打断测试,而是被captureError收集,fatal错误最终随上报发送、非 fatal 错误在reportNonFatalErrors中批量上报——即"捕获失败不能毁掉用户的测试"。而设置CYPRESS_LOCAL_PROTOCOL_PATH后CAPTURE_ERRORS=false,所有invokeSync/invokeAsync中的异常会直接throw出来(protocol.ts#L536-L574),让你 watch 调试时第一时间看到栈——这正是本地开发的体验设计。同理,uploadCaptureArtifact内部也用!process.env.CYPRESS_LOCAL_PROTOCOL_PATH单独判定上传错误是否吞掉(protocol.ts#L371); - 数据库保留:
DELETE_DB=false时,上传完成后的finally不会fs.unlink掉归档文件,os.tmpdir()/cypress/protocol/下的.db/.tar得以保留,供开发者用 SQLite 工具直接检视捕获到的数据; - 单测佐证:packages/server/test/unit/cloud/protocol_spec.ts 明确测试了"when
process.env.CYPRESS_LOCAL_PROTOCOL_PATHis truthy"时"unlinks the db and does not rethrow"的行为分支,与上述实现一一对应。
Studio 模式也复用同一套机制:StudioLifecycleManager.ts 同样 new ProtocolManager() 并以 mode: 'studio' 调用 prepareProtocol,捕获错误时 studio 分支会实时 dispatchErrors 到 Cloud 而非攒批上报(protocol.ts#L417-L436)。
五、生产构建如何剥离本地调试后门
本地路径分支带有 TODO(protocol): Ensure this is removed in production 注释,说明打包成正式二进制时会主动移除它。scripts/binary/binary-sources.js 给出了双重保险:
const getProtocolFileSource = async (protocolFilePath) => {
const fileContents = await fs.readFile(protocolFilePath, 'utf8')
if (!fileContents.includes('process.env.CYPRESS_LOCAL_PROTOCOL_PATH')) {
throw new Error(`Expected to find CYPRESS_LOCAL_PROTOCOL_PATH in protocol file`)
}
return fileContents.replaceAll('process.env.CYPRESS_LOCAL_PROTOCOL_PATH', 'undefined')
}
构建脚本先把所有 process.env.CYPRESS_LOCAL_PROTOCOL_PATH 字面量替换为 undefined,再由 validateProtocolFile 校验替换后的产物中不得残留该字符串。也就是说:发布版二进制里本地覆盖开关在构建期被物理移除,只有从源码检出运行(yarn cypress:run)才会保留这条开发通道。这与指南"从源码克隆 cypress 仓库再跑"的前置要求完全吻合——安装版 Cypress 是无法用该变量调试捕获协议的。
六、小结与适用前提
- 适用前提:你能访问内部
cypress-services仓库,且使用源码检出的 cypress(而非 npm 安装版); - 最小可用闭环:
yarn watch持续构建 → 设置CYPRESS_LOCAL_PROTOCOL_PATH指向dist/index.js→CYPRESS_INTERNAL_ENV=staging保证请求落到非生产 Cloud; - 调试收益来自源码刻意保留的三个本地特性:异常直接抛出(而非静默收集)、归档/数据库文件保留在系统临时目录、跳过脚本签名校验;
- 深入阅读路径:guides/protocol-development.md(官方指南)→ packages/server/lib/cloud/protocol.ts(生命周期与错误策略)→ packages/server/lib/cloud/api/index.ts(脚本获取与签名校验)→ packages/server/lib/cloud/require_script.ts(字符串脚本编译)→ scripts/binary/binary-sources.js(生产剥离)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00