Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

### Fixes

- Session Replay: Fix first recording segment missing for replays in `buffer` mode ([#5753](https://github.com/getsentry/sentry-java/pull/5753))
- Fix main thread identification for tombstone (native crash) events ([#5742](https://github.com/getsentry/sentry-java/pull/5742))

### Dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ public class ReplayCache(private val options: SentryOptions, private val replayI
internal const val SEGMENT_KEY_REPLAY_SCREEN_AT_START = "replay.screen-at-start"
internal const val SEGMENT_KEY_REPLAY_RECORDING = "replay.recording"
internal const val SEGMENT_KEY_ID = "segment.id"
internal const val SEGMENT_KEY_FLUSHED = "replay.flushed"

fun makeReplayCacheDir(options: SentryOptions, replayId: SentryId): File? =
if (options.cacheDirPath.isNullOrEmpty()) {
Expand Down Expand Up @@ -415,8 +416,11 @@ public class ReplayCache(private val options: SentryOptions, private val replayI
}

cache.frames.sortBy { it.timestamp }
// TODO: this should be removed when we start sending buffered segments on next launch
val normalizedSegmentId = if (replayType == SESSION) segmentId else 0
val wasFlushed = lastSegment[SEGMENT_KEY_FLUSHED]?.toBooleanStrictOrNull() == true
// In buffer mode, if the buffer was never flushed (no error triggered captureReplay),
// no segments were ever sent, so we normalize to 0. After a flush + conversion to
// session mode, the persisted segmentId is the real sequence number.
val normalizedSegmentId = if (replayType == SESSION || wasFlushed) segmentId else 0
Comment thread
cursor[bot] marked this conversation as resolved.
val normalizedTimestamp =
if (replayType == SESSION) {
segmentTimestamp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ public class ReplayIntegration(
onSegmentSent = { newTimestamp ->
captureStrategy?.currentSegment = captureStrategy?.currentSegment!! + 1
captureStrategy?.segmentTimestamp = newTimestamp
captureStrategy?.isFlushed = true
Comment thread
romtsn marked this conversation as resolved.
},
)
captureStrategy = captureStrategy?.convert()
Comment thread
romtsn marked this conversation as resolved.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import io.sentry.SentryReplayEvent.ReplayType.BUFFER
import io.sentry.SentryReplayEvent.ReplayType.SESSION
import io.sentry.android.replay.ReplayCache
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_BIT_RATE
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_FLUSHED
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_FRAME_RATE
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_HEIGHT
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_ID
Expand Down Expand Up @@ -89,6 +90,11 @@ internal abstract class BaseCaptureStrategy(
get() = cache?.replayCacheDir

override var replayType by persistableAtomic<ReplayType>(propertyName = SEGMENT_KEY_REPLAY_TYPE)
// Tracks whether the buffer was flushed (segments sent to server). Used by fromDisk()
// to decide whether to normalize the segment ID to 0 on crash recovery: if never flushed,
// no segments reached the server, so the recovered segment must be 0.
override var isFlushed: Boolean by
persistableAtomic(initialValue = false, propertyName = SEGMENT_KEY_FLUSHED)

protected val currentEvents: Deque<RRWebEvent> = ConcurrentLinkedDeque()
private val traceIdsLock = Any()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ internal class BufferCaptureStrategy(

if (segment is ReplaySegment.Created) {
segment.capture(scopes)

// we only want to increment segment_id in the case of success, but currentSegment
// might be irrelevant since we changed strategies, so in the callback we increment
// it on the new strategy already
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal interface CaptureStrategy {
val replayCacheDir: File?
var replayType: ReplayType
var segmentTimestamp: Date?
var isFlushed: Boolean

fun start(segmentId: Int = 0, replayId: SentryId = SentryId(), replayType: ReplayType? = null)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.sentry.SentryOptions
import io.sentry.SentryReplayEvent.ReplayType
import io.sentry.android.replay.ReplayCache.Companion.ONGOING_SEGMENT
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_BIT_RATE
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_FLUSHED
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_FRAME_RATE
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_HEIGHT
import io.sentry.android.replay.ReplayCache.Companion.SEGMENT_KEY_ID
Expand Down Expand Up @@ -443,7 +444,7 @@ class ReplayCacheTest {
}

@Test
fun `sets segmentId to 0 for buffer mode`() {
fun `sets segmentId to 0 for buffer mode when not flushed`() {
fixture.options.run { cacheDirPath = tmpDir.newFolder()?.absolutePath }
val replayId = SentryId()
val replayCacheFolder =
Expand Down Expand Up @@ -474,6 +475,39 @@ class ReplayCacheTest {
assertEquals(0, lastSegment.id)
}

@Test
fun `preserves segmentId for buffer mode when already flushed`() {
fixture.options.run { cacheDirPath = tmpDir.newFolder()?.absolutePath }
val replayId = SentryId()
val replayCacheFolder =
File(fixture.options.cacheDirPath!!, "replay_$replayId").also { it.mkdirs() }
File(replayCacheFolder, ONGOING_SEGMENT).also {
it.writeText(
"""
$SEGMENT_KEY_HEIGHT=912
$SEGMENT_KEY_WIDTH=416
$SEGMENT_KEY_FRAME_RATE=1
$SEGMENT_KEY_BIT_RATE=75000
$SEGMENT_KEY_ID=5
$SEGMENT_KEY_TIMESTAMP=2024-07-11T10:25:21.454Z
$SEGMENT_KEY_REPLAY_TYPE=BUFFER
$SEGMENT_KEY_FLUSHED=true
"""
.trimIndent()
)
}

val screenshot = File(replayCacheFolder, "1720693523997.jpg").also { it.createNewFile() }
screenshot.outputStream().use {
Bitmap.createBitmap(1, 1, ARGB_8888).compress(JPEG, 80, it)
it.flush()
}

val lastSegment = ReplayCache.fromDisk(fixture.options, replayId)!!

assertEquals(5, lastSegment.id)
}

@Test
fun `when screenshot is corrupted, deletes it immediately`() {
ShadowBitmapFactory.setAllowInvalidImageData(false)
Expand Down
Loading