Home / Blog / Playwright trace viewer

Playwright Trace Viewer: how to get a trace out of CI and read it

Quick answer

Set trace: 'on-first-retry' in playwright.config.ts and give the config a non-zero retries, or the trace never records. Upload playwright-report/ as a CI artifact, then download it, unzip it and run npx playwright show-report <folder> — opening index.html from disk will not work. Click the trace link beside the failed test and start in the Actions pane.

Two things sit between a red CI job and the screen that explains it, and both are mechanical. Either no trace was recorded for that run, or one was recorded and it is inside a zip on an artifacts tab that does nothing useful when you double-click it.

Why is there no trace for the run that failed?

The trace option is off until somebody turns it on. Playwright's API reference for test options states the default in one line — "Whether to record trace for each test. Defaults to 'off'." — so a repository nobody has configured for tracing has no trace to find, on any run, however many times the job is re-run.

The trace-viewer guide lists five values for the config option, each with a description of what it records:

ValueWhat docs/trace-viewer says it does
'off'Do not record a trace.
'on'Record a trace for each test. (not recommended as it's performance heavy)
'on-first-retry'Record a trace only when retrying a test for the first time.
'on-all-retries'Record traces for all test retries.
'retain-on-failure'Record a trace for each test, but remove it from successful test runs.

Read the 'on-first-retry' row next to your retries line before you read anything else. That value records nothing when retries is 0, because there is no first retry to record. That combination is the whole explanation for an artifacts tab holding a report and no trace file, and it survives every re-run of the job, because the setting is doing exactly what it says.

These samples ran under Playwright 1.62, which is also the version the trace quoted further down came out of — the Metadata tab says so. The two keys sit in one config on purpose:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure',
  },
});
Playwright 1.62 · TypeScript · playwright.config.ts

The retries line above decides which of the two you want. Where the pipeline retries, 'on-first-retry' is the documentation's own CI recommendation: "Traces should be run on continuous integration on the first retry of a failed test by setting the trace: 'on-first-retry' option in the test configuration file. This will produce a trace.zip file for each test that was retried." Where it does not retry — which includes the local run this config gives retries: 0 — the guide names the value that still leaves you a file: "You can also use trace: 'retain-on-failure' if you do not enable retries but still want traces for failed tests."

For a run you need today, without merging a config change, --trace overrides the config for that run only. Its value list is longer than the guide's. docs/test-cli documents seven values for the flag, adding retain-on-first-failure and retain-on-failure-and-retries; docs/test-use-options documents the same seven as a table of what each mode records and what it keeps; the API reference for test options documents seven again. None of those pages contradicts the guide — a guide names the values worth reaching for. Take the value you write into a config from the config option's own reference before you type one the guide never mentions.

How do you get the trace off the CI runner and open it?

A failure you can reproduce on your laptop needs none of this. The one you cannot happened on a machine the pipeline has already destroyed, and the trace is the only thing left of it.

The trace has to leave the runner

Playwright writes the trace into test-results/, and the HTML reporter copies it into playwright-report/data/ as a hash-named zip, byte for byte the same file. Uploading playwright-report/ therefore takes the traces with it, and the folder goes down with the machine unless a step says otherwise. docs/ci prints that step:

- uses: actions/upload-artifact@v5
  if: ${{ !cancelled() }}
  with:
    name: playwright-report
    path: playwright-report/
    retention-days: 30
Playwright 1.62 · GitHub Actions · the block as docs/ci prints it

docs/ci pins @v5 and docs/ci-intro pins @v4, and both pages answered at the URL typed on 31 August 2026. Use whichever major the rest of your workflow already uses.

retention-days is the clock on everything above it

A trace is readable for as long as the artifact exists and not a day longer. The sample sets 30, which makes that one line the thing that decides whether a failure from March can still be looked at in June — a question that only ever arrives after the answer has been settled.

Download, extract, serve

The obvious move here fails quietly, and docs/ci-intro says why: "Locally opening the report does not work as expected as you need a web server for everything to work correctly." The report is a single-page application reading data files beside it, and a browser opening it off disk will not fetch them.

unzip playwright-report.zip -d extracted-report
npx playwright show-report extracted-report
Playwright 1.62 · shell · show-report serves the folder and opens it

Once it is served, docs/ci-intro tells you to "click on the trace icon next to the test's file name", and points at a screenshot to show you which one. In the 1.62 report it is a link reading View Trace, sitting in the failed test's row directly under the line that gives the spec file and line number. Clicking it opens the viewer with that trace loaded.

Or open the file without the report at all

When the file is already on your disk, the report is a detour. Point the viewer straight at it: npx playwright show-trace path/to/trace.zip. Where the trace has a URL of its own, because your CI publishes artifacts over HTTP or somebody dropped it in object storage, npx playwright show-trace https://example.com/trace.zip removes the download step as well.

For somebody with no Playwright install, trace.playwright.dev is a statically hosted build of the same viewer, and docs/trace-viewer describes how a file gets into it: "You can upload a trace file using drag and drop or via the Select file button." That is the route for a colleague on the product team, a client's engineer, or anyone who needs to see the failure without setting up a repository first.

The report around the trace is written by a reporter, and which reporters to configure has a page on the HTML reporter and what else you can turn on. For this purpose the trace is an attachment inside whatever the reporter wrote.

What do you look at first in a trace you have never opened?

Opened without a plan, a trace is a wall of panes and the temptation is to tour them. The moves below get from an unopened file to a cause, in this order. The numbers quoted come from a trace recorded for this article: a spec that clicks a Refund button on a page that only renders the button after a request which, on that run, never came back.

Start in Actions and find the one that ran long

"In the Actions tab you can see what locator was used for every action and how long each one took to run." The list on that trace reads Before Hooks 619ms, Navigate to "/orders" 56ms, Click 30.9s with getByRole('button', { name: 'Refund' }) under it, then After Hooks 1.2s.

Treat the duration as a diagnosis

That click ran 30.9 seconds against a 30-second test timeout, and an action that spends its whole budget was waiting for something that never turned up. An action that fails in milliseconds found something and objected to it, which is a different problem with different suspects. The Metadata tab prints test timeout: 30.0s beside the engine, the channel and the viewport, so the budget the run had is readable without opening the config. Several clocks in this framework can produce a number like that, and they word their messages differently; telling them apart is its own job.

Compare Before and After on that one action

"Use the Before and After tabs to visually see what happened before and after the action." These are not pictures: "When tracing with the snapshots option turned on (default), Playwright captures a set of complete DOM snapshots for each action." So the element your locator was hunting is either in that DOM or it is not, and you can select it, inspect it and read its attributes. A screenshot leaves you reading pixels; a DOM gives you the node or tells you it was never there.

Then Network, then Console — and not before

By this point both are narrow questions. Did the request that fills this element come back, and did anything throw. On the trace here the Console tab reads No console entries, which took one click and removed a whole category of explanation.

The last frame of the film strip is the state at the end of the run, not the state at the failure. The click on this trace gave up around the 31.6-second mark — it started at 710ms and ran for 30.9s — while the run as a whole lasted 39.7 seconds, carrying on through the after-hooks and worker cleanup. Reasoning backwards from that final image is how a reader concludes the page was empty all along.

What does a race look like in a trace?

Put the failing action's start time next to the network row for the request that fills the element it wanted. Finding two panes is easy. Side by side, those two numbers turn into a diagnosis.

On the trace behind this article the Call tab gives the click start: 710ms and duration: 30.9s. The Network tab has two rows. The document — orders, GET, 200, text/html, 2ms, 517 in the size column, starting at 666ms. Then the fetch the page makes for its own data: orders, GET, status canceled, no duration recorded, no size, starting at 682ms.

The click began 28 milliseconds after the request it depended on, and that request was still open when the browser context closed. Playwright spent the entire test timeout waiting for a button the response was going to create. The Before snapshot is where that gets confirmed: it holds the DOM as it stood when the click was called, on a page whose markup only produces the button once that response lands.

The same shape appears when the response does arrive, only late: a network row whose duration carries it past the action's start, and a Before snapshot holding stale content instead of nothing. Either way the trace has named a real race in the product, which is one of the four causes behind an intermittent spec. What to do about it, and the other three, belong to the article on why Playwright specs flake and how each cause gets fixed.

Reading one trace is an afternoon. A backlog where dozens of specs each need one is the point at which teams start looking for someone to work through a backlog of flaky specs — on that engagement, the middle phase is taking the register from the top and opening the trace for each spec on it.

What a trace records, and what recording it costs

What a trace holds is enumerable, because it is a list of options. On docs/api/class-tracing, tracing.start() takes live, name, screenshots, snapshots, sources and title. The descriptions are the account of what ends up in the file: screenshots is "Whether to capture screenshots during tracing. Screenshots are used to build a timeline preview."; snapshots, when true, captures a DOM snapshot on every action and records network activity; sources is "Whether to include source files for trace actions."

That reference page reads to its copyright line without truncating, so the list can be treated as the whole list. A trace is the browser's side of the run and nothing in the options reaches past the browser, which means your application server's logs for the same minute stay a separate document that has to be lined up by hand.

Recording has a price, and the documentation puts it inside the value's own description: 'on' is "Record a trace for each test. (not recommended as it's performance heavy)". That is why the config earlier on this page reaches for a conditional value. Size scales with what happened: the trace read above — one page, one navigation, one click and two requests — is 8,987 bytes, and a suite of three hundred specs recording on every run is a different artifact entirely. Do not extrapolate from a number that small.

The trace also has an expiry, and it is set by retention-days on the artifact rather than by anything inside Playwright.

When you should use UI mode instead

npx playwright test --ui puts the same reading surface in front of a run you are driving; docs/test-cli describes the flag as "Run tests in interactive UI mode." Its tabs carry the same words as the trace viewer's — Actions, Source, Call, Log, Errors, Console, Network, Attachments, Metadata — so the reading order above transfers whole.

UI mode is for a run you are watching. The trace viewer is for a run nobody watched. That settles which one to open: a failure you can trigger on your own machine belongs in UI mode, where you can edit the spec and watch it go again. A failure that has only ever happened on a runner leaves nothing to watch, and the trace viewer is where you go.

Questions

Why is there no trace.zip in my test results?

The trace option defaults to 'off', so a repository that has never configured it records nothing on any run. If it is already set to 'on-first-retry', look at the retries line in the same config: with retries at 0 there is no first retry, so there is no trace. For a pipeline that does not retry, 'retain-on-failure' records every run and keeps the ones that failed.

Can I open a Playwright trace without installing Playwright?

Yes. trace.playwright.dev is a statically hosted build of the same viewer, and the documentation says you can upload a trace file using drag and drop or via the Select file button. It also states that Trace Viewer loads the trace entirely in your browser and does not transmit any data externally, which is usually the sentence somebody needs before they will approve a trace leaving the network.

Should I just set trace: 'on'?

The documentation attaches its own warning to that value: "Record a trace for each test. (not recommended as it's performance heavy)". It records on every run of every test, so the cost is paid by the green builds as well as the red one. Turning it on for a day while you work out why a suite behaves oddly is reasonable; as a standing setting, 'on-first-retry' or 'retain-on-failure' leaves you the same file for the runs you would open.

What is the difference between UI mode and the trace viewer?

UI mode runs your tests while you watch, with the panes filling as they go, and it starts with npx playwright test --ui. The trace viewer replays a file recorded by a run that finished somewhere else, usually on a runner that was thrown away when the job ended. The tabs are named the same in both, so the reading skill carries across, but only one of them lets you change the spec and trigger the run again.

Send the trace and the spec name

If you have a trace open and cannot say what it is telling you, send it. The spec name and the trace file are enough for an engineer who works in Playwright and nothing else to read the run back to you.