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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ SentryAndroid.init(

- Avoid forking `rootScopes` for Reactor if current thread has `NoOpScopes` ([#4793](https://github.com/getsentry/sentry-java/pull/4793))
- This reduces the SDKs overhead by avoiding unnecessary scope forks
- Discard envelopes on `4xx` and `5xx` response ([#4950](https://github.com/getsentry/sentry-java/pull/4950))
- This aims to not overwhelm Sentry after an outage where too many events are sent at once
- We also do not have to re-send on a `4xx` since that's likely an SDK (e.g. wrong serialization) or envelope problem (e.g. too large) and sending it again won't change the result

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,17 +311,10 @@ public void run() {

// ignore e.g. 429 as we're not the ones actively dropping
if (result.getResponseCode() >= 400 && result.getResponseCode() != 429) {
if (!cached) {
HintUtils.runIfDoesNotHaveType(
hint,
Retryable.class,
(hint) -> {
options
.getClientReportRecorder()
.recordLostEnvelope(
DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
});
}
envelopeCache.discard(envelope);
options
.getClientReportRecorder()
.recordLostEnvelope(DiscardReason.NETWORK_ERROR, envelopeWithClientReport);
}

throw new IllegalStateException(message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import io.sentry.SentryEnvelope
import io.sentry.SentryOptions
import io.sentry.SentryOptionsManipulator
import io.sentry.Session
import io.sentry.cache.IEnvelopeCache
import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableHint
import io.sentry.clientreport.ClientReportTestHelper.Companion.retryableUncaughtExceptionHint
import io.sentry.clientreport.ClientReportTestHelper.Companion.uncaughtExceptionHint
import io.sentry.clientreport.DiscardReason
import io.sentry.clientreport.IClientReportRecorder
import io.sentry.dsnString
import io.sentry.hints.Retryable
import io.sentry.protocol.User
import io.sentry.util.HintUtils
import java.io.IOException
import kotlin.test.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.eq
Expand All @@ -31,11 +35,12 @@ class AsyncHttpTransportClientReportTest {
var transportGate = mock<ITransportGate>()
var executor = mock<QueuedThreadPoolExecutor>()
var rateLimiter = mock<RateLimiter>()
var envelopeCache = mock<IEnvelopeCache>()
var sentryOptions: SentryOptions =
SentryOptions().apply {
dsn = dsnString
setSerializer(mock())
setEnvelopeDiskCache(mock())
setEnvelopeDiskCache(envelopeCache)
}
var clientReportRecorder = mock<IClientReportRecorder>()
val envelopeBeforeAttachingClientReport =
Expand Down Expand Up @@ -66,23 +71,36 @@ class AsyncHttpTransportClientReportTest {
.attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport))
verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any())
verifyNoMoreInteractions(fixture.clientReportRecorder)
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
}

@Test
fun `does not record lost envelope on 500 error for retryable`() {
fun `records lost envelope on 500 error for retryable`() {
// given
givenSetup(TransportResult.error(500))
whenever(
fixture.envelopeCache.storeEnvelope(eq(fixture.envelopeBeforeAttachingClientReport), any())
)
.thenReturn(true)

// when
val retryableHint = retryableHint()
assertFailsWith(java.lang.IllegalStateException::class) {
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint())
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint)
}

// then
verify(fixture.clientReportRecorder, times(1))
.attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport))
verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any())
verify(fixture.clientReportRecorder, times(1))
.recordLostEnvelope(
eq(DiscardReason.NETWORK_ERROR),
same(fixture.envelopeAfterAttachingClientReport),
)
verifyNoMoreInteractions(fixture.clientReportRecorder)
val sentrySdkHint = HintUtils.getSentrySdkHint(retryableHint)
assertFalse((sentrySdkHint as Retryable).isRetry)
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
}

@Test
Expand All @@ -104,23 +122,36 @@ class AsyncHttpTransportClientReportTest {
same(fixture.envelopeAfterAttachingClientReport),
)
verifyNoMoreInteractions(fixture.clientReportRecorder)
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
}

@Test
fun `does not record lost envelope on 400 error for retryable`() {
fun `records lost envelope on 400 error for retryable`() {
// given
givenSetup(TransportResult.error(400))
whenever(
fixture.envelopeCache.storeEnvelope(eq(fixture.envelopeBeforeAttachingClientReport), any())
)
.thenReturn(true)

// when
val retryableHint = retryableHint()
assertFailsWith(java.lang.IllegalStateException::class) {
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint())
fixture.getSUT().send(fixture.envelopeBeforeAttachingClientReport, retryableHint)
}

// then
verify(fixture.clientReportRecorder, times(1))
.attachReportToEnvelope(same(fixture.envelopeBeforeAttachingClientReport))
verify(fixture.clientReportRecorder, never()).recordLostEnvelope(any(), any())
verify(fixture.clientReportRecorder, times(1))
.recordLostEnvelope(
eq(DiscardReason.NETWORK_ERROR),
same(fixture.envelopeAfterAttachingClientReport),
)
verifyNoMoreInteractions(fixture.clientReportRecorder)
val sentrySdkHint = HintUtils.getSentrySdkHint(retryableHint)
assertFalse((sentrySdkHint as Retryable).isRetry)
verify(fixture.envelopeCache).discard(fixture.envelopeBeforeAttachingClientReport)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class AsyncHttpTransportTest {
}

@Test
fun `stores envelope after unsuccessful send`() {
fun `discards envelope after unsuccessful send 500`() {
// given
val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null)
whenever(fixture.transportGate.isConnected).thenReturn(true)
Expand All @@ -127,6 +127,58 @@ class AsyncHttpTransportTest {
// because storeBeforeSend is enabled by default
order.verify(fixture.sentryOptions.envelopeDiskCache).storeEnvelope(eq(envelope), anyOrNull())

order.verify(fixture.connection).send(eq(envelope))
order.verify(fixture.sentryOptions.envelopeDiskCache).discard(eq(envelope))
}

@Test
fun `discards envelope after unsuccessful send 400`() {
// given
val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null)
whenever(fixture.transportGate.isConnected).thenReturn(true)
whenever(fixture.rateLimiter.filter(eq(envelope), anyOrNull())).thenReturn(envelope)
whenever(fixture.connection.send(any())).thenReturn(TransportResult.error(400))

// when
try {
fixture.getSUT().send(envelope)
} catch (e: IllegalStateException) {
// expected - this is how the AsyncConnection signals failure to the executor for it to retry
}

// then
val order = inOrder(fixture.connection, fixture.sentryOptions.envelopeDiskCache)

// because storeBeforeSend is enabled by default
order.verify(fixture.sentryOptions.envelopeDiskCache).storeEnvelope(eq(envelope), anyOrNull())

order.verify(fixture.connection).send(eq(envelope))
order.verify(fixture.sentryOptions.envelopeDiskCache).discard(eq(envelope))
}

@Test
fun `stores envelope after unsuccessful send 429`() {
// given
val envelope = SentryEnvelope.from(fixture.sentryOptions.serializer, createSession(), null)
whenever(fixture.transportGate.isConnected).thenReturn(true)
whenever(fixture.rateLimiter.filter(eq(envelope), anyOrNull())).thenReturn(envelope)
whenever(fixture.sentryOptions.envelopeDiskCache.storeEnvelope(any(), anyOrNull()))
.thenReturn(true)
whenever(fixture.connection.send(any())).thenReturn(TransportResult.error(429))

// when
try {
fixture.getSUT().send(envelope)
} catch (e: IllegalStateException) {
// expected - this is how the AsyncConnection signals failure to the executor for it to retry
}

// then
val order = inOrder(fixture.connection, fixture.sentryOptions.envelopeDiskCache)

// because storeBeforeSend is enabled by default
order.verify(fixture.sentryOptions.envelopeDiskCache).storeEnvelope(eq(envelope), anyOrNull())

order.verify(fixture.connection).send(eq(envelope))
verify(fixture.sentryOptions.envelopeDiskCache, never()).discard(any())
}
Expand Down
Loading