Skip to content

Commit 6d66699

Browse files
authored
Merge branch 'master' into fix/goblin-logging-perf
2 parents 2a3e94f + 5477440 commit 6d66699

13 files changed

Lines changed: 274 additions & 76 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# Files co-owned by Emerge Tools team
1515
/apple-catalog-parsing @getsentry/emerge-tools @getsentry/owners-sentry-cli
16+
/src/api/data_types/snapshots.rs @getsentry/emerge-tools @getsentry/owners-sentry-cli
1617
/src/commands/build @getsentry/emerge-tools @getsentry/owners-sentry-cli
1718
/src/utils/build @getsentry/emerge-tools @getsentry/owners-sentry-cli
1819
/tests/integration/build @getsentry/emerge-tools @getsentry/owners-sentry-cli

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22

33
## Unreleased
44

5+
### Fixes
6+
7+
- (sourcemaps) Skip non-base64 embedded sourcemaps during injection ([#3243](https://github.com/getsentry/sentry-cli/pull/3243))
8+
59
### New Features ✨
610

711
- Add `sentry-cli build download` command to download installable builds (IPA/APK) by build ID ([#3221](https://github.com/getsentry/sentry-cli/pull/3221)).
12+
- 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)).
13+
- 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.
14+
- Repository name and default branch are automatically inferred from your local git remotes, or can be specified explicitly with `--repo` and `--default-branch`.
15+
- Large mapping files are automatically split into batches for upload.
816

917
## 3.3.3
1018

package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/data_types/snapshots.rs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use serde_json::Value;
77

88
use super::VcsInfo;
99

10-
const IMAGE_FILE_NAME_FIELD: &str = "image_file_name";
1110
const WIDTH_FIELD: &str = "width";
1211
const HEIGHT_FIELD: &str = "height";
1312

@@ -42,16 +41,7 @@ pub struct ImageMetadata {
4241
}
4342

4443
impl ImageMetadata {
45-
pub fn new(
46-
image_file_name: String,
47-
width: u32,
48-
height: u32,
49-
mut extra: HashMap<String, Value>,
50-
) -> Self {
51-
extra.insert(
52-
IMAGE_FILE_NAME_FIELD.to_owned(),
53-
Value::String(image_file_name),
54-
);
44+
pub fn new(width: u32, height: u32, mut extra: HashMap<String, Value>) -> Self {
5545
extra.insert(WIDTH_FIELD.to_owned(), Value::from(width));
5646
extra.insert(HEIGHT_FIELD.to_owned(), Value::from(height));
5747

@@ -68,18 +58,16 @@ mod tests {
6858
#[test]
6959
fn cli_managed_fields_override_sidecar_fields() {
7060
let extra = serde_json::from_value(json!({
71-
(IMAGE_FILE_NAME_FIELD): "from-sidecar.png",
7261
(WIDTH_FIELD): 1,
7362
(HEIGHT_FIELD): 2,
7463
"custom": "keep-me"
7564
}))
7665
.unwrap();
7766

78-
let metadata = ImageMetadata::new("from-cli.png".to_owned(), 100, 200, extra);
67+
let metadata = ImageMetadata::new(100, 200, extra);
7968
let serialized = serde_json::to_value(metadata).unwrap();
8069

8170
let expected = json!({
82-
(IMAGE_FILE_NAME_FIELD): "from-cli.png",
8371
(WIDTH_FIELD): 100,
8472
(HEIGHT_FIELD): 200,
8573
"custom": "keep-me"

src/commands/build/snapshots.rs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,28 @@ fn upload_images(
331331

332332
let mut many_builder = session.many();
333333
let mut manifest_entries = HashMap::new();
334-
let image_count = images.len();
335-
334+
let mut collisions: HashMap<String, Vec<String>> = HashMap::new();
335+
let mut kept_paths: HashMap<String, String> = HashMap::new();
336336
for image in images {
337337
debug!("Processing image: {}", image.path.display());
338338

339+
let image_file_name = image
340+
.relative_path
341+
.file_name()
342+
.unwrap_or_default()
343+
.to_string_lossy()
344+
.into_owned();
345+
346+
let relative_path = crate::utils::fs::path_as_url(&image.relative_path);
347+
348+
if manifest_entries.contains_key(&image_file_name) {
349+
collisions
350+
.entry(image_file_name)
351+
.or_default()
352+
.push(relative_path);
353+
continue;
354+
}
355+
339356
let hash = compute_sha256_hash(&image.path)?;
340357
let file = runtime
341358
.block_on(tokio::fs::File::open(&image.path))
@@ -353,33 +370,40 @@ fn upload_images(
353370
.expiration_policy(expiration),
354371
);
355372

356-
let image_file_name = image
357-
.relative_path
358-
.file_name()
359-
.unwrap_or_default()
360-
.to_string_lossy()
361-
.into_owned();
362-
363-
let extra = read_sidecar_metadata(&image.path).unwrap_or_else(|err| {
373+
let mut extra = read_sidecar_metadata(&image.path).unwrap_or_else(|err| {
364374
warn!("Error reading sidecar metadata, ignoring it instead: {err:#}");
365375
HashMap::new()
366376
});
377+
extra.insert("content_hash".to_owned(), serde_json::Value::String(hash));
367378

379+
kept_paths.insert(image_file_name.clone(), relative_path);
368380
manifest_entries.insert(
369-
hash,
370-
ImageMetadata::new(image_file_name, image.width, image.height, extra),
381+
image_file_name,
382+
ImageMetadata::new(image.width, image.height, extra),
371383
);
372384
}
373385

386+
if !collisions.is_empty() {
387+
let mut details = String::new();
388+
for (name, excluded_paths) in &collisions {
389+
let mut all_paths = vec![kept_paths[name].as_str()];
390+
all_paths.extend(excluded_paths.iter().map(|s| s.as_str()));
391+
details.push_str(&format!("\n {name}: {}", all_paths.join(", ")));
392+
}
393+
warn!("Some images share identical file names. Only the first occurrence of each is included:{details}");
394+
}
395+
374396
let result = runtime.block_on(async { many_builder.send().error_for_failures().await });
375397

398+
let uploaded_count = manifest_entries.len();
399+
376400
match result {
377401
Ok(()) => {
378402
println!(
379403
"{} Uploaded {} image {}",
380404
style(">").dim(),
381-
style(image_count).yellow(),
382-
if image_count == 1 { "file" } else { "files" }
405+
style(uploaded_count).yellow(),
406+
if uploaded_count == 1 { "file" } else { "files" }
383407
);
384408
Ok(manifest_entries)
385409
}
@@ -391,7 +415,7 @@ fn upload_images(
391415
eprintln!(" {}", style(format!("{error:#}")).red());
392416
error_count += 1;
393417
}
394-
anyhow::bail!("Failed to upload {error_count} out of {image_count} images")
418+
anyhow::bail!("Failed to upload {error_count} out of {uploaded_count} images")
395419
}
396420
}
397421
}

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::*;

src/utils/sourcemaps.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,12 @@ impl SourceMapProcessor {
790790

791791
let Ok(mut decoded) = data_encoding::BASE64.decode(encoded.as_bytes())
792792
else {
793-
bail!("Invalid embedded sourcemap in source file {source_url}");
793+
// The data URL isn't valid base64, so we can't use it.
794+
// This commonly happens when minified bundles contain
795+
// sourceMappingURL strings inside template literals
796+
// (e.g from bundled terser or babel)
797+
warn!("Skipping {source_url}: embedded sourcemap is not valid base64");
798+
continue;
794799
};
795800

796801
let mut sourcemap =

0 commit comments

Comments
 (0)