Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 023 / 100

Debugging with the trace viewer: lessons from production

See exactly what happened in a failed test.

Expona Admin6 min read

A test passes in your local environment but fails silently in CI, leaving you with just a screenshot and a stack trace. You're stuck guessing what went wrong. This is where debugging with the trace viewer: lessons from production changes everything. Playwright's trace viewer gives you a complete record of every DOM snapshot, network request, and console message—so you can rewind time and see exactly what happened.

Why traces matter in production debugging

In our experience across trading platforms and e-commerce systems, most test failures don't come from broken assertions—they come from timing issues, stale page state, or environmental differences between your machine and CI. A screenshot captures one moment. A trace captures the entire journey.

When a test fails in CI, you typically get:

  • A screenshot at the end (often not helpful for understanding what went wrong)
  • A stack trace pointing to the line that failed
  • Maybe a log message if you're logging

What you don't get is the context: Was the element actually in the DOM when we clicked it? Did a network request hang? Did a modal cover the button? With traces, you have all of it, recorded step by step.

Record traces with on-first-retry

Enabling traces in Playwright is straightforward, but the key decision is when to record them. Recording on every test run creates massive files and slows down your suite. We recommend the on-first-retry strategy: only record traces when a test fails and needs to be retried.

Add this to your Playwright config:

typescript
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  webServer: undefined,
});

The on-first-retry setting means:

  • Test passes on first attempt → no trace recorded (fast)
  • Test fails on first attempt → trace recorded automatically before retry
  • Retry passes → you have the trace from the failure
  • Retry fails → you have the trace to debug further

This keeps your artifact storage lean while capturing exactly what you need. In a CRM system we worked on, switching to on-first-retry cut trace storage by 85% while maintaining full visibility into failures.

If you're running tests locally and want to always capture traces, you can override this per test or per run:

typescript
test.use({ trace: 'on' });

test('payment flow with full trace', async ({ page }) => {
  await page.goto('/checkout');
  await page.fill('input[name="card"]', '4111111111111111');
  await page.click('button:has-text("Pay")');
});

Open and explore traces

After a test fails in CI, the trace file is downloaded as part of your test artifacts. To view it locally, run:

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

This opens the Playwright Inspector—a web-based UI that shows the entire test execution as an interactive timeline.

Navigate through the trace using:

  • Timeline: A bar at the bottom showing every action and network request
  • Action log: Left sidebar listing each step (goto, click, fill, etc.)
  • DOM viewer: Center shows the DOM state at each step
  • Inspector panel: Right sidebar with network, console, and source details

Click any action in the timeline and the DOM viewer shows exactly how the page looked at that moment. This is invaluable when debugging race conditions or flaky waits.

Understand what the trace captures

A trace includes four key pieces of information at each step:

DOM snapshots

For every action, Playwright captures a snapshot of the DOM. You can see the exact HTML, CSS, and computed styles. If a button is hidden with display: none or positioned off-screen, you'll see it immediately. In a ride-hailing app we tested, we caught a flaky test caused by an element with visibility: hidden—something a screenshot would never reveal.

Network activity

The trace records all network requests and responses. If an API call returns an error or takes too long, you see it in the timeline. In an SaaS dashboard test, we discovered that a particular endpoint was returning a 500 error intermittently, only visible in the trace. The logs had hidden it.

Console output

JavaScript errors, warnings, and custom logs appear in the trace. If your application logs errors to the console that aren't surfaced in the UI, the trace will show them. In a Next.js app, we caught hydration mismatches this way.

User actions

Every action is timestamped: goto, click, fill, waitForLoadState, etc. You can see exactly when each action happened and how long it took. If a click happened but nothing changed, the trace shows why.

Share traces from CI as artifacts

To make traces accessible to your team, configure your CI to upload them after test runs. Here's an example for GitHub Actions:

typescript
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm install
      - run: npm run test
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30
      - uses: actions/upload-artifact@v3
        if: always()
        with:
          name: playwright-traces
          path: test-results/
          retention-days: 30

After the test run, download the artifact from the GitHub Actions summary page. Extract the .zip file and open it with npx playwright show-trace.

For larger teams, consider uploading traces to cloud storage (S3, GCS) with pre-signed URLs so engineers can view them without downloading artifacts. In one trading platform we worked on, this reduced debugging time by half—engineers could click a link from Slack and inspect the trace immediately.

A real-world scenario: debugging a flaky checkout

Let's walk through a production issue we debugged using traces.

The problem: A checkout test passes 95% of the time but occasionally fails on the payment step. The error message is vague: "Element not clickable."

Without traces: We'd add more waits, maybe retry logic, and hope it works. We'd never know the root cause.

With traces: We open a trace from a failed run and see the timeline. At step 15, the test clicks the "Pay Now" button. The action takes 3 seconds to complete, and during that time, a success modal starts rendering. By the time the modal fully renders, the page has already navigated away, but the trace shows a console error: "Navigation happened while processing click."

The fix: ensure the navigation promise resolves before checking for the success message.

typescript
await Promise.all([
  page.waitForNavigation(),
  page.click('button:has-text("Pay Now")')
]);
await expect(page).toHaveURL('/checkout/success');

Without the trace, we'd have added arbitrary waits and never understood why the test was flaky.

Common mistakes

  • Recording traces on every test: Wastes storage and slows down CI. Stick with on-first-retry unless you're debugging a specific issue.
  • Not configuring artifact retention: Traces expire and are deleted before you have time to review them. Set retention to at least 30 days in CI.
  • Forgetting to check console errors: The trace shows console output, but many developers skip it. Always check the console tab in the inspector—it often reveals the real issue.
  • Using traces instead of fixing the real problem: Traces are for debugging, not working around issues. If a test is flaky, the bug is in your test or your app. Traces help you find it; then fix it properly.
  • Not sharing trace links with the team: Traces are only useful if your team knows how to access them. Document the process and share traces in incident channels.

Key takeaways

  • Enable trace: 'on-first-retry' in your Playwright config to capture traces only when tests fail and retry, keeping storage lean.
  • Open traces with npx playwright show-trace trace.zip to get a complete, interactive record of DOM snapshots, network requests, console output, and user actions at each step.
  • Use the DOM viewer and timeline to replay the test and see exactly what the page looked like when an action failed.
  • Configure CI to upload traces as artifacts and set retention to at least 30 days so your team can review failures after they happen.
  • Check console errors and network activity in the trace—they often reveal the root cause faster than guessing or adding more waits.
  • Traces are a tool for debugging the real issue, not for working around flakiness. Once you find the problem, fix it in your test or your application.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles