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 @@ -32,6 +32,7 @@
### Fixes

- Prevent the main thread from potentially hanging during app start if Android is slow to register the SDK's network or telephony listeners. `Sentry.init`, Session Replay, and capturing events no longer wait for that registration ([#6537](https://github.com/getsentry/sentry-java/pull/6537))
- Send outbox attachments together with their event or transaction so parent filtering also drops its attachments. Grouped attachments now respect the configured per-attachment `maxAttachmentSize` limit. ([#6513](https://github.com/getsentry/sentry-java/pull/6513))
- Add opt-in support for matching GraphQL ignored error types against classification specifications via `sentry.graphql.enable-specification-error-type-filtering` ([#6092](https://github.com/getsentry/sentry-java/pull/6092))
- Session Replay: Fix `SentryOkHttpInterceptor` hanging forever on responses whose body has no known length, such as Server-Sent Events or a gzipped response, when network details are enabled for the URL ([#6231](https://github.com/getsentry/sentry-java/pull/6231))
- Session Replay: `SentryOkHttpInterceptor` now captures network request and response details only while Session Replay is recording or buffering ([#6486](https://github.com/getsentry/sentry-java/pull/6486))
Expand Down
59 changes: 57 additions & 2 deletions sentry/src/main/java/io/sentry/OutboxSender.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -121,13 +123,52 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
CollectionUtils.size(envelope.getItems()));
int currentItem = 0;

int parentCount = 0;
boolean transactionParent = false;
for (final SentryEnvelopeItem item : envelope.getItems()) {
if (item.getHeader() != null
&& (SentryItemType.Event.equals(item.getHeader().getType())
|| SentryItemType.Transaction.equals(item.getHeader().getType()))) {
parentCount++;
transactionParent = SentryItemType.Transaction.equals(item.getHeader().getType());
}
}

final List<Attachment> attachments = new ArrayList<>();
if (parentCount == 1) {
// Collect before dispatch so attachments may precede or follow their parent. Multiple parents
// are ambiguous and retain the legacy per-item path.
for (final SentryEnvelopeItem item : envelope.getItems()) {
final SentryEnvelopeItemHeader header = item.getHeader();
if (header != null
&& SentryItemType.Attachment.equals(header.getType())
&& header.getFileName() != null) {
attachments.add(
new Attachment(
() -> item.getData(),
header.getFileName(),
header.getContentType(),
header.getAttachmentType(),
transactionParent));
}
}
}
// Hint.clear() leaves attachments intact, so restore this snapshot after each grouped capture.
final List<Attachment> originalAttachments = hint.getAttachments();

for (final SentryEnvelopeItem item : envelope.getItems()) {
currentItem++;

if (item.getHeader() == null) {
logger.log(SentryLevel.ERROR, "Item %d has no header", currentItem);
continue;
}
if (parentCount == 1
&& SentryItemType.Attachment.equals(item.getHeader().getType())
&& item.getHeader().getFileName() != null) {
// Even an invalid or filtered parent owns these attachments. Never send them on their own.
continue;
}
if (SentryItemType.Event.equals(item.getHeader().getType())) {
try (final Reader eventReader =
new BufferedReader(
Expand All @@ -144,7 +185,14 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
logUnexpectedEventId(envelope, event.getEventId(), currentItem);
continue;
}
scopes.captureEvent(event, hint);
try {
hint.addAttachments(attachments);
scopes.captureEvent(event, hint);
} finally {
if (!attachments.isEmpty()) {
hint.replaceAttachments(originalAttachments);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rate limits can orphan grouped attachments

Medium Severity

Grouped outbox attachments are added onto the parent and sent as one envelope, but RateLimiter.filter drops items by category. An error or transaction 429 with attachments still allowed strips the parent and forwards the rest, so minidumps can be ingested without their event. Previously a failed parent capture stopped later items, so those attachments never went out alone.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9097b0b. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I looked at this and I don't think it's a regression. On main, every outbox attachment already goes out in its own envelope through captureEnvelope, so an error or transaction 429 drops the event and the attachments still go out alone. That orphaning exists today, and the PR doesn't add it.

With this change the attachments ride on the parent's hint, so the envelope is built the same way SentryClient builds it for a normal captureEvent with attachments, and RateLimiter.filter treats it exactly like any other SDK capture. If dropping attachments whenever their parent's category is rate limited is the behavior you want, I think it belongs in RateLimiter for all captures rather than special-cased in OutboxSender. Happy to do that as a follow-up if you'd like.

}
logItemCaptured(currentItem);

if (!waitFlush(hint)) {
Expand Down Expand Up @@ -181,7 +229,14 @@ private void processEnvelope(final @NotNull SentryEnvelope envelope, final @NotN
.getTrace()
.setSamplingDecision(extractSamplingDecision(traceContext));
}
scopes.captureTransaction(transaction, traceContext, hint);
try {
hint.addAttachments(attachments);
scopes.captureTransaction(transaction, traceContext, hint);
} finally {
if (!attachments.isEmpty()) {
hint.replaceAttachments(originalAttachments);
}
}
logItemCaptured(currentItem);

if (!waitFlush(hint)) {
Expand Down
Loading