Skip to content
Draft
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 @@ -4,6 +4,7 @@

### Fixes

- Fix background ANR when a session replay reaches its duration deadline: the resulting `stop()` (and its segment encoding) no longer runs on the replay worker thread while holding the replay lifecycle lock, which could block a foreground `start()` on the main thread ([#5826](https://github.com/getsentry/sentry-java/pull/5826))
- Pin the published Sentry Android SDK's AAR metadata `minCompileSdk` to our `minSdk` (`21`) instead of AGP 9's new default of the SDK's own `compileSdk` (`37`), so apps that depend on the SDK aren't forced to raise their `compileSdk` ([#5823](https://github.com/getsentry/sentry-java/pull/5823))

### Performance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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.ReplayRunnable
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 @@ -134,8 +135,13 @@ internal class SessionCaptureStrategy(
}

if ((now - replayStartTimestamp.get() >= options.sessionReplay.sessionDuration)) {
options.replayController.stop()
options.logger.log(INFO, "Session replay deadline exceeded (1h), stopping recording")
// dispatch off the replay worker thread, otherwise stop() would run the final segment
// encoding synchronously while holding the replay lifecycle lock, blocking a foreground
// start() on the main thread (ANR)
options.executorService.submitSafely(options, "$TAG.stop") {
options.replayController.stop()
}
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import io.sentry.protocol.SentryId
import io.sentry.rrweb.RRWebBreadcrumbEvent
import io.sentry.rrweb.RRWebMetaEvent
import io.sentry.rrweb.RRWebOptionsEvent
import io.sentry.test.DeferredExecutorService
import io.sentry.test.ImmediateExecutorService
import io.sentry.transport.CurrentDateProvider
import io.sentry.transport.ICurrentDateProvider
import java.io.File
Expand Down Expand Up @@ -66,6 +68,7 @@ class SessionCaptureStrategyTest {
setReplayController(
mock { on { breadcrumbConverter }.thenReturn(DefaultReplayBreadcrumbConverter()) }
)
executorService = ImmediateExecutorService()
}
val scope = Scope(options)
val scopes =
Expand Down Expand Up @@ -291,6 +294,35 @@ class SessionCaptureStrategyTest {
verify(fixture.options.replayController).stop()
}

@Test
fun `onScreenshotRecorded dispatches deadline stop off the calling thread`() {
val deferredExecutor = DeferredExecutorService()
fixture.options.executorService = deferredExecutor
var count = 0
val strategy =
fixture.getSut(
dateProvider = {
if (count++ == 2) {
System.currentTimeMillis() + (fixture.options.sessionReplay.sessionDuration * 2)
} else {
System.currentTimeMillis()
}
}
)
strategy.start()
strategy.onConfigurationChanged(mock<ScreenshotRecorderConfig>())

strategy.onScreenshotRecorded(mock<Bitmap>()) {}

// stop must not run inline on the caller (replay worker) thread, otherwise it would encode the
// final segment while holding the lifecycle lock and block a foreground start() (ANR)
verify(fixture.options.replayController, never()).stop()

deferredExecutor.runAll()

verify(fixture.options.replayController).stop()
}

@Test
fun `onConfigurationChanged creates new segment and updates config`() {
val strategy = fixture.getSut()
Expand Down
Loading