tracing.startHar() — Network & DOM in One Trace
Stop juggling separate HAR files and trace files. Playwright 1.60 folds HAR recording into the tracing API so you debug network and UI failures in a single window.
📖 The Story: The Flaky Checkout
A QA team had a test that passed 95% of the time but failed randomly in CI. The failure was always the same: the checkout button was clicked, but the success page never appeared. The team spent two weeks trying to reproduce it locally — it never failed on a developer’s machine.
When they added tracing, they saw the click happen. When they added a separate HAR recording, they saw the API call. But the two artifacts were in different files with different timestamps, and correlating “did the click happen before or after the API timeout?” required manual math across two tools.
The root cause turned out to be a race condition: the checkout API had a 4-second timeout, and the CI runner was slow enough that the page’s loading spinner sometimes overlapped with the API call, causing a double-submit guard to fire. The team only found this after a senior engineer manually aligned the HAR timestamps with the trace screenshots — a process that took 40 minutes per failure.
If the HAR and the trace had been one artifact, the engineer would have seen the overlap in 4 seconds.
🎯 Why It Matters
One Artifact
HAR and trace live in the same workflow. No more juggling two files with different timestamps.
Auto-Scoped
await using finalizes the HAR when the scope exits. No manual stop call, no leaked recordings.
Filtered
Same content, mode, and urlFilter options as recordHar. Capture only /api/** calls, skip static assets.
Debuggable
Open the trace in Trace Viewer and see network calls inline with DOM actions, screenshots, and console logs.
🌍 Real World Example
An e-commerce team runs a checkout test that hits 12 API endpoints. When it fails, they need to know which endpoint timed out. Before 1.60, they configured recordHar on the BrowserContext, which captured every network call including analytics pings and CDN asset fetches — the HAR file was 8 MB and took 30 seconds to open in Chrome’s HAR viewer. With tracing.startHar('trace.har', { urlFilter: '**/api/**' }), the HAR is 200 KB, contains only the 12 API calls, and opens inside the Trace Viewer alongside the DOM timeline.
🧒 Explain Like I'm 10
Imagine you’re a detective investigating a crime. You have a video camera that records what people do (the trace), and you have a microphone that records what people say (the HAR).
Before, the camera and microphone were on different clocks. When you played them back, the lip-sync was off, and you couldn’t tell if someone said “fire!” before or after the window broke.
Now, the camera and microphone are built into the same device with the same clock. When you play it back, everything lines up perfectly.
🎓 Professional Explanation
HAR (HTTP Archive) is a JSON-formatted log of all network requests a browser makes. Playwright has supported HAR recording since 1.0 via the recordHar context option, but it was configured separately from tracing. This meant network captures and DOM/action captures lived in different files with different timestamp origins, making correlation manual and error-prone.
Playwright 1.60 introduces tracing.startHar() and tracing.stopHar(), which fold HAR recording into the tracing API. They accept the same content, mode, and urlFilter options as recordHar, but the recording is scoped to a tracing context and can be used with the await using disposable pattern (requires Node 20.17+) for automatic finalization.
📊 The Flow
HAR Recording on Tracing Flow
⚙️ The tracing.startHar() API
The API lives on context.tracing and returns a Disposable that finalizes the HAR when it goes out of scope. You need Node 20.17+ for the await using syntax; on older Node, call stopHar() explicitly.
import { test } from class="tk-str">'@playwright/test'; test(class="tk-str">'checkout API debugging', async ({ context }) => { class=class="tk-str">"tk-com">// Start HAR recording scoped to this test await using har = await context.tracing.startHar(class="tk-str">'trace.har', { urlFilter: class="tk-str">'**/api/**', }); const page = await context.newPage(); await page.goto(class="tk-str">'https:class="tk-com">//storedemo.testdino.com'); await page.getByTestId(class="tk-str">'header-menu-all-products').click(); class=class="tk-str">"tk-com">// HAR is finalized when `har` goes out of scope. await expect(page).toHaveURL(/.*products/); });
The await using keyword (Node 20.17+) creates a Disposable. When the test function exits — normally or via exception — the HAR is automatically finalized and written to trace.har. No manual stopHar() call needed.
class=class="tk-str">"tk-com">// Filter by URL to capture only API calls (skip static assets) await using har = await context.tracing.startHar(class="tk-str">'api.har', { urlFilter: class="tk-str">'**/api/**', mode: class="tk-str">'minimal', class=class="tk-str">"tk-com">// omit request/response bodies for smaller files }); class=class="tk-str">"tk-com">// Or capture everything (default) await using fullHar = await context.tracing.startHar(class="tk-str">'full.har', { content: class="tk-str">'embed', class=class="tk-str">"tk-com">// embed response bodies in the HAR });
urlFilter accepts a glob pattern. mode: 'minimal' omits request/response bodies for smaller HAR files. content: 'embed' includes bodies inline (useful for debugging JSON API responses).
class=class="tk-str">"tk-com">// Without await using (Node 18 compatible) test(class="tk-str">'legacy node version', async ({ context }) => { await context.tracing.startHar(class="tk-str">'trace.har', { urlFilter: class="tk-str">'**/api/**', }); const page = await context.newPage(); await page.goto(class="tk-str">'https:class="tk-com">//app.example.com'); class=class="tk-str">"tk-com">// Manually finalize the HAR await context.tracing.stopHar(); });
If you’re on Node 18 and can’t use await using, call startHar() without assigning the result, and call stopHar() at the end. You lose the auto-finalize-on-exception safety, so wrap the test body in try/finally if you need cleanup on failure.
🔄 Before vs After
Before: Parallel recordHar on BrowserContext (1.59 and earlier)
class=class="tk-str">"tk-com">// playwright.config.ts (Playwright 1.59 and earlier) export default defineConfig({ use: { contextOptions: { recordHar: { path: class="tk-str">'network.har', urlFilter: class="tk-str">'**/api/**' }, }, }, });
The HAR file had no connection to your trace files. You opened the HAR in Chrome’s HAR viewer and the trace in Trace Viewer, then manually correlated timestamps. Network debugging lived in one place; DOM and action history in another.
After: tracing.startHar() with await using (1.60+)
import { test } from class="tk-str">'@playwright/test'; test(class="tk-str">'checkout API debugging', async ({ context }) => { await using har = await context.tracing.startHar(class="tk-str">'trace.har', { urlFilter: class="tk-str">'**/api/**', }); const page = await context.newPage(); await page.goto(class="tk-str">'https:class="tk-com">//storedemo.testdino.com'); await page.getByTestId(class="tk-str">'header-menu-all-products').click(); class=class="tk-str">"tk-com">// HAR is finalized when `har` goes out of scope. await expect(page).toHaveURL(/.*products/); });
HAR and trace are now one workflow. Open trace.zip in Trace Viewer and you’ll see network calls inline with DOM actions, screenshots, and console logs — all on the same timeline. Filter with urlFilter to capture only the API calls you care about.
await using requires Node 20.17 or newer. The Disposable pattern that auto-finalizes the HAR is a Node 20 feature. On Node 18, use the explicit startHar() / stopHar() form wrapped in try/finally. CI runners pinned to Node 18 LTS will need an upgrade before adopting this pattern.
⚠️ Common Mistakes
Mistake 1: Using await using on Node 18
The await using keyword is a Node 20.17+ feature. On Node 18, the test will fail with a syntax error. Either upgrade Node or use the explicit startHar() / stopHar() form.
class=class="tk-str">"tk-com">// Node 18 — will throw SyntaxError await using har = await context.tracing.startHar(class="tk-str">'trace.har'); class=class="tk-str">"tk-com">// Node 18 compatible await context.tracing.startHar(class="tk-str">'trace.har'); class=class="tk-str">"tk-com">// ... test body ... await context.tracing.stopHar();
Mistake 2: Forgetting to start tracing itself
startHar() records network calls, but it does not start the trace. If you want DOM actions and screenshots in the same artifact, you also need context.tracing.start({ snapshots: true, screenshots: true }). The HAR recording layers on top of the trace.
class=class="tk-str">"tk-com">// Start tracing FIRST, then layer HAR on top await context.tracing.start({ snapshots: true, screenshots: true }); await using har = await context.tracing.startHar(class="tk-str">'trace.har', { urlFilter: class="tk-str">'**/api/**', });
Mistake 3: Not filtering the HAR
Without urlFilter, the HAR captures every network call including analytics pings, CDN asset fetches, and WebSocket handshakes. A 200 KB API-only HAR becomes an 8 MB mess that takes 30 seconds to open. Always filter: urlFilter: '**/api/**'.
✅ Best Practices
- Always pass
urlFilterto capture only the API calls you care about. A filtered HAR is 10–40x smaller and opens instantly. - Use
await using(Node 20.17+) for auto-finalization — it prevents leaked recordings when tests throw. - Start tracing first, then layer HAR on top.
startHar()records network;start()records DOM actions. You need both for full debugging. - Use
mode: 'minimal'for CI runs to omit request/response bodies and keep the HAR small. - Use
content: 'embed'for local debugging to include response bodies inline (useful for inspecting JSON API responses). - Combine with
--trace=retain-on-failureso the HAR + trace is only kept for failing runs.
🏗️ Senior Engineer Deep Dive
Why HAR and trace were separate before 1.60
Playwright’s tracing system and its HAR recording system evolved independently. Tracing was built for DOM/action forensics — screenshots, click coordinates, console logs. HAR recording was bolted on via the BrowserContext’s recordHar option, which predated tracing. The two systems used different timestamp origins and different file formats, so correlating “did the API call happen before or after the click?” required manual math. tracing.startHar() unifies the timestamp origin so network calls appear on the same timeline as DOM actions in Trace Viewer.
When to use HAR tracing vs route interception
page.route() is for mocking network calls — you intercept a request and return a fake response. tracing.startHar() is for recording real network calls for later debugging. They are complementary: use route() to make tests deterministic, use startHar() to debug why a test that should pass is failing. You can combine them — route-mocked calls appear in the HAR with a note that they were fulfilled by Playwright.
💼 Interview Questions
Show Answer
Show Answer
Show Answer
Show Answer
🏋️ Practical Exercise
Your First HAR-in-Trace
- Ensure Node 20.17+ is installed:
node --version - Create a test that navigates to a public site like
https://news.ycombinator.com - Add
await using har = await context.tracing.startHar('news.har', { urlFilter: '**/*' }) - Also start tracing:
await context.tracing.start({ snapshots: true, screenshots: true }) - Click a few links, then stop tracing:
await context.tracing.stop({ path: 'trace.zip' }) - Open
trace.zipin Trace Viewer (npx playwright show-trace trace.zip) and verify network calls appear inline
🚀 Mini Project
🏗️ The API Debugging Suite
- Pick a flaky test in your suite that involves API calls
- Add
tracing.startHar()withurlFilter: '**/api/**'to the test - Configure
--trace=retain-on-failurein playwright.config.ts so the trace is only kept on failure - Run the suite 10 times until the flaky test fails
- Open the trace from the failing run in Trace Viewer
- Identify the API call that timed out or returned an unexpected status — it should take less than 30 seconds to find
📝 Quiz
Test your understanding. Click an option to check your answer.
❓ FAQ
Q: Can I use tracing.startHar() with the HTML reporter?
A: Yes. The HAR is embedded in the trace.zip, and the HTML reporter links to the trace for each test. When you click a failing test in the HTML report, the trace opens in Trace Viewer with network calls visible inline.
Q: Does startHar() capture WebSocket frames?
A: Yes. Since Playwright 1.61, HAR and trace recordings include WebSocket requests. Earlier versions captured only HTTP. If you need WebSocket debugging, ensure you’re on 1.61+.
Q: Can I start and stop HAR recording multiple times in one test?
A: Yes. Each startHar() call returns a new Disposable. You can start, finalize, then start again with different urlFilters. Each recording writes to its own file. This is useful for isolating network calls per test phase (e.g., “auth phase” vs “checkout phase”).
📦 Summary
🎯 Key Takeaways
- tracing.startHar() folds HAR recording into the tracing API — network and DOM on one timeline.
- await using (Node 20.17+) auto-finalizes the HAR when the scope exits. No manual stop call.
- Always pass
urlFilterto keep the HAR small —'**/api/**'captures only API calls. - Start tracing first with
tracing.start(), then layer HAR on top for full DOM + network debugging. - Use
mode: 'minimal'for CI (smaller files) andcontent: 'embed'for local debugging (response bodies inline).
Comments
Comments
Post a Comment