Skip to content

Commit c7e2731

Browse files
authored
feat(logs): allow project slugs in logs list (#2688)
1 parent 0b093b3 commit c7e2731

File tree

8 files changed

+118
-19
lines changed

8 files changed

+118
-19
lines changed

src/api/mod.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,7 +1454,7 @@ pub struct FetchEventsOptions<'a> {
14541454
/// Fields to include in the response
14551455
pub fields: &'a [&'a str],
14561456
/// Project ID to filter events by
1457-
pub project_id: &'a str,
1457+
pub project_id: Option<&'a str>,
14581458
/// Cursor for pagination
14591459
pub cursor: Option<&'a str>,
14601460
/// Query string to filter events
@@ -1480,8 +1480,14 @@ impl<'a> FetchEventsOptions<'a> {
14801480
params.push(format!("cursor={}", QueryArg(cursor)));
14811481
}
14821482

1483-
params.push(format!("project={}", QueryArg(self.project_id)));
1484-
params.push(format!("query={}", QueryArg(self.query)));
1483+
if let Some(project) = self.project_id {
1484+
if !project.is_empty() {
1485+
params.push(format!("project={}", QueryArg(project)));
1486+
}
1487+
}
1488+
if !self.query.is_empty() {
1489+
params.push(format!("query={}", QueryArg(self.query)));
1490+
}
14851491
params.push(format!("per_page={}", self.per_page));
14861492
params.push(format!("statsPeriod={}", QueryArg(self.stats_period)));
14871493
params.push(format!("sort={}", QueryArg(self.sort)));

src/commands/logs/list.rs

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::borrow::Cow;
2+
13
use anyhow::Result;
24
use clap::Args;
35

@@ -20,6 +22,11 @@ fn validate_max_rows(s: &str) -> Result<usize> {
2022
}
2123
}
2224

25+
/// Check if a project identifier is numeric (project ID) or string (project slug)
26+
fn is_numeric_project_id(project: &str) -> bool {
27+
!project.is_empty() && project.chars().all(|c| c.is_ascii_digit())
28+
}
29+
2330
/// Fields to fetch from the logs API
2431
const LOG_FIELDS: &[&str] = &[
2532
"sentry.item_id",
@@ -37,7 +44,7 @@ pub(super) struct ListLogsArgs {
3744
org: Option<String>,
3845

3946
#[arg(short = 'p', long = "project")]
40-
#[arg(help = "The project ID (slug not supported).")]
47+
#[arg(help = "The project ID or slug.")]
4148
project: Option<String>,
4249

4350
#[arg(long = "max-rows", default_value = "100")]
@@ -70,29 +77,36 @@ pub(super) fn execute(args: ListLogsArgs) -> Result<()> {
7077

7178
let api = Api::current();
7279

73-
let query = if args.query.is_empty() {
74-
None
80+
// Pass numeric project IDs as project parameter, otherwise pass as query string -
81+
// current API does not support project slugs as a parameter.
82+
let (query, project_id) = if is_numeric_project_id(project) {
83+
(Cow::Borrowed(&args.query), Some(project.as_str()))
7584
} else {
76-
Some(args.query.as_str())
85+
let query = if args.query.is_empty() {
86+
format!("project:{project}")
87+
} else {
88+
format!("project:{project} {}", args.query)
89+
};
90+
(Cow::Owned(query), None)
7791
};
7892

79-
execute_single_fetch(&api, org, project, query, LOG_FIELDS, &args)
93+
execute_single_fetch(&api, org, project_id, &query, LOG_FIELDS, &args)
8094
}
8195

8296
fn execute_single_fetch(
8397
api: &Api,
8498
org: &str,
85-
project: &str,
86-
query: Option<&str>,
99+
project_id: Option<&str>,
100+
query: &str,
87101
fields: &[&str],
88102
args: &ListLogsArgs,
89103
) -> Result<()> {
90104
let options = FetchEventsOptions {
91105
dataset: Dataset::Logs,
92106
fields,
93-
project_id: project,
107+
project_id,
94108
cursor: None,
95-
query: query.unwrap_or(""),
109+
query,
96110
per_page: args.max_rows,
97111
stats_period: "90d",
98112
sort: "-timestamp",
@@ -128,3 +142,34 @@ fn execute_single_fetch(
128142

129143
Ok(())
130144
}
145+
146+
#[cfg(test)]
147+
mod tests {
148+
use super::*;
149+
150+
#[test]
151+
fn test_is_numeric_project_id_purely_numeric() {
152+
assert!(is_numeric_project_id("123456"));
153+
assert!(is_numeric_project_id("1"));
154+
assert!(is_numeric_project_id("999999999"));
155+
}
156+
157+
#[test]
158+
fn test_is_numeric_project_id_alphanumeric() {
159+
assert!(!is_numeric_project_id("abc123"));
160+
assert!(!is_numeric_project_id("123abc"));
161+
assert!(!is_numeric_project_id("my-project"));
162+
}
163+
164+
#[test]
165+
fn test_is_numeric_project_id_numeric_with_dash() {
166+
assert!(!is_numeric_project_id("123-45"));
167+
assert!(!is_numeric_project_id("1-2-3"));
168+
assert!(!is_numeric_project_id("999-888"));
169+
}
170+
171+
#[test]
172+
fn test_is_numeric_project_id_empty_string() {
173+
assert!(!is_numeric_project_id(""));
174+
}
175+
}

tests/integration/_cases/logs/logs-list-help.trycmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Options:
1818
in key:value format.
1919

2020
-p, --project <PROJECT>
21-
The project ID (slug not supported).
21+
The project ID or slug.
2222

2323
--auth-token <AUTH_TOKEN>
2424
Use the given Sentry auth token.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```
2+
$ sentry-cli logs list --org wat-org --project 12345
3+
? success
4+
[BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release.
5+
No logs found
6+
7+
```

tests/integration/_cases/logs/logs-list-no-logs-found.trycmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
```
2-
$ sentry-cli logs list --org wat-org --project 12345
2+
$ sentry-cli logs list --org wat-org --project myproject
33
? success
44
[BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release.
55
No logs found
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
```
2+
$ sentry-cli logs list --project 12345
3+
? success
4+
[BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release.
5+
+------------------+---------------------------+----------+--------------------------+----------------------+
6+
| Item ID | Timestamp | Severity | Message | Trace |
7+
+------------------+---------------------------+----------+--------------------------+----------------------+
8+
| test-item-id-001 | 2025-01-15T10:30:00+00:00 | info | test_log_message_001 | test-trace-id-abc123 |
9+
| test-item-id-002 | 2025-01-15T10:31:00+00:00 | error | test_error_message_002 | test-trace-id-def456 |
10+
| test-item-id-003 | 2025-01-15T10:32:00+00:00 | warning | test_warning_message_003 | test-trace-id-ghi789 |
11+
+------------------+---------------------------+----------+--------------------------+----------------------+
12+
13+
```

tests/integration/_cases/logs/logs-list-with-data.trycmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
```
2-
$ sentry-cli logs list
2+
$ sentry-cli logs list --org wat-org --project myproject
33
? success
44
[BETA] The "logs" command is in beta. The command is subject to breaking changes, including removal, in any Sentry CLI release.
55
+------------------+---------------------------+----------+--------------------------+----------------------+

tests/integration/logs.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
use crate::integration::{MockEndpointBuilder, TestManager};
22

33
#[test]
4-
fn command_logs_with_api_calls() {
4+
fn command_logs_with_api_calls_project_slug() {
55
TestManager::new()
66
.mock_endpoint(
77
MockEndpointBuilder::new(
88
"GET",
9-
"/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"
9+
"/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"
1010
)
1111
.with_response_file("logs/get-logs.json"),
1212
)
@@ -15,19 +15,47 @@ fn command_logs_with_api_calls() {
1515
}
1616

1717
#[test]
18-
fn command_logs_no_logs_found() {
18+
fn command_logs_with_api_calls_project_id() {
1919
TestManager::new()
2020
.mock_endpoint(
2121
MockEndpointBuilder::new(
2222
"GET",
23-
"/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"
23+
"/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"
24+
)
25+
.with_response_file("logs/get-logs.json"),
26+
)
27+
.register_trycmd_test("logs/logs-list-with-data-project-id.trycmd")
28+
.with_default_token();
29+
}
30+
31+
#[test]
32+
fn command_logs_no_logs_found_project_slug() {
33+
TestManager::new()
34+
.mock_endpoint(
35+
MockEndpointBuilder::new(
36+
"GET",
37+
"/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"
2438
)
2539
.with_response_body(r#"{"data": []}"#),
2640
)
2741
.register_trycmd_test("logs/logs-list-no-logs-found.trycmd")
2842
.with_default_token();
2943
}
3044

45+
#[test]
46+
fn command_logs_no_logs_found_project_id() {
47+
TestManager::new()
48+
.mock_endpoint(
49+
MockEndpointBuilder::new(
50+
"GET",
51+
"/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"
52+
)
53+
.with_response_body(r#"{"data": []}"#),
54+
)
55+
.register_trycmd_test("logs/logs-list-no-logs-found-project-id.trycmd")
56+
.with_default_token();
57+
}
58+
3159
#[test]
3260
fn command_logs_zero_max_rows() {
3361
TestManager::new().register_trycmd_test("logs/logs-list-with-zero-max-rows.trycmd");

0 commit comments

Comments
 (0)