Skip to content

Commit cba96da

Browse files
authored
feat(outcomes): add item type to invalid outcome (#4558)
This PR adds the ItemType to the TooLarge outcome. Fixes getsentry/team-ingest#635
1 parent da4d0b2 commit cba96da

File tree

7 files changed

+195
-22
lines changed

7 files changed

+195
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
- Add experimental playstation endpoint. ([#4555](https://github.com/getsentry/relay/pull/4555))
88
- Add naver.me / Yeti spider on the crawler filter list. ([#4602](https://github.com/getsentry/relay/pull/4602))
9+
- Add the item type that made the envelope too large to invalid outcomes. ([#4558](https://github.com/getsentry/relay/pull/4558))
910

1011
**Bug Fixes**:
1112

relay-server/src/endpoints/common.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,9 @@ pub async fn handle_envelope(
383383
if let Err(offender) =
384384
utils::check_envelope_size_limits(state.config(), managed_envelope.envelope())
385385
{
386-
managed_envelope.reject(Outcome::Invalid(DiscardReason::TooLarge));
386+
managed_envelope.reject(Outcome::Invalid(DiscardReason::TooLarge(
387+
(&offender).into(),
388+
)));
387389
return Err(BadStoreRequest::Overflow(offender));
388390
}
389391

relay-server/src/services/outcome.rs

Lines changed: 175 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ use std::sync::Arc;
1212
use std::time::Duration;
1313
use std::{fmt, mem};
1414

15+
use crate::envelope::ItemType;
16+
#[cfg(feature = "processing")]
17+
use crate::service::ServiceError;
18+
use crate::services::processor::{EnvelopeProcessor, SubmitClientReports};
19+
use crate::services::upstream::{Method, SendQuery, UpstreamQuery, UpstreamRelay};
20+
use crate::statsd::RelayCounters;
21+
use crate::utils::SleepHandle;
1522
use chrono::{DateTime, SecondsFormat, Utc};
1623
use relay_base_schema::organization::OrganizationId;
1724
use relay_base_schema::project::ProjectId;
@@ -29,13 +36,6 @@ use relay_statsd::metric;
2936
use relay_system::{Addr, FromMessage, Interface, NoResponse, Service};
3037
use serde::{Deserialize, Serialize};
3138

32-
#[cfg(feature = "processing")]
33-
use crate::service::ServiceError;
34-
use crate::services::processor::{EnvelopeProcessor, SubmitClientReports};
35-
use crate::services::upstream::{Method, SendQuery, UpstreamQuery, UpstreamRelay};
36-
use crate::statsd::RelayCounters;
37-
use crate::utils::SleepHandle;
38-
3939
/// Defines the structure of the HTTP outcomes requests
4040
#[derive(Debug, Default, Deserialize, Serialize)]
4141
pub struct SendOutcomes {
@@ -205,6 +205,9 @@ impl Outcome {
205205
/// Returns the `reason` code field of this outcome.
206206
pub fn to_reason(&self) -> Option<Cow<'_, str>> {
207207
match self {
208+
Outcome::Invalid(DiscardReason::TooLarge(too_large_reason)) => Some(Cow::Owned(
209+
format!("too_large:{}", too_large_reason.as_str()),
210+
)),
208211
Outcome::Invalid(discard_reason) => Some(Cow::Borrowed(discard_reason.name())),
209212
Outcome::Filtered(filter_key) => Some(filter_key.clone().name()),
210213
Outcome::FilteredSampling(rule_ids) => Some(Cow::Owned(format!("Sampled:{rule_ids}"))),
@@ -356,7 +359,7 @@ pub enum DiscardReason {
356359
EmptyEnvelope,
357360

358361
/// (Relay) The event payload exceeds the maximum size limit for the respective endpoint.
359-
TooLarge,
362+
TooLarge(DiscardItemType),
360363

361364
/// (Legacy) A store request was received with an invalid method.
362365
///
@@ -476,7 +479,7 @@ impl DiscardReason {
476479
DiscardReason::AuthVersion => "auth_version",
477480
DiscardReason::AuthClient => "auth_client",
478481
DiscardReason::NoData => "no_data",
479-
DiscardReason::TooLarge => "too_large",
482+
DiscardReason::TooLarge(discard) => discard.as_str(),
480483
DiscardReason::DisallowedMethod => "disallowed_method",
481484
DiscardReason::ContentType => "content_type",
482485
DiscardReason::MultiProjectId => "multi_project_id",
@@ -524,6 +527,149 @@ impl fmt::Display for DiscardReason {
524527
}
525528
}
526529

530+
/// Similar to [`ItemType`] but it does not have any additional information in the
531+
/// Unknown variant so that it can derive [`Copy`] and be used from [`DiscardReason`].
532+
/// The variants should be the same as [`ItemType`].
533+
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
534+
pub enum DiscardItemType {
535+
/// Event payload encoded in JSON.
536+
Event,
537+
/// Transaction event payload encoded in JSON.
538+
Transaction,
539+
/// Security report event payload encoded in JSON.
540+
Security,
541+
/// Raw payload of an arbitrary attachment.
542+
Attachment,
543+
/// Multipart form data collected into a stream of JSON tuples.
544+
FormData,
545+
/// Security report as sent by the browser in JSON.
546+
RawSecurity,
547+
/// NEL report as sent by the browser.
548+
Nel,
549+
/// Raw compressed UE4 crash report.
550+
UnrealReport,
551+
/// User feedback encoded as JSON.
552+
UserReport,
553+
/// Session update data.
554+
Session,
555+
/// Aggregated session data.
556+
Sessions,
557+
/// Individual metrics in text encoding.
558+
Statsd,
559+
/// Buckets of preaggregated metrics encoded as JSON.
560+
MetricBuckets,
561+
/// Client internal report (eg: outcomes).
562+
ClientReport,
563+
/// Profile event payload encoded as JSON.
564+
Profile,
565+
/// Replay metadata and breadcrumb payload.
566+
ReplayEvent,
567+
/// Replay Recording data.
568+
ReplayRecording,
569+
/// Replay Video data.
570+
ReplayVideo,
571+
/// Monitor check-in encoded as JSON.
572+
CheckIn,
573+
/// A log from the [OTEL Log format](https://opentelemetry.io/docs/specs/otel/logs/data-model/#log-and-event-record-definition)
574+
OtelLog,
575+
/// A log for the log product, not internal logs.
576+
Log,
577+
/// A standalone span.
578+
Span,
579+
/// A standalone OpenTelemetry span serialized as JSON.
580+
OtelSpan,
581+
/// An OTLP TracesData container.
582+
OtelTracesData,
583+
/// UserReport as an Event
584+
UserReportV2,
585+
/// ProfileChunk is a chunk of a profiling session.
586+
ProfileChunk,
587+
/// A new item type that is yet unknown by this version of Relay.
588+
///
589+
/// By default, items of this type are forwarded without modification. Processing Relays and
590+
/// Relays explicitly configured to do so will instead drop those items. This allows
591+
/// forward-compatibility with new item types where we expect outdated Relays.
592+
Unknown,
593+
// Keep `Unknown` last in the list. Add new items above `Unknown`.
594+
}
595+
596+
impl DiscardItemType {
597+
/// Returns the enum variant as string representation in snake case.
598+
///
599+
/// For example, `DiscardItemType::UserReportV2` gets converted into `user_report_v2`.
600+
pub fn as_str(&self) -> &'static str {
601+
match self {
602+
Self::Unknown => "unknown",
603+
Self::Event => "event",
604+
Self::Transaction => "transaction",
605+
Self::Security => "security",
606+
Self::Attachment => "attachment",
607+
Self::FormData => "form_data",
608+
Self::RawSecurity => "raw_security",
609+
Self::Nel => "nel",
610+
Self::UnrealReport => "unreal_report",
611+
Self::UserReport => "user_report",
612+
Self::Session => "session",
613+
Self::Sessions => "sessions",
614+
Self::Statsd => "statsd",
615+
Self::MetricBuckets => "metric_buckets",
616+
Self::ClientReport => "client_report",
617+
Self::Profile => "profile",
618+
Self::ReplayEvent => "replay_event",
619+
Self::ReplayRecording => "replay_recording",
620+
Self::ReplayVideo => "replay_video",
621+
Self::CheckIn => "check_in",
622+
Self::OtelLog => "otel_log",
623+
Self::Log => "log",
624+
Self::Span => "span",
625+
Self::OtelSpan => "otel_span",
626+
Self::OtelTracesData => "otel_traces_data",
627+
Self::UserReportV2 => "user_report_v2",
628+
Self::ProfileChunk => "profile_chunk",
629+
}
630+
}
631+
}
632+
633+
impl From<&ItemType> for DiscardItemType {
634+
fn from(value: &ItemType) -> Self {
635+
match value {
636+
ItemType::Event => DiscardItemType::Event,
637+
ItemType::Transaction => DiscardItemType::Transaction,
638+
ItemType::Security => DiscardItemType::Security,
639+
ItemType::Attachment => DiscardItemType::Attachment,
640+
ItemType::FormData => DiscardItemType::FormData,
641+
ItemType::RawSecurity => DiscardItemType::RawSecurity,
642+
ItemType::Nel => DiscardItemType::Nel,
643+
ItemType::UnrealReport => DiscardItemType::UnrealReport,
644+
ItemType::UserReport => DiscardItemType::UserReport,
645+
ItemType::Session => DiscardItemType::Session,
646+
ItemType::Sessions => DiscardItemType::Sessions,
647+
ItemType::Statsd => DiscardItemType::Statsd,
648+
ItemType::MetricBuckets => DiscardItemType::MetricBuckets,
649+
ItemType::ClientReport => DiscardItemType::ClientReport,
650+
ItemType::Profile => DiscardItemType::Profile,
651+
ItemType::ReplayEvent => DiscardItemType::ReplayEvent,
652+
ItemType::ReplayRecording => DiscardItemType::ReplayRecording,
653+
ItemType::ReplayVideo => DiscardItemType::ReplayVideo,
654+
ItemType::CheckIn => DiscardItemType::CheckIn,
655+
ItemType::OtelLog => DiscardItemType::OtelLog,
656+
ItemType::Log => DiscardItemType::Log,
657+
ItemType::Span => DiscardItemType::Span,
658+
ItemType::OtelSpan => DiscardItemType::OtelSpan,
659+
ItemType::OtelTracesData => DiscardItemType::OtelTracesData,
660+
ItemType::UserReportV2 => DiscardItemType::UserReportV2,
661+
ItemType::ProfileChunk => DiscardItemType::ProfileChunk,
662+
ItemType::Unknown(_) => DiscardItemType::Unknown,
663+
}
664+
}
665+
}
666+
667+
impl From<ItemType> for DiscardItemType {
668+
fn from(value: ItemType) -> Self {
669+
From::from(&value)
670+
}
671+
}
672+
527673
/// Raw representation of an outcome for serialization.
528674
///
529675
/// The JSON serialization of this structure is placed on the Kafka topic and used in the HTTP
@@ -754,7 +900,10 @@ impl ClientReportOutcomeProducer {
754900
Outcome::FilteredSampling(_) => &mut client_report.filtered_sampling_events,
755901
Outcome::RateLimited(_) => &mut client_report.rate_limited_events,
756902
_ => {
757-
// Cannot convert this outcome to a client report.
903+
relay_log::debug!(
904+
"Outcome '{}' cannot be converted to client report",
905+
msg.outcome
906+
);
758907
return;
759908
}
760909
};
@@ -1076,4 +1225,20 @@ mod tests {
10761225
MatchedRuleIds([1000, 1004, 1400, 1500, 0].map(RuleId).into())
10771226
);
10781227
}
1228+
1229+
#[test]
1230+
fn test_outcome_discard_reason() {
1231+
assert_eq!(
1232+
Some(Cow::from("too_large:attachment")),
1233+
Outcome::Invalid(DiscardReason::TooLarge(DiscardItemType::Attachment)).to_reason()
1234+
);
1235+
assert_eq!(
1236+
Some(Cow::from("too_large:unknown")),
1237+
Outcome::Invalid(DiscardReason::TooLarge(DiscardItemType::Unknown)).to_reason()
1238+
);
1239+
assert_eq!(
1240+
Some(Cow::from("too_large:unreal_report")),
1241+
Outcome::Invalid(DiscardReason::TooLarge(DiscardItemType::UnrealReport)).to_reason()
1242+
);
1243+
}
10791244
}

relay-server/src/services/processor.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ pub enum ProcessingError {
477477
InvalidUnrealReport(#[source] Unreal4Error),
478478

479479
#[error("event payload too large")]
480-
PayloadTooLarge,
480+
PayloadTooLarge(ItemType),
481481

482482
#[error("invalid transaction event")]
483483
InvalidTransaction,
@@ -536,7 +536,9 @@ impl ProcessingError {
536536
fn to_outcome(&self) -> Option<Outcome> {
537537
match self {
538538
// General outcomes for invalid events
539-
Self::PayloadTooLarge => Some(Outcome::Invalid(DiscardReason::TooLarge)),
539+
Self::PayloadTooLarge(item_type) => {
540+
Some(Outcome::Invalid(DiscardReason::TooLarge(item_type.into())))
541+
}
540542
Self::InvalidJson(_) => Some(Outcome::Invalid(DiscardReason::InvalidJson)),
541543
Self::InvalidMsgpack(_) => Some(Outcome::Invalid(DiscardReason::InvalidMsgpack)),
542544
Self::InvalidSecurityType(_) => {
@@ -588,7 +590,7 @@ impl ProcessingError {
588590
impl From<Unreal4Error> for ProcessingError {
589591
fn from(err: Unreal4Error) -> Self {
590592
match err.kind() {
591-
Unreal4ErrorKind::TooLarge => Self::PayloadTooLarge,
593+
Unreal4ErrorKind::TooLarge => Self::PayloadTooLarge(ItemType::UnrealReport),
592594
_ => ProcessingError::InvalidUnrealReport(err),
593595
}
594596
}

relay-server/src/services/processor/event.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@ fn extract_attached_event(
602602
// Protect against blowing up during deserialization. Attachments can have a significantly
603603
// larger size than regular events and may cause significant processing delays.
604604
if item.len() > config.max_event_size() {
605-
return Err(ProcessingError::PayloadTooLarge);
605+
return Err(ProcessingError::PayloadTooLarge(item.ty().to_owned()));
606606
}
607607

608608
let payload = item.payload();
@@ -625,7 +625,7 @@ fn parse_msgpack_breadcrumbs(
625625
// blowing up during deserialization. As approximation, we use the maximum event payload
626626
// size as bound, which is roughly in the right ballpark.
627627
if item.len() > config.max_event_size() {
628-
return Err(ProcessingError::PayloadTooLarge);
628+
return Err(ProcessingError::PayloadTooLarge(item.ty().to_owned()));
629629
}
630630

631631
let payload = item.payload();

relay-server/src/services/store.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use uuid::Uuid;
3636
use crate::metrics::{ArrayEncoding, BucketEncoder, MetricOutcomes};
3737
use crate::service::ServiceError;
3838
use crate::services::global_config::GlobalConfigHandle;
39-
use crate::services::outcome::{DiscardReason, Outcome, TrackOutcome};
39+
use crate::services::outcome::{DiscardItemType, DiscardReason, Outcome, TrackOutcome};
4040
use crate::services::processor::Processed;
4141
use crate::statsd::{RelayCounters, RelayGauges, RelayTimers};
4242
use crate::utils::{FormDataIter, TypedEnvelope};
@@ -776,7 +776,9 @@ impl StoreService {
776776
self.outcome_aggregator.send(TrackOutcome {
777777
category: DataCategory::Replay,
778778
event_id,
779-
outcome: Outcome::Invalid(DiscardReason::TooLarge),
779+
outcome: Outcome::Invalid(DiscardReason::TooLarge(
780+
DiscardItemType::ReplayRecording,
781+
)),
780782
quantity: 1,
781783
remote_addr: None,
782784
scoping,

relay-server/src/utils/unreal.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ use relay_event_schema::protocol::{
88
};
99
use relay_protocol::{Annotated, Array, Empty, Object, Value};
1010
use symbolic_unreal::{
11-
Unreal4Context, Unreal4Crash, Unreal4Error, Unreal4ErrorKind, Unreal4FileType, Unreal4LogEntry,
11+
Unreal4Context, Unreal4Crash, Unreal4Error, Unreal4FileType, Unreal4LogEntry,
1212
};
1313

1414
use crate::constants::{
1515
ITEM_NAME_BREADCRUMBS1, ITEM_NAME_BREADCRUMBS2, ITEM_NAME_EVENT, UNREAL_USER_HEADER,
1616
};
1717
use crate::envelope::{AttachmentType, ContentType, Envelope, Item, ItemType};
18+
use crate::services::processor::ProcessingError;
1819

1920
/// Maximum number of unreal logs to parse for breadcrumbs.
2021
const MAX_NUM_UNREAL_LOGS: usize = 40;
@@ -50,7 +51,7 @@ pub fn expand_unreal_envelope(
5051
unreal_item: Item,
5152
envelope: &mut Envelope,
5253
config: &Config,
53-
) -> Result<(), Unreal4Error> {
54+
) -> Result<(), ProcessingError> {
5455
let payload = unreal_item.payload();
5556
let crash = Unreal4Crash::parse_with_limit(&payload, config.max_envelope_size())?;
5657

@@ -90,8 +91,8 @@ pub fn expand_unreal_envelope(
9091
envelope.add_item(item);
9192
}
9293

93-
if super::check_envelope_size_limits(config, envelope).is_err() {
94-
return Err(Unreal4ErrorKind::TooLarge.into());
94+
if let Err(offender) = super::check_envelope_size_limits(config, envelope) {
95+
return Err(ProcessingError::PayloadTooLarge(offender));
9596
}
9697

9798
Ok(())

0 commit comments

Comments
 (0)