Skip to content
Draft
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 @@ -124,6 +124,7 @@

- Fix SDK callback error handling ([#6140](https://github.com/getsentry/sentry-java/pull/6140))
- Add `DiscardReason.CALLBACK_ERROR` and use it for telemetry dropped when a `beforeSend*` callback throws. `OnDiscardCallback` can now receive this value.
- Drop telemetry and record `callback_error` when an event processor throws instead of continuing with a potentially partially processed item.
- Disable URL caching when reading `META-INF/MANIFEST.MF` files during version detection so that the SDK no longer keeps jar file handles open for the life of the process ([#6124](https://github.com/getsentry/sentry-java/pull/6124)
- Keep the `EventListener` wrapped by `SentryOkHttpEventListener` per `Call` ([#6003](https://github.com/getsentry/sentry-java/pull/6003))

Expand Down
24 changes: 24 additions & 0 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,10 @@ private SentryEvent processEvent(
e,
"An exception occurred while processing event by processor: %s",
processor.getClass().getName());
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.CALLBACK_ERROR, DataCategory.Error);
return null;
}

if (event == null) {
Expand Down Expand Up @@ -557,6 +561,8 @@ private SentryLogEvent processLogEvent(
e,
"An exception occurred while processing log event by processor: %s",
processor.getClass().getName());
recordLostLogEvent(DiscardReason.CALLBACK_ERROR, eventBeforeProcessor);
return null;
}

if (event == null) {
Expand Down Expand Up @@ -590,6 +596,8 @@ private SentryMetricsEvent processMetricsEvent(
e,
"An exception occurred while processing metrics event by processor: %s",
processor.getClass().getName());
recordLostMetricsEvent(DiscardReason.CALLBACK_ERROR, eventBeforeProcessor);
return null;
}

if (event == null) {
Expand Down Expand Up @@ -622,6 +630,14 @@ private SentryMetricsEvent processMetricsEvent(
e,
"An exception occurred while processing transaction by processor: %s",
processor.getClass().getName());
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.CALLBACK_ERROR, DataCategory.Transaction);
options
.getClientReportRecorder()
.recordLostEvent(
DiscardReason.CALLBACK_ERROR, DataCategory.Span, spanCountBeforeProcessor + 1);
return null;
}
final int spanCountAfterProcessor = transaction == null ? 0 : transaction.getSpans().size();

Expand Down Expand Up @@ -675,6 +691,10 @@ private SentryReplayEvent processReplayEvent(
e,
"An exception occurred while processing replay event by processor: %s",
processor.getClass().getName());
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.CALLBACK_ERROR, DataCategory.Replay);
return null;
}

if (replayEvent == null) {
Expand Down Expand Up @@ -709,6 +729,10 @@ private SentryEvent processFeedbackEvent(
e,
"An exception occurred while processing feedback event by processor: %s",
processor.getClass().getName());
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.CALLBACK_ERROR, DataCategory.Feedback);
return null;
}

if (feedbackEvent == null) {
Expand Down
218 changes: 206 additions & 12 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.sentry

import com.google.common.truth.Truth.assertThat
import io.sentry.Scope.IWithPropagationContext
import io.sentry.SentryLevel.WARNING
import io.sentry.Session.State.Crashed
Expand Down Expand Up @@ -474,6 +475,48 @@ class SentryClientTest {
)
}

@Test
fun `throwing log processor drops log and stops callbacks`() {
val scope = createScope()
val logEvent = SentryLogEvent(SentryId(), SentryNanotimeDate(), "message", SentryLogLevel.WARN)
val logEventNumberOfBytes =
JsonSerializationUtils.byteSizeOf(
fixture.sentryOptions.serializer,
fixture.sentryOptions.logger,
logEvent,
)
val throwingProcessor = mock<EventProcessor>()
val nextProcessor = mock<EventProcessor>()
val beforeSend = mock<SentryOptions.Logs.BeforeSendLogCallback>()
val onDiscard = mock<SentryOptions.OnDiscardCallback>()
whenever(throwingProcessor.process(any<SentryLogEvent>()))
.thenThrow(IllegalStateException("test"))
scope.addEventProcessor(throwingProcessor)
scope.addEventProcessor(nextProcessor)
fixture.sentryOptions.logs.beforeSend = beforeSend
fixture.sentryOptions.onDiscard = onDiscard

fixture.getSut().captureLog(logEvent, scope)

verify(nextProcessor, never()).process(any<SentryLogEvent>())
verify(beforeSend, never()).execute(any())
verify(fixture.loggerBatchProcessor, never()).add(any())
assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(
DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.LogItem.category, 1),
DiscardedEvent(
DiscardReason.CALLBACK_ERROR.reason,
DataCategory.LogByte.category,
logEventNumberOfBytes,
),
),
)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.LogItem, 1)
verify(onDiscard)
.execute(DiscardReason.CALLBACK_ERROR, DataCategory.LogByte, logEventNumberOfBytes)
}

@Test
fun `when beforeSendLog is returns new instance, new instance is sent`() {
val scope = createScope()
Expand Down Expand Up @@ -595,6 +638,56 @@ class SentryClientTest {
)
}

@Test
fun `throwing metric processor drops metric and stops callbacks`() {
val scope = createScope()
val metricsEvent = SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0)
val metricsEventNumberOfBytes =
JsonSerializationUtils.byteSizeOf(
fixture.sentryOptions.serializer,
fixture.sentryOptions.logger,
metricsEvent,
)
val throwingProcessor = mock<EventProcessor>()
val nextProcessor = mock<EventProcessor>()
val beforeSend = mock<SentryOptions.Metrics.BeforeSendMetricCallback>()
val onDiscard = mock<SentryOptions.OnDiscardCallback>()
whenever(throwingProcessor.process(any<SentryMetricsEvent>(), anyOrNull()))
.thenThrow(IllegalStateException("test"))
scope.addEventProcessor(throwingProcessor)
scope.addEventProcessor(nextProcessor)
fixture.sentryOptions.metrics.beforeSend = beforeSend
fixture.sentryOptions.onDiscard = onDiscard

fixture.getSut().captureMetric(metricsEvent, scope, null)

verify(nextProcessor, never()).process(any<SentryMetricsEvent>(), anyOrNull())
verify(beforeSend, never()).execute(any(), anyOrNull())
verify(fixture.metricsBatchProcessor, never()).add(any())
assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(
DiscardedEvent(
DiscardReason.CALLBACK_ERROR.reason,
DataCategory.TraceMetric.category,
1,
),
DiscardedEvent(
DiscardReason.CALLBACK_ERROR.reason,
DataCategory.TraceMetricByte.category,
metricsEventNumberOfBytes,
),
),
)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.TraceMetric, 1)
verify(onDiscard)
.execute(
DiscardReason.CALLBACK_ERROR,
DataCategory.TraceMetricByte,
metricsEventNumberOfBytes,
)
}

@Test
fun `when beforeSendMetric is returns new instance, new instance is sent`() {
val scope = createScope()
Expand Down Expand Up @@ -1242,6 +1335,42 @@ class SentryClientTest {
)
}

@Test
fun `throwing transaction processor drops transaction and stops callbacks`() {
val throwingProcessor = mock<EventProcessor>()
val nextProcessor = mock<EventProcessor>()
val beforeSend = mock<SentryOptions.BeforeSendTransactionCallback>()
val onDiscard = mock<SentryOptions.OnDiscardCallback>()
whenever(throwingProcessor.process(any<SentryTransaction>(), anyOrNull()))
.thenThrow(IllegalStateException("test"))
fixture.sentryOptions.addEventProcessor(throwingProcessor)
fixture.sentryOptions.addEventProcessor(nextProcessor)
fixture.sentryOptions.beforeSendTransaction = beforeSend
fixture.sentryOptions.onDiscard = onDiscard

val id =
fixture
.getSut()
.captureTransaction(
SentryTransaction(fixture.sentryTracer),
fixture.sentryTracer.traceContext(),
)

assertThat(id).isEqualTo(SentryId.EMPTY_ID)
verify(nextProcessor, never()).process(any<SentryTransaction>(), anyOrNull())
verify(beforeSend, never()).execute(any(), anyOrNull())
verify(fixture.transport, never()).send(any(), anyOrNull())
assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(
DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.Transaction.category, 1),
DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.Span.category, 2),
),
)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.Transaction, 1)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.Span, 2)
}

@Test
fun `transaction dropped by ignoredTransactions is recorded`() {
fixture.sentryOptions.setIgnoredTransactions(listOf("a-transaction"))
Expand Down Expand Up @@ -1927,10 +2056,29 @@ class SentryClientTest {
}

@Test
fun `exception thrown by an event processor is handled gracefully`() {
fixture.sentryOptions.addEventProcessor(eventProcessorThrows())
val sut = fixture.getSut()
sut.captureEvent(SentryEvent())
fun `exception thrown by an event processor drops event and stops callbacks`() {
val throwingProcessor = mock<EventProcessor>()
val nextProcessor = mock<EventProcessor>()
val beforeSend = mock<SentryOptions.BeforeSendCallback>()
val onDiscard = mock<SentryOptions.OnDiscardCallback>()
whenever(throwingProcessor.process(any<SentryEvent>(), anyOrNull()))
.thenThrow(IllegalStateException("test"))
fixture.sentryOptions.addEventProcessor(throwingProcessor)
fixture.sentryOptions.addEventProcessor(nextProcessor)
fixture.sentryOptions.beforeSend = beforeSend
fixture.sentryOptions.onDiscard = onDiscard

val id = fixture.getSut().captureEvent(SentryEvent())

assertThat(id).isEqualTo(SentryId.EMPTY_ID)
verify(nextProcessor, never()).process(any<SentryEvent>(), anyOrNull())
verify(beforeSend, never()).execute(any(), anyOrNull())
verify(fixture.transport, never()).send(any(), anyOrNull())
assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.Error.category, 1)),
)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.Error, 1)
}

@Test
Expand Down Expand Up @@ -3524,6 +3672,32 @@ class SentryClientTest {
verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Replay, 1)
}

@Test
fun `throwing replay processor drops replay and stops callbacks`() {
val throwingProcessor = mock<EventProcessor>()
val nextProcessor = mock<EventProcessor>()
val beforeSend = mock<SentryOptions.BeforeSendReplayCallback>()
val onDiscard = mock<SentryOptions.OnDiscardCallback>()
whenever(throwingProcessor.process(any<SentryReplayEvent>(), anyOrNull()))
.thenThrow(IllegalStateException("test"))
fixture.sentryOptions.addEventProcessor(throwingProcessor)
fixture.sentryOptions.addEventProcessor(nextProcessor)
fixture.sentryOptions.beforeSendReplay = beforeSend
fixture.sentryOptions.onDiscard = onDiscard

val id = fixture.getSut().captureReplayEvent(createReplayEvent(), createScope(), null)

assertThat(id).isEqualTo(SentryId.EMPTY_ID)
verify(nextProcessor, never()).process(any<SentryReplayEvent>(), anyOrNull())
verify(beforeSend, never()).execute(any(), anyOrNull())
verify(fixture.transport, never()).send(any(), anyOrNull())
assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.Replay.category, 1)),
)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.Replay, 1)
}

@Test
fun `calls captureReplay on replay controller for error events`() {
var called = false
Expand Down Expand Up @@ -4086,6 +4260,34 @@ class SentryClientTest {
verify(onDiscardMock, times(1)).execute(DiscardReason.EVENT_PROCESSOR, DataCategory.Feedback, 1)
}

@Test
fun `throwing feedback processor drops feedback and stops callbacks`() {
val throwingProcessor = mock<EventProcessor>()
val nextProcessor = mock<EventProcessor>()
val beforeSend = mock<SentryOptions.BeforeSendCallback>()
val onDiscard = mock<SentryOptions.OnDiscardCallback>()
whenever(throwingProcessor.process(any<SentryEvent>(), anyOrNull()))
.thenThrow(IllegalStateException("test"))
fixture.sentryOptions.addEventProcessor(throwingProcessor)
fixture.sentryOptions.addEventProcessor(nextProcessor)
fixture.sentryOptions.beforeSendFeedback = beforeSend
fixture.sentryOptions.onDiscard = onDiscard

val id = fixture.getSut().captureFeedback(Feedback("message"), null, createScope())

assertThat(id).isEqualTo(SentryId.EMPTY_ID)
verify(nextProcessor, never()).process(any<SentryEvent>(), anyOrNull())
verify(beforeSend, never()).execute(any(), anyOrNull())
verify(fixture.transport, never()).send(any(), anyOrNull())
assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(
DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.Feedback.category, 1)
),
)
verify(onDiscard).execute(DiscardReason.CALLBACK_ERROR, DataCategory.Feedback, 1)
}

// endregion

private fun givenScopeWithStartedSession(
Expand Down Expand Up @@ -4352,14 +4554,6 @@ class SentryClientTest {
override fun timestamp(): Long? = null
}

private fun eventProcessorThrows(): EventProcessor {
return object : EventProcessor {
override fun process(event: SentryEvent, hint: Hint): SentryEvent? {
throw Throwable()
}
}
}

private class BackfillableHint : Backfillable {
override fun shouldEnrich(): Boolean = false
}
Expand Down
Loading