首页
/ Cypress Cloud 捕获协议(Capture Protocol)本地开发指南:CYPRESS_LOCAL_PROTOCOL_PATH 原理与调试实战

Cypress Cloud 捕获协议(Capture Protocol)本地开发指南:CYPRESS_LOCAL_PROTOCOL_PATH 原理与调试实战

2026-09-06 21:21:05作者:范靓好Udolf

当使用 --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 signatureapi/index.ts)。这也解释了指南中"生产环境从 Cloud 获取"与本地开发的分叉点。

二、指南中的完整操作步骤(逐条对照)

guides/protocol-development.md 给出的完整流程如下,两个仓库需要同时处于工作状态:

第一步:克隆并构建捕获协议仓库

  1. 克隆 cypress-services 仓库(需要是 Cypress 组织成员,属于内部仓库);
  2. 运行 yarn 安装依赖;
  3. app/packages/capture-protocol 目录下运行 yarn watch,让构建产物 dist/index.js 随源码改动持续更新——这是本地热调试的基础。

第二步:克隆 Cypress 主仓库并从源码运行

  1. 克隆 cypress 仓库;
  2. 运行 yarn 安装依赖;
  3. 在测试项目上执行 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.tscreateRun 流程
--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
}

三个值得注意的实现细节:

  1. captureHash:对脚本内容做 SHA256 并 base64url 编码,作为该捕获版本的身份标识,后续错误上报(dispatchErrors)会带上它,方便服务端区分是哪一版捕获代码出了问题;
  2. requireScript:捕获协议是一段字符串而非文件,require_script.tsnew Module('id', module) + mod._compile(script, '') 把它当作 Node 模块现场编译,取出其 exports.AppCaptureProtocol
  3. 构造参数createRun 成功后的 prepareAndSetupProtocol 调用传入 runIdprojectIdtestingTypecloudApi(重试与请求封装)、projectConfigdevServerPublicPathRouteportproxyUrlnamespace)、mountVersionmode: '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.tsapplication/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
  1. 错误策略:生产(CAPTURE_ERRORS=true)时,协议内部抛出的错误不会打断测试,而是被 captureError 收集,fatal 错误最终随上报发送、非 fatal 错误在 reportNonFatalErrors 中批量上报——即"捕获失败不能毁掉用户的测试"。而设置 CYPRESS_LOCAL_PROTOCOL_PATHCAPTURE_ERRORS=false,所有 invokeSync/invokeAsync 中的异常会直接 throw 出来(protocol.ts#L536-L574),让你 watch 调试时第一时间看到栈——这正是本地开发的体验设计。同理,uploadCaptureArtifact 内部也用 !process.env.CYPRESS_LOCAL_PROTOCOL_PATH 单独判定上传错误是否吞掉(protocol.ts#L371);
  2. 数据库保留DELETE_DB=false 时,上传完成后的 finally 不会 fs.unlink 掉归档文件,os.tmpdir()/cypress/protocol/ 下的 .db/.tar 得以保留,供开发者用 SQLite 工具直接检视捕获到的数据;
  3. 单测佐证packages/server/test/unit/cloud/protocol_spec.ts 明确测试了"when process.env.CYPRESS_LOCAL_PROTOCOL_PATH is 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 是无法用该变量调试捕获协议的。

六、小结与适用前提

登录后查看全文
热门项目推荐
相关项目推荐