Skip to content

Commit 136073f

Browse files
authored
Merge pull request #6165 from getsentry/fix/callback-error-handling-replay-sampling
fix(core): [Callback Errors 7] Handle replay sampling failures
2 parents 5b0d221 + 287418b commit 136073f

3 files changed

Lines changed: 71 additions & 11 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
- Add `DiscardReason.CALLBACK_ERROR` and use it for telemetry dropped when a `beforeSend*` callback throws. `OnDiscardCallback` can now receive this value.
6565
- Drop telemetry and record `callback_error` when a customer event processor throws instead of continuing with a potentially partially processed item. SDK-owned processor failures are logged and processing continues without a `callback_error` client report.
6666
- Drop breadcrumbs when `beforeBreadcrumb` throws instead of storing exception details on the breadcrumb.
67+
- Skip replay capture when `beforeErrorSampling` throws, while still sending the error event ([#6165](https://github.com/getsentry/sentry-java/pull/6165))
6768
- When `tracesSampler` throws, drop the transaction and record `callback_error` instead of inheriting the parent sampling decision or falling back to `tracesSampleRate` ([#6163](https://github.com/getsentry/sentry-java/pull/6163))
6869
- When `profilesSampler` throws, disable profiling instead of falling back to `profilesSampleRate` or inheriting the parent's profiling decision. Trace sampling is unchanged ([#6164](https://github.com/getsentry/sentry-java/pull/6164))
6970
- 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)

‎sentry/src/main/java/io/sentry/SentryClient.java‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,14 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
233233
.getLogger()
234234
.log(
235235
SentryLevel.ERROR,
236-
"The beforeErrorSampling callback threw an exception. Proceeding with replay capture.",
236+
"The beforeErrorSampling callback threw an exception. Skipping replay capture.",
237237
e);
238-
shouldCaptureReplay = true;
238+
if (!SentryId.EMPTY_ID.equals(options.getReplayController().getReplayId())) {
239+
options
240+
.getClientReportRecorder()
241+
.recordLostEvent(DiscardReason.CALLBACK_ERROR, DataCategory.Replay);
242+
}
243+
shouldCaptureReplay = false;
239244
}
240245
}
241246
if (shouldCaptureReplay) {

‎sentry/src/test/java/io/sentry/SentryClientTest.kt‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3973,24 +3973,78 @@ class SentryClientTest {
39733973
}
39743974

39753975
@Test
3976-
fun `beforeErrorSampling throwing exception proceeds with captureReplay`() {
3977-
var called = false
3978-
fixture.sentryOptions.setReplayController(
3979-
object : ReplayController by NoOpReplayController.getInstance() {
3980-
override fun captureReplay(isTerminating: Boolean?): SentryId {
3981-
called = true
3982-
return SentryId.EMPTY_ID
3976+
fun `beforeErrorSampling throwing exception skips replay but still sends errors and crashes`() {
3977+
val replayController = mock<ReplayController>()
3978+
whenever(replayController.replayId).thenReturn(SentryId())
3979+
whenever(replayController.captureReplay(anyOrNull())).thenReturn(SentryId.EMPTY_ID)
3980+
fixture.sentryOptions.setReplayController(replayController)
3981+
val logger = mock<ILogger>()
3982+
fixture.sentryOptions.setLogger(logger)
3983+
val exception = RuntimeException("test")
3984+
fixture.sentryOptions.sessionReplay.beforeErrorSampling =
3985+
SentryReplayOptions.BeforeErrorSamplingCallback { _, _ ->
3986+
throw exception
3987+
}
3988+
val sut = fixture.getSut()
3989+
val events =
3990+
listOf(true, false).map { handled ->
3991+
SentryEvent().apply {
3992+
exceptions =
3993+
listOf(
3994+
SentryException().apply { mechanism = Mechanism().apply { isHandled = handled } }
3995+
)
39833996
}
39843997
}
3998+
3999+
events.forEach { event ->
4000+
assertThat(sut.captureEvent(event)).isEqualTo(event.eventId)
4001+
}
4002+
4003+
verify(replayController, never()).captureReplay(anyOrNull())
4004+
val envelopes = argumentCaptor<SentryEnvelope>()
4005+
verify(fixture.transport, times(2)).send(envelopes.capture(), anyOrNull())
4006+
assertThat(envelopes.allValues.map { getEventFromData(it.items.first().data).eventId })
4007+
.containsExactlyElementsIn(events.map { it.eventId })
4008+
.inOrder()
4009+
verify(logger, times(2))
4010+
.log(
4011+
SentryLevel.ERROR,
4012+
"The beforeErrorSampling callback threw an exception. Skipping replay capture.",
4013+
exception,
4014+
)
4015+
assertClientReport(
4016+
fixture.sentryOptions.clientReportRecorder,
4017+
listOf(DiscardedEvent(DiscardReason.CALLBACK_ERROR.reason, DataCategory.Replay.category, 2)),
39854018
)
4019+
}
4020+
4021+
@Test
4022+
fun `beforeErrorSampling throwing exception does not prevent replay capture for subsequent events`() {
4023+
val replayController = mock<ReplayController>()
4024+
whenever(replayController.replayId).thenReturn(SentryId.EMPTY_ID)
4025+
whenever(replayController.captureReplay(anyOrNull())).thenReturn(SentryId.EMPTY_ID)
4026+
fixture.sentryOptions.setReplayController(replayController)
4027+
var invocations = 0
39864028
fixture.sentryOptions.sessionReplay.beforeErrorSampling =
39874029
SentryReplayOptions.BeforeErrorSamplingCallback { _, _ ->
3988-
throw RuntimeException("test")
4030+
invocations++
4031+
if (invocations == 1) {
4032+
throw RuntimeException("test")
4033+
}
4034+
true
39894035
}
39904036
val sut = fixture.getSut()
39914037

39924038
sut.captureEvent(SentryEvent().apply { exceptions = listOf(SentryException()) })
3993-
assertTrue(called)
4039+
verify(replayController, never()).captureReplay(anyOrNull())
4040+
4041+
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
4042+
assertThat(sut.captureEvent(event)).isEqualTo(event.eventId)
4043+
4044+
assertThat(invocations).isEqualTo(2)
4045+
verify(replayController).captureReplay(false)
4046+
verify(fixture.transport, times(2)).send(any(), anyOrNull())
4047+
assertClientReport(fixture.sentryOptions.clientReportRecorder, emptyList())
39944048
}
39954049

39964050
@Test

0 commit comments

Comments
 (0)