Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ export default defineConfig({
// Generate sourcemaps for Sentry. "hidden" produces .map files without
// adding //# sourceMappingURL comments to the output (the debug IDs
// injected post-build by `sentry sourcemap inject` are used instead).
//
// Astro 6 / Vite 7 reads `sourcemap` from
// `vite.environments.{client,ssr}.build.sourcemap` (Environments API),
// not the legacy top-level `vite.build.sourcemap`.
vite: {
build: {
sourcemap: "hidden",
environments: {
client: { build: { sourcemap: "hidden" } },
ssr: { build: { sourcemap: "hidden" } },
},
},
integrations: [
Expand Down
24 changes: 24 additions & 0 deletions docs/src/fragments/commands/sourcemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,27 @@ sentry sourcemap upload ./dist --release 1.0.0
# Set a custom URL prefix
sentry sourcemap upload ./dist --url-prefix '~/static/js/'
```

## Error handling

Both `sentry sourcemap inject` and `sentry sourcemap upload` exit with an
error if zero JS + sourcemap pairs are discovered in the target
directory. This catches silent bundler misconfigurations — the most
common cause is a bundler that isn't emitting `.map` files:

```
# Vite / Astro: set `vite.build.sourcemap: "hidden"` (Astro 5) or
# `vite.environments.client.build.sourcemap: "hidden"` (Astro 6+).

# webpack: set `devtool: "hidden-source-map"`.

# esbuild: set `sourcemap: true` or `sourcemap: "linked"`.
```

For CI steps that may run against legitimately-empty directories (e.g.,
library-only repos, conditional release skips), pass `--allow-empty` to
suppress the error:

```bash
sentry sourcemap upload ./dist --allow-empty
```
4 changes: 4 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Inject debug IDs into JavaScript files and sourcemaps
**Flags:**
- `--ext <value> - Comma-separated file extensions to process (default: .js,.cjs,.mjs)`
- `--dry-run - Show what would be modified without writing`
- `--allow-empty - Exit successfully when no JS + sourcemap pairs are found (default: error out to catch silent build misconfigurations)`

**Examples:**

Expand All @@ -39,6 +40,7 @@ Upload sourcemaps to Sentry
**Flags:**
- `--release <value> - Release version to associate with the upload`
- `--url-prefix <value> - URL prefix for uploaded files (default: ~/) - (default: "~/")`
- `--allow-empty - Exit successfully when no JS + sourcemap pairs are found (default: error out to catch silent build misconfigurations)`

**Examples:**

Expand All @@ -51,6 +53,8 @@ sentry sourcemap upload ./dist --release 1.0.0

# Set a custom URL prefix
sentry sourcemap upload ./dist --url-prefix '~/static/js/'

sentry sourcemap upload ./dist --allow-empty
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
37 changes: 35 additions & 2 deletions src/commands/sourcemap/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
} from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
assertDirectoryReadable,
buildEmptyDiscoveryError,
diagnoseEmptyDiscovery,
discoverFilePairs,
type InjectResult,
injectDirectory,
} from "../../lib/sourcemap/inject.js";
Expand Down Expand Up @@ -55,10 +59,15 @@ export const injectCommand = buildCommand({
"Scans a directory for .js/.mjs/.cjs files and their companion .map files, " +
"then injects Sentry debug IDs for reliable sourcemap resolution.\n\n" +
"The injection is idempotent — files that already have debug IDs are skipped.\n\n" +
"Exits with an error if zero JS + sourcemap pairs are discovered " +
"(typical cause: bundler not emitting .map files). Pass " +
"--allow-empty to suppress this check for directories that may " +
"legitimately be empty.\n\n" +
"Usage:\n" +
" sentry sourcemap inject ./dist\n" +
" sentry sourcemap inject ./build --ext .js,.mjs\n" +
" sentry sourcemap inject ./out --dry-run",
" sentry sourcemap inject ./out --dry-run\n" +
" sentry sourcemap inject ./maybe-empty --allow-empty",
},
output: {
human: formatInjectResult,
Expand Down Expand Up @@ -88,14 +97,38 @@ export const injectCommand = buildCommand({
optional: true,
default: false,
},
"allow-empty": {
kind: "boolean",
brief:
"Exit successfully when no JS + sourcemap pairs are found " +
"(default: error out to catch silent build misconfigurations)",
optional: true,
default: false,
},
},
},
async *func(
this: SentryContext,
flags: { ext?: string; "dry-run"?: boolean },
flags: { ext?: string; "dry-run"?: boolean; "allow-empty"?: boolean },
dir: string
) {
// Discover pairs read-only first so we don't error after partially
// mutating files. Zero *discovered* pairs (distinct from zero
// *injected* — the idempotent re-run case) almost always means a
// missing-.map bundler misconfiguration; --allow-empty opts out.
await assertDirectoryReadable(dir);

const extensions = flags.ext?.split(",").map((e) => e.trim());
const extSet = extensions
? new Set(extensions.map((e) => (e.startsWith(".") ? e : `.${e}`)))
: undefined;

const pairs = await discoverFilePairs(dir, extSet);
if (pairs.length === 0 && !flags["allow-empty"]) {
const diag = await diagnoseEmptyDiscovery(dir, { extensions });
throw buildEmptyDiscoveryError(dir, diag);
}

const results = await injectDirectory(dir, {
extensions,
dryRun: flags["dry-run"],
Expand Down
90 changes: 64 additions & 26 deletions src/commands/sourcemap/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@ import { ContextError } from "../../lib/errors.js";
import { mdKvTable, renderMarkdown } from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
import { injectDirectory } from "../../lib/sourcemap/inject.js";
import {
assertDirectoryReadable,
buildEmptyDiscoveryError,
diagnoseEmptyDiscovery,
discoverFilePairs,
injectDirectory,
} from "../../lib/sourcemap/inject.js";

/** Result type for the upload command. */
type UploadCommandResult = {
/** Organization slug. */
org: string;
/** Project slug. */
project: string;
/** Organization slug. Omitted when --allow-empty short-circuits before
* org/project resolution. */
org?: string;
/** Project slug. Omitted in the same short-circuit case. */
project?: string;
/** Release version, if provided. */
release?: string;
/** Number of file pairs uploaded. */
Expand All @@ -33,11 +40,14 @@ type UploadCommandResult = {

/** Format human-readable output for upload results. */
function formatUploadResult(data: UploadCommandResult): string {
const rows: [string, string][] = [
["Organization", data.org],
["Project", data.project],
["Files uploaded", String(data.filesUploaded)],
];
const rows: [string, string][] = [];
if (data.org) {
rows.push(["Organization", data.org]);
}
if (data.project) {
rows.push(["Project", data.project]);
}
rows.push(["Files uploaded", String(data.filesUploaded)]);
if (data.release) {
rows.push(["Release", data.release]);
}
Expand All @@ -54,10 +64,15 @@ export const uploadCommand = buildCommand({
"debug-ID-based matching.\n\n" +
"Automatically injects debug IDs into any files that don't already have them.\n" +
"Org/project are auto-detected from DSN, env vars, or config defaults.\n\n" +
"Exits with an error if zero JS + sourcemap pairs are discovered " +
"(typical cause: bundler not emitting .map files). Pass " +
"--allow-empty to suppress this check for directories that may " +
"legitimately be empty.\n\n" +
"Usage:\n" +
" sentry sourcemap upload ./dist\n" +
" sentry sourcemap upload ./dist --release 1.0.0\n" +
" sentry sourcemap upload ./dist --url-prefix '~/static/js/'",
" sentry sourcemap upload ./dist --url-prefix '~/static/js/'\n" +
" sentry sourcemap upload ./maybe-empty --allow-empty",
},
output: {
human: formatUploadResult,
Expand Down Expand Up @@ -87,14 +102,50 @@ export const uploadCommand = buildCommand({
optional: true,
default: "~/",
},
"allow-empty": {
kind: "boolean",
brief:
"Exit successfully when no JS + sourcemap pairs are found " +
"(default: error out to catch silent build misconfigurations)",
optional: true,
default: false,
},
},
},
async *func(
this: SentryContext,
flags: { release?: string; "url-prefix"?: string },
flags: {
release?: string;
"url-prefix"?: string;
"allow-empty"?: boolean;
},
dir: string
) {
// Resolve org/project via the standard cascade
// Validate the directory and discover pairs read-only first so we
// don't write debug IDs when the upload won't proceed (empty dir,
// typoed path, missing credentials).
await assertDirectoryReadable(dir);
const pairs = await discoverFilePairs(dir);

if (pairs.length === 0) {
if (!flags["allow-empty"]) {
const diag = await diagnoseEmptyDiscovery(dir);
throw buildEmptyDiscoveryError(dir, diag);
}
// --allow-empty: nothing to upload, so don't require Sentry
// credentials. This makes the flag actually usable in the
// library-only / conditional-release-skip cases the docs name.
yield new CommandOutput<UploadCommandResult>({
release: flags.release,
filesUploaded: 0,
});
return {
hint:
"No JS + sourcemap pairs found in the target directory. " +
"If this is unexpected, check your bundler emits .map files.",
Comment thread
cursor[bot] marked this conversation as resolved.
};
}

const resolved = await resolveOrgAndProject({
cwd: this.cwd,
usageHint: USAGE_HINT,
Expand All @@ -104,21 +155,8 @@ export const uploadCommand = buildCommand({
}
const { org, project } = resolved;

// Discover and inject debug IDs into JS + sourcemap pairs
const results = await injectDirectory(dir);

if (results.length === 0) {
yield new CommandOutput<UploadCommandResult>({
org,
project,
release: flags.release,
filesUploaded: 0,
});
return {
hint: "No JS + sourcemap pairs found. Run `sentry sourcemap inject` first.",
};
}

const urlPrefix = flags["url-prefix"] ?? "~/";

// Build artifact file list with paths relative to the upload directory
Expand Down
Loading
Loading