Skip to content
Merged
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
10 changes: 9 additions & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ pup infrastructure hosts list
### Development & Quality
- **cicd** - CI/CD visibility (pipelines, events, tests, dora, flaky-tests)
- **code-coverage** - Code coverage summaries (branch, commit)
- **error-tracking** - Error management (issues search, issues get)
- **error-tracking** - Error management (issues search, issues get); search supports `--state`, `--team`, `--assignee` filters
- **scorecards** - Service quality (rules, outcomes)
- **service-catalog** - Service registry (list, get)
- **idp** - Service Catalog agent access (assist, find, owner, deps, register)
Expand Down Expand Up @@ -222,6 +222,14 @@ Available on all commands:

## Recent Enhancements

### v0.64.x — Error Tracking Issue Filters (SDK PRs #1568, #1480)

- **error-tracking issues search** — new optional filter flags:
- `--state <STATE>` — filter by issue state: `OPEN`, `ACKNOWLEDGED`, `RESOLVED`, `IGNORED`, `EXCLUDED`
- `--team <UUID>` — filter by team UUID assignee
- `--assignee <UUID>` — filter by user UUID assignee
- These flags are independent of the existing `--track`/`--persona` mutual exclusion

### v0.34.1 — ACP Server (Datadog AI Agent Integration)

- ✅ **acp** (new) — Local ACP + OpenAI-compatible server that proxies to Datadog Bits AI
Expand Down
156 changes: 155 additions & 1 deletion src/commands/error_tracking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use datadog_api_client::datadogV2::api_error_tracking::{
ErrorTrackingAPI, GetIssueOptionalParams, SearchIssuesOptionalParams,
};
use datadog_api_client::datadogV2::model::{
IssuesSearchRequest, IssuesSearchRequestData, IssuesSearchRequestDataAttributes,
IssueState, IssuesSearchRequest, IssuesSearchRequestData, IssuesSearchRequestDataAttributes,
IssuesSearchRequestDataAttributesOrderBy, IssuesSearchRequestDataAttributesPersona,
IssuesSearchRequestDataAttributesTrack, IssuesSearchRequestDataType,
};
Expand All @@ -22,6 +22,9 @@ pub async fn issues_search(
order_by: String,
track: Option<String>,
persona: Option<String>,
state: Option<String>,
team: Option<String>,
assignee: Option<String>,
) -> Result<()> {
let api = crate::make_api!(ErrorTrackingAPI, cfg);

Expand Down Expand Up @@ -66,6 +69,31 @@ pub async fn issues_search(
};
attrs = attrs.persona(persona_value);
}
if let Some(ref s) = state {
let state_value = match s.to_uppercase().as_str() {
"OPEN" => IssueState::OPEN,
"ACKNOWLEDGED" => IssueState::ACKNOWLEDGED,
"RESOLVED" => IssueState::RESOLVED,
"IGNORED" => IssueState::IGNORED,
"EXCLUDED" => IssueState::EXCLUDED,
other => anyhow::bail!(
"invalid --state value '{}': must be OPEN, ACKNOWLEDGED, RESOLVED, IGNORED, or EXCLUDED",
other
),
};
attrs = attrs.states(vec![state_value]);
}
if let Some(ref t) = team {
let team_id = uuid::Uuid::parse_str(t)
.map_err(|_| anyhow::anyhow!("invalid --team value '{}': must be a valid UUID", t))?;
attrs = attrs.team_ids(vec![team_id]);
}
if let Some(ref a) = assignee {
let assignee_id = uuid::Uuid::parse_str(a).map_err(|_| {
anyhow::anyhow!("invalid --assignee value '{}': must be a valid UUID", a)
})?;
attrs = attrs.assignee_ids(vec![assignee_id]);
}
let data = IssuesSearchRequestData::new(attrs, IssuesSearchRequestDataType::SEARCH_REQUEST);
let body = IssuesSearchRequest::new(data);
let params = SearchIssuesOptionalParams::default();
Expand Down Expand Up @@ -119,6 +147,9 @@ mod tests {
"TOTAL_COUNT".into(),
Some("trace".into()),
None,
None,
None,
None,
)
.await;
cleanup_env();
Expand All @@ -139,6 +170,9 @@ mod tests {
"TOTAL_COUNT".into(),
None,
Some("BROWSER".into()),
None,
None,
None,
)
.await;
cleanup_env();
Expand All @@ -159,6 +193,9 @@ mod tests {
"TOTAL_COUNT".into(),
Some("RUM".into()),
None,
None,
None,
None,
)
.await;
cleanup_env();
Expand All @@ -176,6 +213,9 @@ mod tests {
"INVALID".into(),
Some("trace".into()),
None,
None,
None,
None,
)
.await;
assert!(result.is_err());
Expand All @@ -185,6 +225,102 @@ mod tests {
.contains("invalid --order-by value"));
}

#[tokio::test]
async fn test_issues_search_with_state() {
let _lock = lock_env().await;
let mut s = mockito::Server::new_async().await;
let cfg = test_config(&s.url());
mock_all(&mut s, r#"{"data": []}"#).await;
let result = super::issues_search(
&cfg,
None,
10,
"1d".into(),
"now".into(),
"TOTAL_COUNT".into(),
Some("trace".into()),
None,
Some("OPEN".into()),
None,
None,
)
.await;
assert!(result.is_ok());
cleanup_env();
}

#[tokio::test]
async fn test_issues_search_invalid_state() {
let cfg = test_config("http://unused.local");
let result = super::issues_search(
&cfg,
None,
10,
"1d".into(),
"now".into(),
"TOTAL_COUNT".into(),
Some("trace".into()),
None,
Some("BADSTATE".into()),
None,
None,
)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid --state value"));
}

#[tokio::test]
async fn test_issues_search_invalid_team_uuid() {
let cfg = test_config("http://unused.local");
let result = super::issues_search(
&cfg,
None,
10,
"1d".into(),
"now".into(),
"TOTAL_COUNT".into(),
Some("trace".into()),
None,
None,
Some("not-a-uuid".into()),
None,
)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid --team value"));
}

#[tokio::test]
async fn test_issues_search_invalid_assignee_uuid() {
let cfg = test_config("http://unused.local");
let result = super::issues_search(
&cfg,
None,
10,
"1d".into(),
"now".into(),
"TOTAL_COUNT".into(),
Some("trace".into()),
None,
None,
None,
Some("not-a-uuid".into()),
)
.await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("invalid --assignee value"));
}

#[test]
fn test_error_tracking_clap_mutual_exclusivity() {
let result = crate::Cli::command().try_get_matches_from([
Expand Down Expand Up @@ -216,4 +352,22 @@ mod tests {
"expected error when neither --track nor --persona is provided"
);
}

#[test]
fn test_error_tracking_clap_state_accepted() {
let result = crate::Cli::command().try_get_matches_from([
"pup",
"error-tracking",
"issues",
"search",
"--track",
"trace",
"--state",
"OPEN",
]);
assert!(
result.is_ok(),
"expected success when --state is provided alongside --track"
);
}
}
15 changes: 14 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6879,6 +6879,15 @@ enum ErrorTrackingIssueActions {
help = "Client persona filter: ALL, BROWSER, MOBILE, or BACKEND"
)]
persona: Option<String>,
#[arg(
long,
help = "Filter by issue state: OPEN, ACKNOWLEDGED, RESOLVED, IGNORED, EXCLUDED"
)]
state: Option<String>,
#[arg(long, help = "Filter by team UUID assignee")]
team: Option<String>,
#[arg(long, help = "Filter by user UUID assignee")]
assignee: Option<String>,
},
/// Get issue details
Get { issue_id: String },
Expand Down Expand Up @@ -13045,9 +13054,13 @@ async fn main_inner() -> anyhow::Result<()> {
order_by,
track,
persona,
state,
team,
assignee,
} => {
commands::error_tracking::issues_search(
&cfg, query, limit, from, to, order_by, track, persona,
&cfg, query, limit, from, to, order_by, track, persona, state, team,
assignee,
)
.await?;
}
Expand Down
Loading