Frontend source maps without leaking production code.
A production JavaScript bundle is minified, split, and renamed. When an error lands in your error tracker it points at main.abc123.js:1:84920 and nothing else. Source maps fix this — they let the error tracker translate that position back to the original file name, function name, and line number. The catch: if your source maps are publicly accessible, anyone can read your unminified code. This guide shows how to get the readable stack traces without the exposure.
20 seconds. Set devtool: 'hidden-source-map' in webpack or sourcemap: 'hidden' in Vite. Upload the resulting .map files to urgentry at build time using sentry-cli sourcemaps upload. Tag each deploy with a unique release name. Delete the .map files from your build output before deploying to production. Done.
60 seconds. The hidden-source-map mode generates source maps as separate files but strips the //# sourceMappingURL= comment from the bundle. Browsers never try to fetch the map because they have no URL to request. Your error tracker receives the map during the build step, before the bundle reaches any server. When an error arrives, urgentry looks up the release, finds the corresponding map, and translates the minified frame to its original location. The browser never sees the map; your users never see your source.
urgentry speaks the same source map upload protocol as Sentry. If you already use sentry-cli or a Sentry webpack or Vite plugin, you point it at your urgentry URL and nothing else changes. All the configuration examples in this guide work unchanged with urgentry; the only difference is the value of SENTRY_URL.
What source maps are and why your minified errors are unreadable without them
Modern frontend build tools transform your source files before they reach the browser. Variable names become single letters. Functions are inlined. Multiple modules collapse into one file. Whitespace and comments disappear. The browser receives a dense, incomprehensible string that runs fast but tells you nothing when something goes wrong.
A source map is a JSON file that records the mapping between each character position in the output bundle and the corresponding position in the original source file. When a developer opens the browser DevTools, the DevTools engine reads the map and presents the original code in the sources panel. When an error tracker receives a stack frame, it uses the map to translate the minified position back to the original file name, function name, and line number.
Without a source map, a stack frame in your error tracker looks like this:
at c (main.f3a9b2.js:1:84920)
at Object.d (main.f3a9b2.js:1:12445)
at main.f3a9b2.js:1:3312
With a source map loaded in urgentry, the same frame resolves to:
at submitPaymentForm (src/features/checkout/PaymentForm.tsx:142:18)
at handleSubmit (src/features/checkout/CheckoutPage.tsx:87:5)
at React.useEffect (src/hooks/useFormSubmit.ts:34:3)
The difference between those two outputs is the difference between spending twenty minutes finding the bug and spending two.
The leak problem
The default build configuration for most bundlers serves source maps alongside the bundle. Webpack’s default devtool for production is source-map, which generates a .map file and appends a comment to the bundle pointing directly at it:
//# sourceMappingURL=main.abc123.js.map
That comment tells every browser, every crawler, and every developer tab to fetch main.abc123.js.map from your CDN. The file is public. Any competitor, any security researcher, and any curious visitor can download it and read your original source code with full variable names, comments, and structure intact.
This is not a theoretical risk. Source maps have disclosed proprietary business logic, internal API endpoints, and authentication implementation details. A .map file sitting on a CDN is as exposed as your bundle itself, but far more informative.
The //# sourceMappingURL comment is a tell. If you serve your bundle through a CDN, tools like curl -s https://yourapp.com/main.abc123.js | tail -1 will reveal whether source maps are publicly accessible in about three seconds. Attackers know this.
Serving maps from a restricted path with access controls is one mitigation, but it is operationally fragile. Cache layers strip headers. CDN configurations change. Staging environments get pushed to production with different settings. The only configuration that does not have those failure modes is one where the maps never leave your build environment and go directly to your error tracker.
The two patterns that work
Two approaches produce stack traces without public exposure.
The first, and the one this guide focuses on, is to upload source maps to your error tracker during the build and never serve them in production at all. The build tool generates the maps, the upload step sends them to urgentry, and your deployment pipeline deletes the .map files before they reach any server. The browser never tries to fetch them because the bundle has no sourceMappingURL comment.
The second is to serve source maps from a non-public path protected by authentication. You configure your CDN or reverse proxy to require a valid session token before returning any .map file. DevTools will fetch them when a developer is logged in; public visitors see a 401. This works but requires persistent operational discipline: every CDN rule change, every new environment, and every deploy pipeline must maintain the access controls correctly. Most teams that start with this pattern eventually switch to pattern one after one accidental exposure.
This guide uses pattern one. Upload to urgentry, never serve publicly.
The webpack config
Webpack’s devtool option controls source map behavior. For production builds that will upload maps to an error tracker, use 'hidden-source-map':
// webpack.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
mode: "production",
// Generates .map files but strips the //# sourceMappingURL comment
devtool: "hidden-source-map",
plugins: [
sentryWebpackPlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
url: process.env.SENTRY_URL, // https://errors.example.com for urgentry
// Associate uploaded maps with a release name
release: {
name: process.env.RELEASE_NAME, // e.g. "v1.4.2" or a git SHA
inject: true, // injects the release name into the bundle at build time
uploadLegacySourcemaps: "./dist",
},
}),
],
};
The sentryWebpackPlugin runs after Webpack finishes compiling. It reads the .map files from the output directory, uploads them to urgentry alongside a manifest, and then (optionally) deletes them so they do not end up in your deployment artifact. Set sourcemaps.filesToDeleteAfterUpload to ["./dist/**/*.map"] to enable the cleanup step.
If you prefer sentry-cli directly without the plugin, the build step is a webpack run followed by a CLI upload:
webpack --mode=production
sentry-cli sourcemaps upload \
--org "$SENTRY_ORG" \
--project "$SENTRY_PROJECT" \
--url "$SENTRY_URL" \
--release "$RELEASE_NAME" \
./dist
# Remove maps before deploying
find ./dist -name "*.map" -delete
The --url flag is what redirects the upload to urgentry. Without it, sentry-cli defaults to https://sentry.io. Set it to your urgentry base URL.
The Vite config
Vite uses build.sourcemap to control source map output. The value 'hidden' is the equivalent of webpack’s hidden-source-map:
// vite.config.js
import { defineConfig } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
build: {
// Generates .map files with no sourceMappingURL comment in the bundle
sourcemap: "hidden",
},
plugins: [
sentryVitePlugin({
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
url: process.env.SENTRY_URL,
release: {
name: process.env.RELEASE_NAME,
inject: true,
uploadLegacySourcemaps: "./dist",
},
// Delete .map files from the dist directory after upload
sourcemaps: {
filesToDeleteAfterUpload: ["./dist/**/*.js.map"],
},
}),
],
});
The Vite plugin runs in the closeBundle hook, after the build completes. It uploads maps before the build pipeline moves to the next step, so the deletion and upload are atomic from the pipeline’s perspective.
Without the plugin, use sentry-cli after the build step the same way as with webpack. The CLI does not care which bundler generated the maps.
A note on sourcemap: true versus sourcemap: 'hidden': true generates maps and includes the sourceMappingURL comment. 'hidden' generates maps without the comment. If you use true in production, browsers and crawlers will fetch your maps. Use 'hidden'.
The Next.js case
Next.js wraps webpack internally. The withSentryConfig function in @sentry/nextjs patches the webpack config for you and adds the upload step to the build pipeline.
// next.config.mjs
import { withSentryConfig } from "@sentry/nextjs";
const nextConfig = {
// Your existing Next.js config
};
export default withSentryConfig(nextConfig, {
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
url: process.env.SENTRY_URL, // point at urgentry
// This is the critical flag: strips sourceMappingURL from the bundle
// and uploads maps to urgentry without serving them publicly
hideSourceMaps: true,
// Upload a wider set of files, including client-side chunks
widenClientFileUpload: true,
// Suppress upload logs in CI output
silent: !process.env.CI,
});
The hideSourceMaps: true option sets webpack’s devtool to 'hidden-source-map' for production builds and prevents the .map files from appearing in the .next/static public directory. The source maps are generated into a temporary path inside .next/, uploaded by the plugin, and then removed. Nothing reaches your deployment target.
Next.js also generates server-side source maps for API routes, server components, and middleware. The withSentryConfig wrapper handles those as well, uploading them from the server output directory. Server-side maps resolve stack traces in server errors that urgentry receives from the @sentry/nextjs server SDK.
The most common mistake with the Next.js setup is setting SENTRY_URL at runtime without also setting it at build time. The upload happens during next build, not during server startup. Your CI environment needs the variable, not your deployment environment.
Uploading to urgentry
urgentry implements the same artifact upload API as Sentry. The sentry-cli tool and the official build plugins all target urgentry by setting three environment variables:
SENTRY_URL=https://errors.example.com # your urgentry base URL
SENTRY_AUTH_TOKEN=your_urgentry_token # token from urgentry project settings
SENTRY_ORG=your-org-slug
SENTRY_PROJECT=your-project-slug
With these set in your CI environment, the upload command is:
sentry-cli sourcemaps upload \
--release "$RELEASE_NAME" \
./dist
The --release flag is the link between the uploaded maps and the events urgentry receives at runtime. When the SDK sends an error event, it includes the release name. urgentry uses that name to look up the corresponding source maps and resolve the stack frames. If the release names do not match, resolution fails silently and you see minified frames.
The upload also creates the release in urgentry if it does not already exist. You can create it explicitly beforehand with sentry-cli releases new "$RELEASE_NAME", but the upload command handles that implicitly.
To validate that the upload succeeded, list the artifacts for a release:
sentry-cli releases files "$RELEASE_NAME" list
You should see one .map entry per JavaScript chunk, plus a bundle entry for each JS file. If the list is empty, the upload failed silently — usually because SENTRY_AUTH_TOKEN is missing or has insufficient permissions.
Verify the round trip
Uploads alone are not confirmation that resolution works. Run a full round-trip test before treating the setup as complete.
Deploy a production build with the source map upload step enabled. Then throw a test error from the running application. The simplest way to do this is to add a temporary button to a page that calls:
document.getElementById("test-error").addEventListener("click", () => {
throw new Error("Source map test error " + Date.now());
});
Click the button. Open urgentry and find the event. Look at the stack trace. If source maps resolved, you will see the original file path and line number in the first frame. If you see main.abc123.js:1:12345, resolution failed.
When resolution fails, the two most common causes are a release name mismatch and a missing upload. Check the release name the SDK reports by adding a temporary log:
import * as Sentry from "@sentry/browser";
console.log(Sentry.getClient()?.getOptions().release);
Compare that value against the releases list in urgentry. They must match exactly, including case and any prefix characters.
After verification, remove the test button and consider adding a CI check that runs sentry-cli releases files "$RELEASE_NAME" list and fails the build if the artifact count is zero. This catches upload failures before they reach production.
The release tagging discipline
The release name is the only key urgentry uses to match events to source maps. It must be unique per deploy and consistent between the build (where maps are uploaded) and the runtime (where events are sent).
Three values work well as release names: git commit SHAs, semantic version tags, and CI build IDs.
# Git SHA (unique, always available in CI)
RELEASE_NAME=$(git rev-parse HEAD)
# Short SHA (shorter, still unique within a project)
RELEASE_NAME=$(git rev-parse --short HEAD)
# Semantic version tag from package.json
RELEASE_NAME=$(node -p "require('./package.json').version")
# CI build number (GitHub Actions)
RELEASE_NAME="$GITHUB_SHA"
Inject the release name into the SDK at initialization time so that every event carries it:
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
release: import.meta.env.VITE_RELEASE_NAME, // injected at build time
});
For Vite, set the variable in the build step:
VITE_RELEASE_NAME=$(git rev-parse HEAD) vite build
The “always-latest” pattern — where every build uses the same release name like production-latest — breaks on rollbacks. If you roll back from deploy 200 to deploy 195, urgentry receives events with release name production-latest but the most recently uploaded maps correspond to deploy 200. The stack traces will resolve to the wrong files or fail entirely. Unique release names per deploy eliminate this class of problem completely.
Cleanup and retention
Source maps accumulate with every deploy. A team shipping five times per day generates 150 releases per month, each with 5 to 30 MB of maps. Without a cleanup policy, storage compounds quickly.
urgentry does not enforce a retention policy automatically. You set one that matches your rollback window. A reasonable default is to keep the last 90 days of releases. If your rollback window is seven days, 30 days provides a comfortable margin.
Delete old releases and their artifacts with sentry-cli:
# Delete a specific release and all its artifacts
sentry-cli releases delete "$OLD_RELEASE_NAME"
Automate this in a scheduled CI job. Generate a list of release names older than 90 days (by correlating with your deployment log or git history), and run the delete command for each. The urgentry API also exposes a releases endpoint that returns all releases with their created timestamps, which you can query and filter programmatically.
When budgeting storage for urgentry, plan for:
- Map storage: (average map size per release) x (deploys per day) x (retention days). For a 15 MB average and three deploys per day over 90 days, that is 4 GB.
- Event storage: typically smaller than map storage for teams with moderate error rates.
- S3 storage (if configured): the same calculation applies, but at object storage prices instead of block storage prices.
S3-backed map storage is the better choice for teams with high deploy frequency. Configure urgentry’s STORAGE_BACKEND=s3 option and the maps go to a bucket instead of the local filesystem. This keeps urgentry’s disk footprint small and lets you use S3 lifecycle policies for automated cleanup without any CI scripting.
FAQ
FAQ
Can I serve source maps publicly and still be secure?
Technically yes, if you restrict them by IP or auth header on the CDN or server level. In practice most teams never configure that correctly and the maps end up fully public. The simpler pattern is to never serve them at all: upload to urgentry at build time and set devtool to hidden-source-map.
What happens if I forget to upload source maps for a release?
urgentry receives events with minified stack traces for that release. Existing releases with maps already uploaded are unaffected. You can upload maps retroactively with sentry-cli as long as the release name matches what the SDK reports at runtime. The retroactive upload resolves any events that arrive after the upload completes.
Do source maps affect page load performance?
Not when hidden-source-map or hidden mode is used. The browser never downloads a file it cannot find. The .map files exist in the build output and are uploaded to urgentry but carry no URL the browser can request. There is zero runtime performance impact.
How large do source maps get?
A typical React or Vue SPA generates between 5 MB and 30 MB of source maps per release, uncompressed. Builds that include large dependencies can go higher. Budget 10 to 50 MB per release and apply a 30-day retention policy to keep total storage manageable. S3-backed storage with a lifecycle policy is the cleanest solution for high-frequency deployments.
Does urgentry support every source map format?
urgentry accepts the same artifact bundle format as Sentry: individual .map files uploaded alongside a release, or bundled in a .zip artifact. The sentry-cli sourcemaps upload command handles both. The upload endpoint is compatible with @sentry/webpack-plugin, @sentry/vite-plugin, and sentry-cli directly.
Sources
- webpack devtool configuration — full reference for all
devtoolvalues includinghidden-source-mapand their tradeoffs. - Vite build.sourcemap option — documents the
true,'inline', and'hidden'modes and their behavior. - sentry-cli sourcemaps upload documentation — the
--release,--url, and--orgflags used throughout this guide. - @sentry/webpack-plugin — the official Sentry webpack plugin source and configuration reference.
- @sentry/vite-plugin — the official Sentry Vite plugin, including
sourcemaps.filesToDeleteAfterUploadoption documentation. - FSL-1.1-Apache-2.0 license — urgentry’s source-available license. Converts to Apache 2.0 after two years.
- urgentry compatibility matrix — which SDK versions and sentry-cli versions are tested against the current urgentry release.
Ready to get readable stack traces on hardware you control?
urgentry accepts the same source map upload format as Sentry. Set SENTRY_URL to your urgentry instance, run one build, and your next production error will show the original file name and line number — with your source code never leaving your infrastructure.