Skip to content

Commit 747ff0b

Browse files
runningcodeclaudegetsentry-bot
authored
fix(core): Prevent recursion when a callback triggers another capture (#5737)
* fix(core): Prevent recursion when a callback triggers another capture A user beforeSend/beforeBreadcrumb/beforeSendLog callback that itself captures — directly, or transitively through a logging integration that routes back into Sentry (e.g. Timber, or the Gradle plugin's logcat instrumentation) — recursed until a StackOverflowError, because the callbacks run synchronously on the caller thread with no re-entrancy protection. Add a shared, thread-local SentryCallbackReentrancyGuard that is set only while a callback's execute() runs. Capture entry points (captureEvent, captureTransaction, captureLog, Scope.addBreadcrumb) drop nested captures while the guard is active, breaking the loop for every capture type at once. Dropping (rather than sending) the nested capture is intentional: bypassing beforeSend would send unscrubbed data. The nested capture is dropped, not sent. This also makes the per-integration guard in SentryLogcatAdapter redundant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(changelog): Add entry for callback re-entrancy guard Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 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> * fix(core): Harden re-entrancy guard with depth counter, wrap remaining callbacks Two robustness fixes for the callback re-entrancy guard: Replace the boolean flag with a depth counter so a nested exit() cannot disarm the guard while an outer callback is still running. The boolean relied on the "callbacks never nest" invariant, which every capture entry point must uphold by convention - a future capture path added without an entry check would silently re-open the recursion hole. The counter makes that failure mode structurally impossible, and lets exit() remove() the thread-local entry instead of parking a stale value on pooled threads. Wrap beforeErrorSampling and beforeEnvelopeCallback, the two remaining user callbacks in SentryClient without enter()/exit(). A capture from within beforeErrorSampling recursed unguarded (captureEvent -> beforeErrorSampling -> captureEvent -> ...). Both regression tests were verified to fail with StackOverflowError without the wraps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core): Hand out AutoCloseable token from re-entrancy guard Replace the manual enter()/finally-exit() pattern at every callback site with an ISentryLifecycleToken returned from enter(), used via try-with-resources. This removes nine copies of the finally boilerplate and the risk of forgetting the exit() call. The depth counter stays as the guard's state model - the token's close() just decrements it - so the nesting correctness guarantee is unchanged. The token is a shared static singleton, so no allocation happens per callback, matching the AutoClosableReentrantLock idiom already in the codebase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Format code * docs(changelog): Move re-entrancy entry to Unreleased The rebase folded the previous Unreleased section into the released 8.48.0, stranding the callback re-entrancy entry there. Move it back under Unreleased. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): Guard sendEnvelope against callback re-entrancy captureEnvelope and captureCheckIn route into sendEnvelope without the isActive() entry check every other capture path has. A beforeEnvelope callback that itself captured an envelope or check-in re-entered sendEnvelope, re-ran the callback, and recursed to a StackOverflowError - the same failure class the rest of this PR fixes. Check the guard at the top of sendEnvelope, the single choke point every send flows through, rather than adding a check to each public entry point. In normal flow the guard is already inactive there (the before* callback has exited), so an active guard can only mean a callback triggered the send, which is dropped. This also closes the hole for any future sender routed through sendEnvelope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(core): Drop redundant check-in re-entrancy test captureCheckIn and captureEnvelope both funnel into sendEnvelope and hit the same isActive() guard, so the check-in test exercised no code path the envelope test did not already cover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): Drop callback-recursion captures silently The re-entrancy guard dropped nested captures but logged a DEBUG line at seven of the eight drop sites. That log is itself routed back into Sentry by the same logging integrations this guard protects against (the Gradle plugin's logcat instrumentation feeds captureLog, whose own drop logs again), so logging on the drop path re-opens the very recursion the guard breaks. It only bites with SDK debug logging enabled, since the internal logger is otherwise a no-op, but dropping silently removes the failure mode outright. Drop without logging everywhere and document why in the guard's Javadoc and at each drop site. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(core): Document that captures inside before* callbacks are dropped A customer implementing beforeSend/beforeBreadcrumb/beforeSendLog/etc. has no way to know that capturing from within the callback — directly or through a logging integration — is silently dropped to prevent recursion. State the contract on each customer-facing callback interface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build(core): Update API dump for re-entrancy guard token The token refactor changed SentryCallbackReentrancyGuard.enter() to return an ISentryLifecycleToken and made exit() private, but the API dump was not regenerated at the time. The guard is @ApiStatus.Internal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
1 parent eb1a946 commit 747ff0b

10 files changed

Lines changed: 388 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
- Backfill release, environment, distribution, tags, and app version/build—and use the matching replay-on-error sample rate—for `ApplicationExitInfo` ANR and native crash events captured before SDK initialization, without reusing options cached by a later app update ([#5762](https://github.com/getsentry/sentry-java/pull/5762))
88
- `SentryTagModifierNode.isImportantForBounds` now matches the default behavior and returns `true` ([#5789](https://github.com/getsentry/sentry-java/pull/5789))
9+
- Prevent a `StackOverflowError` when a `beforeSend`, `beforeBreadcrumb`, `beforeSendLog`, or `beforeEnvelope` callback triggers another capture (directly or through a logging integration such as Timber) ([#5737](https://github.com/getsentry/sentry-java/pull/5737))
10+
- Captures made from within a user callback (event, transaction, breadcrumb, log, envelope, or check-in) are now dropped while that callback runs, instead of recursing. Captures made by event processors are unaffected.
911

1012
## 8.49.0
1113

sentry-android-timber/src/test/java/io/sentry/android/timber/SentryTimberIntegrationTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package io.sentry.android.timber
22

33
import io.sentry.IScopes
4+
import io.sentry.ITransportFactory
5+
import io.sentry.ScopesAdapter
6+
import io.sentry.Sentry
47
import io.sentry.SentryLevel
58
import io.sentry.SentryLogLevel
69
import io.sentry.SentryOptions
710
import io.sentry.protocol.SdkVersion
11+
import io.sentry.transport.ITransport
812
import kotlin.test.BeforeTest
913
import kotlin.test.Test
1014
import kotlin.test.assertEquals
1115
import kotlin.test.assertTrue
1216
import org.mockito.kotlin.any
1317
import org.mockito.kotlin.mock
1418
import org.mockito.kotlin.verify
19+
import org.mockito.kotlin.whenever
1520
import timber.log.Timber
1621

1722
class SentryTimberIntegrationTest {
@@ -112,4 +117,43 @@ class SentryTimberIntegrationTest {
112117

113118
assertTrue(fixture.options.sdkVersion!!.integrationSet.contains("Timber"))
114119
}
120+
121+
@Test
122+
fun `a beforeSend callback that logs via Timber does not recurse`() {
123+
// End-to-end guard against SDK-CRASHES-JAVA-3T3H style recursion: with a real Sentry instance,
124+
// a beforeSend callback that logs through the planted SentryTimberTree must not loop back into
125+
// capture forever.
126+
val transport = mock<ITransport>()
127+
val transportFactory = mock<ITransportFactory>()
128+
whenever(transportFactory.create(any(), any())).thenReturn(transport)
129+
130+
var beforeSendInvocations = 0
131+
Sentry.init { options ->
132+
options.dsn = "https://key@sentry.io/123"
133+
options.setTransportFactory(transportFactory)
134+
options.beforeSend = SentryOptions.BeforeSendCallback { event, _ ->
135+
beforeSendInvocations++
136+
Timber.e("logging from beforeSend")
137+
event
138+
}
139+
}
140+
Timber.plant(
141+
SentryTimberTree(
142+
ScopesAdapter.getInstance(),
143+
SentryLevel.ERROR,
144+
SentryLevel.INFO,
145+
SentryLogLevel.INFO,
146+
)
147+
)
148+
149+
try {
150+
Timber.e("outer error")
151+
152+
// Without the core re-entrancy guard this recurses until a StackOverflowError. The nested
153+
// Timber.e is dropped before its own beforeSend, so the callback runs exactly once.
154+
assertEquals(1, beforeSendInvocations)
155+
} finally {
156+
Sentry.close()
157+
}
158+
}
115159
}

sentry/api/sentry.api

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7924,6 +7924,11 @@ public final class io/sentry/util/ScopesUtil {
79247924
public static fun printScopesChain (Lio/sentry/IScopes;)V
79257925
}
79267926

7927+
public final class io/sentry/util/SentryCallbackReentrancyGuard {
7928+
public static fun enter ()Lio/sentry/ISentryLifecycleToken;
7929+
public static fun isActive ()Z
7930+
}
7931+
79277932
public final class io/sentry/util/SentryRandom {
79287933
public fun <init> ()V
79297934
public static fun current ()Lio/sentry/util/Random;

sentry/src/main/java/io/sentry/Scope.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.sentry.util.ExceptionUtils;
1717
import io.sentry.util.Objects;
1818
import io.sentry.util.Pair;
19+
import io.sentry.util.SentryCallbackReentrancyGuard;
1920
import java.lang.ref.WeakReference;
2021
import java.util.ArrayList;
2122
import java.util.Collection;
@@ -464,7 +465,7 @@ public Queue<Breadcrumb> getBreadcrumbs() {
464465
final @NotNull SentryOptions.BeforeBreadcrumbCallback callback,
465466
@NotNull Breadcrumb breadcrumb,
466467
final @NotNull Hint hint) {
467-
try {
468+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
468469
breadcrumb = callback.execute(breadcrumb, hint);
469470
} catch (Throwable e) {
470471
options
@@ -493,6 +494,10 @@ public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) {
493494
if (breadcrumb == null || breadcrumbs instanceof DisabledQueue) {
494495
return;
495496
}
497+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
498+
if (SentryCallbackReentrancyGuard.isActive()) {
499+
return;
500+
}
496501
SentryOptions.BeforeBreadcrumbCallback callback = options.getBeforeBreadcrumb();
497502
if (callback != null) {
498503
if (hint == null) {

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

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
107107
@NotNull SentryEvent event, final @Nullable IScope scope, @Nullable Hint hint) {
108108
Objects.requireNonNull(event, "SentryEvent is required.");
109109

110+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
111+
if (SentryCallbackReentrancyGuard.isActive()) {
112+
return SentryId.EMPTY_ID;
113+
}
114+
110115
if (hint == null) {
111116
hint = new Hint();
112117
}
@@ -236,7 +241,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
236241
final SentryReplayOptions.BeforeErrorSamplingCallback beforeErrorSampling =
237242
options.getSessionReplay().getBeforeErrorSampling();
238243
if (beforeErrorSampling != null) {
239-
try {
244+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
240245
shouldCaptureReplay = beforeErrorSampling.execute(event, hint);
241246
} catch (Throwable e) {
242247
options
@@ -312,6 +317,11 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin
312317
@NotNull SentryReplayEvent event, final @Nullable IScope scope, @Nullable Hint hint) {
313318
Objects.requireNonNull(event, "SessionReplay is required.");
314319

320+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
321+
if (SentryCallbackReentrancyGuard.isActive()) {
322+
return SentryId.EMPTY_ID;
323+
}
324+
315325
if (hint == null) {
316326
hint = new Hint();
317327
}
@@ -937,10 +947,19 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
937947

938948
private @NotNull SentryId sendEnvelope(
939949
@NotNull final SentryEnvelope envelope, @Nullable final Hint hint) throws IOException {
950+
// captureEnvelope and captureCheckIn have no entry-level guard, so a callback that captures
951+
// one of those would recurse back into beforeEnvelopeCallback. In normal flow the guard is
952+
// already inactive by the time we get here (the before* callback has exited), so an active
953+
// guard means a callback triggered this send. Drop silently: a log here can re-enter through a
954+
// logging integration.
955+
if (SentryCallbackReentrancyGuard.isActive()) {
956+
return SentryId.EMPTY_ID;
957+
}
958+
940959
final @Nullable SentryOptions.BeforeEnvelopeCallback beforeEnvelopeCallback =
941960
options.getBeforeEnvelopeCallback();
942961
if (beforeEnvelopeCallback != null) {
943-
try {
962+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
944963
beforeEnvelopeCallback.execute(envelope, hint);
945964
} catch (Throwable e) {
946965
options
@@ -969,6 +988,11 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
969988
final @Nullable ProfilingTraceData profilingTraceData) {
970989
Objects.requireNonNull(transaction, "Transaction is required.");
971990

991+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
992+
if (SentryCallbackReentrancyGuard.isActive()) {
993+
return SentryId.EMPTY_ID;
994+
}
995+
972996
if (hint == null) {
973997
hint = new Hint();
974998
}
@@ -1187,6 +1211,11 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
11871211
@Override
11881212
public @NotNull SentryId captureFeedback(
11891213
final @NotNull Feedback feedback, @Nullable Hint hint, final @NotNull IScope scope) {
1214+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
1215+
if (SentryCallbackReentrancyGuard.isActive()) {
1216+
return SentryId.EMPTY_ID;
1217+
}
1218+
11901219
SentryEvent event = new SentryEvent();
11911220
event.getContexts().setFeedback(feedback);
11921221

@@ -1297,6 +1326,11 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
12971326
@ApiStatus.Experimental
12981327
@Override
12991328
public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope) {
1329+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
1330+
if (SentryCallbackReentrancyGuard.isActive()) {
1331+
return;
1332+
}
1333+
13001334
if (logEvent != null && scope != null) {
13011335
logEvent = processLogEvent(logEvent, scope.getEventProcessors());
13021336
if (logEvent == null) {
@@ -1351,6 +1385,11 @@ public void captureMetric(
13511385
@Nullable SentryMetricsEvent metricsEvent,
13521386
final @Nullable IScope scope,
13531387
@Nullable Hint hint) {
1388+
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
1389+
if (SentryCallbackReentrancyGuard.isActive()) {
1390+
return;
1391+
}
1392+
13541393
if (hint == null) {
13551394
hint = new Hint();
13561395
}
@@ -1612,7 +1651,7 @@ private void sortBreadcrumbsByDate(
16121651
@NotNull SentryEvent event, final @NotNull Hint hint) {
16131652
final SentryOptions.BeforeSendCallback beforeSend = options.getBeforeSend();
16141653
if (beforeSend != null) {
1615-
try {
1654+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
16161655
event = beforeSend.execute(event, hint);
16171656
} catch (Throwable e) {
16181657
options
@@ -1634,7 +1673,7 @@ private void sortBreadcrumbsByDate(
16341673
final SentryOptions.BeforeSendTransactionCallback beforeSendTransaction =
16351674
options.getBeforeSendTransaction();
16361675
if (beforeSendTransaction != null) {
1637-
try {
1676+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
16381677
transaction = beforeSendTransaction.execute(transaction, hint);
16391678
} catch (Throwable e) {
16401679
options
@@ -1655,7 +1694,7 @@ private void sortBreadcrumbsByDate(
16551694
@NotNull SentryEvent event, final @NotNull Hint hint) {
16561695
final SentryOptions.BeforeSendCallback beforeSendFeedback = options.getBeforeSendFeedback();
16571696
if (beforeSendFeedback != null) {
1658-
try {
1697+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
16591698
event = beforeSendFeedback.execute(event, hint);
16601699
} catch (Throwable e) {
16611700
options
@@ -1673,7 +1712,7 @@ private void sortBreadcrumbsByDate(
16731712
@NotNull SentryReplayEvent event, final @NotNull Hint hint) {
16741713
final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay();
16751714
if (beforeSendReplay != null) {
1676-
try {
1715+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
16771716
event = beforeSendReplay.execute(event, hint);
16781717
} catch (Throwable e) {
16791718
options
@@ -1694,7 +1733,7 @@ private void sortBreadcrumbsByDate(
16941733
final SentryOptions.Logs.BeforeSendLogCallback beforeSendLog =
16951734
options.getLogs().getBeforeSend();
16961735
if (beforeSendLog != null) {
1697-
try {
1736+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
16981737
event = beforeSendLog.execute(event);
16991738
} catch (Throwable e) {
17001739
options
@@ -1716,7 +1755,7 @@ private void sortBreadcrumbsByDate(
17161755
final SentryOptions.Metrics.BeforeSendMetricCallback beforeSendMetric =
17171756
options.getMetrics().getBeforeSend();
17181757
if (beforeSendMetric != null) {
1719-
try {
1758+
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
17201759
event = beforeSendMetric.execute(event, hint);
17211760
} catch (Throwable e) {
17221761
options

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3327,6 +3327,10 @@ public interface BeforeSendCallback {
33273327
/**
33283328
* Mutates or drop an event before being sent
33293329
*
3330+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
3331+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
3332+
* prevent infinite recursion.
3333+
*
33303334
* @param event the event
33313335
* @param hint the hints
33323336
* @return the original event or the mutated event or null if event was dropped
@@ -3341,6 +3345,10 @@ public interface BeforeSendTransactionCallback {
33413345
/**
33423346
* Mutates or drop a transaction before being sent
33433347
*
3348+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
3349+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
3350+
* prevent infinite recursion.
3351+
*
33443352
* @param transaction the transaction
33453353
* @param hint the hints
33463354
* @return the original transaction or the mutated transaction or null if transaction was
@@ -3358,6 +3366,10 @@ public interface BeforeSendReplayCallback {
33583366
* for a single replay (i.e. segments), you can check {@link SentryReplayEvent#getReplayId()} to
33593367
* identify that the segments belong to the same replay.
33603368
*
3369+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
3370+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
3371+
* prevent infinite recursion.
3372+
*
33613373
* @param event the event
33623374
* @param hint the hint, contains {@link ReplayRecording}, can be accessed via {@link
33633375
* Hint#getReplayRecording()}
@@ -3373,6 +3385,10 @@ public interface BeforeBreadcrumbCallback {
33733385
/**
33743386
* Mutates or drop a callback before being added
33753387
*
3388+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
3389+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
3390+
* prevent infinite recursion.
3391+
*
33763392
* @param breadcrumb the breadcrumb
33773393
* @param hint the hints, usually the source of the breadcrumb
33783394
* @return the original breadcrumb or the mutated breadcrumb of null if breadcrumb was dropped
@@ -3961,6 +3977,10 @@ public interface BeforeSendLogCallback {
39613977
/**
39623978
* Mutates or drop a log event before being sent
39633979
*
3980+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
3981+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
3982+
* prevent infinite recursion.
3983+
*
39643984
* @param event the event
39653985
* @return the original log event or the mutated event or null if event was dropped
39663986
*/
@@ -4035,6 +4055,10 @@ public interface BeforeSendMetricCallback {
40354055
/**
40364056
* A callback which gets called right before a metric is about to be sent.
40374057
*
4058+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
4059+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
4060+
* prevent infinite recursion.
4061+
*
40384062
* @param metric the metric
40394063
* @return the original metric, mutated metric or null if metric was dropped
40404064
*/

sentry/src/main/java/io/sentry/SentryReplayOptions.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public interface BeforeErrorSamplingCallback {
2828
/**
2929
* Determines whether replay capture should proceed for the given error event.
3030
*
31+
* <p>Do not capture from within this callback — directly, or indirectly through a logging
32+
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
33+
* prevent infinite recursion.
34+
*
3135
* @param event the error event that triggered the replay capture
3236
* @param hint the hint associated with the event
3337
* @return {@code true} if the error sample rate should be checked, {@code false} to skip replay

0 commit comments

Comments
 (0)