-
-
Notifications
You must be signed in to change notification settings - Fork 235
feat(dart): add dart-symbol-map upload
command
#2691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cba6a75
07b6449
2004fa3
c97c31d
2bd4db2
e85a168
4aa9492
169f669
d16747c
6354b48
93c7abb
b4eaec4
f39731c
e0ad3ae
d0c2e1c
0441ed5
00f3896
a369698
2bf9415
fccdd7e
15dd5ba
9dedda7
2eac874
67615ba
44446d3
7986534
6d66242
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
use anyhow::Result; | ||
use clap::{ArgMatches, Args, Command, Parser as _, Subcommand}; | ||
|
||
pub mod 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, | ||
} | ||
|
||
#[derive(Subcommand)] | ||
#[command(about = GROUP_ABOUT)] | ||
pub(super) enum DartSymbolMapSubcommand { | ||
#[command(about = UPLOAD_ABOUT)] | ||
#[command(long_about = UPLOAD_LONG_ABOUT)] | ||
Upload(upload::DartSymbolMapUploadArgs), | ||
} | ||
|
||
pub(super) fn make_command(command: Command) -> Command { | ||
DartSymbolMapSubcommand::augment_subcommands( | ||
command | ||
.about(GROUP_ABOUT) | ||
.subcommand_required(true) | ||
.arg_required_else_help(true), | ||
) | ||
} | ||
|
||
pub(super) fn execute(_: &ArgMatches) -> Result<()> { | ||
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), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
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::Args; | ||
|
||
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<DebugId> { | ||
Some(self.debug_id) | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: DartSymbolMap Uploads MisclassifiedThe |
||
|
||
#[derive(Args, Clone)] | ||
pub(crate) struct DartSymbolMapUploadArgs { | ||
#[arg(short = 'o', long = "org")] | ||
#[arg(help = "The organization ID or slug.")] | ||
pub(super) org: Option<String>, | ||
|
||
#[arg(short = 'p', long = "project")] | ||
#[arg(help = "The project ID or slug.")] | ||
pub(super) project: Option<String>, | ||
|
||
#[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) -> 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<DebugId> = dif.ids().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<Cow<'_, str>> = | ||
serde_json::from_slice(mapping_file_bytes.as_ref()) | ||
.context("Invalid dartsymbolmap: expected a JSON array of strings")?; | ||
|
||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if mapping_entries.len() % 2 != 0 { | ||
bail!( | ||
"Invalid dartsymbolmap: expected an even number of entries, 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(); | ||
// 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)? | ||
.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 { | ||
buenaflor marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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::<Vec<_>>().join(", ") | ||
), | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
[ | ||
"MaterialApp", | ||
"ex", | ||
"Scaffold" | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[ | ||
"MaterialApp", | ||
"ex", | ||
"Scaffold", | ||
"ey" | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"] | ||
} |
Uh oh!
There was an error while loading. Please reload this page.