diff --git a/CHANGELOG.md b/CHANGELOG.md index 79bf01e9cb..41b3f6ebd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ## Features + +- Add session replay id to Sentry Logs ([#4740](https://github.com/getsentry/sentry-java/pull/4740)) - Add support for continuous profiling of JVM applications on macOS and Linux ([#4556](https://github.com/getsentry/sentry-java/pull/4556)) - Sentry continuous profiling on the JVM is using async-profiler under the hood. 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 824bef1eba..25907655f7 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,6 +12,7 @@ import io.sentry.ISpan; import io.sentry.MeasurementUnit; import io.sentry.Sentry; +import io.sentry.SentryLogLevel; import io.sentry.UpdateStatus; import io.sentry.instrumentation.file.SentryFileOutputStream; import io.sentry.protocol.Feedback; @@ -340,7 +341,10 @@ public void run() { }); }); + 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 84abfa71b2..c08550540e 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -211,6 +211,26 @@ private void captureLog( "sentry.environment", new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment)); } + + 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 @NotNull SentryId controllerReplayId = + scopes.getOptions().getReplayController().getReplayId(); + if (!SentryId.EMPTY_ID.equals(controllerReplayId)) { + attributes.put( + "sentry.replay_id", + new SentryLogEventAttributeValue( + SentryAttributeType.STRING, controllerReplayId.toString())); + attributes.put( + "sentry._internal.replay_is_buffering", + new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true)); + } + } + final @Nullable String release = scopes.getOptions().getRelease(); if (release != null) { attributes.put( diff --git a/sentry/src/test/java/io/sentry/NoOpScopeTest.kt b/sentry/src/test/java/io/sentry/NoOpScopeTest.kt index ac17d2a5c0..8b6d237127 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,9 @@ 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) + } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 157740bcf0..4f0ee526dc 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2955,6 +2955,108 @@ 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(), + ) + } + + @Test + 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.logger().log(SentryLogLevel.WARN, "log message") + assertEquals(SentryId.EMPTY_ID, sut.options.replayController.replayId) + + verify(mockClient) + .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(), + ) + } + + @Test + 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.logger().log(SentryLogLevel.WARN, "log message") + + verify(mockClient) + .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(), + ) + } + + @Test + 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 = SentryId.EMPTY_ID + whenever(mockReplayController.replayId).thenReturn(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") + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering")!! + assertEquals(replayId.toString(), logReplayId!!.value) + assertTrue(logReplayType.value as Boolean) + }, + anyOrNull(), + ) + } + // endregion @Test