diff --git a/src/api/mod.rs b/src/api/mod.rs index 8d8cb97e5b..41ac6f822c 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1454,7 +1454,7 @@ pub struct FetchEventsOptions<'a> { /// Fields to include in the response pub fields: &'a [&'a str], /// Project ID to filter events by - pub project_id: &'a str, + pub project_id: Option<&'a str>, /// Cursor for pagination pub cursor: Option<&'a str>, /// Query string to filter events @@ -1480,8 +1480,14 @@ impl<'a> FetchEventsOptions<'a> { params.push(format!("cursor={}", QueryArg(cursor))); } - params.push(format!("project={}", QueryArg(self.project_id))); - params.push(format!("query={}", QueryArg(self.query))); + if let Some(project) = self.project_id { + if !project.is_empty() { + params.push(format!("project={}", QueryArg(project))); + } + } + if !self.query.is_empty() { + params.push(format!("query={}", QueryArg(self.query))); + } params.push(format!("per_page={}", self.per_page)); params.push(format!("statsPeriod={}", QueryArg(self.stats_period))); params.push(format!("sort={}", QueryArg(self.sort))); diff --git a/src/commands/logs/list.rs b/src/commands/logs/list.rs index 052983d1aa..a21b0840fc 100644 --- a/src/commands/logs/list.rs +++ b/src/commands/logs/list.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use anyhow::Result; use clap::Args; @@ -20,6 +22,11 @@ fn validate_max_rows(s: &str) -> Result { } } +/// Check if a project identifier is numeric (project ID) or string (project slug) +fn is_numeric_project_id(project: &str) -> bool { + !project.is_empty() && project.chars().all(|c| c.is_ascii_digit()) +} + /// Fields to fetch from the logs API const LOG_FIELDS: &[&str] = &[ "sentry.item_id", @@ -37,7 +44,7 @@ pub(super) struct ListLogsArgs { org: Option, #[arg(short = 'p', long = "project")] - #[arg(help = "The project ID (slug not supported).")] + #[arg(help = "The project ID or slug.")] project: Option, #[arg(long = "max-rows", default_value = "100")] @@ -70,29 +77,36 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> { let api = Api::current(); - let query = if args.query.is_empty() { - None + // Pass numeric project IDs as project parameter, otherwise pass as query string - + // current API does not support project slugs as a parameter. + let (query, project_id) = if is_numeric_project_id(project) { + (Cow::Borrowed(&args.query), Some(project.as_str())) } else { - Some(args.query.as_str()) + let query = if args.query.is_empty() { + format!("project:{project}") + } else { + format!("project:{project} {}", args.query) + }; + (Cow::Owned(query), None) }; - execute_single_fetch(&api, org, project, query, LOG_FIELDS, &args) + execute_single_fetch(&api, org, project_id, &query, LOG_FIELDS, &args) } fn execute_single_fetch( api: &Api, org: &str, - project: &str, - query: Option<&str>, + project_id: Option<&str>, + query: &str, fields: &[&str], args: &ListLogsArgs, ) -> Result<()> { let options = FetchEventsOptions { dataset: Dataset::Logs, fields, - project_id: project, + project_id, cursor: None, - query: query.unwrap_or(""), + query, per_page: args.max_rows, stats_period: "90d", sort: "-timestamp", @@ -128,3 +142,34 @@ fn execute_single_fetch( Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_numeric_project_id_purely_numeric() { + assert!(is_numeric_project_id("123456")); + assert!(is_numeric_project_id("1")); + assert!(is_numeric_project_id("999999999")); + } + + #[test] + fn test_is_numeric_project_id_alphanumeric() { + assert!(!is_numeric_project_id("abc123")); + assert!(!is_numeric_project_id("123abc")); + assert!(!is_numeric_project_id("my-project")); + } + + #[test] + fn test_is_numeric_project_id_numeric_with_dash() { + assert!(!is_numeric_project_id("123-45")); + assert!(!is_numeric_project_id("1-2-3")); + assert!(!is_numeric_project_id("999-888")); + } + + #[test] + fn test_is_numeric_project_id_empty_string() { + assert!(!is_numeric_project_id("")); + } +} diff --git a/tests/integration/_cases/logs/logs-list-help.trycmd b/tests/integration/_cases/logs/logs-list-help.trycmd index 8d8452a25c..dc4e47d0c4 100644 --- a/tests/integration/_cases/logs/logs-list-help.trycmd +++ b/tests/integration/_cases/logs/logs-list-help.trycmd @@ -18,7 +18,7 @@ Options: in key:value format. -p, --project - The project ID (slug not supported). + The project ID or slug. --auth-token Use the given Sentry auth token. diff --git a/tests/integration/_cases/logs/logs-list-no-logs-found-project-id.trycmd b/tests/integration/_cases/logs/logs-list-no-logs-found-project-id.trycmd new file mode 100644 index 0000000000..490da719ba --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-no-logs-found-project-id.trycmd @@ -0,0 +1,7 @@ +``` +$ sentry-cli logs list --org wat-org --project 12345 +? success +[BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release. +No logs found + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd b/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd index ecc80fb9e5..13b53e7692 100644 --- a/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd +++ b/tests/integration/_cases/logs/logs-list-no-logs-found.trycmd @@ -1,5 +1,5 @@ ``` -$ sentry-cli logs list --org wat-org --project 12345 +$ sentry-cli logs list --org wat-org --project myproject ? success [BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release. No logs found diff --git a/tests/integration/_cases/logs/logs-list-with-data-project-id.trycmd b/tests/integration/_cases/logs/logs-list-with-data-project-id.trycmd new file mode 100644 index 0000000000..7a53110be0 --- /dev/null +++ b/tests/integration/_cases/logs/logs-list-with-data-project-id.trycmd @@ -0,0 +1,13 @@ +``` +$ sentry-cli logs list --project 12345 +? success +[BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release. ++------------------+---------------------------+----------+--------------------------+----------------------+ +| Item ID | Timestamp | Severity | Message | Trace | ++------------------+---------------------------+----------+--------------------------+----------------------+ +| test-item-id-001 | 2025-01-15T10:30:00+00:00 | info | test_log_message_001 | test-trace-id-abc123 | +| test-item-id-002 | 2025-01-15T10:31:00+00:00 | error | test_error_message_002 | test-trace-id-def456 | +| test-item-id-003 | 2025-01-15T10:32:00+00:00 | warning | test_warning_message_003 | test-trace-id-ghi789 | ++------------------+---------------------------+----------+--------------------------+----------------------+ + +``` \ No newline at end of file diff --git a/tests/integration/_cases/logs/logs-list-with-data.trycmd b/tests/integration/_cases/logs/logs-list-with-data.trycmd index 097e8fabe4..d50530d382 100644 --- a/tests/integration/_cases/logs/logs-list-with-data.trycmd +++ b/tests/integration/_cases/logs/logs-list-with-data.trycmd @@ -1,5 +1,5 @@ ``` -$ sentry-cli logs list +$ sentry-cli logs list --org wat-org --project myproject ? success [BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release. +------------------+---------------------------+----------+--------------------------+----------------------+ diff --git a/tests/integration/logs.rs b/tests/integration/logs.rs index 1c08d11392..b79fc9b01f 100644 --- a/tests/integration/logs.rs +++ b/tests/integration/logs.rs @@ -1,12 +1,12 @@ use crate::integration::{MockEndpointBuilder, TestManager}; #[test] -fn command_logs_with_api_calls() { +fn command_logs_with_api_calls_project_slug() { TestManager::new() .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=wat-project&query=&per_page=100&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&query=project:myproject&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_file("logs/get-logs.json"), ) @@ -15,12 +15,26 @@ fn command_logs_with_api_calls() { } #[test] -fn command_logs_no_logs_found() { +fn command_logs_with_api_calls_project_id() { TestManager::new() .mock_endpoint( MockEndpointBuilder::new( "GET", - "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&query=&per_page=100&statsPeriod=90d&sort=-timestamp" + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=100&statsPeriod=90d&sort=-timestamp" + ) + .with_response_file("logs/get-logs.json"), + ) + .register_trycmd_test("logs/logs-list-with-data-project-id.trycmd") + .with_default_token(); +} + +#[test] +fn command_logs_no_logs_found_project_slug() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new( + "GET", + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&query=project:myproject&per_page=100&statsPeriod=90d&sort=-timestamp" ) .with_response_body(r#"{"data": []}"#), ) @@ -28,6 +42,20 @@ fn command_logs_no_logs_found() { .with_default_token(); } +#[test] +fn command_logs_no_logs_found_project_id() { + TestManager::new() + .mock_endpoint( + MockEndpointBuilder::new( + "GET", + "/api/0/organizations/wat-org/events/?dataset=logs&field=sentry.item_id&field=trace&field=severity&field=timestamp&field=message&project=12345&per_page=100&statsPeriod=90d&sort=-timestamp" + ) + .with_response_body(r#"{"data": []}"#), + ) + .register_trycmd_test("logs/logs-list-no-logs-found-project-id.trycmd") + .with_default_token(); +} + #[test] fn command_logs_zero_max_rows() { TestManager::new().register_trycmd_test("logs/logs-list-with-zero-max-rows.trycmd");