July 20, 2026
How to Debug Browser Tests That Pass in DevTools But Fail in CI Because of Source Maps, Minification, and Error Stacks
A practical guide to debugging browser tests that fail in CI with mangled stack traces, missing source maps, and minified errors. Learn how to inspect builds, capture artifacts, and make failures actionable.
When a browser test passes in DevTools but fails in CI, the hardest part is often not the failure itself, it is the lack of a useful signal. In local debugging, DevTools can show original sources, pretty stack traces, and live-reloaded code. In CI, you usually get a minified stack, a line number that points into a bundled file, or an exception message that looks unrelated to the actual bug. If source maps are missing or mismatched, the failure becomes even less readable.
This guide focuses on the practical side of browser tests fail in CI source maps problems: how to separate a real application bug from a tooling problem, how to inspect the build artifacts that CI actually ran, and how to make future failures easier to diagnose. The goal is not to memorize every bundler setting. It is to build a repeatable debugging process that works when the only clue is a mangled stack trace or a missing source map.
A stack trace is only useful if it refers to the same code that the browser actually executed. In CI, that assumption breaks more often than teams expect.
Why this failure pattern is so frustrating
Local debugging and CI differ in ways that matter for browser tests:
- Different build mode, development locally, production-like in CI
- Different code shape, unminified local bundles vs compressed CI bundles
- Different artifact handling, source maps present locally but omitted or stripped in CI
- Different runtime timing, slower CI machines expose race conditions that local machines hide
- Different browser context, headless browser settings can change stack formatting and error propagation
For frontend engineers and SDETs, the problem is rarely just minification. Minification makes the stack trace hard to read, but the deeper issue is usually one of these:
- The CI job is running a different build than the one you think it is.
- Source maps exist, but they do not match the deployed bundle.
- The test harness is swallowing the real exception and showing only a wrapper error.
- The failure is timing-related, and the minified stack is distracting you from the actual root cause.
That means the first job is not to beautify the stack trace. It is to prove what code ran.
Start by proving which artifact CI actually executed
If a test passes in DevTools but fails in CI, assume the CI run may be using a different artifact until proven otherwise. This is especially common when the pipeline rebuilds assets separately from the developer machine or when test and deploy steps drift apart.
Check these items first:
- Build command and environment variables
- Commit SHA used for the test run
- Bundle hash or asset fingerprint in the test environment
- Whether CI reused cached artifacts from a previous build
- Whether the app served to the browser is the same one that was uploaded or deployed
A practical move is to print build metadata into the browser page or app shell, then capture it in the test failure. For example:
<!-- injected at build time -->
<meta name="build-sha" content="a1b2c3d">
Then read it in the test:
typescript
const buildSha = await page.locator('meta[name="build-sha"]').getAttribute('content');
console.log('build sha:', buildSha);
If the failing test is on a deployed preview environment, capture the asset URLs too. A mismatch between the HTML entry point and the JavaScript chunk version is a classic cause of broken source map lookups.
Learn what the stack trace is actually telling you
A minified stack trace is not useless. It still carries clues:
- The exception type, for example
TypeError,ReferenceError, orAssertionError - The top frame location, even if it points into a bundle
- Whether the failure occurs in your app code, test code, or browser runtime
- Whether the stack includes a wrapper from the test runner or framework
If the error is a browser exception, your test framework may be reporting it through a console listener, page error handler, or unhandled rejection handler. In Playwright, for example, page.on('pageerror') gives you browser exceptions, while assertion failures come from the test runner itself. That distinction matters because the remedy is different.
A useful practice is to log both the raw error and the surrounding context:
page.on('pageerror', error => {
console.error('pageerror:', error.message);
console.error(error.stack);
});
page.on(‘console’, msg => { if (msg.type() === ‘error’) console.error(‘browser console:’, msg.text()); });
This will not decode source maps by itself, but it prevents the runner from hiding the original exception behind a generic failure message.
Separate three problems that often get conflated
When teams say “source maps are broken,” they may actually mean one of three different issues.
1. Source maps are missing
The browser cannot find a .map file for the generated bundle. This happens when the build omits source maps in production, when the CI artifact upload excludes them, or when the app serves a stale HTML file that points to a non-existent map.
2. Source maps are present but invalid
The map file exists, but it points to the wrong source content or wrong line offsets. This can happen after:
- a bad build cache
- a rebundling step that changes file paths
- minifier configuration changes
- post-processing that alters the emitted JavaScript after the map was created
3. Source maps are correct, but the failure is elsewhere
The test is failing because of timing, browser differences, auth state, environment variables, or network assumptions. The source map makes the error look mysterious, but it is not the root cause.
The practical takeaway is simple: do not spend an hour inspecting bundle maps until you have checked whether the app build, test run, and deployed artifact all match.
Build a CI-friendly debugging checklist
For recurring CI browser test failures, use a checklist that moves from infrastructure to application logic.
Step 1: Verify environment parity
Compare local and CI settings for:
NODE_ENV,CI, and any bundler-specific flags- Browser version and channel
- Headless vs headed mode
- Locale and timezone
- Base URL and API endpoints
- Feature flags
A test that passes in DevTools may rely on a dev-only code path, a mock service worker, or an unoptimized bundle. If CI runs production mode, you may be looking at a different app.
Step 2: Capture the exact bundle version
Record the bundle hash, commit SHA, or release version in the test logs. If you can, expose it in the UI or a response header. Then attach it to the failure report.
Step 3: Preserve console output, page errors, and network failures
Many “mystery” failures are visible in the browser console before the assertion trips. Keep these artifacts in CI logs and test reports.
Step 4: Reproduce the CI runtime locally
Use the same browser version, same environment variables, and if possible, the same production build. If the app only fails in CI, try running the exact built assets locally instead of the development server.
Step 5: Inspect source map availability directly
Open the network panel or fetch the map URL in a browser. Confirm that:
- the
.mapfile exists - it is served with the expected content type
- it references the correct original source paths
- source content is embedded if needed
How to inspect source maps without guessing
If you are using a bundler such as Webpack, Vite, or Rollup, the source map settings affect both readability and failure mode. In production builds, teams often disable maps to reduce artifact size or avoid exposing source code. That is a valid tradeoff, but it should be a conscious one.
Some practical checks:
- Open the generated bundle and look for a
sourceMappingURLcomment at the end - Check whether the browser network request for the
.mapfile returns200,404, or a blocked response - Verify that the map points to original sources that still exist in CI artifacts
- Confirm whether the map is inlined or external
A quick browser-side check can help:
fetch('/assets/app.js.map', { method: 'HEAD' })
.then(res => console.log(res.status, res.headers.get('content-type')))
.catch(err => console.error(err));
If the map is 404 in CI but present locally, the problem is likely in artifact packaging or deployment, not in the test itself.
If the map file is absent, the stack trace is not “broken,” it is doing the best it can with incomplete input.
Make minified errors easier to interpret
Minification itself is not the enemy. The enemy is losing the mapping between generated code and source code. Even when source maps are unavailable, you can still make errors more actionable.
Keep stack-relevant information in the error message
If your app throws custom errors, include stable identifiers and context in the message. For example, prefer:
Cannot submit checkout form, missing shipping address- over
Validation failed
The more specific the error message, the less you depend on stack parsing.
Preserve error boundaries and rethrow carefully
Framework wrappers can obscure the original exception. If you catch and rethrow, include the original cause when the runtime supports it.
try {
await submitForm();
} catch (error) {
throw new Error('Checkout submit failed', { cause: error });
}
That does not solve source maps, but it does preserve a chain of evidence.
Normalize logs in the test harness
In CI, collect:
- uncaught exceptions
- unhandled promise rejections
- console errors
- failed network requests
- screenshots and trace files
For Playwright, a compact failure hook might look like this:
page.on('requestfailed', request => {
console.error('request failed:', request.url(), request.failure()?.errorText);
});
This often reveals a missing JS chunk or an API failure that the stack trace only indirectly reflects.
Reproduce the problem with the production build
One of the most effective ways to debug a CI-only failure is to run the production build locally, then point your browser test at it. This removes the “works in DevTools because dev mode is forgiving” variable.
A typical sequence is:
npm run build
npm run preview
npx playwright test --config=playwright.prod.config.ts
If the test fails against the production build locally, you now have a stable environment to inspect source maps, bundle contents, and runtime errors.
If it still only fails in CI, the likely culprits are:
- different environment variables
- missing files in the CI artifact
- browser version differences
- container filesystem or permission issues
- flaky timing under slower execution
That narrows the search a lot faster than staring at a bundled stack trace.
Common failure modes and what they usually mean
The stack points to chunk-*.js or main-*.js
This usually means the browser executed a production bundle and the test runner did not map it back to source. Check whether the source map exists and whether it matches the exact bundle hash.
The browser console shows Failed to load source map
This often means the map comment is present, but the file is missing or blocked. In CI, the map may not have been uploaded, or the server may not serve it from the same path as the JS bundle.
The test fails on a line that looks unrelated
Minification can shift line numbers enough that the reported location is misleading if the source map is stale. Rebuild from scratch and invalidate caches before trusting the location.
Only CI fails, but local production build passes
Compare runtime configuration. Secrets, feature flags, API stubs, and test data often diverge between local and CI environments. The source map may be a red herring.
The error message is Cannot read properties of undefined
This is frequently a timing or data-shape problem, not a mapping problem. Check for loading race conditions, missing fixtures, or API responses that differ in CI.
A practical Playwright debugging pattern
When a Playwright test fails only in CI, add enough instrumentation to turn the browser into a witness, not just an execution target.
import { test, expect } from '@playwright/test';
test('checkout flow', async ({ page }) => {
page.on('pageerror', error => console.error('pageerror:', error.message));
page.on('console', msg => {
if (msg.type() === 'error') console.error('console.error:', msg.text());
});
await page.goto(process.env.BASE_URL!); await page.getByRole(‘button’, { name: ‘Checkout’ }).click(); await expect(page.getByText(‘Order complete’)).toBeVisible(); });
This pattern is useful because it preserves browser-side context around the failure. If the test dies before the assertion, you still have the browser errors in the CI log.
For Selenium, the equivalent idea is to capture browser logs and screenshots at the point of failure. The exact API depends on language bindings, but the principle is the same, preserve evidence before the browser closes.
Decide whether to keep source maps in CI
There is no universal rule. The choice depends on what you value more, easier debugging or smaller and more private artifacts.
Keep source maps when:
- CI failures are expensive to triage
- frontend errors are common enough to need stack mapping
- multiple teams depend on the same test suite
- you need fast diagnosis of production-like test environments
Consider limiting or restricting source maps when:
- your build artifacts should not expose internal source code broadly
- map files are large enough to slow deployments materially
- you already have strong observability and error reporting outside the browser
- the app is stable enough that keeping maps around is not worth the storage and transfer cost
A middle ground is to generate source maps but store them only in a restricted artifact bucket or internal build archive. That keeps debugging possible without making every deployed asset bundle more visible.
Treat source maps as part of the test system, not a frontend detail
Teams often assign source maps to frontend build ownership, then discover that test failures depend on them. That creates a coordination problem. QA sees the broken stack. Frontend sees a bundler setting. DevOps sees an artifact issue. Nobody owns the full path.
The practical fix is to make source maps part of the test contract:
- every CI job should know whether maps are expected
- test reports should include build metadata
- artifact retention should cover the bundle and map together
- release pipelines should verify that the map and asset hash align
This is the same thinking that applies to test automation generally, whether you use browser tests, integration tests, or broader continuous integration flows. A test is only as debuggable as the evidence it leaves behind.
For background on the broader categories, see software testing and test automation.
A compact troubleshooting sequence you can reuse
When the next CI browser failure shows up with a mangled stack trace, use this sequence:
- Confirm the exact build CI executed.
- Check whether the source map exists for that build.
- Verify the map matches the bundle hash actually served.
- Capture browser console, page errors, and request failures.
- Re-run the production build locally with the same browser version.
- Compare runtime configuration between local and CI.
- Only then dig into the minified stack trace.
That order matters. It prevents wasted time on a bundle line number that may not correspond to the real bug.
The main lesson
When browser tests fail in CI with minified stack traces, the source map is often the nearest visible problem, not the root cause. The fastest path to resolution is usually to verify the build artifact, confirm the map is correct and available, and preserve enough runtime evidence to distinguish app failures from packaging failures.
If your team can make one operational improvement, make it this: attach build identity, browser console output, and artifact metadata to every CI failure. Once you have those three things, debugging becomes a disciplined process instead of a guessing game.
That is the difference between a test suite that merely detects failure and one that helps you fix it.