Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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('.')
}
}
Expand All @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -330,6 +338,7 @@ class BufferCaptureStrategyTest {
strategy.captureReplay(false) {}

assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId)
assertNull(fixture.scope.replayType)
}

@Test
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
8 changes: 8 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
18 changes: 18 additions & 0 deletions sentry/src/main/java/io/sentry/CombinedScopeView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
18 changes: 18 additions & 0 deletions sentry/src/main/java/io/sentry/IScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
8 changes: 8 additions & 0 deletions sentry/src/main/java/io/sentry/NoOpScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 14 additions & 0 deletions sentry/src/main/java/io/sentry/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<Throwable, Pair<WeakReference<ISpan>, String>> throwableToSpan =
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
*
Expand Down
14 changes: 14 additions & 0 deletions sentry/src/main/java/io/sentry/logger/LoggerApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -211,6 +212,19 @@ private void captureLog(
"sentry.environment",
new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment));
}

final @Nullable SentryId replayId = scopes.getCombinedScopeView().getReplayId();
Copy link
Copy Markdown
Member

@romtsn romtsn Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I think we can simplify this by quite a bit:

  • First get the replay_id from scopes
    • If it's not empty that means all good and we're in session mode (or about to enter it) - hence we don't have to send sentry._internal.replay_is_buffering at all
    • If it's empty - we try to get the replay_id from scopes.getOptions().getReplayController().getReplayId()
      • If it's not empty that means we're in buffer mode - hence we have to send sentry._internal.replay_is_buffering: true
      • If this one is still empty - that means there's no replay being recorded right now and we just don't send any of that

This way we wouldn't have to keep replayType in the scope either. How that sound?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romtsn do you like it now? 😄

if (!replayId.equals(SentryId.EMPTY_ID)) {
attributes.put(
"sentry.replay_id",
new SentryLogEventAttributeValue(SentryAttributeType.STRING, replayId.toString()));
if (scopes.getCombinedScopeView().getReplayType() == SentryReplayEvent.ReplayType.BUFFER) {
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
45 changes: 45 additions & 0 deletions sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading