What's happening?
When OutboxSender (sentry-java/Android) processes envelopes written to disk by sentry-native (e.g. after an NDK crash), it iterates each envelope item individually. Event and Transaction items go through captureEvent/captureTransaction, but any other item type — including Attachment — is wrapped in a brand-new single-item SentryEnvelope and sent standalone via captureEnvelope, instead of being sent together with the event that produced it.
Why is it happening? (root cause)
This is OutboxSender.processEnvelope in sentry/src/main/java/io/sentry/OutboxSender.java. The loop dispatches Event/Transaction items via their dedicated capture calls, and falls through to a generic else branch for every other item type that wraps that single item in its own envelope and sends it immediately (around OutboxSender.java#L124, current captureEnvelope(newEnvelope, hint) call). There is no step that buffers non-event items and reassembles them into one outgoing envelope keyed by event_id before sending.
Possible solutions (proposal, not yet decided)
- Buffer Attachment-type items encountered while iterating
envelope.getItems(), converting each SentryEnvelopeItem into an Attachment (filename/content-type from the item header, bytes from item.getData()).
- When the Event or Transaction item is found, call
hint.addAttachments(buffered) before captureEvent/captureTransaction, instead of sending each attachment through its own captureEnvelope call. Hint already supports addAttachments(List<Attachment>), so this doesn't require a new envelope format.
- Handle item ordering (attachments may appear before or after the event item within the envelope) and the edge case of attachment-only envelopes (no event/transaction present), which must still be sent standalone.
- Add an explicit size check before merging: if attaching would push the combined envelope over the max envelope size, fall back to sending that attachment standalone (or drop it) rather than risking the whole event getting rejected.
- Only merge Attachment-type items — leave other non-event item types (session, user feedback, etc.) on their current per-item send path to avoid changing unrelated behavior.
- Extend
OutboxSenderTest.kt to cover reassembly, mixed item ordering, and the no-event fallback.
- Estimated scope: mostly contained to
OutboxSender.java and its test, no protocol/API changes, no server-side work required — ingestion already accepts combined event+attachment envelopes (used by JS and other SDKs today) and will keep supporting standalone attachments for legacy SDK versions regardless.
User Impact
- Quota/cost impact: Relay has no filter stage for standalone attachments (only
processing/errors/ has one), so when an event is dropped by an inbound filter, its standalone attachment still gets ingested and billed — landing as an orphan the customer can't even see, since there's no event to attach it to. Riot Games hit this concretely, ingesting roughly 10TB/day of attachments with ~90% having no associated event, and a live investigation (INGEST-1167) found Android/Java is currently the SDK with the highest volume of standalone attachments. Ingest is shipping a server-side stopgap (discard orphaned attachments at the filter stage), but fixing OutboxSender removes the root cause on the Android/Java side rather than relying on ingest to keep mopping up after it.
- Mainly affects Android apps using sentry-native (NDK crash reporting) and hybrid SDKs (Unity, Flutter, React Native, etc.) layered on top of sentry-android, since they're the ones writing envelopes to the outbox folder that
OutboxSender later fans out.
History of the issue
The design is incidental, not deliberate. The original envelope-pickup code (getsentry/sentry-android#89, getsentry/sentry-android#145, 2019) only handled Event items; every other item type was simply logged as ignored, with a // TODO: Handle attachments and other types comment left in place. In January 2021, getsentry/sentry-java#1158 ("OutboxSender supports all envelope item types") closed that TODO by adding the current catch-all branch: wrap any non-event item in its own envelope and send it immediately. That was a pragmatic fix to stop dropping items, not a considered protocol decision — there's no historical discussion of envelope size limits, item ordering, or ingestion semantics tied to it.
Other useful context
This surfaced from a cross-SDK discussion (also here) about disallowing standalone attachments at the protocol/spec level (develop-docs). Ingestion will keep accepting standalone attachments indefinitely to support already-shipped legacy SDK versions, so this fix is about sentry-java emitting the preferred combined-envelope format going forward, not a breaking or urgent change. If the spec formally disallows standalone attachments before this is fixed, OutboxSender's native-outbox path should get an explicit documented exception until this issue is resolved.
Action taken on behalf of Adam Brown.
What's happening?
When
OutboxSender(sentry-java/Android) processes envelopes written to disk by sentry-native (e.g. after an NDK crash), it iterates each envelope item individually. Event and Transaction items go throughcaptureEvent/captureTransaction, but any other item type — including Attachment — is wrapped in a brand-new single-itemSentryEnvelopeand sent standalone viacaptureEnvelope, instead of being sent together with the event that produced it.Why is it happening? (root cause)
This is
OutboxSender.processEnvelopeinsentry/src/main/java/io/sentry/OutboxSender.java. The loop dispatches Event/Transaction items via their dedicated capture calls, and falls through to a genericelsebranch for every other item type that wraps that single item in its own envelope and sends it immediately (aroundOutboxSender.java#L124, currentcaptureEnvelope(newEnvelope, hint)call). There is no step that buffers non-event items and reassembles them into one outgoing envelope keyed byevent_idbefore sending.Possible solutions (proposal, not yet decided)
envelope.getItems(), converting eachSentryEnvelopeIteminto anAttachment(filename/content-type from the item header, bytes fromitem.getData()).hint.addAttachments(buffered)beforecaptureEvent/captureTransaction, instead of sending each attachment through its owncaptureEnvelopecall.Hintalready supportsaddAttachments(List<Attachment>), so this doesn't require a new envelope format.OutboxSenderTest.ktto cover reassembly, mixed item ordering, and the no-event fallback.OutboxSender.javaand its test, no protocol/API changes, no server-side work required — ingestion already accepts combined event+attachment envelopes (used by JS and other SDKs today) and will keep supporting standalone attachments for legacy SDK versions regardless.User Impact
processing/errors/has one), so when an event is dropped by an inbound filter, its standalone attachment still gets ingested and billed — landing as an orphan the customer can't even see, since there's no event to attach it to. Riot Games hit this concretely, ingesting roughly 10TB/day of attachments with ~90% having no associated event, and a live investigation (INGEST-1167) found Android/Java is currently the SDK with the highest volume of standalone attachments. Ingest is shipping a server-side stopgap (discard orphaned attachments at the filter stage), but fixingOutboxSenderremoves the root cause on the Android/Java side rather than relying on ingest to keep mopping up after it.OutboxSenderlater fans out.History of the issue
The design is incidental, not deliberate. The original envelope-pickup code (
getsentry/sentry-android#89,getsentry/sentry-android#145, 2019) only handled Event items; every other item type was simply logged as ignored, with a// TODO: Handle attachments and other typescomment left in place. In January 2021,getsentry/sentry-java#1158("OutboxSender supports all envelope item types") closed that TODO by adding the current catch-all branch: wrap any non-event item in its own envelope and send it immediately. That was a pragmatic fix to stop dropping items, not a considered protocol decision — there's no historical discussion of envelope size limits, item ordering, or ingestion semantics tied to it.Other useful context
This surfaced from a cross-SDK discussion (also here) about disallowing standalone attachments at the protocol/spec level (develop-docs). Ingestion will keep accepting standalone attachments indefinitely to support already-shipped legacy SDK versions, so this fix is about sentry-java emitting the preferred combined-envelope format going forward, not a breaking or urgent change. If the spec formally disallows standalone attachments before this is fixed,
OutboxSender's native-outbox path should get an explicit documented exception until this issue is resolved.Action taken on behalf of Adam Brown.