一个 Playwright trace 文件里到底存了什么

2026-08-18

同一个项目、同一条失败用例,队友发过来的 trace 打开以后源码面板有高亮、Network 里点开请求能看到响应体、还挂着几张对比图;自己在 CI 上捞下来的那份点开只有一串动作和一堆截图,别的什么都没有。这不是 Trace Viewer 的版本差异,是录制时的开关不一样——而开关的差异,最后是以 zip 里有没有那几个目录的形式落下来的。

所以这篇不讲怎么用 Trace Viewer,而是把 trace.zip 当成一个压缩包拆开,沿着 github.com/microsoft/playwright 仓库里的代码走一遍:每一类条目是谁写进去的、归哪个开关管。

zip 里其实有两条产线

先说一个容易让人困惑的事实:Test Runner 产出的 trace 包里,同时存在 trace.tracetest.trace 两种条目,它们来自两套不同的记录器。

浏览器侧那套在 packages/playwright-core/src/server/trace/recorder/tracing.ts。它在 start() 里按 traceName 拼出 traceName + '.trace'traceName + '.network' 两个中间文件,停止时在 stopChunk() 里把它们分别以 trace.tracetrace.network 的条目名塞进 zip。

测试侧那套在 packages/playwright/src/worker/testTracing.ts,它把测试步骤、断言、错误、标准输出这类事件攒在内存里,最后以常量 testTraceEntryName(值是 test.trace)写进 zip。这也解释了 docs/src/api/class-tracing.md 里那段提示的由来:文档写明 context.tracing 捕获的是浏览器操作与网络活动,不记录 expect 这类测试断言,推荐通过 Playwright Test 的配置开启 tracing。断言在 test.trace 那一半里。

一次测试可能跑出多个临时 trace 文件,mergeTraceFiles() 负责合并。合并规则值得看一眼:条目名匹配 /trace\.[a-z]*$/ 的会被加上 i- 前缀(0-trace.trace1-trace.network 这种),而 test.trace 保持原名不变——源码注释说明这是为了让信息最全的那份测试 trace 留下来。所以你解压后看到带数字前缀的条目,别以为是坏了。

trace.trace 是一行一个事件

_appendTraceEvent() 的写法是 JSON.stringify(visited) + '\n' 追加,也就是说这是个 JSONL 文件,一行一个事件对象。事件的类型定义都在 packages/trace/src/trace.ts 里,通过 type 字段区分:context-optionsbeforeinputafterlogeventconsolescreenshotaria-snapshotscreencast-frameframe-snapshotresource-snapshotactionstdout / stderrerror

有个细节挺影响调试体验:_appendTraceEvent() 里有个 flush 判断,eventconsolelog 这三类事件默认不立即刷盘,注释写的理由是它们太吵;只有 live 打开时才每条都刷。这意味着进程被强杀时,最后那段控制台日志未必落到磁盘上。

快照:一个开关下面藏着三个

snapshots 是这里最需要看清语言绑定的一个选项。docs/src/api/class-tracing.mdTracing.start.snapshots 写了两份:

  • langs: js 的那份类型是 boolean 或对象,对象里有 domariascreen 三个子开关,文档写明「Passing true is a shortcut for { dom: true }」;
  • langs: java, python, csharp 的那份就是个 boolean,说明是「capture DOM snapshot on every action」加「record network activity」。

也就是说,ariascreen 这两个细分开关目前只在 JavaScript 的文档里写着。写 Python 或 C# 时照着 JS 的对象写法传参,是在赌一个文档里没有的接口。

这三个子开关在录制端对应 TracerOptionssnapshotDomsnapshotAriasnapshotScreen 三个字段。start()resources 目录是无条件 mkdir 的,screenshotsaria 两个目录则分别由 snapshotScreensnapshotAria 决定建不建(screencast 目录由 screencast 字段决定,而客户端 packages/playwright-core/src/client/tracing.ts 里把它写成 screencast: options.screenshots——所以面向用户的那个 screenshots 选项,管的其实是 screencast 帧)。之后各走各的路:

  • snapshotScreen 打开时,_captureScreenshot()screenshots/${progress.metadata.id}-${phase}.png 存 PNG;
  • snapshotAria 打开时,_captureAriaSnapshot()aria/${progress.metadata.id}-${phase}.json 存 JSON;
  • snapshotDom 打开时,DOM 快照走 frame-snapshot 事件写进 trace.trace,而快照引用到的资源由 onSnapshotterBlob() 落成 resources/<sha1> ——按内容哈希命名,所以同一份样式表在整个 trace 里只存一次。

上面那个 phase 就是 Trace Viewer 里 Before / Action / After 三栏的来源,docs/src/trace-viewer.md 的表格把三者分别描述为动作调用时、实际输入发生时、动作之后的快照。

网络:DOM 快照顺带把它打开了

注意上面那句文档原话——snapshots 的 DOM 那一档同时负责「record network activity」。网络记录不是独立开关,它跟着 DOM 快照走。start() 里也是这么写的:只有 options.snapshotDom 为真才调 this._harTracer.start(...)

网络条目最终落在 trace.networkonEntryFinished() 把每条 HAR entry 包成 resource-snapshot 事件追加进去,同样是一行一条。因为这个文件跨 chunk 复用,stopChunk() 里会先复制一份带 -pwnetcopy-<n> 后缀的副本再打包,避免多个 chunk 打架。

这里有一处直接决定体积的设计。start() 调 HAR 记录器时传的是 { omitScripts: !options.live },紧挨着的源码注释原话是:

// Tracing is 10x bigger if we include scripts in every trace.

这是仓库源码里的注释原话,不是我们测出来的结论。它带来的实际后果在 packages/playwright-core/src/server/har/harTracer.ts 里:拿到响应体后先判断 if (this._options.omitScripts && request.resourceType() === 'script'),是脚本就直接返回,body 不入档。所以非 live 模式下,你在 Network 面板点开一个 .js 请求看不到响应内容,这是既定行为,不是 trace 坏了。

源码:src/ 目录与 Java 那个环境变量

sources 开关管的是 zip 里的 src/ 目录。两条产线各有一套实现,但命名规则一致:'src/' + calculateSha1(sourceFile) + path.extname(sourceFile)——哈希的是文件路径,扩展名保留,所以解压出来能看出是 .ts 还是 .py

测试侧在 testTracing.ts 里遍历所有 before 事件的 stack,把出现过的文件读出来打包;浏览器侧在 packages/playwright-core/src/server/localUtils.tszip() 里从 callStacks 收集,同时把调用栈本身序列化成 trace.stacks 这个条目。Trace Viewer 里点一个动作能跳到对应源码行,靠的就是 trace.stackssrc/ 这一对。

Java 绑定这里有个额外前提,docs/src/api/class-tracing.mdTracing.start.sourceslangs: java 那份)写明:应用源码目录必须通过 PLAYWRIGHT_JAVA_SRC 环境变量提供,路径分隔符在 Windows 上是 ;,其它平台是 :。在 Windows 上照着 Linux 文档用冒号分隔,结果就是 src/ 里空空如也——而这个失败是安静的,源码文件读不到时那边是 .catch(() => {}) 吞掉的。

附件:只有测试侧产线才有

attachments 这个开关只出现在 Test Runner 侧的 TraceOptions 里,context.tracing.start() 的选项列表中没有它。逻辑在 testTracing.ts:如果 attachments 为假,就把所有 after 事件上的 attachments 字段删掉;为真则把每个附件的内容读出来,按 'attachments/' + sha1 写入 zip,并用一个 sha1s 集合去重。

有两个细节值得记住。一是 serializeAttachments() 里有 attachments.filter(a => a.name !== 'trace'),名为 trace 的附件会被排除,避免 trace 把自己套进去。二是视觉回归比对的三张图(diff、actual、expected)就是以附件形式进来的,docs/src/trace-viewer.md 里 Attachments 那一节标着 langs: js。关掉 attachments 省下来的就是这部分内容。

把开关和条目对起来

zip 里的条目由哪个开关决定
trace.trace / test.trace只要开始录制就有
trace.network + resources/<sha1>snapshots 的 DOM 那一档
screenshots/<callId>-<phase>.pngsnapshots.screen(仅 JS 文档写明)
aria/<callId>-<phase>.jsonsnapshots.aria(仅 JS 文档写明)
screencast/<pageId>-<ts>.jpegscreenshots
src/... + trace.stackssources
attachments/<sha1>attachments(仅 Test Runner 侧)

Test Runner 侧这几个开关的默认值写在 testTracing.tsdefaultTraceOptions 里:screenshotssnapshotssourcesattachments 都是 truelivefalsemode'off'。这是仓库当前代码里的默认值,随版本可能变动。

再往上一层,mode 决定的是录不录留不留两件事,docs/src/test-use-options-js.md 的 Trace modes 表格把这两列分得很清楚:retain-on-failure 是每次都录、只留失败那次;on-first-retry 是只在第一次重试时录、录了就留。另一处文档 docs/src/trace-viewer.md 在列 'on' 时直接加了括注 not recommended as it’s performance heavy,用在全量回归上要自己掂量。

想按条目裁体积,可以把 mode 和这些子开关写在一个对象里:

// playwright.config.ts
use: {
  trace: {
    mode: 'on-first-retry',
    sources: false,
    attachments: false,
  },
},

以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。

顺带一提,Tracing.start 还有个 live 选项,文档标注 since: v1.59,语义是把 trace 写成实时更新的非归档文件而不是最后打成 zip。前面提到的脚本 body 与事件刷盘策略都会因为它而改变。

自己验证一遍

最直接的办法是打开看:

npx playwright show-trace path/to/trace.zip

Python 绑定是 playwright show-trace trace.zip,C# 是 pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip,都在 docs/src/trace-viewer.md 里。想核对条目结构,把 zip 解压开看目录名更省事——Windows 上资源管理器直接双击就能进去,不用装额外工具。

最后一句提醒和内容本身有关:按上面的清单,一个开满开关的 trace 里装着页面 DOM、非脚本类响应体、控制台输出、你的源码文件,还有测试附件。docs/src/trace-viewer.md 写明 trace.playwright.dev 是完全在浏览器里加载 trace、不向外传输数据的;但这句话保护不了你把 zip 本身发到哪里去。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。