Turning off production source maps is one of the first things you should do if you care about protecting client JavaScript. A shipped .map with sourcesContent is a complete undo button: anyone can open DevTools and read your original files, comments included.
The objection is always the same: "Then how do we debug production?" That is a real problem, and "just leave the maps up" is a bad answer. This article is a workable setup for investigating production bugs without publishing your source.
The constraint is worth stating clearly. Protected or even reasonably minified JavaScript is harder to read in the browser. That is the point. Your debugging workflow has to move off the public Sources panel and onto tools you control.
What goes wrong if you ship the maps
A typical production build emits:
dist/assets/index-a1b2c3d4.js
dist/assets/index-a1b2c3d4.js.map
and the JavaScript file ends with:
//# sourceMappingURL=index-a1b2c3d4.js.map
If both files are on the same CDN, the browser fetches the map as soon as DevTools is open. The map names every original file and, with sourcesContent, embeds the source itself. Obfuscation, identifier renaming, even VM compilation of a few functions — none of that matters once the map is public.
So the production setting is:
- Vite:
build.sourcemap: false, or generate maps and do not deploy them. - webpack:
devtool: falsein the production config, orhidden-source-mapif you will upload maps privately.
Then delete dangling sourceMappingURL comments so the browser does not even try.
find dist -name "*.map" # should be empty in the deploy artifact
grep -rl "sourceMappingURL" dist
Upload maps privately to your crash tracker
You can still have maps. They just should not be public.
Sentry, Bugsnag, and similar services accept a private upload during CI. The tracker stores the maps, and when a stack trace arrives it symbolicates the minified frames back to original files for your team. A visitor to your site never receives those files.
The usual shape of the pipeline is:
npm run build # emit JS and maps into dist/
# deploy only the JS, CSS, HTML — exclude *.map
npx sentry-cli sourcemaps inject dist
npx sentry-cli sourcemaps upload dist --release "$GIT_SHA"
rm -f dist/**/*.map # or never copy them into the deploy bundle
Two details matter:
- The
release(or equivalent) on the upload must match the release tag your app reports with the error. If they drift, you get unsymbolicated traces and you will be tempted to ship maps again. - The uploaded map must correspond to the exact bytes you deployed. Rebuilding "the same commit" later with a slightly different bundler or minifier produces a useless map.
If you also use VM protection on selected functions, those frames will not always symbolicate back to a pretty original function. That is expected. Keep the protected surface small enough that a symbolicated trace still points you at the right module, and log enough context from around the protected function that you can reproduce without reading the bytecode.
Identify the exact build
"It broke in production" is not a bug report you can act on if you cannot name the artifact.
Stamp every production boot with a build id and send it with errors and support tickets:
window.__BUILD__ = {
sha: process.env.VITE_GIT_SHA,
builtAt: process.env.VITE_BUILT_AT,
};
Surface it in an error boundary, a /__version route, or a quiet line in the console that only you look for. When a user sends a screenshot of a crash, you want that sha visible.
Keep artifacts for each release: the exact dist/, the maps, and a note about which protection settings were on. Without that, you cannot reproduce a minified or protected failure two weeks later.
Make errors useful without original source
Most production bugs are not "I need to step through this minified line." They are "this flow failed for this user with this data." You can make that visible without maps.
- Catch at system boundaries (data fetches, checkout, license checks, game save) and log a structured event: action name, input shape (not secrets), and a stable error code you defined.
- Prefer your own error types over raw
e.messagefrom deep in a bundle. - Sample performance and failed assertions in staging with the protected build, not only with the dev server.
An error that says LICENCE_SERVER_TIMEOUT clientVersion=a1b2c3 is more useful than a symbolicated 40-line React frame you could have predicted.
Feature flags help too. If a protected module is new, ship it behind a flag so you can disable it without a full rollback while you inspect the artifact from that release.
Reproduce against the real artifact
DevTools on the live site, with maps off, is a hostile environment. Do not start there.
- Check out the release tag and build with the same production command, including the protection step.
- Serve that
dist/locally (vite preview,npx serve dist, or your production-like image). - Reproduce the failing flow with the same feature flags and fixture data.
- If you need original source for stepping, use your local maps, which exist on the build machine and in the crash tracker, not on the CDN.
This is also why end-to-end tests should run against the protected build. A class of bugs only appears after renaming, string encoding, or VM compilation: fn.name checks, toString inspection, brittle stack parsing. Catching them in CI is much cheaper than catching them from a customer email.
What to tell the team
A short policy that survives contact with an on-call rotation:
- Production CDNs never serve
.mapfiles orsourceMappingURLcomments. - CI uploads maps privately, tagged with the git sha the app reports.
- Release artifacts and maps are retained for the same period as your error retention.
- On-call debugs from the crash tracker and a local protected build, not by asking to "just enable maps for a few days."
That last point is the one that usually fails. A temporary public map is a permanent leak of whatever source was in that release — including the unprotected form of logic you later decided to put through a VM.
If you are still choosing a protection step for the few functions that deserve it, VEILVM is a browser-based JSVMP workspace. You can try one function without signing up: 3 free runs a day, up to 5 KB per file (resets at 00:00 UTC). A free account raises the input limit to 30 KB. Pro is $9/month or $79/year and adds an API so the step can run in CI. Standard requests are processed in memory and are not written to logs. The site is https://vm.ldvmp.com.
Comments
Join the discussionNo comments yet. Be the first to comment.

