Skip to content

Conversation

@HazAT
Copy link
Member

@HazAT HazAT commented Jan 15, 2026

Summary

Add 5 new debugging/triage commands to sentry-cli that mirror the functionality of sentry-mcp tools. These commands expose Sentry's issue investigation capabilities directly from the CLI.

New Commands

Command Description
sentry-cli issues info <id> Get detailed issue information + latest event
sentry-cli issues events <id> List events for an issue
sentry-cli issues tags <id> --key <key> Get tag value distribution
sentry-cli traces info <id> Get trace details with span tree
sentry-cli events attachment <id> [att-id] List or download attachments

Example Usage

# Get detailed info about an issue
sentry-cli issues info PROJ-123 --org my-org

# List recent events for an issue
sentry-cli issues events PROJ-123 --limit 10 --org my-org

# See browser distribution for an issue
sentry-cli issues tags PROJ-123 --key browser --org my-org

# View a trace's span tree
sentry-cli traces info abc123def456 --org my-org

# List attachments for an event
sentry-cli events attachment abc123 --org my-org --project my-project

# Download a specific attachment
sentry-cli events attachment abc123 att_001 --output ./screenshot.png --org my-org --project my-project

API Endpoints Used

All commands use standard Sentry REST API endpoints:

  • /organizations/{org}/issues/{id}/
  • /organizations/{org}/issues/{id}/events/
  • /organizations/{org}/issues/{id}/events/latest/
  • /organizations/{org}/issues/{id}/tags/{key}/
  • /organizations/{org}/events-trace-meta/{id}/
  • /organizations/{org}/events-trace/{id}/
  • /projects/{org}/{project}/events/{id}/attachments/

Files Changed

  • src/api/mod.rs - Added 7 data types + 8 API methods
  • src/commands/issues/info.rs - NEW
  • src/commands/issues/events.rs - NEW
  • src/commands/issues/tags.rs - NEW
  • src/commands/traces/mod.rs - NEW
  • src/commands/events/attachment.rs - NEW
  • src/commands/mod.rs - Registered traces command

Test plan

  • cargo build succeeds
  • cargo clippy passes
  • All --help commands display correctly
  • Manual testing with real Sentry instance
Design

Sentry CLI Debugging/Triage Commands Design

Overview

Add debugging and triage commands to sentry-cli that mirror the functionality of sentry-mcp tools. These commands expose Sentry's issue investigation capabilities directly from the CLI, primarily for agent consumption.

Goal

Surface 5 MCP-equivalent debugging tools in sentry-cli:

  • get_issue_detailssentry-cli issues info
  • list_issue_eventssentry-cli issues events
  • get_issue_tag_valuessentry-cli issues tags
  • get_trace_detailssentry-cli traces info
  • get_event_attachmentsentry-cli events attachment

Commands

sentry-cli issues info <issue-id>

Get detailed information about a specific issue including the latest event.

Arguments:

  • issue_id (required) - Issue ID (e.g., PROJ-123 or full UUID)
  • --org - Organization slug (or from config)

Output:

Issue: PROJ-123
Title: TypeError: Cannot read property 'map' of undefined
Status: unresolved
Level: error
Events: 142
Users: 23
First Seen: 2024-01-15 10:30:00 UTC
Last Seen: 2024-01-15 14:22:00 UTC
Link: https://sentry.io/organizations/my-org/issues/123/

Latest Event: abc123def456
  Timestamp: 2024-01-15 14:22:00 UTC
  Environment: production
  Release: v1.2.3

sentry-cli issues events <issue-id>

List events within a specific issue.

Arguments:

  • issue_id (required) - Issue ID
  • --org - Organization slug
  • --limit - Max events to return (default 50)
  • --sort - Sort field (default "-timestamp")
  • --period - Time period (default "14d")

Output:

Events for PROJ-123 (showing 5 of 142):

  abc123def456  2024-01-15 14:22:00  production  v1.2.3
  def456abc789  2024-01-15 14:18:00  production  v1.2.3
  789xyz123abc  2024-01-15 14:15:00  staging     v1.2.3

sentry-cli issues tags <issue-id> --key <tag-key>

Get tag value distribution for an issue.

Arguments:

  • issue_id (required) - Issue ID
  • --org - Organization slug
  • --key (required) - Tag key to get values for

Output:

Tag: browser (47 unique values)

  Chrome 120.0      892 events (45%)
  Safari 17.2       421 events (21%)
  Firefox 121.0     312 events (16%)
  Edge 120.0        198 events (10%)
  Other             156 events (8%)

sentry-cli traces info <trace-id>

Get trace details including span tree and statistics.

Arguments:

  • trace_id (required) - Trace ID
  • --org - Organization slug

Output:

Trace: abc123def456789

Summary:
  Spans: 47
  Errors: 2
  Performance Issues: 1
  Duration: 1,247ms

Trace Tree:
  ├─ [http.server] GET /api/users (523ms)
  │  ├─ [db.query] SELECT * FROM users (245ms)
  │  ├─ [http.client] GET /auth/validate (189ms)
  │  │  └─ [error] ConnectionTimeout
  │  └─ [cache.get] user:123 (12ms)
  └─ [http.server] POST /api/log (34ms)

Operations:
  db.query      12 spans   avg 89ms   p95 245ms
  http.client    8 spans   avg 67ms   p95 189ms
  cache.get     15 spans   avg  4ms   p95  12ms

sentry-cli events attachment <event-id> [attachment-id]

List or download event attachments.

Arguments:

  • event_id (required) - Event ID
  • attachment_id (optional) - Attachment ID to download
  • --org - Organization slug
  • --project - Project slug (required)
  • --output - Output file path (for download mode)

Output (list mode):

Attachments for event abc123:

  ID          Name                  Type        Size
  ─────────────────────────────────────────────────────
  att_001     screenshot.png        image/png   245 KB
  att_002     console.log           text/plain   12 KB
  att_003     state.json            application/json  3 KB

Output (download mode):

Downloaded: screenshot.png (245 KB)

API Endpoints

All commands use standard Sentry REST API endpoints (same as sentry-mcp):

Command Endpoint(s)
issues info GET /api/0/organizations/{org}/issues/{id}/
GET /api/0/organizations/{org}/issues/{id}/events/latest/
issues events GET /api/0/organizations/{org}/issues/{id}/events/
issues tags GET /api/0/organizations/{org}/issues/{id}/tags/{key}/
traces info GET /api/0/organizations/{org}/trace-meta/{id}/
GET /api/0/organizations/{org}/trace/{id}/
events attachment GET /api/0/projects/{org}/{proj}/events/{id}/attachments/
GET /api/0/projects/{org}/{proj}/events/{id}/attachments/{aid}/?download=1

Implementation

Files to Modify/Create

src/
├── api/
│   └── mod.rs                    # Add 8 API methods, 7 data types
├── commands/
│   ├── mod.rs                    # Register traces command
│   ├── issues/
│   │   ├── mod.rs                # Register info, events, tags
│   │   ├── info.rs               # NEW
│   │   ├── events.rs             # NEW
│   │   └── tags.rs               # NEW
│   ├── traces/
│   │   └── mod.rs                # NEW
│   └── events/
│       ├── mod.rs                # Register attachment
│       └── attachment.rs         # NEW

Data Types

#[derive(Deserialize, Debug)]
pub struct IssueDetails {
    pub id: String,
    pub short_id: String,
    pub title: String,
    pub culprit: Option<String>,
    pub status: String,
    pub level: String,
    pub count: String,
    #[serde(rename = "userCount")]
    pub user_count: u64,
    #[serde(rename = "firstSeen")]
    pub first_seen: DateTime<Utc>,
    #[serde(rename = "lastSeen")]
    pub last_seen: DateTime<Utc>,
    pub permalink: String,
}

#[derive(Deserialize, Debug)]
pub struct IssueEvent {
    #[serde(rename = "eventID")]
    pub event_id: String,
    pub title: String,
    pub timestamp: DateTime<Utc>,
    pub tags: Vec<EventTag>,
}

#[derive(Deserialize, Debug)]
pub struct TagValueDistribution {
    pub key: String,
    pub name: String,
    #[serde(rename = "totalValues")]
    pub total_values: u64,
    #[serde(rename = "topValues")]
    pub top_values: Vec<TagValue>,
}

#[derive(Deserialize, Debug)]
pub struct TraceMeta {
    pub span_count: u64,
    pub errors: u64,
    pub performance_issues: u64,
}

#[derive(Deserialize, Debug)]
pub struct TraceSpan {
    pub event_id: Option<String>,
    pub op: Option<String>,
    pub description: Option<String>,
    pub duration: Option<f64>,
    pub is_transaction: bool,
    pub children: Vec<TraceSpan>,
}

#[derive(Deserialize, Debug)]
pub struct EventAttachment {
    pub id: String,
    pub name: String,
    #[serde(rename = "type")]
    pub attachment_type: String,
    pub size: u64,
    pub mimetype: Option<String>,
}

API Methods

impl AuthenticatedApi<'_> {
    pub fn get_issue(&self, org: &str, issue_id: &str) -> ApiResult<IssueDetails>;
    pub fn get_issue_latest_event(&self, org: &str, issue_id: &str) -> ApiResult<IssueEvent>;
    pub fn list_issue_events(&self, org: &str, issue_id: &str,
        limit: Option<u32>, sort: Option<&str>, period: Option<&str>) -> ApiResult<Vec<IssueEvent>>;
    pub fn get_issue_tag_values(&self, org: &str, issue_id: &str,
        tag_key: &str) -> ApiResult<TagValueDistribution>;
    pub fn get_trace_meta(&self, org: &str, trace_id: &str) -> ApiResult<TraceMeta>;
    pub fn get_trace(&self, org: &str, trace_id: &str) -> ApiResult<Vec<TraceSpan>>;
    pub fn list_event_attachments(&self, org: &str, project: &str,
        event_id: &str) -> ApiResult<Vec<EventAttachment>>;
    pub fn download_event_attachment(&self, org: &str, project: &str,
        event_id: &str, attachment_id: &str) -> ApiResult<Vec<u8>>;
}

Estimate

~650-700 lines of Rust code total.

Follow-up Tasks

  1. Add --json flag - For agent/scripting consumption (output as JSON)
  2. Project management commands - create_project, update_project, find_teams, create_team
  3. DSN management commands - find_dsns, create_dsn

Notes

  • Authentication uses sentry-cli's existing token handling
  • No special MCP headers needed - same REST API endpoints
  • Human-readable output by default, JSON output deferred to follow-up

Implementation Plan

Sentry CLI Debugging Commands Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Add 5 debugging/triage commands to sentry-cli that mirror sentry-mcp functionality.

Architecture: Extend existing issues and events commands, add new traces command. All commands use sentry-cli's existing HTTP/auth infrastructure. API methods added to AuthenticatedApi, data types to api/mod.rs.

Tech Stack: Rust, clap (CLI args), serde (JSON), curl (HTTP), prettytable (output formatting)


Task 1: Add IssueDetails Data Types

Files:

  • Modify: src/api/mod.rs (add after line ~1683, after existing Issue struct)

Step 1: Add the data types

Add after the existing Issue struct (around line 1683):

/// Detailed issue information from the API
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueDetails {
    pub id: String,
    pub short_id: String,
    pub title: String,
    pub culprit: Option<String>,
    pub status: String,
    pub level: String,
    pub count: String,
    pub user_count: u64,
    pub first_seen: DateTime<Utc>,
    pub last_seen: DateTime<Utc>,
    pub permalink: String,
}

/// Latest event for an issue
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueLatestEvent {
    #[serde(alias = "eventID")]
    pub event_id: String,
    pub title: Option<String>,
    #[serde(alias = "dateCreated")]
    pub date_created: Option<String>,
    pub tags: Option<Vec<ProcessedEventTag>>,
}

/// Tag value distribution for an issue
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueTagValues {
    pub key: String,
    pub name: String,
    pub total_values: u64,
    pub top_values: Vec<IssueTagValue>,
}

/// Individual tag value with count
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IssueTagValue {
    pub value: Option<String>,
    pub count: u64,
    pub first_seen: Option<DateTime<Utc>>,
    pub last_seen: Option<DateTime<Utc>>,
}

/// Trace metadata summary
#[derive(Clone, Debug, Deserialize)]
pub struct TraceMeta {
    pub span_count: Option<u64>,
    pub errors: Option<u64>,
    pub performance_issues: Option<u64>,
}

/// Span in a trace tree
#[derive(Clone, Debug, Deserialize)]
pub struct TraceSpan {
    pub event_id: Option<String>,
    pub op: Option<String>,
    pub description: Option<String>,
    #[serde(default)]
    pub duration: Option<f64>,
    #[serde(default)]
    pub is_transaction: bool,
    #[serde(default)]
    pub children: Vec<TraceSpan>,
    #[serde(default)]
    pub errors: Vec<serde_json::Value>,
}

/// Event attachment metadata
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventAttachment {
    pub id: String,
    pub name: String,
    #[serde(rename = "type")]
    pub attachment_type: String,
    pub size: u64,
    pub mimetype: Option<String>,
    pub date_created: Option<DateTime<Utc>>,
}

Step 2: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 3: Format code

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt

Step 4: Commit

cd /Users/haza/Projects/climcp/sentry-cli && git add src/api/mod.rs && git commit -m "$(cat <<'EOF'
feat(api): add data types for debugging commands

Add IssueDetails, IssueLatestEvent, IssueTagValues, TraceMeta,
TraceSpan, and EventAttachment structs for the new debugging
CLI commands.
EOF
)"

Task 2: Add API Methods for Issues

Files:

  • Modify: src/api/mod.rs (add methods to AuthenticatedApi impl block, around line 942)

Step 1: Add API methods

Add these methods inside impl AuthenticatedApi<'_> (after list_organization_project_issues):

    /// Get detailed information about a specific issue
    pub fn get_issue_details(&self, org: &str, issue_id: &str) -> ApiResult<IssueDetails> {
        let path = format!(
            "/organizations/{}/issues/{}/",
            PathArg(org),
            PathArg(issue_id)
        );
        self.get(&path)?.convert_rnf(ApiErrorKind::ResourceNotFound)
    }

    /// Get the latest event for an issue
    pub fn get_issue_latest_event(
        &self,
        org: &str,
        issue_id: &str,
    ) -> ApiResult<Option<IssueLatestEvent>> {
        let path = format!(
            "/organizations/{}/issues/{}/events/latest/",
            PathArg(org),
            PathArg(issue_id)
        );
        let resp = self.get(&path)?;
        if resp.status() == 404 {
            Ok(None)
        } else {
            resp.convert()
        }
    }

    /// List events for a specific issue
    pub fn list_issue_events(
        &self,
        org: &str,
        issue_id: &str,
        limit: Option<usize>,
        sort: Option<&str>,
        stats_period: Option<&str>,
    ) -> ApiResult<Vec<IssueLatestEvent>> {
        let limit = limit.unwrap_or(50);
        let sort = sort.unwrap_or("-timestamp");
        let stats_period = stats_period.unwrap_or("14d");
        let path = format!(
            "/organizations/{}/issues/{}/events/?per_page={}&sort={}&statsPeriod={}",
            PathArg(org),
            PathArg(issue_id),
            limit,
            QueryArg(sort),
            QueryArg(stats_period)
        );
        self.get(&path)?.convert_rnf(ApiErrorKind::ResourceNotFound)
    }

    /// Get tag value distribution for an issue
    pub fn get_issue_tag_values(
        &self,
        org: &str,
        issue_id: &str,
        tag_key: &str,
    ) -> ApiResult<IssueTagValues> {
        let path = format!(
            "/organizations/{}/issues/{}/tags/{}/",
            PathArg(org),
            PathArg(issue_id),
            PathArg(tag_key)
        );
        self.get(&path)?.convert_rnf(ApiErrorKind::ResourceNotFound)
    }

Step 2: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 3: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/api/mod.rs && git commit -m "$(cat <<'EOF'
feat(api): add issue debugging API methods

Add get_issue_details, get_issue_latest_event, list_issue_events,
and get_issue_tag_values methods to AuthenticatedApi.
EOF
)"

Task 3: Add API Methods for Traces and Attachments

Files:

  • Modify: src/api/mod.rs (continue adding to AuthenticatedApi impl)

Step 1: Add trace and attachment API methods

Add these methods inside impl AuthenticatedApi<'_>:

    /// Get trace metadata
    pub fn get_trace_meta(&self, org: &str, trace_id: &str) -> ApiResult<TraceMeta> {
        let path = format!(
            "/organizations/{}/events-trace-meta/{}/",
            PathArg(org),
            PathArg(trace_id)
        );
        self.get(&path)?.convert_rnf(ApiErrorKind::ResourceNotFound)
    }

    /// Get trace span tree
    pub fn get_trace(&self, org: &str, trace_id: &str) -> ApiResult<Vec<TraceSpan>> {
        let path = format!(
            "/organizations/{}/events-trace/{}/?limit=100&statsPeriod=14d",
            PathArg(org),
            PathArg(trace_id)
        );
        self.get(&path)?.convert_rnf(ApiErrorKind::ResourceNotFound)
    }

    /// List attachments for an event
    pub fn list_event_attachments(
        &self,
        org: &str,
        project: &str,
        event_id: &str,
    ) -> ApiResult<Vec<EventAttachment>> {
        let path = format!(
            "/projects/{}/{}/events/{}/attachments/",
            PathArg(org),
            PathArg(project),
            PathArg(event_id)
        );
        self.get(&path)?.convert_rnf(ApiErrorKind::ProjectNotFound)
    }

    /// Download a specific attachment
    pub fn download_event_attachment(
        &self,
        org: &str,
        project: &str,
        event_id: &str,
        attachment_id: &str,
    ) -> ApiResult<Vec<u8>> {
        let path = format!(
            "/projects/{}/{}/events/{}/attachments/{}/?download=1",
            PathArg(org),
            PathArg(project),
            PathArg(event_id),
            PathArg(attachment_id)
        );
        let resp = self.get(&path)?;
        if resp.status() == 404 {
            return Err(ApiErrorKind::ResourceNotFound.into());
        }
        resp.into_result()?;
        Ok(resp.body.unwrap_or_default())
    }

Step 2: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 3: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/api/mod.rs && git commit -m "$(cat <<'EOF'
feat(api): add trace and attachment API methods

Add get_trace_meta, get_trace, list_event_attachments, and
download_event_attachment methods to AuthenticatedApi.
EOF
)"

Task 4: Create issues info Command

Files:

  • Create: src/commands/issues/info.rs
  • Modify: src/commands/issues/mod.rs

Step 1: Create the info command file

Create src/commands/issues/info.rs:

use anyhow::Result;
use clap::{Arg, ArgMatches, Command};

use crate::api::Api;
use crate::config::Config;

pub fn make_command(command: Command) -> Command {
    command
        .about("Get detailed information about a specific issue.")
        .arg(
            Arg::new("issue_id")
                .required(true)
                .value_name("ISSUE_ID")
                .help("The issue ID (e.g., PROJ-123 or full UUID)."),
        )
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
    let config = Config::current();
    let org = config.get_org(matches)?;
    let issue_id = matches.get_one::<String>("issue_id").unwrap();

    let api = Api::current();
    let authenticated = api.authenticated()?;

    let issue = authenticated.get_issue_details(&org, issue_id)?;
    let latest_event = authenticated.get_issue_latest_event(&org, issue_id).ok().flatten();

    println!("Issue: {}", issue.short_id);
    println!("Title: {}", issue.title);
    if let Some(culprit) = &issue.culprit {
        println!("Culprit: {}", culprit);
    }
    println!("Status: {}", issue.status);
    println!("Level: {}", issue.level);
    println!("Events: {}", issue.count);
    println!("Users: {}", issue.user_count);
    println!("First Seen: {}", issue.first_seen);
    println!("Last Seen: {}", issue.last_seen);
    println!("Link: {}", issue.permalink);

    if let Some(event) = latest_event {
        println!();
        println!("Latest Event: {}", event.event_id);
        if let Some(date) = &event.date_created {
            println!("  Timestamp: {}", date);
        }
        if let Some(tags) = &event.tags {
            for tag in tags {
                if tag.key == "environment" || tag.key == "release" {
                    println!("  {}: {}", tag.key, tag.value);
                }
            }
        }
    }

    Ok(())
}

Step 2: Register the command in mod.rs

Modify src/commands/issues/mod.rs:

Add to the module declarations (after line 9):

pub mod info;

Update the each_subcommand! macro (around line 11-17) to include info:

macro_rules! each_subcommand {
    ($mac:ident) => {
        $mac!(info);
        $mac!(list);
        $mac!(mute);
        $mac!(resolve);
        $mac!(unresolve);
    };
}

Step 3: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 4: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/commands/issues/ && git commit -m "$(cat <<'EOF'
feat(cli): add issues info command

Add 'sentry-cli issues info <issue-id>' command to get detailed
information about a specific issue including the latest event.
EOF
)"

Task 5: Create issues events Command

Files:

  • Create: src/commands/issues/events.rs
  • Modify: src/commands/issues/mod.rs

Step 1: Create the events command file

Create src/commands/issues/events.rs:

use anyhow::Result;
use clap::{Arg, ArgMatches, Command};

use crate::api::Api;
use crate::config::Config;
use crate::utils::formatting::Table;

pub fn make_command(command: Command) -> Command {
    command
        .about("List events for a specific issue.")
        .arg(
            Arg::new("issue_id")
                .required(true)
                .value_name("ISSUE_ID")
                .help("The issue ID (e.g., PROJ-123 or full UUID)."),
        )
        .arg(
            Arg::new("limit")
                .long("limit")
                .short('l')
                .value_name("LIMIT")
                .default_value("50")
                .value_parser(clap::value_parser!(usize))
                .help("Maximum number of events to return."),
        )
        .arg(
            Arg::new("sort")
                .long("sort")
                .value_name("SORT")
                .default_value("-timestamp")
                .help("Sort field (e.g., -timestamp, timestamp)."),
        )
        .arg(
            Arg::new("period")
                .long("period")
                .value_name("PERIOD")
                .default_value("14d")
                .help("Time period (e.g., 24h, 7d, 14d)."),
        )
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
    let config = Config::current();
    let org = config.get_org(matches)?;
    let issue_id = matches.get_one::<String>("issue_id").unwrap();
    let limit = *matches.get_one::<usize>("limit").unwrap();
    let sort = matches.get_one::<String>("sort").map(|s| s.as_str());
    let period = matches.get_one::<String>("period").map(|s| s.as_str());

    let api = Api::current();
    let events = api
        .authenticated()?
        .list_issue_events(&org, issue_id, Some(limit), sort, period)?;

    if events.is_empty() {
        println!("No events found for issue {}", issue_id);
        return Ok(());
    }

    println!("Events for {} (showing {}):", issue_id, events.len());
    println!();

    let mut table = Table::new();
    table.title_row().add("Event ID").add("Timestamp").add("Environment").add("Release");

    for event in &events {
        let env = event
            .tags
            .as_ref()
            .and_then(|tags| tags.iter().find(|t| t.key == "environment"))
            .map(|t| t.value.as_str())
            .unwrap_or("-");
        let release = event
            .tags
            .as_ref()
            .and_then(|tags| tags.iter().find(|t| t.key == "release"))
            .map(|t| t.value.as_str())
            .unwrap_or("-");
        let timestamp = event.date_created.as_deref().unwrap_or("-");

        table
            .add_row()
            .add(&event.event_id)
            .add(timestamp)
            .add(env)
            .add(release);
    }

    table.print();

    Ok(())
}

Step 2: Register the command in mod.rs

Modify src/commands/issues/mod.rs - add to module declarations:

pub mod events;

Update the each_subcommand! macro:

macro_rules! each_subcommand {
    ($mac:ident) => {
        $mac!(events);
        $mac!(info);
        $mac!(list);
        $mac!(mute);
        $mac!(resolve);
        $mac!(unresolve);
    };
}

Step 3: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 4: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/commands/issues/ && git commit -m "$(cat <<'EOF'
feat(cli): add issues events command

Add 'sentry-cli issues events <issue-id>' command to list events
within a specific issue with filtering options.
EOF
)"

Task 6: Create issues tags Command

Files:

  • Create: src/commands/issues/tags.rs
  • Modify: src/commands/issues/mod.rs

Step 1: Create the tags command file

Create src/commands/issues/tags.rs:

use anyhow::Result;
use clap::{Arg, ArgMatches, Command};

use crate::api::Api;
use crate::config::Config;

pub fn make_command(command: Command) -> Command {
    command
        .about("Get tag value distribution for an issue.")
        .arg(
            Arg::new("issue_id")
                .required(true)
                .value_name("ISSUE_ID")
                .help("The issue ID (e.g., PROJ-123 or full UUID)."),
        )
        .arg(
            Arg::new("key")
                .long("key")
                .short('k')
                .required(true)
                .value_name("TAG_KEY")
                .help("The tag key to get values for (e.g., browser, os, environment)."),
        )
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
    let config = Config::current();
    let org = config.get_org(matches)?;
    let issue_id = matches.get_one::<String>("issue_id").unwrap();
    let tag_key = matches.get_one::<String>("key").unwrap();

    let api = Api::current();
    let tag_values = api
        .authenticated()?
        .get_issue_tag_values(&org, issue_id, tag_key)?;

    println!("Tag: {} ({} unique values)", tag_values.name, tag_values.total_values);
    println!();

    if tag_values.top_values.is_empty() {
        println!("No values found for tag '{}'", tag_key);
        return Ok(());
    }

    // Calculate total for percentage
    let total: u64 = tag_values.top_values.iter().map(|v| v.count).sum();

    for value in &tag_values.top_values {
        let display_value = value.value.as_deref().unwrap_or("(none)");
        let percentage = if total > 0 {
            (value.count as f64 / total as f64) * 100.0
        } else {
            0.0
        };
        println!(
            "  {:30} {:>8} events ({:.0}%)",
            display_value, value.count, percentage
        );
    }

    Ok(())
}

Step 2: Register the command in mod.rs

Modify src/commands/issues/mod.rs - add to module declarations:

pub mod tags;

Update the each_subcommand! macro:

macro_rules! each_subcommand {
    ($mac:ident) => {
        $mac!(events);
        $mac!(info);
        $mac!(list);
        $mac!(mute);
        $mac!(resolve);
        $mac!(tags);
        $mac!(unresolve);
    };
}

Step 3: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 4: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/commands/issues/ && git commit -m "$(cat <<'EOF'
feat(cli): add issues tags command

Add 'sentry-cli issues tags <issue-id> --key <tag>' command to get
tag value distribution for a specific issue.
EOF
)"

Task 7: Create traces Command

Files:

  • Create: src/commands/traces/mod.rs
  • Modify: src/commands/mod.rs

Step 1: Create the traces command directory and file

Create src/commands/traces/mod.rs:

use anyhow::Result;
use clap::{Arg, ArgMatches, Command};

use crate::api::{TraceSpan, Api};
use crate::config::Config;
use crate::utils::args::ArgExt as _;

pub fn make_command(command: Command) -> Command {
    command
        .about("Manage traces in Sentry.")
        .subcommand_required(true)
        .arg_required_else_help(true)
        .org_arg()
        .subcommand(
            Command::new("info")
                .about("Get detailed information about a trace.")
                .arg(
                    Arg::new("trace_id")
                        .required(true)
                        .value_name("TRACE_ID")
                        .help("The trace ID."),
                ),
        )
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
    if let Some(sub_matches) = matches.subcommand_matches("info") {
        return execute_info(sub_matches);
    }
    unreachable!();
}

fn execute_info(matches: &ArgMatches) -> Result<()> {
    let config = Config::current();
    let org = config.get_org(matches)?;
    let trace_id = matches.get_one::<String>("trace_id").unwrap();

    let api = Api::current();
    let authenticated = api.authenticated()?;

    let meta = authenticated.get_trace_meta(&org, trace_id)?;
    let spans = authenticated.get_trace(&org, trace_id)?;

    println!("Trace: {}", trace_id);
    println!();
    println!("Summary:");
    println!("  Spans: {}", meta.span_count.unwrap_or(0));
    println!("  Errors: {}", meta.errors.unwrap_or(0));
    println!("  Performance Issues: {}", meta.performance_issues.unwrap_or(0));

    if !spans.is_empty() {
        println!();
        println!("Trace Tree:");
        for span in &spans {
            print_span(span, 0);
        }
    }

    Ok(())
}

fn print_span(span: &TraceSpan, depth: usize) {
    let indent = "  ".repeat(depth);
    let prefix = if depth == 0 { "─" } else { "├─" };

    let op = span.op.as_deref().unwrap_or("unknown");
    let desc = span.description.as_deref().unwrap_or("");
    let duration = span
        .duration
        .map(|d| format!(" ({:.0}ms)", d))
        .unwrap_or_default();

    let has_error = !span.errors.is_empty();
    let error_marker = if has_error { " [error]" } else { "" };

    println!("{}{} [{}] {}{}{}", indent, prefix, op, desc, duration, error_marker);

    for child in &span.children {
        print_span(child, depth + 1);
    }
}

Step 2: Register traces in main commands mod.rs

Modify src/commands/mod.rs:

Add to module declarations (around line 31, alphabetically near other modules):

mod traces;

Update the each_subcommand! macro to include traces (around line 50-78):

macro_rules! each_subcommand {
    ($mac:ident) => {
        $mac!(bash_hook);
        $mac!(build);
        $mac!(debug_files);
        $mac!(deploys);
        $mac!(events);
        $mac!(info);
        $mac!(issues);
        $mac!(login);
        $mac!(logs);
        $mac!(monitors);
        $mac!(organizations);
        $mac!(projects);
        $mac!(react_native);
        $mac!(releases);
        $mac!(repos);
        $mac!(send_event);
        $mac!(send_envelope);
        $mac!(sourcemaps);
        $mac!(traces);
        $mac!(dart_symbol_map);
        #[cfg(not(feature = "managed"))]
        $mac!(uninstall);
        #[cfg(not(feature = "managed"))]
        $mac!(update);
        $mac!(upload_dif);
        $mac!(upload_dsym);
        $mac!(upload_proguard);
    };
}

Step 3: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 4: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/commands/traces/ src/commands/mod.rs && git commit -m "$(cat <<'EOF'
feat(cli): add traces info command

Add 'sentry-cli traces info <trace-id>' command to get trace
details including span tree and error information.
EOF
)"

Task 8: Create events attachment Command

Files:

  • Create: src/commands/events/attachment.rs
  • Modify: src/commands/events/mod.rs

Step 1: Create the attachment command file

Create src/commands/events/attachment.rs:

use std::fs::File;
use std::io::Write;
use std::path::Path;

use anyhow::{bail, Result};
use clap::{Arg, ArgMatches, Command};

use crate::api::Api;
use crate::config::Config;
use crate::utils::formatting::Table;

pub fn make_command(command: Command) -> Command {
    command
        .about("List or download attachments for an event.")
        .arg(
            Arg::new("event_id")
                .required(true)
                .value_name("EVENT_ID")
                .help("The event ID."),
        )
        .arg(
            Arg::new("attachment_id")
                .value_name("ATTACHMENT_ID")
                .help("The attachment ID to download (optional, lists all if omitted)."),
        )
        .arg(
            Arg::new("output")
                .long("output")
                .short('o')
                .value_name("PATH")
                .help("Output file path for download (required when downloading)."),
        )
}

pub fn execute(matches: &ArgMatches) -> Result<()> {
    let config = Config::current();
    let org = config.get_org(matches)?;
    let project = config.get_project(matches)?;
    let event_id = matches.get_one::<String>("event_id").unwrap();
    let attachment_id = matches.get_one::<String>("attachment_id");
    let output_path = matches.get_one::<String>("output");

    let api = Api::current();
    let authenticated = api.authenticated()?;

    match attachment_id {
        None => {
            // List mode
            let attachments = authenticated.list_event_attachments(&org, &project, event_id)?;

            if attachments.is_empty() {
                println!("No attachments found for event {}", event_id);
                return Ok(());
            }

            println!("Attachments for event {}:", event_id);
            println!();

            let mut table = Table::new();
            table.title_row().add("ID").add("Name").add("Type").add("Size");

            for att in &attachments {
                let size = format_size(att.size);
                table
                    .add_row()
                    .add(&att.id)
                    .add(&att.name)
                    .add(att.mimetype.as_deref().unwrap_or(&att.attachment_type))
                    .add(&size);
            }

            table.print();
        }
        Some(att_id) => {
            // Download mode
            let output = match output_path {
                Some(p) => p.clone(),
                None => bail!("--output is required when downloading an attachment"),
            };

            let data = authenticated.download_event_attachment(&org, &project, event_id, att_id)?;

            let path = Path::new(&output);
            let mut file = File::create(path)?;
            file.write_all(&data)?;

            println!("Downloaded: {} ({})", output, format_size(data.len() as u64));
        }
    }

    Ok(())
}

fn format_size(bytes: u64) -> String {
    if bytes >= 1024 * 1024 {
        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
    } else if bytes >= 1024 {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    } else {
        format!("{} B", bytes)
    }
}

Step 2: Register the command in mod.rs

Modify src/commands/events/mod.rs:

Add to module declarations:

pub mod attachment;

Update the each_subcommand! macro:

macro_rules! each_subcommand {
    ($mac:ident) => {
        $mac!(attachment);
        $mac!(list);
    };
}

Step 3: Verify it compiles

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo check
Expected: Compiles without errors

Step 4: Format and commit

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add src/commands/events/ && git commit -m "$(cat <<'EOF'
feat(cli): add events attachment command

Add 'sentry-cli events attachment <event-id> [attachment-id]' command
to list or download event attachments.
EOF
)"

Task 9: Test All Commands

Step 1: Build the CLI

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo build
Expected: Build succeeds

Step 2: Test help output for each command

Run these commands and verify help text appears:

cd /Users/haza/Projects/climcp/sentry-cli
./target/debug/sentry-cli issues info --help
./target/debug/sentry-cli issues events --help
./target/debug/sentry-cli issues tags --help
./target/debug/sentry-cli traces info --help
./target/debug/sentry-cli events attachment --help

Expected: Each command shows its help text with arguments and options

Step 3: Run clippy

Run: cd /Users/haza/Projects/climcp/sentry-cli && cargo clippy
Expected: No warnings (fix any that appear)

Step 4: Final commit if any fixes

If clippy required fixes:

cd /Users/haza/Projects/climcp/sentry-cli && cargo fmt && git add -A && git commit -m "fix: address clippy warnings"

Summary

After completing all tasks, the following commands will be available:

Command Description
sentry-cli issues info <id> Get detailed issue information
sentry-cli issues events <id> List events for an issue
sentry-cli issues tags <id> --key <key> Get tag value distribution
sentry-cli traces info <id> Get trace details and span tree
sentry-cli events attachment <id> [att-id] List or download attachments

Total files created: 5
Total files modified: 3
Estimated lines of code: ~650

HazAT added 8 commits January 15, 2026 10:42
Add IssueDetails, IssueLatestEvent, IssueTagValues, TraceMeta,
TraceSpan, and EventAttachment structs for the new debugging
CLI commands.
Add get_issue_details, get_issue_latest_event, list_issue_events,
and get_issue_tag_values methods to AuthenticatedApi.
Add get_trace_meta, get_trace, list_event_attachments, and
download_event_attachment methods to AuthenticatedApi.
Add 'sentry-cli issues info <issue-id>' command to get detailed
information about a specific issue including the latest event.
Add 'sentry-cli issues events <issue-id>' command to list events
within a specific issue with filtering options.
Add 'sentry-cli issues tags <issue-id> --key <tag>' command to get
tag value distribution for a specific issue.
Add 'sentry-cli traces info <trace-id>' command to get trace
details including span tree and error information.
Add 'sentry-cli events attachment <event-id> [attachment-id]' command
to list or download event attachments.
@HazAT HazAT requested review from a team and szokeasaurusrex as code owners January 15, 2026 10:17
@github-actions
Copy link
Contributor

github-actions bot commented Jan 15, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against a25dd3b

@HazAT HazAT marked this pull request as draft January 15, 2026 10:32
@HazAT
Copy link
Member Author

HazAT commented Jan 15, 2026

We gonna close this - it was a POC how much it takes to get this running but we want to focus on making our MCP to be the interface agents interface with

@HazAT HazAT closed this Jan 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants