Resource library

QA How-To

Playwright trace on retry: Examples and Best Practices

Use practical Playwright trace on retry examples for CI, local debugging, mode selection, attachments, flaky tests, and secure artifact handling for QA teams.

22 min read | 2,655 words

TL;DR

A production-ready Playwright trace on retry example combines top-level CI retries, `use.trace`, an HTML report, and unconditional artifact upload. Start with `on-first-retry`, use `retain-on-first-failure` when the original attempt matters, enrich traces with safe steps and attachments, and never use retries as a substitute for deterministic state.

Key Takeaways

  • Use a CI-aware config so retries and first-retry traces activate together while local feedback stays fast.
  • Select trace mode by the attempt you need to preserve, especially the initial failure versus a retry.
  • Add small sanitized attachments and named test steps to make a trace explain business context.
  • Upload reports with `if: always()` and unique matrix names so failed jobs do not discard evidence.
  • Run a focused CLI trace locally when no retry artifact exists or a CI issue needs direct reproduction.
  • Convert trace observations into a root-cause statement and a deterministic test or product fix.

The Playwright trace on retry examples in this guide move beyond the one-line setting. They show a CI-aware configuration, focused file overrides, command-line diagnosis, useful attachments, artifact upload, retry-chain comparison, and a repeatable investigation method. All examples use current Playwright Test options available in 2026.

A trace is valuable only if it captures the right attempt, survives the CI job, and gives an engineer enough context to identify the earliest wrong state. Copy the smallest pattern that fits your suite, then adapt retention and privacy controls to your application.

TL;DR

Need Configuration Attempt captured
Lean CI default trace: 'on-first-retry' Retry index 1 only
Initial failure evidence trace: 'retain-on-first-failure' Index 0 if it fails
Every failed attempt trace: 'retain-on-failure' Each run that fails
Compare failure and retries trace: 'retain-on-failure-and-retries' Failures plus every retry
Focused local capture CLI --trace=on Every selected local run
Library script context.tracing.start() and stop() Manually controlled context

The baseline config is:

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

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: { trace: 'on-first-retry' },
});

Remember that retry index 1 exists only after index 0 fails. If retries are disabled, the first-retry mode records nothing.

1. Minimal Playwright trace on Retry Examples

A small project needs only two coordinated settings. retries is a top-level runner option, while trace is a browser-context option inside use:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  retries: 1,
  reporter: [['html', { open: 'never' }]],
  use: {
    trace: 'on-first-retry',
  },
});

Run any normal test with npx playwright test. A test that passes initially produces no trace. A test that fails on its initial run is executed once more, and that retry gets the trace. Open the HTML report with npx playwright show-report, select the flaky or failed test, and open the trace attachment.

For most teams, make retries CI-specific:

const inCI = Boolean(process.env.CI);

export default defineConfig({
  retries: inCI ? 2 : 0,
  use: { trace: 'on-first-retry' },
});

Local failures then stop immediately, which keeps development feedback direct. CI gets retry evidence for intermittent failures. When an engineer needs a local trace, a CLI override can record the selected test without permanently changing suite policy.

Do not infer that the retry is the same process continuing. Playwright starts a new worker after failure. Browser and worker-scoped state are recreated, while records in databases or remote systems may remain. That difference is often the key to interpreting the trace.

2. A Runnable Retry Demonstration

This laboratory test intentionally fails on index 0 and passes on index 1. Use it only to validate trace collection, then remove it. Production tests must not change expected product behavior based on retry count.

// tests/retry-trace-demo.spec.ts
import { test, expect } from '@playwright/test';

test('creates a trace for retry index one', async ({ page }, testInfo) => {
  await page.setContent(`
    <main>
      <h1>Deployment status</h1>
      <p role='status'>${testInfo.retry === 0 ? 'Starting' : 'Healthy'}</p>
    </main>
  `);

  await testInfo.attach('attempt.json', {
    body: Buffer.from(JSON.stringify({ retry: testInfo.retry })),
    contentType: 'application/json',
  });

  await expect(page.getByRole('status')).toHaveText('Healthy');
});

Run it and open the report:

npx playwright test tests/retry-trace-demo.spec.ts --project=chromium
npx playwright show-report

The overall test is flaky because its first run failed and its retry passed. Under on-first-retry, the trace belongs to the passing retry and shows Healthy. It does not show the initial Starting page. The attempt attachment confirms retry index 1. This is a useful verification of semantics that many teams misunderstand.

To compare behavior, switch temporarily to retain-on-failure-and-retries. The report can then retain the initial failed attempt and the retry. Restore the intended production policy after the exercise.

3. CI-Ready Configuration With Rich Trace Contents

Use the object form when you want the mode to be obvious and need control over trace features:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  outputDir: 'test-results',
  retries: process.env.CI ? 2 : 0,
  forbidOnly: Boolean(process.env.CI),
  reporter: [
    ['line'],
    ['html', { outputFolder: 'playwright-report', open: 'never' }],
  ],
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    trace: {
      mode: 'on-first-retry',
      screenshots: true,
      snapshots: true,
      sources: true,
      attachments: true,
    },
    screenshot: 'only-on-failure',
    video: 'off',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});

Screenshots build the trace filmstrip. Snapshots make the DOM around each action inspectable. Sources connect the action to test code. Attachments preserve items created by the test and runner. The normal defaults are already useful, so object configuration is not required unless it communicates or changes policy.

Choose whether to record video separately. A trace usually provides stronger debugging evidence than a video, but video can help with continuous motion, complex drag interactions, or behavior outside useful DOM snapshots. Recording both on every attempt increases artifact volume. A risk-based retry policy is a better starting point.

Pin the Playwright package with the lockfile and install matching browser binaries in CI. A trace from one revision should be analyzed alongside that run's exact code and environment metadata.

4. Mode Selection Examples by Failure Pattern

Different systems fail in different ways. Use mode selection to capture the evidence your failure model needs.

Example A: retry usually reproduces the issue

use: { trace: 'on-first-retry' }

This is efficient for a large suite where intermittent UI and service conditions often remain visible on the next attempt. Successful original runs have no recording overhead.

Example B: the first attempt mutates external state

use: { trace: 'retain-on-first-failure' }

This records the original attempt and keeps it if it fails. It is strong for create, payment, booking, and queue workflows where the retry might see duplicate or partial state.

Example C: compare every retry

use: { trace: 'retain-on-failure-and-retries' }

This records every run and retains any failed run plus all retry attempts. It provides the richest chain when an investigator must compare network or page state across attempts, but it consumes more resources.

Failure pattern Preferred starting mode Reason
Rare CI timing issue on-first-retry Low routine cost
Non-idempotent workflow retain-on-first-failure Preserves first mutation path
Multi-attempt investigation retain-on-failure-and-retries Keeps comparison chain
Retries disabled retain-on-failure Retains failed original run
One local reproduction CLI --trace=on No config edit needed

Review the policy after collecting real data. The default should serve the common failure, while teams can temporarily use a richer project or CLI mode for a focused investigation.

5. File-Level and Command-Line Overrides

Some test groups have a different risk profile. A payment spec may need original-failure evidence even if the general suite uses first-retry tracing. test.use() can scope the option to that file or describe block:

import { test, expect } from '@playwright/test';

test.use({ trace: 'retain-on-first-failure' });

test('shows a declined payment message', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay now' }).click();
  await expect(page.getByRole('alert')).toContainText('Payment declined');
});

Keep overrides near an explanation of why this workflow differs. Avoid scattered settings that make artifact behavior unpredictable. A separate Playwright project for sensitive or stateful tests can be clearer if many files share the same policy.

For a one-off local diagnosis, use the CLI:

npx playwright test tests/payments.spec.ts \
  --grep 'declined payment' \
  --workers=1 \
  --trace=on

The CLI also accepts the supported retry and retention trace modes. A focused command avoids recording the whole suite. Do not commit a global trace: 'on' change after finishing the investigation.

UI Mode is often better for local test development because it offers interactive execution and inspection. A saved trace remains better for asynchronous CI handoff and evidence tied to one completed attempt.

6. Add Business Context With Steps and Attachments

Trace Viewer is easier to read when technical calls are grouped into business actions. Use test.step() for meaningful phases, and attach small sanitized context through testInfo.attach():

import { test, expect } from '@playwright/test';

test('submits an expense claim', async ({ page }, testInfo) => {
  const claimId = 'CLM-visual-1042';
  await testInfo.attach('claim-context', {
    body: Buffer.from(JSON.stringify({ claimId, employee: 'test-user-7' })),
    contentType: 'application/json',
  });

  await test.step('create claim', async () => {
    await page.goto('/claims/new');
    await page.getByLabel('Amount').fill('125.00');
    await page.getByLabel('Description').fill(claimId);
    await page.getByRole('button', { name: 'Submit claim' }).click();
  });

  await test.step('verify submitted state', async () => {
    await expect(page.getByRole('status')).toHaveText('Submitted');
    await expect(page.getByText(claimId)).toBeVisible();
  });
});

The attachment provides a correlation value without dumping credentials or an entire response. The steps communicate intent, which helps the investigator navigate a long action list. Keep step boundaries meaningful and avoid wrapping every click in a redundant label.

Attachments can contain buffers or files. Sanitize them before attachment, cap size, and use synthetic data. If backend logs are external, attach a request or trace ID rather than copying a sensitive log stream. The Playwright test attachments guide can support a consistent suite convention.

7. Upload Retry Traces in GitHub Actions

A CI test command exits nonzero on final failure, so artifact upload must run regardless of earlier step status:

name: Playwright checks

on:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test --project=chromium
      - name: Upload Playwright evidence
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-evidence-${{ github.run_attempt }}
          path: |
            playwright-report/
            test-results/
          if-no-files-found: ignore
          retention-days: 14

The report and result paths must match your configuration. Upload both because reports can reference result attachments, and direct archives may be useful outside the report. For a browser matrix or shard, include ${{ matrix.browser }} or the shard number in each artifact name to prevent collisions.

The example retention is illustrative. Use the shortest period that meets investigation and audit needs. Protect downloads with repository permissions and avoid production user data. If the application puts tokens in URLs or page content, fix that exposure rather than relying on the trace viewer to hide it.

For broader pipeline patterns, see GitHub Actions for Playwright.

8. Read a Retry Trace With a Repeatable Checklist

Do not click randomly through the timeline. Start with the result metadata: project, retry index, duration, worker, deployment, and error. Confirm whether you are viewing the initial run or retry. Under on-first-retry, a passing trace belongs to the retry and may show a healthy flow after the original failure.

Then follow this sequence:

  1. Select the failed assertion or last relevant action.
  2. Read its call log and locator resolution details.
  3. Compare before and after DOM snapshots.
  4. Move backward to the earliest page state that violates expectations.
  5. Correlate that time with network requests and console errors.
  6. Read safe attachments for record IDs and environment context.
  7. Compare against another attempt if the chosen mode retained one.
  8. Write a hypothesis that explains all observed evidence.

For example, a trace might show that the retry passed because a previously created invoice already existed. That is not an application timing success. It means the original attempt left external state, and the fixture or API needs idempotent cleanup or unique data.

If a locator waits for an element, inspect actionability rather than adding a timeout. If a response is slow, check whether the UI contract should wait on a visible state or whether the service breached an expectation. The Playwright auto-waiting guide helps separate built-in actionability from application readiness.

9. Compare Retry Attempts Programmatically and Operationally

A trace is optimized for human investigation, while reporters and test metadata support trend detection. Track flaky classifications, test identifiers, project names, durations, retry counts, and failure signatures over time. Do not parse internal trace archive files as a stable analytics API. Use reporter interfaces and published result metadata for automation.

Within a test, attach correlation details that remain constant across attempts and attempt details that vary:

test('uses isolated tenant data', async ({ page }, testInfo) => {
  const tenant = `tenant-${testInfo.parallelIndex}`;
  await testInfo.attach('execution-context', {
    body: Buffer.from(JSON.stringify({
      tenant,
      project: testInfo.project.name,
      retry: testInfo.retry,
    })),
    contentType: 'application/json',
  });

  await page.goto(`/tenants/${tenant}/dashboard`);
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

The exact data strategy depends on the environment. A parallel index alone may not be globally unique across CI jobs, so include a run identifier in real seed fixtures. The key principle is to make the relationship between attempts observable without changing the acceptance criteria on retry.

Use flaky-rate trends to prioritize investigation, but avoid arbitrary retry counts as a success metric. One flaky payment test can carry more release risk than many low-impact tests. Combine frequency, user impact, ownership, and trace evidence.

10. Turn Trace Evidence Into a Fix

A trace investigation should end in a change that makes the test or product more deterministic. Common evidence-to-fix mappings include:

Trace evidence Weak reaction Strong fix
Button covered by cookie banner Force the click Model banner acceptance or test saved consent
Dashboard waits after 401 Increase timeout Fix authentication state or token lifecycle
Duplicate order on retry Add another retry Generate isolated idempotent test data
Skeleton still visible Add waitForTimeout() Assert the product's ready state
Locator matches two controls Use .first() blindly Select by role and accessible name
CI requests rate-limited Slow all tests Isolate accounts, reduce contention, or provision capacity

Write the root cause in causal language: because X occurred, the system entered Y, so assertion Z failed. Link the exact trace, request ID, or snapshot. Verify the proposed fix with repeated focused runs and then with normal parallel CI settings.

A retry policy should surface instability, not institutionalize it. Consider failing CI on flaky results if the runner and team policy support that gate, or at minimum create owned work automatically. Quarantine only when the release-risk tradeoff is explicit, time-bounded, and visible.

11. Best Practices for Playwright trace on Retry Examples

Keep the default simple and understandable. A new engineer should be able to predict which attempts have traces from the configuration alone. Use richer modes temporarily for a focused project instead of burdening every test forever. Keep retries low and never condition normal assertions on retry count.

Preserve enough context to reproduce: source revision, application deployment, browser project, Playwright version, shard, base environment, and synthetic data identifiers. Use named steps sparingly and attachments intentionally. Ensure failed jobs still publish evidence.

Treat traces as sensitive. They can contain user-visible text, requests, source, and attachments. Use nonproduction data, restrict artifact access, and expire archives. Review whether sources and attachments are appropriate for the repository.

Finally, validate your artifact path with an intentional laboratory failure before relying on the workflow. Many teams configure tracing correctly but lose the archive because the report directory is not uploaded, matrix artifacts overwrite each other, or cleanup runs first. A five-minute controlled check proves the full evidence chain.

Interview Questions and Answers

Q: Provide a production-ready trace-on-retry config.

I set CI-only retries at the config root, set trace: 'on-first-retry' inside use, enable an HTML reporter, and use a stable output directory. CI uploads the report and result directories with an always condition. I also record the commit, deployment, project, and shard so the trace has environment context.

Q: How would you prove that on-first-retry captures index 1?

I create a temporary lab test that deliberately fails at testInfo.retry === 0 and passes at index 1, with retry metadata attached. The report shows the trace on the retry and no trace on the original attempt. I remove the artificial test after validating the pipeline.

Q: How do you handle a workflow that changes server state before failing?

I prefer retain-on-first-failure during investigation so the original mutation path is captured. I make data creation unique or idempotent and add cleanup that owns its records. Retries should not encounter partial state from a previous attempt.

Q: What makes a useful trace attachment?

A useful attachment is small, sanitized, and correlates browser evidence with the business or backend operation. Examples include a synthetic record ID, deployment ID, or redacted response summary. Raw secrets and huge logs do not belong in the trace.

Q: Why use test steps in a trace?

Named steps group low-level browser calls into business phases such as create claim and verify approval. This accelerates navigation and communicates intent. I keep steps meaningful rather than wrapping every action, because excessive nesting adds noise.

Q: How do you debug a passing retry trace when the initial run failed?

I confirm that the trace belongs to index 1, then look for state that could have been created by index 0. I correlate record IDs, network responses, and deployment logs. If the retry is completely healthy, I temporarily switch to an original-failure retention mode to capture the missing evidence.

Q: How do you stop retries from hiding flaky tests?

I track flaky outcomes separately from clean passes, assign ownership, keep retry counts low, and use trace evidence to open a root-cause fix. Where appropriate, CI fails on flakiness. A retry is a diagnostic execution, not an implicit wait.

Common Mistakes

  • Copying on-first-retry without enabling at least one retry in the same environment.
  • Using an intentional retry-dependent demonstration as a permanent product test.
  • Expecting the first-retry trace to explain an unrecorded original mutation.
  • Adding huge response bodies and credentials as trace attachments.
  • Uploading artifacts only on success or cleaning result directories before upload.
  • Giving every matrix job the same artifact name and losing evidence through collisions.
  • Leaving a local --trace=on policy as a permanent global suite setting.
  • Creating dozens of meaningless test.step() wrappers that obscure the timeline.
  • Parsing the internal trace ZIP format for long-term analytics instead of using reporter data.
  • Increasing timeouts or retries before checking locator, network, and state evidence.
  • Sharing trace archives without considering DOM, request, source, and attachment privacy.

Conclusion

These Playwright trace on retry examples form a complete evidence pipeline: select the correct attempt, enrich it with safe context, retain it after failure, and analyze it with a consistent checklist. on-first-retry is a lean default, while the retention modes let you capture the original failure or the whole retry chain when necessary.

Validate the workflow with a temporary controlled failure, then use the first real trace to produce a causal defect or test fix. The goal is not a larger archive collection. The goal is fewer unexplained retries and a suite whose outcomes remain stable under normal CI conditions.

Interview Questions and Answers

How would you implement trace-on-retry in a CI pipeline?

I configure CI-only retries, set first-retry tracing in `use`, produce an HTML report, and upload both report and result directories with an unconditional step. Artifact names include matrix or shard identity. Access and retention follow test-data policy.

When would you override tracing for one file?

I use a scoped override when a group has a distinct state or risk model, such as payment tests that need original-failure evidence. I document the reason and consider a separate project if many files share it. Predictability matters more than clever configuration.

How do test steps improve trace analysis?

They group calls by business intent and make long traces easier to navigate. A step such as submit expense claim is more useful than a flat sequence of fills and clicks. I avoid one-action steps because they duplicate information.

What would you attach to a retry trace?

I attach sanitized correlation context: synthetic entity ID, environment, deployment, project, and retry index. I keep it small and machine-readable. Sensitive payloads and full server logs remain in protected systems referenced by an ID.

How do you validate that CI preserves traces?

I run a temporary controlled test that fails initially and exercises the configured retry. I confirm the report classification, trace attachment, uploaded artifact paths, matrix naming, and download usability. Then I remove the artificial failure.

What is the strongest response to a duplicate-state failure on retry?

Capture the original attempt, prove where state was created, and make the fixture data unique or the operation idempotent. Additional retries only create more duplicates. Cleanup should own exactly the records created by that test.

How do you communicate a trace-derived root cause?

I state the expected state, observed state, earliest divergence, and evidence. Then I explain the causal chain and proposed fix. A trace link alone is not a diagnosis, and a failed final assertion alone is rarely the root cause.

Frequently Asked Questions

What is the simplest Playwright on-first-retry example?

Set `retries: 1` at the config root and `use: { trace: 'on-first-retry' }`. Run the test normally, then open the retry trace from the HTML report if the initial attempt fails.

Can I enable Playwright tracing for one test file?

Yes. Call `test.use({ trace: 'retain-on-first-failure' })` at file or describe scope. Keep the override documented so engineers can predict which attempt is recorded.

How do I force a local Playwright trace?

Run a focused command with `--trace=on`, preferably with a test file or grep filter and one worker. This overrides configuration for that command without requiring a permanent suite change.

What should I upload from CI for Playwright traces?

Upload the configured HTML report and test result directories even when tests fail. Use unique artifact names for shards or matrix jobs and protect archives according to their potentially sensitive contents.

Can testInfo attachments appear in Playwright traces?

Yes, when trace attachments are enabled. Attach only small sanitized context such as test record IDs or deployment identifiers, and avoid secrets or unbounded logs.

Which trace mode should I use with no retries?

Use `retain-on-failure` to record attempts and keep only failed ones, or `retain-on-first-failure` for the original run. `on-first-retry` cannot produce a trace when no retry exists.

How do I compare the initial Playwright failure with its retry?

Use `retain-on-failure-and-retries` during the focused investigation. It keeps failed runs and retry attempts, allowing you to compare state, requests, and actions across the chain.

Related Guides