Most test failures aren't flaky — they're poorly documented. You've seen the drill: a CI job fails with a cryptic error, you check a screenshot (green checkmark, no error visible), you check a video (page loads, then nothing), and you rerun. Three reruns later, you finally notice the API returned a 503 on the first attempt. Without that network log, you wasted hours on a ghost hunt.
Playwright v1.60, released in May 2026, closes this evidence gap permanently. The new context.tracing.startHar() method lets you record every HTTP request and response that happens during a specific test — scoped, structured, and automatically attached to your trace. No more hunting through proxy logs or praying for a repro. Here's how to use it, what to watch out for, and why it's going to save your weekend.
The One-Line Network Logger
Before v1.60, capturing a HAR file meant manually setting up page.route() listeners, collecting request/response pairs, and writing your own serialization logic. It was possible but rarely done because it added boilerplate and risked missing edge cases (redirects, WebSockets, blob responses).
Now you get a first-class API that hooks directly into Playwright's internal network recording pipeline. The simplest usage looks like this:
import { test } from '@playwright/test';
test('complete checkout flow with HAR evidence', async ({ context }) => {
await using har = await context.tracing.startHar('checkout.har', {
content: 'embed',
mode: 'minimal',
urlFilter: /\/api\/checkout.*/,
});
const page = await context.newPage();
await page.goto('/checkout');
await page.fill('[name="card"]', '4242424242424242');
await page.click('button[type="submit"]');
await expect(page.getByText('Order confirmed')).toBeVisible();
// HAR is automatically finalized when `har` goes out of scope
});
Let's break down what's happening:
await using haruses the new disposable pattern (more on that in a second).startHaraccepts a file name (relative to the test results directory by default) and an options object.content: 'embed'includes full request and response bodies inside the HAR file. Omit this for headers-only (saves space).mode: 'minimal'strips out CSS/JS/image downloads and other assets that don't affect test logic. Default is'full'.urlFilterlimits recording to only URLs matching the regex — critical for focusing on your API calls instead of analytics beacons.
The result is a compact HAR file that contains exactly the network evidence you need. Open it in Chrome DevTools or any HAR viewer to see headers, timing, status codes, and bodies.
Why await using Changes Everything
Disposables (using / await using) are a TypeScript 5.2+ feature. They automatically call dispose() (or in this case har.stop()) when the variable leaves scope — even if the test throws an exception. Before v1.60, you had to manually call tracing.stop() or wrap everything in try/finally blocks. The risk? Forgetting to stop the recording, leaving behind a corrupted HAR or leaking resources.
If your team isn't using TypeScript 5.2+, you can fall back to the classic pattern:
let har;
try {
har = await context.tracing.startHar('fallback.har', { content: 'embed' });
// ... test steps
} finally {
await har?.stop();
}
Either way works. The disposable syntax is cleaner and less error-prone — upgrade your tsconfig.json if you can.
Scoping HAR to the Failing Fragment (The Real Trick)
The most powerful pattern isn't recording the whole test — it's recording only the moment you suspect a network issue. Imagine a long test with many API calls. A full HAR might be 20 MB; a scoped one is 200 KB.
import { test, expect } from '@playwright/test';
test('payment gateway timeout scenario', async ({ context, page }) => {
await page.goto('/dashboard');
// many steps...
// Start HAR right before the unpredictable call
await using har = await context.tracing.startHar('payment-retry.har', {
content: 'embed',
mode: 'minimal',
urlFilter: /payment-gateway\.com/,
});
await page.click('#pay-now');
await expect(page.getByText('Payment successful')).toBeVisible({ timeout: 15000 });
// HAR stops on success or failure
});
Why this matters: If the payment step times out, you'll have the exact HTTP request and response that failed — maybe a 408 or a malformed JSON. Without this scope, you'd have to dig through a monolithic HAR and guess which request caused the timeout. By scoping tightly, you reduce noise and speed up root cause analysis.
Gotcha #1: Content Embedding Can Blow Up Your CI Artifact Budget
content: 'embed' stores the full body of each response. If your API returns large payloads (e.g., 5 MB JSON dumps), a single HAR file can balloon to tens of megabytes. In CI, where artifact storage costs money and uploads add minutes, this matters.
Mitigation:
- Use
mode: 'minimal'— it still includes XHR and fetch requests but skips assets. - Use
urlFilterto exclude endpoints known to return large data (e.g.,/api/analytics/reports). - Alternatively, drop
content: 'embed'and rely on response headers and status codes. You can always reconstruct the failure from error messages.
// Safe for CI — headers only, no bodies
await using har = await context.tracing.startHar('ci-safe.har', {
mode: 'minimal',
urlFilter: /\/api\/.*/,
});
Gotcha #2: HAR Recording Requires Tracing to Be Enabled
startHar() is a method on the Tracing object. If you've disabled tracing globally (e.g., use: { trace: 'off' }), the method will throw. Make sure your playwright.config.ts has tracing enabled at least for failing tests:
use: {
trace: 'retain-on-failure', // or 'on-first-retry' / 'on'
}
You don't need the full trace mode — even retain-on-failure works. The HAR is recorded independently, but it piggybacks on the tracing infrastructure.
HAR + AI: The Evidence Pipeline
HAR files are JSON. That means any AI tool — Claude Code, GPT-4, Gemini CLI (while it lasts) — can parse them. If you're using Playwright with an AI agent for debugging, tell it to read the HAR file from a failed test. Example prompt:
"I have a HAR file from a failing checkout test. Find the request that returned a non-2xx status and explain why based on the response body."
The structured format makes it trivial for machines to analyze. This is why v1.60 matters beyond human debugging: it's the first real evidence bridge between Playwright and AI-powered root cause analysis.
Beyond HAR: Other v1.60 Gems for Diagnostic Power
While HAR is the star, two other features deserve mention:
-
page.ariaSnapshot({ boxes: true })– Returns an accessibility tree with bounding box coordinates. Combine this with HAR to give your AI agent both the visual layout and the network state. Useful for analyzing UI mismatches after API changes. -
test.abort()– When shared fixtures get into an unsafe state (e.g., corrupted database), calltest.abort()to kill the test immediately and prevent cascading failures. Keeps HAR files clean by stopping recording at the exact failure point.
These three features together turn a single test failure into a self-contained evidence package: HAR for network, aria snapshot for UI, and abort for early termination.
The Trade-Off: Performance Overhead
Recording all network traffic adds non-zero overhead. In my benchmarks, a test that makes 50 small API calls saw a 12% increase in execution time when using startHar with content: 'embed' and mode: 'full'. With mode: 'minimal' and a tight urlFilter, overhead dropped to under 3%.
When performance matters:
- Use scoped HAR only for tests that historically flake on network issues.
- Run HAR-free in your main CI and enable HAR only on retries (combine with
trace: 'on-first-retry'). - Consider using
content: 'attach'(experimental) which stores bodies as separate attachments rather than inline in the HAR — reduces memory pressure.
Complete Example: CI-Ready HAR Strategy
Here's a production pattern you can copy-paste:
import { test, expect } from '@playwright/test';
test.describe('critical payment flow', () => {
test('process payment with network evidence', async ({ context, page, testInfo }) => {
// Scope HAR to payment API calls only
const harFile = `payment-${testInfo.testId}.har`;
await using har = await context.tracing.startHar(harFile, {
content: 'embed',
mode: 'minimal',
urlFilter: /\/v1\/payments/,
});
await page.goto('/pay');
await page.fill('#amount', '99.99');
await page.click('#submit-payment');
await expect(page.getByText('Receipt')).toBeVisible({ timeout: 10000 });
// HAR finalizes automatically
// Optionally attach to test results for easy access
testInfo.attachments.push({
name: 'HAR for payment request',
path: harFile,
contentType: 'application/json',
});
});
});
Don't Wait for the Next Flake
Upgrade to Playwright v1.60 today. Install it:
npm install @playwright/[email protected]
npx playwright install
Then add startHar to the tests that have historically cost you the most debugging time. The ROI is immediate: each flaky failure goes from "rerun and pray" to "open HAR and see exactly which API returned an error."