Skip to content

Commit 4e7d0e4

Browse files
feat(dart): add dart-symbol-map upload command (#2691)
Adds support for uploading dart symbol maps that are associated via debug id extracted from the provided debug file example usage: ```bash sentry-cli dart-symbol-map upload --org my-org --project my-proj /path/to/mapping /path/to/debug-file ``` Tests: - integration tests - manual tests of uploading to the test sdk repo on sentry Part of getsentry/sentry-dart#2805 --------- Co-authored-by: Sebastian Zivota <loewenheim@users.noreply.github.com>
1 parent 9f3bba8 commit 4e7d0e4

File tree

13 files changed

+374
-0
lines changed

13 files changed

+374
-0
lines changed

src/api/data_types/chunking/upload/capability.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ pub enum ChunkUploadCapability {
3030
/// Upload of il2cpp line mappings
3131
Il2Cpp,
3232

33+
/// Upload of Dart symbol maps
34+
DartSymbolMap,
35+
3336
/// Upload of preprod artifacts
3437
PreprodArtifacts,
3538

@@ -52,6 +55,7 @@ impl<'de> Deserialize<'de> for ChunkUploadCapability {
5255
"sources" => ChunkUploadCapability::Sources,
5356
"bcsymbolmaps" => ChunkUploadCapability::BcSymbolmap,
5457
"il2cpp" => ChunkUploadCapability::Il2Cpp,
58+
"dartsymbolmap" => ChunkUploadCapability::DartSymbolMap,
5559
"preprod_artifacts" => ChunkUploadCapability::PreprodArtifacts,
5660
_ => ChunkUploadCapability::Unknown,
5761
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use anyhow::Result;
2+
use clap::{ArgMatches, Args, Command, Parser as _, Subcommand};
3+
4+
pub mod upload;
5+
6+
const GROUP_ABOUT: &str = "Manage Dart/Flutter symbol maps for Sentry.";
7+
const UPLOAD_ABOUT: &str =
8+
"Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.";
9+
const UPLOAD_LONG_ABOUT: &str =
10+
"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.";
11+
12+
#[derive(Args)]
13+
pub(super) struct DartSymbolMapArgs {
14+
#[command(subcommand)]
15+
pub(super) subcommand: DartSymbolMapSubcommand,
16+
}
17+
18+
#[derive(Subcommand)]
19+
#[command(about = GROUP_ABOUT)]
20+
pub(super) enum DartSymbolMapSubcommand {
21+
#[command(about = UPLOAD_ABOUT)]
22+
#[command(long_about = UPLOAD_LONG_ABOUT)]
23+
Upload(upload::DartSymbolMapUploadArgs),
24+
}
25+
26+
pub(super) fn make_command(command: Command) -> Command {
27+
DartSymbolMapSubcommand::augment_subcommands(
28+
command
29+
.about(GROUP_ABOUT)
30+
.subcommand_required(true)
31+
.arg_required_else_help(true),
32+
)
33+
}
34+
35+
pub(super) fn execute(_: &ArgMatches) -> Result<()> {
36+
let subcommand = match crate::commands::derive_parser::SentryCLI::parse().command {
37+
crate::commands::derive_parser::SentryCLICommand::DartSymbolMap(DartSymbolMapArgs {
38+
subcommand,
39+
}) => subcommand,
40+
_ => unreachable!("expected dart-symbol-map subcommand"),
41+
};
42+
43+
match subcommand {
44+
DartSymbolMapSubcommand::Upload(args) => upload::execute(args),
45+
}
46+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
use std::borrow::Cow;
2+
use std::ffi::OsStr;
3+
use std::fmt::{Display, Formatter, Result as FmtResult};
4+
use std::path::Path;
5+
6+
use anyhow::{bail, Context as _, Result};
7+
use clap::Args;
8+
9+
use crate::api::{Api, ChunkUploadCapability};
10+
use crate::config::Config;
11+
use crate::constants::{DEFAULT_MAX_DIF_SIZE, DEFAULT_MAX_WAIT};
12+
use crate::utils::chunks::{upload_chunked_objects, Assemblable, ChunkOptions, Chunked};
13+
use crate::utils::dif::DifFile;
14+
use symbolic::common::ByteView;
15+
use symbolic::common::DebugId;
16+
17+
struct DartSymbolMapObject<'a> {
18+
bytes: &'a [u8],
19+
name: &'a str,
20+
debug_id: DebugId,
21+
}
22+
23+
impl<'a> AsRef<[u8]> for DartSymbolMapObject<'a> {
24+
fn as_ref(&self) -> &[u8] {
25+
self.bytes
26+
}
27+
}
28+
29+
impl<'a> Display for DartSymbolMapObject<'a> {
30+
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
31+
write!(f, "dartsymbolmap {}", self.name)
32+
}
33+
}
34+
35+
impl<'a> Assemblable for DartSymbolMapObject<'a> {
36+
fn name(&self) -> Cow<'_, str> {
37+
Cow::Borrowed(self.name)
38+
}
39+
40+
fn debug_id(&self) -> Option<DebugId> {
41+
Some(self.debug_id)
42+
}
43+
}
44+
45+
#[derive(Args, Clone)]
46+
pub(crate) struct DartSymbolMapUploadArgs {
47+
#[arg(short = 'o', long = "org")]
48+
#[arg(help = "The organization ID or slug.")]
49+
pub(super) org: Option<String>,
50+
51+
#[arg(short = 'p', long = "project")]
52+
#[arg(help = "The project ID or slug.")]
53+
pub(super) project: Option<String>,
54+
55+
#[arg(value_name = "MAPPING")]
56+
#[arg(
57+
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)."
58+
)]
59+
pub(super) mapping: String,
60+
61+
#[arg(value_name = "DEBUG_FILE")]
62+
#[arg(
63+
help = "Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID."
64+
)]
65+
pub(super) debug_file: String,
66+
}
67+
68+
pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
69+
let mapping_path = &args.mapping;
70+
let debug_file_path = &args.debug_file;
71+
72+
// Extract Debug ID(s) from the provided debug file
73+
let dif = DifFile::open_path(debug_file_path, None)?;
74+
let mut ids: Vec<DebugId> = dif.ids().filter(|id| !id.is_nil()).collect();
75+
76+
// Ensure a single, unambiguous Debug ID
77+
ids.sort();
78+
ids.dedup();
79+
match ids.len() {
80+
0 => bail!(
81+
"No debug identifier found in the provided debug file ({}). Ensure the file contains an embedded Debug ID.",
82+
debug_file_path
83+
),
84+
1 => {
85+
let debug_id = ids.remove(0);
86+
87+
// Validate the dartsymbolmap JSON: must be a JSON array of strings with even length
88+
let mapping_file_bytes = ByteView::open(mapping_path)
89+
.with_context(|| format!("Failed to read mapping file at {mapping_path}"))?;
90+
let mapping_entries: Vec<Cow<'_, str>> =
91+
serde_json::from_slice(mapping_file_bytes.as_ref())
92+
.context("Invalid dartsymbolmap: expected a JSON array of strings")?;
93+
94+
if mapping_entries.len() % 2 != 0 {
95+
bail!(
96+
"Invalid dartsymbolmap: expected an even number of entries, got {}",
97+
mapping_entries.len()
98+
);
99+
}
100+
101+
// Prepare upload object
102+
let file_name = Path::new(mapping_path)
103+
.file_name()
104+
.and_then(OsStr::to_str)
105+
.unwrap_or(mapping_path)
106+
;
107+
108+
let mapping_len = mapping_file_bytes.len();
109+
let object = DartSymbolMapObject {
110+
bytes: mapping_file_bytes.as_ref(),
111+
name: file_name,
112+
debug_id,
113+
};
114+
115+
// Prepare chunked upload
116+
let api = Api::current();
117+
// Resolve org and project like logs: prefer args, fallback to defaults
118+
let config = Config::current();
119+
let (default_org, default_project) = config.get_org_and_project_defaults();
120+
let org = args
121+
.org
122+
.as_ref()
123+
.or(default_org.as_ref())
124+
.ok_or_else(|| anyhow::anyhow!(
125+
"No organization specified. Please specify an organization using the --org argument."
126+
))?;
127+
let project = args
128+
.project
129+
.as_ref()
130+
.or(default_project.as_ref())
131+
.ok_or_else(|| anyhow::anyhow!(
132+
"No project specified. Use --project or set a default in config."
133+
))?;
134+
let chunk_upload_options = api
135+
.authenticated()?
136+
.get_chunk_upload_options(org)?
137+
.ok_or_else(|| anyhow::anyhow!(
138+
"server does not support chunked uploading. Please update your Sentry server."
139+
))?;
140+
141+
if !chunk_upload_options.supports(ChunkUploadCapability::DartSymbolMap) {
142+
bail!(
143+
"Server does not support uploading Dart symbol maps via chunked upload. Please update your Sentry server."
144+
);
145+
}
146+
147+
// Early file size check against server or default limits (same as debug files)
148+
let effective_max_file_size = if chunk_upload_options.max_file_size > 0 {
149+
chunk_upload_options.max_file_size
150+
} else {
151+
DEFAULT_MAX_DIF_SIZE
152+
};
153+
154+
if (mapping_len as u64) > effective_max_file_size {
155+
bail!(
156+
"The dartsymbolmap '{}' exceeds the maximum allowed size ({} bytes > {} bytes).",
157+
mapping_path,
158+
mapping_len,
159+
effective_max_file_size
160+
);
161+
}
162+
163+
let options = ChunkOptions::new(chunk_upload_options, org, project)
164+
.with_max_wait(DEFAULT_MAX_WAIT);
165+
166+
let chunked = Chunked::from(object, options.server_options().chunk_size as usize)?;
167+
let (_uploaded, has_processing_errors) = upload_chunked_objects(&[chunked], options)?;
168+
if has_processing_errors {
169+
bail!("Some symbol maps did not process correctly");
170+
}
171+
172+
Ok(())
173+
}
174+
_ => bail!(
175+
"Multiple debug identifiers found in the provided debug file ({}): {}. Please provide a file that contains a single Debug ID.",
176+
debug_file_path,
177+
ids.into_iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", ")
178+
),
179+
}
180+
}

src/commands/derive_parser.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::utils::auth_token::AuthToken;
22
use crate::utils::value_parsers::{auth_token_parser, kv_parser};
33
use clap::{command, ArgAction::SetTrue, Parser, Subcommand};
44

5+
use super::dart_symbol_map::DartSymbolMapArgs;
56
use super::logs::LogsArgs;
67
use super::send_metric::SendMetricArgs;
78

@@ -35,4 +36,5 @@ pub(super) struct SentryCLI {
3536
pub(super) enum SentryCLICommand {
3637
Logs(LogsArgs),
3738
SendMetric(SendMetricArgs),
39+
DartSymbolMap(DartSymbolMapArgs),
3840
}

src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use crate::utils::update::run_sentrycli_update_nagger;
2020
use crate::utils::value_parsers::auth_token_parser;
2121

2222
mod bash_hook;
23+
mod dart_symbol_map;
2324
mod debug_files;
2425
mod deploys;
2526
mod derive_parser;
@@ -71,6 +72,7 @@ macro_rules! each_subcommand {
7172
$mac!(send_envelope);
7273
$mac!(send_metric);
7374
$mac!(sourcemaps);
75+
$mac!(dart_symbol_map);
7476
#[cfg(not(feature = "managed"))]
7577
$mac!(uninstall);
7678
#[cfg(not(feature = "managed"))]

tests/integration/_cases/help/help-windows.trycmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Commands:
2727
send-event Send a manual event to Sentry.
2828
send-envelope Send a stored envelope to Sentry.
2929
sourcemaps Manage sourcemaps for Sentry releases.
30+
dart-symbol-map Manage Dart/Flutter symbol maps for Sentry.
3031
upload-proguard Upload ProGuard mapping files to a project.
3132
help Print this message or the help of the given subcommand(s)
3233

tests/integration/_cases/help/help.trycmd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Commands:
2727
send-event Send a manual event to Sentry.
2828
send-envelope Send a stored envelope to Sentry.
2929
sourcemaps Manage sourcemaps for Sentry releases.
30+
dart-symbol-map Manage Dart/Flutter symbol maps for Sentry.
3031
uninstall Uninstall the sentry-cli executable.
3132
upload-proguard Upload ProGuard mapping files to a project.
3233
help Print this message or the help of the given subcommand(s)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
"MaterialApp",
3+
"ex",
4+
"Scaffold"
5+
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
"MaterialApp",
3+
"ex",
4+
"Scaffold",
5+
"ey"
6+
]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"url": "organizations/wat-org/chunk-upload/",
3+
"chunkSize": 8388608,
4+
"chunksPerRequest": 64,
5+
"maxFileSize": 2147483648,
6+
"maxRequestSize": 33554432,
7+
"concurrency": 8,
8+
"hashAlgorithm": "sha1",
9+
"compression": ["gzip"],
10+
"accept": ["dartsymbolmap"]
11+
}

0 commit comments

Comments
 (0)