From a9eeb78ab98a039c5d1bc462ec36cf4db34f1a26 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 29 Aug 2025 18:11:35 +0200 Subject: [PATCH 1/6] When a crash occurs, logs are flushed --- .../src/main/java/io/sentry/SentryClient.java | 6 ++++++ .../test/java/io/sentry/SentryClientTest.kt | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 998f1c5d8db..6f6dc1e7462 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -245,6 +245,12 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul finalizeTransaction(scope, hint); } + // if event is backfillable or cached we don't need to flush the logs, because it's an event + // from the past. Otherwise we need to flush the logs to ensure they are sent on crash + if (event != null && !isBackfillable && !isCached && event.isCrashed()) { + loggerBatchProcessor.flush(options.getFlushTimeoutMillis()); + } + return sentryId; } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 878eb2d4aa7..662b2bb795f 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -3023,6 +3023,25 @@ class SentryClientTest { assertTrue(terminated == true) } + @Test + fun `flush logs for crash events`() { + val sut = fixture.getSut() + val batchProcessor = mock() + sut.injectForField("loggerBatchProcessor", batchProcessor) + sut.captureLog( + SentryLogEvent(SentryId(), SentryNanotimeDate(), "message", SentryLogLevel.WARN), + fixture.scopes.scope, + ) + + sut.captureEvent( + SentryEvent().apply { + exceptions = + listOf(SentryException().apply { mechanism = Mechanism().apply { isHandled = false } }) + } + ) + verify(batchProcessor).flush(any()) + } + @Test fun `cleans up replay folder for Backfillable replay events`() { val dir = File(tmpDir.newFolder().absolutePath) From bfc3606ff9b4800da785476e80e78acbda94ddec Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 29 Aug 2025 18:16:48 +0200 Subject: [PATCH 2/6] updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce808f2be5e..ca6f0f7b52a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - Replace `UUIDGenerator` implementation with Apache licensed code ([#4662](https://github.com/getsentry/sentry-java/pull/4662)) - Replace `Random` implementation with MIT licensed code ([#4664](https://github.com/getsentry/sentry-java/pull/4664)) +### Fixes + +- Flush logs on crash ([#4684](https://github.com/getsentry/sentry-java/pull/4684)) + ## 8.20.0 ### Fixes From a5e99b0fbed29f115a7e355878658b1eb78b78cb Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 1 Sep 2025 10:46:11 +0200 Subject: [PATCH 3/6] set a 500 timeout millis to flush the logs on crash moved replay capture after sending the crash --- .../src/main/java/io/sentry/SentryClient.java | 28 +++++++++---------- .../test/java/io/sentry/SentryClientTest.kt | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 6f6dc1e7462..266cc9fa85e 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -36,6 +36,8 @@ public final class SentryClient implements ISentryClient { static final String SENTRY_PROTOCOL_VERSION = "7"; + private static final int LOG_FLUSH_ON_CRASH_TIMEOUT_MILLIS = 500; + private boolean enabled; private final @NotNull SentryOptions options; @@ -211,16 +213,6 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul sentryId = event.getEventId(); } - final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); - final boolean isCached = - HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); - // if event is backfillable or cached we don't wanna trigger capture replay, because it's - // an event from the past. If it's cached, but with ApplyScopeData, it comes from the outbox - // folder and we still want to capture replay (e.g. a native captureException error) - if (event != null && !isBackfillable && !isCached && (event.isErrored() || event.isCrashed())) { - options.getReplayController().captureReplay(event.isCrashed()); - } - try { final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event); final boolean shouldSendAttachments = event != null; @@ -245,10 +237,18 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul finalizeTransaction(scope, hint); } - // if event is backfillable or cached we don't need to flush the logs, because it's an event - // from the past. Otherwise we need to flush the logs to ensure they are sent on crash - if (event != null && !isBackfillable && !isCached && event.isCrashed()) { - loggerBatchProcessor.flush(options.getFlushTimeoutMillis()); + final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); + final boolean isCached = + HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); + // if event is backfillable or cached we don't wanna trigger capture replay, because it's + // an event from the past. If it's cached, but with ApplyScopeData, it comes from the outbox + // folder and we still want to capture replay (e.g. a native captureException error) + if (event != null && !isBackfillable && !isCached && (event.isErrored() || event.isCrashed())) { + options.getReplayController().captureReplay(event.isCrashed()); + // We need to flush the logs to ensure they are sent on crash + if (event.isCrashed()) { + loggerBatchProcessor.flush(LOG_FLUSH_ON_CRASH_TIMEOUT_MILLIS); + } } return sentryId; diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 662b2bb795f..c45dd1002fc 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -3039,7 +3039,7 @@ class SentryClientTest { listOf(SentryException().apply { mechanism = Mechanism().apply { isHandled = false } }) } ) - verify(batchProcessor).flush(any()) + verify(batchProcessor).flush(eq(500)) } @Test From cba83bf283ac7c36acfa4ee8e0d190510caef876 Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 1 Sep 2025 11:05:54 +0200 Subject: [PATCH 4/6] set a 500 timeout millis to flush the logs on crash moved replay capture after sending the crash --- sentry/src/main/java/io/sentry/SentryClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 266cc9fa85e..11d9fc4741e 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -213,6 +213,10 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul sentryId = event.getEventId(); } + final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); + final boolean isCached = + HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); + try { final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event); final boolean shouldSendAttachments = event != null; @@ -236,10 +240,6 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul if (scope != null) { finalizeTransaction(scope, hint); } - - final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); - final boolean isCached = - HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); // if event is backfillable or cached we don't wanna trigger capture replay, because it's // an event from the past. If it's cached, but with ApplyScopeData, it comes from the outbox // folder and we still want to capture replay (e.g. a native captureException error) From a940a9fe1d02e6127fa19a9b30ac52edc8c24619 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 1 Sep 2025 09:08:56 +0000 Subject: [PATCH 5/6] Format code --- sentry/src/main/java/io/sentry/SentryClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 11d9fc4741e..a65a49a3702 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -215,7 +215,7 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); final boolean isCached = - HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); + HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); try { final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event); From c76fe23c6a38d517a76fa601564f5a479a1eb881 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 4 Sep 2025 11:30:47 +0200 Subject: [PATCH 6/6] reverted replay capturing order --- .../src/main/java/io/sentry/SentryClient.java | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index a65a49a3702..43a5a684235 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -216,6 +216,12 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul final boolean isBackfillable = HintUtils.hasType(hint, Backfillable.class); final boolean isCached = HintUtils.hasType(hint, Cached.class) && !HintUtils.hasType(hint, ApplyScopeData.class); + // if event is backfillable or cached we don't wanna trigger capture replay, because it's + // an event from the past. If it's cached, but with ApplyScopeData, it comes from the outbox + // folder and we still want to capture replay (e.g. a native captureException error) + if (event != null && !isBackfillable && !isCached && (event.isErrored() || event.isCrashed())) { + options.getReplayController().captureReplay(event.isCrashed()); + } try { final @Nullable TraceContext traceContext = getTraceContext(scope, hint, event); @@ -240,15 +246,10 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul if (scope != null) { finalizeTransaction(scope, hint); } - // if event is backfillable or cached we don't wanna trigger capture replay, because it's - // an event from the past. If it's cached, but with ApplyScopeData, it comes from the outbox - // folder and we still want to capture replay (e.g. a native captureException error) - if (event != null && !isBackfillable && !isCached && (event.isErrored() || event.isCrashed())) { - options.getReplayController().captureReplay(event.isCrashed()); - // We need to flush the logs to ensure they are sent on crash - if (event.isCrashed()) { - loggerBatchProcessor.flush(LOG_FLUSH_ON_CRASH_TIMEOUT_MILLIS); - } + // if event is backfillable or cached, it's an event from the past. + // Otherwise, we want to flush logs, as we encountered a crash. + if (event != null && !isBackfillable && !isCached && event.isCrashed()) { + loggerBatchProcessor.flush(LOG_FLUSH_ON_CRASH_TIMEOUT_MILLIS); } return sentryId;