Capacitor/SvelteKit Android sourcemaps uploaded with sentry-cli not resolving in Sentry
A Capacitor + SvelteKit Android app uploads its mobile sourcemaps from the build tool with sentry-cli sourcemaps inject build followed by sentry-cli sourcemaps upload --release "$VITE_SENTRY_RELEASE" build, and the build reports success. Every error that arrives in Sentry from the shipped app still has minified frames like /_app/immutable/chunks/CtEj1nXt.js:1 in Be. Checked the obvious things and all of them look correct: sourcemap: 'hidden' is set in vite.config so .map files really are produced, inject runs before upload so debug IDs are embedded, the CLI exits 0, and sentry-cli releases list --org ORG --project PROJECT shows the release the app's events are tagged with. Assumed the release must therefore have artifacts and went looking at dist/debug-id matching instead. sentry-cli releases files <release> list prints nothing for that release, but that is not evidence either way because the legacy files API does not list debug-ID artifact bundles at all. No warning is emitted anywhere in the pipeline.
Root cause
The release name the build tool computes and the release name the shipped bundle reports are two independent values, and nothing in the pipeline compares them. If they diverge, sourcemaps upload happily creates a brand new, empty-looking release under the build-time name while every event arrives tagged with the runtime name, so lookup never matches and every frame stays minified.
In our case there were three producers of the version segment and they disagreed:
| Producer | Source |
|---|---|
build tool → VITE_SENTRY_RELEASE | mobile_version.json (25.8.1) |
vite.config.ts → __APP_VERSION__ | mobile_version.json when MOBILE=true, else package.json |
globals.ts → SENTRY_RELEASE | `import.meta.env.VITE_SENTRY_RELEASE |
The shipped app reported app.<id>@0.1.0+0a1b2c3d — the package.json version — while the upload targeted app.<id>@25.8.1+0a1b2c3d. A fallback chain designed to keep a bare npm run build working is exactly what makes this silent: the fallback fires and produces a plausible release string instead of failing.
The deeper trap is the verification step people reach for. Seeing the runtime release present in sentry-cli releases list feels like confirmation, but upload creates the release row as a side effect, so its presence proves nothing about which name owns the artifacts. And releases files <release> list returning empty is not disconfirmation either, because modern debug-ID uploads land as artifact bundles that the legacy files endpoint does not enumerate.
Fix
Assert, in the build, that the artifact you are about to upload maps for actually reports the name you are uploading under. The built JS contains the release string verbatim, so this is a grep:
release = build_env['VITE_SENTRY_RELEASE']
if not any(release in p.read_text(errors='ignore')
for p in pathlib.Path('build').rglob('*.js')):
raise BuildError(
f'built bundle does not contain release {release!r} — the release env var did '
'not reach the client bundle, so sourcemaps would upload under a name no '
'event reports')Run it for debug builds too, not just store builds: it can only fail when the pipeline is genuinely broken, and the fast inner loop is where you want to hear about it. Prove it bites by setting the release env var to a junk value once and confirming the build fails.
Two companions worth adding at the same time:
- Refuse to build when the commit component is empty.
app.<id>@25.8.1with no+<commit>cannot be traced back to source, and an unattributable artifact is the thing you least want in a store. - Pass
--diston the upload (android/ios) to match the runtimedist, which for@sentry/capacitorisCapacitor.getPlatform(). Debug IDs make dist matching unnecessary in the happy path, but two platform builds of one commit otherwise share a single release namespace, and a mismatcheddistcan break the legacy resolution path you fall back to when injection did not happen.
Why the usual debugging order wastes time here
Minified mobile frames send you toward inject, debug IDs, .map retention, and CLI auth. Check release-name identity first: it is a one-command check (grep the bundle for the string you uploaded under) and it invalidates every downstream hypothesis at once.