From a20440fe57f44283df8f01601d41ed63f0a2f3e1 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 22 Jul 2026 11:00:46 +0200 Subject: [PATCH 1/2] fix(replay): Skip buffer-mode replay capture when rate-limited (DART-313) In buffer (on-error) mode the recorder keeps running while rate-limited so the rolling buffer stays warm, but capturing on an error still encoded the current and buffered segments and handed them to the transport, which then dropped them. That wasted CPU, I/O, and MediaMuxer file descriptors on envelopes that could never be sent. Bail out of BufferCaptureStrategy.captureReplay when the Replay (or All) category is rate-limited, mirroring the guard session mode already applies. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../replay/capture/BufferCaptureStrategy.kt | 14 ++++++++++++++ .../replay/capture/BufferCaptureStrategyTest.kt | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt index f6c6f3997ae..8d94ca01ad9 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import android.annotation.TargetApi import android.graphics.Bitmap import android.view.MotionEvent +import io.sentry.DataCategory.All +import io.sentry.DataCategory.Replay import io.sentry.DateUtils import io.sentry.IScopes import io.sentry.SentryLevel.DEBUG @@ -76,6 +78,13 @@ internal class BufferCaptureStrategy( } override fun captureReplay(isTerminating: Boolean, onSegmentSent: (Date) -> Unit) { + if (isReplayRateLimited()) { + // the segment envelopes would be dropped by the transport anyway, so don't waste resources + // encoding videos that will only be discarded + options.logger.log(INFO, "Replay is rate-limited, not capturing for event") + return + } + val sampled = random.sample(options.sessionReplay.onErrorSampleRate) if (!sampled) { @@ -170,6 +179,11 @@ internal class BufferCaptureStrategy( rotateEvents(currentEvents, bufferLimit) } + private fun isReplayRateLimited(): Boolean = + scopes?.rateLimiter?.let { + it.isActiveForCategory(All) || it.isActiveForCategory(Replay) + } == true + private fun deleteFile(file: File?) { if (file == null) { return diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt index b5048e856ff..1adcc7bc4dc 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt @@ -20,6 +20,7 @@ import io.sentry.android.replay.capture.BufferCaptureStrategyTest.Fixture.Compan import io.sentry.protocol.SentryId import io.sentry.transport.CurrentDateProvider import io.sentry.transport.ICurrentDateProvider +import io.sentry.transport.RateLimiter import io.sentry.util.Random import java.io.File import kotlin.test.Test @@ -336,6 +337,22 @@ class BufferCaptureStrategyTest { assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) } + @Test + fun `captureReplay does nothing when rate-limited`() { + val rateLimiter = mock { on { isActiveForCategory(any()) }.thenReturn(true) } + whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) + val strategy = fixture.getSut() + strategy.start() + strategy.onConfigurationChanged(fixture.recorderConfig) + strategy.pause() + + strategy.captureReplay(false) {} + + // neither the current nor the buffered segment should be sent while rate-limited + verify(fixture.scopes, never()).captureReplay(any(), any()) + assertEquals(SentryId.EMPTY_ID, fixture.scope.replayId) + } + @Test fun `captureReplay sets replayId to scope and captures buffered segments`() { var called = false From 8a92042879c85d8c2e28a8cede887f571fdfab02 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Wed, 22 Jul 2026 11:01:31 +0200 Subject: [PATCH 2/2] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b74388723..77b364b75c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- Skip encoding and capturing buffered session replay segments while rate-limited, so we don't waste resources on envelopes the transport will drop ([#5813](https://github.com/getsentry/sentry-java/pull/5813)) - Reduce main-thread work during `Sentry.init` by resolving the shake-detector accelerometer off the main thread (~1.75ms on a Pixel 10) ([#5784](https://github.com/getsentry/sentry-java/pull/5784)) - Backfill release, environment, distribution, tags, and app version/build—and use the matching replay-on-error sample rate—for `ApplicationExitInfo` ANR and native crash events captured before SDK initialization, without reusing options cached by a later app update ([#5762](https://github.com/getsentry/sentry-java/pull/5762)) - `SentryTagModifierNode.isImportantForBounds` now matches the default behavior and returns `true` ([#5789](https://github.com/getsentry/sentry-java/pull/5789))