Trace Viewer & Debugging
The Playwright Trace Viewer is the closest thing to a flight recorder for an automated browser. With tracing enabled, every test run is captured into a single trace.zip containing a frame-by-frame timeline of actions, before-and-after DOM snapshots, the complete network log, console output, and a source map back to the exact line of test code. Open it with npx playwright show-trace and you get a time-travel debugger: scrub to any action and see precisely what the page looked like, what the network was doing, and which locator the step resolved. This guide, part of Debugging & Test Observability, covers capturing traces, reading every panel, and turning a recorded failure into a fix.
What a trace records
A trace is not a log file — it is a structured recording with three synchronized layers. The action layer lists every Playwright call in order (goto(), click(), fill(), expect()), with timing and pass/fail status, and links each back to the source line that issued it. The snapshot layer stores a serialized DOM before and after each action, so you can hover a step and see the rendered page exactly as it was, including styles and the highlighted target element. The resource layer captures the full network waterfall, console messages, and any test attachments. Because all three are timestamped against one clock, selecting an action moves every panel to that moment — this is what makes "time travel" literal rather than a metaphor.
The practical consequence: a failed assertion is no longer a stack trace and a guess. You scrub to the failing expect(), read the DOM snapshot to see whether the element existed, and check the network panel to see whether the request that should have populated it actually returned. Most failures resolve in under a minute this way.
Turning capture on
Tracing is off by default because recording carries overhead. You enable it in three ways depending on context.
For a one-off local run, pass the flag:
import { test, expect } from '@playwright/test';
// Run this spec with: npx playwright test --trace on
test('checkout completes', async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
await expect(page.getByText('Order confirmed')).toBeVisible();
});
For a whole project, set the mode in config. The most economical value is on-first-retry: green runs stay fast, and the moment a test is retried Playwright records a full trace of the retry so you have evidence without paying the cost on every run. This setting lives in Playwright Config & Fixtures and is the backbone of Flaky Test Management.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
// 'on' records always, 'retain-on-failure' keeps only failed traces,
// 'on-first-retry' records the first retry — the best cost/evidence trade.
trace: 'on-first-retry',
},
});
For surgical control inside a test, drive the tracing API on the context directly. This is useful when you only want to trace one critical block of a long test.
import { test } from '@playwright/test';
test('trace only the risky block', async ({ context, page }) => {
await context.tracing.start({ snapshots: true, screenshots: true });
await page.goto('/reports');
await page.getByRole('button', { name: 'Generate' }).click();
// Write the trace to disk; open later with show-trace.
await context.tracing.stop({ path: 'trace.zip' });
});
Whichever mode you use, the output is a trace.zip written under the configured output directory (by default test-results/<test>/trace.zip).
Opening and reading the trace
Open any trace with the bundled viewer:
// Not test code — run in your shell:
// npx playwright show-trace test-results/checkout/trace.zip
import { test } from '@playwright/test';
test.skip('placeholder so the block compiles', async () => {});
You can also drag a trace.zip onto trace.playwright.dev — the viewer is a static web app, so traces are never uploaded anywhere. Inside the viewer:
- Timeline (top): a filmstrip of screenshots. Drag across it to scrub; the action list and panels follow.
- Actions (left): every call in order. Click one to pin the entire viewer to that step; the failing action is highlighted in red.
- Before/After/Action tabs: the DOM snapshot at each phase of the selected action, with the target element outlined — this is where you confirm a locator hit the right node.
- Network tab: the request waterfall, where you verify a 200 versus a 500, inspect payloads, and confirm a mock from Network Interception Basics actually served the request.
- Console / Source / Call tabs: browser console output, the exact source line, and the resolved arguments for the step.
Snapshots are interactive — you can open browser DevTools against a stored DOM snapshot and inspect elements as if the page were live, which makes selector debugging far easier than reading Reliable Selector Strategies for Playwright advice in the abstract.
The trace modes, compared
Choosing the right trace mode is the difference between a fast suite with evidence when you need it and a slow suite that records gigabytes nobody reads. There are five values for trace:
off— no recording. The default. Use it only when you have another evidence path or are deliberately optimizing a hot loop.on— record every run, pass or fail. Maximally informative and maximally expensive; reserve it for local debugging sessions (--trace on) where you want a trace of a green run too.retain-on-failure— record everything but keep the trace only if the test fails. You pay the recording cost on every run but the disk cost only on failures. Good when failures are rare and you want a trace for each one without retries.on-first-retry— record nothing on the first attempt; if the test fails and is retried, record the retry in full. The best cost/evidence trade for CI: green runs are free, and the first flake is captured automatically. This is the recommended default and the one Flaky Test Management is built around.on-all-retries— record every retry attempt. Useful when a test passes on the second retry but not the first and you want both recordings to compare.
The decision hinges on how you run. Locally, on while you debug a specific spec gives you a trace even when the test passes, which is invaluable when "passing" is itself suspicious. In CI, on-first-retry paired with retries: 2 is almost always correct: it costs nothing on the common path and produces a full trace exactly when a flake appears.
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
// Cheap on green, full evidence on the first flake.
trace: process.env.CI ? 'on-first-retry' : 'off',
},
});
The network panel in depth
The network panel is where trace analysis pays off most, because a large share of test failures are really data-timing failures wearing a selector costume. The panel shows every request the page made during the recording as a waterfall, with method, URL, status, size, and duration. Selecting an action on the timeline filters the waterfall to the requests in flight at that moment, so you can see exactly what the page was waiting on when an assertion fired.
Three readings are routine. First, status codes: a 500 or 403 on a request that should populate the UI explains a missing element immediately. Second, timing: a request that resolves after the failing assertion's timeout is a race — the data was on its way but the test gave up. Third, the request that never happened: if an expected call is absent from the waterfall, the action that should have triggered it did not fire, which points back to the click or submit one step earlier.
This panel is also how you confirm interception worked. When you mock or rewrite a request per Network Interception Basics, the trace flags the served request, so you can verify your handler ran and returned the shape you expected instead of trusting that it did. A mock that silently fails to match its glob shows up here as a real network call where you expected a fulfilled one.
import { test, expect } from '@playwright/test';
test('trace shows the mocked request was served', async ({ page }) => {
// Mock the endpoint so the network panel flags it as fulfilled, not live.
await page.route('**/api/profile', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ name: 'Ada', plan: 'pro' }),
});
});
await page.goto('/account');
// If this fails, the trace network panel tells you whether the mock fired.
await expect(page.getByText('Ada')).toBeVisible();
});
DOM snapshots and selector debugging
A trace's DOM snapshots are live, inspectable copies of the page at each phase of an action, not screenshots. That distinction is what makes them powerful for selector work. When an assertion fails because a locator matched zero or many elements, you open the snapshot, launch DevTools against it, and run the selector by hand to see what it actually resolved to. There is no faster way to fix a strict mode violation than to query the snapshot and watch three elements light up.
The viewer outlines the target element of the selected action in the snapshot, so you can confirm at a glance that getByRole('button', { name: 'Save' }) landed on the button you meant and not a hidden duplicate in a collapsed menu. When it landed wrong, the snapshot shows you the real DOM structure to write a better locator against — turning the abstract advice in Reliable Selector Strategies for Playwright into a concrete fix for the page in front of you.
Snapshots come in Before, Action, and After variants. Before shows the page as the action began; Action captures the input point (for clicks, where the pointer landed); After shows the result. Comparing Before and After across the failing step often reveals the problem directly — a modal that opened over your target, a list that re-ordered, a spinner that never resolved.
Attaching context to a trace
Traces become more useful when they carry the test's own context. Anything you attach to a test — a screenshot, a downloaded file, a JSON payload — appears in the trace and the HTML report, so you can annotate a recording with the data that explains it. This is the bridge between traces and the broader artifact story in Reporters & Test Artifacts.
import { test, expect } from '@playwright/test';
test('attach the response body for later inspection', async ({ page }, testInfo) => {
const res = await page.request.get('/api/health');
// Attach the raw body so it shows up in the trace and HTML report.
await testInfo.attach('health.json', {
body: await res.text(),
contentType: 'application/json',
});
expect(res.ok()).toBeTruthy();
});
Attachments turn a trace from a recording of clicks into a record of decisions: the exact payload a test asserted against, the file it downloaded, the screenshot of a state worth keeping. When a failure needs more than the built-in snapshots to explain, an attachment is how you put that evidence in the same place everyone already looks.
Sharing and storing traces
A trace is most valuable when the right person can open it without friction. Because trace.zip is a single self-contained file, sharing is simple: send the file, or send a link to the published HTML report that embeds it. The recipient needs nothing installed — trace.playwright.dev opens it client-side, and the report links to the same viewer. For sensitive applications where the DOM or network panel might contain secrets, prefer the offline npx playwright show-trace and avoid uploading the file anywhere.
In CI, traces are written under the output directory and should be uploaded as job artifacts so a failed pipeline carries its own evidence. The mechanics of publishing belong to CI/CD Integration, but the principle is simple: a red check should be one click from the trace that explains it. Retention is the trade-off to manage — traces from on-first-retry are small because they only exist for flakes, while on in CI would produce a trace per test and quickly exhaust artifact storage. Match retention to the trace mode: short for on runs, longer for the rare on-first-retry captures that are worth keeping.
When a trace is not the right tool
Traces are post-mortem evidence, which makes them the wrong tool for two jobs. The first is authoring: while you are still writing a flow and do not yet know the selector or the right wait, the record-then-read loop is too slow. For that, step through the live browser with the Inspector or UI mode. The second is a hang: a test that never finishes never writes a complete trace, so a stuck run needs a timeout to fail it first, after which the trace up to the hang becomes available. Set bounded timeout and expect.timeout values in Playwright Config & Fixtures so a hang turns into a diagnosable failure rather than an infinite wait.
For everything else — a failure you cannot reproduce, an intermittent flake, a CI-only break, a selector that resolves wrong — the trace is the fastest path from red to root cause, which is why it anchors the entire Debugging & Test Observability workflow.
Where to go next
This guide branches into two focused walkthroughs.
Analyzing Test Failures with the Playwright Trace Viewer is the end-to-end procedure for taking a red CI run, opening its trace, and pinning the exact action that broke.
Debugging with Playwright Inspector and UI Mode covers the interactive tooling — PWDEBUG=1, --debug, --ui, page.pause(), watch mode, and the locator picker — for stepping through a test live while you write it.
Frequently Asked Questions
Does enabling traces slow down my tests?
Recording adds overhead, which is why on is rarely used in CI. The recommended trace: 'on-first-retry' records nothing on a passing first attempt and only captures a full trace when a test is retried, so the cost lands exactly when you need the evidence and never on green runs.
Is my trace data uploaded anywhere when I use the online viewer?
No. The viewer at trace.playwright.dev is a fully client-side application; the trace.zip you drop in is parsed in your browser and never sent to a server. For sensitive data, npx playwright show-trace runs the same viewer entirely offline.
Why is the Network tab empty in my trace?
Network capture requires snapshots and resources to be recorded, which the standard trace modes enable. If you start tracing manually with context.tracing.start(), pass snapshots: true and screenshots: true; without them the timeline and resource panels will be sparse or empty.