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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

- Session Replay: Fix `NoSuchElementException` in `BufferCaptureStrategy` ([#4717](https://github.com/getsentry/sentry-java/pull/4717))
- Session Replay: Fix continue recording in Session mode after Buffer is triggered ([#4719](https://github.com/getsentry/sentry-java/pull/4719))
- Session Replay: Do not use recycled screenshots for masking ([#4790](https://github.com/getsentry/sentry-java/pull/4790))
- This fixes native crashes seen in `Canvas.<init>`/`ScreenshotRecorder.capture`

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import io.sentry.android.replay.capture.SessionCaptureStrategy
import io.sentry.android.replay.gestures.GestureRecorder
import io.sentry.android.replay.gestures.TouchRecorderCallback
import io.sentry.android.replay.util.MainLooperHandler
import io.sentry.android.replay.util.ReplayExecutorService
import io.sentry.android.replay.util.appContext
import io.sentry.android.replay.util.gracefullyShutdown
import io.sentry.android.replay.util.sample
import io.sentry.android.replay.util.submitSafely
import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME
Expand Down Expand Up @@ -103,7 +103,8 @@ public class ReplayIntegration(
private val random by lazy { Random() }
internal val rootViewsSpy by lazy { RootViewsSpy.install() }
private val replayExecutor by lazy {
Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
val delegate = Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
ReplayExecutorService(delegate, options)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Lazy Initialization Accesses Unset Property

The replayExecutor's lazy initializer references the options lateinit var. If replayExecutor is accessed before options is set in register(), this causes an UninitializedPropertyAccessException.

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point but we don't call it before options is initializaed

}

internal val isEnabled = AtomicBoolean(false)
Expand Down Expand Up @@ -330,7 +331,7 @@ public class ReplayIntegration(
recorder?.close()
recorder = null
rootViewsSpy.close()
replayExecutor.gracefullyShutdown(options)
replayExecutor.shutdown()
lifecycle.currentState = CLOSED
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.capture.CaptureStrategy.Companion.createSegment
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
import io.sentry.android.replay.gestures.ReplayGestureConverter
import io.sentry.android.replay.util.submitSafely
import io.sentry.android.replay.util.ReplayExecutorService
import io.sentry.android.replay.util.ReplayRunnable
import io.sentry.protocol.SentryId
import io.sentry.rrweb.RRWebEvent
import io.sentry.transport.ICurrentDateProvider
Expand Down Expand Up @@ -54,7 +55,9 @@ internal abstract class BaseCaptureStrategy(
}

private val persistingExecutor: ScheduledExecutorService by lazy {
Executors.newSingleThreadScheduledExecutor(ReplayPersistingExecutorServiceThreadFactory())
val delegate =
Executors.newSingleThreadScheduledExecutor(ReplayPersistingExecutorServiceThreadFactory())
ReplayExecutorService(delegate, options)
}
private val gestureConverter = ReplayGestureConverter(dateProvider)

Expand Down Expand Up @@ -185,7 +188,7 @@ internal abstract class BaseCaptureStrategy(

private fun runInBackground(task: () -> Unit) {
if (options.threadChecker.isMainThread) {
persistingExecutor.submitSafely(options, "$TAG.runInBackground") { task() }
persistingExecutor.submit(ReplayRunnable("$TAG.runInBackground") { task() })
} else {
try {
task()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import io.sentry.android.replay.ReplayCache
import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.capture.CaptureStrategy.Companion.rotateEvents
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
import io.sentry.android.replay.util.ReplayRunnable
import io.sentry.android.replay.util.sample
import io.sentry.android.replay.util.submitSafely
import io.sentry.protocol.SentryId
import io.sentry.transport.ICurrentDateProvider
import io.sentry.util.FileUtils
Expand Down Expand Up @@ -62,10 +62,12 @@ internal class BufferCaptureStrategy(

override fun stop() {
val replayCacheDir = cache?.replayCacheDir
replayExecutor.submitSafely(options, "$TAG.stop") {
FileUtils.deleteRecursively(replayCacheDir)
currentSegment = -1
}
replayExecutor.submit(
ReplayRunnable("$TAG.stop") {
FileUtils.deleteRecursively(replayCacheDir)
currentSegment = -1
}
)
super.stop()
}

Expand Down Expand Up @@ -115,14 +117,16 @@ internal class BufferCaptureStrategy(
// have to do it before submitting, otherwise if the queue is busy, the timestamp won't be
// reflecting the exact time of when it was captured
val frameTimestamp = dateProvider.currentTimeMillis
replayExecutor.submitSafely(options, "$TAG.add_frame") {
cache?.store(frameTimestamp)

val now = dateProvider.currentTimeMillis
val bufferLimit = now - options.sessionReplay.errorReplayDuration
screenAtStart = cache?.rotate(bufferLimit)
bufferedSegments.rotate(bufferLimit)
}
replayExecutor.submit(
ReplayRunnable("$TAG.add_frame") {
cache?.store(frameTimestamp)

val now = dateProvider.currentTimeMillis
val bufferLimit = now - options.sessionReplay.errorReplayDuration
screenAtStart = cache?.rotate(bufferLimit)
bufferedSegments.rotate(bufferLimit)
}
)
}

override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
Expand Down Expand Up @@ -225,19 +229,21 @@ internal class BufferCaptureStrategy(
val duration = now - currentSegmentTimestamp.time
val replayId = currentReplayId

replayExecutor.submitSafely(options, "$TAG.$taskName") {
val segment =
createSegmentInternal(
duration,
currentSegmentTimestamp,
replayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
onSegmentCreated(segment)
}
replayExecutor.submit(
ReplayRunnable("$TAG.$taskName") {
val segment =
createSegmentInternal(
duration,
currentSegmentTimestamp,
replayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
onSegmentCreated(segment)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import io.sentry.SentryReplayEvent.ReplayType
import io.sentry.android.replay.ReplayCache
import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
import io.sentry.android.replay.util.submitSafely
import io.sentry.android.replay.util.ReplayRunnable
import io.sentry.protocol.SentryId
import io.sentry.transport.ICurrentDateProvider
import io.sentry.util.FileUtils
Expand Down Expand Up @@ -79,55 +79,57 @@ internal class SessionCaptureStrategy(
// reflecting the exact time of when it was captured
val currentConfig = recorderConfig
val frameTimestamp = dateProvider.currentTimeMillis
replayExecutor.submitSafely(options, "$TAG.add_frame") {
cache?.store(frameTimestamp)

val currentSegmentTimestamp = segmentTimestamp
currentSegmentTimestamp
?: run {
options.logger.log(DEBUG, "Segment timestamp is not set, not recording frame")
return@submitSafely
replayExecutor.submit(
ReplayRunnable("$TAG.add_frame") {
cache?.store(frameTimestamp)

val currentSegmentTimestamp = segmentTimestamp
currentSegmentTimestamp
?: run {
options.logger.log(DEBUG, "Segment timestamp is not set, not recording frame")
return@ReplayRunnable
}

if (isTerminating.get()) {
options.logger.log(
DEBUG,
"Not capturing segment, because the app is terminating, will be captured on next launch",
)
return@ReplayRunnable
}

if (isTerminating.get()) {
options.logger.log(
DEBUG,
"Not capturing segment, because the app is terminating, will be captured on next launch",
)
return@submitSafely
}

if (currentConfig == null) {
options.logger.log(DEBUG, "Recorder config is not set, not capturing a segment")
return@submitSafely
}
if (currentConfig == null) {
options.logger.log(DEBUG, "Recorder config is not set, not capturing a segment")
return@ReplayRunnable
}

val now = dateProvider.currentTimeMillis
if ((now - currentSegmentTimestamp.time >= options.sessionReplay.sessionSegmentDuration)) {
val segment =
createSegmentInternal(
options.sessionReplay.sessionSegmentDuration,
currentSegmentTimestamp,
currentReplayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
if (segment is ReplaySegment.Created) {
segment.capture(scopes)
currentSegment++
// set next segment timestamp as close to the previous one as possible to avoid gaps
segmentTimestamp = segment.replay.timestamp
val now = dateProvider.currentTimeMillis
if ((now - currentSegmentTimestamp.time >= options.sessionReplay.sessionSegmentDuration)) {
val segment =
createSegmentInternal(
options.sessionReplay.sessionSegmentDuration,
currentSegmentTimestamp,
currentReplayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
if (segment is ReplaySegment.Created) {
segment.capture(scopes)
currentSegment++
// set next segment timestamp as close to the previous one as possible to avoid gaps
segmentTimestamp = segment.replay.timestamp
}
}
}

if ((now - replayStartTimestamp.get() >= options.sessionReplay.sessionDuration)) {
options.replayController.stop()
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
if ((now - replayStartTimestamp.get() >= options.sessionReplay.sessionDuration)) {
options.replayController.stop()
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
}
}
}
)
}

override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
Expand Down Expand Up @@ -161,19 +163,21 @@ internal class SessionCaptureStrategy(
val currentSegmentTimestamp = segmentTimestamp ?: return
val duration = now - currentSegmentTimestamp.time
val replayId = currentReplayId
replayExecutor.submitSafely(options, "$TAG.$taskName") {
val segment =
createSegmentInternal(
duration,
currentSegmentTimestamp,
replayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
onSegmentCreated(segment)
}
replayExecutor.submit(
ReplayRunnable("$TAG.$taskName") {
val segment =
createSegmentInternal(
duration,
currentSegmentTimestamp,
replayId,
currentSegment,
currentConfig.recordingHeight,
currentConfig.recordingWidth,
currentConfig.frameRate,
currentConfig.bitRate,
)
onSegmentCreated(segment)
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.android.replay.ScreenshotRecorderCallback
import io.sentry.android.replay.ScreenshotRecorderConfig
import io.sentry.android.replay.util.submitSafely
import io.sentry.android.replay.util.ReplayRunnable
import io.sentry.util.IntegrationUtils
import java.util.LinkedList
import java.util.WeakHashMap
Expand Down Expand Up @@ -124,7 +124,7 @@ internal class CanvasStrategy(
}
if (holder == null) {
options.logger.log(SentryLevel.DEBUG, "No free Picture available, skipping capture")
executor.submitSafely(options, "screenshot_recorder.canvas", pictureRenderTask)
executor.submit(ReplayRunnable("screenshot_recorder.canvas", pictureRenderTask))
return
}

Expand All @@ -136,7 +136,7 @@ internal class CanvasStrategy(

synchronized(unprocessedPictures) { unprocessedPictures.add(holder) }

executor.submitSafely(options, "screenshot_recorder.canvas", pictureRenderTask)
executor.submit(ReplayRunnable("screenshot_recorder.canvas", pictureRenderTask))
}

override fun onContentChanged() {
Expand Down
Loading
Loading