From e8f8050d79fa98f169e7b140ec032f2be85a2c97 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 11:55:08 +0000 Subject: [PATCH] feat(error-tracking): add --state, --team, --assignee filters to issues search Exposes new SDK filter params from PRs #1568 and #1480: - --state: OPEN, ACKNOWLEDGED, RESOLVED, IGNORED, EXCLUDED - --team : filter by team UUID assignee - --assignee : filter by user UUID assignee Co-Authored-By: Claude --- docs/COMMANDS.md | 10 ++- src/commands/error_tracking.rs | 156 ++++++++++++++++++++++++++++++++- src/main.rs | 15 +++- 3 files changed, 178 insertions(+), 3 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 09481384..4851e557 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -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) @@ -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 ` — filter by issue state: `OPEN`, `ACKNOWLEDGED`, `RESOLVED`, `IGNORED`, `EXCLUDED` + - `--team ` — filter by team UUID assignee + - `--assignee ` — 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 diff --git a/src/commands/error_tracking.rs b/src/commands/error_tracking.rs index 00dce418..75ca9bf7 100644 --- a/src/commands/error_tracking.rs +++ b/src/commands/error_tracking.rs @@ -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, }; @@ -22,6 +22,9 @@ pub async fn issues_search( order_by: String, track: Option, persona: Option, + state: Option, + team: Option, + assignee: Option, ) -> Result<()> { let api = crate::make_api!(ErrorTrackingAPI, cfg); @@ -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(); @@ -119,6 +147,9 @@ mod tests { "TOTAL_COUNT".into(), Some("trace".into()), None, + None, + None, + None, ) .await; cleanup_env(); @@ -139,6 +170,9 @@ mod tests { "TOTAL_COUNT".into(), None, Some("BROWSER".into()), + None, + None, + None, ) .await; cleanup_env(); @@ -159,6 +193,9 @@ mod tests { "TOTAL_COUNT".into(), Some("RUM".into()), None, + None, + None, + None, ) .await; cleanup_env(); @@ -176,6 +213,9 @@ mod tests { "INVALID".into(), Some("trace".into()), None, + None, + None, + None, ) .await; assert!(result.is_err()); @@ -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([ @@ -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" + ); + } } diff --git a/src/main.rs b/src/main.rs index 8fbdede0..bb3565d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6879,6 +6879,15 @@ enum ErrorTrackingIssueActions { help = "Client persona filter: ALL, BROWSER, MOBILE, or BACKEND" )] persona: Option, + #[arg( + long, + help = "Filter by issue state: OPEN, ACKNOWLEDGED, RESOLVED, IGNORED, EXCLUDED" + )] + state: Option, + #[arg(long, help = "Filter by team UUID assignee")] + team: Option, + #[arg(long, help = "Filter by user UUID assignee")] + assignee: Option, }, /// Get issue details Get { issue_id: String }, @@ -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?; }