Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Add `--install-group` parameter to `sentry-cli build upload` for controlling update visibility between builds ([#3094](https://github.com/getsentry/sentry-cli/pull/3094))

### Fixes

- The `dart-symbol-map upload` command now correctly resolves the organization from the auth token payload ([#3113](https://github.com/getsentry/sentry-cli/pull/3113)).

## 3.1.0

### New Features
Expand Down
9 changes: 0 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
# Adding new commands
For new commands, it is recommended to use clap's [Derive API](https://docs.rs/clap/latest/clap/_derive/index.html).
In contrast to the [Builder API](https://docs.rs/clap/latest/clap/_tutorial/index.html), the Derive API makes it:
- Easier to read, write, and modify commands and arguments.
- Easier to keep argument declaration and reading in sync.
- Easier to reuse shared arguments.

An existing example of how to use the Derive API is the `send-metric` command.

# Integration Tests

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/.
Expand Down
52 changes: 16 additions & 36 deletions src/commands/dart_symbol_map/mod.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,27 @@
use anyhow::Result;
use clap::{ArgMatches, Args, Command, Parser as _, Subcommand};
use clap::{ArgMatches, Command};

use crate::utils::args::ArgExt as _;

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

#[derive(Args)]
pub(super) struct DartSymbolMapArgs {
#[command(subcommand)]
pub(super) subcommand: DartSymbolMapSubcommand,
}
pub(super) fn make_command(mut command: Command) -> Command {
command = command
.about(GROUP_ABOUT)
.subcommand_required(true)
.arg_required_else_help(true)
.org_arg()
.project_arg(false);

#[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.subcommand(upload::make_command(Command::new("upload")));
command
}

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),
pub(super) fn execute(matches: &ArgMatches) -> Result<()> {
if let Some(sub_matches) = matches.subcommand_matches("upload") {
return upload::execute(sub_matches);
}
unreachable!();
}
81 changes: 35 additions & 46 deletions src/commands/dart_symbol_map/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
use clap::{Arg, ArgMatches, Command};

use crate::api::Api;
use crate::config::Config;
Expand Down Expand Up @@ -42,32 +42,37 @@ impl Assemblable for DartSymbolMapObject<'_> {
}
}

#[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,
const MAPPING_ARG: &str = "mapping";
const DEBUG_FILE_ARG: &str = "debug_file";

pub(super) fn make_command(command: Command) -> Command {
command
.about("Upload a Dart/Flutter symbol map (dartsymbolmap) for deobfuscating Dart exception types.")
.long_about(
"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}\
This command is supported on Sentry SaaS and self-hosted versions ≥25.8.0.",
)
.arg(
Arg::new(MAPPING_ARG)
.value_name("MAPPING")
.required(true)
.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(
Arg::new(DEBUG_FILE_ARG)
.value_name("DEBUG_FILE")
.required(true)
.help("Path to the corresponding debug file to extract the Debug ID from. The file must contain exactly one Debug ID."),
)
}

pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
let mapping_path = &args.mapping;
let debug_file_path = &args.debug_file;
pub(super) fn execute(matches: &ArgMatches) -> Result<()> {
let mapping_path = matches
.get_one::<String>(MAPPING_ARG)
.expect("required by clap");
let debug_file_path = matches
.get_one::<String>(DEBUG_FILE_ARG)
.expect("required by clap");

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

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

// 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 org = config.get_org(matches)?;
let project = config.get_project(matches)?;
let chunk_upload_options = api
.authenticated()?
.get_chunk_upload_options(org)?;

.get_chunk_upload_options(&org)?;

// 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 {
Expand All @@ -148,7 +137,7 @@ pub(super) fn execute(args: DartSymbolMapUploadArgs) -> Result<()> {
);
}

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);
Expand Down
2 changes: 0 additions & 2 deletions src/commands/derive_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use crate::utils::auth_token::AuthToken;
use crate::utils::value_parsers::{auth_token_parser, kv_parser};
use clap::{ArgAction::SetTrue, Parser, Subcommand};

use super::dart_symbol_map::DartSymbolMapArgs;
use super::logs::LogsArgs;

#[derive(Parser)]
Expand Down Expand Up @@ -34,5 +33,4 @@ pub(super) struct SentryCLI {
#[derive(Subcommand)]
pub(super) enum SentryCLICommand {
Logs(LogsArgs),
DartSymbolMap(DartSymbolMapArgs),
}
4 changes: 1 addition & 3 deletions src/commands/logs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ pub(super) fn make_command(command: Command) -> Command {
}

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

match subcommand {
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/test_utils/test_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,16 @@ impl AssertCmdTestManager {
self
}

/// Set a custom environment variable for the test.
pub fn env(
mut self,
key: impl AsRef<std::ffi::OsStr>,
value: impl AsRef<std::ffi::OsStr>,
) -> Self {
self.command.env(key, value);
self
}

/// Run the command and perform assertions.
///
/// This function asserts both the mocks and the command result.
Expand Down
77 changes: 77 additions & 0 deletions tests/integration/upload_dart_symbol_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ use std::sync::atomic::{AtomicU8, Ordering};
use crate::integration::test_utils::AssertCommand;
use crate::integration::{MockEndpointBuilder, TestManager};

/// A test org auth token with org="wat-org" and empty URL.
/// Format: sntrys_{base64_payload}_{base64_secret}
/// Payload: {"iat":1704374159.069583,"url":"","region_url":"","org":"wat-org"}
const ORG_AUTH_TOKEN_WAT_ORG: &str = "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";

#[test]
fn command_upload_dart_symbol_map_missing_capability() {
// Server does not advertise `dartsymbolmap` capability → command should bail early.
Expand Down Expand Up @@ -102,3 +107,75 @@ fn command_upload_dart_symbol_map_invalid_mapping() {
.with_default_token()
.run_and_assert(AssertCommand::Failure);
}

#[test]
fn command_upload_dart_symbol_map_org_from_token() {
// When no --org is provided and SENTRY_ORG is not set, the org should be resolved
// from the org auth token.
let call_count = AtomicU8::new(0);

TestManager::new()
// Server advertises capability including `dartsymbolmap`.
// This endpoint uses "wat-org" in the path - if org resolution fails,
// the request would go to a different path and not match.
.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
.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");

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",
// No --org flag provided!
"tests/integration/_fixtures/dart_symbol_map/dartsymbolmap.json",
"tests/integration/_fixtures/Sentry.Samples.Console.Basic.pdb",
])
// Use org auth token with embedded org="wat-org" instead of default token
.env("SENTRY_AUTH_TOKEN", ORG_AUTH_TOKEN_WAT_ORG)
// Explicitly unset SENTRY_ORG to ensure org comes from token
.env("SENTRY_ORG", "")
.run_and_assert(AssertCommand::Success);
}