Uploading Source Maps to Error Monitoring Without Shipping Them

Two requirements sit in tension. Production errors are unreadable without source maps:

TypeError: Cannot read properties of undefined (reading 'id')
    at t (index-4f2a91.js:1:284119)
    at o (index-4f2a91.js:1:118204)
    at Array.map (<anonymous>)

And publishing source maps hands anyone your original source: file structure, comments, internal names, the code you carefully stripped from the bundle, and occasionally an embedded configuration value that should never have been in the repository.

The resolution is that the browser and the monitoring service do not need the same access. Generate maps, keep them private, and give them only to the service. The broader source-map mechanics are covered in source map generation and debugging workflows; this page is the deployment discipline around them.

Root cause: one comment advertises the map to the world

A standard production build appends a line to every chunk:

//# sourceMappingURL=index-4f2a91.js.map

That comment is the entire discovery mechanism. Browsers read it and fetch the map when DevTools is open; so does anyone who opens the file. Remove the comment and the map becomes undiscoverable β€” while remaining perfectly usable by a service you hand it to directly.

Two Places a Source Map Can Go With a sourceMappingURL comment the map is fetched from the CDN by anyone; with hidden maps the file is uploaded to the monitoring service at deploy time and never reaches the CDN. build output chunk + .map file Published with the bundle sourceMappingURL comment present anyone can fetch the original source Uploaded at deploy time no comment, map deleted before upload only the monitoring service holds it readable stack traces either way

Step 1: generate hidden maps and stamp the build

// vite.config.js β€” Vite 5+
export default {
  build: {
    // 'hidden' emits full .map files and omits the sourceMappingURL comment.
    sourcemap: 'hidden',
  },
  define: {
    // The key that ties a client error report to an uploaded map set.
    __BUILD_ID__: JSON.stringify(process.env.BUILD_ID),
  },
};
// webpack.config.js β€” Webpack 5
const webpack = require('webpack');

module.exports = {
  // 'hidden-source-map': full fidelity, no comment in the emitted chunks.
  devtool: 'hidden-source-map',
  plugins: [
    new webpack.DefinePlugin({ __BUILD_ID__: JSON.stringify(process.env.BUILD_ID) }),
  ],
};

Choose hidden-source-map, not nosources-source-map. The latter ships a map with mappings but no source content, which is safe to publish and gives you line numbers without code β€” better than nothing, and considerably worse than a real map in the monitoring service.

Step 2: upload, then delete, then deploy

Order matters. The maps must exist when the upload runs and be gone before the asset sync runs.

#!/usr/bin/env bash
# deploy.sh β€” upload maps privately, then publish assets without them
set -euo pipefail

npm run build

# 1. Upload while the maps are still on disk, keyed by the same build id
#    the client reports with every error.
npx error-monitor sourcemaps:upload \
  --release "$BUILD_ID" \
  --url-prefix "https://cdn.example.com/assets" \
  dist/assets

# 2. Remove them from the artefact BEFORE anything is published.
find dist -name '*.map' -delete

# 3. Now publish. There is no map file left to leak.
aws s3 sync dist/ "s3://cdn-bucket/releases/$BUILD_ID/" \
  --cache-control "public,max-age=31536000,immutable"

The --url-prefix is what lets the service map a stack frame’s URL back to an uploaded file. Get it wrong and the upload succeeds while every trace stays minified β€” the single most common reason this setup appears to work and does not.

Step 3: report the build id with every error

// monitoring.js β€” the client half of the matching contract
import { init } from '@error-monitor/browser';

init({
  dsn: window.__MONITOR_DSN__,
  // Must be byte-identical to the value used in the upload step.
  release: __BUILD_ID__,
  environment: 'production',
});
The Build Id Is the Join Key With two releases in production simultaneously, only the build id reported with each error can select the correct uploaded map set. tab on release A reports build id A tab on release B reports build id B monitoring service map sets keyed by build id correct original lines for both releases Matching on filename fails as soon as two releases share a chunk name

With several releases live at once β€” a staged rollout, a stale tab, a cached shell β€” the build id is the only reliable way for the service to choose the right map set. Filename matching fails as soon as two releases share a chunk name, which content hashing makes likely rather than rare.

Step 4: verify from outside the network

# Every one of these must return 404, from an unauthenticated client.
curl -sI https://cdn.example.com/assets/index-4f2a91.js.map | head -1
curl -s  https://cdn.example.com/assets/index-4f2a91.js | tail -c 200 | grep sourceMappingURL

The first command proves the file is absent. The second proves the comment is absent. Both are needed: a leftover comment pointing at a missing file is harmless but noisy, and a present file with no comment is still fully readable by anyone who guesses the URL.

Deploy Ordering The maps are uploaded while still on disk, deleted from the artefact, and only then are the assets published; a final verification confirms neither the map nor its comment is reachable. 1. Build hidden maps on disk 2. Upload keyed by build id 3. Delete no .map in artefact 4. Publish assets only 5. Verify: map URL 404s, no sourceMappingURL comment run from outside the network, unauthenticated

Step-by-step verification

  1. Confirm the comment is absent. Inspect the tail of each production chunk; no sourceMappingURL line should be present.

  2. Confirm the files are absent. Request several map URLs from the public origin and confirm every one returns 404.

  3. Confirm the upload landed. Check the monitoring service’s release page lists the expected number of map files for the build id.

  4. Confirm symbolication end to end. Trigger a deliberate error in production and verify the reported stack shows original file names and line numbers.

  5. Confirm the release matching. With two releases live, verify errors from each resolve against their own maps rather than the newest.

  6. Confirm retention alignment. Keep uploaded maps at least as long as chunk files are retained for stale tabs, as discussed in fixing ChunkLoadError after a new deploy β€” otherwise old errors arrive with no map to resolve them.

Edge cases and gotchas

Maps in the repository or the container image. Deleting maps from the published assets does not remove them from a Docker layer or a build artefact stored in CI. Check every place the build output is retained.

Third-party chunk maps. A dependency that ships its own map alongside its published files can reintroduce a sourceMappingURL into your output. Scan the built chunks rather than trusting the configuration.

Development builds deployed to a preview environment. Preview deployments often use the default configuration, publishing maps on a URL that is public even if unlisted.

Mismatched maps after a rebuild. Rebuilding without a new build id can upload maps that do not correspond to the deployed chunks, producing traces that point at plausible but wrong lines β€” worse than no map at all.

FAQ

What is the difference between hidden and regular source maps?

Only the trailing comment. A regular build appends a sourceMappingURL comment to each chunk, which is how a browser knows a map exists and where to fetch it. A hidden build generates identical map files and omits that comment, so nothing advertises them. The maps are equally usable by a monitoring service that has been given them directly β€” the difference is purely whether the browser is told they exist.

Is it enough to block source map requests at the CDN?

It is a reasonable second layer and a poor first one. A rule that returns 403 for map extensions depends on that rule being present in every environment, surviving every configuration change, and covering every path pattern. Not deploying the files at all removes the possibility entirely, which is a stronger guarantee than a rule that can be edited. Do both, with deletion as the primary control.

How do the maps get matched to a stack trace after a deploy?

Through a build identifier that both sides carry. The client reports it with each error, and the uploaded maps are registered under the same value, so the service can pick the correct release even when several are live at once. Matching on filename alone breaks the moment two releases share a chunk name, and matching on URL breaks when assets move between origins.