Fix: Sentry Source Maps Not Working — Release Matching, sentry-cli Upload, Vite/Webpack Plugins
Part of: React & Frontend Errors
Quick Answer
How to fix Sentry source maps errors — minified stack traces, release name mismatch between build and runtime, sentry-cli upload-sourcemaps options, Vite/Webpack/Next.js plugin setup, and hiding maps from public.
The Error
You see a Sentry error and the stack is unreadable:
TypeError: undefined is not an object (evaluating 'a.b')
at t (https://app.example.com/static/js/main-abc123.js:1:1234)
at e (https://app.example.com/static/js/main-abc123.js:1:5678)Instead of:
TypeError: Cannot read property 'name' of undefined
at fetchUser (src/api/users.ts:42:18)
at loadProfile (src/pages/Profile.tsx:15:8)Or the upload command succeeds but Sentry’s UI still shows minified stacks:
$ sentry-cli sourcemaps upload [email protected] ./dist
✓ Uploaded 12 source mapsBut the next error reported is still minified.
Or your plugin processes a build but no maps make it to Sentry:
// vite.config.ts
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default { plugins: [sentryVitePlugin({ org: "...", project: "..." })] };Or maps appear in production HTML, exposing your source:
<script>...</script>
<!--# sourceMappingURL=main-abc123.js.map -->
<!-- Anyone can download /static/js/main-abc123.js.map -->Why This Happens
Sentry symbolicates minified stack traces by matching:
- The URL of the JS file (
https://app.example.com/static/js/main-abc123.js). - The release name reported by the SDK (
[email protected]). - An artifact uploaded for that release with the same URL.
If any of those don’t match, symbolication fails silently — Sentry shows the minified stack with no warning. Common causes:
- Release mismatch. The SDK reports
[email protected]but you uploaded for[email protected]. - URL mismatch. Artifact uploaded as
main.jsbut runtime ismain-abc123.js(or vice versa). - Maps not actually generated. Build doesn’t emit
.mapfiles, or the bundler inlines them. - Wrong upload directory.
sentry-cliwalks the path but skips files that don’t have.mapextensions or are in node_modules.
Why “silently”? Sentry’s design priority is to keep your event ingestion alive even when source-map data is missing — a half-symbolicated event is more useful than a dropped one. So when symbolication fails, you get the event with the minified stack and a small “Issues with Source Maps” warning in the event detail page. If you never click in, you never see the warning. Multiply that by hundreds of unique issues per week, and the missing-map problem hides in plain sight. The fix is always to verify on a known test error before assuming it’s working: throw a deliberate exception, find it in Sentry, and look for either symbolicated frames or the source-map warning.
The release identity is the most underappreciated knob. Two builds with different content but the same release name overwrite each other’s artifacts; two builds with identical content but different release names are treated as unrelated by Sentry. Most teams trip over this in CI: the release name is built from $GITHUB_SHA at upload time but package.json version at runtime, and the two never match. Modern Sentry’s “Debug IDs” feature solves this by embedding a unique identifier into the bundle itself and into the map file — when both are present, Sentry pairs them by ID rather than by release + URL. But Debug IDs only work if the official bundler plugins inject them; hand-rolled webpack configs miss them.
Fix 1: Use the Sentry Bundler Plugins (Recommended)
Bundler plugins handle release detection, source map generation, upload, and deletion in one step:
Vite:
// vite.config.ts
import { defineConfig } from "vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
export default defineConfig({
plugins: [
sentryVitePlugin({
org: "my-org",
project: "my-app",
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: {
assets: ["./dist/**"],
},
release: {
name: process.env.GITHUB_SHA, // Or your release naming
},
}),
],
build: {
sourcemap: true, // Required: emit source maps
},
});Webpack:
// webpack.config.js
const { sentryWebpackPlugin } = require("@sentry/webpack-plugin");
module.exports = {
devtool: "source-map", // Generate maps
plugins: [
sentryWebpackPlugin({
org: "my-org",
project: "my-app",
authToken: process.env.SENTRY_AUTH_TOKEN,
}),
],
};Next.js (App Router):
// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
// your config
};
module.exports = withSentryConfig(nextConfig, {
org: "my-org",
project: "my-app",
authToken: process.env.SENTRY_AUTH_TOKEN,
widenClientFileUpload: true,
hideSourceMaps: true, // Hide map URLs from production HTML
disableLogger: true,
});The Next.js plugin handles client and server bundles separately, uploads both, and removes source map comments from the HTML.
Pro Tip: The bundler plugins read SENTRY_AUTH_TOKEN from env automatically. Set it via your CI secrets, never commit it.
Fix 2: Manual Upload With sentry-cli
If you can’t use a plugin, upload manually:
# Set up:
export SENTRY_AUTH_TOKEN=...
export SENTRY_ORG=my-org
export SENTRY_PROJECT=my-app
# Or via ~/.sentryclirc:
# [auth]
# token = ...
# [defaults]
# org = my-org
# project = my-app
# Build:
npm run build # Produces dist/ with .map files
# Create the release:
sentry-cli releases new "$RELEASE_NAME"
# Upload source maps:
sentry-cli sourcemaps upload \
--release="$RELEASE_NAME" \
--url-prefix="~/static/js" \
./dist/static/js
# Finalize the release:
sentry-cli releases finalize "$RELEASE_NAME"--url-prefix:
~/static/js— the~/means “any host,” matchinghttps://anywhere.com/static/js/....- For absolute URLs:
--url-prefix=https://app.example.com/static/js.
The prefix must match the URL the browser reports. If your CDN serves https://cdn.example.com/static/... but you set ~/static/..., the artifact won’t match.
For uploading both .js and .map:
sentry-cli sourcemaps upload \
--release="$RELEASE_NAME" \
--url-prefix="~/static/js" \
--validate \
--rewrite \
./dist/static/js--validate— checks each source map is valid.--rewrite— flattenssourcespaths in the maps, removing local absolute paths.
Fix 3: Match the Release Name
The release name reported at runtime must match what you uploaded:
Runtime (in your app):
import * as Sentry from "@sentry/react";
Sentry.init({
dsn: "...",
release: import.meta.env.VITE_RELEASE, // Same source as build time
});Build time:
VITE_RELEASE="app@$(git rev-parse --short HEAD)" npm run build
sentry-cli sourcemaps upload --release="$VITE_RELEASE" ./distBoth use the same git rev-parse --short HEAD value.
For SemVer-based releases:
VERSION=$(cat package.json | jq -r '.version')
RELEASE="app@$VERSION"Then use $RELEASE everywhere — at build, runtime, and upload.
Common Mistake: Reading package.json at runtime in a frontend context — that file isn’t bundled. Read it at build time, inject as an env var.
For Next.js, the plugin handles this automatically via SENTRY_RELEASE:
// next.config.js
module.exports = withSentryConfig(nextConfig, {
release: {
name: process.env.GITHUB_SHA ?? "dev",
},
});Fix 4: Hide Source Maps From Production
Source maps reveal your code. You don’t want them publicly accessible. Two options:
Option A — Don’t deploy .map files:
# Build:
npm run build
sentry-cli sourcemaps upload --release=$RELEASE ./dist
# Then delete maps before deploying:
find ./dist -name "*.map" -delete
# Now /dist has the .js but no .map files.The browser will fail to load *.map URLs (and won’t show maps in DevTools either), but Sentry’s server-side symbolication still works — Sentry has the maps uploaded.
Option B — Use the bundler plugin’s hideSourceMaps:
withSentryConfig(nextConfig, {
hideSourceMaps: true, // Removes sourceMappingURL comments
});The maps are uploaded but their sourceMappingURL comments are stripped from the JS. DevTools can’t auto-load them.
Pro Tip: Combine: set hideSourceMaps: true (no public reference) and delete .map files post-build (no public file). Sentry still has the maps from the upload step.
Fix 5: Verify the Upload
After uploading, check via CLI or UI:
sentry-cli releases artifacts list "$RELEASE_NAME"Should list .js and .js.map files. If empty, your upload didn’t include source maps.
In the Sentry UI: Project → Releases → [your release] → Artifacts. The list should show paired main-abc123.js and main-abc123.js.map.
For test events:
Sentry.captureException(new Error("Test error"));In the resulting error, click “Source Maps” → “Show Resolved” — confirms symbolication is working.
If Sentry’s UI says “no matching source map found for https://example.com/static/js/main.js”:
- The URL doesn’t match any uploaded artifact. Check exact strings.
- The release reported by the SDK doesn’t have artifacts. Check release name match.
Fix 6: Multi-Bundle Apps
For apps with multiple bundles (vendor, main, chunks), upload all of them:
sentry-cli sourcemaps upload \
--release="$RELEASE" \
--url-prefix="~/static/js" \
./dist/static/js./dist/static/js contains main.*.js, vendor.*.js, chunk-*.js — all uploaded.
For dynamically imported chunks, Sentry needs maps for them too. The bundler plugins handle this automatically — manual upload may need to crawl the dist directory recursively, which the plugins do but a single sourcemaps upload may not.
Common Mistake: Uploading only main.js.map but not vendor or chunk maps. Errors that occur inside vendor code won’t symbolicate.
Fix 7: Server-Side Source Maps
For Node.js servers, source maps work the same way but for .js in node_modules or your dist/:
// In your server:
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: "...",
release: process.env.SENTRY_RELEASE,
integrations: [Sentry.rewriteFramesIntegration({ root: process.cwd() })],
});rewriteFramesIntegration normalizes stack frames so Sentry can match them against uploaded maps.
For uploading server maps:
sentry-cli sourcemaps upload \
--release="$RELEASE_NAME" \
--url-prefix="app:///" \
./dist--url-prefix=app:/// (yes, three slashes) is the convention for server-side. The runtime SDK uses app:/// as the synthetic origin for local files.
For Next.js (mixed client/server), the plugin handles both automatically.
Fix 8: TypeScript Sources vs Compiled JS
If your stacks show .js line numbers but you want .ts line numbers, ensure your TS compiler emits maps that point to the .ts source:
// tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"inlineSources": true,
"sourceRoot": "/src"
}
}sourceMap— emit.mapfiles.inlineSources— include the original.tssource inside the map. Sentry can show the original code in error views.sourceRoot— used by Sentry to resolve relativesourcespaths.
For Vite/esbuild:
build: {
sourcemap: true, // External .map files
// or "inline" for inline data URLs
// or "hidden" for external maps without sourceMappingURL comment
}sourcemap: "hidden" is recommended for Sentry — generates maps for upload but no sourceMappingURL in production JS.
Pro Tip: inlineSources: true slightly increases map size but makes Sentry’s UI more useful — you can see the actual source code at the error location, not just file/line/column.
Sentry vs Datadog vs New Relic vs Rollbar vs Honeybadger for Source Maps
Every error-tracking vendor handles source maps slightly differently. If you’re migrating between them — or evaluating — the upload model is one of the bigger decision points.
Sentry. Releases + URL matching, or modern Debug IDs. Official bundler plugins for Vite, Webpack, Rollup, esbuild, Next.js, Remix, SvelteKit. Auth tokens are project-scoped or org-scoped. Free tier includes source-map storage; paid tiers raise the per-release file count cap. The official plugin is opinionated and usually right — fight it only when you have an unusual build pipeline (multi-CDN, dynamic chunk URLs).
Datadog RUM (Real User Monitoring). Uses release name + service name + version. Upload via datadog-ci sourcemaps upload. No Debug-ID equivalent yet — you must keep release names in lockstep between build and runtime. Datadog ties source maps to its RUM product (separate from its APM and logs products), which can confuse first-time users looking for them in the wrong place.
New Relic Browser Agent. Upload via the New Relic CLI or REST API. Map files are tied to your “application ID” and “release name” — closer to Sentry’s model. Stack traces in errors get symbolicated server-side. Less polished bundler-plugin story than Sentry; expect to script your upload step.
Rollbar. Upload via rollbar source map CLI or HTTP POST to /api/1/sourcemap. Matched by code_version (their term for release) and minified URL. Historically Rollbar’s source-map UI was excellent — clean stack traces with surrounding code context. Less active development in recent years; their bundler plugins lag behind Sentry’s.
Honeybadger. Upload via the Honeybadger CLI or webpack plugin. Source maps are matched by revision (their term for release) and source URL. Smaller team than Sentry, simpler API, fewer features. Often the cheapest option for low-volume apps. If you’re using Honeybadger, you’re already opted out of the “biggest vendor” race.
The common pitfalls cross vendor lines: every system needs (a) the release/version name to match between build and runtime, (b) the source URL in the map to match what the browser reports, and (c) the map file to be reachable by the vendor’s symbolication service. Sentry’s Debug IDs sidestep (a) and (b) — until other vendors adopt similar identifiers, expect to spend setup time on release-name discipline.
Still Not Working?
A few less-obvious failures:
- Maps uploaded but Sentry still shows minified. Wait a minute — Sentry processes uploads asynchronously. Hit the same error again after upload finishes.
No artifact found matching URL. URL is one of: bundler emitted different filename than what’s served (hashes), CDN rewrites paths, or--url-prefixdoesn’t match. Try--url-prefix=~/(any path).- Wrong file paths in maps after build. Use
--rewriteto flatten paths or setsourceRootin tsconfig. - CI uploads run but no release shows. Check
SENTRY_ORG/SENTRY_PROJECTenv vars match the project you’re viewing. Wrong project = artifacts go elsewhere. - Plugin works locally, fails in CI. Set
SENTRY_AUTH_TOKENas a CI secret withproject:read,project:releases,org:readscopes. Don’t use a personal token. - Source maps reveal proprietary code in inspector.
hideSourceMaps: truestrips the comment but maps may still be on the server. Delete them post-build. Cannot upload source map: file too large. Sentry has per-artifact size limits. Split huge bundles or use bundling that emits smaller chunks.Debug IDmismatch. Modern Sentry uses Debug IDs (instead of release+URL matching) — but only if the bundler plugin injects them. Use the official plugins.- Source maps stop working after switching CDNs. If you serve assets from a new origin, the runtime URLs change. Either re-upload with the new
--url-prefix, or switch to Debug IDs and stop caring about URLs entirely. @sentry/cliworks locally but CI uploads return 200 with no artifacts. Almost always an auth scope issue.project:readlets you query;project:writeandproject:releasesare required for upload. Test withsentry-cli infofrom CI to confirm the token resolves to the expected org/project.- Self-hosted Sentry rejects modern Debug-ID uploads. Debug IDs need self-hosted Sentry 23.x or newer. Older deployments only understand the release+URL model, even if your bundler plugin tries the modern path. Pin the plugin’s
release.createandsourcemaps.assetsoptions to the legacy flow.
For related error tracking and observability issues, see Sentry not working, OpenTelemetry not working, Webpack bundle size too large, and Vite failed to resolve import.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Loading chunk failed / ChunkLoadError
How to fix 'Loading chunk failed', 'ChunkLoadError', and 'Failed to fetch dynamically imported module' in webpack, Next.js, React, and Vite. Covers stale deployments, CDN caching, publicPath misconfiguration, service worker cache, code splitting, dynamic import retry strategies, React.lazy error boundaries, and Next.js-specific solutions.
Fix: Webpack/Vite Path Alias Not Working — Module Not Found with @/ Prefix
How to fix path alias errors in webpack and Vite — configuring resolve.alias, tsconfig paths, babel-plugin-module-resolver, Vite alias configuration, and Jest moduleNameMapper.
Fix: Module not found: Can't resolve / Cannot find module or its corresponding type declarations
How to fix 'Module not found: Can't resolve' in webpack, Vite, and React, and 'Cannot find module or its corresponding type declarations' in TypeScript. Covers missing packages, wrong import paths, case sensitivity, path aliases, node_modules corruption, monorepo hoisting, barrel files, and asset imports.
Fix: React Compiler Not Working — ESLint Plugin, Babel Setup, Bail-Outs, and Vite/Next.js Config
How to fix React Compiler issues — eslint-plugin-react-compiler not flagging, babel-plugin-react-compiler not running, 'Function contains a code construct that prevents compilation', Next.js 15 config, and removing useMemo/useCallback safely.