Configuring Source Map Upload in the Vite Sentry Plugin
The @sentry/vite-plugin package hooks into the Rollup build that Vite runs and does three things in one pass: it uploads your .map files, stamps a release identifier onto every chunk, and injects a debug ID that pairs each bundle with its map even when file names or query strings change. This how-to walks through wiring the plugin so production stack traces resolve to original source. It builds on Vite Build Settings for Accurate Stack Traces and fits into the wider Source Map Generation & Stack Trace Debugging pipeline; if you have not yet decided how maps are emitted, read How to Generate Hidden Source Maps in Vite first, because the plugin only uploads maps that Vite actually writes to disk.
Symptom / Trigger
You have installed the Sentry SDK, deployed to production, and errors are arriving in the dashboard — but every frame still points at minified output. The event detail shows raw column offsets and hash-named files instead of your components, and a yellow banner reads “We could not find the source map for this event.”
TypeError: Cannot read properties of null (reading 'focus')
at Ct (index-D7f3Ka9x.js:48:1902)
at onClick (index-D7f3Ka9x.js:48:2471)
at HTMLButtonElement.<anonymous> (vendor-Bq2mLp0w.js:12:88410)
⚠ Source map not found for index-D7f3Ka9x.js
Expected release: unknown · debug_id: none
The two clues in that banner are decisive. Expected release: unknown means the browser SDK never reported a release, so Sentry has nothing to match uploaded artifacts against. debug_id: none means the bundle carries no fingerprint, so even a correctly uploaded map cannot be associated with it. Both are exactly what the Vite plugin fixes.
It is worth noticing what is not the problem here. The maps may well exist and may even have been uploaded by a hand-rolled CI step — the failure is purely that Sentry cannot decide which map belongs to the chunk that threw the error. Symbolication is a matching problem before it is an upload problem, and a dashboard full of minified frames almost always means the match key is wrong, not that the artifacts are absent. Keeping that distinction in mind saves you from re-uploading maps that were already there.
Root Cause Explanation
Manual sentry-cli sourcemaps upload calls work, but they depend on you passing a --release value that matches the release string configured in Sentry.init() at runtime. Those two values live in different files, get set at different times, and drift the moment someone edits one and forgets the other. The broken pattern looks harmless:
// vite.config.ts — maps are emitted but nothing links them to a release
export default defineConfig({
build: { sourcemap: true }, // Rollup writes .map files, then... nothing uploads them
});
Sentry’s newer symbolication path solves the drift by not relying on the release string at all for the match. During the build, the plugin writes a unique debug ID into both the JS chunk (as a //# debugId= comment plus a global marker) and the corresponding .map file. At runtime the SDK reads the debug ID out of the executing bundle and sends it with the event; Sentry looks up the uploaded artifact by that ID. The release still matters for grouping and for issue-to-deploy correlation, but symbolication itself becomes a content-addressed lookup that cannot silently drift. The plugin automates all of it — emitting the IDs, injecting them, and uploading — inside the same vite build invocation.
The reason a debug ID is more reliable than a filename is that hashed asset names are not stable across environments. A chunk called index-D7f3Ka9x.js on your CDN may be served from /assets/ in production but from a versioned path behind a proxy, and a filename-plus-URL-prefix match breaks the moment either changes. The debug ID travels inside the bundle, so it survives renames, path rewrites, and cache-busting query strings. That property is what lets the plugin skip the fragile --url-prefix bookkeeping that manual uploads depend on, and it is why a single wrong prefix no longer silently disables symbolication for an entire release.
Step-by-Step Fix
- Install the plugin as a dev dependency alongside your existing Sentry SDK.
# The plugin runs at build time only, so --save-dev is correct
npm install --save-dev @sentry/vite-plugin
- Create an auth token with
project:releasesscope and store it as an environment variable, never in the repo.
# Set in CI secrets / local .env — the plugin reads it automatically
export SENTRY_AUTH_TOKEN="sntrys_your_token_here"
- Add the plugin to
vite.config.tsafter your framework plugins, and enable source maps so Vite writes the.mapfiles it needs to upload.
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { sentryVitePlugin } from '@sentry/vite-plugin';
export default defineConfig({
build: { sourcemap: true }, // required: the plugin uploads maps Vite emits
plugins: [
react(),
sentryVitePlugin({
org: 'my-org',
project: 'my-frontend',
authToken: process.env.SENTRY_AUTH_TOKEN, // pulled from env, not committed
}),
],
});
- Pin the release name so the build-time upload and the runtime SDK agree on a single value.
// vite.config.ts — reuse one release string everywhere
const release = process.env.SENTRY_RELEASE ?? process.env.GITHUB_SHA ?? 'dev';
// pass release into sentryVitePlugin({ release: { name: release } })
- Set the same release in
Sentry.init()so events and artifacts correlate on deploys.
// src/main.ts — release must equal the build-time value from step 4
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
release: import.meta.env.VITE_SENTRY_RELEASE, // injected by CI at build time
});
- Delete maps from the deploy artifact after upload so the plugin ships symbols to Sentry but not to your CDN.
// inside sentryVitePlugin({ ... })
sourcemaps: {
filesToDeleteAfterUpload: ['./dist/**/*.map'], // upload, then remove from dist/
},
A note on ordering: the plugin must run after your framework plugin (here react()), because it operates on the final Rollup output once every transform has produced its bundles and maps. Placing it earlier means it injects debug IDs into intermediate code that later transforms overwrite, and the injected marker disappears. The plugin is deliberately the last entry in the plugins array for this reason, and it only activates during vite build — the dev server never triggers upload, so local development stays fast and offline.
With filesToDeleteAfterUpload the plugin handles emit, inject, upload, and cleanup in a single vite build, so no separate postbuild script is required. This matters for build hygiene: because the delete step runs inside the same process that performed the upload, there is no window in which fully populated maps sit in dist/ waiting for a later script that a failed CI run might skip. Either the whole build succeeds with maps uploaded and removed, or it fails before the CDN sync stage ever sees them. The full plugin block reads like this once every option is combined:
// vite.config.ts — complete Sentry plugin configuration
sentryVitePlugin({
org: 'my-org',
project: 'my-frontend',
authToken: process.env.SENTRY_AUTH_TOKEN,
release: { name: process.env.SENTRY_RELEASE ?? 'dev' },
sourcemaps: {
assets: ['./dist/**'], // where to look for .js and .map pairs
filesToDeleteAfterUpload: ['./dist/**/*.map'], // strip maps before CDN sync
},
});
Verification
Confirm the plugin injected debug IDs into the bundles and that the maps left the dist directory before your CDN sync ran:
# 1. Every chunk should carry a debugId comment injected by the plugin
grep -rl "debugId" dist/assets/*.js | wc -l
# Expected: matches the number of JS chunks (non-zero)
# 2. No .map files should remain after filesToDeleteAfterUpload ran
find ./dist -name '*.map' | wc -l
# Expected: 0
# 3. Ask Sentry to explain a real event by its debug ID
npx sentry-cli sourcemaps explain <event-id>
# Expected: "Event has a valid debug id" and a resolved source frame
A resolved frame in the Sentry event view — showing your .tsx file name, the correct line, and surrounding source context — is the definitive pass. If sourcemaps explain reports the debug ID matched an uploaded artifact, symbolication is wired correctly end to end. Run the check against a genuinely minified production event rather than a local one; the dev server serves inline maps that resolve regardless of upload, so a passing frame in development tells you nothing about whether the plugin did its job. Trigger a real error on the deployed build, then explain that event, and treat only that result as authoritative.
Edge Cases & Gotchas
- Debug IDs need a recent SDK. Debug-ID matching requires
@sentry/browser(or the framework SDK) version 7.47 or later at runtime. On an older SDK the plugin still uploads, but Sentry falls back to release-plus-filename matching, so thereleasevalues in step 4 and step 5 must line up exactly or symbolication fails. - The auth token must never be committed. Read it from
process.env.SENTRY_AUTH_TOKENonly. If the token is missing the plugin logs a warning and skips upload rather than failing the build, which quietly ships un-symbolicated releases — assert the variable exists in CI before building. sourcemapmust be truthy. Withbuild.sourcemap: falsethere are no.mapfiles to upload and the plugin becomes a no-op. Usetrueor'hidden'; the plugin deletes them afterward either way, so'hidden'adds no benefit here beyond suppressing the discovery comment.- Dynamic-import chunks need coverage too. Lazily loaded routes produce their own hashed chunks and maps; make sure the
assetsglob covers every output directory. If your app splits heavily, the Fixing Vite Source Maps with Dynamic Imports reference explains where those chunks land.
FAQ
Do I still need to set a release if the plugin injects debug IDs?
Yes, but for a different reason. Debug IDs handle the symbolication match on their own, so a mismatched release no longer breaks stack traces. The release is still what links an issue to a specific deploy, powers regression detection, and drives release health metrics. Set it in both sentryVitePlugin and Sentry.init() so those features work, and let the debug ID guarantee symbolication independently.
Why did the plugin upload nothing even though the build succeeded?
The two usual causes are a missing SENTRY_AUTH_TOKEN — the plugin warns and skips rather than erroring — and build.sourcemap being unset or false, which means Rollup never wrote any .map files to find. Run the build with debug: true inside sentryVitePlugin to print exactly which artifacts it discovered and attempted to upload.
Can I keep using sentry-cli in CI instead of the plugin?
You can, and the CLI is the right tool for non-Vite build steps or server bundles. For a Vite frontend the plugin is preferable because it injects debug IDs, which the bare sentry-cli sourcemaps upload command does not do unless you also run sentry-cli sourcemaps inject first. The plugin collapses inject, upload, and cleanup into one configured step.
Related
- Vite Build Settings for Accurate Stack Traces — the parent guide covering all Vite build-time map options.
- How to Generate Hidden Source Maps in Vite — emit maps without exposing them on your CDN.
- Fixing Vite Source Maps with Dynamic Imports — keep code-split chunks correctly mapped.
- CI/CD Source Map Upload and Validation — gate uploads and verify them in the pipeline.
- Source Map Generation & Stack Trace Debugging — the full reference for map generation and symbolication.