From cba6a75d3dad8af28359f48937b3c10126b256a6 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 7 Aug 2025 20:58:46 +0200 Subject: [PATCH 01/25] temporary --- DART_SYMBOL_MAP_PLAN.md | 131 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 DART_SYMBOL_MAP_PLAN.md diff --git a/DART_SYMBOL_MAP_PLAN.md b/DART_SYMBOL_MAP_PLAN.md new file mode 100644 index 0000000000..c720f1bc7a --- /dev/null +++ b/DART_SYMBOL_MAP_PLAN.md @@ -0,0 +1,131 @@ +## Implementation Plan: upload-dart-symbol-map + +Goal: Add a CLI command to upload a Dart/Flutter symbol map ("dartsymbolmap") for deobfuscating Dart exception types. The upload must use the chunk-upload flow and associate the mapping to a required debug id, preferably extracted from an associated debug file. + +### High-level architecture (aligned with existing codebase) +- Reuse the existing chunked upload infrastructure in `src/utils/chunks/` and the assemble endpoint in `src/api/mod.rs::assemble_difs(...)`. +- Introduce a small wrapper object implementing `Assemblable` for the mapping file so it can be sent through `chunks::upload_chunked_objects`. +- Add a new CLI subcommand, following patterns used by `upload_proguard` and `sourcemaps upload`. +- Validate the mapping file locally before upload to fail fast. +- Ensure server capability support via `ChunkServerOptions.accept` for a new capability string "dartsymbolmap". + +--- + +### Step-by-step tasks + +1) Capability support for "dartsymbolmap" + - Add a new variant to `ChunkUploadCapability` and update deserialization: + - File: `src/api/data_types/chunking/upload/capability.rs` + - Add `DartSymbolMap` variant and map the string `"dartsymbolmap"` to it in the `Deserialize` impl. + - No change needed to `ChunkServerOptions.should_strip_debug_ids()`. + - Optional: Add a helper method (if desired) to check support: `options.supports(ChunkUploadCapability::DartSymbolMap)`. + +2) Represent the mapping to be uploaded + - Create a lightweight struct, e.g. `DartSymbolMapObject { bytes: Vec, name: String, debug_id: DebugId }`. + - Implement: + - `AsRef<[u8]>` to expose raw bytes + - `Assemblable` to provide `name()` and `debug_id()` (see `src/utils/chunks/types.rs`) + - `Display` for user-friendly printing during summaries + - We will then wrap it with `chunks::Chunked::from(object, chunk_size)` so we can reuse `chunks::upload_chunked_objects(...)` which: + - Calls `assemble_difs` to retrieve missing chunks and current states + - Uploads missing chunks via `Api::upload_chunks(...)` + - Polls assemble until completion (configurable via `ChunkOptions::with_max_wait`) + +3) Local validation of the Dart symbol map file + - Implement a small validator function used by the command before uploading: + - Read file bytes; parse JSON as `Vec` using `serde_json`. + - Ensure even length; if odd or not an array of strings, error with a clear message. + - Keep the file name as provided; if the user passes a directory name or odd extension, continue but recommend `dartsymbolmap.json` in help text. + +4) Debug ID resolution + - Inputs: a required path to the associated debug file (not the dartsymbolmap). + - Behavior: + - Open the associated debug file and extract debug ids via `utils::dif::DifFile::open_path(...).ids()`. + - If exactly one id is present, use it. + - If multiple, error and ask the user to disambiguate (e.g., select the correct variant via a future flag) — for now we will error with a clear message. + - If none, error out with a clear message. + +5) New CLI command: upload-dart-symbol-map + - File: `src/commands/upload_dart_symbol_map.rs` + - Command shape (similar to `upload_proguard.rs`): + - `.about("Upload a Dart symbol map file to a project.")` + - `.org_arg()` and `.project_arg(false)` and fetch both via `Config::current().get_org_and_project(matches)?`. + - Positional args: + - `mapping` (required): path to `dartsymbolmap.json` + - `debug_file` (required): path to the associated debug file to extract the debug id + - Flags: none (keep command simple; no wait flags) + - Execution flow: + 1. Validate mapping file (JSON array of strings, even length) + 2. Resolve `debug_id` as described in Step 4 + 3. Get `ChunkServerOptions` via `api.authenticated()?.get_chunk_upload_options(&org)?` + 4. Ensure `options.supports(ChunkUploadCapability::DartSymbolMap)`, else bail with an actionable message + 5. Build `DartSymbolMapObject` with bytes, `name` (use basename of path; recommend `dartsymbolmap.json`), and `debug_id` + 6. Compute chunking: `Chunked::from(object, options.chunk_size as usize)` + 7. Construct `ChunkOptions::new(options, org, project).with_max_wait(DEFAULT_MAX_WAIT)` to always wait/poll until completion (bounded by server `max_wait`) + 8. Call `chunks::upload_chunked_objects(&[chunked], chunk_options)` + 9. Rely on existing summary output from `chunks::upload_chunked_objects`/`poll_assemble`; optionally print a concise success line for the mapping + +6) Wire into the root command + - File: `src/commands/mod.rs` + - Add `mod upload_dart_symbol_map;` + - Add to `each_subcommand!` list so it’s registered + - Optional: consider adding to `UPDATE_NAGGER_CMDS` + +7) Tests (integration-focused) + - Directory: `tests/integration/_cases/dart_symbol_map/` + - Cases: + - `help.trycmd`: validates help text + - `validate-json-invalid.trycmd`: invalid JSON (non-array) -> error + - `validate-json-odd-length.trycmd`: odd number of elements -> error + - `missing-debug-id.trycmd`: debug file has no debug id -> error + - `multiple-debug-ids.trycmd`: ambiguous debug file -> error with guidance to disambiguate (no `--debug-id` supported) + - `happy-path.trycmd`: mock chunk-upload+assemble: + - GET chunk-upload returns accept includes `"dartsymbolmap"` and compression includes `gzip` + - First assemble returns `NOT_FOUND` with `missingChunks` + - Upload chunks + - Subsequent assemble returns `OK` (or `CREATED` → subsequent `OK`) + - Fixtures: + - `tests/integration/_fixtures/dartsymbolmap.json` + - Sample debug file already in `_fixtures/` (Mach-O, ELF, Breakpad, etc.) + - Mock responses live under `tests/integration/_responses/` + +8) Documentation and UX + - Add command description and examples to `--help` and a short note in `README.md`. + - Clear errors for capability missing, invalid mapping, and debug id issues. + +9) Non-functional notes + - Hash algorithm: server specifies `sha1`; existing utilities already use SHA1 (see `get_sha1_checksums`). + - Region correctness: the `url` from chunk-upload options is used directly by `Api::upload_chunks`. + - Compression: chosen automatically from `ChunkServerOptions.compression` (prefers `gzip` when supported). + +--- + +### Key code references +- Chunk server options and assemble: + - `src/api/mod.rs::get_chunk_upload_options`, `assemble_difs` (POST `/files/difs/assemble/`) +- Chunking utilities and traits: + - `src/utils/chunks/` (`ChunkOptions`, `Chunked`, `Assemblable`, `upload_chunked_objects`) +- Capability parsing: + - `src/api/data_types/chunking/upload/capability.rs` +- Chunk-upload network call (multipart with filename = chunk sha1): + - `src/api/mod.rs::upload_chunks` +- Debug-id extraction from debug files: + - `src/utils/dif.rs` (`DifFile::open_path`, `ids()`) + +--- + +### Assumptions & open questions +- Server side recognizes `name` and `debug_id` for `dartsymbolmap` via `assemble_difs`. No extra type discriminator required. +- Association rule: the mapping’s `debug_id` must match the associated debug file’s id; we will enforce extraction exclusively from the provided debug file path (no `--debug-id` override). +- If you want the command to accept a single path and infer the debug file in the future, we can extend UX later, but for now two paths are required. + +--- + +### Rollout checklist +1. Implement code changes above +2. `cargo build --workspace` +3. `cargo test --workspace` (run with `TRYCMD=overwrite` once to create snapshots) +4. Verify on a project with a real dartsymbolmap and debug file +5. Add brief docs snippet to README + + From 07b6449b48dcb2f386bd5784cbacd8ec0ba2668b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Thu, 7 Aug 2025 23:26:58 +0200 Subject: [PATCH 02/25] Initial implementation --- .../data_types/chunking/upload/capability.rs | 4 + src/commands/mod.rs | 2 + src/commands/upload_dart_symbol_map.rs | 157 ++++++++++++++++++ src/utils/chunks/options.rs | 18 +- 4 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 src/commands/upload_dart_symbol_map.rs diff --git a/src/api/data_types/chunking/upload/capability.rs b/src/api/data_types/chunking/upload/capability.rs index aa9f3fda8a..729a5e4611 100644 --- a/src/api/data_types/chunking/upload/capability.rs +++ b/src/api/data_types/chunking/upload/capability.rs @@ -30,6 +30,9 @@ pub enum ChunkUploadCapability { /// Upload of il2cpp line mappings Il2Cpp, + /// Upload of Dart symbol maps + DartSymbolMap, + /// Upload of preprod artifacts PreprodArtifacts, @@ -52,6 +55,7 @@ impl<'de> Deserialize<'de> for ChunkUploadCapability { "sources" => ChunkUploadCapability::Sources, "bcsymbolmaps" => ChunkUploadCapability::BcSymbolmap, "il2cpp" => ChunkUploadCapability::Il2Cpp, + "dartsymbolmap" => ChunkUploadCapability::DartSymbolMap, "preprod_artifacts" => ChunkUploadCapability::PreprodArtifacts, _ => ChunkUploadCapability::Unknown, }) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a894619d9f..0b9b40bf0d 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -47,6 +47,7 @@ mod update; mod upload_dif; mod upload_dsym; mod upload_proguard; +mod upload_dart_symbol_map; macro_rules! each_subcommand { ($mac:ident) => { @@ -78,6 +79,7 @@ macro_rules! each_subcommand { $mac!(upload_dif); $mac!(upload_dsym); $mac!(upload_proguard); + $mac!(upload_dart_symbol_map); }; } diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs new file mode 100644 index 0000000000..2f63329ead --- /dev/null +++ b/src/commands/upload_dart_symbol_map.rs @@ -0,0 +1,157 @@ +use std::borrow::Cow; +use std::ffi::OsStr; +use std::path::Path; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use anyhow::{bail, Context as _, Result}; +use clap::{Arg, ArgMatches, Command}; +use console::style; + +use crate::api::{Api, ChunkUploadCapability}; +use crate::config::Config; +use crate::constants::DEFAULT_MAX_WAIT; +use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked}; +use crate::utils::args::ArgExt as _; +use crate::utils::dif::DifFile; +use symbolic::common::DebugId; + +struct DartSymbolMapObject { + bytes: Vec, + name: String, + debug_id: DebugId, +} + +impl AsRef<[u8]> for DartSymbolMapObject { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +impl Display for DartSymbolMapObject { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "dartsymbolmap {}", self.name) + } +} + +impl Assemblable for DartSymbolMapObject { + fn name(&self) -> Cow<'_, str> { + Cow::from(self.name.as_str()) + } + + fn debug_id(&self) -> Option { + Some(self.debug_id) + } +} + +pub fn make_command(command: Command) -> Command { + command + .about("Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.") + .after_help( + "Examples:\n sentry-cli upload-dart-symbol-map --org my-org --project my-proj path/to/dartsymbolmap.json path/to/debug/file\n\n The mapping must be a JSON array of strings with an even number of entries (pairs).\n The debug file must contain exactly one Debug ID.", + ) + .org_arg() + .project_arg(false) + .arg( + Arg::new("mapping") + .value_name("MAPPING") + .help("Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs).") + .required(true), + ) + .arg( + Arg::new("debug_file") + .value_name("DEBUG_FILE") + .help("Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID.") + .required(true), + ) +} + +pub fn execute(matches: &ArgMatches) -> Result<()> { + // Parse required positional arguments + let mapping_path = matches.get_one::("mapping").unwrap(); + let debug_file_path = matches.get_one::("debug_file").unwrap(); + + // Extract Debug ID(s) from the provided debug file + let dif = DifFile::open_path(debug_file_path, None)?; + let mut ids: Vec<_> = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); + + // Ensure a single, unambiguous Debug ID + ids.sort(); + ids.dedup(); + match ids.len() { + 0 => bail!( + "No debug identifier found in the provided debug file ({}). Ensure the file contains an embedded Debug ID.", + debug_file_path + ), + 1 => { + let debug_id = ids.remove(0); + + // Validate the dartsymbolmap JSON: must be a JSON array of strings with even length + let mapping_file_bytes = std::fs::read(mapping_path) + .with_context(|| format!("Failed to read mapping file at {}", mapping_path))?; + let mapping_entries: Vec = serde_json::from_slice(&mapping_file_bytes) + .context("Invalid dartsymbolmap: expected a JSON array of strings")?; + + if mapping_entries.len() % 2 != 0 { + bail!( + "Invalid dartsymbolmap: expected an even number of entries (pairs), got {}", + mapping_entries.len() + ); + } + + // Prepare upload object + let file_name = Path::new(mapping_path) + .file_name() + .and_then(OsStr::to_str) + .unwrap_or(mapping_path) + .to_string(); + + let object = DartSymbolMapObject { + bytes: mapping_file_bytes, + name: file_name.clone(), + debug_id, + }; + + // Prepare chunked upload + let api = Api::current(); + let (org, project) = Config::current().get_org_and_project(matches)?; + let chunk_upload_options = api + .authenticated()? + .get_chunk_upload_options(&org)? + .ok_or_else(|| anyhow::anyhow!( + "server does not support chunked uploading. Please update your Sentry server." + ))?; + + if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) { + bail!( + "Server does not support uploading Dart symbol maps via chunked upload. Please update your Sentry server." + ); + } + + let options = ChunkOptions::new(chunk_upload_options, &org, &project) + .with_max_wait(DEFAULT_MAX_WAIT) + .with_strip_debug_ids_override(false); + + let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?; + let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?; + if has_processing_errors { + bail!("Some symbol maps did not process correctly"); + } + + println!( + "{} Uploaded dartsymbolmap '{}' for Debug ID {}", + style(">").dim(), + file_name, + style(debug_id).dim() + ); + + Ok(()) + } + _ => bail!( + "Multiple debug identifiers found in the provided debug file ({}): {}. Please provide a file that contains a single Debug ID.", + debug_file_path, + ids.into_iter().map(|id| id.to_string()).collect::>().join(", ") + ), + } +} + + diff --git a/src/utils/chunks/options.rs b/src/utils/chunks/options.rs index 767a8bf37d..6f82050b3a 100644 --- a/src/utils/chunks/options.rs +++ b/src/utils/chunks/options.rs @@ -13,6 +13,11 @@ pub struct ChunkOptions<'a> { /// If the server_options.max_wait is set to a smaller nonzero value, /// we use that value instead. max_wait: Duration, + + /// Optional override for whether to strip debug ids. + /// When `Some(value)`, this value takes precedence over the server option. + /// When `None`, we defer to `server_options.should_strip_debug_ids()`. + strip_debug_ids_override: Option, } impl<'a> ChunkOptions<'a> { @@ -22,6 +27,7 @@ impl<'a> ChunkOptions<'a> { org, project, max_wait: Duration::ZERO, + strip_debug_ids_override: None, } } @@ -32,7 +38,17 @@ impl<'a> ChunkOptions<'a> { } pub fn should_strip_debug_ids(&self) -> bool { - self.server_options.should_strip_debug_ids() + match self.strip_debug_ids_override { + Some(override_value) => override_value, + None => self.server_options.should_strip_debug_ids(), + } + } + + /// Override whether to strip debug ids from uploaded files. + /// If not set, the behavior falls back to the server options. + pub fn with_strip_debug_ids_override(mut self, strip: bool) -> Self { + self.strip_debug_ids_override = Some(strip); + self } pub fn org(&self) -> &str { From 2004fa31aeeae0c9019c1fa39e776555ac34826a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 01:00:45 +0200 Subject: [PATCH 03/25] Update --- src/commands/upload_dart_symbol_map.rs | 27 ++++++++++----- tests/integration/_cases/help/help.trycmd | 42 ++++++++++++----------- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs index 2f63329ead..33d442a480 100644 --- a/src/commands/upload_dart_symbol_map.rs +++ b/src/commands/upload_dart_symbol_map.rs @@ -5,11 +5,10 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; -use console::style; use crate::api::{Api, ChunkUploadCapability}; use crate::config::Config; -use crate::constants::DEFAULT_MAX_WAIT; +use crate::constants::{DEFAULT_MAX_DIF_SIZE, DEFAULT_MAX_WAIT}; use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked}; use crate::utils::args::ArgExt as _; use crate::utils::dif::DifFile; @@ -105,6 +104,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .unwrap_or(mapping_path) .to_string(); + let mapping_len = mapping_file_bytes.len(); let object = DartSymbolMapObject { bytes: mapping_file_bytes, name: file_name.clone(), @@ -127,6 +127,22 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { ); } + // Early file size check against server or default limits (same as debug files) + let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { + chunk_upload_options.max_file_size + } else { + DEFAULT_MAX_DIF_SIZE + }; + + if (mapping_len as u64) > effective_max_file_size { + bail!( + "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", + mapping_path, + mapping_len, + effective_max_file_size + ); + } + let options = ChunkOptions::new(chunk_upload_options, &org, &project) .with_max_wait(DEFAULT_MAX_WAIT) .with_strip_debug_ids_override(false); @@ -137,13 +153,6 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { bail!("Some symbol maps did not process correctly"); } - println!( - "{} Uploaded dartsymbolmap '{}' for Debug ID {}", - style(">").dim(), - file_name, - style(debug_id).dim() - ); - Ok(()) } _ => bail!( diff --git a/tests/integration/_cases/help/help.trycmd b/tests/integration/_cases/help/help.trycmd index 03ac6a02d4..d6e3aee13d 100644 --- a/tests/integration/_cases/help/help.trycmd +++ b/tests/integration/_cases/help/help.trycmd @@ -10,26 +10,28 @@ to learn more about them. Usage: sentry-cli[EXE] [OPTIONS] Commands: - completions Generate completions for the specified shell. - debug-files Locate, analyze or upload debug information files. [aliases: dif] - deploys Manage deployments for Sentry releases. - events Manage events on Sentry. - info Print information about the configuration and verify authentication. - issues Manage issues in Sentry. - login Authenticate with the Sentry server. - logs Manage logs in Sentry - monitors Manage cron monitors on Sentry. - organizations Manage organizations on Sentry. - projects Manage projects on Sentry. - react-native Upload build artifacts for react-native projects. - releases Manage releases on Sentry. - repos Manage repositories on Sentry. - send-event Send a manual event to Sentry. - send-envelope Send a stored envelope to Sentry. - sourcemaps Manage sourcemaps for Sentry releases. - uninstall Uninstall the sentry-cli executable. - upload-proguard Upload ProGuard mapping files to a project. - help Print this message or the help of the given subcommand(s) + completions Generate completions for the specified shell. + debug-files Locate, analyze or upload debug information files. [aliases: dif] + deploys Manage deployments for Sentry releases. + events Manage events on Sentry. + info Print information about the configuration and verify authentication. + issues Manage issues in Sentry. + login Authenticate with the Sentry server. + logs Manage logs in Sentry + monitors Manage cron monitors on Sentry. + organizations Manage organizations on Sentry. + projects Manage projects on Sentry. + react-native Upload build artifacts for react-native projects. + releases Manage releases on Sentry. + repos Manage repositories on Sentry. + send-event Send a manual event to Sentry. + send-envelope Send a stored envelope to Sentry. + sourcemaps Manage sourcemaps for Sentry releases. + uninstall Uninstall the sentry-cli executable. + upload-proguard Upload ProGuard mapping files to a project. + upload-dart-symbol-map Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating + Dart exception types. + help Print this message or the help of the given subcommand(s) Options: --url Fully qualified URL to the Sentry server. From c97c31db036aa650a6a08486244335c43ca07471 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 15:03:57 +0200 Subject: [PATCH 04/25] Update --- DART_SYMBOL_MAP_PLAN.md | 131 ------------------------- src/commands/upload_dart_symbol_map.rs | 3 +- src/utils/chunks/options.rs | 18 +--- 3 files changed, 2 insertions(+), 150 deletions(-) delete mode 100644 DART_SYMBOL_MAP_PLAN.md diff --git a/DART_SYMBOL_MAP_PLAN.md b/DART_SYMBOL_MAP_PLAN.md deleted file mode 100644 index c720f1bc7a..0000000000 --- a/DART_SYMBOL_MAP_PLAN.md +++ /dev/null @@ -1,131 +0,0 @@ -## Implementation Plan: upload-dart-symbol-map - -Goal: Add a CLI command to upload a Dart/Flutter symbol map ("dartsymbolmap") for deobfuscating Dart exception types. The upload must use the chunk-upload flow and associate the mapping to a required debug id, preferably extracted from an associated debug file. - -### High-level architecture (aligned with existing codebase) -- Reuse the existing chunked upload infrastructure in `src/utils/chunks/` and the assemble endpoint in `src/api/mod.rs::assemble_difs(...)`. -- Introduce a small wrapper object implementing `Assemblable` for the mapping file so it can be sent through `chunks::upload_chunked_objects`. -- Add a new CLI subcommand, following patterns used by `upload_proguard` and `sourcemaps upload`. -- Validate the mapping file locally before upload to fail fast. -- Ensure server capability support via `ChunkServerOptions.accept` for a new capability string "dartsymbolmap". - ---- - -### Step-by-step tasks - -1) Capability support for "dartsymbolmap" - - Add a new variant to `ChunkUploadCapability` and update deserialization: - - File: `src/api/data_types/chunking/upload/capability.rs` - - Add `DartSymbolMap` variant and map the string `"dartsymbolmap"` to it in the `Deserialize` impl. - - No change needed to `ChunkServerOptions.should_strip_debug_ids()`. - - Optional: Add a helper method (if desired) to check support: `options.supports(ChunkUploadCapability::DartSymbolMap)`. - -2) Represent the mapping to be uploaded - - Create a lightweight struct, e.g. `DartSymbolMapObject { bytes: Vec, name: String, debug_id: DebugId }`. - - Implement: - - `AsRef<[u8]>` to expose raw bytes - - `Assemblable` to provide `name()` and `debug_id()` (see `src/utils/chunks/types.rs`) - - `Display` for user-friendly printing during summaries - - We will then wrap it with `chunks::Chunked::from(object, chunk_size)` so we can reuse `chunks::upload_chunked_objects(...)` which: - - Calls `assemble_difs` to retrieve missing chunks and current states - - Uploads missing chunks via `Api::upload_chunks(...)` - - Polls assemble until completion (configurable via `ChunkOptions::with_max_wait`) - -3) Local validation of the Dart symbol map file - - Implement a small validator function used by the command before uploading: - - Read file bytes; parse JSON as `Vec` using `serde_json`. - - Ensure even length; if odd or not an array of strings, error with a clear message. - - Keep the file name as provided; if the user passes a directory name or odd extension, continue but recommend `dartsymbolmap.json` in help text. - -4) Debug ID resolution - - Inputs: a required path to the associated debug file (not the dartsymbolmap). - - Behavior: - - Open the associated debug file and extract debug ids via `utils::dif::DifFile::open_path(...).ids()`. - - If exactly one id is present, use it. - - If multiple, error and ask the user to disambiguate (e.g., select the correct variant via a future flag) — for now we will error with a clear message. - - If none, error out with a clear message. - -5) New CLI command: upload-dart-symbol-map - - File: `src/commands/upload_dart_symbol_map.rs` - - Command shape (similar to `upload_proguard.rs`): - - `.about("Upload a Dart symbol map file to a project.")` - - `.org_arg()` and `.project_arg(false)` and fetch both via `Config::current().get_org_and_project(matches)?`. - - Positional args: - - `mapping` (required): path to `dartsymbolmap.json` - - `debug_file` (required): path to the associated debug file to extract the debug id - - Flags: none (keep command simple; no wait flags) - - Execution flow: - 1. Validate mapping file (JSON array of strings, even length) - 2. Resolve `debug_id` as described in Step 4 - 3. Get `ChunkServerOptions` via `api.authenticated()?.get_chunk_upload_options(&org)?` - 4. Ensure `options.supports(ChunkUploadCapability::DartSymbolMap)`, else bail with an actionable message - 5. Build `DartSymbolMapObject` with bytes, `name` (use basename of path; recommend `dartsymbolmap.json`), and `debug_id` - 6. Compute chunking: `Chunked::from(object, options.chunk_size as usize)` - 7. Construct `ChunkOptions::new(options, org, project).with_max_wait(DEFAULT_MAX_WAIT)` to always wait/poll until completion (bounded by server `max_wait`) - 8. Call `chunks::upload_chunked_objects(&[chunked], chunk_options)` - 9. Rely on existing summary output from `chunks::upload_chunked_objects`/`poll_assemble`; optionally print a concise success line for the mapping - -6) Wire into the root command - - File: `src/commands/mod.rs` - - Add `mod upload_dart_symbol_map;` - - Add to `each_subcommand!` list so it’s registered - - Optional: consider adding to `UPDATE_NAGGER_CMDS` - -7) Tests (integration-focused) - - Directory: `tests/integration/_cases/dart_symbol_map/` - - Cases: - - `help.trycmd`: validates help text - - `validate-json-invalid.trycmd`: invalid JSON (non-array) -> error - - `validate-json-odd-length.trycmd`: odd number of elements -> error - - `missing-debug-id.trycmd`: debug file has no debug id -> error - - `multiple-debug-ids.trycmd`: ambiguous debug file -> error with guidance to disambiguate (no `--debug-id` supported) - - `happy-path.trycmd`: mock chunk-upload+assemble: - - GET chunk-upload returns accept includes `"dartsymbolmap"` and compression includes `gzip` - - First assemble returns `NOT_FOUND` with `missingChunks` - - Upload chunks - - Subsequent assemble returns `OK` (or `CREATED` → subsequent `OK`) - - Fixtures: - - `tests/integration/_fixtures/dartsymbolmap.json` - - Sample debug file already in `_fixtures/` (Mach-O, ELF, Breakpad, etc.) - - Mock responses live under `tests/integration/_responses/` - -8) Documentation and UX - - Add command description and examples to `--help` and a short note in `README.md`. - - Clear errors for capability missing, invalid mapping, and debug id issues. - -9) Non-functional notes - - Hash algorithm: server specifies `sha1`; existing utilities already use SHA1 (see `get_sha1_checksums`). - - Region correctness: the `url` from chunk-upload options is used directly by `Api::upload_chunks`. - - Compression: chosen automatically from `ChunkServerOptions.compression` (prefers `gzip` when supported). - ---- - -### Key code references -- Chunk server options and assemble: - - `src/api/mod.rs::get_chunk_upload_options`, `assemble_difs` (POST `/files/difs/assemble/`) -- Chunking utilities and traits: - - `src/utils/chunks/` (`ChunkOptions`, `Chunked`, `Assemblable`, `upload_chunked_objects`) -- Capability parsing: - - `src/api/data_types/chunking/upload/capability.rs` -- Chunk-upload network call (multipart with filename = chunk sha1): - - `src/api/mod.rs::upload_chunks` -- Debug-id extraction from debug files: - - `src/utils/dif.rs` (`DifFile::open_path`, `ids()`) - ---- - -### Assumptions & open questions -- Server side recognizes `name` and `debug_id` for `dartsymbolmap` via `assemble_difs`. No extra type discriminator required. -- Association rule: the mapping’s `debug_id` must match the associated debug file’s id; we will enforce extraction exclusively from the provided debug file path (no `--debug-id` override). -- If you want the command to accept a single path and infer the debug file in the future, we can extend UX later, but for now two paths are required. - ---- - -### Rollout checklist -1. Implement code changes above -2. `cargo build --workspace` -3. `cargo test --workspace` (run with `TRYCMD=overwrite` once to create snapshots) -4. Verify on a project with a real dartsymbolmap and debug file -5. Add brief docs snippet to README - - diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs index 33d442a480..b1854a4d74 100644 --- a/src/commands/upload_dart_symbol_map.rs +++ b/src/commands/upload_dart_symbol_map.rs @@ -144,8 +144,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { } let options = ChunkOptions::new(chunk_upload_options, &org, &project) - .with_max_wait(DEFAULT_MAX_WAIT) - .with_strip_debug_ids_override(false); + .with_max_wait(DEFAULT_MAX_WAIT); let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?; let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?; diff --git a/src/utils/chunks/options.rs b/src/utils/chunks/options.rs index 6f82050b3a..767a8bf37d 100644 --- a/src/utils/chunks/options.rs +++ b/src/utils/chunks/options.rs @@ -13,11 +13,6 @@ pub struct ChunkOptions<'a> { /// If the server_options.max_wait is set to a smaller nonzero value, /// we use that value instead. max_wait: Duration, - - /// Optional override for whether to strip debug ids. - /// When `Some(value)`, this value takes precedence over the server option. - /// When `None`, we defer to `server_options.should_strip_debug_ids()`. - strip_debug_ids_override: Option, } impl<'a> ChunkOptions<'a> { @@ -27,7 +22,6 @@ impl<'a> ChunkOptions<'a> { org, project, max_wait: Duration::ZERO, - strip_debug_ids_override: None, } } @@ -38,17 +32,7 @@ impl<'a> ChunkOptions<'a> { } pub fn should_strip_debug_ids(&self) -> bool { - match self.strip_debug_ids_override { - Some(override_value) => override_value, - None => self.server_options.should_strip_debug_ids(), - } - } - - /// Override whether to strip debug ids from uploaded files. - /// If not set, the behavior falls back to the server options. - pub fn with_strip_debug_ids_override(mut self, strip: bool) -> Self { - self.strip_debug_ids_override = Some(strip); - self + self.server_options.should_strip_debug_ids() } pub fn org(&self) -> &str { From 2bd4db257a771b5d5ebbdb23497e0ad26baf8def Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Fri, 8 Aug 2025 15:54:15 +0200 Subject: [PATCH 05/25] Update --- .../dartsymbolmap-invalid.json | 5 + .../dart_symbol_map/dartsymbolmap.json | 6 + .../get-chunk-upload-no-debug-files.json | 11 ++ .../dart_symbol_map/get-chunk-upload.json | 11 ++ tests/integration/mod.rs | 1 + tests/integration/upload_dart_symbol_map.rs | 103 ++++++++++++++++++ 6 files changed, 137 insertions(+) create mode 100644 tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json create mode 100644 tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json create mode 100644 tests/integration/_responses/dart_symbol_map/get-chunk-upload-no-debug-files.json create mode 100644 tests/integration/_responses/dart_symbol_map/get-chunk-upload.json create mode 100644 tests/integration/upload_dart_symbol_map.rs diff --git a/tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json b/tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json new file mode 100644 index 0000000000..78b2e3e87d --- /dev/null +++ b/tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json @@ -0,0 +1,5 @@ +[ + "MaterialApp", + "ex", + "Scaffold" +] diff --git a/tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json b/tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json new file mode 100644 index 0000000000..ced609f6b6 --- /dev/null +++ b/tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json @@ -0,0 +1,6 @@ +[ + "MaterialApp", + "ex", + "Scaffold", + "ey" +] diff --git a/tests/integration/_responses/dart_symbol_map/get-chunk-upload-no-debug-files.json b/tests/integration/_responses/dart_symbol_map/get-chunk-upload-no-debug-files.json new file mode 100644 index 0000000000..3ad65e968d --- /dev/null +++ b/tests/integration/_responses/dart_symbol_map/get-chunk-upload-no-debug-files.json @@ -0,0 +1,11 @@ +{ + "url": "organizations/wat-org/chunk-upload/", + "chunkSize": 8388608, + "chunksPerRequest": 64, + "maxFileSize": 2147483648, + "maxRequestSize": 33554432, + "concurrency": 8, + "hashAlgorithm": "sha1", + "compression": ["gzip"], + "accept": ["dartsymbolmap"] +} diff --git a/tests/integration/_responses/dart_symbol_map/get-chunk-upload.json b/tests/integration/_responses/dart_symbol_map/get-chunk-upload.json new file mode 100644 index 0000000000..e85f3f2fa8 --- /dev/null +++ b/tests/integration/_responses/dart_symbol_map/get-chunk-upload.json @@ -0,0 +1,11 @@ +{ + "url": "organizations/wat-org/chunk-upload/", + "chunkSize": 8388608, + "chunksPerRequest": 64, + "maxFileSize": 2147483648, + "maxRequestSize": 33554432, + "concurrency": 8, + "hashAlgorithm": "sha1", + "compression": ["gzip"], + "accept": ["debug_files", "dartsymbolmap"] +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 62b68ea816..72d710f9ae 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -27,6 +27,7 @@ mod update; mod upload_dif; mod upload_dsym; mod upload_proguard; +mod upload_dart_symbol_map; use std::fs; use std::io; diff --git a/tests/integration/upload_dart_symbol_map.rs b/tests/integration/upload_dart_symbol_map.rs new file mode 100644 index 0000000000..350792f4c7 --- /dev/null +++ b/tests/integration/upload_dart_symbol_map.rs @@ -0,0 +1,103 @@ +use std::sync::atomic::{AtomicU8, Ordering}; + +use crate::integration::{MockEndpointBuilder, TestManager}; +use crate::integration::test_utils::AssertCommand; + +#[test] +fn command_upload_dart_symbol_map_missing_capability() { + // Server does not advertise `dartsymbolmap` capability → command should bail early. + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("debug_files/get-chunk-upload.json"), + ) + .assert_cmd([ + "upload-dart-symbol-map", + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", + // Use a fixture with a single Debug ID + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Failure); +}} + +#[test] +fn command_upload_dart_symbol_map_chunk_upload_flow() { + // Happy path: server supports dartsymbolmap capability, file needs upload, then assembles to ok. + let call_count = AtomicU8::new(0); + + TestManager::new() + // Server advertises capability including `dartsymbolmap`. + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("dart_symbol_map/get-chunk-upload.json"), + ) + // Accept chunk upload requests for the missing chunks; no validation needed here. + .mock_endpoint(MockEndpointBuilder::new( + "POST", + "/api/0/organizations/wat-org/chunk-upload/", + )) + // Assemble flow: 1) not_found (missingChunks), 2) created, 3) ok + .mock_endpoint( + MockEndpointBuilder::new( + "POST", + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", + ) + .with_header_matcher("content-type", "application/json") + .with_response_fn(move |request| { + let body = request.body().expect("body should be readable"); + let body_json: serde_json::Value = serde_json::from_slice(body) + .expect("request body should be valid JSON"); + + // The request map has a single entry keyed by checksum; reuse it in responses. + let (checksum, _obj) = body_json + .as_object() + .and_then(|m| m.iter().next()) + .map(|(k, v)| (k.clone(), v.clone())) + .expect("assemble request must contain at least one object"); + + match call_count.fetch_add(1, Ordering::Relaxed) { + 0 => format!( + "{{\"{checksum}\":{{\"state\":\"not_found\",\"missingChunks\":[\"{checksum}\"]}}}}" + ) + .into(), + 1 => format!( + "{{\"{checksum}\":{{\"state\":\"created\",\"missingChunks\":[]}}}}" + ) + .into(), + 2 => format!( + "{{\"{checksum}\":{{\"state\":\"ok\",\"detail\":null,\"missingChunks\":[],\"dif\":{{\"id\":\"1\",\"uuid\":\"00000000-0000-0000-0000-000000000000\",\"debugId\":\"00000000-0000-0000-0000-000000000000\",\"objectName\":\"dartsymbolmap.json\",\"cpuName\":\"any\",\"headers\":{{\"Content-Type\":\"application/octet-stream\"}},\"size\":1,\"sha1\":\"{checksum}\",\"dateCreated\":\"1776-07-04T12:00:00.000Z\",\"data\":{{}}}}}}}}" + ) + .into(), + n => panic!( + "Only 3 calls to the assemble endpoint expected, but there were {}.", + n + 1 + ), + } + }) + .expect(3), + ) + .assert_cmd([ + "upload-dart-symbol-map", + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", + // Use a fixture with a single Debug ID (embedded PDB) + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Success); +} + +#[test] +fn command_upload_dart_symbol_map_invalid_mapping() { + // Invalid mapping (odd number of entries) should fail before any HTTP calls. + TestManager::new() + .assert_cmd([ + "upload-dart-symbol-map", + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json", + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Failure); +} + + From e85a1689180f5403d8d6c0183640355b38fc38d0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 11:47:16 +0200 Subject: [PATCH 06/25] Update command from upload-dart-symbol-map to dart-symbol-map upload --- src/commands/dart_symbol_map/mod.rs | 45 +++++++++++++++++++++ src/commands/dart_symbol_map/upload.rs | 13 ++++++ src/commands/mod.rs | 18 ++++++++- tests/integration/_cases/help/help.trycmd | 43 ++++++++++---------- tests/integration/upload_dart_symbol_map.rs | 11 +++-- 5 files changed, 103 insertions(+), 27 deletions(-) create mode 100644 src/commands/dart_symbol_map/mod.rs create mode 100644 src/commands/dart_symbol_map/upload.rs diff --git a/src/commands/dart_symbol_map/mod.rs b/src/commands/dart_symbol_map/mod.rs new file mode 100644 index 0000000000..fe4c3bf42f --- /dev/null +++ b/src/commands/dart_symbol_map/mod.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use clap::{ArgMatches, Command}; + +// Keep consistent import patterns with other grouped commands; currently no ArgExt usage here + +pub mod upload; + +macro_rules! each_subcommand { + ($mac:ident) => { + $mac!(upload); + }; +} + +pub fn make_command(mut command: Command) -> Command { + macro_rules! add_subcommand { + ($name:ident) => {{ + command = command.subcommand(crate::commands::dart_symbol_map::$name::make_command( + Command::new(stringify!($name).replace('_', "-")), + )); + }}; + } + + command = command + .about("Manage Dart/Flutter symbol maps for Sentry.") + .subcommand_required(true) + .arg_required_else_help(true); + each_subcommand!(add_subcommand); + command +} + +pub fn execute(matches: &ArgMatches) -> Result<()> { + macro_rules! execute_subcommand { + ($name:ident) => {{ + if let Some(sub_matches) = + matches.subcommand_matches(&stringify!($name).replace('_', "-")) + { + return crate::commands::dart_symbol_map::$name::execute(&sub_matches); + } + }}; + } + each_subcommand!(execute_subcommand); + unreachable!(); +} + + diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs new file mode 100644 index 0000000000..92b189aeed --- /dev/null +++ b/src/commands/dart_symbol_map/upload.rs @@ -0,0 +1,13 @@ +use anyhow::Result; +use clap::{ArgMatches, Command}; + +// Reuse the existing implementation +pub fn make_command(command: Command) -> Command { + crate::commands::upload_dart_symbol_map::make_command(command) +} + +pub fn execute(matches: &ArgMatches) -> Result<()> { + crate::commands::upload_dart_symbol_map::execute(matches) +} + + diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0b9b40bf0d..ab674fa031 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -18,6 +18,7 @@ use crate::utils::logging::Logger; use crate::utils::system::{load_dotenv, print_error, set_panic_hook, QuietExit}; use crate::utils::update::run_sentrycli_update_nagger; use crate::utils::value_parsers::auth_token_parser; +use crate::utils::args::ArgExt as _; mod bash_hook; mod debug_files; @@ -40,6 +41,7 @@ mod send_envelope; mod send_event; mod send_metric; mod sourcemaps; +mod dart_symbol_map; #[cfg(not(feature = "managed"))] mod uninstall; #[cfg(not(feature = "managed"))] @@ -72,6 +74,7 @@ macro_rules! each_subcommand { $mac!(send_envelope); $mac!(send_metric); $mac!(sourcemaps); + $mac!(dart_symbol_map); #[cfg(not(feature = "managed"))] $mac!(uninstall); #[cfg(not(feature = "managed"))] @@ -79,7 +82,6 @@ macro_rules! each_subcommand { $mac!(upload_dif); $mac!(upload_dsym); $mac!(upload_proguard); - $mac!(upload_dart_symbol_map); }; } @@ -237,6 +239,16 @@ fn add_commands(mut app: Command) -> Command { } each_subcommand!(add_subcommand); + // Backward compatibility: keep the old flat command as a hidden alias that delegates to + // the new group subcommand. + // Maintain the old flat command as a hidden alias that delegates to the new implementation + app = app.subcommand(Command::new("upload-dart-symbol-map").hide(true) + .about("Deprecated: use 'dart-symbol-map upload' instead") + .arg(Arg::new("mapping").value_name("MAPPING").required(true)) + .arg(Arg::new("debug_file").value_name("DEBUG_FILE").required(true)) + .org_arg() + .project_arg(false) + ); app } @@ -255,6 +267,10 @@ fn run_command(matches: &ArgMatches) -> Result<()> { } each_subcommand!(execute_subcommand); + // Execute compatibility alias if used + if let Some(sub_matches) = matches.subcommand_matches("upload-dart-symbol-map") { + return crate::commands::upload_dart_symbol_map::execute(&sub_matches); + } unreachable!(); } diff --git a/tests/integration/_cases/help/help.trycmd b/tests/integration/_cases/help/help.trycmd index d6e3aee13d..a04f2021b9 100644 --- a/tests/integration/_cases/help/help.trycmd +++ b/tests/integration/_cases/help/help.trycmd @@ -10,28 +10,27 @@ to learn more about them. Usage: sentry-cli[EXE] [OPTIONS] Commands: - completions Generate completions for the specified shell. - debug-files Locate, analyze or upload debug information files. [aliases: dif] - deploys Manage deployments for Sentry releases. - events Manage events on Sentry. - info Print information about the configuration and verify authentication. - issues Manage issues in Sentry. - login Authenticate with the Sentry server. - logs Manage logs in Sentry - monitors Manage cron monitors on Sentry. - organizations Manage organizations on Sentry. - projects Manage projects on Sentry. - react-native Upload build artifacts for react-native projects. - releases Manage releases on Sentry. - repos Manage repositories on Sentry. - send-event Send a manual event to Sentry. - send-envelope Send a stored envelope to Sentry. - sourcemaps Manage sourcemaps for Sentry releases. - uninstall Uninstall the sentry-cli executable. - upload-proguard Upload ProGuard mapping files to a project. - upload-dart-symbol-map Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating - Dart exception types. - help Print this message or the help of the given subcommand(s) + completions Generate completions for the specified shell. + debug-files Locate, analyze or upload debug information files. [aliases: dif] + deploys Manage deployments for Sentry releases. + events Manage events on Sentry. + info Print information about the configuration and verify authentication. + issues Manage issues in Sentry. + login Authenticate with the Sentry server. + logs Manage logs in Sentry + monitors Manage cron monitors on Sentry. + organizations Manage organizations on Sentry. + projects Manage projects on Sentry. + react-native Upload build artifacts for react-native projects. + releases Manage releases on Sentry. + repos Manage repositories on Sentry. + send-event Send a manual event to Sentry. + send-envelope Send a stored envelope to Sentry. + sourcemaps Manage sourcemaps for Sentry releases. + dart-symbol-map Manage Dart/Flutter symbol maps for Sentry. + uninstall Uninstall the sentry-cli executable. + upload-proguard Upload ProGuard mapping files to a project. + help Print this message or the help of the given subcommand(s) Options: --url Fully qualified URL to the Sentry server. diff --git a/tests/integration/upload_dart_symbol_map.rs b/tests/integration/upload_dart_symbol_map.rs index 350792f4c7..70d3997d26 100644 --- a/tests/integration/upload_dart_symbol_map.rs +++ b/tests/integration/upload_dart_symbol_map.rs @@ -12,14 +12,15 @@ fn command_upload_dart_symbol_map_missing_capability() { .with_response_file("debug_files/get-chunk-upload.json"), ) .assert_cmd([ - "upload-dart-symbol-map", + "dart-symbol-map", + "upload", "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", // Use a fixture with a single Debug ID "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", ]) .with_default_token() .run_and_assert(AssertCommand::Failure); -}} +} #[test] fn command_upload_dart_symbol_map_chunk_upload_flow() { @@ -78,7 +79,8 @@ fn command_upload_dart_symbol_map_chunk_upload_flow() { .expect(3), ) .assert_cmd([ - "upload-dart-symbol-map", + "dart-symbol-map", + "upload", "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", // Use a fixture with a single Debug ID (embedded PDB) "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", @@ -92,7 +94,8 @@ fn command_upload_dart_symbol_map_invalid_mapping() { // Invalid mapping (odd number of entries) should fail before any HTTP calls. TestManager::new() .assert_cmd([ - "upload-dart-symbol-map", + "dart-symbol-map", + "upload", "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json", "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", ]) From 4aa94924b61dd0da7a0e1b800d0b984f2967a15a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 12:02:04 +0200 Subject: [PATCH 07/25] Rust fmt --- src/commands/dart_symbol_map/mod.rs | 2 -- src/commands/dart_symbol_map/upload.rs | 2 -- src/commands/mod.rs | 24 +++++++++++++-------- src/commands/upload_dart_symbol_map.rs | 6 ++---- tests/integration/mod.rs | 2 +- tests/integration/upload_dart_symbol_map.rs | 4 +--- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/commands/dart_symbol_map/mod.rs b/src/commands/dart_symbol_map/mod.rs index fe4c3bf42f..c02b860d60 100644 --- a/src/commands/dart_symbol_map/mod.rs +++ b/src/commands/dart_symbol_map/mod.rs @@ -41,5 +41,3 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { each_subcommand!(execute_subcommand); unreachable!(); } - - diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 92b189aeed..d981762388 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -9,5 +9,3 @@ pub fn make_command(command: Command) -> Command { pub fn execute(matches: &ArgMatches) -> Result<()> { crate::commands::upload_dart_symbol_map::execute(matches) } - - diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ab674fa031..5459933eed 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -12,15 +12,16 @@ use std::{env, iter}; use crate::api::Api; use crate::config::{Auth, Config}; use crate::constants::{ARCH, PLATFORM, VERSION}; +use crate::utils::args::ArgExt as _; use crate::utils::auth_token::{redact_token_from_string, AuthToken}; use crate::utils::logging::set_quiet_mode; use crate::utils::logging::Logger; use crate::utils::system::{load_dotenv, print_error, set_panic_hook, QuietExit}; use crate::utils::update::run_sentrycli_update_nagger; use crate::utils::value_parsers::auth_token_parser; -use crate::utils::args::ArgExt as _; mod bash_hook; +mod dart_symbol_map; mod debug_files; mod deploys; mod derive_parser; @@ -41,15 +42,14 @@ mod send_envelope; mod send_event; mod send_metric; mod sourcemaps; -mod dart_symbol_map; #[cfg(not(feature = "managed"))] mod uninstall; #[cfg(not(feature = "managed"))] mod update; +mod upload_dart_symbol_map; mod upload_dif; mod upload_dsym; mod upload_proguard; -mod upload_dart_symbol_map; macro_rules! each_subcommand { ($mac:ident) => { @@ -242,12 +242,18 @@ fn add_commands(mut app: Command) -> Command { // Backward compatibility: keep the old flat command as a hidden alias that delegates to // the new group subcommand. // Maintain the old flat command as a hidden alias that delegates to the new implementation - app = app.subcommand(Command::new("upload-dart-symbol-map").hide(true) - .about("Deprecated: use 'dart-symbol-map upload' instead") - .arg(Arg::new("mapping").value_name("MAPPING").required(true)) - .arg(Arg::new("debug_file").value_name("DEBUG_FILE").required(true)) - .org_arg() - .project_arg(false) + app = app.subcommand( + Command::new("upload-dart-symbol-map") + .hide(true) + .about("Deprecated: use 'dart-symbol-map upload' instead") + .arg(Arg::new("mapping").value_name("MAPPING").required(true)) + .arg( + Arg::new("debug_file") + .value_name("DEBUG_FILE") + .required(true), + ) + .org_arg() + .project_arg(false), ); app } diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs index b1854a4d74..15ff740d72 100644 --- a/src/commands/upload_dart_symbol_map.rs +++ b/src/commands/upload_dart_symbol_map.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::ffi::OsStr; -use std::path::Path; use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::path::Path; use anyhow::{bail, Context as _, Result}; use clap::{Arg, ArgMatches, Command}; @@ -9,8 +9,8 @@ use clap::{Arg, ArgMatches, Command}; use crate::api::{Api, ChunkUploadCapability}; use crate::config::Config; use crate::constants::{DEFAULT_MAX_DIF_SIZE, DEFAULT_MAX_WAIT}; -use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked}; use crate::utils::args::ArgExt as _; +use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked}; use crate::utils::dif::DifFile; use symbolic::common::DebugId; @@ -161,5 +161,3 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { ), } } - - diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 72d710f9ae..124a288743 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -24,10 +24,10 @@ mod test_utils; mod token_validation; mod uninstall; mod update; +mod upload_dart_symbol_map; mod upload_dif; mod upload_dsym; mod upload_proguard; -mod upload_dart_symbol_map; use std::fs; use std::io; diff --git a/tests/integration/upload_dart_symbol_map.rs b/tests/integration/upload_dart_symbol_map.rs index 70d3997d26..784ceeeda5 100644 --- a/tests/integration/upload_dart_symbol_map.rs +++ b/tests/integration/upload_dart_symbol_map.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicU8, Ordering}; -use crate::integration::{MockEndpointBuilder, TestManager}; use crate::integration::test_utils::AssertCommand; +use crate::integration::{MockEndpointBuilder, TestManager}; #[test] fn command_upload_dart_symbol_map_missing_capability() { @@ -102,5 +102,3 @@ fn command_upload_dart_symbol_map_invalid_mapping() { .with_default_token() .run_and_assert(AssertCommand::Failure); } - - From 169f6695d0030023bf0f7a212146f0cce56d9fdb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 13:11:08 +0200 Subject: [PATCH 08/25] Clippy --- src/commands/mod.rs | 2 +- src/commands/upload_dart_symbol_map.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 5459933eed..ff048b546f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -275,7 +275,7 @@ fn run_command(matches: &ArgMatches) -> Result<()> { each_subcommand!(execute_subcommand); // Execute compatibility alias if used if let Some(sub_matches) = matches.subcommand_matches("upload-dart-symbol-map") { - return crate::commands::upload_dart_symbol_map::execute(&sub_matches); + return crate::commands::upload_dart_symbol_map::execute(sub_matches); } unreachable!(); } diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs index 15ff740d72..ee871063bf 100644 --- a/src/commands/upload_dart_symbol_map.rs +++ b/src/commands/upload_dart_symbol_map.rs @@ -66,8 +66,12 @@ pub fn make_command(command: Command) -> Command { pub fn execute(matches: &ArgMatches) -> Result<()> { // Parse required positional arguments - let mapping_path = matches.get_one::("mapping").unwrap(); - let debug_file_path = matches.get_one::("debug_file").unwrap(); + let mapping_path = matches + .get_one::("mapping") + .expect("required argument 'mapping' not provided by clap"); + let debug_file_path = matches + .get_one::("debug_file") + .expect("required argument 'debug_file' not provided by clap"); // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; @@ -86,7 +90,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { // Validate the dartsymbolmap JSON: must be a JSON array of strings with even length let mapping_file_bytes = std::fs::read(mapping_path) - .with_context(|| format!("Failed to read mapping file at {}", mapping_path))?; + .with_context(|| format!("Failed to read mapping file at {mapping_path}"))?; let mapping_entries: Vec = serde_json::from_slice(&mapping_file_bytes) .context("Invalid dartsymbolmap: expected a JSON array of strings")?; @@ -102,7 +106,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> { .file_name() .and_then(OsStr::to_str) .unwrap_or(mapping_path) - .to_string(); + .to_owned(); let mapping_len = mapping_file_bytes.len(); let object = DartSymbolMapObject { From 6354b48a62097865ee607ed424d964e0fd312f21 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Mon, 11 Aug 2025 14:01:29 +0200 Subject: [PATCH 09/25] Fix bugbot comment --- src/commands/upload_dart_symbol_map.rs | 2 +- tests/integration/_cases/help/help-windows.trycmd | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs index ee871063bf..74d39b0922 100644 --- a/src/commands/upload_dart_symbol_map.rs +++ b/src/commands/upload_dart_symbol_map.rs @@ -46,7 +46,7 @@ pub fn make_command(command: Command) -> Command { command .about("Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.") .after_help( - "Examples:\n sentry-cli upload-dart-symbol-map --org my-org --project my-proj path/to/dartsymbolmap.json path/to/debug/file\n\n The mapping must be a JSON array of strings with an even number of entries (pairs).\n The debug file must contain exactly one Debug ID.", + "Examples:\n sentry-cli dart-symbol-map upload --org my-org --project my-proj path/to/dartsymbolmap.json path/to/debug/file\n\n The mapping must be a JSON array of strings with an even number of entries (pairs).\n The debug file must contain exactly one Debug ID.", ) .org_arg() .project_arg(false) diff --git a/tests/integration/_cases/help/help-windows.trycmd b/tests/integration/_cases/help/help-windows.trycmd index bbdc458575..fdbe15c6dd 100644 --- a/tests/integration/_cases/help/help-windows.trycmd +++ b/tests/integration/_cases/help/help-windows.trycmd @@ -27,6 +27,7 @@ Commands: send-event Send a manual event to Sentry. send-envelope Send a stored envelope to Sentry. sourcemaps Manage sourcemaps for Sentry releases. + dart-symbol-map Manage Dart/Flutter symbol maps for Sentry. upload-proguard Upload ProGuard mapping files to a project. help Print this message or the help of the given subcommand(s) From 93c7abb1fa8a82a0a2af449cfd09d5566360c388 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:43:15 +0200 Subject: [PATCH 10/25] ref(dart-symbol-map): Migrate upload command to Clap Derive API --- src/commands/dart_symbol_map/mod.rs | 71 +++++----- src/commands/dart_symbol_map/upload.rs | 171 ++++++++++++++++++++++++- src/commands/derive_parser.rs | 2 + src/commands/mod.rs | 5 +- src/commands/upload_dart_symbol_map.rs | 167 ------------------------ 5 files changed, 208 insertions(+), 208 deletions(-) delete mode 100644 src/commands/upload_dart_symbol_map.rs diff --git a/src/commands/dart_symbol_map/mod.rs b/src/commands/dart_symbol_map/mod.rs index c02b860d60..f7536c27db 100644 --- a/src/commands/dart_symbol_map/mod.rs +++ b/src/commands/dart_symbol_map/mod.rs @@ -1,43 +1,50 @@ use anyhow::Result; -use clap::{ArgMatches, Command}; - -// Keep consistent import patterns with other grouped commands; currently no ArgExt usage here +use clap::{Args, ArgMatches, Command, Parser as _, Subcommand}; +use crate::utils::args::ArgExt as _; pub mod upload; -macro_rules! each_subcommand { - ($mac:ident) => { - $mac!(upload); - }; +const GROUP_ABOUT: &str = "Manage Dart/Flutter symbol maps for Sentry."; +const UPLOAD_ABOUT: &str = + "Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types."; +const UPLOAD_LONG_ABOUT: &str = + "Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.{n}{n}Examples:{n} sentry-cli dart-symbol-map upload --org my-org --project my-proj path/to/dartsymbolmap.json path/to/debug/file{n}{n}The mapping must be a JSON array of strings with an even number of entries (pairs).{n}The debug file must contain exactly one Debug ID."; + +#[derive(Args)] +pub(super) struct DartSymbolMapArgs { + #[command(subcommand)] + pub(super) subcommand: DartSymbolMapSubcommand, } -pub fn make_command(mut command: Command) -> Command { - macro_rules! add_subcommand { - ($name:ident) => {{ - command = command.subcommand(crate::commands::dart_symbol_map::$name::make_command( - Command::new(stringify!($name).replace('_', "-")), - )); - }}; - } +#[derive(Subcommand)] +#[command(about = GROUP_ABOUT)] +pub(super) enum DartSymbolMapSubcommand { + #[command(about = UPLOAD_ABOUT)] + #[command(long_about = UPLOAD_LONG_ABOUT)] + Upload(upload::DartSymbolMapUploadArgs), +} - command = command - .about("Manage Dart/Flutter symbol maps for Sentry.") - .subcommand_required(true) - .arg_required_else_help(true); - each_subcommand!(add_subcommand); - command +pub(super) fn make_command(command: Command) -> Command { + DartSymbolMapSubcommand::augment_subcommands( + command + .about(GROUP_ABOUT) + .subcommand_required(true) + .arg_required_else_help(true) + .org_arg() + .project_arg(false), + ) } -pub fn execute(matches: &ArgMatches) -> Result<()> { - macro_rules! execute_subcommand { - ($name:ident) => {{ - if let Some(sub_matches) = - matches.subcommand_matches(&stringify!($name).replace('_', "-")) - { - return crate::commands::dart_symbol_map::$name::execute(&sub_matches); - } - }}; +pub(super) fn execute(matches: &ArgMatches) -> Result<()> { + // Re-parse with the derive-based parser, mirroring the send-metric pattern. + let subcommand = match crate::commands::derive_parser::SentryCLI::parse().command { + crate::commands::derive_parser::SentryCLICommand::DartSymbolMap(DartSymbolMapArgs { + subcommand, + }) => subcommand, + _ => unreachable!("expected dart-symbol-map subcommand"), + }; + + match subcommand { + DartSymbolMapSubcommand::Upload(args) => upload::execute(args, matches), } - each_subcommand!(execute_subcommand); - unreachable!(); } diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index d981762388..9da6b90273 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -1,11 +1,168 @@ -use anyhow::Result; -use clap::{ArgMatches, Command}; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::path::Path; -// Reuse the existing implementation -pub fn make_command(command: Command) -> Command { - crate::commands::upload_dart_symbol_map::make_command(command) +use anyhow::{bail, Context as _, Result}; +use clap::{Args, ArgMatches}; + +use crate::api::{Api, ChunkUploadCapability}; +use crate::config::Config; +use crate::constants::{DEFAULT_MAX_DIF_SIZE, DEFAULT_MAX_WAIT}; +use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked}; +use crate::utils::dif::DifFile; +use symbolic::common::ByteView; +use symbolic::common::DebugId; + +struct DartSymbolMapObject<'a> { + bytes: &'a [u8], + name: &'a str, + debug_id: DebugId, +} + +impl<'a> AsRef<[u8]> for DartSymbolMapObject<'a> { + fn as_ref(&self) -> &[u8] { + self.bytes + } +} + +impl<'a> Display for DartSymbolMapObject<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + write!(f, "dartsymbolmap {}", self.name) + } +} + +impl<'a> Assemblable for DartSymbolMapObject<'a> { + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed(self.name) + } + + fn debug_id(&self) -> Option { + Some(self.debug_id) + } +} + +#[derive(Args, Clone)] +pub(crate) struct DartSymbolMapUploadArgs { + #[arg(value_name = "MAPPING")] + #[arg(help = "Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs).")] + pub(super) mapping: String, + + #[arg(value_name = "DEBUG_FILE")] + #[arg(help = "Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID.")] + pub(super) debug_file: String, +} + +pub(super) fn execute(args: DartSymbolMapUploadArgs, matches: &ArgMatches) -> Result<()> { + let mapping_path = &args.mapping; + let debug_file_path = &args.debug_file; + + // Extract Debug ID(s) from the provided debug file + let dif = DifFile::open_path(debug_file_path, None)?; + let mut ids: Vec<_> = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); + + // Ensure a single, unambiguous Debug ID + ids.sort(); + ids.dedup(); + match ids.len() { + 0 => bail!( + "No debug identifier found in the provided debug file ({}). Ensure the file contains an embedded Debug ID.", + debug_file_path + ), + 1 => { + let debug_id = ids.remove(0); + + // Validate the dartsymbolmap JSON: must be a JSON array of strings with even length + let mapping_file_bytes = ByteView::open(mapping_path) + .with_context(|| format!("Failed to read mapping file at {mapping_path}"))?; + let mapping_entries: Vec<&str> = serde_json::from_slice(&mapping_file_bytes) + .context("Invalid dartsymbolmap: expected a JSON array of strings")?; + + if mapping_entries.len() % 2 != 0 { + bail!( + "Invalid dartsymbolmap: expected an even number of entries (pairs), got {}", + mapping_entries.len() + ); + } + + // Prepare upload object + let file_name = Path::new(mapping_path) + .file_name() + .and_then(OsStr::to_str) + .unwrap_or(mapping_path) + ; + + let mapping_len = mapping_file_bytes.len(); + let object = DartSymbolMapObject { + bytes: mapping_file_bytes.as_ref(), + name: file_name, + debug_id, + }; + + // Prepare chunked upload + let api = Api::current(); + let (org, project) = Config::current().get_org_and_project(matches)?; + let chunk_upload_options = api + .authenticated()? + .get_chunk_upload_options(&org)? + .ok_or_else(|| anyhow::anyhow!( + "server does not support chunked uploading. Please update your Sentry server." + ))?; + + if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) { + bail!( + "Server does not support uploading Dart symbol maps via chunked upload. Please update your Sentry server." + ); + } + + // Early file size check against server or default limits (same as debug files) + let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { + chunk_upload_options.max_file_size + } else { + DEFAULT_MAX_DIF_SIZE + }; + + if (mapping_len as u64) > effective_max_file_size { + bail!( + "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", + mapping_path, + mapping_len, + effective_max_file_size + ); + } + + let options = ChunkOptions::new(chunk_upload_options, &org, &project) + .with_max_wait(DEFAULT_MAX_WAIT); + + let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?; + let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?; + if has_processing_errors { + bail!("Some symbol maps did not process correctly"); + } + + Ok(()) + } + _ => bail!( + "Multiple debug identifiers found in the provided debug file ({}): {}. Please provide a file that contains a single Debug ID.", + debug_file_path, + ids.into_iter().map(|id| id.to_string()).collect::>().join(", ") + ), + } } -pub fn execute(matches: &ArgMatches) -> Result<()> { - crate::commands::upload_dart_symbol_map::execute(matches) +/// Compatibility adapter for the legacy flat command `upload-dart-symbol-map`. +pub(crate) fn execute_alias(matches: &ArgMatches) -> Result<()> { + let mapping = matches + .get_one::("mapping") + .expect("required argument 'mapping' not provided by clap") + .to_owned(); + let debug_file = matches + .get_one::("debug_file") + .expect("required argument 'debug_file' not provided by clap") + .to_owned(); + + execute( + DartSymbolMapUploadArgs { mapping, debug_file }, + matches, + ) } diff --git a/src/commands/derive_parser.rs b/src/commands/derive_parser.rs index 3d81b94733..6fb0ef3883 100644 --- a/src/commands/derive_parser.rs +++ b/src/commands/derive_parser.rs @@ -4,6 +4,7 @@ use clap::{command, ArgAction::SetTrue, Parser, Subcommand}; use super::logs::LogsArgs; use super::send_metric::SendMetricArgs; +use super::dart_symbol_map::DartSymbolMapArgs; #[derive(Parser)] pub(super) struct SentryCLI { @@ -35,4 +36,5 @@ pub(super) struct SentryCLI { pub(super) enum SentryCLICommand { Logs(LogsArgs), SendMetric(SendMetricArgs), + DartSymbolMap(DartSymbolMapArgs), } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ff048b546f..104b3514f7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -46,7 +46,7 @@ mod sourcemaps; mod uninstall; #[cfg(not(feature = "managed"))] mod update; -mod upload_dart_symbol_map; +// removed: upload_dart_symbol_map (replaced by derive-based dart-symbol-map group) mod upload_dif; mod upload_dsym; mod upload_proguard; @@ -275,7 +275,8 @@ fn run_command(matches: &ArgMatches) -> Result<()> { each_subcommand!(execute_subcommand); // Execute compatibility alias if used if let Some(sub_matches) = matches.subcommand_matches("upload-dart-symbol-map") { - return crate::commands::upload_dart_symbol_map::execute(sub_matches); + // Delegate to the new derive-based implementation adapter + return crate::commands::dart_symbol_map::upload::execute_alias(sub_matches); } unreachable!(); } diff --git a/src/commands/upload_dart_symbol_map.rs b/src/commands/upload_dart_symbol_map.rs deleted file mode 100644 index 74d39b0922..0000000000 --- a/src/commands/upload_dart_symbol_map.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::borrow::Cow; -use std::ffi::OsStr; -use std::fmt::{Display, Formatter, Result as FmtResult}; -use std::path::Path; - -use anyhow::{bail, Context as _, Result}; -use clap::{Arg, ArgMatches, Command}; - -use crate::api::{Api, ChunkUploadCapability}; -use crate::config::Config; -use crate::constants::{DEFAULT_MAX_DIF_SIZE, DEFAULT_MAX_WAIT}; -use crate::utils::args::ArgExt as _; -use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked}; -use crate::utils::dif::DifFile; -use symbolic::common::DebugId; - -struct DartSymbolMapObject { - bytes: Vec, - name: String, - debug_id: DebugId, -} - -impl AsRef<[u8]> for DartSymbolMapObject { - fn as_ref(&self) -> &[u8] { - &self.bytes - } -} - -impl Display for DartSymbolMapObject { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "dartsymbolmap {}", self.name) - } -} - -impl Assemblable for DartSymbolMapObject { - fn name(&self) -> Cow<'_, str> { - Cow::from(self.name.as_str()) - } - - fn debug_id(&self) -> Option { - Some(self.debug_id) - } -} - -pub fn make_command(command: Command) -> Command { - command - .about("Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.") - .after_help( - "Examples:\n sentry-cli dart-symbol-map upload --org my-org --project my-proj path/to/dartsymbolmap.json path/to/debug/file\n\n The mapping must be a JSON array of strings with an even number of entries (pairs).\n The debug file must contain exactly one Debug ID.", - ) - .org_arg() - .project_arg(false) - .arg( - Arg::new("mapping") - .value_name("MAPPING") - .help("Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs).") - .required(true), - ) - .arg( - Arg::new("debug_file") - .value_name("DEBUG_FILE") - .help("Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID.") - .required(true), - ) -} - -pub fn execute(matches: &ArgMatches) -> Result<()> { - // Parse required positional arguments - let mapping_path = matches - .get_one::("mapping") - .expect("required argument 'mapping' not provided by clap"); - let debug_file_path = matches - .get_one::("debug_file") - .expect("required argument 'debug_file' not provided by clap"); - - // Extract Debug ID(s) from the provided debug file - let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids: Vec<_> = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); - - // Ensure a single, unambiguous Debug ID - ids.sort(); - ids.dedup(); - match ids.len() { - 0 => bail!( - "No debug identifier found in the provided debug file ({}). Ensure the file contains an embedded Debug ID.", - debug_file_path - ), - 1 => { - let debug_id = ids.remove(0); - - // Validate the dartsymbolmap JSON: must be a JSON array of strings with even length - let mapping_file_bytes = std::fs::read(mapping_path) - .with_context(|| format!("Failed to read mapping file at {mapping_path}"))?; - let mapping_entries: Vec = serde_json::from_slice(&mapping_file_bytes) - .context("Invalid dartsymbolmap: expected a JSON array of strings")?; - - if mapping_entries.len() % 2 != 0 { - bail!( - "Invalid dartsymbolmap: expected an even number of entries (pairs), got {}", - mapping_entries.len() - ); - } - - // Prepare upload object - let file_name = Path::new(mapping_path) - .file_name() - .and_then(OsStr::to_str) - .unwrap_or(mapping_path) - .to_owned(); - - let mapping_len = mapping_file_bytes.len(); - let object = DartSymbolMapObject { - bytes: mapping_file_bytes, - name: file_name.clone(), - debug_id, - }; - - // Prepare chunked upload - let api = Api::current(); - let (org, project) = Config::current().get_org_and_project(matches)?; - let chunk_upload_options = api - .authenticated()? - .get_chunk_upload_options(&org)? - .ok_or_else(|| anyhow::anyhow!( - "server does not support chunked uploading. Please update your Sentry server." - ))?; - - if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) { - bail!( - "Server does not support uploading Dart symbol maps via chunked upload. Please update your Sentry server." - ); - } - - // Early file size check against server or default limits (same as debug files) - let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { - chunk_upload_options.max_file_size - } else { - DEFAULT_MAX_DIF_SIZE - }; - - if (mapping_len as u64) > effective_max_file_size { - bail!( - "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", - mapping_path, - mapping_len, - effective_max_file_size - ); - } - - let options = ChunkOptions::new(chunk_upload_options, &org, &project) - .with_max_wait(DEFAULT_MAX_WAIT); - - let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?; - let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?; - if has_processing_errors { - bail!("Some symbol maps did not process correctly"); - } - - Ok(()) - } - _ => bail!( - "Multiple debug identifiers found in the provided debug file ({}): {}. Please provide a file that contains a single Debug ID.", - debug_file_path, - ids.into_iter().map(|id| id.to_string()).collect::>().join(", ") - ), - } -} From b4eaec43f12a7046fe2cf9588bd435beac263e0a Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:50:49 +0200 Subject: [PATCH 11/25] ref(dart-symbol-map): Remove legacy upload-dart-symbol-map alias and handler --- src/commands/dart_symbol_map/upload.rs | 17 +---------------- src/commands/mod.rs | 21 --------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 9da6b90273..a5a434c3f4 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -150,19 +150,4 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs, matches: &ArgMatches) -> Re } } -/// Compatibility adapter for the legacy flat command `upload-dart-symbol-map`. -pub(crate) fn execute_alias(matches: &ArgMatches) -> Result<()> { - let mapping = matches - .get_one::("mapping") - .expect("required argument 'mapping' not provided by clap") - .to_owned(); - let debug_file = matches - .get_one::("debug_file") - .expect("required argument 'debug_file' not provided by clap") - .to_owned(); - - execute( - DartSymbolMapUploadArgs { mapping, debug_file }, - matches, - ) -} +// legacy alias removed diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 104b3514f7..4e87cd544f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -239,22 +239,6 @@ fn add_commands(mut app: Command) -> Command { } each_subcommand!(add_subcommand); - // Backward compatibility: keep the old flat command as a hidden alias that delegates to - // the new group subcommand. - // Maintain the old flat command as a hidden alias that delegates to the new implementation - app = app.subcommand( - Command::new("upload-dart-symbol-map") - .hide(true) - .about("Deprecated: use 'dart-symbol-map upload' instead") - .arg(Arg::new("mapping").value_name("MAPPING").required(true)) - .arg( - Arg::new("debug_file") - .value_name("DEBUG_FILE") - .required(true), - ) - .org_arg() - .project_arg(false), - ); app } @@ -273,11 +257,6 @@ fn run_command(matches: &ArgMatches) -> Result<()> { } each_subcommand!(execute_subcommand); - // Execute compatibility alias if used - if let Some(sub_matches) = matches.subcommand_matches("upload-dart-symbol-map") { - // Delegate to the new derive-based implementation adapter - return crate::commands::dart_symbol_map::upload::execute_alias(sub_matches); - } unreachable!(); } From f39731c962e13b5fb314546a091ba0fe9bb56e6d Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:54:33 +0200 Subject: [PATCH 12/25] Update impl --- src/commands/dart_symbol_map/mod.rs | 1 - src/commands/dart_symbol_map/upload.rs | 2 - tests/integration/upload_dart_symbol_map.rs | 104 -------------------- 3 files changed, 107 deletions(-) delete mode 100644 tests/integration/upload_dart_symbol_map.rs diff --git a/src/commands/dart_symbol_map/mod.rs b/src/commands/dart_symbol_map/mod.rs index f7536c27db..833335e53b 100644 --- a/src/commands/dart_symbol_map/mod.rs +++ b/src/commands/dart_symbol_map/mod.rs @@ -36,7 +36,6 @@ pub(super) fn make_command(command: Command) -> Command { } pub(super) fn execute(matches: &ArgMatches) -> Result<()> { - // Re-parse with the derive-based parser, mirroring the send-metric pattern. let subcommand = match crate::commands::derive_parser::SentryCLI::parse().command { crate::commands::derive_parser::SentryCLICommand::DartSymbolMap(DartSymbolMapArgs { subcommand, diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index a5a434c3f4..7d473db2bd 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -149,5 +149,3 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs, matches: &ArgMatches) -> Re ), } } - -// legacy alias removed diff --git a/tests/integration/upload_dart_symbol_map.rs b/tests/integration/upload_dart_symbol_map.rs deleted file mode 100644 index 784ceeeda5..0000000000 --- a/tests/integration/upload_dart_symbol_map.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::sync::atomic::{AtomicU8, Ordering}; - -use crate::integration::test_utils::AssertCommand; -use crate::integration::{MockEndpointBuilder, TestManager}; - -#[test] -fn command_upload_dart_symbol_map_missing_capability() { - // Server does not advertise `dartsymbolmap` capability → command should bail early. - TestManager::new() - .mock_endpoint( - MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") - .with_response_file("debug_files/get-chunk-upload.json"), - ) - .assert_cmd([ - "dart-symbol-map", - "upload", - "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", - // Use a fixture with a single Debug ID - "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", - ]) - .with_default_token() - .run_and_assert(AssertCommand::Failure); -} - -#[test] -fn command_upload_dart_symbol_map_chunk_upload_flow() { - // Happy path: server supports dartsymbolmap capability, file needs upload, then assembles to ok. - let call_count = AtomicU8::new(0); - - TestManager::new() - // Server advertises capability including `dartsymbolmap`. - .mock_endpoint( - MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") - .with_response_file("dart_symbol_map/get-chunk-upload.json"), - ) - // Accept chunk upload requests for the missing chunks; no validation needed here. - .mock_endpoint(MockEndpointBuilder::new( - "POST", - "/api/0/organizations/wat-org/chunk-upload/", - )) - // Assemble flow: 1) not_found (missingChunks), 2) created, 3) ok - .mock_endpoint( - MockEndpointBuilder::new( - "POST", - "/api/0/projects/wat-org/wat-project/files/difs/assemble/", - ) - .with_header_matcher("content-type", "application/json") - .with_response_fn(move |request| { - let body = request.body().expect("body should be readable"); - let body_json: serde_json::Value = serde_json::from_slice(body) - .expect("request body should be valid JSON"); - - // The request map has a single entry keyed by checksum; reuse it in responses. - let (checksum, _obj) = body_json - .as_object() - .and_then(|m| m.iter().next()) - .map(|(k, v)| (k.clone(), v.clone())) - .expect("assemble request must contain at least one object"); - - match call_count.fetch_add(1, Ordering::Relaxed) { - 0 => format!( - "{{\"{checksum}\":{{\"state\":\"not_found\",\"missingChunks\":[\"{checksum}\"]}}}}" - ) - .into(), - 1 => format!( - "{{\"{checksum}\":{{\"state\":\"created\",\"missingChunks\":[]}}}}" - ) - .into(), - 2 => format!( - "{{\"{checksum}\":{{\"state\":\"ok\",\"detail\":null,\"missingChunks\":[],\"dif\":{{\"id\":\"1\",\"uuid\":\"00000000-0000-0000-0000-000000000000\",\"debugId\":\"00000000-0000-0000-0000-000000000000\",\"objectName\":\"dartsymbolmap.json\",\"cpuName\":\"any\",\"headers\":{{\"Content-Type\":\"application/octet-stream\"}},\"size\":1,\"sha1\":\"{checksum}\",\"dateCreated\":\"1776-07-04T12:00:00.000Z\",\"data\":{{}}}}}}}}" - ) - .into(), - n => panic!( - "Only 3 calls to the assemble endpoint expected, but there were {}.", - n + 1 - ), - } - }) - .expect(3), - ) - .assert_cmd([ - "dart-symbol-map", - "upload", - "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", - // Use a fixture with a single Debug ID (embedded PDB) - "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", - ]) - .with_default_token() - .run_and_assert(AssertCommand::Success); -} - -#[test] -fn command_upload_dart_symbol_map_invalid_mapping() { - // Invalid mapping (odd number of entries) should fail before any HTTP calls. - TestManager::new() - .assert_cmd([ - "dart-symbol-map", - "upload", - "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json", - "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", - ]) - .with_default_token() - .run_and_assert(AssertCommand::Failure); -} From e0ad3ae4b0b994d05ea293bdc64a393c757c4549 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 16:58:12 +0200 Subject: [PATCH 13/25] Fmt --- src/commands/dart_symbol_map/mod.rs | 4 +- src/commands/dart_symbol_map/upload.rs | 10 +- src/commands/derive_parser.rs | 2 +- tests/integration/upload_dart_symbol_map.rs | 104 ++++++++++++++++++++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 tests/integration/upload_dart_symbol_map.rs diff --git a/src/commands/dart_symbol_map/mod.rs b/src/commands/dart_symbol_map/mod.rs index 833335e53b..bf95e4ef3d 100644 --- a/src/commands/dart_symbol_map/mod.rs +++ b/src/commands/dart_symbol_map/mod.rs @@ -1,6 +1,6 @@ -use anyhow::Result; -use clap::{Args, ArgMatches, Command, Parser as _, Subcommand}; use crate::utils::args::ArgExt as _; +use anyhow::Result; +use clap::{ArgMatches, Args, Command, Parser as _, Subcommand}; pub mod upload; diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 7d473db2bd..e60fc4437c 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use std::path::Path; use anyhow::{bail, Context as _, Result}; -use clap::{Args, ArgMatches}; +use clap::{ArgMatches, Args}; use crate::api::{Api, ChunkUploadCapability}; use crate::config::Config; @@ -45,11 +45,15 @@ impl<'a> Assemblable for DartSymbolMapObject<'a> { #[derive(Args, Clone)] pub(crate) struct DartSymbolMapUploadArgs { #[arg(value_name = "MAPPING")] - #[arg(help = "Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs).")] + #[arg( + help = "Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs)." + )] pub(super) mapping: String, #[arg(value_name = "DEBUG_FILE")] - #[arg(help = "Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID.")] + #[arg( + help = "Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID." + )] pub(super) debug_file: String, } diff --git a/src/commands/derive_parser.rs b/src/commands/derive_parser.rs index 6fb0ef3883..c11242e667 100644 --- a/src/commands/derive_parser.rs +++ b/src/commands/derive_parser.rs @@ -2,9 +2,9 @@ use crate::utils::auth_token::AuthToken; use crate::utils::value_parsers::{auth_token_parser, kv_parser}; use clap::{command, ArgAction::SetTrue, Parser, Subcommand}; +use super::dart_symbol_map::DartSymbolMapArgs; use super::logs::LogsArgs; use super::send_metric::SendMetricArgs; -use super::dart_symbol_map::DartSymbolMapArgs; #[derive(Parser)] pub(super) struct SentryCLI { diff --git a/tests/integration/upload_dart_symbol_map.rs b/tests/integration/upload_dart_symbol_map.rs new file mode 100644 index 0000000000..784ceeeda5 --- /dev/null +++ b/tests/integration/upload_dart_symbol_map.rs @@ -0,0 +1,104 @@ +use std::sync::atomic::{AtomicU8, Ordering}; + +use crate::integration::test_utils::AssertCommand; +use crate::integration::{MockEndpointBuilder, TestManager}; + +#[test] +fn command_upload_dart_symbol_map_missing_capability() { + // Server does not advertise `dartsymbolmap` capability → command should bail early. + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("debug_files/get-chunk-upload.json"), + ) + .assert_cmd([ + "dart-symbol-map", + "upload", + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", + // Use a fixture with a single Debug ID + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Failure); +} + +#[test] +fn command_upload_dart_symbol_map_chunk_upload_flow() { + // Happy path: server supports dartsymbolmap capability, file needs upload, then assembles to ok. + let call_count = AtomicU8::new(0); + + TestManager::new() + // Server advertises capability including `dartsymbolmap`. + .mock_endpoint( + MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/") + .with_response_file("dart_symbol_map/get-chunk-upload.json"), + ) + // Accept chunk upload requests for the missing chunks; no validation needed here. + .mock_endpoint(MockEndpointBuilder::new( + "POST", + "/api/0/organizations/wat-org/chunk-upload/", + )) + // Assemble flow: 1) not_found (missingChunks), 2) created, 3) ok + .mock_endpoint( + MockEndpointBuilder::new( + "POST", + "/api/0/projects/wat-org/wat-project/files/difs/assemble/", + ) + .with_header_matcher("content-type", "application/json") + .with_response_fn(move |request| { + let body = request.body().expect("body should be readable"); + let body_json: serde_json::Value = serde_json::from_slice(body) + .expect("request body should be valid JSON"); + + // The request map has a single entry keyed by checksum; reuse it in responses. + let (checksum, _obj) = body_json + .as_object() + .and_then(|m| m.iter().next()) + .map(|(k, v)| (k.clone(), v.clone())) + .expect("assemble request must contain at least one object"); + + match call_count.fetch_add(1, Ordering::Relaxed) { + 0 => format!( + "{{\"{checksum}\":{{\"state\":\"not_found\",\"missingChunks\":[\"{checksum}\"]}}}}" + ) + .into(), + 1 => format!( + "{{\"{checksum}\":{{\"state\":\"created\",\"missingChunks\":[]}}}}" + ) + .into(), + 2 => format!( + "{{\"{checksum}\":{{\"state\":\"ok\",\"detail\":null,\"missingChunks\":[],\"dif\":{{\"id\":\"1\",\"uuid\":\"00000000-0000-0000-0000-000000000000\",\"debugId\":\"00000000-0000-0000-0000-000000000000\",\"objectName\":\"dartsymbolmap.json\",\"cpuName\":\"any\",\"headers\":{{\"Content-Type\":\"application/octet-stream\"}},\"size\":1,\"sha1\":\"{checksum}\",\"dateCreated\":\"1776-07-04T12:00:00.000Z\",\"data\":{{}}}}}}}}" + ) + .into(), + n => panic!( + "Only 3 calls to the assemble endpoint expected, but there were {}.", + n + 1 + ), + } + }) + .expect(3), + ) + .assert_cmd([ + "dart-symbol-map", + "upload", + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json", + // Use a fixture with a single Debug ID (embedded PDB) + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Success); +} + +#[test] +fn command_upload_dart_symbol_map_invalid_mapping() { + // Invalid mapping (odd number of entries) should fail before any HTTP calls. + TestManager::new() + .assert_cmd([ + "dart-symbol-map", + "upload", + "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap-invalid.json", + "tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb", + ]) + .with_default_token() + .run_and_assert(AssertCommand::Failure); +} From d0c2e1c273bdf36fc5be268028e732c5e9d45bbb Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:13:34 +0200 Subject: [PATCH 14/25] Fmt --- src/commands/dart_symbol_map/mod.rs | 9 +++----- src/commands/dart_symbol_map/upload.rs | 32 ++++++++++++++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/commands/dart_symbol_map/mod.rs b/src/commands/dart_symbol_map/mod.rs index bf95e4ef3d..24dd414c98 100644 --- a/src/commands/dart_symbol_map/mod.rs +++ b/src/commands/dart_symbol_map/mod.rs @@ -1,4 +1,3 @@ -use crate::utils::args::ArgExt as _; use anyhow::Result; use clap::{ArgMatches, Args, Command, Parser as _, Subcommand}; @@ -29,13 +28,11 @@ pub(super) fn make_command(command: Command) -> Command { command .about(GROUP_ABOUT) .subcommand_required(true) - .arg_required_else_help(true) - .org_arg() - .project_arg(false), + .arg_required_else_help(true), ) } -pub(super) fn execute(matches: &ArgMatches) -> Result<()> { +pub(super) fn execute(_: &ArgMatches) -> Result<()> { let subcommand = match crate::commands::derive_parser::SentryCLI::parse().command { crate::commands::derive_parser::SentryCLICommand::DartSymbolMap(DartSymbolMapArgs { subcommand, @@ -44,6 +41,6 @@ pub(super) fn execute(matches: &ArgMatches) -> Result<()> { }; match subcommand { - DartSymbolMapSubcommand::Upload(args) => upload::execute(args, matches), + DartSymbolMapSubcommand::Upload(args) => upload::execute(args), } } diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index e60fc4437c..258b2342c9 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use std::path::Path; use anyhow::{bail, Context as _, Result}; -use clap::{ArgMatches, Args}; +use clap::Args; use crate::api::{Api, ChunkUploadCapability}; use crate::config::Config; @@ -44,6 +44,14 @@ impl<'a> Assemblable for DartSymbolMapObject<'a> { #[derive(Args, Clone)] pub(crate) struct DartSymbolMapUploadArgs { + #[arg(short = 'o', long = "org")] + #[arg(help = "The organization ID or slug.")] + pub(super) org: Option, + + #[arg(short = 'p', long = "project")] + #[arg(help = "The project ID or slug.")] + pub(super) project: Option, + #[arg(value_name = "MAPPING")] #[arg( help = "Path to the dartsymbolmap JSON file (e.g. dartsymbolmap.json). Must be a JSON array of strings with an even number of entries (pairs)." @@ -57,7 +65,7 @@ pub(crate) struct DartSymbolMapUploadArgs { pub(super) debug_file: String, } -pub(super) fn execute(args: DartSymbolMapUploadArgs, matches: &ArgMatches) -> Result<()> { +pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { let mapping_path = &args.mapping; let debug_file_path = &args.debug_file; @@ -105,7 +113,23 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs, matches: &ArgMatches) -> Re // Prepare chunked upload let api = Api::current(); - let (org, project) = Config::current().get_org_and_project(matches)?; + // Resolve org and project like logs: prefer args, fallback to defaults + let config = Config::current(); + let (default_org, default_project) = config.get_org_and_project_defaults(); + let org = args + .org + .as_ref() + .or(default_org.as_ref()) + .ok_or_else(|| anyhow::anyhow!( + "No organization specified. Please specify an organization using the --org argument." + ))?; + let project = args + .project + .as_ref() + .or(default_project.as_ref()) + .ok_or_else(|| anyhow::anyhow!( + "No project specified. Use --project or set a default in config." + ))?; let chunk_upload_options = api .authenticated()? .get_chunk_upload_options(&org)? @@ -135,7 +159,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs, matches: &ArgMatches) -> Re ); } - let options = ChunkOptions::new(chunk_upload_options, &org, &project) + let options = ChunkOptions::new(chunk_upload_options, org, project) .with_max_wait(DEFAULT_MAX_WAIT); let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?; From 0441ed574fc71fc4d324ddd6e9d2dc38ee87b653 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:14:53 +0200 Subject: [PATCH 15/25] Clippy --- src/commands/dart_symbol_map/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 258b2342c9..371dc2ad07 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -132,7 +132,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { ))?; let chunk_upload_options = api .authenticated()? - .get_chunk_upload_options(&org)? + .get_chunk_upload_options(org)? .ok_or_else(|| anyhow::anyhow!( "server does not support chunked uploading. Please update your Sentry server." ))?; From 00f38965c2b9d0272d3b306cc718dc6d94ed5301 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:16:29 +0200 Subject: [PATCH 16/25] Clippy --- src/commands/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4e87cd544f..bd5c984b07 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -12,7 +12,6 @@ use std::{env, iter}; use crate::api::Api; use crate::config::{Auth, Config}; use crate::constants::{ARCH, PLATFORM, VERSION}; -use crate::utils::args::ArgExt as _; use crate::utils::auth_token::{redact_token_from_string, AuthToken}; use crate::utils::logging::set_quiet_mode; use crate::utils::logging::Logger; @@ -46,7 +45,6 @@ mod sourcemaps; mod uninstall; #[cfg(not(feature = "managed"))] mod update; -// removed: upload_dart_symbol_map (replaced by derive-based dart-symbol-map group) mod upload_dif; mod upload_dsym; mod upload_proguard; From a369698c8c33feca74d98317047d3da107900c3f Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:19:20 +0200 Subject: [PATCH 17/25] Return early when checking file size --- src/commands/dart_symbol_map/upload.rs | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 371dc2ad07..543ae828cb 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -111,6 +111,22 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { debug_id, }; + // Early file size check against server or default limits (same as debug files) + let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { + chunk_upload_options.max_file_size + } else { + DEFAULT_MAX_DIF_SIZE + }; + + if (mapping_len as u64) > effective_max_file_size { + bail!( + "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", + mapping_path, + mapping_len, + effective_max_file_size + ); + } + // Prepare chunked upload let api = Api::current(); // Resolve org and project like logs: prefer args, fallback to defaults @@ -143,22 +159,6 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { ); } - // Early file size check against server or default limits (same as debug files) - let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { - chunk_upload_options.max_file_size - } else { - DEFAULT_MAX_DIF_SIZE - }; - - if (mapping_len as u64) > effective_max_file_size { - bail!( - "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", - mapping_path, - mapping_len, - effective_max_file_size - ); - } - let options = ChunkOptions::new(chunk_upload_options, org, project) .with_max_wait(DEFAULT_MAX_WAIT); From 2bf941596705e04df87316de7b864428246a0a42 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:24:48 +0200 Subject: [PATCH 18/25] Fix clippy --- src/commands/dart_symbol_map/upload.rs | 32 +++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 543ae828cb..371dc2ad07 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -111,22 +111,6 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { debug_id, }; - // Early file size check against server or default limits (same as debug files) - let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { - chunk_upload_options.max_file_size - } else { - DEFAULT_MAX_DIF_SIZE - }; - - if (mapping_len as u64) > effective_max_file_size { - bail!( - "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", - mapping_path, - mapping_len, - effective_max_file_size - ); - } - // Prepare chunked upload let api = Api::current(); // Resolve org and project like logs: prefer args, fallback to defaults @@ -159,6 +143,22 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { ); } + // Early file size check against server or default limits (same as debug files) + let effective_max_file_size = if chunk_upload_options.max_file_size > 0 { + chunk_upload_options.max_file_size + } else { + DEFAULT_MAX_DIF_SIZE + }; + + if (mapping_len as u64) > effective_max_file_size { + bail!( + "The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).", + mapping_path, + mapping_len, + effective_max_file_size + ); + } + let options = ChunkOptions::new(chunk_upload_options, org, project) .with_max_wait(DEFAULT_MAX_WAIT); From fccdd7ee523732de01fe427a00c51636e9037954 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 17:32:49 +0200 Subject: [PATCH 19/25] Clippy --- src/commands/dart_symbol_map/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 371dc2ad07..178ba67275 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -71,7 +71,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids: Vec<_> = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); + let mut ids: Vec<_> = dif.ids().filter(|id| !id.is_nil()).collect(); // Ensure a single, unambiguous Debug ID ids.sort(); From 15dd5ba6a5aa450b7b67bea52e4a3c0bd60a2957 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 20:10:44 +0200 Subject: [PATCH 20/25] Update --- src/commands/dart_symbol_map/upload.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 178ba67275..d1d7a2e9cf 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -71,7 +71,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids: Vec<_> = dif.ids().filter(|id| !id.is_nil()).collect(); + let mut ids: Vec<_> = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); // Ensure a single, unambiguous Debug ID ids.sort(); @@ -87,8 +87,9 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Validate the dartsymbolmap JSON: must be a JSON array of strings with even length let mapping_file_bytes = ByteView::open(mapping_path) .with_context(|| format!("Failed to read mapping file at {mapping_path}"))?; - let mapping_entries: Vec<&str> = serde_json::from_slice(&mapping_file_bytes) - .context("Invalid dartsymbolmap: expected a JSON array of strings")?; + let mapping_entries: Vec> = + serde_json::from_slice(mapping_file_bytes.as_ref()) + .context("Invalid dartsymbolmap: expected a JSON array of strings")?; if mapping_entries.len() % 2 != 0 { bail!( From 9dedda703e1ca0f0f357e84af7976f41910aa2f4 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 20:21:09 +0200 Subject: [PATCH 21/25] Update --- src/commands/dart_symbol_map/upload.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index d1d7a2e9cf..ada254ef46 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -71,7 +71,8 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids: Vec<_> = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); + let mut ids = dif.ids(); + ids.retain(|id| !id.is_nil()); // Ensure a single, unambiguous Debug ID ids.sort(); From 2eac874c1a78ec1bf37d8b5dfc2c826e4b2b0016 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 21:54:16 +0200 Subject: [PATCH 22/25] Update --- src/commands/dart_symbol_map/upload.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index ada254ef46..79da53bca4 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -71,8 +71,11 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids = dif.ids(); - ids.retain(|id| !id.is_nil()); + let mut ids: Vec = dif + .ids() + .into_iter() + .filter(|id| !id.is_nil()) + .collect(); // Ensure a single, unambiguous Debug ID ids.sort(); From 44446d311f03fdd490b149a319f79589043cafa7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 22:02:44 +0200 Subject: [PATCH 23/25] fmt --- src/commands/dart_symbol_map/upload.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index 79da53bca4..f04620a66e 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -71,11 +71,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids: Vec = dif - .ids() - .into_iter() - .filter(|id| !id.is_nil()) - .collect(); + let mut ids: Vec = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); // Ensure a single, unambiguous Debug ID ids.sort(); From 7986534d3e430a68433afcf639c0d7dc22952ced Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Tue, 12 Aug 2025 22:10:34 +0200 Subject: [PATCH 24/25] Update --- src/commands/dart_symbol_map/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index f04620a66e..a4426be590 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -71,7 +71,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { // Extract Debug ID(s) from the provided debug file let dif = DifFile::open_path(debug_file_path, None)?; - let mut ids: Vec = dif.ids().into_iter().filter(|id| !id.is_nil()).collect(); + let mut ids: Vec = dif.ids().filter(|id| !id.is_nil()).collect(); // Ensure a single, unambiguous Debug ID ids.sort(); From 6d662420dc6dfa181b1e4664fc13757ab207ad63 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 13 Aug 2025 11:31:48 +0200 Subject: [PATCH 25/25] Update src/commands/dart_symbol_map/upload.rs Co-authored-by: Sebastian Zivota --- src/commands/dart_symbol_map/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/dart_symbol_map/upload.rs b/src/commands/dart_symbol_map/upload.rs index a4426be590..f4da7f17e4 100644 --- a/src/commands/dart_symbol_map/upload.rs +++ b/src/commands/dart_symbol_map/upload.rs @@ -93,7 +93,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> { if mapping_entries.len() % 2 != 0 { bail!( - "Invalid dartsymbolmap: expected an even number of entries (pairs), got {}", + "Invalid dartsymbolmap: expected an even number of entries, got {}", mapping_entries.len() ); }