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
4 changes: 4 additions & 0 deletions apps/cli-docs/src/fragments/commands/debug-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
files. Combine with `--include-sources` to also bundle the referenced C#
source files.
- Upload a JVM bundle separately via `sentry debug-files upload --type jvm`.
- Compiled shaders (`.dxil` files from an Unreal Engine cook) are uploaded as
`dxil` debug files, which let Sentry name the shader that caused a GPU crash.
If the Sentry Unreal plugin wrote a `sentry-shader-manifest.tsv` next to them,
the shader hash and group from that manifest are uploaded as well.
- Supported JVM source file extensions: `.java`, `.kt`, `.scala`, `.sc`,
`.groovy`, `.gvy`, `.gy`, `.gsh`, `.clj`, `.cljc`
- Build output directories (`build/`, `target/`, `out/`, `bin/`) are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Upload debug information files to Sentry
| `<path...>` | Files or directories to scan for debug information files |

**Flags:**
- `-t, --type <value>... - Only upload files of this type (repeatable): dsym, elf, pe, pdb, portablepdb, wasm, breakpad, sourcebundle, jvm`
- `-t, --type <value>... - Only upload files of this type (repeatable): dsym, elf, pe, pdb, portablepdb, wasm, breakpad, sourcebundle, jvm, dxil`
- `--id <value>... - Only upload the object with this debug id (repeatable)`
- `--require-all - Fail if any --id value was not found among scanned files`
- `--no-debug - Do not upload files whose only feature is debug/symbol info`
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/commands/debug-files/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ function buildDifList(
difs.push({
name: basename(file.path),
debugId: file.debugId,
...(file.codeId ? { codeId: file.codeId } : {}),
...(file.shaderGroup ? { shaderGroup: file.shaderGroup } : {}),
content: file.content,
});

Expand Down Expand Up @@ -533,13 +535,13 @@ export const uploadCommand = buildCommand({
fullDescription:
"Scan files and directories for native debug information files and " +
"upload them to Sentry using the chunk-upload protocol. Supports " +
"Mach-O/dSYM, ELF, PE/PDB, Portable PDB, WebAssembly, Breakpad, and " +
"source bundles. Directories are scanned recursively.\n\n" +
"Mach-O/dSYM, ELF, PE/PDB, Portable PDB, WebAssembly, Breakpad, source " +
"bundles, and DXIL shader binaries. Directories are scanned recursively.\n\n" +
"Org/project are auto-detected from DSN, env vars, or config defaults.\n\n" +
"Filters:\n" +
" --type Only upload files of the given type (repeatable):\n" +
" dsym, elf, pe, pdb, portablepdb, wasm, breakpad,\n" +
" sourcebundle, jvm\n" +
" sourcebundle, jvm, dxil\n" +
" --id Only upload the object with the given debug id (repeatable)\n" +
" --no-debug / --no-unwind / --no-sources Drop files whose only\n" +
" useful feature is the named one\n" +
Expand Down Expand Up @@ -621,7 +623,7 @@ export const uploadCommand = buildCommand({
parse: choiceParser(VALID_DIF_TYPES),
brief:
"Only upload files of this type (repeatable): dsym, elf, pe, pdb, " +
"portablepdb, wasm, breakpad, sourcebundle, jvm",
"portablepdb, wasm, breakpad, sourcebundle, jvm, dxil",
optional: true,
variadic: true,
},
Expand Down
135 changes: 99 additions & 36 deletions packages/cli/src/lib/api/chunk-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,25 +420,67 @@ export async function uploadMissingChunks(params: {
* Same wire format as {@link uploadChunk} but reads from a buffer
* instead of a file handle.
*/
async function uploadBufferChunk(params: {
chunk: ChunkInfo;
content: Buffer;
/** One chunk's bytes, keyed by its checksum, ready to upload. */
export type ChunkPayload = {
sha1: string;
data: Buffer;
};

/**
* Group chunks into upload requests of at most `chunksPerRequest` chunks and
* `maxRequestSize` bytes (a chunk larger than the cap still travels alone).
* Debug file sets can hold tens of thousands of small files, so one request
* per chunk would take minutes; the server accepts repeated `file` fields.
*/
export function batchChunkPayloads(
chunks: readonly ChunkPayload[],
serverOptions: Pick<
ChunkServerOptions,
"chunksPerRequest" | "maxRequestSize"
>,
): ChunkPayload[][] {
const maxChunks = Math.max(1, serverOptions.chunksPerRequest);
const maxBytes = Math.max(1, serverOptions.maxRequestSize);
const batches: ChunkPayload[][] = [];
let batch: ChunkPayload[] = [];
let bytes = 0;
for (const chunk of chunks) {
if (
batch.length > 0 &&
(batch.length >= maxChunks || bytes + chunk.data.length > maxBytes)
) {
batches.push(batch);
batch = [];
bytes = 0;
}
batch.push(chunk);
bytes += chunk.data.length;
}
if (batch.length > 0) {
batches.push(batch);
}
return batches;
}

/** POST one batch of chunks as a multipart request, one field per chunk. */
async function uploadChunkBatch(params: {
batch: readonly ChunkPayload[];
encoding: UploadEncoding | undefined;
fetch: (url: string, init: RequestInit) => Promise<Response>;
url: string;
}): Promise<void> {
const { chunk, content, encoding, fetch: authFetch, url } = params;

const buf = content.subarray(chunk.offset, chunk.offset + chunk.size);
const payload = await encodeChunk(buf, encoding);
const { batch, encoding, fetch: authFetch, url } = params;

const fieldName = encoding === "gzip" ? "file_gzip" : "file";
const form = new FormData();
form.append(
fieldName,
new Blob([payload], { type: "application/octet-stream" }),
chunk.sha1,
);
for (const chunk of batch) {
const payload = await encodeChunk(chunk.data, encoding);
form.append(
fieldName,
new Blob([payload], { type: "application/octet-stream" }),
chunk.sha1,
);
}

const init: RequestInit = { method: "POST", body: form };
if (encoding === "zstd") {
Expand All @@ -460,31 +502,17 @@ async function uploadBufferChunk(params: {
}

/**
* Upload missing chunks from an in-memory buffer, in parallel.
*
* Same as {@link uploadMissingChunks} but reads chunk data from a buffer
* instead of a file. Used by DIF uploads (e.g. ProGuard) where raw bytes
* are chunked directly without wrapping in a ZIP.
* Upload chunks in batched requests (see {@link batchChunkPayloads}), with
* `concurrency` requests in flight at once.
*/
export async function uploadMissingBufferChunks(params: {
chunks: ChunkInfo[];
missingChecksums: Set<string>;
content: Buffer;
export async function uploadChunkPayloads(params: {
chunks: readonly ChunkPayload[];
serverOptions: ChunkServerOptions;
encoding: UploadEncoding | undefined;
regionUrl: string;
}): Promise<void> {
const {
chunks,
missingChecksums,
content,
serverOptions,
encoding,
regionUrl,
} = params;
const missingChunks = chunks.filter((c) => missingChecksums.has(c.sha1));

if (missingChunks.length === 0) {
const { chunks, serverOptions, encoding, regionUrl } = params;
if (chunks.length === 0) {
return;
}

Expand All @@ -494,11 +522,10 @@ export async function uploadMissingBufferChunks(params: {
const { fetch: authFetch } = getSdkConfig(regionUrl, { idempotent: true });

await Promise.all(
missingChunks.map((chunk) =>
batchChunkPayloads(chunks, serverOptions).map((batch) =>
limit(() =>
uploadBufferChunk({
chunk,
content,
uploadChunkBatch({
batch,
encoding,
fetch: authFetch,
url: serverOptions.url,
Expand All @@ -508,6 +535,42 @@ export async function uploadMissingBufferChunks(params: {
);
}

/**
* Upload missing chunks from an in-memory buffer, in parallel.
*
* Same as {@link uploadMissingChunks} but reads chunk data from a buffer
* instead of a file. Used by DIF uploads (e.g. ProGuard) where raw bytes
* are chunked directly without wrapping in a ZIP.
*/
export async function uploadMissingBufferChunks(params: {
chunks: ChunkInfo[];
missingChecksums: Set<string>;
content: Buffer;
serverOptions: ChunkServerOptions;
encoding: UploadEncoding | undefined;
regionUrl: string;
}): Promise<void> {
const {
chunks,
missingChecksums,
content,
serverOptions,
encoding,
regionUrl,
} = params;
await uploadChunkPayloads({
chunks: chunks
.filter((c) => missingChecksums.has(c.sha1))
.map((c) => ({
sha1: c.sha1,
data: content.subarray(c.offset, c.offset + c.size),
})),
serverOptions,
encoding,
regionUrl,
});
}

/**
* Poll an assemble endpoint until the server reports completion.
*
Expand Down
Loading
Loading