Skip to content

Commit adbc51d

Browse files
authored
[SR] Disable replay in session mode when rate limit is active (#3854)
* Do not capture screenshots in session mode when rate limit is active * Changelog * WIP * Format code * Change approach to rate-limit and offline * Clean up * Tests * Api dump * Fix tests * Address PR feedback * Fix tests
1 parent dab52e2 commit adbc51d

File tree

14 files changed

+369
-16
lines changed

14 files changed

+369
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887))
1414
- Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888))
1515
- This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported
16+
- Session Replay: Disable replay in session mode when rate limit is active ([#3854](https://github.com/getsentry/sentry-java/pull/3854))
1617

1718
### Dependencies
1819

sentry-android-replay/api/sentry-android-replay.api

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public final class io/sentry/android/replay/ReplayCache$Companion {
5757
public final fun makeReplayCacheDir (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)Ljava/io/File;
5858
}
5959

60-
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, java/io/Closeable {
60+
public final class io/sentry/android/replay/ReplayIntegration : android/content/ComponentCallbacks, io/sentry/IConnectionStatusProvider$IConnectionStatusObserver, io/sentry/Integration, io/sentry/ReplayController, io/sentry/android/replay/ScreenshotRecorderCallback, io/sentry/android/replay/gestures/TouchRecorderCallback, io/sentry/transport/RateLimiter$IRateLimitObserver, java/io/Closeable {
6161
public static final field $stable I
6262
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V
6363
public fun <init> (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
@@ -69,7 +69,9 @@ public final class io/sentry/android/replay/ReplayIntegration : android/content/
6969
public fun getReplayId ()Lio/sentry/protocol/SentryId;
7070
public fun isRecording ()Z
7171
public fun onConfigurationChanged (Landroid/content/res/Configuration;)V
72+
public fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V
7273
public fun onLowMemory ()V
74+
public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V
7375
public fun onScreenshotRecorded (Landroid/graphics/Bitmap;)V
7476
public fun onScreenshotRecorded (Ljava/io/File;J)V
7577
public fun onTouchEvent (Landroid/view/MotionEvent;)V

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

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import android.graphics.Bitmap
77
import android.os.Build
88
import android.view.MotionEvent
99
import io.sentry.Breadcrumb
10+
import io.sentry.DataCategory.All
11+
import io.sentry.DataCategory.Replay
12+
import io.sentry.IConnectionStatusProvider.ConnectionStatus
13+
import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED
14+
import io.sentry.IConnectionStatusProvider.IConnectionStatusObserver
1015
import io.sentry.IHub
1116
import io.sentry.Integration
1217
import io.sentry.NoOpReplayBreadcrumbConverter
@@ -32,6 +37,8 @@ import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME
3237
import io.sentry.hints.Backfillable
3338
import io.sentry.protocol.SentryId
3439
import io.sentry.transport.ICurrentDateProvider
40+
import io.sentry.transport.RateLimiter
41+
import io.sentry.transport.RateLimiter.IRateLimitObserver
3542
import io.sentry.util.FileUtils
3643
import io.sentry.util.HintUtils
3744
import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion
@@ -48,7 +55,14 @@ public class ReplayIntegration(
4855
private val recorderProvider: (() -> Recorder)? = null,
4956
private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null,
5057
private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null
51-
) : Integration, Closeable, ScreenshotRecorderCallback, TouchRecorderCallback, ReplayController, ComponentCallbacks {
58+
) : Integration,
59+
Closeable,
60+
ScreenshotRecorderCallback,
61+
TouchRecorderCallback,
62+
ReplayController,
63+
ComponentCallbacks,
64+
IConnectionStatusObserver,
65+
IRateLimitObserver {
5266

5367
// needed for the Java's call site
5468
constructor(context: Context, dateProvider: ICurrentDateProvider) : this(
@@ -113,6 +127,8 @@ public class ReplayIntegration(
113127
gestureRecorder = gestureRecorderProvider?.invoke() ?: GestureRecorder(options, this)
114128
isEnabled.set(true)
115129

130+
options.connectionStatusProvider.addConnectionStatusObserver(this)
131+
hub.rateLimiter?.addRateLimitObserver(this)
116132
try {
117133
context.registerComponentCallbacks(this)
118134
} catch (e: Throwable) {
@@ -222,12 +238,14 @@ public class ReplayIntegration(
222238
hub?.configureScope { screen = it.screen?.substringAfterLast('.') }
223239
captureStrategy?.onScreenshotRecorded(bitmap) { frameTimeStamp ->
224240
addFrame(bitmap, frameTimeStamp, screen)
241+
checkCanRecord()
225242
}
226243
}
227244

228245
override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) {
229246
captureStrategy?.onScreenshotRecorded { _ ->
230247
addFrame(screenshot, frameTimestamp)
248+
checkCanRecord()
231249
}
232250
}
233251

@@ -236,6 +254,8 @@ public class ReplayIntegration(
236254
return
237255
}
238256

257+
options.connectionStatusProvider.removeConnectionStatusObserver(this)
258+
hub?.rateLimiter?.removeRateLimitObserver(this)
239259
try {
240260
context.unregisterComponentCallbacks(this)
241261
} catch (ignored: Throwable) {
@@ -259,12 +279,55 @@ public class ReplayIntegration(
259279
recorder?.start(recorderConfig)
260280
}
261281

282+
override fun onConnectionStatusChanged(status: ConnectionStatus) {
283+
if (captureStrategy !is SessionCaptureStrategy) {
284+
// we only want to stop recording when offline for session mode
285+
return
286+
}
287+
288+
if (status == DISCONNECTED) {
289+
pause()
290+
} else {
291+
// being positive for other states, even if it's NO_PERMISSION
292+
resume()
293+
}
294+
}
295+
296+
override fun onRateLimitChanged(rateLimiter: RateLimiter) {
297+
if (captureStrategy !is SessionCaptureStrategy) {
298+
// we only want to stop recording when rate-limited for session mode
299+
return
300+
}
301+
302+
if (rateLimiter.isActiveForCategory(All) || rateLimiter.isActiveForCategory(Replay)) {
303+
pause()
304+
} else {
305+
resume()
306+
}
307+
}
308+
262309
override fun onLowMemory() = Unit
263310

264311
override fun onTouchEvent(event: MotionEvent) {
265312
captureStrategy?.onTouchEvent(event)
266313
}
267314

315+
/**
316+
* Check if we're offline or rate-limited and pause for session mode to not overflow the
317+
* envelope cache.
318+
*/
319+
private fun checkCanRecord() {
320+
if (captureStrategy is SessionCaptureStrategy &&
321+
(
322+
options.connectionStatusProvider.connectionStatus == DISCONNECTED ||
323+
hub?.rateLimiter?.isActiveForCategory(All) == true ||
324+
hub?.rateLimiter?.isActiveForCategory(Replay) == true
325+
)
326+
) {
327+
pause()
328+
}
329+
}
330+
268331
private fun registerRootViewListeners() {
269332
if (recorder is OnRootViewsChangedListener) {
270333
rootViewsSpy.listeners += (recorder as OnRootViewsChangedListener)

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import java.lang.ref.WeakReference
3535
import java.util.concurrent.Executors
3636
import java.util.concurrent.ThreadFactory
3737
import java.util.concurrent.atomic.AtomicBoolean
38-
import java.util.concurrent.atomic.AtomicReference
3938
import kotlin.LazyThreadSafetyMode.NONE
4039
import kotlin.math.roundToInt
4140

@@ -51,7 +50,6 @@ internal class ScreenshotRecorder(
5150
Executors.newSingleThreadScheduledExecutor(RecorderExecutorServiceThreadFactory())
5251
}
5352
private var rootView: WeakReference<View>? = null
54-
private val pendingViewHierarchy = AtomicReference<ViewHierarchyNode>()
5553
private val maskingPaint by lazy(NONE) { Paint() }
5654
private val singlePixelBitmap: Bitmap by lazy(NONE) {
5755
Bitmap.createBitmap(
@@ -230,7 +228,6 @@ internal class ScreenshotRecorder(
230228
unbind(rootView?.get())
231229
rootView?.clear()
232230
lastScreenshot?.recycle()
233-
pendingViewHierarchy.set(null)
234231
isCapturing.set(false)
235232
recorder.gracefullyShutdown(options)
236233
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ internal interface CaptureStrategy {
5656
fun close()
5757

5858
companion object {
59+
private const val BREADCRUMB_START_OFFSET = 100L
5960
internal val currentEventsLock = Any()
6061

6162
fun createSegment(
@@ -161,7 +162,10 @@ internal interface CaptureStrategy {
161162

162163
val urls = LinkedList<String>()
163164
breadcrumbs.forEach { breadcrumb ->
164-
if (breadcrumb.timestamp.time >= segmentTimestamp.time &&
165+
// we add some fixed breadcrumb offset to make sure we don't miss any
166+
// breadcrumbs that might be relevant for the current segment, but just happened
167+
// earlier than the current segment (e.g. network connectivity changed)
168+
if ((breadcrumb.timestamp.time + BREADCRUMB_START_OFFSET) >= segmentTimestamp.time &&
165169
breadcrumb.timestamp.time < endTimestamp.time
166170
) {
167171
val rrwebEvent = options

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package io.sentry.android.replay.capture
22

33
import android.graphics.Bitmap
4-
import io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED
54
import io.sentry.IHub
65
import io.sentry.SentryLevel.DEBUG
76
import io.sentry.SentryLevel.INFO
@@ -73,11 +72,6 @@ internal class SessionCaptureStrategy(
7372
}
7473

7574
override fun onScreenshotRecorded(bitmap: Bitmap?, store: ReplayCache.(frameTimestamp: Long) -> Unit) {
76-
if (options.connectionStatusProvider.connectionStatus == DISCONNECTED) {
77-
options.logger.log(DEBUG, "Skipping screenshot recording, no internet connection")
78-
bitmap?.recycle()
79-
return
80-
}
8175
// have to do it before submitting, otherwise if the queue is busy, the timestamp won't be
8276
// reflecting the exact time of when it was captured
8377
val frameTimestamp = dateProvider.currentTimeMillis

0 commit comments

Comments
 (0)