Skip to content

Commit 3d5a001

Browse files
authored
Discard envelopes on 4xx and 5xx response (#4950)
* Discard envelopes on 4xx and 5xx response * do not discard on 429 * changelog * also discard cached envelope on 429 * move changelog; reword * shorten changelog entry
1 parent 421ee46 commit 3d5a001

File tree

4 files changed

+107
-21
lines changed

4 files changed

+107
-21
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Improvements
6+
7+
- Discard envelopes on `4xx` and `5xx` response ([#4950](https://github.com/getsentry/sentry-java/pull/4950))
8+
- This aims to not overwhelm Sentry after an outage or load shedding (including HTTP 429) where too many events are sent at once
9+
10+
311
## 8.29.0
412

513
### Fixes

sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -309,18 +309,13 @@ public void run() {
309309

310310
options.getLogger().log(SentryLevel.ERROR, message);
311311

312-
// ignore e.g. 429 as we're not the ones actively dropping
313-
if (result.getResponseCode() >= 400 && result.getResponseCode() != 429) {
314-
if (!cached) {
315-
HintUtils.runIfDoesNotHaveType(
316-
hint,
317-
Retryable.class,
318-
(hint) -> {
319-
options
320-
.getClientReportRecorder()
321-
.recordLostEnvelope(
322-
DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
323-
});
312+
if (result.getResponseCode() >= 400) {
313+
envelopeCache.discard(envelope);
314+
// ignore e.g. 429 as we're not the ones actively dropping
315+
if (result.getResponseCode() != 429) {
316+
options
317+
.getClientReportRecorder()
318+
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
324319
}
325320
}
326321

sentry/src/test/java/io/sentry/transport/AsyncHttpTransportClientReportTest.kt

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@ import io.sentry.SentryEnvelope
44
import io.sentry.SentryOptions
55
import io.sentry.SentryOptionsManipulator
66
import io.sentry.Session
7+
import io.sentry.cache.IEnvelopeCache
78
import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableHint
89
import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableUncaughtExceptionHint
910
import io.sentry.clientreport.ClientReportTestHelper.Companion.uncaughtExceptionHint
1011
import io.sentry.clientreport.DiscardReason
1112
import io.sentry.clientreport.IClientReportRecorder
1213
import io.sentry.dsnString
14+
import io.sentry.hints.Retryable
1315
import io.sentry.protocol.User
16+
import io.sentry.util.HintUtils
1417
import java.io.IOException
1518
import kotlin.test.Test
1619
import kotlin.test.assertFailsWith
20+
import kotlin.test.assertFalse
1721
import org.mockito.kotlin.any
1822
import org.mockito.kotlin.anyOrNull
1923
import org.mockito.kotlin.eq
@@ -31,11 +35,12 @@ class AsyncHttpTransportClientReportTest {
3135
var transportGate = mock<ITransportGate>()
3236
var executor = mock<QueuedThreadPoolExecutor>()
3337
var rateLimiter = mock<RateLimiter>()
38+
var envelopeCache = mock<IEnvelopeCache>()
3439
var sentryOptions: SentryOptions =
3540
SentryOptions().apply {
3641
dsn = dsnString
3742
setSerializer(mock())
38-
setEnvelopeDiskCache(mock())
43+
setEnvelopeDiskCache(envelopeCache)
3944
}
4045
var clientReportRecorder = mock<IClientReportRecorder>()
4146
val envelopeBeforeAttachingClientReport =
@@ -66,23 +71,36 @@ class AsyncHttpTransportClientReportTest {
6671
.attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport))
6772
verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any())
6873
verifyNoMoreInteractions(fixture.clientReportRecorder)
74+
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
6975
}
7076

7177
@Test
72-
fun `does not record lost envelope on 500 error for retryable`() {
78+
fun `records lost envelope on 500 error for retryable`() {
7379
// given
7480
givenSetup(TransportResult.error(500))
81+
whenever(
82+
fixture.envelopeCache.storeEnvelope(eq(fixture.envelopeBeforeAttachingClientReport), any())
83+
)
84+
.thenReturn(true)
7585

7686
// when
87+
val retryableHint = retryableHint()
7788
assertFailsWith(java.lang.IllegalStateException::class) {
78-
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint())
89+
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint)
7990
}
8091

8192
// then
8293
verify(fixture.clientReportRecorder, times(1))
8394
.attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport))
84-
verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any())
95+
verify(fixture.clientReportRecorder, times(1))
96+
.recordLostEnvelope(
97+
eq(DiscardReason.NETWORK_ERROR),
98+
same(fixture.envelopeAfterAttachingClientReport),
99+
)
85100
verifyNoMoreInteractions(fixture.clientReportRecorder)
101+
val sentrySdkHint = HintUtils.getSentrySdkHint(retryableHint)
102+
assertFalse((sentrySdkHint as Retryable).isRetry)
103+
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
86104
}
87105

88106
@Test
@@ -104,23 +122,36 @@ class AsyncHttpTransportClientReportTest {
104122
same(fixture.envelopeAfterAttachingClientReport),
105123
)
106124
verifyNoMoreInteractions(fixture.clientReportRecorder)
125+
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
107126
}
108127

109128
@Test
110-
fun `does not record lost envelope on 400 error for retryable`() {
129+
fun `records lost envelope on 400 error for retryable`() {
111130
// given
112131
givenSetup(TransportResult.error(400))
132+
whenever(
133+
fixture.envelopeCache.storeEnvelope(eq(fixture.envelopeBeforeAttachingClientReport), any())
134+
)
135+
.thenReturn(true)
113136

114137
// when
138+
val retryableHint = retryableHint()
115139
assertFailsWith(java.lang.IllegalStateException::class) {
116-
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint())
140+
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint)
117141
}
118142

119143
// then
120144
verify(fixture.clientReportRecorder, times(1))
121145
.attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport))
122-
verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any())
146+
verify(fixture.clientReportRecorder, times(1))
147+
.recordLostEnvelope(
148+
eq(DiscardReason.NETWORK_ERROR),
149+
same(fixture.envelopeAfterAttachingClientReport),
150+
)
123151
verifyNoMoreInteractions(fixture.clientReportRecorder)
152+
val sentrySdkHint = HintUtils.getSentrySdkHint(retryableHint)
153+
assertFalse((sentrySdkHint as Retryable).isRetry)
154+
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
124155
}
125156

126157
@Test

sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class AsyncHttpTransportTest {
107107
}
108108

109109
@Test
110-
fun `stores envelope after unsuccessful send`() {
110+
fun `discards envelope after unsuccessful send 500`() {
111111
// given
112112
val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null)
113113
whenever(fixture.transportGate.isConnected).thenReturn(true)
@@ -128,7 +128,59 @@ class AsyncHttpTransportTest {
128128
order.verify(fixture.sentryOptions.envelopeDiskCache).storeEnvelope(eq(envelope), anyOrNull())
129129

130130
order.verify(fixture.connection).send(eq(envelope))
131-
verify(fixture.sentryOptions.envelopeDiskCache, never()).discard(any())
131+
order.verify(fixture.sentryOptions.envelopeDiskCache).discard(eq(envelope))
132+
}
133+
134+
@Test
135+
fun `discards envelope after unsuccessful send 400`() {
136+
// given
137+
val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null)
138+
whenever(fixture.transportGate.isConnected).thenReturn(true)
139+
whenever(fixture.rateLimiter.filter(eq(envelope), anyOrNull())).thenReturn(envelope)
140+
whenever(fixture.connection.send(any())).thenReturn(TransportResult.error(400))
141+
142+
// when
143+
try {
144+
fixture.getSUT().send(envelope)
145+
} catch (e: IllegalStateException) {
146+
// expected - this is how the AsyncConnection signals failure to the executor for it to retry
147+
}
148+
149+
// then
150+
val order = inOrder(fixture.connection, fixture.sentryOptions.envelopeDiskCache)
151+
152+
// because storeBeforeSend is enabled by default
153+
order.verify(fixture.sentryOptions.envelopeDiskCache).storeEnvelope(eq(envelope), anyOrNull())
154+
155+
order.verify(fixture.connection).send(eq(envelope))
156+
order.verify(fixture.sentryOptions.envelopeDiskCache).discard(eq(envelope))
157+
}
158+
159+
@Test
160+
fun `discards envelope after unsuccessful send 429`() {
161+
// given
162+
val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null)
163+
whenever(fixture.transportGate.isConnected).thenReturn(true)
164+
whenever(fixture.rateLimiter.filter(eq(envelope), anyOrNull())).thenReturn(envelope)
165+
whenever(fixture.sentryOptions.envelopeDiskCache.storeEnvelope(any(), anyOrNull()))
166+
.thenReturn(true)
167+
whenever(fixture.connection.send(any())).thenReturn(TransportResult.error(429))
168+
169+
// when
170+
try {
171+
fixture.getSUT().send(envelope)
172+
} catch (e: IllegalStateException) {
173+
// expected - this is how the AsyncConnection signals failure to the executor for it to retry
174+
}
175+
176+
// then
177+
val order = inOrder(fixture.connection, fixture.sentryOptions.envelopeDiskCache)
178+
179+
// because storeBeforeSend is enabled by default
180+
order.verify(fixture.sentryOptions.envelopeDiskCache).storeEnvelope(eq(envelope), anyOrNull())
181+
182+
order.verify(fixture.connection).send(eq(envelope))
183+
order.verify(fixture.sentryOptions.envelopeDiskCache).discard(eq(envelope))
132184
}
133185

134186
@Test

0 commit comments

Comments
 (0)