Skip to content

Commit 9f6f094

Browse files
authored
Merge 311a496 into ca6b6d8
2 parents ca6b6d8 + 311a496 commit 9f6f094

10 files changed

Lines changed: 192 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Session Replay: Populate `trace_ids` in replay events to enable searching replays by trace ID ([#5473](https://github.com/getsentry/sentry-java/pull/5473))
8+
39
## 8.43.0
410

511
### Features

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
@@ -280,6 +280,13 @@ public class ReplayIntegration(
280280

281281
override fun isDebugMaskingOverlayEnabled(): Boolean = debugMaskingEnabled
282282

283+
override fun registerTraceId(traceId: SentryId) {
284+
if (!isEnabled.get() || !isRecording()) {
285+
return
286+
}
287+
captureStrategy?.registerTraceId(traceId)
288+
}
289+
283290
private fun pauseInternal() {
284291
lifecycleLock.acquire().use {
285292
if (!isEnabled.get() || !lifecycle.isAllowed(PAUSED)) {

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

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

5961
private val persistingExecutor: ScheduledExecutorService by lazy {
@@ -96,6 +98,8 @@ internal abstract class BaseCaptureStrategy(
9698
override var replayType by persistableAtomic<ReplayType>(propertyName = SEGMENT_KEY_REPLAY_TYPE)
9799

98100
protected val currentEvents: Deque<RRWebEvent> = ConcurrentLinkedDeque()
101+
private val traceIdsLock = Any()
102+
private val currentTraceIds: MutableList<String> = mutableListOf()
99103

100104
override fun start(segmentId: Int, replayId: SentryId, replayType: ReplayType?) {
101105
cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId)
@@ -135,8 +139,14 @@ internal abstract class BaseCaptureStrategy(
135139
screenAtStart: String? = this.screenAtStart,
136140
breadcrumbs: List<Breadcrumb>? = null,
137141
events: Deque<RRWebEvent> = this.currentEvents,
138-
): ReplaySegment =
139-
createSegment(
142+
): ReplaySegment {
143+
val traceIds =
144+
synchronized(traceIdsLock) {
145+
val ids = currentTraceIds.toList().ifEmpty { null }
146+
currentTraceIds.clear()
147+
ids
148+
}
149+
return createSegment(
140150
scopes,
141151
options,
142152
duration,
@@ -152,7 +162,9 @@ internal abstract class BaseCaptureStrategy(
152162
screenAtStart,
153163
breadcrumbs,
154164
events,
165+
traceIds,
155166
)
167+
}
156168

157169
override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
158170
this.recorderConfig = recorderConfig
@@ -167,6 +179,19 @@ internal abstract class BaseCaptureStrategy(
167179
}
168180
}
169181

182+
override fun registerTraceId(traceId: SentryId) {
183+
if (traceId != SentryId.EMPTY_ID) {
184+
synchronized(traceIdsLock) {
185+
if (currentTraceIds.size < MAX_TRACE_IDS) {
186+
val id = traceId.toString()
187+
if (!currentTraceIds.contains(id)) {
188+
currentTraceIds.add(id)
189+
}
190+
}
191+
}
192+
}
193+
}
194+
170195
private class ReplayPersistingExecutorServiceThreadFactory : ThreadFactory {
171196
private var cnt = 0
172197

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
@@ -53,6 +53,8 @@ internal interface CaptureStrategy {
5353

5454
fun convert(): CaptureStrategy
5555

56+
fun registerTraceId(traceId: SentryId)
57+
5658
companion object {
5759
private fun Breadcrumb?.isNetworkAvailable(): Boolean =
5860
this != null &&
@@ -84,6 +86,7 @@ internal interface CaptureStrategy {
8486
screenAtStart: String?,
8587
breadcrumbs: List<Breadcrumb>?,
8688
events: Deque<RRWebEvent>,
89+
traceIds: List<String>? = null,
8790
): ReplaySegment {
8891
val generatedVideo =
8992
cache?.createVideoOf(
@@ -122,6 +125,7 @@ internal interface CaptureStrategy {
122125
screenAtStart,
123126
replayBreadcrumbs,
124127
events,
128+
traceIds,
125129
)
126130
}
127131

@@ -141,6 +145,7 @@ internal interface CaptureStrategy {
141145
screenAtStart: String?,
142146
breadcrumbs: List<Breadcrumb>,
143147
events: Deque<RRWebEvent>,
148+
traceIds: List<String>?,
144149
): ReplaySegment {
145150
val endTimestamp = DateUtils.getDateTime(segmentTimestamp.time + videoDuration)
146151
val replay =
@@ -152,6 +157,7 @@ internal interface CaptureStrategy {
152157
this.replayStartTimestamp = segmentTimestamp
153158
this.replayType = replayType
154159
this.videoFile = video
160+
this.traceIds = traceIds
155161
}
156162

157163
val recordingPayload = mutableListOf<RRWebEvent>()

sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,38 @@ class ReplayIntegrationTest {
10721072
verify(fixture.replayCache).addFrame(any<Bitmap>(), any(), anyOrNull())
10731073
}
10741074

1075+
@Test
1076+
fun `registerTraceId does nothing when replay is not started`() {
1077+
val replay = fixture.getSut(context)
1078+
1079+
replay.register(fixture.scopes, fixture.options)
1080+
// Don't call start()
1081+
1082+
// Should not throw
1083+
replay.registerTraceId(SentryId())
1084+
}
1085+
1086+
@Test
1087+
fun `registerTraceId forwards to capture strategy when recording`() {
1088+
var traceIdRegistered: SentryId? = null
1089+
val captureStrategy =
1090+
mock<CaptureStrategy> {
1091+
on { currentReplayId }.thenReturn(SentryId())
1092+
doAnswer { traceIdRegistered = it.arguments[0] as SentryId }
1093+
.whenever(mock)
1094+
.registerTraceId(any())
1095+
}
1096+
val replay = fixture.getSut(context, replayCaptureStrategyProvider = { captureStrategy })
1097+
1098+
replay.register(fixture.scopes, fixture.options)
1099+
replay.start()
1100+
1101+
val traceId = SentryId()
1102+
replay.registerTraceId(traceId)
1103+
1104+
assertEquals(traceId, traceIdRegistered)
1105+
}
1106+
10751107
private fun getSessionCaptureStrategy(options: SentryOptions): SessionCaptureStrategy =
10761108
SessionCaptureStrategy(
10771109
options,

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,83 @@ class SessionCaptureStrategyTest {
475475
},
476476
)
477477
}
478+
479+
@Test
480+
fun `registerTraceId includes trace IDs in next segment`() {
481+
val now =
482+
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
483+
val strategy = fixture.getSut(dateProvider = { now })
484+
strategy.start()
485+
strategy.onConfigurationChanged(fixture.recorderConfig)
486+
487+
val traceId1 = SentryId()
488+
val traceId2 = SentryId()
489+
strategy.registerTraceId(traceId1)
490+
strategy.registerTraceId(traceId2)
491+
492+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
493+
494+
verify(fixture.scopes)
495+
.captureReplay(
496+
argThat { event ->
497+
event is SentryReplayEvent &&
498+
event.traceIds?.size == 2 &&
499+
event.traceIds!!.contains(traceId1.toString()) &&
500+
event.traceIds!!.contains(traceId2.toString())
501+
},
502+
any(),
503+
)
504+
}
505+
506+
@Test
507+
fun `registerTraceId clears trace IDs after segment is created`() {
508+
val now =
509+
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
510+
val strategy = fixture.getSut(dateProvider = { now })
511+
strategy.start()
512+
strategy.onConfigurationChanged(fixture.recorderConfig)
513+
514+
val traceId = SentryId()
515+
strategy.registerTraceId(traceId)
516+
517+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
518+
519+
verify(fixture.scopes)
520+
.captureReplay(
521+
argThat { event ->
522+
event is SentryReplayEvent && event.traceIds?.contains(traceId.toString()) == true
523+
},
524+
any(),
525+
)
526+
527+
// trigger another segment, trace IDs should be cleared
528+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
529+
530+
verify(fixture.scopes)
531+
.captureReplay(
532+
argThat { event ->
533+
event is SentryReplayEvent && event.segmentId == 1 && event.traceIds.isNullOrEmpty()
534+
},
535+
any(),
536+
)
537+
}
538+
539+
@Test
540+
fun `registerTraceId ignores empty trace ID`() {
541+
val now =
542+
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
543+
val strategy = fixture.getSut(dateProvider = { now })
544+
strategy.start()
545+
strategy.onConfigurationChanged(fixture.recorderConfig)
546+
547+
strategy.registerTraceId(SentryId.EMPTY_ID)
548+
549+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
550+
551+
verify(fixture.scopes)
552+
.captureReplay(
553+
argThat { event -> event is SentryReplayEvent && event.traceIds.isNullOrEmpty() },
554+
any(),
555+
)
556+
}
478557
}

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

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

5858
@Override
5959
public void disableDebugMaskingOverlay() {}
60+
61+
@Override
62+
public void registerTraceId(@NotNull SentryId traceId) {}
6063
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,12 @@ public interface ReplayController extends IReplayApi {
2828
ReplayBreadcrumbConverter getBreadcrumbConverter();
2929

3030
boolean isDebugMaskingOverlayEnabled();
31+
32+
/**
33+
* Registers a trace ID to be associated with the current replay. This is called when a
34+
* transaction is captured while replay is recording, to enable searching for replays by trace ID.
35+
*
36+
* @param traceId the trace ID to associate with the current replay
37+
*/
38+
void registerTraceId(@NotNull SentryId traceId);
3139
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,13 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
10431043
sentryId = SentryId.EMPTY_ID;
10441044
}
10451045

1046+
if (!sentryId.equals(SentryId.EMPTY_ID)) {
1047+
final @Nullable SpanContext trace = transaction.getContexts().getTrace();
1048+
if (trace != null) {
1049+
options.getReplayController().registerTraceId(trace.getTraceId());
1050+
}
1051+
}
1052+
10461053
return sentryId;
10471054
}
10481055

sentry/src/test/java/io/sentry/SentryClientTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,23 @@ class SentryClientTest {
19581958
assertEquals("abc", transaction.platform)
19591959
}
19601960

1961+
@Test
1962+
fun `captureTransaction registers trace ID with replay controller`() {
1963+
var registeredTraceId: SentryId? = null
1964+
fixture.sentryOptions.setReplayController(
1965+
object : ReplayController by NoOpReplayController.getInstance() {
1966+
override fun registerTraceId(traceId: SentryId) {
1967+
registeredTraceId = traceId
1968+
}
1969+
}
1970+
)
1971+
val sut = fixture.getSut()
1972+
val sentryTracer = SentryTracer(TransactionContext("name", "op"), fixture.scopes)
1973+
val transaction = SentryTransaction(sentryTracer)
1974+
sut.captureTransaction(transaction, sentryTracer.traceContext())
1975+
assertEquals(sentryTracer.spanContext.traceId, registeredTraceId)
1976+
}
1977+
19611978
@Test
19621979
fun `when exception type is ignored, capturing event does not send it`() {
19631980
fixture.sentryOptions.addIgnoredExceptionForType(IllegalStateException::class.java)

0 commit comments

Comments
 (0)