Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features

- Session Replay: Record segment names (transaction names) ([#5763](https://github.com/getsentry/sentry-java/pull/5763))

- Add `io.sentry:sentry-opentelemetry-bom` to align Sentry OpenTelemetry modules with tested OpenTelemetry dependencies ([#5629](https://github.com/getsentry/sentry-java/pull/5629))
- Spring Boot Gradle plugin: add the Sentry BOM to `dependencyManagement`; explicit imports are applied after Spring Boot's implicit BOM
```kotlin
Expand Down
1 change: 1 addition & 0 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public final class io/sentry/android/replay/ReplayIntegration : io/sentry/IConne
public fun onWindowSizeChanged (II)V
public fun pause ()V
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
public fun registerSegmentName (Ljava/lang/String;)V
public fun registerTraceId (Lio/sentry/protocol/SentryId;)V
public fun resume ()V
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,13 @@ public class ReplayIntegration(
captureStrategy?.registerTraceId(traceId)
}

override fun registerSegmentName(segmentName: String) {
if (!isEnabled.get() || !isRecording()) {
return
}
captureStrategy?.registerSegmentName(segmentName)
}

private fun pauseInternal() {
lifecycleLock.acquire().use {
if (!isEnabled.get() || !lifecycle.isAllowed(PAUSED)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal abstract class BaseCaptureStrategy(
internal companion object {
private const val TAG = "CaptureStrategy"
// https://github.com/getsentry/sentry-javascript/blob/30eb68fff5077211c30c61ba74625e66ab514870/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts#L41
private const val MAX_TRACE_IDS = 100
private const val MAX_CONTEXT_VALUES = 100
}

private val gestureConverter = ReplayGestureConverter(dateProvider)
Expand Down Expand Up @@ -97,8 +97,9 @@ internal abstract class BaseCaptureStrategy(
persistableAtomic(initialValue = false, propertyName = SEGMENT_KEY_FLUSHED)

protected val currentEvents: Deque<RRWebEvent> = ConcurrentLinkedDeque()
private val traceIdsLock = Any()
private val currentTraceIds: MutableList<String> = mutableListOf()
private val replayContextLock = Any()
private val currentTraceIds: MutableSet<String> = linkedSetOf()
private val currentSegmentNames: MutableSet<String> = linkedSetOf()

override fun start(segmentId: Int, replayId: SentryId, replayType: ReplayType?) {
cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId)
Expand Down Expand Up @@ -139,11 +140,12 @@ internal abstract class BaseCaptureStrategy(
breadcrumbs: List<Breadcrumb>? = null,
events: Deque<RRWebEvent> = this.currentEvents,
): ReplaySegment {
val traceIds =
synchronized(traceIdsLock) {
val ids = currentTraceIds.toList()
val (traceIds, segmentNames) =
synchronized(replayContextLock) {
val context = currentTraceIds.toList() to currentSegmentNames.toList()
currentTraceIds.clear()
ids
currentSegmentNames.clear()
context
}
return createSegment(
scopes,
Expand All @@ -162,6 +164,7 @@ internal abstract class BaseCaptureStrategy(
breadcrumbs,
events,
traceIds,
segmentNames,
)
}

Expand All @@ -180,12 +183,19 @@ internal abstract class BaseCaptureStrategy(

override fun registerTraceId(traceId: SentryId) {
if (traceId != SentryId.EMPTY_ID) {
synchronized(traceIdsLock) {
if (currentTraceIds.size < MAX_TRACE_IDS) {
val id = traceId.toString()
if (!currentTraceIds.contains(id)) {
currentTraceIds.add(id)
}
synchronized(replayContextLock) {
if (currentTraceIds.size < MAX_CONTEXT_VALUES) {
currentTraceIds.add(traceId.toString())
}
}
}
}

override fun registerSegmentName(segmentName: String) {
if (segmentName.isNotEmpty()) {
synchronized(replayContextLock) {
if (currentSegmentNames.size < MAX_CONTEXT_VALUES) {
currentSegmentNames.add(segmentName)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ internal interface CaptureStrategy {

fun registerTraceId(traceId: SentryId)

fun registerSegmentName(segmentName: String)

companion object {
private fun Breadcrumb?.isNetworkAvailable(): Boolean =
this != null &&
Expand Down Expand Up @@ -88,6 +90,7 @@ internal interface CaptureStrategy {
breadcrumbs: List<Breadcrumb>?,
events: Deque<RRWebEvent>,
traceIds: List<String> = emptyList(),
segmentNames: List<String> = emptyList(),
): ReplaySegment {
val generatedVideo =
cache?.createVideoOf(
Expand Down Expand Up @@ -127,6 +130,7 @@ internal interface CaptureStrategy {
replayBreadcrumbs,
events,
traceIds,
segmentNames,
)
}

Expand All @@ -147,6 +151,7 @@ internal interface CaptureStrategy {
breadcrumbs: List<Breadcrumb>,
events: Deque<RRWebEvent>,
traceIds: List<String>,
segmentNames: List<String>,
): ReplaySegment {
val endTimestamp = DateUtils.getDateTime(segmentTimestamp.time + videoDuration)
val replay =
Expand All @@ -159,6 +164,7 @@ internal interface CaptureStrategy {
this.replayType = replayType
this.videoFile = video
this.traceIds = traceIds
this.segmentNames = segmentNames
}

val recordingPayload = mutableListOf<RRWebEvent>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -562,4 +562,58 @@ class SessionCaptureStrategyTest {
any(),
)
}

@Test
fun `registerSegmentName includes unique segment names in next segment and clears them`() {
val now =
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
val strategy = fixture.getSut(dateProvider = { now })
strategy.start()
strategy.onConfigurationChanged(fixture.recorderConfig)

strategy.registerSegmentName("CheckoutActivity")
strategy.registerSegmentName("CheckoutActivity")
strategy.registerSegmentName("ProductDetailsActivity")

strategy.onScreenshotRecorded(mock<Bitmap>()) {}

verify(fixture.scopes)
.captureReplay(
argThat { event ->
event is SentryReplayEvent &&
event.segmentNames == listOf("CheckoutActivity", "ProductDetailsActivity")
},
any(),
)

strategy.onScreenshotRecorded(mock<Bitmap>()) {}

verify(fixture.scopes)
.captureReplay(
argThat { event ->
event is SentryReplayEvent && event.segmentId == 1 && event.segmentNames.isNullOrEmpty()
},
any(),
)
}

@Test
fun `registerSegmentName ignores empty names and limits names to 100`() {
val now =
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
val strategy = fixture.getSut(dateProvider = { now })
strategy.start()
strategy.onConfigurationChanged(fixture.recorderConfig)

strategy.registerSegmentName("")
repeat(101) { strategy.registerSegmentName("ProductActivity$it") }

strategy.onScreenshotRecorded(mock<Bitmap>()) {}

verify(fixture.scopes)
.captureReplay(
argThat { event -> event is SentryReplayEvent && event.segmentNames?.size == 100 },
any(),
)
}
}
5 changes: 5 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,7 @@ public final class io/sentry/NoOpReplayController : io/sentry/ReplayController {
public fun isDebugMaskingOverlayEnabled ()Z
public fun isRecording ()Z
public fun pause ()V
public fun registerSegmentName (Ljava/lang/String;)V
public fun registerTraceId (Lio/sentry/protocol/SentryId;)V
public fun resume ()V
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
Expand Down Expand Up @@ -2357,6 +2358,7 @@ public abstract interface class io/sentry/ReplayController : io/sentry/IReplayAp
public abstract fun isDebugMaskingOverlayEnabled ()Z
public abstract fun isRecording ()Z
public abstract fun pause ()V
public abstract fun registerSegmentName (Ljava/lang/String;)V
public abstract fun registerTraceId (Lio/sentry/protocol/SentryId;)V
public abstract fun resume ()V
public abstract fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
Expand Down Expand Up @@ -4023,6 +4025,7 @@ public final class io/sentry/SentryReplayEvent : io/sentry/SentryBaseEvent, io/s
public fun getReplayStartTimestamp ()Ljava/util/Date;
public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType;
public fun getSegmentId ()I
public fun getSegmentNames ()Ljava/util/List;
public fun getTimestamp ()Ljava/util/Date;
public fun getTraceIds ()Ljava/util/List;
public fun getType ()Ljava/lang/String;
Expand All @@ -4036,6 +4039,7 @@ public final class io/sentry/SentryReplayEvent : io/sentry/SentryBaseEvent, io/s
public fun setReplayStartTimestamp (Ljava/util/Date;)V
public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V
public fun setSegmentId (I)V
public fun setSegmentNames (Ljava/util/List;)V
public fun setTimestamp (Ljava/util/Date;)V
public fun setTraceIds (Ljava/util/List;)V
public fun setType (Ljava/lang/String;)V
Expand All @@ -4056,6 +4060,7 @@ public final class io/sentry/SentryReplayEvent$JsonKeys {
public static final field REPLAY_START_TIMESTAMP Ljava/lang/String;
public static final field REPLAY_TYPE Ljava/lang/String;
public static final field SEGMENT_ID Ljava/lang/String;
public static final field SEGMENT_NAMES Ljava/lang/String;
public static final field TIMESTAMP Ljava/lang/String;
public static final field TRACE_IDS Ljava/lang/String;
public static final field TYPE Ljava/lang/String;
Expand Down
3 changes: 3 additions & 0 deletions sentry/src/main/java/io/sentry/NoOpReplayController.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ public void disableDebugMaskingOverlay() {}

@Override
public void registerTraceId(@NotNull SentryId traceId) {}

@Override
public void registerSegmentName(@NotNull String segmentName) {}
}
3 changes: 3 additions & 0 deletions sentry/src/main/java/io/sentry/ReplayController.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,7 @@ public interface ReplayController extends IReplayApi {
* @param traceId the trace ID to associate with the current replay
*/
void registerTraceId(@NotNull SentryId traceId);

/** Registers a segment name to be associated with the current replay segment. */
void registerSegmentName(@NotNull String segmentName);
}
4 changes: 4 additions & 0 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,10 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
if (trace != null) {
options.getReplayController().registerTraceId(trace.getTraceId());
}
final @Nullable String segmentName = transaction.getTransaction();
if (segmentName != null && !segmentName.isEmpty()) {
options.getReplayController().registerSegmentName(segmentName);
}
}

return sentryId;
Expand Down
26 changes: 24 additions & 2 deletions sentry/src/main/java/io/sentry/SentryReplayEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static final class Deserializer implements JsonDeserializer<ReplayType> {
private @Nullable List<String> urls;
private @Nullable List<String> errorIds;
private @Nullable List<String> traceIds;
private @Nullable List<String> segmentNames;
private @Nullable Map<String, Object> unknown;

public SentryReplayEvent() {
Expand All @@ -58,6 +59,7 @@ public SentryReplayEvent() {
this.replayType = ReplayType.SESSION;
this.errorIds = new ArrayList<>();
this.traceIds = new ArrayList<>();
this.segmentNames = new ArrayList<>();
this.urls = new ArrayList<>();
timestamp = DateUtils.getCurrentDateTime();
}
Expand Down Expand Up @@ -142,6 +144,15 @@ public void setTraceIds(final @Nullable List<String> traceIds) {
this.traceIds = traceIds;
}

@Nullable
public List<String> getSegmentNames() {
return segmentNames;
}

public void setSegmentNames(final @Nullable List<String> segmentNames) {
this.segmentNames = segmentNames;
}

@NotNull
public ReplayType getReplayType() {
return replayType;
Expand All @@ -162,12 +173,14 @@ public boolean equals(Object o) {
&& Objects.equals(replayId, that.replayId)
&& Objects.equals(urls, that.urls)
&& Objects.equals(errorIds, that.errorIds)
&& Objects.equals(traceIds, that.traceIds);
&& Objects.equals(traceIds, that.traceIds)
&& Objects.equals(segmentNames, that.segmentNames);
}

@Override
public int hashCode() {
return Objects.hash(type, replayType, replayId, segmentId, urls, errorIds, traceIds);
return Objects.hash(
type, replayType, replayId, segmentId, urls, errorIds, traceIds, segmentNames);
}

// region json
Expand All @@ -181,6 +194,7 @@ public static final class JsonKeys {
public static final String URLS = "urls";
public static final String ERROR_IDS = "error_ids";
public static final String TRACE_IDS = "trace_ids";
public static final String SEGMENT_NAMES = "segment_names";
}

@Override
Expand All @@ -207,6 +221,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger
if (traceIds != null) {
writer.name(JsonKeys.TRACE_IDS).value(logger, traceIds);
}
if (segmentNames != null) {
writer.name(JsonKeys.SEGMENT_NAMES).value(logger, segmentNames);
}

new SentryBaseEvent.Serializer().serialize(this, writer, logger);

Expand Down Expand Up @@ -250,6 +267,7 @@ public static final class Deserializer implements JsonDeserializer<SentryReplayE
@Nullable List<String> urls = null;
@Nullable List<String> errorIds = null;
@Nullable List<String> traceIds = null;
@Nullable List<String> segmentNames = null;

reader.beginObject();
while (reader.peek() == JsonToken.NAME) {
Expand Down Expand Up @@ -282,6 +300,9 @@ public static final class Deserializer implements JsonDeserializer<SentryReplayE
case JsonKeys.TRACE_IDS:
traceIds = (List<String>) reader.nextObjectOrNull();
break;
case JsonKeys.SEGMENT_NAMES:
segmentNames = (List<String>) reader.nextObjectOrNull();
break;
default:
if (!baseEventDeserializer.deserializeValue(replay, nextName, reader, logger)) {
if (unknown == null) {
Expand Down Expand Up @@ -311,6 +332,7 @@ public static final class Deserializer implements JsonDeserializer<SentryReplayE
replay.setUrls(urls);
replay.setErrorIds(errorIds);
replay.setTraceIds(traceIds);
replay.setSegmentNames(segmentNames);
replay.setUnknown(unknown);
return replay;
}
Expand Down
20 changes: 20 additions & 0 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2130,6 +2130,8 @@ class SentryClientTest {
override fun registerTraceId(traceId: SentryId) {
registeredTraceId = traceId
}

override fun registerSegmentName(segmentName: String) {}
}
)
val sut = fixture.getSut()
Expand All @@ -2139,6 +2141,24 @@ class SentryClientTest {
assertEquals(sentryTracer.spanContext.traceId, registeredTraceId)
}

@Test
fun `captureTransaction registers segment name with replay controller`() {
var registeredSegmentName: String? = null
fixture.sentryOptions.setReplayController(
object : ReplayController by NoOpReplayController.getInstance() {
override fun registerSegmentName(segmentName: String) {
registeredSegmentName = segmentName
}
}
)
val sut = fixture.getSut()
val sentryTracer =
SentryTracer(TransactionContext("CheckoutActivity", "ui.load"), fixture.scopes)
val transaction = SentryTransaction(sentryTracer)
sut.captureTransaction(transaction, sentryTracer.traceContext())
assertEquals("CheckoutActivity", registeredSegmentName)
}

@Test
fun `when exception type is ignored, capturing event does not send it`() {
fixture.sentryOptions.addIgnoredExceptionForType(IllegalStateException::class.java)
Expand Down
Loading
Loading