Skip to content
Open
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 @@ -16,6 +16,7 @@
**Internal**:

- Embed AI operation type mappings into Relay. ([#5555](https://github.com/getsentry/relay/pull/5555))
- Apply continuous profiling rate limits to transaction profiles. ([#5614](https://github.com/getsentry/relay/pull/5614)
- Use new processor architecture to process transactions. ([#5379](https://github.com/getsentry/relay/pull/5379))
- Add `gen_ai_response_time_to_first_token` as a `SpanData` attribute. ([#5575](https://github.com/getsentry/relay/pull/5575))
- Add sampling to expensive envelope buffer statsd metrics. ([#5576](https://github.com/getsentry/relay/pull/5576))
Expand Down
32 changes: 30 additions & 2 deletions relay-server/src/processing/transactions/types/expanded.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use either::Either;
use relay_event_schema::protocol::Event;
use relay_profiling::ProfileMetadata;
use relay_profiling::{ProfileMetadata, ProfileType};
use relay_protocol::Annotated;
use relay_quotas::DataCategory;
use relay_sampling::evaluation::SamplingDecision;
Expand Down Expand Up @@ -175,7 +175,11 @@ impl RateLimited for Managed<Box<ExpandedTransaction<TotalAndIndexed>>> {
let attachment_quantities = self.attachments.quantities();

// Check profile limits:
for (category, quantity) in self.profile.quantities() {
let profile_quantities = self
.profile
.as_ref()
.map_or(Default::default(), |p| p.rate_limiting_quantities());
for (category, quantity) in profile_quantities {
let limits = rate_limiter
.try_consume(scoping.item(category), quantity)
.await;
Expand Down Expand Up @@ -286,6 +290,30 @@ impl ExpandedProfile {
self.item.set_profile_type(self.meta.kind);
self.item
}

/// Returns the data categories and quantities in which this profile should be rate limited.
///
/// This is a remnant of the introduction of continuous profiling. Transaction profiles are
/// essentially deprecated, but for backwards compatibility reasons they still are being sent
/// and exist and we want to still support them.
///
/// Continuous profiling is split into two separate categories, backend and ui, based on the
/// platform being profiled. Customers can now assign different quota for each category,
/// and we want these limits to also apply to transaction profiles.
///
/// But these transaction profiles, are still visible as separate items in the UI and stats
/// pages, this is why we only rate limit against the respective category instead of also
/// counting them/adding outcomes for them in the continuous profiling categories.
///
/// It's basically a big pile of legacy reasons.
pub fn rate_limiting_quantities(&self) -> Quantities {
let mut quantities = self.quantities();
quantities.push(match self.meta.kind {
ProfileType::Backend => (DataCategory::ProfileChunk, 1),
ProfileType::Ui => (DataCategory::ProfileChunkUi, 1),
});
quantities
}
}

impl Counted for ExpandedProfile {
Expand Down
30 changes: 27 additions & 3 deletions relay-server/src/processing/transactions/types/profile.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::Envelope;
use crate::envelope::EnvelopeHeaders;
use crate::managed::{Counted, Managed, Quantities};
use crate::processing::CountRateLimited;
use crate::managed::{Counted, Managed, Quantities, Rejected};
use crate::processing::transactions::Error;
use crate::processing::transactions::types::ExpandedProfile;
use crate::processing::{Context, RateLimited, RateLimiter};

/// A standalone profile, which is no longer attached to a transaction as the transaction was
/// dropped by dynamic sampling.
Expand All @@ -29,6 +29,30 @@ impl Counted for StandaloneProfile {
}
}

impl CountRateLimited for Managed<Box<StandaloneProfile>> {
impl RateLimited for Managed<Box<StandaloneProfile>> {
type Output = Self;
type Error = Error;

async fn enforce<R>(
self,
mut rate_limiter: R,
_ctx: Context<'_>,
) -> Result<Self::Output, Rejected<Self::Error>>
where
R: RateLimiter,
{
let scoping = self.scoping();

for (category, quantity) in self.profile.rate_limiting_quantities() {
let limits = rate_limiter
.try_consume(scoping.item(category), quantity)
.await;

if !limits.is_empty() {
return Err(self.reject_err(Error::from(limits)));
}
}

Ok(self)
}
}
15 changes: 5 additions & 10 deletions tests/integration/test_outcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -1578,7 +1578,9 @@ def make_envelope():
assert profiles_consumer.get_profile()


@pytest.mark.parametrize("quota_category", ["transaction", "profile"])
@pytest.mark.parametrize(
"quota_category", ["transaction", "profile", "profile_chunk_ui"]
)
def test_profile_outcomes_rate_limited(
mini_sentry,
relay_with_processing,
Expand Down Expand Up @@ -1607,15 +1609,12 @@ def test_profile_outcomes_rate_limited(
config = {
"outcomes": {
"emit_outcomes": True,
"batch_size": 1,
"batch_interval": 1,
"aggregator": {
"bucket_interval": 1,
"flush_interval": 1,
},
},
}
}

upstream = relay_with_processing(config)

with open(
Expand Down Expand Up @@ -1667,7 +1666,7 @@ def test_profile_outcomes_rate_limited(
for (category, quantity) in expected_categories
]

if quota_category == "profile":
if quota_category != "transaction":
expected_outcomes.append(
{
"category": DataCategory.SPAN_INDEXED,
Expand Down Expand Up @@ -1712,10 +1711,6 @@ def test_profile_outcomes_rate_limited_when_dynamic_sampling_drops(
"flush_interval": 0,
},
},
"aggregator": {
"bucket_interval": 1,
"initial_delay": 0,
},
}

relay = relay(mini_sentry, options=config)
Expand Down
Loading