Skip to content

Commit 514df68

Browse files
authored
fix(replay): Do not use recycled screenshots (#4790)
1 parent 3258517 commit 514df68

File tree

12 files changed

+386
-226
lines changed

12 files changed

+386
-226
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
- Session Replay: Avoid deadlock when pausing replay if no connection ([#4788](https://github.com/getsentry/sentry-java/pull/4788))
3838
- Session Replay: Fix capturing roots with no windows ([#4805](https://github.com/getsentry/sentry-java/pull/4805))
3939
- Session Replay: Fix `java.lang.IllegalArgumentException: width and height must be > 0` ([#4805](https://github.com/getsentry/sentry-java/pull/4805))
40+
- Session Replay: Do not use recycled screenshots for masking ([#4790](https://github.com/getsentry/sentry-java/pull/4790))
41+
- This fixes native crashes seen in `Canvas.<init>`/`ScreenshotRecorder.capture`
4042

4143
### Miscellaneous
4244

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ import io.sentry.android.replay.capture.SessionCaptureStrategy
3131
import io.sentry.android.replay.gestures.GestureRecorder
3232
import io.sentry.android.replay.gestures.TouchRecorderCallback
3333
import io.sentry.android.replay.util.MainLooperHandler
34+
import io.sentry.android.replay.util.ReplayExecutorService
3435
import io.sentry.android.replay.util.appContext
35-
import io.sentry.android.replay.util.gracefullyShutdown
3636
import io.sentry.android.replay.util.sample
3737
import io.sentry.android.replay.util.submitSafely
3838
import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME
@@ -103,7 +103,8 @@ public class ReplayIntegration(
103103
private val random by lazy { Random() }
104104
internal val rootViewsSpy by lazy { RootViewsSpy.install() }
105105
private val replayExecutor by lazy {
106-
Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
106+
val delegate = Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
107+
ReplayExecutorService(delegate, options)
107108
}
108109

109110
internal val isEnabled = AtomicBoolean(false)
@@ -328,7 +329,7 @@ public class ReplayIntegration(
328329
recorder?.close()
329330
recorder = null
330331
rootViewsSpy.close()
331-
replayExecutor.gracefullyShutdown(options)
332+
replayExecutor.shutdown()
332333
lifecycle.currentState = CLOSED
333334
}
334335
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import io.sentry.android.replay.ScreenshotRecorderConfig
2424
import io.sentry.android.replay.capture.CaptureStrategy.Companion.createSegment
2525
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
2626
import io.sentry.android.replay.gestures.ReplayGestureConverter
27-
import io.sentry.android.replay.util.submitSafely
27+
import io.sentry.android.replay.util.ReplayExecutorService
28+
import io.sentry.android.replay.util.ReplayRunnable
2829
import io.sentry.protocol.SentryId
2930
import io.sentry.rrweb.RRWebEvent
3031
import io.sentry.transport.ICurrentDateProvider
@@ -54,7 +55,9 @@ internal abstract class BaseCaptureStrategy(
5455
}
5556

5657
private val persistingExecutor: ScheduledExecutorService by lazy {
57-
Executors.newSingleThreadScheduledExecutor(ReplayPersistingExecutorServiceThreadFactory())
58+
val delegate =
59+
Executors.newSingleThreadScheduledExecutor(ReplayPersistingExecutorServiceThreadFactory())
60+
ReplayExecutorService(delegate, options)
5861
}
5962
private val gestureConverter = ReplayGestureConverter(dateProvider)
6063

@@ -185,7 +188,7 @@ internal abstract class BaseCaptureStrategy(
185188

186189
private fun runInBackground(task: () -> Unit) {
187190
if (options.threadChecker.isMainThread) {
188-
persistingExecutor.submitSafely(options, "$TAG.runInBackground") { task() }
191+
persistingExecutor.submit(ReplayRunnable("$TAG.runInBackground") { task() })
189192
} else {
190193
try {
191194
task()

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

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import io.sentry.android.replay.ReplayCache
1414
import io.sentry.android.replay.ScreenshotRecorderConfig
1515
import io.sentry.android.replay.capture.CaptureStrategy.Companion.rotateEvents
1616
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
17+
import io.sentry.android.replay.util.ReplayRunnable
1718
import io.sentry.android.replay.util.sample
18-
import io.sentry.android.replay.util.submitSafely
1919
import io.sentry.protocol.SentryId
2020
import io.sentry.transport.ICurrentDateProvider
2121
import io.sentry.util.FileUtils
@@ -62,10 +62,12 @@ internal class BufferCaptureStrategy(
6262

6363
override fun stop() {
6464
val replayCacheDir = cache?.replayCacheDir
65-
replayExecutor.submitSafely(options, "$TAG.stop") {
66-
FileUtils.deleteRecursively(replayCacheDir)
67-
currentSegment = -1
68-
}
65+
replayExecutor.submit(
66+
ReplayRunnable("$TAG.stop") {
67+
FileUtils.deleteRecursively(replayCacheDir)
68+
currentSegment = -1
69+
}
70+
)
6971
super.stop()
7072
}
7173

@@ -115,14 +117,16 @@ internal class BufferCaptureStrategy(
115117
// have to do it before submitting, otherwise if the queue is busy, the timestamp won't be
116118
// reflecting the exact time of when it was captured
117119
val frameTimestamp = dateProvider.currentTimeMillis
118-
replayExecutor.submitSafely(options, "$TAG.add_frame") {
119-
cache?.store(frameTimestamp)
120-
121-
val now = dateProvider.currentTimeMillis
122-
val bufferLimit = now - options.sessionReplay.errorReplayDuration
123-
screenAtStart = cache?.rotate(bufferLimit)
124-
bufferedSegments.rotate(bufferLimit)
125-
}
120+
replayExecutor.submit(
121+
ReplayRunnable("$TAG.add_frame") {
122+
cache?.store(frameTimestamp)
123+
124+
val now = dateProvider.currentTimeMillis
125+
val bufferLimit = now - options.sessionReplay.errorReplayDuration
126+
screenAtStart = cache?.rotate(bufferLimit)
127+
bufferedSegments.rotate(bufferLimit)
128+
}
129+
)
126130
}
127131

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

228-
replayExecutor.submitSafely(options, "$TAG.$taskName") {
229-
val segment =
230-
createSegmentInternal(
231-
duration,
232-
currentSegmentTimestamp,
233-
replayId,
234-
currentSegment,
235-
currentConfig.recordingHeight,
236-
currentConfig.recordingWidth,
237-
currentConfig.frameRate,
238-
currentConfig.bitRate,
239-
)
240-
onSegmentCreated(segment)
241-
}
232+
replayExecutor.submit(
233+
ReplayRunnable("$TAG.$taskName") {
234+
val segment =
235+
createSegmentInternal(
236+
duration,
237+
currentSegmentTimestamp,
238+
replayId,
239+
currentSegment,
240+
currentConfig.recordingHeight,
241+
currentConfig.recordingWidth,
242+
currentConfig.frameRate,
243+
currentConfig.bitRate,
244+
)
245+
onSegmentCreated(segment)
246+
}
247+
)
242248
}
243249
}

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

Lines changed: 62 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import io.sentry.SentryReplayEvent.ReplayType
99
import io.sentry.android.replay.ReplayCache
1010
import io.sentry.android.replay.ScreenshotRecorderConfig
1111
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
12-
import io.sentry.android.replay.util.submitSafely
12+
import io.sentry.android.replay.util.ReplayRunnable
1313
import io.sentry.protocol.SentryId
1414
import io.sentry.transport.ICurrentDateProvider
1515
import io.sentry.util.FileUtils
@@ -79,55 +79,57 @@ internal class SessionCaptureStrategy(
7979
// reflecting the exact time of when it was captured
8080
val currentConfig = recorderConfig
8181
val frameTimestamp = dateProvider.currentTimeMillis
82-
replayExecutor.submitSafely(options, "$TAG.add_frame") {
83-
cache?.store(frameTimestamp)
84-
85-
val currentSegmentTimestamp = segmentTimestamp
86-
currentSegmentTimestamp
87-
?: run {
88-
options.logger.log(DEBUG, "Segment timestamp is not set, not recording frame")
89-
return@submitSafely
82+
replayExecutor.submit(
83+
ReplayRunnable("$TAG.add_frame") {
84+
cache?.store(frameTimestamp)
85+
86+
val currentSegmentTimestamp = segmentTimestamp
87+
currentSegmentTimestamp
88+
?: run {
89+
options.logger.log(DEBUG, "Segment timestamp is not set, not recording frame")
90+
return@ReplayRunnable
91+
}
92+
93+
if (isTerminating.get()) {
94+
options.logger.log(
95+
DEBUG,
96+
"Not capturing segment, because the app is terminating, will be captured on next launch",
97+
)
98+
return@ReplayRunnable
9099
}
91100

92-
if (isTerminating.get()) {
93-
options.logger.log(
94-
DEBUG,
95-
"Not capturing segment, because the app is terminating, will be captured on next launch",
96-
)
97-
return@submitSafely
98-
}
99-
100-
if (currentConfig == null) {
101-
options.logger.log(DEBUG, "Recorder config is not set, not capturing a segment")
102-
return@submitSafely
103-
}
101+
if (currentConfig == null) {
102+
options.logger.log(DEBUG, "Recorder config is not set, not capturing a segment")
103+
return@ReplayRunnable
104+
}
104105

105-
val now = dateProvider.currentTimeMillis
106-
if ((now - currentSegmentTimestamp.time >= options.sessionReplay.sessionSegmentDuration)) {
107-
val segment =
108-
createSegmentInternal(
109-
options.sessionReplay.sessionSegmentDuration,
110-
currentSegmentTimestamp,
111-
currentReplayId,
112-
currentSegment,
113-
currentConfig.recordingHeight,
114-
currentConfig.recordingWidth,
115-
currentConfig.frameRate,
116-
currentConfig.bitRate,
117-
)
118-
if (segment is ReplaySegment.Created) {
119-
segment.capture(scopes)
120-
currentSegment++
121-
// set next segment timestamp as close to the previous one as possible to avoid gaps
122-
segmentTimestamp = segment.replay.timestamp
106+
val now = dateProvider.currentTimeMillis
107+
if ((now - currentSegmentTimestamp.time >= options.sessionReplay.sessionSegmentDuration)) {
108+
val segment =
109+
createSegmentInternal(
110+
options.sessionReplay.sessionSegmentDuration,
111+
currentSegmentTimestamp,
112+
currentReplayId,
113+
currentSegment,
114+
currentConfig.recordingHeight,
115+
currentConfig.recordingWidth,
116+
currentConfig.frameRate,
117+
currentConfig.bitRate,
118+
)
119+
if (segment is ReplaySegment.Created) {
120+
segment.capture(scopes)
121+
currentSegment++
122+
// set next segment timestamp as close to the previous one as possible to avoid gaps
123+
segmentTimestamp = segment.replay.timestamp
124+
}
123125
}
124-
}
125126

126-
if ((now - replayStartTimestamp.get() >= options.sessionReplay.sessionDuration)) {
127-
options.replayController.stop()
128-
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
127+
if ((now - replayStartTimestamp.get() >= options.sessionReplay.sessionDuration)) {
128+
options.replayController.stop()
129+
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
130+
}
129131
}
130-
}
132+
)
131133
}
132134

133135
override fun onConfigurationChanged(recorderConfig: ScreenshotRecorderConfig) {
@@ -161,19 +163,21 @@ internal class SessionCaptureStrategy(
161163
val currentSegmentTimestamp = segmentTimestamp ?: return
162164
val duration = now - currentSegmentTimestamp.time
163165
val replayId = currentReplayId
164-
replayExecutor.submitSafely(options, "$TAG.$taskName") {
165-
val segment =
166-
createSegmentInternal(
167-
duration,
168-
currentSegmentTimestamp,
169-
replayId,
170-
currentSegment,
171-
currentConfig.recordingHeight,
172-
currentConfig.recordingWidth,
173-
currentConfig.frameRate,
174-
currentConfig.bitRate,
175-
)
176-
onSegmentCreated(segment)
177-
}
166+
replayExecutor.submit(
167+
ReplayRunnable("$TAG.$taskName") {
168+
val segment =
169+
createSegmentInternal(
170+
duration,
171+
currentSegmentTimestamp,
172+
replayId,
173+
currentSegment,
174+
currentConfig.recordingHeight,
175+
currentConfig.recordingWidth,
176+
currentConfig.frameRate,
177+
currentConfig.bitRate,
178+
)
179+
onSegmentCreated(segment)
180+
}
181+
)
178182
}
179183
}

sentry-android-replay/src/main/java/io/sentry/android/replay/screenshot/CanvasStrategy.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import io.sentry.SentryOptions
3232
import io.sentry.android.replay.ExecutorProvider
3333
import io.sentry.android.replay.ScreenshotRecorderCallback
3434
import io.sentry.android.replay.ScreenshotRecorderConfig
35-
import io.sentry.android.replay.util.submitSafely
35+
import io.sentry.android.replay.util.ReplayRunnable
3636
import io.sentry.util.AutoClosableReentrantLock
3737
import io.sentry.util.IntegrationUtils
3838
import java.io.Closeable
@@ -173,7 +173,7 @@ internal class CanvasStrategy(
173173
holder.close()
174174
} else {
175175
unprocessedPictureRef.set(holder)
176-
executor.getExecutor().submitSafely(options, "screenshot_recorder.canvas", pictureRenderTask)
176+
executor.getExecutor().submit(ReplayRunnable("screenshot_recorder.canvas", pictureRenderTask))
177177
}
178178
}
179179

0 commit comments

Comments
 (0)