Skip to content

Commit 7bd0418

Browse files
fix(dart-symbol-map): Resolve org from token via config
1 parent 50091ba commit 7bd0418

File tree

7 files changed

+140
-87
lines changed

7 files changed

+140
-87
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Fixes
1010

1111
- Fixed a bug where the `dart-symbol-map` command did not accept the `--url` argument ([#3108](https://github.com/getsentry/sentry-cli/pull/3108)).
12+
- The `dart-symbol-map upload` command now correctly resolves the organization from the auth token payload ([#3065](https://github.com/getsentry/sentry-cli/pull/3065)).
1213

1314
## 3.1.0
1415

Lines changed: 16 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,27 @@
11
use anyhow::Result;
2-
use clap::{ArgMatches, Args, Command, Parser as _, Subcommand};
2+
use clap::{ArgMatches, Command};
3+
4+
use crate::utils::args::ArgExt as _;
35

46
pub mod upload;
57

68
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. {n}{n}\
11-
This command is supported on Sentry SaaS and self-hosted versions ≥25.8.0.";
129

13-
#[derive(Args)]
14-
pub(super) struct DartSymbolMapArgs {
15-
#[command(subcommand)]
16-
pub(super) subcommand: DartSymbolMapSubcommand,
17-
}
10+
pub(super) fn make_command(mut command: Command) -> Command {
11+
command = command
12+
.about(GROUP_ABOUT)
13+
.subcommand_required(true)
14+
.arg_required_else_help(true)
15+
.org_arg()
16+
.project_arg(false);
1817

19-
#[derive(Subcommand)]
20-
#[command(about = GROUP_ABOUT)]
21-
pub(super) enum DartSymbolMapSubcommand {
22-
#[command(about = UPLOAD_ABOUT)]
23-
#[command(long_about = UPLOAD_LONG_ABOUT)]
24-
Upload(upload::DartSymbolMapUploadArgs),
18+
command = command.subcommand(upload::make_command(Command::new("upload")));
19+
command
2520
}
2621

27-
pub(super) fn make_command(command: Command) -> Command {
28-
DartSymbolMapSubcommand::augment_subcommands(
29-
command
30-
.about(GROUP_ABOUT)
31-
.subcommand_required(true)
32-
.arg_required_else_help(true),
33-
)
34-
}
35-
36-
pub(super) fn execute(_: &ArgMatches) -> Result<()> {
37-
let subcommand = match crate::commands::derive_parser::SentryCLI::parse().command {
38-
crate::commands::derive_parser::SentryCLICommand::DartSymbolMap(DartSymbolMapArgs {
39-
subcommand,
40-
}) => subcommand,
41-
_ => unreachable!("expected dart-symbol-map subcommand"),
42-
};
43-
44-
match subcommand {
45-
DartSymbolMapSubcommand::Upload(args) => upload::execute(args),
22+
pub(super) fn execute(matches: &ArgMatches) -> Result<()> {
23+
if let Some(sub_matches) = matches.subcommand_matches("upload") {
24+
return upload::execute(sub_matches);
4625
}
26+
unreachable!();
4727
}

src/commands/dart_symbol_map/upload.rs

Lines changed: 35 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter, Result as FmtResult};
44
use std::path::Path;
55

66
use anyhow::{bail, Context as _, Result};
7-
use clap::Args;
7+
use clap::{Arg, ArgMatches, Command};
88

99
use crate::api::Api;
1010
use crate::config::Config;
@@ -42,32 +42,37 @@ impl Assemblable for DartSymbolMapObject<'_> {
4242
}
4343
}
4444

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,
45+
const MAPPING_ARG: &str = "mapping";
46+
const DEBUG_FILE_ARG: &str = "debug_file";
47+
48+
pub(super) fn make_command(command: Command) -> Command {
49+
command
50+
.about("Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.")
51+
.long_about(
52+
"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. {n}{n}\
53+
This command is supported on Sentry SaaS and self-hosted versions ≥25.8.0.",
54+
)
55+
.arg(
56+
Arg::new(MAPPING_ARG)
57+
.value_name("MAPPING")
58+
.required(true)
59+
.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)."),
60+
)
61+
.arg(
62+
Arg::new(DEBUG_FILE_ARG)
63+
.value_name("DEBUG_FILE")
64+
.required(true)
65+
.help("Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID."),
66+
)
6667
}
6768

68-
pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
69-
let mapping_path = &args.mapping;
70-
let debug_file_path = &args.debug_file;
69+
pub(super) fn execute(matches: &ArgMatches) -> Result<()> {
70+
let mapping_path = matches
71+
.get_one::<String>(MAPPING_ARG)
72+
.expect("required by clap");
73+
let debug_file_path = matches
74+
.get_one::<String>(DEBUG_FILE_ARG)
75+
.expect("required by clap");
7176

7277
// Extract Debug ID(s) from the provided debug file
7378
let dif = DifFile::open_path(debug_file_path, None)?;
@@ -101,8 +106,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
101106
let file_name = Path::new(mapping_path)
102107
.file_name()
103108
.and_then(OsStr::to_str)
104-
.unwrap_or(mapping_path)
105-
;
109+
.unwrap_or(mapping_path);
106110

107111
let mapping_len = mapping_file_bytes.len();
108112
let object = DartSymbolMapObject {
@@ -113,27 +117,12 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
113117

114118
// Prepare chunked upload
115119
let api = Api::current();
116-
// Resolve org and project like logs: prefer args, fallback to defaults
117120
let config = Config::current();
118-
let (default_org, default_project) = config.get_org_and_project_defaults();
119-
let org = args
120-
.org
121-
.as_ref()
122-
.or(default_org.as_ref())
123-
.ok_or_else(|| anyhow::anyhow!(
124-
"No organization specified. Please specify an organization using the --org argument."
125-
))?;
126-
let project = args
127-
.project
128-
.as_ref()
129-
.or(default_project.as_ref())
130-
.ok_or_else(|| anyhow::anyhow!(
131-
"No project specified. Use --project or set a default in config."
132-
))?;
121+
let org = config.get_org(matches)?;
122+
let project = config.get_project(matches)?;
133123
let chunk_upload_options = api
134124
.authenticated()?
135-
.get_chunk_upload_options(org)?;
136-
125+
.get_chunk_upload_options(&org)?;
137126

138127
// Early file size check against server or default limits (same as debug files)
139128
let effective_max_file_size = if chunk_upload_options.max_file_size > 0 {
@@ -148,7 +137,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
148137
);
149138
}
150139

151-
let options = ChunkOptions::new(chunk_upload_options, org, project)
140+
let options = ChunkOptions::new(chunk_upload_options, &org, &project)
152141
.with_max_wait(DEFAULT_MAX_WAIT);
153142

154143
let chunked = Chunked::from(object, options.server_options().chunk_size);

src/commands/derive_parser.rs

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

5-
use super::dart_symbol_map::DartSymbolMapArgs;
65
use super::logs::LogsArgs;
76

87
#[derive(Parser)]
@@ -38,5 +37,4 @@ pub(super) struct SentryCLI {
3837
#[derive(Subcommand)]
3938
pub(super) enum SentryCLICommand {
4039
Logs(LogsArgs),
41-
DartSymbolMap(DartSymbolMapArgs),
4240
}

src/commands/logs/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,7 @@ pub(super) fn make_command(command: Command) -> Command {
3939
}
4040

4141
pub(super) fn execute(_: &ArgMatches) -> Result<()> {
42-
let SentryCLICommand::Logs(LogsArgs { subcommand }) = SentryCLI::parse().command else {
43-
unreachable!("expected logs subcommand");
44-
};
42+
let SentryCLICommand::Logs(LogsArgs { subcommand }) = SentryCLI::parse().command;
4543
eprintln!("{BETA_WARNING}");
4644

4745
match subcommand {

tests/integration/test_utils/test_manager.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,16 @@ impl AssertCmdTestManager {
207207
self
208208
}
209209

210+
/// Set a custom environment variable for the test.
211+
pub fn env(
212+
mut self,
213+
key: impl AsRef<std::ffi::OsStr>,
214+
value: impl AsRef<std::ffi::OsStr>,
215+
) -> Self {
216+
self.command.env(key, value);
217+
self
218+
}
219+
210220
/// Run the command and perform assertions.
211221
///
212222
/// This function asserts both the mocks and the command result.

tests/integration/upload_dart_symbol_map.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ use std::sync::atomic::{AtomicU8, Ordering};
33
use crate::integration::test_utils::AssertCommand;
44
use crate::integration::{MockEndpointBuilder, TestManager};
55

6+
/// A test org auth token with org="wat-org" and empty URL.
7+
/// Format: sntrys_{base64_payload}_{base64_secret}
8+
/// Payload: {"iat":1704374159.069583,"url":"","region_url":"","org":"wat-org"}
9+
const ORG_AUTH_TOKEN_WAT_ORG: &str = "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";
10+
611
#[test]
712
fn command_upload_dart_symbol_map_missing_capability() {
813
// Server does not advertise `dartsymbolmap` capability → command should bail early.
@@ -175,3 +180,75 @@ fn command_upload_dart_symbol_map_with_custom_url() {
175180
.with_default_token()
176181
.run_and_assert(AssertCommand::Success);
177182
}
183+
184+
#[test]
185+
fn command_upload_dart_symbol_map_org_from_token() {
186+
// When no --org is provided and SENTRY_ORG is not set, the org should be resolved
187+
// from the org auth token.
188+
let call_count = AtomicU8::new(0);
189+
190+
TestManager::new()
191+
// Server advertises capability including `dartsymbolmap`.
192+
// This endpoint uses "wat-org" in the path - if org resolution fails,
193+
// the request would go to a different path and not match.
194+
.mock_endpoint(
195+
MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/")
196+
.with_response_file("dart_symbol_map/get-chunk-upload.json"),
197+
)
198+
// Accept chunk upload requests for the missing chunks
199+
.mock_endpoint(MockEndpointBuilder::new(
200+
"POST",
201+
"/api/0/organizations/wat-org/chunk-upload/",
202+
))
203+
// Assemble flow: 1) not_found (missingChunks), 2) created, 3) ok
204+
.mock_endpoint(
205+
MockEndpointBuilder::new(
206+
"POST",
207+
"/api/0/projects/wat-org/wat-project/files/difs/assemble/",
208+
)
209+
.with_header_matcher("content-type", "application/json")
210+
.with_response_fn(move |request| {
211+
let body = request.body().expect("body should be readable");
212+
let body_json: serde_json::Value =
213+
serde_json::from_slice(body).expect("request body should be valid JSON");
214+
215+
let (checksum, _obj) = body_json
216+
.as_object()
217+
.and_then(|m| m.iter().next())
218+
.map(|(k, v)| (k.clone(), v.clone()))
219+
.expect("assemble request must contain at least one object");
220+
221+
match call_count.fetch_add(1, Ordering::Relaxed) {
222+
0 => format!(
223+
"{{\"{checksum}\":{{\"state\":\"not_found\",\"missingChunks\":[\"{checksum}\"]}}}}"
224+
)
225+
.into(),
226+
1 => format!(
227+
"{{\"{checksum}\":{{\"state\":\"created\",\"missingChunks\":[]}}}}"
228+
)
229+
.into(),
230+
2 => format!(
231+
"{{\"{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\":{{}}}}}}}}"
232+
)
233+
.into(),
234+
n => panic!(
235+
"Only 3 calls to the assemble endpoint expected, but there were {}.",
236+
n + 1
237+
),
238+
}
239+
})
240+
.expect(3),
241+
)
242+
.assert_cmd([
243+
"dart-symbol-map",
244+
"upload",
245+
// No --org flag provided!
246+
"tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json",
247+
"tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb",
248+
])
249+
// Use org auth token with embedded org="wat-org" instead of default token
250+
.env("SENTRY_AUTH_TOKEN", ORG_AUTH_TOKEN_WAT_ORG)
251+
// Explicitly unset SENTRY_ORG to ensure org comes from token
252+
.env("SENTRY_ORG", "")
253+
.run_and_assert(AssertCommand::Success);
254+
}

0 commit comments

Comments
 (0)