Skip to content

Commit 94a8931

Browse files
romtsnclaude
andauthored
feat(code-mappings): Add batch splitting for large uploads (#3210)
_#skip-changelog_ Split large mapping files into batches of 300 (the backend limit) per request. Each batch is sent sequentially with progress reporting, and results are merged into a single summary. Also changes the output table to only show error rows — for large uploads (hundreds of mappings), printing every row would flood the terminal. Successful mappings are reflected in the summary counts instead. Stack: #3207#3208#3209 → **#3210** Backend PRs: getsentry/sentry#109783, getsentry/sentry#109785, getsentry/sentry#109786 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ccf349e commit 94a8931

File tree

6 files changed

+180
-34
lines changed

6 files changed

+180
-34
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
### New Features ✨
66

77
- Add `sentry-cli build download` command to download installable builds (IPA/APK) by build ID ([#3221](https://github.com/getsentry/sentry-cli/pull/3221)).
8+
- Add `sentry-cli code-mappings upload` command to bulk upload code mappings from a JSON file ([#3207](https://github.com/getsentry/sentry-cli/pull/3207), [#3208](https://github.com/getsentry/sentry-cli/pull/3208), [#3209](https://github.com/getsentry/sentry-cli/pull/3209), [#3210](https://github.com/getsentry/sentry-cli/pull/3210)).
9+
- Code mappings link stack trace paths (e.g. `com/example/module`) to source paths in your repository (e.g. `src/main/java/com/example/module`), enabling Sentry to display source context and link directly to your code from error stack traces.
10+
- Repository name and default branch are automatically inferred from your local git remotes, or can be specified explicitly with `--repo` and `--default-branch`.
11+
- Large mapping files are automatically split into batches for upload.
812

913
## 3.3.3
1014

src/commands/code_mappings/upload.rs

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ use anyhow::{bail, Context as _, Result};
44
use clap::{Arg, ArgMatches, Command};
55
use log::debug;
66

7-
use crate::api::{Api, BulkCodeMapping, BulkCodeMappingResult, BulkCodeMappingsRequest};
7+
use crate::api::{
8+
Api, BulkCodeMapping, BulkCodeMappingResult, BulkCodeMappingsRequest, BulkCodeMappingsResponse,
9+
};
810
use crate::config::Config;
911
use crate::utils::formatting::Table;
1012
use crate::utils::vcs;
1113

14+
/// Maximum number of mappings the backend accepts per request.
15+
const BATCH_SIZE: usize = 300;
16+
1217
pub fn make_command(command: Command) -> Command {
1318
command
1419
.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).")
@@ -72,31 +77,47 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
7277
)?;
7378

7479
let mapping_count = mappings.len();
75-
let request = BulkCodeMappingsRequest {
76-
project: &project,
77-
repository: &repo_name,
78-
default_branch: &default_branch,
79-
mappings: &mappings,
80-
};
80+
let total_batches = mapping_count.div_ceil(BATCH_SIZE);
8181

8282
println!("Uploading {mapping_count} code mapping(s)...");
8383

8484
let api = Api::current();
85-
let response = api
86-
.authenticated()?
87-
.bulk_upload_code_mappings(&org, &request)?;
85+
let authenticated = api.authenticated()?;
86+
87+
let merged: MergedResponse = mappings
88+
.chunks(BATCH_SIZE)
89+
.enumerate()
90+
.map(|(i, batch)| {
91+
if total_batches > 1 {
92+
println!("Sending batch {}/{total_batches}...", i + 1);
93+
}
94+
let request = BulkCodeMappingsRequest {
95+
project: &project,
96+
repository: &repo_name,
97+
default_branch: &default_branch,
98+
mappings: batch,
99+
};
100+
authenticated
101+
.bulk_upload_code_mappings(&org, &request)
102+
.map_err(|err| format!("Batch {}/{total_batches} failed: {err}", i + 1))
103+
})
104+
.collect();
105+
106+
// Display error details (successful mappings are summarized in counts only).
107+
print_error_table(&merged.mappings);
88108

89-
print_results_table(response.mappings);
109+
for err in &merged.batch_errors {
110+
println!("{err}");
111+
}
112+
113+
let total_errors = merged.errors + merged.batch_errors.len() as u64;
90114
println!(
91-
"Created: {}, Updated: {}, Errors: {}",
92-
response.created, response.updated, response.errors
115+
"Created: {}, Updated: {}, Errors: {total_errors}",
116+
merged.created, merged.updated
93117
);
94118

95-
if response.errors > 0 {
96-
bail!(
97-
"{} mapping(s) failed to upload. See errors above.",
98-
response.errors
99-
);
119+
if total_errors > 0 {
120+
bail!("{total_errors} error(s) during upload. See details above.");
100121
}
101122

102123
Ok(())
@@ -196,30 +217,61 @@ fn infer_default_branch(git_repo: Option<&git2::Repository>, remote_name: Option
196217
})
197218
}
198219

199-
fn print_results_table(mappings: Vec<BulkCodeMappingResult>) {
220+
fn print_error_table(mappings: &[BulkCodeMappingResult]) {
221+
if !mappings.iter().any(|r| r.status == "error") {
222+
return;
223+
}
224+
200225
let mut table = Table::new();
201226
table
202227
.title_row()
203228
.add("Stack Root")
204229
.add("Source Root")
205-
.add("Status");
230+
.add("Detail");
206231

207-
for result in mappings {
208-
let status = match result.detail {
209-
Some(detail) if result.status == "error" => format!("error: {detail}"),
210-
_ => result.status,
211-
};
232+
for result in mappings.iter().filter(|r| r.status == "error") {
233+
let detail = result.detail.as_deref().unwrap_or("unknown error");
212234
table
213235
.add_row()
214236
.add(&result.stack_root)
215237
.add(&result.source_root)
216-
.add(&status);
238+
.add(detail);
217239
}
218240

219241
table.print();
220242
println!();
221243
}
222244

245+
#[derive(Default)]
246+
struct MergedResponse {
247+
created: u64,
248+
updated: u64,
249+
errors: u64,
250+
mappings: Vec<BulkCodeMappingResult>,
251+
batch_errors: Vec<String>,
252+
}
253+
254+
impl FromIterator<Result<BulkCodeMappingsResponse, String>> for MergedResponse {
255+
fn from_iter<I>(iter: I) -> Self
256+
where
257+
I: IntoIterator<Item = Result<BulkCodeMappingsResponse, String>>,
258+
{
259+
let mut merged = Self::default();
260+
for result in iter {
261+
match result {
262+
Ok(response) => {
263+
merged.created += response.created;
264+
merged.updated += response.updated;
265+
merged.errors += response.errors;
266+
merged.mappings.extend(response.mappings);
267+
}
268+
Err(err) => merged.batch_errors.push(err),
269+
}
270+
}
271+
merged
272+
}
273+
}
274+
223275
#[cfg(test)]
224276
mod tests {
225277
use super::*;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
```
2+
$ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main
3+
? failed
4+
Uploading 2 code mapping(s)...
5+
+------------------+---------------------------------------------+-------------------+
6+
| Stack Root | Source Root | Detail |
7+
+------------------+---------------------------------------------+-------------------+
8+
| com/example/maps | modules/maps/src/main/java/com/example/maps | duplicate mapping |
9+
+------------------+---------------------------------------------+-------------------+
10+
11+
Created: 1, Updated: 0, Errors: 1
12+
error: 1 error(s) during upload. See details above.
13+
14+
Add --log-level=[info|debug] or export SENTRY_LOG_LEVEL=[info|debug] to see more output.
15+
Please attach the full debug log to all bug reports.
16+
17+
```

tests/integration/_cases/code_mappings/code-mappings-upload.trycmd

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,6 @@
22
$ sentry-cli code-mappings upload tests/integration/_fixtures/code_mappings/mappings.json --org wat-org --project wat-project --repo owner/repo --default-branch main
33
? success
44
Uploading 2 code mapping(s)...
5-
+------------------+---------------------------------------------+---------+
6-
| Stack Root | Source Root | Status |
7-
+------------------+---------------------------------------------+---------+
8-
| com/example/core | modules/core/src/main/java/com/example/core | created |
9-
| com/example/maps | modules/maps/src/main/java/com/example/maps | created |
10-
+------------------+---------------------------------------------+---------+
11-
125
Created: 2, Updated: 0, Errors: 0
136

147
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"created": 1,
3+
"updated": 0,
4+
"errors": 1,
5+
"mappings": [
6+
{"stackRoot": "com/example/core", "sourceRoot": "modules/core/src/main/java/com/example/core", "status": "created"},
7+
{"stackRoot": "com/example/maps", "sourceRoot": "modules/maps/src/main/java/com/example/maps", "status": "error", "detail": "duplicate mapping"}
8+
]
9+
}

tests/integration/code_mappings/upload.rs

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::integration::{MockEndpointBuilder, TestManager};
1+
use std::sync::atomic::{AtomicU8, Ordering};
2+
3+
use crate::integration::{AssertCommand, MockEndpointBuilder, TestManager};
24

35
#[test]
46
fn command_code_mappings_upload() {
@@ -10,3 +12,72 @@ fn command_code_mappings_upload() {
1012
.register_trycmd_test("code_mappings/code-mappings-upload.trycmd")
1113
.with_default_token();
1214
}
15+
16+
#[test]
17+
fn command_code_mappings_upload_partial_error() {
18+
TestManager::new()
19+
.mock_endpoint(
20+
MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/code-mappings/bulk/")
21+
.with_response_file("code_mappings/post-bulk-partial-error.json"),
22+
)
23+
.register_trycmd_test("code_mappings/code-mappings-upload-partial-error.trycmd")
24+
.with_default_token();
25+
}
26+
27+
#[test]
28+
fn command_code_mappings_upload_batches() {
29+
// Generate a fixture with 301 mappings to force 2 batches (300 + 1).
30+
let mut mappings = Vec::with_capacity(301);
31+
for i in 0..301 {
32+
mappings.push(serde_json::json!({
33+
"stackRoot": format!("com/example/m{i}"),
34+
"sourceRoot": format!("modules/m{i}/src/main/java/com/example/m{i}"),
35+
}));
36+
}
37+
let fixture = tempfile::NamedTempFile::new().expect("failed to create temp file");
38+
serde_json::to_writer(&fixture, &mappings).expect("failed to write fixture");
39+
40+
let call_count = AtomicU8::new(0);
41+
42+
TestManager::new()
43+
.mock_endpoint(
44+
MockEndpointBuilder::new("POST", "/api/0/organizations/wat-org/code-mappings/bulk/")
45+
.expect(2)
46+
.with_response_fn(move |_request| {
47+
let n = call_count.fetch_add(1, Ordering::Relaxed);
48+
// Return appropriate counts per batch
49+
let (created, mapping_count) = if n == 0 { (300, 300) } else { (1, 1) };
50+
let mut batch_mappings = Vec::new();
51+
for i in 0..mapping_count {
52+
let idx = n as usize * 300 + i;
53+
batch_mappings.push(serde_json::json!({
54+
"stackRoot": format!("com/example/m{idx}"),
55+
"sourceRoot": format!("modules/m{idx}/src/main/java/com/example/m{idx}"),
56+
"status": "created",
57+
}));
58+
}
59+
serde_json::to_vec(&serde_json::json!({
60+
"created": created,
61+
"updated": 0,
62+
"errors": 0,
63+
"mappings": batch_mappings,
64+
}))
65+
.expect("failed to serialize response")
66+
}),
67+
)
68+
.assert_cmd([
69+
"code-mappings",
70+
"upload",
71+
fixture.path().to_str().expect("valid utf-8 path"),
72+
"--org",
73+
"wat-org",
74+
"--project",
75+
"wat-project",
76+
"--repo",
77+
"owner/repo",
78+
"--default-branch",
79+
"main",
80+
])
81+
.with_default_token()
82+
.run_and_assert(AssertCommand::Success);
83+
}

0 commit comments

Comments
 (0)