Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ cli/
│ │ ├── code-mappings/# upload
│ │ ├── dart-symbol-map/# upload
│ │ ├── dashboard/ # add, create, delete, edit, list, restore, revisions, view
│ │ ├── debug-files/ # bundle-jvm, bundle-sources, check, find, print-sources, upload
│ │ ├── debug-files/ # bundle-jvm, bundle-sources, check, find, print-sources, upload, wasm-upload
│ │ ├── docs/ # list, query
│ │ ├── event/ # list, send, view
│ │ ├── feedback/ # list, view
Expand Down
35 changes: 35 additions & 0 deletions apps/cli-docs/src/fragments/commands/debug-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,43 @@ sentry debug-files upload ./build --il2cpp-mapping --include-sources

# Preview what would be uploaded without uploading (no credentials needed)
sentry debug-files upload ./build --no-upload

# Split WebAssembly debug info and upload it (scans directories recursively)
sentry debug-files wasm-upload ./dist

# Preview the split without writing or uploading anything
sentry debug-files wasm-upload ./dist --dry-run

# Split only, keeping the companions local
sentry debug-files wasm-upload ./dist --no-upload

# Write companions elsewhere; modules are still stripped in place
sentry debug-files wasm-upload ./dist --out-dir ./symbols

# Fail the build if any module was compiled without DWARF
sentry debug-files wasm-upload ./dist --require-dwarf
```

## Notes on `prepare`

- For each module carrying inline DWARF it injects a `build_id` (if absent),
writes a `<stem>.<build_id>.debug.wasm` companion retaining the Code section
and DWARF, strips the `.debug_*` sections from the deployable module **in
place**, and points it at the companion via `external_debug_info`. Your build
artifact keeps its path; only the companion is new.
- The companion must keep the Code section — DWARF addresses are relative to it,
so a companion without it cannot be symbolicated.
- Modules without DWARF are still stamped with a `build_id` and reported with a
warning rather than failing the run. Sentry matches a frame to its debug file
by `build_id`, so stamping now keeps symbolication possible later.
- Name/symtab-only modules are not uploaded: the `name` section stays in the
deployable module and runtimes read function names from it directly, so a
debug file built from one adds nothing to the stack trace.
- The command is idempotent.
- `--require-dwarf` exits non-zero when any scanned module lacks DWARF, which is
the flag to use in CI. A module whose `external_debug_info` names a companion
that cannot be found fails the gate too, since its debug info is unreachable.

## Notes on `find`

- `debug-files find` locates debug files **locally** by debug identifier — it
Expand Down
1 change: 1 addition & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ Work with debug information files
- `sentry debug-files check <path>` — Inspect a debug information file
- `sentry debug-files find <id...>` — Locate debug files for given debug identifiers
- `sentry debug-files upload <path...>` — Upload debug information files to Sentry
- `sentry debug-files wasm-upload <path...>` — Split WebAssembly debug info and upload it to Sentry
- `sentry debug-files print-sources <path>` — List the source files a debug file references
- `sentry debug-files bundle-sources <path>` — Bundle a debug file's source files for source context
- `sentry debug-files bundle-jvm <path>` — Create a JVM source bundle for source context
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ Upload debug information files to Sentry
- `--wait - Wait for server-side processing and report any errors`
- `--wait-for <value> - Wait up to this many seconds for server-side processing`

### `sentry debug-files wasm-upload <path...>`

Split WebAssembly debug info and upload it to Sentry

**Flags:**
- `--dry-run - Classify modules without writing or uploading anything`
- `--no-upload - Split modules but do not upload the companions`
- `--require-dwarf - Fail if any scanned module lacks DWARF debug info`
- `--out-dir <value> - Directory for *.debug.wasm companions (modules are stripped in place)`
- `--strip-names - Also drop the name section from split modules (companion keeps it)`
- `--build-id <value> - Use this UUID as the build id instead of a random one (one .wasm file only)`
- `--include-sources - Also upload a source bundle for each companion`
- `--ignore <value>... - Skip files and folders matching this glob (repeatable)`
- `--ignore-file <value> - Skip files and folders listed in this ignore file`
- `--wait - Wait for server-side processing and report any errors`
- `--wait-for <value> - Wait up to this many seconds for server-side processing`

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

List the source files a debug file references
Expand Down Expand Up @@ -112,6 +129,21 @@ sentry debug-files upload ./build --il2cpp-mapping --include-sources

# Preview what would be uploaded without uploading (no credentials needed)
sentry debug-files upload ./build --no-upload

# Split WebAssembly debug info and upload it (scans directories recursively)
sentry debug-files wasm-upload ./dist

# Preview the split without writing or uploading anything
sentry debug-files wasm-upload ./dist --dry-run

# Split only, keeping the companions local
sentry debug-files wasm-upload ./dist --no-upload

# Write companions elsewhere; modules are still stripped in place
sentry debug-files wasm-upload ./dist --out-dir ./symbols

# Fail the build if any module was compiled without DWARF
sentry debug-files wasm-upload ./dist --require-dwarf
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
15 changes: 2 additions & 13 deletions packages/cli/src/commands/debug-files/bundle-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* 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";
Expand All @@ -28,7 +27,7 @@ import {
} from "../../lib/formatters/markdown.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { logger } from "../../lib/logger.js";
import { readDebugFile } from "./read-file.js";
import { readDebugFile, readSourceFile } from "./read-file.js";

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

Expand Down Expand Up @@ -119,17 +118,7 @@ export const bundleSourcesCommand = buildCommand({
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;
}
}
readSourceFile
);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/debug-files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { checkCommand } from "./check.js";
import { findCommand } from "./find.js";
import { printSourcesCommand } from "./print-sources.js";
import { uploadCommand } from "./upload.js";
import { wasmUploadCommand } from "./wasm-upload.js";

export const debugFilesRoute = buildRouteMap({
routes: {
check: checkCommand,
find: findCommand,
upload: uploadCommand,
"wasm-upload": wasmUploadCommand,
"print-sources": printSourcesCommand,
"bundle-sources": bundleSourcesCommand,
"bundle-jvm": bundleJvmCommand,
Expand Down
23 changes: 23 additions & 0 deletions packages/cli/src/commands/debug-files/read-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
* Shared file reading for `debug-files` commands.
*/

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

const log = logger.withTag("debug-files.read-file");

/**
* Read a debug information file from disk with descriptive error handling.
Expand All @@ -30,3 +34,22 @@ export async function readDebugFile(path: string): Promise<Buffer> {
throw new ValidationError(`Cannot read file '${path}': ${msg}`, "path");
}
}

/**
* Read a source file for source-bundle resolution.
*
* A debug file names the sources it was compiled from, but those sources are
* often absent on the machine running the upload. A missing one is expected, so
* it is skipped rather than failing the bundle.
*
* @param sourcePath - Path the debug file references.
* @returns The contents, or `null` when the file is not available locally.
*/
export function readSourceFile(sourcePath: string): Uint8Array | null {
Comment thread
d2anamaria marked this conversation as resolved.
try {
return readFileSync(sourcePath);
} catch (err) {
log.debug(`Source file not available, skipping: ${sourcePath}`, err);
return null;
}
}
52 changes: 5 additions & 47 deletions packages/cli/src/commands/debug-files/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join } from "node:path";
import type { SentryContext } from "../../context.js";
Expand All @@ -31,7 +31,6 @@ import {
getChunkUploadOptions,
} from "../../lib/api/chunk-upload.js";
import {
DEBUG_FILES_MAX_WAIT_MS,
type DebugFileUpload,
type DebugFileUploadResult,
uploadDebugFiles,
Expand All @@ -48,7 +47,7 @@ import {
prepareDifs,
scanPaths,
} from "../../lib/dif/scan.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import { ContextError } from "../../lib/errors.js";
import {
colorTag,
mdKvTable,
Expand All @@ -57,6 +56,8 @@ import {
import { CommandOutput } from "../../lib/formatters/output.js";
import { logger } from "../../lib/logger.js";
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
import { readSourceFile } from "./read-file.js";
import { resolveWaitMode, type WaitFlags } from "./wait.js";

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

Expand Down Expand Up @@ -125,7 +126,7 @@ type DebugFilesUploadResult = {
};

/** Flags accepted by the upload command. */
type UploadFlags = {
type UploadFlags = WaitFlags & {
type?: string[];
id?: string[];
"require-all"?: boolean;
Expand All @@ -137,8 +138,6 @@ type UploadFlags = {
"derived-data"?: boolean;
"no-zips"?: boolean;
"no-upload"?: boolean;
wait?: boolean;
"wait-for"?: number;
};

// ── Formatter ───────────────────────────────────────────────────────
Expand Down Expand Up @@ -177,19 +176,6 @@ function difKey(dif: DebugFileUpload): string {
return `${dif.debugId ?? ""}:${hash}`;
}

/**
* Read a source file from disk for source-bundle/IL2CPP resolution, returning
* `null` (and logging at debug level) when it is not available locally.
*/
function readSourceFile(sourcePath: string): Uint8Array | null {
try {
return readFileSync(sourcePath);
} catch (err) {
log.debug(`Source file not available, skipping: ${sourcePath}`, err);
return null;
}
}

/**
* Compute a Unity IL2CPP line mapping for a prepared file and, when non-empty,
* append it as a separate `il2cpp` DIF carrying the file's debug id.
Expand Down Expand Up @@ -318,34 +304,6 @@ function missingRequestedIds(
);
}

/**
* Resolve the wait mode and deadline from `--wait` / `--wait-for`.
*
* @throws {ValidationError} If both flags are set, or `--wait-for` is invalid.
*/
function resolveWaitMode(flags: UploadFlags): {
wait: boolean;
maxWaitMs: number;
} {
const waitFor = flags["wait-for"];
if (flags.wait && waitFor !== undefined) {
throw new ValidationError(
"--wait and --wait-for cannot be combined",
"wait"
);
}
if (waitFor !== undefined) {
if (!Number.isFinite(waitFor) || waitFor <= 0) {
throw new ValidationError(
"--wait-for must be a positive number of seconds",
"wait-for"
);
}
return { wait: true, maxWaitMs: Math.round(waitFor * 1000) };
}
return { wait: Boolean(flags.wait), maxWaitMs: DEBUG_FILES_MAX_WAIT_MS };
}

// ── Command ─────────────────────────────────────────────────────────

/**
Expand Down
49 changes: 49 additions & 0 deletions packages/cli/src/commands/debug-files/wait.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Shared `--wait` / `--wait-for` handling for `debug-files` commands.
*
* Uploading is only half the job: the server still has to assemble and index
* what it received. Commands that upload therefore offer the same pair of
* flags, and they have to agree on what the pair means.
*/

import { DEBUG_FILES_MAX_WAIT_MS } from "../../lib/api/debug-files.js";
import { ValidationError } from "../../lib/errors.js";

/** The wait flags, as a command declares them. */
export type WaitFlags = {
/** Wait for server-side processing, using the default deadline. */
wait?: boolean;
/** Wait for server-side processing, in seconds. */
"wait-for"?: number;
};

/** Whether to wait, and for how long. */
export type WaitMode = {
wait: boolean;
maxWaitMs: number;
};

/**
* Resolve the wait mode and deadline from the flags.
*
* @throws {ValidationError} If both flags are set, or `--wait-for` is invalid.
*/
export function resolveWaitMode(flags: WaitFlags): WaitMode {
const waitFor = flags["wait-for"];
if (flags.wait && waitFor !== undefined) {
throw new ValidationError(
"--wait and --wait-for cannot be combined",
"wait"
);
}
if (waitFor !== undefined) {
if (!Number.isFinite(waitFor) || waitFor <= 0) {
throw new ValidationError(
"--wait-for must be a positive number of seconds",
"wait-for"
);
}
return { wait: true, maxWaitMs: Math.round(waitFor * 1000) };
}
return { wait: Boolean(flags.wait), maxWaitMs: DEBUG_FILES_MAX_WAIT_MS };
}
Loading
Loading