Skip to content

Commit 2b24d7f

Browse files
fix(dart-symbol-map): Resolve org from token via config (#3113)
### Description - Switch `dart-symbol-map` to the clap builder pattern to align with most commands. - Resolve org/project via existing `Config` helpers so org auth tokens take precedence. - Add integration coverage for org-from-token behavior. ### Issues * resolves: #3063 * resolves: [CLI-260](https://linear.app/getsentry/issue/CLI-260/dart-symbol-map-upload-does-not-resolve-org-from-token) ### Legal Boilerplate Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.
1 parent 460372b commit 2b24d7f

File tree

8 files changed

+171
-96
lines changed

8 files changed

+171
-96
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- 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)).
8+
39
## 3.2.0
410

511
### Features

CONTRIBUTING.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
# Adding new commands
2-
For new commands, it is recommended to use clap's [Derive API](https://docs.rs/clap/latest/clap/_derive/index.html).
3-
In contrast to the [Builder API](https://docs.rs/clap/latest/clap/_tutorial/index.html), the Derive API makes it:
4-
- Easier to read, write, and modify commands and arguments.
5-
- Easier to keep argument declaration and reading in sync.
6-
- Easier to reuse shared arguments.
7-
8-
An existing example of how to use the Derive API is the `send-metric` command.
9-
101
# Integration Tests
112

123
Integration tests are written using `trycmd` crate. Consult the docs in case you need to understand how it works https://docs.rs/trycmd/latest/trycmd/.
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: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
use std::sync::atomic::{AtomicU8, Ordering};
2+
use std::sync::LazyLock;
3+
4+
use serde_json::Value;
25

36
use crate::integration::test_utils::AssertCommand;
47
use crate::integration::{MockEndpointBuilder, TestManager};
@@ -175,3 +178,103 @@ fn command_upload_dart_symbol_map_with_custom_url() {
175178
.with_default_token()
176179
.run_and_assert(AssertCommand::Success);
177180
}
181+
182+
/// A test to ensure that the command can resolve an organization from an
183+
/// org auth token.
184+
#[test]
185+
fn command_upload_dart_symbol_map_org_from_token() {
186+
/// Path to the mapping file
187+
const MAPPING_PATH: &str = "tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json";
188+
189+
/// A test org auth token with org="wat-org" and empty URL.
190+
/// Format: sntrys_{base64_payload}_{base64_secret}
191+
/// Payload: {"iat":1704374159.069583,"url":"","region_url":"","org":"wat-org"}
192+
const ORG_AUTH_TOKEN_WAT_ORG: &str = "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";
193+
194+
/// Checksum of the mapping file
195+
const EXPECTED_CHECKSUM: &str = "6aa44eb08e4a72d1cf32fe7c2504216fb1a3e862";
196+
197+
/// Expected request body for uploading the Dart symbol map
198+
static EXPECTED_REQUEST: LazyLock<Value> = LazyLock::new(|| {
199+
serde_json::json!({
200+
EXPECTED_CHECKSUM: {
201+
"chunks": [EXPECTED_CHECKSUM],
202+
"debug_id": "54fdf14a-41a1-426a-a073-8185e11a89d6-83920e6f",
203+
"name": "dartsymbolmap.json",
204+
}
205+
})
206+
});
207+
208+
// When no --org is provided and SENTRY_ORG is not set, the org should be resolved
209+
// from the org auth token.
210+
let call_count = AtomicU8::new(0);
211+
212+
TestManager::new()
213+
// This endpoint uses "wat-org" in the path - if org resolution fails,
214+
// the request would go to a different path and not match.
215+
.mock_endpoint(
216+
MockEndpointBuilder::new("GET", "/api/0/organizations/wat-org/chunk-upload/")
217+
.with_response_file("dart_symbol_map/get-chunk-upload.json"),
218+
)
219+
.mock_endpoint(MockEndpointBuilder::new(
220+
"POST",
221+
"/api/0/organizations/wat-org/chunk-upload/",
222+
))
223+
.mock_endpoint(
224+
MockEndpointBuilder::new(
225+
"POST",
226+
"/api/0/projects/wat-org/wat-project/files/difs/assemble/",
227+
)
228+
.with_header_matcher("content-type", "application/json")
229+
.with_response_fn(move |request| {
230+
let body = request.body().expect("body should be readable");
231+
let body_json: serde_json::Value =
232+
serde_json::from_slice(body).expect("request body should be valid JSON");
233+
234+
assert_eq!(
235+
body_json, *EXPECTED_REQUEST,
236+
"assemble request should match expected checksum payload"
237+
);
238+
239+
let response = match call_count.fetch_add(1, Ordering::Relaxed) {
240+
0 => serde_json::json!({
241+
EXPECTED_CHECKSUM: {
242+
"state": "not_found",
243+
"missingChunks": [EXPECTED_CHECKSUM],
244+
}
245+
}),
246+
1 => serde_json::json!({
247+
EXPECTED_CHECKSUM: {
248+
"state": "created",
249+
"missingChunks": [],
250+
}
251+
}),
252+
2 => serde_json::json!({
253+
EXPECTED_CHECKSUM: {
254+
"state": "ok",
255+
"missingChunks": [],
256+
}
257+
}),
258+
n => panic!(
259+
"Only 3 calls to the assemble endpoint expected, but there were {}.",
260+
n + 1
261+
),
262+
};
263+
264+
serde_json::to_vec(&response).expect("assemble response should be valid JSON")
265+
})
266+
.expect(3),
267+
)
268+
.assert_cmd([
269+
"dart-symbol-map",
270+
"upload",
271+
// No --org flag provided!
272+
MAPPING_PATH,
273+
"tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb",
274+
])
275+
// Use org auth token with embedded org="wat-org" instead of default token
276+
.env("SENTRY_AUTH_TOKEN", ORG_AUTH_TOKEN_WAT_ORG)
277+
// Explicitly unset SENTRY_ORG to ensure org comes from token
278+
.env("SENTRY_ORG", "")
279+
.run_and_assert(AssertCommand::Success);
280+
}

0 commit comments

Comments
 (0)