From 4a51ece82276853b2bfd0eb1a24ea87297c5ba14 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 19 Sep 2025 14:43:45 +0200 Subject: [PATCH 1/9] added session replay id to logs --- .../core/AndroidContinuousProfiler.java | 4 +-- .../sentry/samples/android/MainActivity.java | 4 +++ .../main/java/io/sentry/logger/LoggerApi.java | 8 +++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 34 +++++++++++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index a3fb6f6c8db..9a924aa0b91 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -208,11 +208,11 @@ private void start() { isRunning = true; - if (profilerId == SentryId.EMPTY_ID) { + if (profilerId.equals(SentryId.EMPTY_ID)) { profilerId = new SentryId(); } - if (chunkId == SentryId.EMPTY_ID) { + if (chunkId.equals(SentryId.EMPTY_ID)) { chunkId = new SentryId(); } diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index 1b9acc3c267..9b3bdb1d93d 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -11,6 +11,7 @@ import io.sentry.ISpan; import io.sentry.MeasurementUnit; import io.sentry.Sentry; +import io.sentry.SentryLogLevel; import io.sentry.instrumentation.file.SentryFileOutputStream; import io.sentry.protocol.Feedback; import io.sentry.protocol.User; @@ -304,7 +305,10 @@ public void run() { Sentry.replay().enableDebugMaskingOverlay(); }); + Sentry.logger().log(SentryLogLevel.INFO, "Creating content view"); setContentView(binding.getRoot()); + + Sentry.logger().log(SentryLogLevel.INFO, "MainActivity created"); } private void stackOverflow() { diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 84abfa71b2e..383d8b12f55 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -211,6 +211,14 @@ private void captureLog( "sentry.environment", new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment)); } + + final @Nullable SentryId replayId = scopes.getScope().getReplayId(); + if (!replayId.equals(SentryId.EMPTY_ID)) { + attributes.put( + "sentry.replay_id", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, replayId.toString())); + } + final @Nullable String release = scopes.getOptions().getRelease(); if (release != null) { attributes.put( diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 716bda99aea..349ee87ae33 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2927,6 +2927,40 @@ class ScopesTest { ) } + @Test + fun `adds session replay id to log attributes`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + val replayId = SentryId() + sut.scope.replayId = replayId + sut.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .captureLog( + check { + assertEquals("log message", it.body) + val logReplayId = it.attributes?.get("sentry.replay_id")!! + assertEquals(replayId.toString(), logReplayId.value) + }, + anyOrNull(), + ) + } + + @Test + fun `missing session replay id do not break attributes`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + sut.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .captureLog( + check { + assertEquals("log message", it.body) + val logReplayId = it.attributes?.get("sentry.replay_id") + assertNull(logReplayId) + }, + anyOrNull(), + ) + } + // endregion @Test From 0e97cfeb149bd79c90196c491ffcbb38c3ddae72 Mon Sep 17 00:00:00 2001 From: Stefano Date: Fri, 19 Sep 2025 14:48:38 +0200 Subject: [PATCH 2/9] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 972c1e20522..9175592b82a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Add session replay id to Sentry Logs ([#4740](https://github.com/getsentry/sentry-java/pull/4740)) - Move SentryLogs out of experimental ([#4710](https://github.com/getsentry/sentry-java/pull/4710)) - Add support for w3c traceparent header ([#4671](https://github.com/getsentry/sentry-java/pull/4671)) - This feature is disabled by default. If enabled, outgoing requests will include the w3c `traceparent` header. From 4b76e00cf164a0e3170a1f769f771d883857e61c Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 25 Sep 2025 15:49:19 +0200 Subject: [PATCH 3/9] added replay type to the scope added sentry._internal.replay_is_buffering to logs when replay is in buffer mode --- .../replay/capture/BufferCaptureStrategy.kt | 8 ++- .../replay/capture/SessionCaptureStrategy.kt | 6 +- .../capture/BufferCaptureStrategyTest.kt | 10 ++++ .../capture/SessionCaptureStrategyTest.kt | 4 ++ sentry/api/sentry.api | 8 +++ .../java/io/sentry/CombinedScopeView.java | 18 ++++++ sentry/src/main/java/io/sentry/IScope.java | 18 ++++++ sentry/src/main/java/io/sentry/NoOpScope.java | 8 +++ sentry/src/main/java/io/sentry/Scope.java | 14 +++++ .../main/java/io/sentry/logger/LoggerApi.java | 6 ++ .../java/io/sentry/CombinedScopeViewTest.kt | 45 ++++++++++++++ .../src/test/java/io/sentry/NoOpScopeTest.kt | 11 ++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 58 +++++++++++++++++++ 13 files changed, 212 insertions(+), 2 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt index 706a958f3f8..9d09c0b09c9 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt @@ -10,6 +10,7 @@ import io.sentry.SentryLevel.ERROR import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions import io.sentry.SentryReplayEvent.ReplayType.BUFFER +import io.sentry.SentryReplayEvent.ReplayType.SESSION import io.sentry.android.replay.ReplayCache import io.sentry.android.replay.ScreenshotRecorderConfig import io.sentry.android.replay.capture.CaptureStrategy.Companion.rotateEvents @@ -82,7 +83,10 @@ internal class BufferCaptureStrategy( // write replayId to scope right away, so it gets picked up by the event that caused buffer // to flush - scopes?.configureScope { it.replayId = currentReplayId } + scopes?.configureScope { + it.replayId = currentReplayId + it.replayType = replayType + } if (isTerminating) { this.isTerminating.set(true) @@ -152,6 +156,8 @@ internal class BufferCaptureStrategy( replayId = currentReplayId, replayType = BUFFER, ) + // The type on the scope should change, as logs read it + scopes?.configureScope { it.replayType = SESSION } return captureStrategy } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt index cc007d07067..2278c2addf9 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt @@ -33,6 +33,7 @@ internal class SessionCaptureStrategy( // tagged with the replay that might never be sent when we're recording in buffer mode scopes?.configureScope { it.replayId = currentReplayId + it.replayType = this.replayType screenAtStart = it.screen?.substringAfterLast('.') } } @@ -57,7 +58,10 @@ internal class SessionCaptureStrategy( currentSegment = -1 FileUtils.deleteRecursively(replayCacheDir) } - scopes?.configureScope { it.replayId = SentryId.EMPTY_ID } + scopes?.configureScope { + it.replayId = SentryId.EMPTY_ID + it.replayType = null + } super.stop() } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt index b3fb9058a95..6f693491357 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt @@ -25,6 +25,7 @@ import java.io.File import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNull import kotlin.test.assertTrue import org.awaitility.kotlin.await import org.junit.Rule @@ -139,6 +140,8 @@ class BufferCaptureStrategyTest { assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) assertEquals(replayId, strategy.currentReplayId) assertEquals(0, strategy.currentSegment) + assertNull(fixture.scope.replayType) + assertEquals(ReplayType.BUFFER, strategy.replayType) } @Test @@ -239,10 +242,15 @@ class BufferCaptureStrategyTest { fun `convert converts to session strategy and sets replayId to scope`() { val strategy = fixture.getSut() strategy.start() + assertNull(fixture.scope.replayType) + assertEquals(ReplayType.BUFFER, strategy.replayType) val converted = strategy.convert() assertTrue(converted is SessionCaptureStrategy) assertEquals(strategy.currentReplayId, fixture.scope.replayId) + // Type of strategy is kept buffer, but type on the scope is updated to session + assertEquals(ReplayType.BUFFER, strategy.replayType) + assertEquals(ReplayType.SESSION, fixture.scope.replayType) } @Test @@ -330,6 +338,7 @@ class BufferCaptureStrategyTest { strategy.captureReplay(false) {} assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) + assertNull(fixture.scope.replayType) } @Test @@ -346,6 +355,7 @@ class BufferCaptureStrategyTest { // buffered + current = 2 verify(fixture.scopes, times(2)).captureReplay(any(), any()) assertEquals(strategy.currentReplayId, fixture.scope.replayId) + assertEquals(ReplayType.BUFFER, fixture.scope.replayType) assertTrue(called) } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt index af30a5b73f7..1a101f34188 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt @@ -139,6 +139,8 @@ class SessionCaptureStrategyTest { assertEquals(replayId, fixture.scope.replayId) assertEquals(replayId, strategy.currentReplayId) + assertEquals(ReplayType.SESSION, fixture.scope.replayType) + assertEquals(ReplayType.SESSION, strategy.replayType) assertEquals(0, strategy.currentSegment) } @@ -200,6 +202,8 @@ class SessionCaptureStrategyTest { .captureReplay(argThat { event -> event is SentryReplayEvent && event.segmentId == 0 }, any()) assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) assertEquals(SentryId.EMPTY_ID, strategy.currentReplayId) + assertNull(fixture.scope.replayType) + assertEquals(ReplayType.SESSION, strategy.replayType) assertEquals(-1, strategy.currentSegment) assertFalse(currentReplay.exists()) verify(fixture.replayCache).close() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 527461870aa..7a505d8caa2 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -285,6 +285,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getReplayId ()Lio/sentry/protocol/SentryId; + public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; public fun getSession ()Lio/sentry/Session; @@ -311,6 +312,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun setLevel (Lio/sentry/SentryLevel;)V public fun setPropagationContext (Lio/sentry/PropagationContext;)V public fun setReplayId (Lio/sentry/protocol/SentryId;)V + public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setScreen (Ljava/lang/String;)V public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V @@ -851,6 +853,7 @@ public abstract interface class io/sentry/IScope { public abstract fun getOptions ()Lio/sentry/SentryOptions; public abstract fun getPropagationContext ()Lio/sentry/PropagationContext; public abstract fun getReplayId ()Lio/sentry/protocol/SentryId; + public abstract fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public abstract fun getRequest ()Lio/sentry/protocol/Request; public abstract fun getScreen ()Ljava/lang/String; public abstract fun getSession ()Lio/sentry/Session; @@ -877,6 +880,7 @@ public abstract interface class io/sentry/IScope { public abstract fun setLevel (Lio/sentry/SentryLevel;)V public abstract fun setPropagationContext (Lio/sentry/PropagationContext;)V public abstract fun setReplayId (Lio/sentry/protocol/SentryId;)V + public abstract fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public abstract fun setRequest (Lio/sentry/protocol/Request;)V public abstract fun setScreen (Ljava/lang/String;)V public abstract fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V @@ -1619,6 +1623,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getReplayId ()Lio/sentry/protocol/SentryId; + public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; public fun getSession ()Lio/sentry/Session; @@ -1645,6 +1650,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun setLevel (Lio/sentry/SentryLevel;)V public fun setPropagationContext (Lio/sentry/PropagationContext;)V public fun setReplayId (Lio/sentry/protocol/SentryId;)V + public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setScreen (Ljava/lang/String;)V public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V @@ -2272,6 +2278,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getReplayId ()Lio/sentry/protocol/SentryId; + public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; public fun getSession ()Lio/sentry/Session; @@ -2298,6 +2305,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun setLevel (Lio/sentry/SentryLevel;)V public fun setPropagationContext (Lio/sentry/PropagationContext;)V public fun setReplayId (Lio/sentry/protocol/SentryId;)V + public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setScreen (Ljava/lang/String;)V public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index d6ac5b824a9..db75424c2b8 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -507,4 +507,22 @@ public void replaceOptions(@NotNull SentryOptions options) { public void setReplayId(@NotNull SentryId replayId) { getDefaultWriteScope().setReplayId(replayId); } + + @Override + public @Nullable SentryReplayEvent.ReplayType getReplayType() { + final @Nullable SentryReplayEvent.ReplayType current = scope.getReplayType(); + if (current != null) { + return current; + } + final @Nullable SentryReplayEvent.ReplayType isolation = isolationScope.getReplayType(); + if (isolation != null) { + return isolation; + } + return globalScope.getReplayType(); + } + + @Override + public void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType) { + getDefaultWriteScope().setReplayType(replayType); + } } diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index ddabd00569e..81fd4111c8a 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -106,6 +106,24 @@ public interface IScope { @ApiStatus.Internal void setReplayId(final @NotNull SentryId replayId); + /** + * Returns the Scope's current replayType, previously set by {@link + * IScope#setReplayType(SentryReplayEvent.ReplayType)} + * + * @return the type of the current session replay + */ + @ApiStatus.Internal + @Nullable + SentryReplayEvent.ReplayType getReplayType(); + + /** + * Sets the Scope's current replayType + * + * @param replayType the type of the current session replay + */ + @ApiStatus.Internal + void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType); + /** * Returns the Scope's request * diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index dd1a202b548..fe24dbd46f5 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -83,6 +83,14 @@ public void setScreen(@Nullable String screen) {} @Override public void setReplayId(@Nullable SentryId replayId) {} + @Override + public @Nullable SentryReplayEvent.ReplayType getReplayType() { + return null; + } + + @Override + public void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType) {} + @Override public @Nullable Request getRequest() { return null; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 7a54c4c755a..864b164e0e1 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -98,6 +98,9 @@ public final class Scope implements IScope { /** Scope's session replay id */ private @NotNull SentryId replayId = SentryId.EMPTY_ID; + /** Scope's session replay type */ + private @Nullable SentryReplayEvent.ReplayType replayType = null; + private @NotNull ISentryClient client = NoOpSentryClient.getInstance(); private final @NotNull Map, String>> throwableToSpan = @@ -128,6 +131,7 @@ private Scope(final @NotNull Scope scope) { this.user = userRef != null ? new User(userRef) : null; this.screen = scope.screen; this.replayId = scope.replayId; + this.replayType = scope.replayType; final Request requestRef = scope.request; this.request = requestRef != null ? new Request(requestRef) : null; @@ -363,6 +367,16 @@ public void setReplayId(final @NotNull SentryId replayId) { } } + @Override + public @Nullable SentryReplayEvent.ReplayType getReplayType() { + return replayType; + } + + @Override + public void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType) { + this.replayType = replayType; + } + /** * Returns the Scope's request * diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 383d8b12f55..10a44c8cef7 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -14,6 +14,7 @@ import io.sentry.SentryLogEventAttributeValue; import io.sentry.SentryLogLevel; import io.sentry.SentryOptions; +import io.sentry.SentryReplayEvent; import io.sentry.SpanId; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; @@ -217,6 +218,11 @@ private void captureLog( attributes.put( "sentry.replay_id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, replayId.toString())); + if (scopes.getScope().getReplayType() == SentryReplayEvent.ReplayType.BUFFER) { + attributes.put( + "sentry._internal.replay_is_buffering", + new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true)); + } } final @Nullable String release = scopes.getOptions().getRelease(); diff --git a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt index baeb076463f..84a1296fd7d 100644 --- a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt +++ b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt @@ -1185,6 +1185,51 @@ class CombinedScopeViewTest { assertEquals(SentryId.EMPTY_ID, fixture.globalScope.replayId) } + @Test + fun `prefers replay type from current scope`() { + val combined = fixture.getSut() + fixture.scope.replayType = SentryReplayEvent.ReplayType.BUFFER + fixture.isolationScope.replayType = SentryReplayEvent.ReplayType.SESSION + fixture.globalScope.replayType = SentryReplayEvent.ReplayType.SESSION + + assertEquals(SentryReplayEvent.ReplayType.BUFFER, combined.replayType) + } + + @Test + fun `uses isolation scope replay type if none in current scope`() { + val combined = fixture.getSut() + fixture.isolationScope.replayType = SentryReplayEvent.ReplayType.SESSION + fixture.globalScope.replayType = SentryReplayEvent.ReplayType.BUFFER + + assertEquals(SentryReplayEvent.ReplayType.SESSION, combined.replayType) + } + + @Test + fun `uses global scope replay type if none in current or isolation scope`() { + val combined = fixture.getSut() + fixture.globalScope.replayType = SentryReplayEvent.ReplayType.BUFFER + + assertEquals(SentryReplayEvent.ReplayType.BUFFER, combined.replayType) + } + + @Test + fun `returns null replay type if none in any scope`() { + val combined = fixture.getSut() + + assertNull(combined.replayType) + } + + @Test + fun `set replay type modifies default scope`() { + val combined = fixture.getSut() + combined.replayType = SentryReplayEvent.ReplayType.BUFFER + + assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) + assertNull(fixture.scope.replayType) + assertEquals(SentryReplayEvent.ReplayType.BUFFER, fixture.isolationScope.replayType) + assertNull(fixture.globalScope.replayType) + } + @Test fun `null tags do not cause NPE`() { val scope = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/NoOpScopeTest.kt b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt index ac17d2a5c0a..7a34d269d53 100644 --- a/sentry/src/test/java/io/sentry/NoOpScopeTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt @@ -1,6 +1,7 @@ package io.sentry import io.sentry.Scope.IWithSession +import io.sentry.protocol.SentryId import kotlin.test.assertEquals import kotlin.test.assertNull import kotlin.test.assertSame @@ -120,4 +121,14 @@ class NoOpScopeTest { } @Test fun `clone returns the same instance`() = assertSame(NoOpScope.getInstance(), sut.clone()) + + @Test + fun `getReplayId returns empty id`() { + assertEquals(SentryId.EMPTY_ID, sut.replayId) + } + + @Test + fun `getReplayType returns null`() { + assertNull(sut.replayType) + } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 349ee87ae33..bd93bbddb74 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2961,6 +2961,64 @@ class ScopesTest { ) } + @Test + fun `does not add session replay type to log attributes if no replay id`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + sut.scope.replayType = SentryReplayEvent.ReplayType.BUFFER + + sut.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .captureLog( + check { + assertEquals("log message", it.body) + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering") + assertNull(logReplayType) + }, + anyOrNull(), + ) + } + + @Test + fun `does not add session replay type to log attributes if replay type is session`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + val replayId = SentryId() + sut.scope.replayId = replayId + sut.scope.replayType = SentryReplayEvent.ReplayType.SESSION + + sut.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .captureLog( + check { + assertEquals("log message", it.body) + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering") + assertNull(logReplayType) + }, + anyOrNull(), + ) + } + + @Test + fun `adds session replay type to log attributes if replay type is buffer`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + val replayId = SentryId() + sut.scope.replayId = replayId + sut.scope.replayType = SentryReplayEvent.ReplayType.BUFFER + + sut.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .captureLog( + check { + assertEquals("log message", it.body) + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering")!! + assertTrue(logReplayType.value as Boolean) + }, + anyOrNull(), + ) + } + // endregion @Test From 7a58bcecfd5dea70a22cbd51da3e6f033ea33f04 Mon Sep 17 00:00:00 2001 From: Stefano Date: Thu, 25 Sep 2025 15:55:54 +0200 Subject: [PATCH 4/9] replaced scopes.getScope with scopes.getCombinedScopeView --- sentry/src/main/java/io/sentry/logger/LoggerApi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 10a44c8cef7..0d3fdbdf3d7 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -213,12 +213,12 @@ private void captureLog( new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment)); } - final @Nullable SentryId replayId = scopes.getScope().getReplayId(); + final @Nullable SentryId replayId = scopes.getCombinedScopeView().getReplayId(); if (!replayId.equals(SentryId.EMPTY_ID)) { attributes.put( "sentry.replay_id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, replayId.toString())); - if (scopes.getScope().getReplayType() == SentryReplayEvent.ReplayType.BUFFER) { + if (scopes.getCombinedScopeView().getReplayType() == SentryReplayEvent.ReplayType.BUFFER) { attributes.put( "sentry._internal.replay_is_buffering", new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true)); From 7065f47018b70928f6a4d28823ffaa1aa51551b3 Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 30 Sep 2025 18:26:33 +0200 Subject: [PATCH 5/9] removed replayType from scope sentry._internal.replay_is_buffering is attached to logs if replay id is not in scope, but is in replay controller --- .../replay/capture/BufferCaptureStrategy.kt | 8 +--- .../replay/capture/SessionCaptureStrategy.kt | 6 +-- .../capture/BufferCaptureStrategyTest.kt | 10 ----- .../capture/SessionCaptureStrategyTest.kt | 4 -- sentry/api/sentry.api | 8 ---- .../java/io/sentry/CombinedScopeView.java | 18 -------- sentry/src/main/java/io/sentry/IScope.java | 18 -------- sentry/src/main/java/io/sentry/NoOpScope.java | 8 ---- sentry/src/main/java/io/sentry/Scope.java | 14 ------ .../main/java/io/sentry/logger/LoggerApi.java | 12 ++--- .../java/io/sentry/CombinedScopeViewTest.kt | 45 ------------------- .../src/test/java/io/sentry/NoOpScopeTest.kt | 5 --- sentry/src/test/java/io/sentry/ScopesTest.kt | 20 +++++---- 13 files changed, 21 insertions(+), 155 deletions(-) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt index 9d09c0b09c9..706a958f3f8 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt @@ -10,7 +10,6 @@ import io.sentry.SentryLevel.ERROR import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions import io.sentry.SentryReplayEvent.ReplayType.BUFFER -import io.sentry.SentryReplayEvent.ReplayType.SESSION import io.sentry.android.replay.ReplayCache import io.sentry.android.replay.ScreenshotRecorderConfig import io.sentry.android.replay.capture.CaptureStrategy.Companion.rotateEvents @@ -83,10 +82,7 @@ internal class BufferCaptureStrategy( // write replayId to scope right away, so it gets picked up by the event that caused buffer // to flush - scopes?.configureScope { - it.replayId = currentReplayId - it.replayType = replayType - } + scopes?.configureScope { it.replayId = currentReplayId } if (isTerminating) { this.isTerminating.set(true) @@ -156,8 +152,6 @@ internal class BufferCaptureStrategy( replayId = currentReplayId, replayType = BUFFER, ) - // The type on the scope should change, as logs read it - scopes?.configureScope { it.replayType = SESSION } return captureStrategy } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt index 2278c2addf9..cc007d07067 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt @@ -33,7 +33,6 @@ internal class SessionCaptureStrategy( // tagged with the replay that might never be sent when we're recording in buffer mode scopes?.configureScope { it.replayId = currentReplayId - it.replayType = this.replayType screenAtStart = it.screen?.substringAfterLast('.') } } @@ -58,10 +57,7 @@ internal class SessionCaptureStrategy( currentSegment = -1 FileUtils.deleteRecursively(replayCacheDir) } - scopes?.configureScope { - it.replayId = SentryId.EMPTY_ID - it.replayType = null - } + scopes?.configureScope { it.replayId = SentryId.EMPTY_ID } super.stop() } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt index 6f693491357..b3fb9058a95 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt @@ -25,7 +25,6 @@ import java.io.File import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse -import kotlin.test.assertNull import kotlin.test.assertTrue import org.awaitility.kotlin.await import org.junit.Rule @@ -140,8 +139,6 @@ class BufferCaptureStrategyTest { assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) assertEquals(replayId, strategy.currentReplayId) assertEquals(0, strategy.currentSegment) - assertNull(fixture.scope.replayType) - assertEquals(ReplayType.BUFFER, strategy.replayType) } @Test @@ -242,15 +239,10 @@ class BufferCaptureStrategyTest { fun `convert converts to session strategy and sets replayId to scope`() { val strategy = fixture.getSut() strategy.start() - assertNull(fixture.scope.replayType) - assertEquals(ReplayType.BUFFER, strategy.replayType) val converted = strategy.convert() assertTrue(converted is SessionCaptureStrategy) assertEquals(strategy.currentReplayId, fixture.scope.replayId) - // Type of strategy is kept buffer, but type on the scope is updated to session - assertEquals(ReplayType.BUFFER, strategy.replayType) - assertEquals(ReplayType.SESSION, fixture.scope.replayType) } @Test @@ -338,7 +330,6 @@ class BufferCaptureStrategyTest { strategy.captureReplay(false) {} assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) - assertNull(fixture.scope.replayType) } @Test @@ -355,7 +346,6 @@ class BufferCaptureStrategyTest { // buffered + current = 2 verify(fixture.scopes, times(2)).captureReplay(any(), any()) assertEquals(strategy.currentReplayId, fixture.scope.replayId) - assertEquals(ReplayType.BUFFER, fixture.scope.replayType) assertTrue(called) } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt index 1a101f34188..af30a5b73f7 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt @@ -139,8 +139,6 @@ class SessionCaptureStrategyTest { assertEquals(replayId, fixture.scope.replayId) assertEquals(replayId, strategy.currentReplayId) - assertEquals(ReplayType.SESSION, fixture.scope.replayType) - assertEquals(ReplayType.SESSION, strategy.replayType) assertEquals(0, strategy.currentSegment) } @@ -202,8 +200,6 @@ class SessionCaptureStrategyTest { .captureReplay(argThat { event -> event is SentryReplayEvent && event.segmentId == 0 }, any()) assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) assertEquals(SentryId.EMPTY_ID, strategy.currentReplayId) - assertNull(fixture.scope.replayType) - assertEquals(ReplayType.SESSION, strategy.replayType) assertEquals(-1, strategy.currentSegment) assertFalse(currentReplay.exists()) verify(fixture.replayCache).close() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 7a505d8caa2..527461870aa 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -285,7 +285,6 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getReplayId ()Lio/sentry/protocol/SentryId; - public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; public fun getSession ()Lio/sentry/Session; @@ -312,7 +311,6 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun setLevel (Lio/sentry/SentryLevel;)V public fun setPropagationContext (Lio/sentry/PropagationContext;)V public fun setReplayId (Lio/sentry/protocol/SentryId;)V - public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setScreen (Ljava/lang/String;)V public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V @@ -853,7 +851,6 @@ public abstract interface class io/sentry/IScope { public abstract fun getOptions ()Lio/sentry/SentryOptions; public abstract fun getPropagationContext ()Lio/sentry/PropagationContext; public abstract fun getReplayId ()Lio/sentry/protocol/SentryId; - public abstract fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public abstract fun getRequest ()Lio/sentry/protocol/Request; public abstract fun getScreen ()Ljava/lang/String; public abstract fun getSession ()Lio/sentry/Session; @@ -880,7 +877,6 @@ public abstract interface class io/sentry/IScope { public abstract fun setLevel (Lio/sentry/SentryLevel;)V public abstract fun setPropagationContext (Lio/sentry/PropagationContext;)V public abstract fun setReplayId (Lio/sentry/protocol/SentryId;)V - public abstract fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public abstract fun setRequest (Lio/sentry/protocol/Request;)V public abstract fun setScreen (Ljava/lang/String;)V public abstract fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V @@ -1623,7 +1619,6 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getReplayId ()Lio/sentry/protocol/SentryId; - public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; public fun getSession ()Lio/sentry/Session; @@ -1650,7 +1645,6 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun setLevel (Lio/sentry/SentryLevel;)V public fun setPropagationContext (Lio/sentry/PropagationContext;)V public fun setReplayId (Lio/sentry/protocol/SentryId;)V - public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setScreen (Ljava/lang/String;)V public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V @@ -2278,7 +2272,6 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun getOptions ()Lio/sentry/SentryOptions; public fun getPropagationContext ()Lio/sentry/PropagationContext; public fun getReplayId ()Lio/sentry/protocol/SentryId; - public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType; public fun getRequest ()Lio/sentry/protocol/Request; public fun getScreen ()Ljava/lang/String; public fun getSession ()Lio/sentry/Session; @@ -2305,7 +2298,6 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun setLevel (Lio/sentry/SentryLevel;)V public fun setPropagationContext (Lio/sentry/PropagationContext;)V public fun setReplayId (Lio/sentry/protocol/SentryId;)V - public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V public fun setRequest (Lio/sentry/protocol/Request;)V public fun setScreen (Ljava/lang/String;)V public fun setSpanContext (Ljava/lang/Throwable;Lio/sentry/ISpan;Ljava/lang/String;)V diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index db75424c2b8..d6ac5b824a9 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -507,22 +507,4 @@ public void replaceOptions(@NotNull SentryOptions options) { public void setReplayId(@NotNull SentryId replayId) { getDefaultWriteScope().setReplayId(replayId); } - - @Override - public @Nullable SentryReplayEvent.ReplayType getReplayType() { - final @Nullable SentryReplayEvent.ReplayType current = scope.getReplayType(); - if (current != null) { - return current; - } - final @Nullable SentryReplayEvent.ReplayType isolation = isolationScope.getReplayType(); - if (isolation != null) { - return isolation; - } - return globalScope.getReplayType(); - } - - @Override - public void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType) { - getDefaultWriteScope().setReplayType(replayType); - } } diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index 81fd4111c8a..ddabd00569e 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -106,24 +106,6 @@ public interface IScope { @ApiStatus.Internal void setReplayId(final @NotNull SentryId replayId); - /** - * Returns the Scope's current replayType, previously set by {@link - * IScope#setReplayType(SentryReplayEvent.ReplayType)} - * - * @return the type of the current session replay - */ - @ApiStatus.Internal - @Nullable - SentryReplayEvent.ReplayType getReplayType(); - - /** - * Sets the Scope's current replayType - * - * @param replayType the type of the current session replay - */ - @ApiStatus.Internal - void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType); - /** * Returns the Scope's request * diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index fe24dbd46f5..dd1a202b548 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -83,14 +83,6 @@ public void setScreen(@Nullable String screen) {} @Override public void setReplayId(@Nullable SentryId replayId) {} - @Override - public @Nullable SentryReplayEvent.ReplayType getReplayType() { - return null; - } - - @Override - public void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType) {} - @Override public @Nullable Request getRequest() { return null; diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 864b164e0e1..7a54c4c755a 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -98,9 +98,6 @@ public final class Scope implements IScope { /** Scope's session replay id */ private @NotNull SentryId replayId = SentryId.EMPTY_ID; - /** Scope's session replay type */ - private @Nullable SentryReplayEvent.ReplayType replayType = null; - private @NotNull ISentryClient client = NoOpSentryClient.getInstance(); private final @NotNull Map, String>> throwableToSpan = @@ -131,7 +128,6 @@ private Scope(final @NotNull Scope scope) { this.user = userRef != null ? new User(userRef) : null; this.screen = scope.screen; this.replayId = scope.replayId; - this.replayType = scope.replayType; final Request requestRef = scope.request; this.request = requestRef != null ? new Request(requestRef) : null; @@ -367,16 +363,6 @@ public void setReplayId(final @NotNull SentryId replayId) { } } - @Override - public @Nullable SentryReplayEvent.ReplayType getReplayType() { - return replayType; - } - - @Override - public void setReplayType(final @Nullable SentryReplayEvent.ReplayType replayType) { - this.replayType = replayType; - } - /** * Returns the Scope's request * diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 0d3fdbdf3d7..d8780818d5d 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -14,7 +14,6 @@ import io.sentry.SentryLogEventAttributeValue; import io.sentry.SentryLogLevel; import io.sentry.SentryOptions; -import io.sentry.SentryReplayEvent; import io.sentry.SpanId; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; @@ -213,12 +212,15 @@ private void captureLog( new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment)); } - final @Nullable SentryId replayId = scopes.getCombinedScopeView().getReplayId(); - if (!replayId.equals(SentryId.EMPTY_ID)) { + final @Nullable SentryId scopeReplayId = scopes.getCombinedScopeView().getReplayId(); + if (!scopeReplayId.equals(SentryId.EMPTY_ID)) { attributes.put( "sentry.replay_id", - new SentryLogEventAttributeValue(SentryAttributeType.STRING, replayId.toString())); - if (scopes.getCombinedScopeView().getReplayType() == SentryReplayEvent.ReplayType.BUFFER) { + new SentryLogEventAttributeValue(SentryAttributeType.STRING, scopeReplayId.toString())); + } else { + final @Nullable SentryId controllerReplayId = + scopes.getOptions().getReplayController().getReplayId(); + if (!controllerReplayId.equals(SentryId.EMPTY_ID)) { attributes.put( "sentry._internal.replay_is_buffering", new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true)); diff --git a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt index 84a1296fd7d..baeb076463f 100644 --- a/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt +++ b/sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt @@ -1185,51 +1185,6 @@ class CombinedScopeViewTest { assertEquals(SentryId.EMPTY_ID, fixture.globalScope.replayId) } - @Test - fun `prefers replay type from current scope`() { - val combined = fixture.getSut() - fixture.scope.replayType = SentryReplayEvent.ReplayType.BUFFER - fixture.isolationScope.replayType = SentryReplayEvent.ReplayType.SESSION - fixture.globalScope.replayType = SentryReplayEvent.ReplayType.SESSION - - assertEquals(SentryReplayEvent.ReplayType.BUFFER, combined.replayType) - } - - @Test - fun `uses isolation scope replay type if none in current scope`() { - val combined = fixture.getSut() - fixture.isolationScope.replayType = SentryReplayEvent.ReplayType.SESSION - fixture.globalScope.replayType = SentryReplayEvent.ReplayType.BUFFER - - assertEquals(SentryReplayEvent.ReplayType.SESSION, combined.replayType) - } - - @Test - fun `uses global scope replay type if none in current or isolation scope`() { - val combined = fixture.getSut() - fixture.globalScope.replayType = SentryReplayEvent.ReplayType.BUFFER - - assertEquals(SentryReplayEvent.ReplayType.BUFFER, combined.replayType) - } - - @Test - fun `returns null replay type if none in any scope`() { - val combined = fixture.getSut() - - assertNull(combined.replayType) - } - - @Test - fun `set replay type modifies default scope`() { - val combined = fixture.getSut() - combined.replayType = SentryReplayEvent.ReplayType.BUFFER - - assertEquals(ScopeType.ISOLATION, fixture.options.defaultScopeType) - assertNull(fixture.scope.replayType) - assertEquals(SentryReplayEvent.ReplayType.BUFFER, fixture.isolationScope.replayType) - assertNull(fixture.globalScope.replayType) - } - @Test fun `null tags do not cause NPE`() { val scope = fixture.getSut() diff --git a/sentry/src/test/java/io/sentry/NoOpScopeTest.kt b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt index 7a34d269d53..8b6d2371273 100644 --- a/sentry/src/test/java/io/sentry/NoOpScopeTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt @@ -126,9 +126,4 @@ class NoOpScopeTest { fun `getReplayId returns empty id`() { assertEquals(SentryId.EMPTY_ID, sut.replayId) } - - @Test - fun `getReplayType returns null`() { - assertNull(sut.replayType) - } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index bd93bbddb74..8a79691fbf0 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2962,11 +2962,11 @@ class ScopesTest { } @Test - fun `does not add session replay type to log attributes if no replay id`() { + fun `does not add session replay buffering to log attributes if no replay id in scope and in controller`() { val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } - sut.scope.replayType = SentryReplayEvent.ReplayType.BUFFER sut.logger().log(SentryLogLevel.WARN, "log message") + assertEquals(SentryId.EMPTY_ID, sut.options.replayController.replayId) verify(mockClient) .captureLog( @@ -2980,11 +2980,10 @@ class ScopesTest { } @Test - fun `does not add session replay type to log attributes if replay type is session`() { + fun `does not add session replay buffering to log attributes if replay id in scope`() { val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } val replayId = SentryId() sut.scope.replayId = replayId - sut.scope.replayType = SentryReplayEvent.ReplayType.SESSION sut.logger().log(SentryLogLevel.WARN, "log message") @@ -3000,11 +2999,16 @@ class ScopesTest { } @Test - fun `adds session replay type to log attributes if replay type is buffer`() { - val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + fun `adds session replay buffering to log attributes if replay id in controller and not in scope`() { + val mockReplayController = mock() + val (sut, mockClient) = + getEnabledScopes { + it.logs.isEnabled = true + it.setReplayController(mockReplayController) + } val replayId = SentryId() - sut.scope.replayId = replayId - sut.scope.replayType = SentryReplayEvent.ReplayType.BUFFER + sut.scope.replayId = SentryId.EMPTY_ID + whenever(mockReplayController.replayId).thenReturn(replayId) sut.logger().log(SentryLogLevel.WARN, "log message") From 7259235d4ed653fc1c1c96a2427fff6e033ebf21 Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 30 Sep 2025 18:28:13 +0200 Subject: [PATCH 6/9] merged main --- .../src/main/java/io/sentry/samples/android/MainActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java index cda00fecef8..25907655f7f 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MainActivity.java @@ -12,8 +12,8 @@ import io.sentry.ISpan; import io.sentry.MeasurementUnit; import io.sentry.Sentry; -import io.sentry.UpdateStatus; import io.sentry.SentryLogLevel; +import io.sentry.UpdateStatus; import io.sentry.instrumentation.file.SentryFileOutputStream; import io.sentry.protocol.Feedback; import io.sentry.protocol.User; From 4339a9b7df7ccd01c44324d73a005d5f929f53c3 Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 30 Sep 2025 18:30:02 +0200 Subject: [PATCH 7/9] updated changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62aba3dbf49..277f3692bb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add session replay id to Sentry Logs ([#4740](https://github.com/getsentry/sentry-java/pull/4740)) + ### Fixes - Start performance collection on AppStart continuous profiling ([#4752](https://github.com/getsentry/sentry-java/pull/4752)) @@ -16,7 +20,6 @@ ### Features -- Add session replay id to Sentry Logs ([#4740](https://github.com/getsentry/sentry-java/pull/4740)) - Move SentryLogs out of experimental ([#4710](https://github.com/getsentry/sentry-java/pull/4710)) - Add support for w3c traceparent header ([#4671](https://github.com/getsentry/sentry-java/pull/4671)) - This feature is disabled by default. If enabled, outgoing requests will include the w3c `traceparent` header. From 7d5276dd092a8a3190b413d7165c7863451271f7 Mon Sep 17 00:00:00 2001 From: Stefano Date: Tue, 30 Sep 2025 18:38:16 +0200 Subject: [PATCH 8/9] add replay id to logs even if it's in controller and not in scope --- sentry/src/main/java/io/sentry/logger/LoggerApi.java | 4 ++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index d8780818d5d..886c8e2bb19 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -221,6 +221,10 @@ private void captureLog( final @Nullable SentryId controllerReplayId = scopes.getOptions().getReplayController().getReplayId(); if (!controllerReplayId.equals(SentryId.EMPTY_ID)) { + attributes.put( + "sentry.replay_id", + new SentryLogEventAttributeValue( + SentryAttributeType.STRING, controllerReplayId.toString())); attributes.put( "sentry._internal.replay_is_buffering", new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true)); diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 8a79691fbf0..53a4fc45807 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2972,7 +2972,9 @@ class ScopesTest { .captureLog( check { assertEquals("log message", it.body) + val logReplayId = it.attributes?.get("sentry.replay_id") val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering") + assertNull(logReplayId) assertNull(logReplayType) }, anyOrNull(), @@ -2991,7 +2993,9 @@ class ScopesTest { .captureLog( check { assertEquals("log message", it.body) + val logReplayId = it.attributes?.get("sentry.replay_id") val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering") + assertEquals(replayId.toString(), logReplayId!!.value) assertNull(logReplayType) }, anyOrNull(), @@ -3016,7 +3020,9 @@ class ScopesTest { .captureLog( check { assertEquals("log message", it.body) + val logReplayId = it.attributes?.get("sentry.replay_id") val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering")!! + assertEquals(replayId.toString(), logReplayId!!.value) assertTrue(logReplayType.value as Boolean) }, anyOrNull(), From 8f58f631130741f5751ef4e3619b46b3d3945d3c Mon Sep 17 00:00:00 2001 From: Stefano Date: Wed, 1 Oct 2025 11:12:37 +0200 Subject: [PATCH 9/9] added @NotNull annotations --- sentry/src/main/java/io/sentry/logger/LoggerApi.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 886c8e2bb19..c08550540e2 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -212,15 +212,15 @@ private void captureLog( new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment)); } - final @Nullable SentryId scopeReplayId = scopes.getCombinedScopeView().getReplayId(); - if (!scopeReplayId.equals(SentryId.EMPTY_ID)) { + final @NotNull SentryId scopeReplayId = scopes.getCombinedScopeView().getReplayId(); + if (!SentryId.EMPTY_ID.equals(scopeReplayId)) { attributes.put( "sentry.replay_id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, scopeReplayId.toString())); } else { - final @Nullable SentryId controllerReplayId = + final @NotNull SentryId controllerReplayId = scopes.getOptions().getReplayController().getReplayId(); - if (!controllerReplayId.equals(SentryId.EMPTY_ID)) { + if (!SentryId.EMPTY_ID.equals(controllerReplayId)) { attributes.put( "sentry.replay_id", new SentryLogEventAttributeValue(