Skip to content

Commit 67371e2

Browse files
runningcodeclaude
andcommitted
fix(core): Guard captureFeedback, captureReplayEvent, captureMetric too
These three capture methods had their beforeSend* executors wrapped by the re-entrancy guard but no entry check, so they were never dropped when a callback was active. That broke the "callbacks never nest" invariant: a callback that captured feedback/replay/metric ran that executor, whose exit() cleared the shared flag mid-callback, re-enabling recursion for any captureEvent/captureLog that followed in the same callback. They also recursed directly (e.g. beforeSendFeedback -> captureFeedback). Add the same isActive() entry guard to all three so every executor-wrapped capture path also drops while a callback runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1c810b5 commit 67371e2

2 files changed

Lines changed: 65 additions & 0 deletions

File tree

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin
309309
@NotNull SentryReplayEvent event, final @Nullable IScope scope, @Nullable Hint hint) {
310310
Objects.requireNonNull(event, "SessionReplay is required.");
311311

312+
if (SentryCallbackReentrancyGuard.isActive()) {
313+
options
314+
.getLogger()
315+
.log(
316+
SentryLevel.DEBUG,
317+
"Replay event captured from within a callback was dropped to prevent recursion.");
318+
return SentryId.EMPTY_ID;
319+
}
320+
312321
if (hint == null) {
313322
hint = new Hint();
314323
}
@@ -1189,6 +1198,15 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
11891198
@Override
11901199
public @NotNull SentryId captureFeedback(
11911200
final @NotNull Feedback feedback, @Nullable Hint hint, final @NotNull IScope scope) {
1201+
if (SentryCallbackReentrancyGuard.isActive()) {
1202+
options
1203+
.getLogger()
1204+
.log(
1205+
SentryLevel.DEBUG,
1206+
"Feedback captured from within a callback was dropped to prevent recursion.");
1207+
return SentryId.EMPTY_ID;
1208+
}
1209+
11921210
SentryEvent event = new SentryEvent();
11931211
event.getContexts().setFeedback(feedback);
11941212

@@ -1362,6 +1380,15 @@ public void captureMetric(
13621380
@Nullable SentryMetricsEvent metricsEvent,
13631381
final @Nullable IScope scope,
13641382
@Nullable Hint hint) {
1383+
if (SentryCallbackReentrancyGuard.isActive()) {
1384+
options
1385+
.getLogger()
1386+
.log(
1387+
SentryLevel.DEBUG,
1388+
"Metric captured from within a callback was dropped to prevent recursion.");
1389+
return;
1390+
}
1391+
13651392
if (hint == null) {
13661393
hint = new Hint();
13671394
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,44 @@ class SentryClientTest {
348348
verify(fixture.loggerBatchProcessor, times(1)).add(any())
349349
}
350350

351+
@Test
352+
fun `when beforeSend captures feedback before an event, the guard is not cleared prematurely`() {
353+
val scope = createScope()
354+
var invocations = 0
355+
lateinit var sut: SentryClient
356+
fixture.sentryOptions.setBeforeSend { e, _ ->
357+
invocations++
358+
// Capturing feedback must not clear the re-entrancy guard for captures that follow it in the
359+
// same callback, otherwise the captureEvent below would recurse.
360+
sut.captureFeedback(Feedback("feedback"), null, scope)
361+
sut.captureEvent(SentryEvent())
362+
e
363+
}
364+
sut = fixture.getSut()
365+
366+
sut.captureEvent(SentryEvent())
367+
368+
assertEquals(1, invocations)
369+
verify(fixture.transport, times(1)).send(any(), anyOrNull())
370+
}
371+
372+
@Test
373+
fun `when beforeSendFeedback captures feedback again, the nested capture is dropped and does not recurse`() {
374+
val scope = createScope()
375+
var invocations = 0
376+
lateinit var sut: SentryClient
377+
fixture.sentryOptions.setBeforeSendFeedback { e, _ ->
378+
invocations++
379+
sut.captureFeedback(Feedback("nested"), null, scope)
380+
e
381+
}
382+
sut = fixture.getSut()
383+
384+
sut.captureFeedback(Feedback("outer"), null, scope)
385+
386+
assertEquals(1, invocations)
387+
}
388+
351389
@Test
352390
fun `when beforeSendLog is set, callback is invoked`() {
353391
val scope = createScope()

0 commit comments

Comments
 (0)