Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- 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))
- `SentryTagModifierNode.isImportantForBounds` now matches the default behavior and returns `true` ([#5789](https://github.com/getsentry/sentry-java/pull/5789))
- 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))
- 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.

## 8.49.0

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package io.sentry.android.timber

import io.sentry.IScopes
import io.sentry.ITransportFactory
import io.sentry.ScopesAdapter
import io.sentry.Sentry
import io.sentry.SentryLevel
import io.sentry.SentryLogLevel
import io.sentry.SentryOptions
import io.sentry.protocol.SdkVersion
import io.sentry.transport.ITransport
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import timber.log.Timber

class SentryTimberIntegrationTest {
Expand Down Expand Up @@ -112,4 +117,43 @@ class SentryTimberIntegrationTest {

assertTrue(fixture.options.sdkVersion!!.integrationSet.contains("Timber"))
}

@Test
fun `a beforeSend callback that logs via Timber does not recurse`() {
// End-to-end guard against SDK-CRASHES-JAVA-3T3H style recursion: with a real Sentry instance,
// a beforeSend callback that logs through the planted SentryTimberTree must not loop back into
// capture forever.
val transport = mock<ITransport>()
val transportFactory = mock<ITransportFactory>()
whenever(transportFactory.create(any(), any())).thenReturn(transport)

var beforeSendInvocations = 0
Sentry.init { options ->
options.dsn = "https://key@sentry.io/123"
options.setTransportFactory(transportFactory)
options.beforeSend = SentryOptions.BeforeSendCallback { event, _ ->
beforeSendInvocations++
Timber.e("logging from beforeSend")
event
}
}
Timber.plant(
SentryTimberTree(
ScopesAdapter.getInstance(),
SentryLevel.ERROR,
SentryLevel.INFO,
SentryLogLevel.INFO,
)
)

try {
Timber.e("outer error")

// Without the core re-entrancy guard this recurses until a StackOverflowError. The nested
// Timber.e is dropped before its own beforeSend, so the callback runs exactly once.
assertEquals(1, beforeSendInvocations)
} finally {
Sentry.close()
}
}
}
5 changes: 5 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -7924,6 +7924,11 @@ public final class io/sentry/util/ScopesUtil {
public static fun printScopesChain (Lio/sentry/IScopes;)V
}

public final class io/sentry/util/SentryCallbackReentrancyGuard {
public static fun enter ()Lio/sentry/ISentryLifecycleToken;
public static fun isActive ()Z
}

public final class io/sentry/util/SentryRandom {
public fun <init> ()V
public static fun current ()Lio/sentry/util/Random;
Expand Down
7 changes: 6 additions & 1 deletion sentry/src/main/java/io/sentry/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.sentry.util.ExceptionUtils;
import io.sentry.util.Objects;
import io.sentry.util.Pair;
import io.sentry.util.SentryCallbackReentrancyGuard;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -464,7 +465,7 @@ public Queue<Breadcrumb> getBreadcrumbs() {
final @NotNull SentryOptions.BeforeBreadcrumbCallback callback,
@NotNull Breadcrumb breadcrumb,
final @NotNull Hint hint) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
breadcrumb = callback.execute(breadcrumb, hint);
} catch (Throwable e) {
options
Expand Down Expand Up @@ -493,6 +494,10 @@ public void addBreadcrumb(@NotNull Breadcrumb breadcrumb, @Nullable Hint hint) {
if (breadcrumb == null || breadcrumbs instanceof DisabledQueue) {
return;
}
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return;
}
SentryOptions.BeforeBreadcrumbCallback callback = options.getBeforeBreadcrumb();
if (callback != null) {
if (hint == null) {
Expand Down
55 changes: 47 additions & 8 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
@NotNull SentryEvent event, final @Nullable IScope scope, @Nullable Hint hint) {
Objects.requireNonNull(event, "SentryEvent is required.");

// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return SentryId.EMPTY_ID;
}

if (hint == null) {
hint = new Hint();
}
Expand Down Expand Up @@ -236,7 +241,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
final SentryReplayOptions.BeforeErrorSamplingCallback beforeErrorSampling =
options.getSessionReplay().getBeforeErrorSampling();
if (beforeErrorSampling != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
shouldCaptureReplay = beforeErrorSampling.execute(event, hint);
} catch (Throwable e) {
options
Expand Down Expand Up @@ -312,6 +317,11 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin
@NotNull SentryReplayEvent event, final @Nullable IScope scope, @Nullable Hint hint) {
Objects.requireNonNull(event, "SessionReplay is required.");

// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return SentryId.EMPTY_ID;
}

if (hint == null) {
hint = new Hint();
}
Expand Down Expand Up @@ -937,10 +947,19 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint

private @NotNull SentryId sendEnvelope(
@NotNull final SentryEnvelope envelope, @Nullable final Hint hint) throws IOException {
// captureEnvelope and captureCheckIn have no entry-level guard, so a callback that captures
// one of those would recurse back into beforeEnvelopeCallback. In normal flow the guard is
// already inactive by the time we get here (the before* callback has exited), so an active
// guard means a callback triggered this send. Drop silently: a log here can re-enter through a
// logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return SentryId.EMPTY_ID;
}

final @Nullable SentryOptions.BeforeEnvelopeCallback beforeEnvelopeCallback =
options.getBeforeEnvelopeCallback();
if (beforeEnvelopeCallback != null) {
try {
Comment thread
cursor[bot] marked this conversation as resolved.
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
beforeEnvelopeCallback.execute(envelope, hint);
} catch (Throwable e) {
options
Expand Down Expand Up @@ -969,6 +988,11 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
final @Nullable ProfilingTraceData profilingTraceData) {
Objects.requireNonNull(transaction, "Transaction is required.");

// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return SentryId.EMPTY_ID;
}

if (hint == null) {
hint = new Hint();
}
Expand Down Expand Up @@ -1187,6 +1211,11 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
@Override
public @NotNull SentryId captureFeedback(
final @NotNull Feedback feedback, @Nullable Hint hint, final @NotNull IScope scope) {
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return SentryId.EMPTY_ID;
}

SentryEvent event = new SentryEvent();
event.getContexts().setFeedback(feedback);

Expand Down Expand Up @@ -1297,6 +1326,11 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
@ApiStatus.Experimental
@Override
public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope) {
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return;
}

if (logEvent != null && scope != null) {
logEvent = processLogEvent(logEvent, scope.getEventProcessors());
if (logEvent == null) {
Expand Down Expand Up @@ -1351,6 +1385,11 @@ public void captureMetric(
@Nullable SentryMetricsEvent metricsEvent,
final @Nullable IScope scope,
@Nullable Hint hint) {
// Drop silently to prevent recursion; a log here can re-enter through a logging integration.
if (SentryCallbackReentrancyGuard.isActive()) {
return;
}

if (hint == null) {
hint = new Hint();
}
Expand Down Expand Up @@ -1612,7 +1651,7 @@ private void sortBreadcrumbsByDate(
@NotNull SentryEvent event, final @NotNull Hint hint) {
final SentryOptions.BeforeSendCallback beforeSend = options.getBeforeSend();
if (beforeSend != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
event = beforeSend.execute(event, hint);
} catch (Throwable e) {
options
Expand All @@ -1634,7 +1673,7 @@ private void sortBreadcrumbsByDate(
final SentryOptions.BeforeSendTransactionCallback beforeSendTransaction =
options.getBeforeSendTransaction();
if (beforeSendTransaction != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
transaction = beforeSendTransaction.execute(transaction, hint);
} catch (Throwable e) {
options
Expand All @@ -1655,7 +1694,7 @@ private void sortBreadcrumbsByDate(
@NotNull SentryEvent event, final @NotNull Hint hint) {
final SentryOptions.BeforeSendCallback beforeSendFeedback = options.getBeforeSendFeedback();
if (beforeSendFeedback != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
event = beforeSendFeedback.execute(event, hint);
} catch (Throwable e) {
options
Expand All @@ -1673,7 +1712,7 @@ private void sortBreadcrumbsByDate(
@NotNull SentryReplayEvent event, final @NotNull Hint hint) {
final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay();
if (beforeSendReplay != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
event = beforeSendReplay.execute(event, hint);
} catch (Throwable e) {
options
Expand All @@ -1694,7 +1733,7 @@ private void sortBreadcrumbsByDate(
final SentryOptions.Logs.BeforeSendLogCallback beforeSendLog =
options.getLogs().getBeforeSend();
if (beforeSendLog != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
event = beforeSendLog.execute(event);
} catch (Throwable e) {
options
Expand All @@ -1716,7 +1755,7 @@ private void sortBreadcrumbsByDate(
final SentryOptions.Metrics.BeforeSendMetricCallback beforeSendMetric =
options.getMetrics().getBeforeSend();
if (beforeSendMetric != null) {
try {
try (final @NotNull ISentryLifecycleToken ignored = SentryCallbackReentrancyGuard.enter()) {
event = beforeSendMetric.execute(event, hint);
} catch (Throwable e) {
options
Expand Down
24 changes: 24 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -3327,6 +3327,10 @@ public interface BeforeSendCallback {
/**
* Mutates or drop an event before being sent
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param event the event
* @param hint the hints
* @return the original event or the mutated event or null if event was dropped
Expand All @@ -3341,6 +3345,10 @@ public interface BeforeSendTransactionCallback {
/**
* Mutates or drop a transaction before being sent
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param transaction the transaction
* @param hint the hints
* @return the original transaction or the mutated transaction or null if transaction was
Expand All @@ -3358,6 +3366,10 @@ public interface BeforeSendReplayCallback {
* for a single replay (i.e. segments), you can check {@link SentryReplayEvent#getReplayId()} to
* identify that the segments belong to the same replay.
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param event the event
* @param hint the hint, contains {@link ReplayRecording}, can be accessed via {@link
* Hint#getReplayRecording()}
Expand All @@ -3373,6 +3385,10 @@ public interface BeforeBreadcrumbCallback {
/**
* Mutates or drop a callback before being added
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param breadcrumb the breadcrumb
* @param hint the hints, usually the source of the breadcrumb
* @return the original breadcrumb or the mutated breadcrumb of null if breadcrumb was dropped
Expand Down Expand Up @@ -3961,6 +3977,10 @@ public interface BeforeSendLogCallback {
/**
* Mutates or drop a log event before being sent
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param event the event
* @return the original log event or the mutated event or null if event was dropped
*/
Expand Down Expand Up @@ -4035,6 +4055,10 @@ public interface BeforeSendMetricCallback {
/**
* A callback which gets called right before a metric is about to be sent.
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param metric the metric
* @return the original metric, mutated metric or null if metric was dropped
*/
Expand Down
4 changes: 4 additions & 0 deletions sentry/src/main/java/io/sentry/SentryReplayOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ public interface BeforeErrorSamplingCallback {
/**
* Determines whether replay capture should proceed for the given error event.
*
* <p>Do not capture from within this callback — directly, or indirectly through a logging
* integration that routes logs back into Sentry. Such nested captures are silently dropped to
* prevent infinite recursion.
*
* @param event the error event that triggered the replay capture
* @param hint the hint associated with the event
* @return {@code true} if the error sample rate should be checked, {@code false} to skip replay
Expand Down
Loading
Loading