Skip to content

Commit 9486895

Browse files
authored
[SR] Buffer mode improvements (#3622)
* Persist buffer replay type when switching to session * Ensure no gaps in segment timestamps when converting strategies * Properly store screen name at start for buffer mode * Changelog
1 parent d4b1f82 commit 9486895

File tree

11 files changed

+129
-96
lines changed

11 files changed

+129
-96
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
- Avoid ArrayIndexOutOfBoundsException on Android cpu data collection ([#3598](https://github.com/getsentry/sentry-java/pull/3598))
88
- Fix lazy select queries instrumentation ([#3604](https://github.com/getsentry/sentry-java/pull/3604))
9+
- Session Replay: buffer mode improvements ([#3622](https://github.com/getsentry/sentry-java/pull/3622))
10+
- Align next segment timestamp with the end of the buffered segment when converting from buffer mode to session mode
11+
- Persist `buffer` replay type for the entire replay when converting from buffer mode to session mode
12+
- Properly store screen names for `buffer` mode
913

1014
### Chores
1115

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ public abstract interface class io/sentry/android/replay/Recorder : java/io/Clos
3636
public final class io/sentry/android/replay/ReplayCache : java/io/Closeable {
3737
public static final field Companion Lio/sentry/android/replay/ReplayCache$Companion;
3838
public fun <init> (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V
39-
public final fun addFrame (Ljava/io/File;J)V
39+
public final fun addFrame (Ljava/io/File;JLjava/lang/String;)V
40+
public static synthetic fun addFrame$default (Lio/sentry/android/replay/ReplayCache;Ljava/io/File;JLjava/lang/String;ILjava/lang/Object;)V
4041
public fun close ()V
4142
public final fun createVideoOf (JJIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo;
4243
public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo;
4344
public final fun persistSegmentValues (Ljava/lang/String;Ljava/lang/String;)V
44-
public final fun rotate (J)V
45+
public final fun rotate (J)Ljava/lang/String;
4546
}
4647

4748
public final class io/sentry/android/replay/ReplayCache$Companion {

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public class ReplayCache(
7676
* @param bitmap the frame screenshot
7777
* @param frameTimestamp the timestamp when the frame screenshot was taken
7878
*/
79-
internal fun addFrame(bitmap: Bitmap, frameTimestamp: Long) {
79+
internal fun addFrame(bitmap: Bitmap, frameTimestamp: Long, screen: String? = null) {
8080
if (replayCacheDir == null || bitmap.isRecycled) {
8181
return
8282
}
@@ -89,7 +89,7 @@ public class ReplayCache(
8989
it.flush()
9090
}
9191

92-
addFrame(screenshot, frameTimestamp)
92+
addFrame(screenshot, frameTimestamp, screen)
9393
}
9494

9595
/**
@@ -101,8 +101,8 @@ public class ReplayCache(
101101
* @param screenshot file containing the frame screenshot
102102
* @param frameTimestamp the timestamp when the frame screenshot was taken
103103
*/
104-
public fun addFrame(screenshot: File, frameTimestamp: Long) {
105-
val frame = ReplayFrame(screenshot, frameTimestamp)
104+
public fun addFrame(screenshot: File, frameTimestamp: Long, screen: String? = null) {
105+
val frame = ReplayFrame(screenshot, frameTimestamp, screen)
106106
frames += frame
107107
}
108108

@@ -233,15 +233,20 @@ public class ReplayCache(
233233
* Removes frames from the in-memory and disk cache from start to [until].
234234
*
235235
* @param until value until whose the frames should be removed, represented as unix timestamp
236+
* @return the first screen in the rotated buffer, if any
236237
*/
237-
fun rotate(until: Long) {
238+
fun rotate(until: Long): String? {
239+
var screen: String? = null
238240
frames.removeAll {
239241
if (it.timestamp < until) {
240242
deleteFile(it.screenshot)
241243
return@removeAll true
244+
} else if (screen == null) {
245+
screen = it.screen
242246
}
243247
return@removeAll false
244248
}
249+
return screen
245250
}
246251

247252
override fun close() {
@@ -426,7 +431,8 @@ internal data class LastSegmentData(
426431

427432
internal data class ReplayFrame(
428433
val screenshot: File,
429-
val timestamp: Long
434+
val timestamp: Long,
435+
val screen: String? = null
430436
)
431437

432438
public data class GeneratedVideo(

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import io.sentry.Integration
1212
import io.sentry.NoOpReplayBreadcrumbConverter
1313
import io.sentry.ReplayBreadcrumbConverter
1414
import io.sentry.ReplayController
15-
import io.sentry.ScopeObserverAdapter
1615
import io.sentry.SentryIntegrationPackageStorage
1716
import io.sentry.SentryLevel.DEBUG
1817
import io.sentry.SentryLevel.INFO
@@ -28,7 +27,6 @@ import io.sentry.cache.PersistingScopeObserver
2827
import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME
2928
import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME
3029
import io.sentry.hints.Backfillable
31-
import io.sentry.protocol.Contexts
3230
import io.sentry.protocol.SentryId
3331
import io.sentry.transport.ICurrentDateProvider
3432
import io.sentry.util.FileUtils
@@ -102,12 +100,6 @@ public class ReplayIntegration(
102100
}
103101

104102
this.hub = hub
105-
this.options.addScopeObserver(object : ScopeObserverAdapter() {
106-
override fun setContexts(contexts: Contexts) {
107-
// scope screen has fully-qualified name
108-
captureStrategy?.onScreenChanged(contexts.app?.viewNames?.lastOrNull()?.substringAfterLast('.'))
109-
}
110-
})
111103
recorder = recorderProvider?.invoke() ?: WindowRecorder(options, this, this, mainLooperHandler)
112104
isEnabled.set(true)
113105

@@ -176,8 +168,9 @@ public class ReplayIntegration(
176168
return
177169
}
178170

179-
captureStrategy?.captureReplay(isTerminating == true, onSegmentSent = {
171+
captureStrategy?.captureReplay(isTerminating == true, onSegmentSent = { newTimestamp ->
180172
captureStrategy?.currentSegment = captureStrategy?.currentSegment!! + 1
173+
captureStrategy?.segmentTimestamp = newTimestamp
181174
})
182175
captureStrategy = captureStrategy?.convert()
183176
}
@@ -212,8 +205,10 @@ public class ReplayIntegration(
212205
}
213206

214207
override fun onScreenshotRecorded(bitmap: Bitmap) {
208+
var screen: String? = null
209+
hub?.configureScope { screen = it.screen?.substringAfterLast('.') }
215210
captureStrategy?.onScreenshotRecorded(bitmap) { frameTimeStamp ->
216-
addFrame(bitmap, frameTimeStamp)
211+
addFrame(bitmap, frameTimeStamp, screen)
217212
}
218213
}
219214

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ internal abstract class BaseCaptureStrategy(
7878
cache?.persistSegmentValues(SEGMENT_KEY_FRAME_RATE, newValue.frameRate.toString())
7979
cache?.persistSegmentValues(SEGMENT_KEY_BIT_RATE, newValue.bitRate.toString())
8080
}
81-
protected var segmentTimestamp by persistableAtomicNullable<Date>(propertyName = SEGMENT_KEY_TIMESTAMP) { _, _, newValue ->
81+
override var segmentTimestamp by persistableAtomicNullable<Date>(propertyName = SEGMENT_KEY_TIMESTAMP) { _, _, newValue ->
8282
cache?.persistSegmentValues(SEGMENT_KEY_TIMESTAMP, if (newValue == null) null else DateUtils.getTimestamp(newValue))
8383
}
8484
protected val replayStartTimestamp = AtomicLong()
@@ -87,7 +87,7 @@ internal abstract class BaseCaptureStrategy(
8787
override var currentSegment: Int by persistableAtomic(initialValue = -1, propertyName = SEGMENT_KEY_ID)
8888
override val replayCacheDir: File? get() = cache?.replayCacheDir
8989

90-
private var replayType by persistableAtomic<ReplayType>(propertyName = SEGMENT_KEY_REPLAY_TYPE)
90+
override var replayType by persistableAtomic<ReplayType>(propertyName = SEGMENT_KEY_REPLAY_TYPE)
9191
protected val currentEvents: LinkedList<RRWebEvent> = PersistableLinkedList(
9292
propertyName = SEGMENT_KEY_REPLAY_RECORDING,
9393
options,
@@ -105,15 +105,15 @@ internal abstract class BaseCaptureStrategy(
105105
override fun start(
106106
recorderConfig: ScreenshotRecorderConfig,
107107
segmentId: Int,
108-
replayId: SentryId
108+
replayId: SentryId,
109+
replayType: ReplayType?
109110
) {
110111
cache = replayCacheProvider?.invoke(replayId, recorderConfig) ?: ReplayCache(options, replayId, recorderConfig)
111112

112-
// TODO: this should be persisted even after conversion
113-
replayType = if (this is SessionCaptureStrategy) SESSION else BUFFER
113+
this.replayType = replayType ?: (if (this is SessionCaptureStrategy) SESSION else BUFFER)
114114
this.recorderConfig = recorderConfig
115-
currentSegment = segmentId
116-
currentReplayId = replayId
115+
this.currentSegment = segmentId
116+
this.currentReplayId = replayId
117117

118118
segmentTimestamp = DateUtils.getCurrentDateTime()
119119
replayStartTimestamp.set(dateProvider.currentTimeMillis)
@@ -140,7 +140,7 @@ internal abstract class BaseCaptureStrategy(
140140
segmentId: Int,
141141
height: Int,
142142
width: Int,
143-
replayType: ReplayType = SESSION,
143+
replayType: ReplayType = this.replayType,
144144
cache: ReplayCache? = this.cache,
145145
frameRate: Int = recorderConfig.frameRate,
146146
screenAtStart: String? = this.screenAtStart,

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

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.sentry.transport.ICurrentDateProvider
2020
import io.sentry.util.FileUtils
2121
import java.io.File
2222
import java.security.SecureRandom
23+
import java.util.Date
2324
import java.util.concurrent.ScheduledExecutorService
2425

2526
internal class BufferCaptureStrategy(
@@ -34,41 +35,11 @@ internal class BufferCaptureStrategy(
3435
// TODO: capture envelopes for buffered segments instead, but don't send them until buffer is triggered
3536
private val bufferedSegments = mutableListOf<ReplaySegment.Created>()
3637

37-
// TODO: rework this bs, it doesn't work with sending replay on restart
38-
private val bufferedScreensLock = Any()
39-
private val bufferedScreens = mutableListOf<Pair<String, Long>>()
40-
4138
internal companion object {
4239
private const val TAG = "BufferCaptureStrategy"
4340
private const val ENVELOPE_PROCESSING_DELAY: Long = 100L
4441
}
4542

46-
override fun start(
47-
recorderConfig: ScreenshotRecorderConfig,
48-
segmentId: Int,
49-
replayId: SentryId
50-
) {
51-
super.start(recorderConfig, segmentId, replayId)
52-
53-
hub?.configureScope {
54-
val screen = it.screen?.substringAfterLast('.')
55-
if (screen != null) {
56-
synchronized(bufferedScreensLock) {
57-
bufferedScreens.add(screen to dateProvider.currentTimeMillis)
58-
}
59-
}
60-
}
61-
}
62-
63-
override fun onScreenChanged(screen: String?) {
64-
synchronized(bufferedScreensLock) {
65-
val lastKnownScreen = bufferedScreens.lastOrNull()?.first
66-
if (screen != null && lastKnownScreen != screen) {
67-
bufferedScreens.add(screen to dateProvider.currentTimeMillis)
68-
}
69-
}
70-
}
71-
7243
override fun pause() {
7344
createCurrentSegment("pause") { segment ->
7445
if (segment is ReplaySegment.Created) {
@@ -90,7 +61,7 @@ internal class BufferCaptureStrategy(
9061

9162
override fun captureReplay(
9263
isTerminating: Boolean,
93-
onSegmentSent: () -> Unit
64+
onSegmentSent: (Date) -> Unit
9465
) {
9566
val sampled = random.sample(options.experimental.sessionReplay.errorSampleRate)
9667

@@ -121,8 +92,7 @@ internal class BufferCaptureStrategy(
12192
// we only want to increment segment_id in the case of success, but currentSegment
12293
// might be irrelevant since we changed strategies, so in the callback we increment
12394
// it on the new strategy already
124-
// TODO: also pass new segmentTimestamp to the new strategy
125-
onSegmentSent()
95+
onSegmentSent(segment.replay.timestamp)
12696
}
12797
}
12898
}
@@ -136,7 +106,7 @@ internal class BufferCaptureStrategy(
136106

137107
val now = dateProvider.currentTimeMillis
138108
val bufferLimit = now - options.experimental.sessionReplay.errorReplayDuration
139-
cache?.rotate(bufferLimit)
109+
screenAtStart = cache?.rotate(bufferLimit)
140110
bufferedSegments.rotate(bufferLimit)
141111
}
142112
}
@@ -159,7 +129,7 @@ internal class BufferCaptureStrategy(
159129
}
160130
// we hand over replayExecutor to the new strategy to preserve order of execution
161131
val captureStrategy = SessionCaptureStrategy(options, hub, dateProvider, replayExecutor)
162-
captureStrategy.start(recorderConfig, segmentId = currentSegment, replayId = currentReplayId)
132+
captureStrategy.start(recorderConfig, segmentId = currentSegment, replayId = currentReplayId, replayType = BUFFER)
163133
return captureStrategy
164134
}
165135

@@ -169,21 +139,6 @@ internal class BufferCaptureStrategy(
169139
rotateEvents(currentEvents, bufferLimit)
170140
}
171141

172-
private fun findAndSetStartScreen(segmentStart: Long) {
173-
synchronized(bufferedScreensLock) {
174-
val startScreen = bufferedScreens.lastOrNull { (_, timestamp) ->
175-
timestamp <= segmentStart
176-
}?.first
177-
// if no screen is found before the segment start, this likely means the buffer is from the
178-
// app start, and the start screen will be taken from the navigation crumbs
179-
if (startScreen != null) {
180-
screenAtStart = startScreen
181-
}
182-
// can clear as we switch to session mode and don't care anymore about buffering
183-
bufferedScreens.clear()
184-
}
185-
}
186-
187142
private fun deleteFile(file: File?) {
188143
if (file == null) {
189144
return
@@ -246,11 +201,9 @@ internal class BufferCaptureStrategy(
246201
val height = this.recorderConfig.recordingHeight
247202
val width = this.recorderConfig.recordingWidth
248203

249-
findAndSetStartScreen(currentSegmentTimestamp.time)
250-
251204
replayExecutor.submitSafely(options, "$TAG.$taskName") {
252205
val segment =
253-
createSegmentInternal(duration, currentSegmentTimestamp, replayId, segmentId, height, width, BUFFER)
206+
createSegmentInternal(duration, currentSegmentTimestamp, replayId, segmentId, height, width)
254207
onSegmentCreated(segment)
255208
}
256209
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ internal interface CaptureStrategy {
2525
var currentSegment: Int
2626
var currentReplayId: SentryId
2727
val replayCacheDir: File?
28+
var replayType: ReplayType
29+
var segmentTimestamp: Date?
2830

2931
fun start(
3032
recorderConfig: ScreenshotRecorderConfig,
3133
segmentId: Int = 0,
32-
replayId: SentryId = SentryId()
34+
replayId: SentryId = SentryId(),
35+
replayType: ReplayType? = null
3336
)
3437

3538
fun stop()
@@ -38,7 +41,7 @@ internal interface CaptureStrategy {
3841

3942
fun resume()
4043

41-
fun captureReplay(isTerminating: Boolean, onSegmentSent: () -> Unit)
44+
fun captureReplay(isTerminating: Boolean, onSegmentSent: (Date) -> Unit)
4245

4346
fun onScreenshotRecorded(bitmap: Bitmap? = null, store: ReplayCache.(frameTimestamp: Long) -> Unit)
4447

@@ -194,7 +197,6 @@ internal interface CaptureStrategy {
194197

195198
replay.urls = urls
196199
return ReplaySegment.Created(
197-
videoDuration = videoDuration,
198200
replay = replay,
199201
recording = recording
200202
)
@@ -219,7 +221,6 @@ internal interface CaptureStrategy {
219221
sealed class ReplaySegment {
220222
object Failed : ReplaySegment()
221223
data class Created(
222-
val videoDuration: Long,
223224
val replay: SentryReplayEvent,
224225
val recording: ReplayRecording
225226
) : ReplaySegment() {

0 commit comments

Comments
 (0)