Skip to content

feat(sourcemaps): Multi-project sourcemaps upload #2497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 8, 2025
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
1 change: 1 addition & 0 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,7 @@ impl RegionSpecificApi<'_> {
PathArg(context.release())
)
};

let mut form = curl::easy::Form::new();

let filename = Path::new(name)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/debug_files/bundle_jvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {

let context = &UploadContext {
org: &org,
project: project.as_deref(),
projects: &project.into_iter().collect::<Vec<_>>(),
release: None,
dist: None,
note: None,
Expand Down
2 changes: 1 addition & 1 deletion src/commands/files/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {

let context = &UploadContext {
org: &org,
project: project.as_deref(),
projects: &project.into_iter().collect::<Vec<_>>(),
release: Some(&release),
dist,
note: None,
Expand Down
7 changes: 4 additions & 3 deletions src/commands/react_native/appcenter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
let config = Config::current();
let here = env::current_dir()?;
let here_str: &str = &here.to_string_lossy();
let (org, project) = config.get_org_and_project(matches)?;
let org = config.get_org(matches)?;
let projects = config.get_projects(matches)?;
let app = matches.get_one::<String>("app_name").unwrap();
let platform = matches.get_one::<String>("platform").unwrap();
let deployment = matches
Expand Down Expand Up @@ -189,7 +190,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {

processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects: &projects,
release: Some(&release),
dist: None,
note: None,
Expand All @@ -208,7 +209,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {

processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects: &projects,
release: Some(&release),
dist: Some(dist),
note: None,
Expand Down
7 changes: 4 additions & 3 deletions src/commands/react_native/gradle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ pub fn make_command(command: Command) -> Command {

pub fn execute(matches: &ArgMatches) -> Result<()> {
let config = Config::current();
let (org, project) = config.get_org_and_project(matches)?;
let org = config.get_org(matches)?;
let projects = config.get_projects(matches)?;
let api = Api::current();
let base = env::current_dir()?;

Expand Down Expand Up @@ -123,7 +124,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {

processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects: &projects,
release: Some(version),
dist: Some(dist),
note: None,
Expand All @@ -137,7 +138,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
// Debug Id Upload
processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects: &projects,
release: None,
dist: None,
note: None,
Expand Down
7 changes: 4 additions & 3 deletions src/commands/react_native/xcode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
if dist_from_env.is_err() && release_from_env.is_err() && matches.get_flag("no_auto_release") {
processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects: &[project],
release: None,
dist: None,
note: None,
Expand Down Expand Up @@ -376,7 +376,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
None => {
processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects: &[project],
release: release_name.as_deref(),
dist: dist.as_deref(),
note: None,
Expand All @@ -387,10 +387,11 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
})?;
}
Some(dists) => {
let projects = &[project];
for dist in dists {
processor.upload(&UploadContext {
org: &org,
project: Some(&project),
projects,
release: release_name.as_deref(),
dist: Some(dist),
note: None,
Expand Down
5 changes: 3 additions & 2 deletions src/commands/sourcemaps/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,8 @@ fn process_sources_from_paths(
pub fn execute(matches: &ArgMatches) -> Result<()> {
let config = Config::current();
let version = config.get_release_with_legacy_fallback(matches).ok();
let (org, project) = config.get_org_and_project(matches)?;
let org = config.get_org(matches)?;
let projects = config.get_projects(matches)?;
let api = Api::current();
let mut processor = SourceMapProcessor::new();
let mut chunk_upload_options = api.authenticated()?.get_chunk_upload_options(&org)?;
Expand Down Expand Up @@ -450,7 +451,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
let max_wait = wait_for_secs.map_or(DEFAULT_MAX_WAIT, Duration::from_secs);
let upload_context = UploadContext {
org: &org,
project: Some(&project),
projects: &projects,
release: version.as_deref(),
dist: matches.get_one::<String>("dist").map(String::as_str),
note: matches.get_one::<String>("note").map(String::as_str),
Expand Down
52 changes: 36 additions & 16 deletions src/utils/file_upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn initialize_legacy_release_upload(context: &UploadContext) -> Result<()> {
// need to do anything here. Artifact bundles will also only work
// if a project is provided which is technically unnecessary for the
// legacy upload though it will unlikely to be what users want.
if context.project.is_some()
if !context.projects.is_empty()
&& context.chunk_upload_options.is_some_and(|x| {
x.supports(ChunkUploadCapability::ArtifactBundles)
|| x.supports(ChunkUploadCapability::ArtifactBundlesV2)
Expand All @@ -52,7 +52,7 @@ pub fn initialize_legacy_release_upload(context: &UploadContext) -> Result<()> {
}

// TODO: make this into an error later down the road
if context.project.is_none() {
if context.projects.is_empty() {
eprintln!(
"{}",
style(
Expand All @@ -71,7 +71,7 @@ pub fn initialize_legacy_release_upload(context: &UploadContext) -> Result<()> {
context.org,
&NewRelease {
version: version.to_string(),
projects: context.project.map(|x| x.to_string()).into_iter().collect(),
projects: context.projects.to_vec(),
..Default::default()
},
)?;
Expand All @@ -84,7 +84,7 @@ pub fn initialize_legacy_release_upload(context: &UploadContext) -> Result<()> {
#[derive(Debug, Clone)]
pub struct UploadContext<'a> {
pub org: &'a str,
pub project: Option<&'a str>,
pub projects: &'a [String],
pub release: Option<&'a str>,
pub dist: Option<&'a str>,
pub note: Option<&'a str>,
Expand All @@ -105,6 +105,8 @@ impl UploadContext<'_> {
pub enum LegacyUploadContextError {
#[error("a release is required for this upload")]
ReleaseMissing,
#[error("only a single project is supported for this upload")]
ProjectMultiple,
}

/// Represents the context for legacy release uploads.
Expand Down Expand Up @@ -182,12 +184,18 @@ impl<'a> TryFrom<&'a UploadContext<'_>> for LegacyUploadContext<'a> {
fn try_from(value: &'a UploadContext) -> Result<Self, Self::Error> {
let &UploadContext {
org,
project,
projects,
release,
dist,
..
} = value;

let project = match projects {
[] => None,
[project] => Some(project.as_str()),
[_, _, ..] => Err(LegacyUploadContextError::ProjectMultiple)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern could also be _ but actually makes sense this way, because it clarifies that the list has at least two elements.

};

let release = release.ok_or(LegacyUploadContextError::ReleaseMissing)?;

Ok(Self {
Expand Down Expand Up @@ -292,14 +300,23 @@ impl<'a> FileUpload<'a> {
}

pub fn upload(&self) -> Result<()> {
// multiple projects OK
initialize_legacy_release_upload(self.context)?;

if let Some(chunk_options) = self.context.chunk_upload_options {
if chunk_options.supports(ChunkUploadCapability::ReleaseFiles) {
// multiple projects OK
return upload_files_chunked(self.context, &self.files, chunk_options);
}
}

log::warn!(
"Your Sentry server does not support chunked uploads. \
We are falling back to a legacy upload method, which \
has fewer features and is less reliable. Please consider \
upgrading your Sentry server or switching to our SaaS offering."
);

// Do not permit uploads of more than 20k files if the server does not
// support artifact bundles. This is a temporary downside protection to
// protect users from uploading more sources than we support.
Expand All @@ -318,10 +335,12 @@ impl<'a> FileUpload<'a> {
let legacy_context = &self.context.try_into().map_err(|e| {
anyhow::anyhow!(
"Error while performing legacy upload: {e}. \
If you would like to upload files {}, you need to upgrade your Sentry server \
or switch to our SaaS offering.",
If you would like to upload files {}, you need to upgrade your Sentry server \
or switch to our SaaS offering.",
match e {
LegacyUploadContextError::ReleaseMissing => "without specifying a release",
LegacyUploadContextError::ProjectMultiple =>
"to multiple projects simultaneously",
}
)
})?;
Expand Down Expand Up @@ -448,13 +467,13 @@ fn poll_assemble(
let authenticated_api = api.authenticated()?;
let use_artifact_bundle = (options.supports(ChunkUploadCapability::ArtifactBundles)
|| options.supports(ChunkUploadCapability::ArtifactBundlesV2))
&& context.project.is_some();
&& !context.projects.is_empty();
let response = loop {
// prefer standalone artifact bundle upload over legacy release based upload
let response = if use_artifact_bundle {
authenticated_api.assemble_artifact_bundle(
context.org,
&[context.project.unwrap().to_string()],
context.projects,
checksum,
chunks,
context.release,
Expand Down Expand Up @@ -540,11 +559,11 @@ fn upload_files_chunked(

// Filter out chunks that are already on the server. This only matters if the server supports
// `ArtifactBundlesV2`, otherwise the `missing_chunks` field is meaningless.
if options.supports(ChunkUploadCapability::ArtifactBundlesV2) && context.project.is_some() {
if options.supports(ChunkUploadCapability::ArtifactBundlesV2) && !context.projects.is_empty() {
let api = Api::current();
let response = api.authenticated()?.assemble_artifact_bundle(
context.org,
&[context.project.unwrap().to_string()],
context.projects,
checksum,
&checksums,
context.release,
Expand Down Expand Up @@ -611,8 +630,9 @@ fn build_artifact_bundle(
}

bundle.set_attribute("org".to_owned(), context.org.to_owned());
if let Some(project) = context.project {
bundle.set_attribute("project".to_owned(), project.to_owned());
if let [project] = context.projects {
// Only set project if there is exactly one project
bundle.set_attribute("project".to_owned(), project);
}
if let Some(release) = context.release {
bundle.set_attribute("release".to_owned(), release.to_owned());
Expand Down Expand Up @@ -703,8 +723,8 @@ fn print_upload_context_details(context: &UploadContext) {
);
println!(
"{} {}",
style("> Project:").dim(),
style(context.project.unwrap_or("None")).yellow()
style("> Projects:").dim(),
style(context.projects.join(", ")).yellow()
);
println!(
"{} {}",
Expand Down Expand Up @@ -768,7 +788,7 @@ mod tests {
fn build_artifact_bundle_deterministic() {
let context = UploadContext {
org: "wat-org",
project: Some("wat-project"),
projects: &["wat-project".into()],
release: None,
dist: None,
note: None,
Expand Down
19 changes: 8 additions & 11 deletions src/utils/sourcemaps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -769,13 +769,16 @@ impl SourceMapProcessor {
fn flag_uploaded_sources(&mut self, context: &UploadContext<'_>) -> usize {
let mut files_needing_upload = self.sources.len();

// TODO: this endpoint does not exist for non release based uploads
if !context.dedupe {
return files_needing_upload;
}
let release = match context.release {
Some(release) => release,
None => return files_needing_upload,

// This endpoint only supports at most one project, and a release is required.
// If the upload contains multiple projects or no release, we do not use deduplication.
let (project, release) = match (context.projects, context.release) {
([project], Some(release)) => (Some(project.as_str()), release),
([], Some(release)) => (None, release),
_ => return files_needing_upload,
};

let mut sources_checksums: Vec<_> = self
Expand All @@ -790,12 +793,7 @@ impl SourceMapProcessor {
let api = Api::current();

if let Ok(artifacts) = api.authenticated().and_then(|api| {
api.list_release_files_by_checksum(
context.org,
context.project,
release,
&sources_checksums,
)
api.list_release_files_by_checksum(context.org, project, release, &sources_checksums)
}) {
let already_uploaded_checksums: HashSet<_> = artifacts
.into_iter()
Expand Down Expand Up @@ -852,7 +850,6 @@ impl SourceMapProcessor {
}
}
}

let files_needing_upload = self.flag_uploaded_sources(context);
if files_needing_upload > 0 {
let mut uploader = FileUpload::new(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Processing react-native sourcemaps for Sentry upload.
> Uploaded files to Sentry
> File upload complete (processing pending on server)
> Organization: wat-org
> Project: wat-project
> Projects: wat-project
> Release: test-release
> Dist: test-dist
> Upload type: artifact bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ $ sentry-cli sourcemaps upload tests/integration/_fixtures/upload_debugid_alias
> Uploaded files to Sentry
> File upload complete (processing pending on server)
> Organization: wat-org
> Project: wat-project
> Projects: wat-project
> Release: None
> Dist: None
> Upload type: artifact bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ $ sentry-cli sourcemaps upload --bundle tests/integration/_fixtures/file-hermes-
> Uploaded files to Sentry
> File upload complete (processing pending on server)
> Organization: wat-org
> Project: wat-project
> Projects: wat-project
> Release: None
> Dist: None
> Upload type: artifact bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $ sentry-cli sourcemaps upload --bundle tests/integration/_fixtures/file-ram-bun
> Uploaded files to Sentry
> File upload complete (processing pending on server)
> Organization: wat-org
> Project: wat-project
> Projects: wat-project
> Release: wat-release
> Dist: None
> Upload type: artifact bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $ sentry-cli sourcemaps upload --bundle tests/integration/_fixtures/indexed-ram-
> Uploaded files to Sentry
> File upload complete (processing pending on server)
> Organization: wat-org
> Project: wat-project
> Projects: wat-project
> Release: wat-release
> Dist: None
> Upload type: artifact bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ $ sentry-cli sourcemaps upload tests/integration/_fixtures/bundle.min.js.map tes
> Uploaded files to Sentry
> File upload complete (processing pending on server)
> Organization: wat-org
> Project: wat-project
> Projects: wat-project
> Release: None
> Dist: None
> Upload type: artifact bundle
Expand Down
Loading