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
87 changes: 78 additions & 9 deletions .lore.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ cli/
│ │ ├── code-mappings/# upload
│ │ ├── dart-symbol-map/# upload
│ │ ├── dashboard/ # add, create, delete, edit, list, restore, revisions, view
│ │ ├── debug-files/ # bundle-jvm, check
│ │ ├── debug-files/ # bundle-jvm, bundle-sources, check
│ │ ├── event/ # list, send, view
│ │ ├── issue/ # archive, events, explain, list, merge, plan, resolve, unresolve, view
│ │ ├── local/ # run, serve
Expand Down
16 changes: 13 additions & 3 deletions docs/src/fragments/commands/debug-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ sentry debug-files check ./libexample.so
sentry debug-files check MyApp.dSYM/Contents/Resources/DWARF/MyApp
sentry debug-files check ./app.pdb --json

# Bundle a debug file's referenced source files (run on the build machine)
sentry debug-files bundle-sources ./libexample.so
sentry debug-files bundle-sources ./app.pdb --output ./app.src.zip

# Bundle JVM sources with a debug ID
sentry debug-files bundle-jvm --output ./out --debug-id <uuid> ./src

Expand All @@ -21,11 +25,17 @@ sentry debug-files bundle-jvm --output ./out --debug-id <uuid> --json ./src

## Important Notes

- `check` and `bundle-jvm` are **local-only** — they make no network requests.
Both parse object files in-process (Mach-O/dSYM, ELF, PE/PDB, Portable PDB,
WebAssembly, Breakpad, source bundles) via a bundled `symbolic` WASM module.
- `check`, `bundle-sources`, and `bundle-jvm` are **local-only** — they make no
network requests. They parse object files in-process (Mach-O/dSYM, ELF,
PE/PDB, Portable PDB, WebAssembly, Breakpad, source bundles) via a bundled
`symbolic` WASM module.
- `check` exits non-zero if the file is not usable for symbolication (no debug
id or no useful features).
- `bundle-sources` reads source files from the paths recorded in the debug info,
so it is normally run on the build machine right after compiling. Referenced
files that are not present locally are skipped; it exits non-zero (writing
nothing) when none are found. The bundle defaults to `<path>.src.zip` and is
uploaded via `sentry debug-files upload`.
- Upload a JVM bundle separately via `sentry debug-files upload --type jvm`.
- Supported JVM source file extensions: `.java`, `.kt`, `.scala`, `.sc`,
`.groovy`, `.gvy`, `.gy`, `.gsh`, `.clj`, `.cljc`
Expand Down
1 change: 1 addition & 0 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ Work with debug information files

- `sentry debug-files check <path>` — Inspect a debug information file
- `sentry debug-files bundle-jvm <path>` — Create a JVM source bundle for source context
- `sentry debug-files bundle-sources <path>` — Bundle a debug file's source files for source context

→ Full flags and examples: `references/debug-files.md`

Expand Down
11 changes: 11 additions & 0 deletions plugins/sentry-cli/skills/sentry-cli/references/debug-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ Create a JVM source bundle for source context
- `-d, --debug-id <value> - Debug ID (UUID) to stamp on the bundle`
- `-e, --exclude <value>... - Additional directory names to exclude (repeatable)`

### `sentry debug-files bundle-sources <path>`

Bundle a debug file's source files for source context

**Flags:**
- `-o, --output <value> - Output path for the source bundle ZIP (default: <path>.src.zip)`

**Examples:**

```bash
Expand All @@ -32,6 +39,10 @@ sentry debug-files check ./libexample.so
sentry debug-files check MyApp.dSYM/Contents/Resources/DWARF/MyApp
sentry debug-files check ./app.pdb --json

# Bundle a debug file's referenced source files (run on the build machine)
sentry debug-files bundle-sources ./libexample.so
sentry debug-files bundle-sources ./app.pdb --output ./app.src.zip

# Bundle JVM sources with a debug ID
sentry debug-files bundle-jvm --output ./out --debug-id <uuid> ./src

Expand Down
178 changes: 178 additions & 0 deletions src/commands/debug-files/bundle-sources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* sentry debug-files bundle-sources <path>
*
* Build a source bundle from the source files referenced by a debug
* information file (Mach-O/dSYM, ELF, PE/PDB, Portable PDB, WASM, Breakpad).
* The bundle is a ZIP archive carrying the object's debug id, which can be
* uploaded to Sentry for source context in stack traces.
*
* Source files are read from the paths recorded in the debug info, so this is
* typically run on the build machine right after compiling. Referenced files
* that are not present locally are skipped.
*
* Local-only — no API calls. Parsing and bundling happen in-process via the
* bundled `symbolic` WASM module (see `src/lib/dif/`).
*/

import { readFileSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { basename, dirname, resolve } from "node:path";
import type { SentryContext } from "../../context.js";
import { buildCommand } from "../../lib/command.js";
import { createSourceBundle } from "../../lib/dif/index.js";
import { ValidationError } from "../../lib/errors.js";
import {
colorTag,
mdKvTable,
renderMarkdown,
} from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { logger } from "../../lib/logger.js";
import { readDebugFile } from "./read-file.js";

const log = logger.withTag("debug-files.bundle-sources");

const USAGE_HINT = "sentry debug-files bundle-sources <path>";

/** Structured result for the bundle-sources command. */
type BundleSourcesResult = {
/** Path to the inspected debug information file. */
path: string;
/** Path the bundle was written to, or `null` if no bundle was produced. */
outputPath: string | null;
/** Debug id of the bundled object, or `null` if the file has no objects. */
debugId: string | null;
/** Number of source files included in the bundle. */
fileCount: number;
};

/** Human-readable formatter for the bundle result. */
function formatBundleResult(data: BundleSourcesResult): string {
if (data.outputPath === null) {
return renderMarkdown(
colorTag(
"warning",
"No source files referenced by this debug file were found on disk; nothing was bundled."
)
);
}
const rows: [string, string][] = [
["Output", data.outputPath],
["Debug ID", data.debugId ?? colorTag("muted", "none")],
["Files bundled", String(data.fileCount)],
];
return renderMarkdown(mdKvTable(rows));
}

export const bundleSourcesCommand = buildCommand({
// Local-only: parses + bundles in-process, no API calls.
auth: false,
docs: {
brief: "Bundle a debug file's source files for source context",
fullDescription:
"Build a source bundle from the source files referenced by a debug " +
"information file. The bundle is a ZIP archive stamped with the " +
"object's debug id that can be uploaded to Sentry (debug-files upload) " +
"for source context in stack traces. Supports Mach-O/dSYM, ELF, " +
"PE/PDB, Portable PDB, WebAssembly, and Breakpad.\n\n" +
"Source files are read from the paths recorded in the debug info, so " +
"this is normally run on the build machine right after compiling. " +
"Referenced files that are not present locally are skipped. The format " +
"is auto-detected, and this command makes no network requests.\n\n" +
"Usage:\n" +
" sentry debug-files bundle-sources ./libexample.so\n" +
" sentry debug-files bundle-sources ./app.pdb -o ./app.src.zip\n\n" +
"Exits non-zero if no referenced source files are found on disk.",
},
output: {
human: formatBundleResult,
},
parameters: {
positional: {
kind: "tuple",
parameters: [
{
brief: "Path to the debug information file",
parse: String,
placeholder: "path",
},
],
},
flags: {
output: {
kind: "parsed",
parse: String,
brief:
"Output path for the source bundle ZIP (default: <path>.src.zip)",
optional: true,
},
},
aliases: {
o: "output",
},
},
async *func(this: SentryContext, flags: { output?: string }, path: string) {
const content = await readDebugFile(path);

let result: ReturnType<typeof createSourceBundle>;
try {
result = createSourceBundle(
new Uint8Array(content),
basename(path),
(sourcePath) => {
try {
return readFileSync(sourcePath);
} catch (err) {
log.debug(
`Source file not available, skipping: ${sourcePath}`,
err
);
return null;
}
}
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new ValidationError(
`'${path}' is not a recognized debug information file: ${msg}`,
"path"
);
}

if (result.bundle === null || result.fileCount === 0) {
// this.process === the global process in production (see buildContext);
// using it here keeps the exit-code observable in tests.
this.process.exitCode = 1;
yield new CommandOutput<BundleSourcesResult>({
path,
outputPath: null,
debugId: result.debugId,
fileCount: 0,
});
return {
hint: `No source files found on disk for '${path}'. This is normally run on the build machine. Try: ${USAGE_HINT}`,
};
}

if (result.objectCount > 1) {
log.warn(
`'${path}' contains ${result.objectCount} objects; bundled sources for ${result.debugId} only. Other slices are not included.`
);
}

const outputPath = resolve(flags.output ?? `${path}.src.zip`);
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, result.bundle);

yield new CommandOutput<BundleSourcesResult>({
path,
outputPath,
debugId: result.debugId,
fileCount: result.fileCount,
});

return {
hint: `Created ${outputPath} with ${result.fileCount} source file(s). Upload with: sentry debug-files upload`,
};
},
});
26 changes: 1 addition & 25 deletions src/commands/debug-files/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
* no useful features), mirroring the legacy `sentry-cli difutil check`.
*/

import { readFile } from "node:fs/promises";
import type { SentryContext } from "../../context.js";
import { buildCommand } from "../../lib/command.js";
import { type DifArchiveInfo, parseDebugFile } from "../../lib/dif/index.js";
Expand All @@ -23,6 +22,7 @@ import {
renderMarkdown,
} from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { readDebugFile } from "./read-file.js";

const USAGE_HINT = "sentry debug-files check <path>";

Expand Down Expand Up @@ -80,30 +80,6 @@ function formatCheckResult(data: DebugFilesCheckResult): string {
return out;
}

/**
* Read a file from disk with descriptive error handling.
*
* @throws {ValidationError} On ENOENT, EISDIR, or other read failures.
*/
async function readDebugFile(path: string): Promise<Buffer> {
try {
return await readFile(path);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new ValidationError(`File '${path}' does not exist.`, "path");
}
if (code === "EISDIR") {
throw new ValidationError(
`Path '${path}' is a directory, not a debug information file.`,
"path"
);
}
const msg = err instanceof Error ? err.message : String(err);
throw new ValidationError(`Cannot read file '${path}': ${msg}`, "path");
}
}

/**
* Nil debug id (hyphenated UUID form). A debug id starting with this means the
* object carries no real identifier. PE/PDB ids may append an `-<age>` suffix,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/debug-files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

import { buildRouteMap } from "../../lib/route-map.js";
import { bundleJvmCommand } from "./bundle-jvm.js";
import { bundleSourcesCommand } from "./bundle-sources.js";
import { checkCommand } from "./check.js";

export const debugFilesRoute = buildRouteMap({
routes: {
check: checkCommand,
"bundle-jvm": bundleJvmCommand,
"bundle-sources": bundleSourcesCommand,
},
docs: {
brief: "Work with debug information files",
Expand Down
32 changes: 32 additions & 0 deletions src/commands/debug-files/read-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Shared file reading for `debug-files` commands.
*/

import { readFile } from "node:fs/promises";
import { ValidationError } from "../../lib/errors.js";

/**
* Read a debug information file from disk with descriptive error handling.
*
* @param path - Path to the file.
* @returns The file contents.
* @throws {ValidationError} On ENOENT, EISDIR, or other read failures.
*/
export async function readDebugFile(path: string): Promise<Buffer> {
try {
return await readFile(path);
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new ValidationError(`File '${path}' does not exist.`, "path");
}
if (code === "EISDIR") {
throw new ValidationError(
`Path '${path}' is a directory, not a debug information file.`,
"path"
);
}
const msg = err instanceof Error ? err.message : String(err);
throw new ValidationError(`Cannot read file '${path}': ${msg}`, "path");
}
}
Loading
Loading