Skip to content

Commit a212e8f

Browse files
sentry-junior[bot]buenaflorgetsentry-botCopilot
authored
feat(replay): Record segment names (transaction names) (#5763)
* feat(replay): Record segment names Co-Authored-By: Gino Buenaflor <giancarlo.buenaflor@sentry.io> * docs: Add replay segment names changelog entry * Format code * test(replay): Use realistic Android transaction name Co-Authored-By: Gino Buenaflor <giancarlo.buenaflor@sentry.io> * build: Update replay API dumps * test(replay): Use realistic Android segment names Co-Authored-By: Gino Buenaflor <giancarlo.buenaflor@sentry.io> * Format code * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Gino Buenaflor <giancarlo.buenaflor@sentry.io> Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io> Co-authored-by: Giancarlo Buenaflor <giancarlo_buenaflor@yahoo.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent bcd3e76 commit a212e8f

14 files changed

Lines changed: 157 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Features
66

7+
- Session Replay: Record segment names (transaction names) ([#5763](https://github.com/getsentry/sentry-java/pull/5763))
8+
79
- Add `io.sentry:sentry-opentelemetry-bom` to align Sentry OpenTelemetry modules with tested OpenTelemetry dependencies ([#5629](https://github.com/getsentry/sentry-java/pull/5629))
810
- Spring Boot Gradle plugin: add the Sentry BOM to `dependencyManagement`; explicit imports are applied after Spring Boot's implicit BOM
911
```kotlin

‎sentry-android-replay/api/sentry-android-replay.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public final class io/sentry/android/replay/ReplayIntegration : io/sentry/IConne
7676
public fun onWindowSizeChanged (II)V
7777
public fun pause ()V
7878
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
79+
public fun registerSegmentName (Ljava/lang/String;)V
7980
public fun registerTraceId (Lio/sentry/protocol/SentryId;)V
8081
public fun resume ()V
8182
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V

‎sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,13 @@ public class ReplayIntegration(
297297
captureStrategy?.registerTraceId(traceId)
298298
}
299299

300+
override fun registerSegmentName(segmentName: String) {
301+
if (!isEnabled.get() || !isRecording()) {
302+
return
303+
}
304+
captureStrategy?.registerSegmentName(segmentName)
305+
}
306+
300307
private fun pauseInternal() {
301308
lifecycleLock.acquire().use {
302309
if (!isEnabled.get() || !lifecycle.isAllowed(PAUSED)) {

‎sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt‎

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ internal abstract class BaseCaptureStrategy(
5454
internal companion object {
5555
private const val TAG = "CaptureStrategy"
5656
// https://github.com/getsentry/sentry-javascript/blob/30eb68fff5077211c30c61ba74625e66ab514870/packages/replay-internal/src/coreHandlers/handleAfterSendEvent.ts#L41
57-
private const val MAX_TRACE_IDS = 100
57+
private const val MAX_CONTEXT_VALUES = 100
5858
}
5959

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

9999
protected val currentEvents: Deque<RRWebEvent> = ConcurrentLinkedDeque()
100-
private val traceIdsLock = Any()
101-
private val currentTraceIds: MutableList<String> = mutableListOf()
100+
private val replayContextLock = Any()
101+
private val currentTraceIds: MutableSet<String> = linkedSetOf()
102+
private val currentSegmentNames: MutableSet<String> = linkedSetOf()
102103

103104
override fun start(segmentId: Int, replayId: SentryId, replayType: ReplayType?) {
104105
cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId)
@@ -139,11 +140,12 @@ internal abstract class BaseCaptureStrategy(
139140
breadcrumbs: List<Breadcrumb>? = null,
140141
events: Deque<RRWebEvent> = this.currentEvents,
141142
): ReplaySegment {
142-
val traceIds =
143-
synchronized(traceIdsLock) {
144-
val ids = currentTraceIds.toList()
143+
val (traceIds, segmentNames) =
144+
synchronized(replayContextLock) {
145+
val context = currentTraceIds.toList() to currentSegmentNames.toList()
145146
currentTraceIds.clear()
146-
ids
147+
currentSegmentNames.clear()
148+
context
147149
}
148150
return createSegment(
149151
scopes,
@@ -162,6 +164,7 @@ internal abstract class BaseCaptureStrategy(
162164
breadcrumbs,
163165
events,
164166
traceIds,
167+
segmentNames,
165168
)
166169
}
167170

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

181184
override fun registerTraceId(traceId: SentryId) {
182185
if (traceId != SentryId.EMPTY_ID) {
183-
synchronized(traceIdsLock) {
184-
if (currentTraceIds.size < MAX_TRACE_IDS) {
185-
val id = traceId.toString()
186-
if (!currentTraceIds.contains(id)) {
187-
currentTraceIds.add(id)
188-
}
186+
synchronized(replayContextLock) {
187+
if (currentTraceIds.size < MAX_CONTEXT_VALUES) {
188+
currentTraceIds.add(traceId.toString())
189+
}
190+
}
191+
}
192+
}
193+
194+
override fun registerSegmentName(segmentName: String) {
195+
if (segmentName.isNotEmpty()) {
196+
synchronized(replayContextLock) {
197+
if (currentSegmentNames.size < MAX_CONTEXT_VALUES) {
198+
currentSegmentNames.add(segmentName)
189199
}
190200
}
191201
}

‎sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ internal interface CaptureStrategy {
5656

5757
fun registerTraceId(traceId: SentryId)
5858

59+
fun registerSegmentName(segmentName: String)
60+
5961
companion object {
6062
private fun Breadcrumb?.isNetworkAvailable(): Boolean =
6163
this != null &&
@@ -88,6 +90,7 @@ internal interface CaptureStrategy {
8890
breadcrumbs: List<Breadcrumb>?,
8991
events: Deque<RRWebEvent>,
9092
traceIds: List<String> = emptyList(),
93+
segmentNames: List<String> = emptyList(),
9194
): ReplaySegment {
9295
val generatedVideo =
9396
cache?.createVideoOf(
@@ -127,6 +130,7 @@ internal interface CaptureStrategy {
127130
replayBreadcrumbs,
128131
events,
129132
traceIds,
133+
segmentNames,
130134
)
131135
}
132136

@@ -147,6 +151,7 @@ internal interface CaptureStrategy {
147151
breadcrumbs: List<Breadcrumb>,
148152
events: Deque<RRWebEvent>,
149153
traceIds: List<String>,
154+
segmentNames: List<String>,
150155
): ReplaySegment {
151156
val endTimestamp = DateUtils.getDateTime(segmentTimestamp.time + videoDuration)
152157
val replay =
@@ -159,6 +164,7 @@ internal interface CaptureStrategy {
159164
this.replayType = replayType
160165
this.videoFile = video
161166
this.traceIds = traceIds
167+
this.segmentNames = segmentNames
162168
}
163169

164170
val recordingPayload = mutableListOf<RRWebEvent>()

‎sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,4 +562,58 @@ class SessionCaptureStrategyTest {
562562
any(),
563563
)
564564
}
565+
566+
@Test
567+
fun `registerSegmentName includes unique segment names in next segment and clears them`() {
568+
val now =
569+
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
570+
val strategy = fixture.getSut(dateProvider = { now })
571+
strategy.start()
572+
strategy.onConfigurationChanged(fixture.recorderConfig)
573+
574+
strategy.registerSegmentName("CheckoutActivity")
575+
strategy.registerSegmentName("CheckoutActivity")
576+
strategy.registerSegmentName("ProductDetailsActivity")
577+
578+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
579+
580+
verify(fixture.scopes)
581+
.captureReplay(
582+
argThat { event ->
583+
event is SentryReplayEvent &&
584+
event.segmentNames == listOf("CheckoutActivity", "ProductDetailsActivity")
585+
},
586+
any(),
587+
)
588+
589+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
590+
591+
verify(fixture.scopes)
592+
.captureReplay(
593+
argThat { event ->
594+
event is SentryReplayEvent && event.segmentId == 1 && event.segmentNames.isNullOrEmpty()
595+
},
596+
any(),
597+
)
598+
}
599+
600+
@Test
601+
fun `registerSegmentName ignores empty names and limits names to 100`() {
602+
val now =
603+
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
604+
val strategy = fixture.getSut(dateProvider = { now })
605+
strategy.start()
606+
strategy.onConfigurationChanged(fixture.recorderConfig)
607+
608+
strategy.registerSegmentName("")
609+
repeat(101) { strategy.registerSegmentName("ProductActivity$it") }
610+
611+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
612+
613+
verify(fixture.scopes)
614+
.captureReplay(
615+
argThat { event -> event is SentryReplayEvent && event.segmentNames?.size == 100 },
616+
any(),
617+
)
618+
}
565619
}

‎sentry/api/sentry.api‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,6 +1715,7 @@ public final class io/sentry/NoOpReplayController : io/sentry/ReplayController {
17151715
public fun isDebugMaskingOverlayEnabled ()Z
17161716
public fun isRecording ()Z
17171717
public fun pause ()V
1718+
public fun registerSegmentName (Ljava/lang/String;)V
17181719
public fun registerTraceId (Lio/sentry/protocol/SentryId;)V
17191720
public fun resume ()V
17201721
public fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
@@ -2357,6 +2358,7 @@ public abstract interface class io/sentry/ReplayController : io/sentry/IReplayAp
23572358
public abstract fun isDebugMaskingOverlayEnabled ()Z
23582359
public abstract fun isRecording ()Z
23592360
public abstract fun pause ()V
2361+
public abstract fun registerSegmentName (Ljava/lang/String;)V
23602362
public abstract fun registerTraceId (Lio/sentry/protocol/SentryId;)V
23612363
public abstract fun resume ()V
23622364
public abstract fun setBreadcrumbConverter (Lio/sentry/ReplayBreadcrumbConverter;)V
@@ -4023,6 +4025,7 @@ public final class io/sentry/SentryReplayEvent : io/sentry/SentryBaseEvent, io/s
40234025
public fun getReplayStartTimestamp ()Ljava/util/Date;
40244026
public fun getReplayType ()Lio/sentry/SentryReplayEvent$ReplayType;
40254027
public fun getSegmentId ()I
4028+
public fun getSegmentNames ()Ljava/util/List;
40264029
public fun getTimestamp ()Ljava/util/Date;
40274030
public fun getTraceIds ()Ljava/util/List;
40284031
public fun getType ()Ljava/lang/String;
@@ -4036,6 +4039,7 @@ public final class io/sentry/SentryReplayEvent : io/sentry/SentryBaseEvent, io/s
40364039
public fun setReplayStartTimestamp (Ljava/util/Date;)V
40374040
public fun setReplayType (Lio/sentry/SentryReplayEvent$ReplayType;)V
40384041
public fun setSegmentId (I)V
4042+
public fun setSegmentNames (Ljava/util/List;)V
40394043
public fun setTimestamp (Ljava/util/Date;)V
40404044
public fun setTraceIds (Ljava/util/List;)V
40414045
public fun setType (Ljava/lang/String;)V
@@ -4056,6 +4060,7 @@ public final class io/sentry/SentryReplayEvent$JsonKeys {
40564060
public static final field REPLAY_START_TIMESTAMP Ljava/lang/String;
40574061
public static final field REPLAY_TYPE Ljava/lang/String;
40584062
public static final field SEGMENT_ID Ljava/lang/String;
4063+
public static final field SEGMENT_NAMES Ljava/lang/String;
40594064
public static final field TIMESTAMP Ljava/lang/String;
40604065
public static final field TRACE_IDS Ljava/lang/String;
40614066
public static final field TYPE Ljava/lang/String;

‎sentry/src/main/java/io/sentry/NoOpReplayController.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,7 @@ public void disableDebugMaskingOverlay() {}
6060

6161
@Override
6262
public void registerTraceId(@NotNull SentryId traceId) {}
63+
64+
@Override
65+
public void registerSegmentName(@NotNull String segmentName) {}
6366
}

‎sentry/src/main/java/io/sentry/ReplayController.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ public interface ReplayController extends IReplayApi {
3636
* @param traceId the trace ID to associate with the current replay
3737
*/
3838
void registerTraceId(@NotNull SentryId traceId);
39+
40+
/** Registers a segment name to be associated with the current replay segment. */
41+
void registerSegmentName(@NotNull String segmentName);
3942
}

‎sentry/src/main/java/io/sentry/SentryClient.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,10 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
10791079
if (trace != null) {
10801080
options.getReplayController().registerTraceId(trace.getTraceId());
10811081
}
1082+
final @Nullable String segmentName = transaction.getTransaction();
1083+
if (segmentName != null && !segmentName.isEmpty()) {
1084+
options.getReplayController().registerSegmentName(segmentName);
1085+
}
10821086
}
10831087

10841088
return sentryId;

0 commit comments

Comments
 (0)