Skip to content

fix(sourcemap): error on zero pairs + restore docs sourcemap emission#846

Merged
BYK merged 6 commits intomainfrom
byk/fix-sourcemap-upload-strictness
Apr 25, 2026
Merged

fix(sourcemap): error on zero pairs + restore docs sourcemap emission#846
BYK merged 6 commits intomainfrom
byk/fix-sourcemap-upload-strictness

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented Apr 24, 2026

Fixes #845.

Summary

Two related bugs caused Sentry events for cli.sentry.dev to ship unsymbolicated in production with no CI signal:

  1. Docs build emitted no .map files since the Astro 6 upgrade (security(deps): upgrade docs to Astro 6 / Starlight 0.38 #816). astro.config.mjs sets vite.build.sourcemap: "hidden", but Astro 6 reads from vite.environments.{client,ssr}.build.sourcemap (Vite 7 Environments API), falling back to false. The legacy top-level path is silently ignored for the client bundle.
  2. sentry sourcemap inject/upload silently succeeded on zero pairs. The docs step logged "Files uploaded: 0" and exited 0 for 20+ consecutive main-push runs, hiding the misconfiguration.

Behavior change

sentry sourcemap inject and sentry sourcemap upload now exit non-zero by default when zero JS+sourcemap pairs are discovered. Callers that legitimately invoke these commands on potentially-empty directories should pass --allow-empty to preserve the old behavior.

This is a deliberate default-strict change — the silent-zero-file failure mode has no legitimate use case in CI and turns every bundler regression into a silent production outage. Worth calling out in release notes.

Changes

  • docs/astro.config.mjs: set sourcemap: "hidden" on vite.environments.client (and ssr, defensive). Verified locally: docs/dist/_astro/ now contains 11 .map files (previously 0), and sentry sourcemap inject docs/dist/ injects debug IDs into 5 module chunks.
  • src/lib/sourcemap/inject.ts:
    • Export discoverFilePairs for read-only discovery (no writes).
    • Add assertDirectoryReadable(dir) — distinguishes ENOENT / not-a-directory / EACCES with specific error messages.
    • Add diagnoseEmptyDiscovery(dir) + buildEmptyDiscoveryError(dir, diag) — tailors the zero-pair error to the specific shape (empty dir / JS-only / maps-only / basename-mismatch).
  • src/commands/sourcemap/{inject,upload}.ts:
    • New control flow: assertDirectoryReadable → discoverFilePairs → empty-check → resolveOrgAndProject → injectDirectory → upload. All pre-API error paths are side-effect-free — no debug IDs get written until we know the upload will proceed.
    • Add --allow-empty flag (boolean, default false).
    • Tailored error messages with bundler-specific config snippets (Vite/Astro, webpack, esbuild).
  • Tests: 16 new tests in test/commands/sourcemap/upload.test.ts covering every empty-discovery branch, the non-existent-dir and not-a-directory branches, happy path (uploadSourcemaps spy), and a regression test that asserts the command does not mutate files on the error path.
  • Docs: new "Error handling" section in docs/src/fragments/commands/sourcemap.md with --allow-empty examples and bundler-config cheat-sheet.

CI wiring

No workflow changes needed. The default-strict behavior catches any future regression automatically — .github/workflows/ci.yml and docs-preview.yml don't pass --allow-empty, so they'll fail loud if .map files stop being emitted again.

Verification

  • 47 sourcemap-related tests pass locally (bun test test/lib/sourcemap test/commands/sourcemap test/lib/api/sourcemaps.test.ts).
  • bun run typecheck clean.
  • bun run lint has only a single pre-existing warning (unrelated, in src/lib/formatters/markdown.ts).
  • End-to-end manual verification covers all 6 error branches (empty dir, JS-only, maps-only, nonexistent, not-a-directory, basename-mismatch) plus --allow-empty and the happy path.

Review cycle

  • Initial review (1 Cursor Bugbot finding on auth mocking) — false positive; replied with evidence from CI logs.
  • Self-review via subagent surfaced 5 IMPORTANT and 5 MINOR findings — addressed in commit 1094d9bf. See review thread on that commit for per-item responses.
  • Second-round bot pass (Cursor + Seer) found a real side-effect-ordering issue (debug IDs written before credential resolution) and a misleading --allow-empty hint — fixed in e8452ad7 with a regression test that locks in the discover-first invariant.
  • Final: all bots SUCCESS / NEUTRAL, zero unresolved review threads, mergeable = CLEAN.

Follow-ups (not in this PR)

Two related bugs caused Sentry events for the docs site (cli.sentry.dev)
to ship unsymbolicated in production, with no CI signal:

1. **Docs build emitted no .map files.** The `astro.config.mjs` sets
   `vite.build.sourcemap: "hidden"`, but Astro 6 reads that from
   `vite.environments.{client,ssr}.build.sourcemap` (Vite 7's Environments
   API — see `astro/dist/core/build/static-build.js:281`), falling back
   to `false` if unset. The legacy top-level path is ignored for the
   client bundle. Fix: set the flag on both environments.

2. **`sentry sourcemap inject/upload` silently succeeded on zero pairs.**
   When the bundler didn't emit `.map` files, `sourcemap upload` would
   print "Files uploaded: 0" and exit 0, hiding the misconfiguration.
   Add a default-strict behavior: throw `ValidationError` when no
   JS + sourcemap pairs are discovered, with `--allow-empty` as the
   escape hatch for callers that legitimately invoke upload on
   potentially-empty directories.

   Applies symmetrically to `inject` — a zero-discovery inject almost
   always means a missing-.map misconfiguration, distinct from the
   idempotent "0 injected, N skipped" re-run case which still succeeds.

   Docs/CI workflows don't need changes: the default-strict behavior
   catches the regression automatically.

Verified locally: after fix (1), `docs/dist/_astro/` contains 11
`.map` files and `sentry sourcemap inject docs/dist/` injects debug IDs
into 5 JS chunks (matching the number of module chunks that have
companion maps — the remaining 6 `.map` files are split-chunk variants
without a primary JS counterpart, expected).

The CLI binary build already uploads sourcemaps correctly per target
(see `script/build.ts#uploadSourcemapToSentry`) — this was only a
docs-site regression.

Ref: CLI binary zstd compression (#823) couldn't be evaluated via docs
uploads because 0 bytes were ever being sent; the CLI binary path has
been exercising zstd since PR #823 merged.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-846/

Built to branch gh-pages at 2026-04-25 18:44 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

Codecov Results 📊

5950 passed | Total: 5950 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests 📈 +17
Passed Tests 📈 +17
Failed Tests
Skipped Tests

All tests are passing successfully.

❌ Patch coverage is 76.36%. Project has 12877 uncovered lines.
✅ Project coverage is 75.51%. Comparing base (base) to head (head).

Files with missing lines (3)
File Patch % Lines
src/lib/sourcemap/inject.ts 79.79% ⚠️ 19 Missing
src/commands/sourcemap/upload.ts 71.74% ⚠️ 13 Missing
src/commands/sourcemap/inject.ts 72.00% ⚠️ 7 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    75.17%    75.51%    +0.34%
==========================================
  Files          285       285         —
  Lines        52476     52577      +101
  Branches         0         0         —
==========================================
+ Hits         39443     39700      +257
- Misses       13033     12877      -156
- Partials         0         0         —

Generated by Codecov Action

Comment thread test/commands/sourcemap/upload.test.ts
Addresses self-review findings on PR #846 (see review thread):

- **Non-existent / non-directory argument now produces a distinct error.**
  Added `assertDirectoryReadable()` in `src/lib/sourcemap/inject.ts` that
  runs BEFORE the discovery walk and throws `ValidationError` with
  specific wording for ENOENT ("Directory '…' does not exist.") and
  non-directory paths ("Path '…' is not a directory."). The previous
  zero-pair error erroneously told users their bundler was
  misconfigured when the actual problem was a typoed path.

- **Reorder `sentry sourcemap upload` checks: dir-read → discover →
  resolve org/project.** A user hitting the bundler misconfig on a
  laptop without Sentry credentials configured used to see "Organization
  and project are required" (misleading — the upstream cause was
  something else entirely). Moving the directory check first ensures
  local/unauthenticated invocations get the actionable error.

- **Richer zero-pair diagnostics.** New `diagnoseEmptyDiscovery()`
  re-walks the dir once counting JS and .map files separately, and
  `buildEmptyDiscoveryError()` produces a tailored message for each
  shape:
    - 0 JS + 0 maps → "contains no JS or sourcemap files"
    - N JS + 0 maps → "Found N JS file(s) but no companion .map …"
    - 0 JS + N maps → "Found N .map file(s) but no companion JS …"
    - N JS + M maps, no basename match → "no JS file has a matching
      `<name>.map` companion"
  All four branches are regression-tested in
  `test/commands/sourcemap/upload.test.ts`.

- **Add happy-path upload test.** Verifies that a dir with a valid
  JS+map pair reaches `uploadSourcemaps` with the right org/project and
  artifact count. Previously only error paths were tested — the feature
  we're protecting was untested at the command level.

- **Drop dead `process.stdout`/`process.stderr` keys from test context.**
  The wrapper uses `this.stdout`/`this.stderr` directly (see
  `src/lib/command.ts:566`); the nested `process.*` keys were misleading
  and contradicted the Stricli convention documented in AGENTS.md.

- **Tighten doc fragments + command descriptions.** Both commands now
  explicitly mention the strict-by-default behavior and `--allow-empty`
  in `fullDescription` and in `docs/src/fragments/commands/sourcemap.md`
  (which renders into the public docs site and SKILL.md for AI agents).

- **Soften the Astro config comment.** The SSR `sourcemap: "hidden"`
  line is a no-op today (static-only site), but guards against regressions
  if a server adapter is added later — comment now says so, and adds a
  crossref to issue #845.

Test count: 15 new tests (5 added over the initial 10), all passing in
CI. Full sourcemap suite: 46 pass, 0 fail, 301 expect() calls.
Comment thread src/commands/sourcemap/upload.ts Outdated
Comment thread src/commands/sourcemap/upload.ts Outdated
BYK added 3 commits April 24, 2026 19:12
Addresses two follow-up bot findings on the previous commit
(1094d9b):

- **Cursor Bugbot (Medium)**: "File modifications happen before
  credential validation." Previously, `injectDirectory` ran BEFORE
  `resolveOrgAndProject`, so an empty/misconfigured directory would
  still have no files to mutate — but a non-empty directory with
  missing SENTRY_ORG/SENTRY_PROJECT would get debug IDs written into
  every JS pair before the command errored on credential resolution.
  An unannounced behavior change.

  Fix: split `injectDirectory` into a read-only discovery pass
  (`discoverFilePairs`, now exported) and the mutating inject pass.
  New control flow in both commands:

    1. assertDirectoryReadable(dir)       — stat, no writes
    2. discoverFilePairs(dir)              — walk, no writes
    3. if empty and not --allow-empty → error (no writes)
    4. resolveOrgAndProject                — may fail, no writes
    5. if empty and --allow-empty → early exit (no writes)
    6. injectDirectory(dir)                — writes debug IDs
    7. uploadSourcemaps                    — API call

  Upload-to-API failures still leave files mutated (user explicitly
  requested upload, injection is part of that operation), but all
  error paths BEFORE the API call are now side-effect-free.

- **Seer (Low)**: The `--allow-empty` success-path hint told users to
  run `sentry sourcemap inject`, which the upload command had already
  attempted internally (via `injectDirectory`). Misleading guidance.

  Fix: replace the hint with "If this is unexpected, check your
  bundler emits .map files." — which matches the actual root cause
  for anyone who lands in this state.

Added a regression test (`test/commands/sourcemap/upload.test.ts`
"error path does not mutate files (js-only dir)") that writes a JS
file, invokes upload, asserts the error fires, and verifies the file
bytes are unchanged afterwards. Locks in the discover-first
invariant so future refactors can't silently reintroduce the
pre-error mutation.

16 tests in the new file, all passing.
- docs/astro.config.mjs: trim 12-line comment with date ranges and PR
  refs to 3 lines explaining the technical reason for the
  Environments-API path.
- src/lib/sourcemap/inject.ts:
  - Trim JSDoc on assertDirectoryReadable, diagnoseEmptyDiscovery,
    discoverFilePairs, buildEmptyDiscoveryError to single-purpose
    descriptions; drop caller-specific narration ("see callers in
    src/commands/sourcemap/", "the getsentry/cli docs-site silent
    failure mode", historical context that belongs in commit messages).
  - Replace `extensions: new Set([...jsExtensions, ".map"])` array
    spread with mutate-and-add on a fresh Set; avoids the intermediate
    array and a mutation hazard against the shared DEFAULT_EXTENSIONS
    when the caller doesn't override extensions.
- src/commands/sourcemap/upload.ts: drop "Phase 1 / Phase 2" framing
  comments; the call sequence is self-explanatory from the function
  names. Trim the multi-line block restating the PR description.
- test/commands/sourcemap/upload.test.ts:
  - Drop the runFunc() pass-through wrapper (just call func.call()
    directly, matching test/commands/init.test.ts).
  - Simplify makeContext() to return the ctx directly instead of a
    {ctx, stdout, stderr} struct — tests don't read stdout/stderr.
  - Trim header comment from 13 lines to 4; drop the loader()
    explainer comment that referenced `src/lib/command.ts:566` and
    AGENTS.md.
  - Drop in-test comments referencing #845/#846/Bugbot — process noise
    that doesn't help future readers.
Final-review polish:

- src/lib/sourcemap/inject.ts: `diagnoseEmptyDiscovery` now matches the
  Set-construction convention from `injectDirectory` (single ternary +
  one mutation) instead of clear-and-refill.
- src/commands/sourcemap/{inject,upload}.ts: align `--allow-empty`
  brief wording (both: "Exit successfully when no JS + sourcemap pairs
  are found …").
- src/commands/sourcemap/inject.ts: drop "Phase 1 / Phase 2" comments
  (missed in the prior slop-removal pass).
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5dcf415. Configure here.

Comment thread src/commands/sourcemap/upload.ts
Cursor Bugbot caught: with `--allow-empty`, the empty-pairs path still
called `resolveOrgAndProject` and threw `ContextError` when no
DSN/env/config was available. The use cases the docs name for
`--allow-empty` (library-only repos, conditional release skips) often
run without that context configured, so the flag wasn't actually usable
in its advertised scenarios.

Short-circuit the empty + allow-empty branch before resolution. Make
`org`/`project` optional on the result type so the no-op output renders
without them. Adds a regression test that runs the path with
SENTRY_ORG/SENTRY_PROJECT cleared.
@BYK BYK merged commit 3f326be into main Apr 25, 2026
26 checks passed
@BYK BYK deleted the byk/fix-sourcemap-upload-strictness branch April 25, 2026 18:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docs sourcemap upload has been a no-op since Astro 6 upgrade (#816); CLI silently succeeds on 0 files

1 participant