Skip to content
Draft
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 src/api/data_types/code_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct BulkCodeMappingsRequest {
pub mappings: Vec<BulkCodeMapping>,
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct BulkCodeMapping {
pub stack_root: String,
Expand Down
120 changes: 82 additions & 38 deletions src/commands/code_mappings/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ use anyhow::{bail, Context as _, Result};
use clap::{Arg, ArgMatches, Command};
use log::debug;

use crate::api::{Api, BulkCodeMapping, 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.")
Expand Down Expand Up @@ -118,56 +123,95 @@ 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)?;

// Display results
let mut table = Table::new();
table
.title_row()
.add("Stack Root")
.add("Source Root")
.add("Status");

for result in &response.mappings {
let status = match result.status.as_str() {
"error" => match &result.detail {
Some(detail) => format!("error: {detail}"),
None => "error".to_owned(),
},
s => s.to_owned(),
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 error details (successful mappings are summarized in counts only).
let error_mappings: Vec<_> = merged
.mappings
.iter()
.filter(|r| r.status == "error")
.collect();

if !error_mappings.is_empty() {
let mut table = Table::new();
table
.add_row()
.add(&result.stack_root)
.add(&result.source_root)
.add(&status);
.title_row()
.add("Stack Root")
.add("Source Root")
.add("Detail");

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(detail);
}

table.print();
println!();
}

for err in &merged.batch_errors {
println!("{err}");
}

table.print();
println!();
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(())
}

#[derive(Default)]
struct MergedResponse {
created: u64,
updated: u64,
errors: u64,
mappings: Vec<BulkCodeMappingResult>,
batch_errors: Vec<String>,
}

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);
}
}
Original file line number Diff line number Diff line change
@@ -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.

```
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Original file line number Diff line number Diff line change
@@ -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"}
]
}
73 changes: 72 additions & 1 deletion tests/integration/code_mappings/upload.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -10,3 +12,72 @@ fn command_code_mappings_upload() {
.register_trycmd_test("code_mappings/code-mappings-upload.trycmd")
.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).
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);
}
Loading