From 59bff1f1236af67cc44d842e0943c22ef1c78a9e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 12 Mar 2026 09:44:23 +0100 Subject: [PATCH 1/5] feat(cli): Add batch splitting for code-mappings upload Split large mapping files into batches of 300 (the backend limit) per request. Each batch is sent sequentially with progress reporting. Results are merged into a single summary table. Batch-level HTTP failures are captured without aborting remaining batches, and the final exit code reflects any errors. Co-Authored-By: Claude Opus 4.6 --- src/api/data_types/code_mappings.rs | 2 +- src/commands/code_mappings/upload.rs | 79 ++++++++++++++++++----- tests/integration/code_mappings/upload.rs | 62 +++++++++++++++++- 3 files changed, 124 insertions(+), 19 deletions(-) diff --git a/src/api/data_types/code_mappings.rs b/src/api/data_types/code_mappings.rs index 21a8aedd55..e53b04161a 100644 --- a/src/api/data_types/code_mappings.rs +++ b/src/api/data_types/code_mappings.rs @@ -11,7 +11,7 @@ pub struct BulkCodeMappingsRequest { pub mappings: Vec, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct BulkCodeMapping { pub stack_root: String, diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 78d05f8c0e..58e97ad9a1 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -4,11 +4,16 @@ use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; use log::debug; -use crate::api::{Api, BulkCodeMapping, BulkCodeMappingResult, BulkCodeMappingsRequest}; +use crate::api::{ + Api, BulkCodeMapping, BulkCodeMappingResult, BulkCodeMappingsRequest, BulkCodeMappingsResponse, +}; use crate::config::Config; use crate::utils::formatting::Table; use crate::utils::vcs; +/// Maximum number of mappings the backend accepts per request. +const BATCH_SIZE: usize = 300; + pub fn make_command(command: Command) -> Command { command .about("Upload code mappings for a project from a JSON file. Each mapping pairs a stack trace root (e.g. com/example/module) with the corresponding source path in your repository (e.g. modules/module/src/main/java/com/example/module).") @@ -144,31 +149,53 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { }; let mapping_count = mappings.len(); - let request = BulkCodeMappingsRequest { - project, - repository: repo_name, - default_branch, - mappings, - }; + let batches: Vec<&[BulkCodeMapping]> = mappings.chunks(BATCH_SIZE).collect(); + let total_batches = batches.len(); println!("Uploading {mapping_count} code mapping(s)..."); let api = Api::current(); - let response = api - .authenticated()? - .bulk_upload_code_mappings(&org, &request)?; + let authenticated = api.authenticated()?; + + let mut merged = MergedResponse::default(); + + for (i, batch) in batches.iter().enumerate() { + if total_batches > 1 { + println!("Sending batch {}/{total_batches}...", i + 1); + } + let request = BulkCodeMappingsRequest { + project: project.clone(), + repository: repo_name.clone(), + default_branch: default_branch.clone(), + mappings: batch.to_vec(), + }; + match authenticated.bulk_upload_code_mappings(&org, &request) { + Ok(response) => merged.add(response), + Err(err) => { + merged + .batch_errors + .push(format!("Batch {}/{total_batches} failed: {err}", i + 1)); + } + } + } + + // Display results + if !merged.mappings.is_empty() { + print_results_table(merged.mappings); + } + + for err in &merged.batch_errors { + println!("{err}"); + } - print_results_table(response.mappings); println!( "Created: {}, Updated: {}, Errors: {}", - response.created, response.updated, response.errors + merged.created, merged.updated, merged.errors ); - if response.errors > 0 { - bail!( - "{} mapping(s) failed to upload. See errors above.", - response.errors - ); + if merged.errors > 0 || !merged.batch_errors.is_empty() { + let total_errors = merged.errors + merged.batch_errors.len() as u64; + bail!("{total_errors} error(s) during upload. See details above."); } Ok(()) @@ -197,3 +224,21 @@ fn print_results_table(mappings: Vec) { table.print(); println!(); } + +#[derive(Default)] +struct MergedResponse { + created: u64, + updated: u64, + errors: u64, + mappings: Vec, + batch_errors: Vec, +} + +impl MergedResponse { + fn add(&mut self, response: BulkCodeMappingsResponse) { + self.created += response.created; + self.updated += response.updated; + self.errors += response.errors; + self.mappings.extend(response.mappings); + } +} diff --git a/tests/integration/code_mappings/upload.rs b/tests/integration/code_mappings/upload.rs index 861ed199a9..3f7a198eca 100644 --- a/tests/integration/code_mappings/upload.rs +++ b/tests/integration/code_mappings/upload.rs @@ -1,4 +1,6 @@ -use crate::integration::{MockEndpointBuilder, TestManager}; +use std::sync::atomic::{AtomicU16, Ordering}; + +use crate::integration::{AssertCommand, MockEndpointBuilder, TestManager}; #[test] fn command_code_mappings_upload() { @@ -10,3 +12,61 @@ fn command_code_mappings_upload() { .register_trycmd_test("code_mappings/code-mappings-upload.trycmd") .with_default_token(); } + +#[test] +fn command_code_mappings_upload_batches() { + // Generate a fixture with 301 mappings to force 2 batches (300 + 1). + let mut mappings = Vec::with_capacity(301); + for i in 0..301 { + mappings.push(serde_json::json!({ + "stackRoot": format!("com/example/m{i}"), + "sourceRoot": format!("modules/m{i}/src/main/java/com/example/m{i}"), + })); + } + let fixture = tempfile::NamedTempFile::new().expect("failed to create temp file"); + serde_json::to_writer(&fixture, &mappings).expect("failed to write fixture"); + + let call_count = AtomicU16::new(0); + + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/code-mappings/bulk/") + .expect(2) + .with_response_fn(move |_request| { + let n = call_count.fetch_add(1, Ordering::Relaxed); + // Return appropriate counts per batch + let (created, mapping_count) = if n == 0 { (300, 300) } else { (1, 1) }; + let mut batch_mappings = Vec::new(); + for i in 0..mapping_count { + let idx = n as usize * 300 + i; + batch_mappings.push(serde_json::json!({ + "stackRoot": format!("com/example/m{idx}"), + "sourceRoot": format!("modules/m{idx}/src/main/java/com/example/m{idx}"), + "status": "created", + })); + } + serde_json::to_vec(&serde_json::json!({ + "created": created, + "updated": 0, + "errors": 0, + "mappings": batch_mappings, + })) + .expect("failed to serialize response") + }), + ) + .assert_cmd([ + "code-mappings", + "upload", + fixture.path().to_str().expect("valid utf-8 path"), + "--org", + "wat-org", + "--project", + "wat-project", + "--repo", + "owner/repo", + "--default-branch", + "main", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Success); +} From ae5351aab40278712beb066f0bb93b11e9de4e9b Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Thu, 12 Mar 2026 09:58:31 +0100 Subject: [PATCH 2/5] ref(cli): Only show error rows in code-mappings upload table For large uploads (hundreds of mappings), printing every row floods the terminal. Show only failed mappings in the table since those are actionable; successful ones are already reflected in the summary counts. Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 25 ++++++++++--------- .../code-mappings-upload-partial-error.trycmd | 17 +++++++++++++ .../code_mappings/code-mappings-upload.trycmd | 7 ------ .../post-bulk-partial-error.json | 9 +++++++ tests/integration/code_mappings/upload.rs | 11 ++++++++ 5 files changed, 50 insertions(+), 19 deletions(-) create mode 100644 tests/integration/_cases/code_mappings/code-mappings-upload-partial-error.trycmd create mode 100644 tests/integration/_responses/code_mappings/post-bulk-partial-error.json diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 58e97ad9a1..d795622fd8 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -179,10 +179,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } } - // Display results - if !merged.mappings.is_empty() { - print_results_table(merged.mappings); - } + // Display error details (successful mappings are summarized in counts only). + print_error_table(&merged.mappings); for err in &merged.batch_errors { println!("{err}"); @@ -201,24 +199,27 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { Ok(()) } -fn print_results_table(mappings: Vec) { +fn print_error_table(mappings: &[BulkCodeMappingResult]) { + let error_mappings: Vec<_> = mappings.iter().filter(|r| r.status == "error").collect(); + + if error_mappings.is_empty() { + return; + } + let mut table = Table::new(); table .title_row() .add("Stack Root") .add("Source Root") - .add("Status"); + .add("Detail"); - for result in mappings { - let status = match result.detail { - Some(detail) if result.status == "error" => format!("error: {detail}"), - _ => result.status, - }; + for result in &error_mappings { + let detail = result.detail.as_deref().unwrap_or("unknown error"); table .add_row() .add(&result.stack_root) .add(&result.source_root) - .add(&status); + .add(detail); } table.print(); diff --git a/tests/integration/_cases/code_mappings/code-mappings-upload-partial-error.trycmd b/tests/integration/_cases/code_mappings/code-mappings-upload-partial-error.trycmd new file mode 100644 index 0000000000..252d7a1866 --- /dev/null +++ b/tests/integration/_cases/code_mappings/code-mappings-upload-partial-error.trycmd @@ -0,0 +1,17 @@ +``` +$ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main +? failed +Uploading 2 code mapping(s)... ++------------------+---------------------------------------------+-------------------+ +| Stack Root | Source Root | Detail | ++------------------+---------------------------------------------+-------------------+ +| com/example/maps | modules/maps/src/main/java/com/example/maps | duplicate mapping | ++------------------+---------------------------------------------+-------------------+ + +Created: 1, Updated: 0, Errors: 1 +error: 1 error(s) during upload. See details above. + +Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. +Please attach the full debug log to all bug reports. + +``` diff --git a/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd b/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd index 72c35d9d19..779db9b650 100644 --- a/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd +++ b/tests/integration/_cases/code_mappings/code-mappings-upload.trycmd @@ -2,13 +2,6 @@ $ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main ? success Uploading 2 code mapping(s)... -+------------------+---------------------------------------------+---------+ -| Stack Root | Source Root | Status | -+------------------+---------------------------------------------+---------+ -| com/example/core | modules/core/src/main/java/com/example/core | created | -| com/example/maps | modules/maps/src/main/java/com/example/maps | created | -+------------------+---------------------------------------------+---------+ - Created: 2, Updated: 0, Errors: 0 ``` diff --git a/tests/integration/_responses/code_mappings/post-bulk-partial-error.json b/tests/integration/_responses/code_mappings/post-bulk-partial-error.json new file mode 100644 index 0000000000..f44f466634 --- /dev/null +++ b/tests/integration/_responses/code_mappings/post-bulk-partial-error.json @@ -0,0 +1,9 @@ +{ + "created": 1, + "updated": 0, + "errors": 1, + "mappings": [ + {"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core", "status": "created"}, + {"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps", "status": "error", "detail": "duplicate mapping"} + ] +} diff --git a/tests/integration/code_mappings/upload.rs b/tests/integration/code_mappings/upload.rs index 3f7a198eca..3580c45837 100644 --- a/tests/integration/code_mappings/upload.rs +++ b/tests/integration/code_mappings/upload.rs @@ -13,6 +13,17 @@ fn command_code_mappings_upload() { .with_default_token(); } +#[test] +fn command_code_mappings_upload_partial_error() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/code-mappings/bulk/") + .with_response_file("code_mappings/post-bulk-partial-error.json"), + ) + .register_trycmd_test("code_mappings/code-mappings-upload-partial-error.trycmd") + .with_default_token(); +} + #[test] fn command_code_mappings_upload_batches() { // Generate a fixture with 301 mappings to force 2 batches (300 + 1). From 4b94685bc40b21162e5f3ed897231d34548551d3 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 18 Mar 2026 17:44:42 +0100 Subject: [PATCH 3/5] test(code-mappings): Add test for HTTP 207 partial success response Verify the CLI correctly handles HTTP 207 Multi-Status responses from the bulk code-mappings endpoint. The CLI treats 207 as a success at the HTTP level and relies on the JSON body to surface per-mapping errors. Co-Authored-By: Claude Opus 4.6 --- ...ode-mappings-upload-207-partial-error.trycmd | 17 +++++++++++++++++ .../post-bulk-207-partial-error.json | 9 +++++++++ tests/integration/code_mappings/upload.rs | 12 ++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 tests/integration/_cases/code_mappings/code-mappings-upload-207-partial-error.trycmd create mode 100644 tests/integration/_responses/code_mappings/post-bulk-207-partial-error.json diff --git a/tests/integration/_cases/code_mappings/code-mappings-upload-207-partial-error.trycmd b/tests/integration/_cases/code_mappings/code-mappings-upload-207-partial-error.trycmd new file mode 100644 index 0000000000..252d7a1866 --- /dev/null +++ b/tests/integration/_cases/code_mappings/code-mappings-upload-207-partial-error.trycmd @@ -0,0 +1,17 @@ +``` +$ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main +? failed +Uploading 2 code mapping(s)... ++------------------+---------------------------------------------+-------------------+ +| Stack Root | Source Root | Detail | ++------------------+---------------------------------------------+-------------------+ +| com/example/maps | modules/maps/src/main/java/com/example/maps | duplicate mapping | ++------------------+---------------------------------------------+-------------------+ + +Created: 1, Updated: 0, Errors: 1 +error: 1 error(s) during upload. See details above. + +Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output. +Please attach the full debug log to all bug reports. + +``` diff --git a/tests/integration/_responses/code_mappings/post-bulk-207-partial-error.json b/tests/integration/_responses/code_mappings/post-bulk-207-partial-error.json new file mode 100644 index 0000000000..f44f466634 --- /dev/null +++ b/tests/integration/_responses/code_mappings/post-bulk-207-partial-error.json @@ -0,0 +1,9 @@ +{ + "created": 1, + "updated": 0, + "errors": 1, + "mappings": [ + {"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core", "status": "created"}, + {"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps", "status": "error", "detail": "duplicate mapping"} + ] +} diff --git a/tests/integration/code_mappings/upload.rs b/tests/integration/code_mappings/upload.rs index 3580c45837..13d3c79320 100644 --- a/tests/integration/code_mappings/upload.rs +++ b/tests/integration/code_mappings/upload.rs @@ -24,6 +24,18 @@ fn command_code_mappings_upload_partial_error() { .with_default_token(); } +#[test] +fn command_code_mappings_upload_207_partial_error() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/code-mappings/bulk/") + .with_status(207) + .with_response_file("code_mappings/post-bulk-207-partial-error.json"), + ) + .register_trycmd_test("code_mappings/code-mappings-upload-207-partial-error.trycmd") + .with_default_token(); +} + #[test] fn command_code_mappings_upload_batches() { // Generate a fixture with 301 mappings to force 2 batches (300 + 1). From a6bfd85dc0d2d61d890b9cacd4add34f021b7b9f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 18 Mar 2026 23:06:22 +0100 Subject: [PATCH 4/5] fix(code-mappings): Include batch errors in summary error count The Errors count in the summary line now includes both per-mapping errors and batch-level transport failures, matching the bail message. Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index d795622fd8..5540bd1598 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -186,13 +186,13 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { println!("{err}"); } + let total_errors = merged.errors + merged.batch_errors.len() as u64; println!( "Created: {}, Updated: {}, Errors: {}", - merged.created, merged.updated, merged.errors + merged.created, merged.updated, total_errors ); - if merged.errors > 0 || !merged.batch_errors.is_empty() { - let total_errors = merged.errors + merged.batch_errors.len() as u64; + if total_errors > 0 { bail!("{total_errors} error(s) during upload. See details above."); } From 1b835b3e58dd3f4786ab5d04e2e264ef16c9a2d8 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Wed, 18 Mar 2026 23:10:02 +0100 Subject: [PATCH 5/5] style(code-mappings): Inline format arg to fix clippy warning Co-Authored-By: Claude Opus 4.6 --- src/commands/code_mappings/upload.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/code_mappings/upload.rs b/src/commands/code_mappings/upload.rs index 5540bd1598..22f1dd2738 100644 --- a/src/commands/code_mappings/upload.rs +++ b/src/commands/code_mappings/upload.rs @@ -188,8 +188,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { let total_errors = merged.errors + merged.batch_errors.len() as u64; println!( - "Created: {}, Updated: {}, Errors: {}", - merged.created, merged.updated, total_errors + "Created: {}, Updated: {}, Errors: {total_errors}", + merged.created, merged.updated ); if total_errors > 0 {