Skip to content

Fix: Sentry Source Maps Not Working — Release Matching, sentry-cli Upload, Vite/Webpack Plugins

FixDevs ·

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 maps

But 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:

  1. The URL of the JS file (https://app.example.com/static/js/main-abc123.js).
  2. The release name reported by the SDK ([email protected]).
  3. 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.js but runtime is main-abc123.js (or vice versa).
  • Maps not actually generated. Build doesn’t emit .map files, or the bundler inlines them.
  • Wrong upload directory. sentry-cli walks the path but skips files that don’t have .map extensions or are in node_modules.

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,” matching https://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 — flattens sources paths 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" ./dist

Both 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 .map files.
  • inlineSources — include the original .ts source inside the map. Sentry can show the original code in error views.
  • sourceRoot — used by Sentry to resolve relative sources paths.

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.

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-prefix doesn’t match. Try --url-prefix=~/ (any path).
  • Wrong file paths in maps after build. Use --rewrite to flatten paths or set sourceRoot in tsconfig.
  • CI uploads run but no release shows. Check SENTRY_ORG/SENTRY_PROJECT env vars match the project you’re viewing. Wrong project = artifacts go elsewhere.
  • Plugin works locally, fails in CI. Set SENTRY_AUTH_TOKEN as a CI secret with project:read, project:releases, org:read scopes. Don’t use a personal token.
  • Source maps reveal proprietary code in inspector. hideSourceMaps: true strips 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 ID mismatch. Modern Sentry uses Debug IDs (instead of release+URL matching) — but only if the bundler plugin injects them. Use the official plugins.

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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles