Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- (events) Add `events dropped` command to summarize dropped events by outcome and reason ([#3431](https://github.com/getsentry/sentry-cli/pull/3431))
- (events) Add `--json` output to `events dropped` ([#3432](https://github.com/getsentry/sentry-cli/pull/3432))

## 3.8.0

Expand Down
68 changes: 53 additions & 15 deletions src/commands/events/dropped.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::collections::BTreeMap;
use std::io;

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

use crate::api::{outcome_label, Api, DroppedEventsBucket};
use crate::config::Config;
Expand All @@ -27,54 +29,83 @@ pub fn make_command(command: Command) -> Command {
.default_value("90d")
.help("Time range to summarize (e.g. 24h, 14d, 90d)."),
)
.arg(
Arg::new("json")
.long("json")
.action(ArgAction::SetTrue)
.help("Output the summary as JSON (for agents/automation)."),
)
}

#[derive(Serialize)]
struct ReasonRow {
reason: String,
category: String,
events: u64,
share: f64,
}

#[derive(Serialize)]
struct OutcomeSection {
outcome: String,
label: String,
events: u64,
share: f64,
reasons: Vec<ReasonRow>,
}

fn format_share(events: u64, total: u64) -> String {
fn share_ratio(events: u64, total: u64) -> f64 {
if total == 0 {
return "0%".to_owned();
0.0
} else {
events as f64 / total as f64
}
let ratio = events as f64 / total as f64;
}

fn format_share(ratio: f64) -> String {
if ratio > 0.0 && ratio < 0.0001 {
return "<0.01%".to_owned();
}
format!("{:.2}%", ratio * 100.0)
}

fn to_sections(dropped: &[DroppedEventsBucket]) -> Vec<OutcomeSection> {
let mut by_outcome: BTreeMap<String, BTreeMap<String, ReasonRow>> = BTreeMap::new();
struct ReasonTotals {
category: String,
events: u64,
}

fn to_sections(dropped: &[DroppedEventsBucket], total: u64) -> Vec<OutcomeSection> {
let mut by_outcome: BTreeMap<String, BTreeMap<String, ReasonTotals>> = BTreeMap::new();
for bucket in dropped {
let reasons = by_outcome.entry(bucket.outcome.clone()).or_default();
let row = reasons
let totals = reasons
.entry(bucket.reason.clone())
.or_insert_with(|| ReasonRow {
reason: bucket.reason.clone(),
.or_insert_with(|| ReasonTotals {
category: bucket.category.clone(),
events: 0,
});
row.events += bucket.count as u64;
totals.events += bucket.count as u64;
}

let mut sections: Vec<OutcomeSection> = by_outcome
.into_iter()
.map(|(outcome, reasons)| {
let mut reasons: Vec<ReasonRow> = reasons.into_values().collect();
let mut reasons: Vec<ReasonRow> = reasons
.into_iter()
.map(|(reason, t)| ReasonRow {
reason,
category: t.category,
events: t.events,
share: share_ratio(t.events, total),
})
.collect();
reasons.sort_by_key(|r| std::cmp::Reverse(r.events));
let events = reasons.iter().map(|r| r.events).sum();
OutcomeSection {
label: outcome_label(&outcome).to_owned(),
outcome,
events,
share: share_ratio(events, total),
reasons,
}
})
Expand Down Expand Up @@ -107,7 +138,14 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
.map(|b| b.count as u64)
.sum();

let sections = to_sections(&response.dropped_events);
let sections = to_sections(&response.dropped_events, total);

if matches.get_flag("json") {
serde_json::to_writer_pretty(&mut io::stdout(), &sections)?;
println!();
return Ok(());
}

if sections.is_empty() {
println!("No dropped events found");
return Ok(());
Expand All @@ -116,9 +154,9 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
for section in &sections {
println!(
"\n{}: {} dropped ({})",
outcome_label(&section.outcome),
section.label,
section.events,
format_share(section.events, total),
format_share(section.share),
);

let mut table = Table::new();
Expand All @@ -134,7 +172,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
.add(&row.reason)
.add(&row.category)
.add(row.events)
.add(format_share(row.events, total));
.add(format_share(row.share));
}
table.print();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
```
$ sentry-cli events dropped --org wat-org --project wat-project --json
? success
[
{
"outcome": "client_discard",
"label": "Client Discard",
"events": 100,
"share": 0.1,
"reasons": [
{
"reason": "sample_rate",
"category": "error",
"events": 100,
"share": 0.1
}
]
},
{
"outcome": "rate_limited",
"label": "Rate Limited",
"events": 17,
"share": 0.017,
"reasons": [
{
"reason": "spike_protection",
"category": "error",
"events": 15,
"share": 0.015
},
{
"reason": "usage_exceeded",
"category": "error",
"events": 2,
"share": 0.002
}
]
},
{
"outcome": "filtered",
"label": "Inbound Filter",
"events": 3,
"share": 0.003,
"reasons": [
{
"reason": "web-crawlers",
"category": "error",
"events": 3,
"share": 0.003
}
]
}
]

```
14 changes: 14 additions & 0 deletions tests/integration/events_dropped.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ fn command_events_dropped_project_id() {
.with_default_token();
}

#[test]
fn command_events_dropped_json() {
TestManager::new()
.mock_endpoint(
MockEndpointBuilder::new(
"GET",
"/api/0/organizations/wat-org/events-dropped/?projectSlug=wat-project&dataset=errors&statsPeriod=90d",
)
.with_response_file("events/get-dropped.json"),
)
.register_trycmd_test("events_dropped/events-dropped-json.trycmd")
.with_default_token();
}

#[test]
fn command_events_dropped_empty() {
TestManager::new()
Expand Down
Loading