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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
20 changes: 20 additions & 0 deletions sentry/src/main/java/io/sentry/logger/LoggerApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions sentry/src/test/java/io/sentry/NoOpScopeTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
102 changes: 102 additions & 0 deletions sentry/src/test/java/io/sentry/ScopesTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReplayController>()
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
Expand Down
Loading