Skip to content

Commit 2c2ee9e

Browse files
authored
Merge 68edd9a into adad078
2 parents adad078 + 68edd9a commit 2c2ee9e

10 files changed

Lines changed: 189 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: 26 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+
protected 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,13 @@ 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 = synchronized(traceIdsLock) {
144+
val ids = currentTraceIds.toList().ifEmpty { null }
145+
currentTraceIds.clear()
146+
ids
147+
}
148+
return createSegment(
140149
scopes,
141150
options,
142151
duration,
@@ -152,7 +161,9 @@ internal abstract class BaseCaptureStrategy(
152161
screenAtStart,
153162
breadcrumbs,
154163
events,
164+
traceIds,
155165
)
166+
}
156167

157168
override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
158169
this.recorderConfig = recorderConfig
@@ -167,6 +178,19 @@ internal abstract class BaseCaptureStrategy(
167178
}
168179
}
169180

181+
override fun registerTraceId(traceId: SentryId) {
182+
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+
}
189+
}
190+
}
191+
}
192+
}
193+
170194
private class ReplayPersistingExecutorServiceThreadFactory : ThreadFactory {
171195
private var cnt = 0
172196

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: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,81 @@ 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 -> event is SentryReplayEvent && event.segmentId == 1 && event.traceIds.isNullOrEmpty() },
533+
any(),
534+
)
535+
}
536+
537+
@Test
538+
fun `registerTraceId ignores empty trace ID`() {
539+
val now =
540+
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionSegmentDuration * 5)
541+
val strategy = fixture.getSut(dateProvider = { now })
542+
strategy.start()
543+
strategy.onConfigurationChanged(fixture.recorderConfig)
544+
545+
strategy.registerTraceId(SentryId.EMPTY_ID)
546+
547+
strategy.onScreenshotRecorded(mock<Bitmap>()) {}
548+
549+
verify(fixture.scopes)
550+
.captureReplay(
551+
argThat { event -> event is SentryReplayEvent && event.traceIds.isNullOrEmpty() },
552+
any(),
553+
)
554+
}
478555
}

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)