Skip to content

Commit 12c7f7d

Browse files
committed
feat: implement debug-files bundle-jvm command (#1055)
Create JVM source bundles from directories of Java, Kotlin, Scala, Groovy, and Clojure source files. The bundle is a ZIP archive that can be uploaded to Sentry for source context in JVM stack traces. This command is local-only — it makes no API calls. The bundle is uploaded separately via `debug-files upload --type jvm`. Key features: - Walks a directory for JVM source files (10 extensions supported) - Intelligent filtering: always excludes IDE dirs (.idea, .gradle, etc.) - Smart build-output detection: excludes bin/build/out/target dirs UNLESS they appear under a src/ ancestor (handles JVM package names like com.example.build correctly) - Strips source-set prefixes (src/main/java/, src/test/kotlin/, etc.) to produce package-relative paths matching JVM stack traces - Replaces file extensions with .jvm for symbolicator URL matching - First-seen-wins deduplication with collision warnings - Writes ZIP with SYSB header + manifest.json (symbolic source bundle format) using the existing ZipWriter - Hidden from help (experimental, like legacy CLI) Files: - src/lib/jvm-bundle.ts — core logic (walking, filtering, URL construction, ZIP) - src/commands/debug-files/bundle-jvm.ts — CLI command - src/commands/debug-files/index.ts — route map - src/app.ts — route registration (hidden) - test/lib/jvm-bundle.test.ts — 25 unit tests - test/commands/debug-files/bundle-jvm.test.ts — 6 command tests
1 parent 3af5252 commit 12c7f7d

11 files changed

Lines changed: 1118 additions & 48 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
3+
4+
## Examples
5+
6+
```bash
7+
# Bundle JVM sources with a debug ID
8+
sentry debug-files bundle-jvm --output ./out --debug-id <uuid> ./src
9+
10+
# Exclude additional directories
11+
sentry debug-files bundle-jvm --output ./out --debug-id <uuid> --exclude generated --exclude build-tools ./src
12+
13+
# Output as JSON
14+
sentry debug-files bundle-jvm --output ./out --debug-id <uuid> --json ./src
15+
```
16+
17+
## Important Notes
18+
19+
- This command is **local-only** — it makes no network requests. Upload the
20+
generated bundle separately via `sentry debug-files upload --type jvm`.
21+
- Supported JVM source file extensions: `.java`, `.kt`, `.scala`, `.sc`,
22+
`.groovy`, `.gvy`, `.gy`, `.gsh`, `.clj`, `.cljc`
23+
- Build output directories (`build/`, `target/`, `out/`, `bin/`) are
24+
automatically excluded unless they appear under a `src/` ancestor.
25+
- Source-set prefixes (e.g., `src/main/java/`) are stripped to produce
26+
package-relative paths matching JVM stack traces.

plugins/sentry-cli/skills/sentry-cli/references/cli.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -143,19 +143,6 @@ Uninstall Sentry CLI
143143
- `-f, --force - Force the operation without confirmation`
144144
- `-n, --dry-run - Show what would happen without making changes`
145145

146-
**Examples:**
147-
148-
```bash
149-
# Show what would be removed (dry run)
150-
sentry cli uninstall --dry-run
151-
152-
# Uninstall, keeping config directory
153-
sentry cli uninstall --yes --keep-config
154-
155-
# Full uninstall with confirmation
156-
sentry cli uninstall
157-
```
158-
159146
### `sentry cli upgrade <version>`
160147

161148
Update the Sentry CLI to the latest version

plugins/sentry-cli/skills/sentry-cli/references/code-mappings.md

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,4 @@ Upload code mappings for stack trace linking
1919
- `--repo <value> - Repository name (e.g., owner/repo). Auto-detected from git remote if omitted.`
2020
- `--default-branch <value> - Default branch name. Auto-detected from git remote HEAD if omitted.`
2121

22-
**Examples:**
23-
24-
```bash
25-
# Upload code mappings from a JSON file
26-
sentry code-mappings upload mappings.json
27-
28-
# Specify repository explicitly
29-
sentry code-mappings upload mappings.json --repo owner/repo
30-
31-
# Specify repository and default branch
32-
sentry code-mappings upload mappings.json --repo owner/repo --default-branch develop
33-
34-
# Output as JSON
35-
sentry code-mappings upload mappings.json --json
36-
```
37-
3822
All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.

plugins/sentry-cli/skills/sentry-cli/references/dart-symbol-map.md

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,4 @@ Upload a Dart/Flutter symbol map to Sentry
1919
- `-d, --debug-id <value> - Debug ID (UUID) from the companion native debug file`
2020
- `--no-upload - Validate the file without uploading (dry-run)`
2121

22-
**Examples:**
23-
24-
```bash
25-
# Upload a dart symbol map with a debug ID
26-
sentry dart-symbol-map upload --debug-id 12345678-1234-1234-1234-123456789abc mapping.json
27-
28-
# Validate without uploading
29-
sentry dart-symbol-map upload --debug-id 12345678-1234-1234-1234-123456789abc mapping.json --no-upload
30-
31-
# Output as JSON
32-
sentry dart-symbol-map upload --debug-id 12345678-1234-1234-1234-123456789abc mapping.json --json
33-
```
34-
3522
All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.

plugins/sentry-cli/skills/sentry-cli/references/issue.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ List issues in a project
3131
| `id` | string | Numeric issue ID |
3232
| `shortId` | string | Human-readable short ID (e.g. PROJ-ABC) |
3333
| `title` | string | Issue title |
34-
| `culprit` | string \| null | Culprit string |
34+
| `culprit` | string | Culprit string |
3535
| `count` | string | Total event count |
3636
| `userCount` | number | Number of affected users |
37-
| `firstSeen` | string \| null | First occurrence (ISO 8601) |
38-
| `lastSeen` | string \| null | Most recent occurrence (ISO 8601) |
37+
| `firstSeen` | string | First occurrence (ISO 8601) |
38+
| `lastSeen` | string | Most recent occurrence (ISO 8601) |
3939
| `level` | string | Severity level |
4040
| `status` | string | Issue status |
4141
| `permalink` | string | URL to the issue in Sentry |
@@ -190,11 +190,11 @@ View details of a specific issue
190190
| `id` | string | Numeric issue ID |
191191
| `shortId` | string | Human-readable short ID (e.g. PROJ-ABC) |
192192
| `title` | string | Issue title |
193-
| `culprit` | string \| null | Culprit string |
193+
| `culprit` | string | Culprit string |
194194
| `count` | string | Total event count |
195195
| `userCount` | number | Number of affected users |
196-
| `firstSeen` | string \| null | First occurrence (ISO 8601) |
197-
| `lastSeen` | string \| null | Most recent occurrence (ISO 8601) |
196+
| `firstSeen` | string | First occurrence (ISO 8601) |
197+
| `lastSeen` | string | Most recent occurrence (ISO 8601) |
198198
| `level` | string | Severity level |
199199
| `status` | string | Issue status |
200200
| `permalink` | string | URL to the issue in Sentry |

src/app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { codeMappingsRoute } from "./commands/code-mappings/index.js";
1515
import { dartSymbolMapRoute } from "./commands/dart-symbol-map/index.js";
1616
import { dashboardRoute } from "./commands/dashboard/index.js";
1717
import { listCommand as dashboardListCommand } from "./commands/dashboard/list.js";
18+
import { debugFilesRoute } from "./commands/debug-files/index.js";
1819
import { eventRoute } from "./commands/event/index.js";
1920
import { listCommand as eventListCommand } from "./commands/event/list.js";
2021
import { exploreCommand } from "./commands/explore.js";
@@ -99,6 +100,7 @@ export const routes = buildRouteMap({
99100
cli: cliRoute,
100101
"code-mappings": codeMappingsRoute,
101102
"dart-symbol-map": dartSymbolMapRoute,
103+
"debug-files": debugFilesRoute,
102104
dashboard: dashboardRoute,
103105
org: orgRoute,
104106
project: projectRoute,
@@ -164,6 +166,7 @@ export const routes = buildRouteMap({
164166
trials: true,
165167
sourcemaps: true,
166168
whoami: true,
169+
"debug-files": true,
167170
"send-event": true,
168171
"send-envelope": true,
169172
"bash-hook": true,
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* sentry debug-files bundle-jvm <path>
3+
*
4+
* Create a JVM source bundle from a directory of JVM source files.
5+
* The bundle is a ZIP archive that can be uploaded to Sentry via
6+
* `debug-files upload --type jvm` for source context in stack traces.
7+
*
8+
* This command is local-only — it makes no API calls.
9+
*/
10+
11+
import { mkdir, stat } from "node:fs/promises";
12+
import { join, resolve } from "node:path";
13+
import type { SentryContext } from "../../context.js";
14+
import { buildCommand } from "../../lib/command.js";
15+
import { ValidationError } from "../../lib/errors.js";
16+
import { mdKvTable, renderMarkdown } from "../../lib/formatters/markdown.js";
17+
import { CommandOutput } from "../../lib/formatters/output.js";
18+
import { buildJvmBundle } from "../../lib/jvm-bundle.js";
19+
import { logger } from "../../lib/logger.js";
20+
21+
const log = logger.withTag("debug-files.bundle-jvm");
22+
23+
/** UUID format: 8-4-4-4-12 hex with hyphens. */
24+
const UUID_RE =
25+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
26+
27+
/** Data shape yielded by the command and rendered by both human and JSON modes. */
28+
type BundleJvmResult = {
29+
outputPath: string;
30+
debugId: string;
31+
fileCount: number;
32+
collisionCount: number;
33+
};
34+
35+
/** Human-readable formatter for the bundle result. */
36+
function formatBundleResult(data: BundleJvmResult): string {
37+
const rows: [string, string][] = [
38+
["Output", data.outputPath],
39+
["Debug ID", data.debugId],
40+
["Files bundled", String(data.fileCount)],
41+
];
42+
43+
if (data.collisionCount > 0) {
44+
rows.push(["URL collisions", String(data.collisionCount)]);
45+
}
46+
47+
return renderMarkdown(mdKvTable(rows));
48+
}
49+
50+
export const bundleJvmCommand = buildCommand({
51+
auth: false,
52+
docs: {
53+
brief: "Create a JVM source bundle for source context",
54+
fullDescription:
55+
"Create a JVM source bundle from a directory of Java, Kotlin, Scala, " +
56+
"Groovy, or Clojure source files. The bundle is a ZIP archive that " +
57+
"can be uploaded to Sentry for source context in JVM stack traces.\n\n" +
58+
"This command is local-only — it makes no network requests.",
59+
},
60+
output: {
61+
human: formatBundleResult,
62+
},
63+
parameters: {
64+
positional: {
65+
kind: "tuple",
66+
parameters: [
67+
{
68+
brief: "Directory containing JVM source files",
69+
parse: String,
70+
placeholder: "path",
71+
},
72+
],
73+
},
74+
flags: {
75+
output: {
76+
kind: "parsed",
77+
parse: String,
78+
brief: "Output directory for the bundle ZIP",
79+
},
80+
"debug-id": {
81+
kind: "parsed",
82+
parse: String,
83+
brief: "Debug ID (UUID) to stamp on the bundle",
84+
},
85+
exclude: {
86+
kind: "parsed",
87+
parse: String,
88+
brief: "Additional directory names to exclude (repeatable)",
89+
optional: true,
90+
variadic: true,
91+
},
92+
},
93+
aliases: {
94+
o: "output",
95+
d: "debug-id",
96+
e: "exclude",
97+
},
98+
},
99+
async *func(
100+
this: SentryContext,
101+
flags: {
102+
output: string;
103+
"debug-id": string;
104+
exclude?: string[];
105+
},
106+
sourcePath: string
107+
) {
108+
// 1. Validate debug ID format
109+
if (!UUID_RE.test(flags["debug-id"])) {
110+
throw new ValidationError(
111+
`Invalid debug ID format: '${flags["debug-id"]}'. ` +
112+
"Expected UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
113+
"debug-id"
114+
);
115+
}
116+
117+
// 2. Validate source path exists and is a directory
118+
const resolvedSource = resolve(sourcePath);
119+
const srcStat = await stat(resolvedSource).catch(() => {
120+
throw new ValidationError(
121+
`Source path '${sourcePath}' does not exist.`,
122+
"path"
123+
);
124+
});
125+
if (!srcStat.isDirectory()) {
126+
throw new ValidationError(
127+
`Source path '${sourcePath}' is not a directory.`,
128+
"path"
129+
);
130+
}
131+
132+
// 3. Create output directory if needed
133+
const resolvedOutput = resolve(flags.output);
134+
await mkdir(resolvedOutput, { recursive: true });
135+
136+
// 4. Build the bundle
137+
const outputFile = join(resolvedOutput, `${flags["debug-id"]}.zip`);
138+
139+
const result = await buildJvmBundle({
140+
sourcePath: resolvedSource,
141+
outputPath: outputFile,
142+
debugId: flags["debug-id"],
143+
excludePatterns: flags.exclude,
144+
});
145+
146+
if (result.fileCount === 0) {
147+
log.warn("No JVM source files found in the given directory.");
148+
}
149+
150+
// 5. Yield result
151+
yield new CommandOutput<BundleJvmResult>({
152+
outputPath: result.outputPath,
153+
debugId: flags["debug-id"],
154+
fileCount: result.fileCount,
155+
collisionCount: result.collisionCount,
156+
});
157+
158+
return {
159+
hint: `Created ${outputFile} with ${result.fileCount} source file(s)`,
160+
};
161+
},
162+
});

src/commands/debug-files/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* sentry debug-files
3+
*
4+
* Route map for debug file commands.
5+
*/
6+
7+
import { buildRouteMap } from "../../lib/route-map.js";
8+
import { bundleJvmCommand } from "./bundle-jvm.js";
9+
10+
export const debugFilesRoute = buildRouteMap({
11+
routes: {
12+
"bundle-jvm": bundleJvmCommand,
13+
},
14+
docs: {
15+
brief: "Work with debug information files",
16+
fullDescription:
17+
"Create and manage debug information files (DIFs) for source context " +
18+
"in Sentry stack traces.",
19+
},
20+
});

0 commit comments

Comments
 (0)