Skip to content

Conversation

@jjbayer
Copy link
Member

@jjbayer jjbayer commented Nov 14, 2025

Closes INGEST-610.

@linear
Copy link

linear bot commented Nov 14, 2025

pub struct ExpandedTransaction<C> {
headers: EnvelopeHeaders,
transaction: T, // might be empty
event: Annotated<Event>,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this from a wrapper type that implements Counted to a phantom-data-style field, for three reasons:

  1. Quantities for the wrapper type could not be inferred correctly because we need to check flags.spans_extracted to get the quantities.
  2. Consistency with the spans processor.
  3. Simpler code.

Comment on lines -66 to -71
} else if work.transaction.0.value().is_none() && profile_item.sampled() {
// A profile with `sampled=true` should never be without a transaction
record_keeper.reject_err(
Outcome::Invalid(DiscardReason::Profiling("missing_transaction")),
work.profile.take(),
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is leftover code, there is always a transaction.

Comment on lines +1167 to +1168
{"reason": "release-version", "category": "span", "quantity": 2},
{"reason": "release-version", "category": "span_indexed", "quantity": 2},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now counts quantities correctly when the transaction is filtered.

},
"blacklistedIps": ["127.43.33.22"],
"trustedRelays": [],
"transactionMetrics": {"version": 3},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now a requirement, otherwise debug assertions will fail.

(DataCategory.TRANSACTION, 1, "invalid_transaction"),
(DataCategory.TRANSACTION_INDEXED, 1, "invalid_transaction"),
(DataCategory.SPAN, 1, "invalid_span"),
(DataCategory.SPAN, 2, "invalid_transaction"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now counts embedded spans correctly.

@jjbayer jjbayer marked this pull request as ready for review November 20, 2025 15:15
@jjbayer jjbayer requested a review from a team as a code owner November 20, 2025 15:15
metrics_extracted,
spans_extracted,
} = ctx;
// TODO(follow-up): this function should always extract metrics. Dynamic sampling should validate
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wanna link to a GH issue here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: #5403

Comment on lines +136 to +137
#[cfg(debug_assertions)]
event.ensure_span_count();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that mess with the Managed assertions and we may get misaccounting in prod, but not in tests because this is ran in tests but not in production?

Or is this there to specifically defuse these Managed assertions and it's okay if we misaccount in prod until the transaction is parsed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is this there to specifically defuse these Managed assertions and it's okay if we misaccount in prod until the transaction is parsed?

Yes, I believe it's not worth shallow-parsing in prod, because it gets properly parsed immediately afterwards.

category: _,
} = self;
debug_assert!(flags.metrics_extracted);
let mut quantities = smallvec![(DataCategory::TransactionIndexed, 1),];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut quantities = smallvec![(DataCategory::TransactionIndexed, 1),];
let mut quantities = smallvec![(DataCategory::TransactionIndexed, 1)];

Comment on lines +471 to +473
let limits = rate_limiter
.try_consume(scoping.item(DataCategory::Transaction), 1)
.await;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the transaction is limited, we can already drop the entire payload and don't need to consider attachments and profiles, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I thought I had an early return here, good catch.

// If there is a transaction limit, drop everything.
// This also affects profiles that lost their transaction due to sampling.
let limits = rate_limiter
.try_consume(scoping.item(DataCategory::TransactionIndexed), 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to check both categories here, that's what the EnvelopeLimiter does.

Note fn is_event_active() checks both categories, indexed and non-indexed as well.

Copy link
Member

@Dav1dde Dav1dde Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naivly I would now expect transaction rate limits never to apply, what is strange, I would've expected a test to fail here. Am I missing something?

The spans processor always only rate limits total + indexed, it extracts the metrics only after rate limiting. I think this is also what we should do here.

Then you also don't have to implement RateLimited twice.

($($variant:ident => $ty:ty,)*) => {
/// All known [`Processor`] outputs.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(clippy::large_enum_variant)]
#[expect(clippy::large_enum_variant), reason = "..."]

Alternatively we can also box the transaction output, I think that kinda depends why the lint triggers, if it's just because of a small-ish amount it's fine, if there is a large discrepancy maybe it's worth boxing.

}

Ok(())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Rate limiting doesn't check indexed transaction quota

The rate limiting implementation for ExpandedTransaction<TotalAndIndexed> only checks the Transaction category quota, but the quantities() method reports both Transaction and TransactionIndexed categories. This means TransactionIndexed quota limits are never enforced for transactions before metrics extraction. In contrast, the ExpandedTransaction<Indexed> rate limiting correctly checks both categories (lines 537-546). Without checking the indexed category, rate limits on indexed transactions can be bypassed.

Additional Locations (1)

Fix in Cursor Fix in Web

let main = match self.limiter.enforce_quotas(&mut indexed, ctx).await {
Err(e) => {
if let Error::RateLimited(rate_limits) = e.inner() {
if rate_limits.is_any_limited(&[scoping.item(DataCategory::Transaction)]) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Transaction rate limit check missing TransactionIndexed category

The enforce_quotas error handling for transactions only checks DataCategory::Transaction to decide if an event should be dropped. It misses DataCategory::TransactionIndexed limits, which the RateLimited implementation correctly checks. This causes transactions to not be dropped when only TransactionIndexed is rate-limited, and related integration tests are currently disabled.

Additional Locations (1)

Fix in Cursor Fix in Web

@jjbayer jjbayer marked this pull request as draft December 4, 2025 13:43
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.

4 participants