Advanced Trace Viewer — Hunting Heisenbugs
Screenshots tell you what broke. Logs tell you when. But the Trace Viewer tells you why. Master timeline scrubbing, network waterfalls, and source mapping to hunt down the most elusive flaky tests.
📖 The Story: The Heisenbug
A SaaS company had a test named "Checkout Flow" that failed exactly once every 50 runs. It was a classic "Heisenbug"—when the QA team tried to observe it locally using console.log or headed mode, it passed perfectly. The bug disappeared when observed.
The QA lead, Sarah, configured the suite with trace: 'on-first-retry'. On the 50th run, the test failed in CI, and a trace.zip was generated. Sarah downloaded it and opened the Trace Viewer.
Instead of looking at the final error, she hovered her mouse over the Timeline at the exact millisecond before the failure. She watched the DOM snapshot shift. She saw a promotional pop-up appear for exactly 200 milliseconds, intercept the "Submit" button click, and then disappear before the final assertion ran.
The pop-up was triggered by a race condition in a third-party analytics script. It only manifested when the CI server's CPU was under high load. Sarah added a line to dismiss the pop-up. The test passed 1,000 times in a row. The Heisenbug was dead.
Logs are for events. Traces are for time. If you don't scrub the timeline, you are blind to race conditions.
🎯 Why Advanced Tracing?
Timeline Scrubbing
Hover over the timeline to see the exact DOM state at any millisecond.
Network Waterfall
Inspect API request headers, payloads, and response bodies in context.
Console & Errors
Catch silent JavaScript errors and warnings that break the UI later.
Source Mapping
Click any action to see the exact line of TypeScript code that executed it.
🌍 Real World Example
A test fails because a "Success" message didn't appear. In the Trace Viewer Network tab, you see the POST request was made, but the response took 6 seconds. In the Console tab, you see a warning: "JWT token expired." The backend rejected the delayed request. The trace reveals it wasn't a UI bug, but a session timeout issue.
🧒 Explain Like I'm 10
Imagine a magic video camera that records your house. A normal camera just shows you a movie. But this magic camera also has an "X-ray" mode (to see inside walls), a "wiretap" mode (to hear phone calls), and a "time-scrub" dial that lets you freeze time and look at exactly what was on the table at 3:02 PM.
The Playwright Trace Viewer is that magic camera for your web app. It records everything—what the screen looked like, what the network was doing, and what the code was doing—so you can freeze time and investigate.
🎓 Professional Explanation
The Trace Viewer is a local Progressive Web App (PWA) that renders a serialized timeline of test events. Playwright instruments the browser via the Chrome DevTools Protocol (CDP) to capture DOM snapshots (using mutation diffing), network traffic, and console output. The viewer stitches these together, allowing you to "scrub" through time. As you hover over the timeline, the viewer reconstructs the DOM state at that exact millisecond, overlaying action metadata and source code mappings.
📊 Trace UI Anatomy
The Trace Viewer Interface Layout
1. page.goto()
2. click('#submit')
Visual rendering of the page at the selected time.
spec.ts:42
await page.click()
⏳ Timeline Scrubbing (The Secret Weapon)
Most engineers only look at the final snapshot when a test fails. This is a mistake. The root cause is almost always in the before state.
// How to hunt a race condition: 1. Open trace.zip 2. Find the failed action (red bar on timeline) 3. Slowly drag your mouse LEFT across the timeline 4. Watch the "Snapshots" panel (center) 5. Look for elements appearing, disappearing, or shifting 6. Notice the exact moment an overlay or spinner intercepts your target
🌐 Network Waterfall
Click the "Network" tab at the bottom of the Trace Viewer. It displays a Chrome DevTools-style waterfall of all requests.
// What to look for in the Network tab: - Look for 500 status codes (Server errors) - Look for requests taking > 3000ms (Latency) - Click a request to inspect Request Headers & Payload - Check the Response Body to see if the backend sent bad data
💻 Console & Errors
The frontend can look perfect while a silent JavaScript error destroys the app's state. The Console tab captures all console.log, console.error, and unhandled Promise rejections.
If a React component throws an error inside a try/catch block, the UI might not crash immediately, but state updates will stop. The only trace of this will be a red line in the Trace Viewer's Console tab. Always check the console!
📜 Source Mapping
The "Source" panel (usually right side) maps the selected action back to your TypeScript code. If an action took 5 seconds to complete, you can see exactly which line of code triggered it, helping you identify slow waitForResponse or page.evaluate calls.
📌 Custom Annotations
You can inject your own notes directly into the Trace timeline using test.info().annotate(). This is invaluable for marking business logic steps.
test('checkout flow', async ({ page }) => { await page.goto('/cart'); // Add a custom annotation to the trace timeline await test.info().annotate('User has 3 items in cart, starting checkout'); await page.click('Checkout'); await test.info().annotate(`Applying discount code: SAVE20`); await page.fill('#promo', 'SAVE20'); });
When you open the trace, these annotations appear as markers on the timeline, making it easy to navigate complex test flows.
⚠️ Common Mistakes
Mistake 1: Only Looking at the End
Engineers often open a trace, click the last action, and look at the final screenshot. If a pop-up appeared and disappeared earlier, it won't be visible in the final state. You must scrub the timeline.
Mistake 2: Ignoring the Console
A test fails to find a button. The engineer assumes the selector is wrong. In reality, a JavaScript error earlier in the test prevented the button from rendering. The Console tab would have shown the error instantly.
✅ Best Practices
- Always check the Console tab first. Look for red errors before inspecting the DOM.
- Scrub slowly. Drag your mouse across the timeline to catch fleeting overlays.
- Use annotations. Tag complex business steps with
test.info().annotate(). - Check Network timing. A 200 OK response that takes 8 seconds is just as bad as a 500 error.
🏗️ Senior Engineer Deep Dive
DOM Mutation Diffing
How does the Trace Viewer show the DOM at any millisecond without recording a full HTML snapshot every frame (which would be gigabytes)? Playwright uses DOM mutation observers. It takes one initial snapshot, then records only the differences (added/removed nodes, changed attributes) as events. When you scrub the timeline, the viewer reconstructs the DOM by replaying the diff patch. This is why trace files are typically only 1-5 MB.
Local vs CI Trace Viewing
You can view traces locally via the CLI (npx playwright show-trace), but Playwright also hosts a public version at trace.playwright.dev. It is a client-side PWA; dragging and dropping your .zip file there processes everything in your browser. No data is uploaded to Microsoft's servers, making it secure for enterprise use.
💼 Interview Questions
Show Answer
trace: 'retain-on-failure' in the Playwright config. When the test fails in CI, the trace.zip is uploaded as an artifact. I download it, run npx playwright show-trace trace.zip, and inspect the timeline, network, and console tabs to find the root cause.Show Answer
Show Answer
test.info().annotate('Step description') inside the test. This injects a custom marker directly into the trace timeline, making it easy to navigate long, complex test flows without guessing which action corresponds to which business step.Show Answer
🏋️ Practical Exercise
Scrub a Trace
- Run a test locally with
--trace=on. - Open the generated trace file.
- Find the
page.goto()action in the Actions list. - Hover your mouse over the Timeline at the top. Watch how the DOM snapshot changes as the page loads.
- Click the Network tab and find the main document request.
🚀 Mini Project
🏗️ The Annotated Flow
- Write a test for a multi-step form (e.g., shipping -> payment -> review).
- Before each major step, add
await test.info().annotate('Starting Payment Step'). - Run with tracing enabled.
- Open the trace and verify your annotations appear as distinct markers on the timeline.
📝 Quiz
Test your understanding. Click an option to check your answer.
test.attach()console.annotate()test.info().annotate()page.markTrace()test.info().annotate() injects custom markers into the trace timeline.console.log.❓ FAQ
Q: How long should I retain trace files in CI?
A: 7 to 14 days is standard. Traces are small, but they accumulate over time. Set retention-days: 14 in your CI artifact upload step.
Q: Can I view a trace on my mobile phone?
A: While technically possible via the web app, the Trace Viewer UI is highly optimized for desktop screens with multiple panels. Mobile viewing is not recommended.
📦 Summary
🎯 Key Takeaways
- Scrub the timeline to catch fleeting DOM elements and race conditions.
- Check the Console tab for silent JavaScript errors.
- Inspect the Network waterfall for slow or failed API calls.
- Use the Source panel to map actions back to your spec.ts code.
- Inject custom notes using
test.info().annotate(). - Use trace.playwright.dev to view traces securely in any browser.
