绝大多数自动化项目的问题只有两类:跑不通的,和跑通了但结果是错的。而最让人崩溃的,是CI环境里跑不通,本地怎么复现都是绿的。以前遇到这种情况,我能做的就是翻日志、翻截图,运气好了能从screenshot里看出点端倪,运气不好只能加一堆打印重新跑一遍。
Playwright自带的追踪查看器(Trace Viewer)把这种"猜"变成了"看"——它能把一次浏览器会话完整录制下来,包括每一步操作对应的DOM快照、像素截图、发出去的每个网络请求、控制台的每一条输出,最后打包成一个zip文件,拖进查看器就能像放录像一样逐步回放整个执行过程。这篇文章写给三类人:写端到端测试被定位问题折腾的测试开发、用Playwright做数据采集但经常被动态内容搞晕的爬虫工程师,以及想把E2E调试效率提上去的前端开发者。
1. 追踪查看器到底是个什么"黑匣子":定位与能力边界
1.1 六类信息一次抓齐
追踪查看器在底层是Playwright tracing API的图形化前端。调用tracing.start之后,Playwright会在浏览器上下文层面启动记录,把CDP事件、请求生命周期、DOM快照、控制台消息全部按时间线组织起来,stop的时候导出一个zip包。
打开zip之后,界面左侧是一条操作时间线,右侧是六个面板:
- Actions:每一步操作,包括goto、click、fill、wait等,带时间戳,点击任意一步能看到该操作前后的快照。
- 快照:操作前后的DOM状态,不是简单截图,是可交互的DOM结构,能看元素属性、文本、标签层级。
- 网络:这次会话里发出的所有请求,瀑布图排列,点开任意请求能看到header、状态码、响应体预览和完整timing。
- 控制台:页面里的console.log、warn、error,以及未捕获异常都会被记录。
- 源码:如果开启sources参数,会记录执行过的JS脚本片段,方便看页面内部到底跑了什么代码。
- 元数据:浏览器版本、视口尺寸、UA、设备信息等环境上下文。
这六类信息不是简单堆在一起,而是按照同一时间轴对齐的。拖到时间轴的某个位置,左侧快照和右侧网络请求会同步切换到这个时刻,这是它和普通日志工具最本质的区别——所有证据链共享一个"时间戳坐标系"。
1.2 哪些场景值得开trace
根据我的实际经验,下面这些场景开trace的收益最大:
- 断言失败但截图"看起来正常"的时候。例如点击后应该弹出提示,但实际没有;或者页面状态A/B切换太快,截图捕捉不到中间态。
- CI上挂、本地不挂的环境差异问题。trace里记录到的资源加载顺序、请求timing、UA和浏览器版本,往往比报错信息更有说服力。
- 元素定位不稳定。快照可以还原元素在页面里的真实DOM位置,能快速确认是不是iframe、是否被遮挡、是否出现在错误节点下。
- 爬虫类脚本调试动态内容。页面渲染出来的内容是JS动态生成的,但脚本跑的时候内容还没加载完;通过trace可以核实"目标数据在哪个时间点才出现,以及是哪个请求带回的"。
- 性能类排查。单看一段瀑布图可能看不出问题,但配合操作时间线上的步骤耗时,能定位"到底是页面加载慢,还是某个操作触发了重型渲染"。
如果你是刚接手一个Playwright项目的维护者,第一件事就是在fixture里把trace开起来,而不是急着改用例。因为改用例之前,你需要先有"现场"。
1.3 和录屏、截图、控制台日志的关系
很多人拿trace和录屏比,其实两者目标完全不同。我整理过一张对照表:
| 工具 | 记录内容 | 可搜索性 | 体积 | 典型价值 |
|---|---|---|---|---|
| 截图 | 单帧像素 | 低 | 小 | 人工肉眼快速确认 |
| 录屏视频 | 连续画面 | 低 | 很大 | 看视觉层面的变化 |
| console日志 | 程序员手动插桩输出 | 中 | 小 | 检查关键业务节点 |
| Trace Viewer | 结构化全量记录 | 高 | 中 | 深入排查、团队协作 |
trace不是要来替代截图和日志,它是兜底方案。截图的局限是只能看到"你预设好的瞬间",录屏的局限是只能看到"画面",而trace能同时回答三个问题:页面当时长什么样、代码当时执行到哪一步、网络当时在干什么。
2. 产出追踪文件的正确姿势:三种方式与关键参数
2.1 代码里手动start/stop:最灵活
最基础的做法是直接在脚本里调用tracing接口。以Python同步API为例:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() context = browser.new_context() # 必须在创建page之前启动tracing context.tracing.start(screenshots=True, snapshots=True, sources=True) page = context.new_page() page.goto("https://example.com") page.fill("#search", "playwright trace viewer") page.click("button[type='submit']") context.tracing.stop(path="output/trace.zip") browser.close()这里有一个新手最容易踩的坑:tracing.start必须在创建page之前调用。原因在于trace是以context为单位记录的,context内部所有的页面活动都属于追踪范围,但如果context里已经存在活跃的page,早期的部分事件就不会被完整捕获。我看到过有人把page创建放在start之前,结果跑完整个case,trace打开是空的,白忙活一场。
如果你用async版本,逻辑完全一样,只是改成await context.tracing.start(...)和await context.tracing.stop(path=...)。在Node.js里则是await context.tracing.start({ screenshots: true, snapshots: true, sources: true }),属性名稍有区别。
stop之后会生成一个zip文件。这个zip本质上是一个多文件归档,里面包含trace结构、资源截图和网络HAR相关数据。平时我们不需要手动拆开看,但了解这一点有助于理解"为什么trace能跨机器回放"——所有现场信息都已经固化到文件里,和跑trace的机器无关。
2.2 Playwright Test Runner的retain-on-failure:只在失败时保留
如果用的是Playwright Test Runner,不需要每个用例手动写start/stop,直接在配置里声明策略:
export default defineConfig({ testDir: './tests', use: { trace: 'retain-on-failure', }, });可选的trace值有四种:
off:完全不记录,省资源但出事没现场。on:每个用例都保存trace,信息最全,但大量通过用例的trace会变成磁盘垃圾。retain-on-failure:测试通过就丢弃trace,测试失败才保留。这是CI环境最推荐的值。on-first-retry:第一次重试时才开启trace,适合偶发问题,既控制体积又能在重试时取证。
我个人的落地建议是:本地开发用on,因为本地操作成本低,而且你可能会想反复看一个通过用例的某个步骤;CI上用retain-on-failure,避免跑一次流水线存下几百个zip。如果是处理偶发性的flaky问题,on-first-retry的效果很直观——我见过有团队把trace调成on之后,一个本来每天跑500个用例的项目,trace产物从几十MB膨胀到几个GB,后来切回retain-on-failure才恢复正常。
2.3 命令行查看与远程共享
拿到trace.zip之后,本地查看用一条命令:
npx playwright show-trace output/trace.zip执行后会自动在默认浏览器里打开查看器。如果trace文件在远程服务器或CI机器上,可以通过参数指定监听地址,让别人也能看:
npx playwright show-trace --host 0.0.0.0 --port 9876 output/trace.zip另外官方还提供了在线查看器,把trace.zip拖进去即可解析。但我必须提醒一句:如果你是排查公司内部系统,trace里很可能包含接口请求参数、Cookie、Token这类敏感信息,不要随便传到公网在线工具。优先用本地show-trace,或者走内网共享。这是我踩过之后长记性的地方——有一次我把一个内部系统的trace发给外包同事,对方一个开屏截图就把页面标题漏出去了,虽然没造成实质问题,但足够难堪。
2.4 文件体积的三个开关怎么取舍
tracing.start里最重要的三个参数是screenshots、snapshots和sources,它们直接决定trace的体积和能查什么:
| 参数 | 记录内容 | 对体积的影响 | 推荐场景 |
|---|---|---|---|
| screenshots | 持续的像素截图 | 明显增大 | 要看页面真实渲染效果时开 |
| snapshots | 操作前后的DOM快照 | 中等 | 排查元素定位、DOM状态,建议常开 |
| sources | 页面JS源码片段 | 显著增大 | 需要调试页面内部JS逻辑时开 |
如果用例以接口和状态断言为主,只开snapshots就够了。如果是复杂的前端交互、需要确认某个元素有没有在错误时机出现,就开screenshots+snapshots。如果是页面JS逻辑导致的异常,比如某个变量未定义、某个函数没执行,那就必须开sources,否则Trace Viewer的源码面板是空的。
体积敏感的项目还可以给stop指定path到独立目录,配合CI清理策略,只保留最近N天的trace。这些细节看起来不起眼,但在规模化的自动化项目里都是实打实的成本。
3. 还原失败现场:Actions时间线与DOM快照实战
3.1 before/after快照怎么用
打开任何一个含操作记录的trace,左侧的Actions列表会按时间顺序列出每个动作。点击任意动作,右侧快照区域会出现两个视图:before和after,分别代表该动作执行前、执行后的DOM状态。
这个before/after对照非常实用。比如fill操作后必须断言某个字段的值,你可以切到before看输入框原始状态,切到after看填入后的状态,确认输入是否真的落到了正确的控件上。很多"看起来填了但断言不过"的诡异问题,都是因为代码把文本填到了一个不可见的隐藏input里,或者填到了另一个同名控件上——这时候快照会直接告诉你真相。
另一个常用手法是拖动时间线。注意Trace Viewer不是逐帧视频,它按事件组织,在两个动作之间的空闲期会跳到对应的下一个事件点。这反而是好事,因为你可以快速跨过无意义的等待,直接聚焦在关键动作节点上。
3.2 元素"明明存在"却定位失败:快照给出的答案
排查元素定位问题是我用Trace Viewer频率最高的场景。分享几个真实案例。
第一个案例:脚本断言page.locator("text=登录").is_visible(),结果返回False。单看截图,"登录"两个字明明就在页面上。打开trace的before快照后我发现,这个"登录"按钮在一个iframe里,而我的locator没有指定frame,默认只在主frame里查找,自然找不到。这个结论通过快照一眼就能确认——因为快照会完整显示DOM树,包括iframe嵌套结构。
第二个案例:严格模式歧义报错。页面有多个"提交"按钮,一个在弹窗里,一个在页面底部,外层容器不同,但可见文本相同。Trace Viewer的快照在展示匹配元素时会高亮,你能直接看到到底命中了几个节点、分别在哪。这种歧义问题光靠截图很难发现,因为截图看起来"只有一个提交按钮",只有DOM快照才能暴露隐藏弹窗里的那一个。
第三个案例:stale element。click之后马上对某个元素做断言,结果报元素已从DOM分离。打开after快照会发现,点击动作触发了页面跳转或重绘,旧节点被替换,而断言针对的引用还是旧节点。这种问题的标准解法是等待:先expect(locator).to_be_visible()再执行后续操作。Trace Viewer的价值在于能一眼确认"动作之后页面到底发生了什么",不用靠猜。
3.3 console面板揪出页面级JS报错
有一类问题非常讨厌:所有操作都成功、没有任何断言失败,但页面某个区块就是不显示数据。这种情况截图不会告诉你原因,测试日志也不会,而Console面板会。
trace会记录页面所有console输出,包括console.log、console.warn、console.error和未捕获异常,每条都带时间戳,并且能和Actions时间线对应上。比如页面依赖的一个CDN脚本加载失败,导致某个全局函数未定义,控制台会报ReferenceError: xxx is not defined。你在trace里拖动时间线,就能看到这个报错出现在哪个操作之后,进而推断是哪个步骤引入了依赖。
我印象最深的是一次解决广告拦截类插件导致的脚本异常。页面本身正常,但某些环境下浏览器扩展拦截了某个第三方统计脚本,导致后续业务代码崩了。Console面板里能清楚看到是哪个脚本的哪个调用抛了异常,这比在业务代码里加打印高效得多。
4. 网络瀑布与接口时序:把排查下沉到HTTP层
4.1 单请求全链路时序
Trace Viewer的Network面板把整次会话的所有请求列成瀑布图,点击任意请求,右侧能看到几部分信息:
- General:请求URL、请求方法、状态码、响应类型。
- Request Headers:实际发出的请求头。
- Response Headers:响应返回的头信息。
- Response Content:文本和JSON响应体的预览。
- Timing:Queueing、Stalled、Request sent、Waiting(TTFB)、Content Download等阶段的耗时分解。
对排查问题来说,Timing是最有价值的部分。TTFB(Waiting)时间长,说明服务端处理慢;Content Download时间长,说明响应体大或带宽受限;Queueing长,说明前端并发连接可能饱和。我遇到过脚本在页面上等一个数据表格,等了十秒都没出现,最后在trace里看到某个数据接口TTFB高达9秒。这根本不是自动化脚本的问题,是后端服务本身响应慢,trace把责任方一次性锁定了。
4.2 动态内容与爬虫场景:用网络日志定位真实数据源
做数据采集的朋友经常会遇到一个问题:页面渲染出来的内容一大片,但DOM结构复杂、类名混乱,很难稳定地解析出目标数据。与其在DOM解析上死磕,不如直接看trace网络面板里"页面到底请求了哪些接口",因为浏览器每一次动态渲染的背后,一定有一个或几个XHR请求把数据带回来。
举个典型场景:一个页面用动态iframe嵌入了图表模块,iframe里的DOM和主页面隔离,常规的页面级selector根本穿透不进去。用trace跑一遍采集脚本,Network面板会列出所有请求,其中必然包括iframe内部发出的XHR,响应体里就是图表数据源。这时候直接把抓取目标从"解析渲染后的DOM"改成"直接请求数据接口",稳定性和速度都会提升一大截。
这一类"通过trace逆向数据接口"的用法,在合法合规的公开数据采集中非常实用。需要注意边界:只做公开可见数据的合理获取,遵守目标网站的条款和robots约束,不做对抗性操作,不碰需要权限或绕过验证机制的数据。trace在这里只承担"调试现场"的角色,不参与任何绕过逻辑。
我注意到最近很多讨论集中在Playwright配合MCP做AI驱动的浏览器操作,以及爬取动态页面内容,但不管前端调用方式怎么变,动态页面的核心规律不变:数据一定经过网络。Trace Viewer的Network面板就是在教你"用网络视角看页面",而不是"用DOM视角猜页面"。
4.3 慢查询与资源加载瓶颈
性能类问题同样可以靠trace定位。有一次我做首屏优化排查,页面上图片很多,本地加载看起来不慢,但用户环境总反馈卡顿。我用trace跑了一遍,Network瀑布图里明确看到某几个大图资源是串行加载的,而字体文件阻塞了渲染。瀑布图把这个问题视觉化之后,根本不需要争论,直接按trace里的顺序调整资源优先级即可。
对比多次运行也很方便:两次trace分别打开,看同样的操作在不同时刻的耗时差异,能帮助你判断是网络抖动、服务端波动,还是代码实现了新的慢路径。
5. CI环境、团队协作与常见坑
5.1 让trace成为CI产物的自动化流程
trace真正发挥威力是在CI环境。本地回放的成功率再高,也不如直接把失败用例的trace变成构建产物。在GitHub Actions里配合Playwright Test Runner,配置几乎没有额外成本:
- uses: actions/upload-artifact@v4 with: name: playwright-traces path: test-results/**/*.zip if-no-files-found: ignore当某个用例失败,trace.zip会保留在test-results目录下,upload-artifact会自动把它归档到当前运行记录里。团队的协作模式因此变得非常简单:PR挂掉之后,直接把artifact链接发给负责的同事,对方下载解压后一条show-trace命令就能看到完整的失败现场。
Jenkins或GitLab CI同理,用archiveArtifacts或者artifacts关键字把trace zip收集起来。有了这个流程,我基本告别了"远程加打印、重跑、再看日志"的三段式循环。
5.2 大文件的快速定位技巧
trace文件一大,原始的时间线会变密。以下几个技巧我在处理大型工程时常用:
- 把trace按用例命名,而不是全部塞到一个目录下。
retain-on-failure模式下,通常一个case一个zip,命名里带上用例名和时间戳,检索起来最方便。 - Actions列表上方有筛选框,可以直接搜索操作文本,比如输入"click"就能过滤出所有click动作;定位某个操作不用整个时间线来回拖。
- Network面板支持按URL搜索,直接粘贴接口路径,快速定位某个关键请求,省去在大瀑布图里肉眼找。
- 如果某个时间段明显异常,拖动时间线放大,把时间窗口缩到你怀疑的区间,再对照左侧Actions和右侧网络变化。
这些操作细节,不用记快捷命令,熟悉界面布局之后自然就形成了肌肉记忆。
5.3 容易忽略的四个坑
第一,tracing.start必须在创建page之前执行。我见过太多"trace打开后只有空白页面"的案例,根因都是这个。请把start语句放在browser.new_context之后、page创建之前。
第二,没有给stop传path参数,trace不会被保存。context.tracing.stop()如果只调用不指定path,等于什么都没导出。检查一下自己的脚本是不是漏了这个参数。
第三,多个context要分别开trace。如果你的测试脚本同时创建了多个浏览器上下文,trace只记录你在哪个context上调用了start。别只给默认context开启,然后发现另一个窗口的活动全部缺失。
第四,trace会包含敏感信息。URL参数、Cookie、Token都可能出现在网络记录里。分享trace之前先检查有没有敏感字段,企业环境坚决用本地show-trace离线查看。
5.4 MCP工作流里trace的配合方式
最近browser-use MCP、Playwright MCP这类AI辅助浏览器工具讨论度很高,不少人好奇它们和Trace Viewer什么关系。我的理解是:MCP类工具解决的是"实时对话式操作浏览器"的问题,而Trace Viewer解决的是"事后完整回溯"的问题,两者并不冲突。
在实际工作流里,AI驱动的浏览器操作同样会产生操作序列和网络请求,关键会话完全可以导出成trace文件留档。反过来,当你用Trace Viewer发现一个关键线索时,也可以把线索描述出来作为提示词,让AI代理针对性地做下一步验证。把trace当作人和AI之间共享的"现场语言",排查效率会明显上一个台阶。
结尾
说实话,我最初对Trace Viewer的期待也就是"高级录屏",甚至觉得日常调试截图加日志已经够用。直到有一次帮同事排查元素定位不稳的问题,发现根本不是元素没加载,而是页面上两个同名按钮都被捕获,严格模式按规矩报了歧义——这种事情靠肉眼看截图完全发现不了,只有DOM快照能把底层结构老老实实摊开。
从那以后,我养成了一个习惯:无论什么项目,先把trace配置写进基础fixture里,让它作为默认能力存在。要么用retain-on-failure,要么在所有手动调试脚本里固定保留trace.zip输出。这个习惯的成本几乎为零,但换来的是——一旦出问题,你手上永远有完整的现场,而不是一堆碎片日志。如果你还没试过,下次调试时开一个trace看看,大概率就回不去了。