From 2df34a345f90eaf10436f2954cb069e69ec0f090 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 25 Mar 2024 14:10:40 +0100 Subject: [PATCH 1/5] Add SessionReplayOptions --- .../io/sentry/android/replay/ReplayCache.kt | 7 +- .../android/replay/ReplayIntegration.kt | 29 +------ .../android/replay/ScreenshotRecorder.kt | 36 +++++++- .../replay/video/SimpleVideoEncoder.kt | 11 +-- sentry/api/sentry.api | 20 +++++ .../java/io/sentry/ExperimentalOptions.java | 16 ++++ .../main/java/io/sentry/SentryOptions.java | 7 ++ .../java/io/sentry/SessionReplayOptions.java | 85 +++++++++++++++++++ 8 files changed, 171 insertions(+), 40 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/ExperimentalOptions.java create mode 100644 sentry/src/main/java/io/sentry/SessionReplayOptions.java diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt index da62dfb6b18..e591370444c 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt @@ -20,12 +20,7 @@ internal class ReplayCache( private val encoderCreator: (File) -> SimpleVideoEncoder = { videoFile -> SimpleVideoEncoder( options, - MuxerConfig( - file = videoFile, - recorderConfig = recorderConfig, - frameRate = recorderConfig.frameRate.toFloat(), - bitrate = 20 * 1000 - ) + MuxerConfig(file = videoFile, recorderConfig = recorderConfig) ).also { it.start() } } ) : Closeable { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index e1f0d2f7e3f..fa5859745cd 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -2,12 +2,9 @@ package io.sentry.android.replay import android.content.Context import android.graphics.Bitmap -import android.graphics.Point -import android.graphics.Rect import android.os.Build import android.os.Build.VERSION import android.os.Build.VERSION_CODES -import android.view.WindowManager import io.sentry.DateUtils import io.sentry.Hint import io.sentry.IHub @@ -33,7 +30,6 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference import kotlin.LazyThreadSafetyMode.NONE -import kotlin.math.roundToInt class ReplayIntegration( private val context: Context, @@ -59,28 +55,11 @@ class ReplayIntegration( private val saver = Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory()) - private val screenBounds by lazy(NONE) { - // PixelCopy takes screenshots including system bars, so we have to get the real size here - val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - if (VERSION.SDK_INT >= VERSION_CODES.R) { - wm.currentWindowMetrics.bounds - } else { - val screenBounds = Point() - @Suppress("DEPRECATION") - wm.defaultDisplay.getRealSize(screenBounds) - Rect(0, 0, screenBounds.x, screenBounds.y) - } - } - - private val aspectRatio by lazy(NONE) { - screenBounds.bottom.toFloat() / screenBounds.right.toFloat() - } - private val recorderConfig by lazy(NONE) { - ScreenshotRecorderConfig( - recordingWidth = (720 / aspectRatio).roundToInt(), - recordingHeight = 720, - scaleFactor = 720f / screenBounds.bottom + ScreenshotRecorderConfig.from( + context, + targetHeight = 720, + options._experimental.sessionReplayOptions ) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index 87915f8505a..a7094e10745 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -1,13 +1,17 @@ package io.sentry.android.replay import android.annotation.TargetApi +import android.content.Context import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.Canvas import android.graphics.Matrix import android.graphics.Paint +import android.graphics.Point import android.graphics.Rect import android.graphics.RectF +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import android.os.Handler import android.os.HandlerThread import android.os.Looper @@ -15,13 +19,16 @@ import android.view.PixelCopy import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver +import android.view.WindowManager import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions +import io.sentry.SessionReplayOptions import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode import java.lang.ref.WeakReference import java.util.WeakHashMap import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.roundToInt import kotlin.system.measureTimeMillis @TargetApi(26) @@ -217,8 +224,33 @@ internal data class ScreenshotRecorderConfig( val recordingWidth: Int, val recordingHeight: Int, val scaleFactor: Float, - val frameRate: Int = 2 -) + val frameRate: Int, + val bitRate: Int +) { + companion object { + fun from(context: Context, targetHeight: Int, sessionReplayOptions: SessionReplayOptions): ScreenshotRecorderConfig { + // PixelCopy takes screenshots including system bars, so we have to get the real size here + val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val screenBounds = if (VERSION.SDK_INT >= VERSION_CODES.R) { + wm.currentWindowMetrics.bounds + } else { + val screenBounds = Point() + @Suppress("DEPRECATION") + wm.defaultDisplay.getRealSize(screenBounds) + Rect(0, 0, screenBounds.x, screenBounds.y) + } + val aspectRatio = screenBounds.bottom.toFloat() / screenBounds.right.toFloat() + + return ScreenshotRecorderConfig( + recordingWidth = (targetHeight / aspectRatio).roundToInt(), + recordingHeight = targetHeight, + scaleFactor = targetHeight.toFloat() / screenBounds.bottom, + frameRate = sessionReplayOptions.frameRate, + bitRate = sessionReplayOptions.bitRate + ) + } + } +} interface ScreenshotRecorderCallback { fun onScreenshotRecorded(bitmap: Bitmap) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt index e2561faa1bb..73b88624349 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/video/SimpleVideoEncoder.kt @@ -62,8 +62,8 @@ internal class SimpleVideoEncoder( MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface ) - format.setInteger(MediaFormat.KEY_BIT_RATE, muxerConfig.bitrate) - format.setFloat(MediaFormat.KEY_FRAME_RATE, muxerConfig.frameRate) + format.setInteger(MediaFormat.KEY_BIT_RATE, muxerConfig.recorderConfig.bitRate) + format.setFloat(MediaFormat.KEY_FRAME_RATE, muxerConfig.recorderConfig.frameRate.toFloat()) format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10) format @@ -79,7 +79,7 @@ internal class SimpleVideoEncoder( } private val bufferInfo: MediaCodec.BufferInfo = MediaCodec.BufferInfo() - private val frameMuxer = muxerConfig.frameMuxer + private val frameMuxer = SimpleMp4FrameMuxer(muxerConfig.file.absolutePath, muxerConfig.recorderConfig.frameRate.toFloat()) val duration get() = frameMuxer.getVideoTime() private var surface: Surface? = null @@ -187,8 +187,5 @@ internal class SimpleVideoEncoder( internal data class MuxerConfig( val file: File, val recorderConfig: ScreenshotRecorderConfig, - val bitrate: Int = 20_000, - val frameRate: Float = recorderConfig.frameRate.toFloat(), - val mimeType: String = MediaFormat.MIMETYPE_VIDEO_AVC, - val frameMuxer: SimpleFrameMuxer = SimpleMp4FrameMuxer(file.absolutePath, frameRate) + val mimeType: String = MediaFormat.MIMETYPE_VIDEO_AVC ) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9e3a90946fb..756fd592044 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -305,6 +305,12 @@ public abstract interface class io/sentry/EventProcessor { public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } +public final class io/sentry/ExperimentalOptions { + public fun ()V + public fun getSessionReplayOptions ()Lio/sentry/SessionReplayOptions; + public fun setSessionReplayOptions (Lio/sentry/SessionReplayOptions;)V +} + public final class io/sentry/ExternalOptions { public fun ()V public fun addBundleId (Ljava/lang/String;)V @@ -2315,6 +2321,7 @@ public class io/sentry/SentryOptions { public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; public final fun getViewHierarchyExporters ()Ljava/util/List; + public fun get_experimental ()Lio/sentry/ExperimentalOptions; public fun isAttachServerName ()Z public fun isAttachStacktrace ()Z public fun isAttachThreads ()Z @@ -2686,6 +2693,19 @@ public final class io/sentry/Session$State : java/lang/Enum { public static fun values ()[Lio/sentry/Session$State; } +public final class io/sentry/SessionReplayOptions { + public fun ()V + public fun (Ljava/lang/Double;Ljava/lang/Double;)V + public fun getBitRate ()I + public fun getErrorReplayDuration ()J + public fun getErrorSampleRate ()Ljava/lang/Double; + public fun getFrameRate ()I + public fun getSessionSampleRate ()Ljava/lang/Double; + public fun getSessionSegmentDuration ()J + public fun setErrorSampleRate (Ljava/lang/Double;)V + public fun setSessionSampleRate (Ljava/lang/Double;)V +} + public final class io/sentry/ShutdownHookIntegration : io/sentry/Integration, java/io/Closeable { public fun ()V public fun (Ljava/lang/Runtime;)V diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java new file mode 100644 index 00000000000..8e7ade8ca93 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -0,0 +1,16 @@ +package io.sentry; + +import org.jetbrains.annotations.NotNull; + +public final class ExperimentalOptions { + private @NotNull SessionReplayOptions sessionReplayOptions = new SessionReplayOptions(); + + @NotNull + public SessionReplayOptions getSessionReplayOptions() { + return sessionReplayOptions; + } + + public void setSessionReplayOptions(final @NotNull SessionReplayOptions sessionReplayOptions) { + this.sessionReplayOptions = sessionReplayOptions; + } +} diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index d97b8c79d19..026f2c9e903 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -460,6 +460,8 @@ public class SentryOptions { */ private int profilingTracesHz = 101; + private final @NotNull ExperimentalOptions _experimental = new ExperimentalOptions(); + /** * Adds an event processor * @@ -2274,6 +2276,11 @@ public void setSessionFlushTimeoutMillis(final long sessionFlushTimeoutMillis) { this.sessionFlushTimeoutMillis = sessionFlushTimeoutMillis; } + @NotNull + public ExperimentalOptions get_experimental() { + return _experimental; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/SessionReplayOptions.java b/sentry/src/main/java/io/sentry/SessionReplayOptions.java new file mode 100644 index 00000000000..88b815a7c06 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SessionReplayOptions.java @@ -0,0 +1,85 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +public final class SessionReplayOptions { + + /** + * Indicates the percentage in which the replay for the session will be created. Specifying 0 + * means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0 The default is null + * (disabled). + */ + private @Nullable Double sessionSampleRate; + + /** + * Indicates the percentage in which a 30 seconds replay will be send with error events. + * Specifying 0 means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0. The + * default is null (disabled). + */ + private @Nullable Double errorSampleRate; + + /** + * Defines the quality of the session replay. Higher bit rates have better replay quality, but + * also affect the final payload size to transfer. The default value is 20kbps; + */ + private int bitRate = 20_000; + + /** + * Number of frames per second of the replay. The bigger the number, the more accurate the replay + * will be, but also more data to transfer and more CPU load. + */ + private int frameRate = 1; + + /** The maximum duration of replays for error events. */ + private long errorReplayDuration = 30_000L; + + /** The maximum duration of the segment of a session replay. */ + private long sessionSegmentDuration = 5000L; + + public SessionReplayOptions() {} + + public SessionReplayOptions( + final @Nullable Double sessionSampleRate, final @Nullable Double errorSampleRate) { + this.sessionSampleRate = sessionSampleRate; + this.errorSampleRate = errorSampleRate; + } + + @Nullable + public Double getErrorSampleRate() { + return errorSampleRate; + } + + public void setErrorSampleRate(final @Nullable Double errorSampleRate) { + this.errorSampleRate = errorSampleRate; + } + + @Nullable + public Double getSessionSampleRate() { + return sessionSampleRate; + } + + public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { + this.sessionSampleRate = sessionSampleRate; + } + + @ApiStatus.Internal + public int getBitRate() { + return bitRate; + } + + @ApiStatus.Internal + public int getFrameRate() { + return frameRate; + } + + @ApiStatus.Internal + public long getErrorReplayDuration() { + return errorReplayDuration; + } + + @ApiStatus.Internal + public long getSessionSegmentDuration() { + return sessionSegmentDuration; + } +} From c02f1dba4f894b7e9029c13774b4581708a63087 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 25 Mar 2024 14:57:44 +0100 Subject: [PATCH 2/5] Call listeners when installing RootViewsSpy --- .../src/main/java/io/sentry/android/replay/Windows.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt index ece684c46e0..e9c6761c75b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt @@ -139,6 +139,15 @@ internal class RootViewsSpy private constructor() { val listeners = CopyOnWriteArrayList() private val delegatingViewList = object : ArrayList() { + override fun addAll(elements: Collection): Boolean { + listeners.forEach { listener -> + elements.forEach { element -> + listener.onRootViewsChanged(element, true) + } + } + return super.addAll(elements) + } + override fun add(element: View): Boolean { listeners.forEach { it.onRootViewsChanged(element, true) } return super.add(element) From d6bb9ab654f4b0f7d883f0572dc121932fdb1630 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Mon, 25 Mar 2024 16:33:53 +0100 Subject: [PATCH 3/5] SessionReplayOptions -> SentryReplayOptions --- .../android/replay/ReplayIntegration.kt | 4 +-- .../android/replay/ScreenshotRecorder.kt | 8 ++--- sentry/api/sentry.api | 30 +++++++++---------- .../java/io/sentry/ExperimentalOptions.java | 10 +++---- ...yOptions.java => SentryReplayOptions.java} | 19 ++++++++++-- 5 files changed, 41 insertions(+), 30 deletions(-) rename sentry/src/main/java/io/sentry/{SessionReplayOptions.java => SentryReplayOptions.java} (77%) diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt index fa5859745cd..a99c8a9c95f 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt @@ -3,8 +3,6 @@ package io.sentry.android.replay import android.content.Context import android.graphics.Bitmap import android.os.Build -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES import io.sentry.DateUtils import io.sentry.Hint import io.sentry.IHub @@ -59,7 +57,7 @@ class ReplayIntegration( ScreenshotRecorderConfig.from( context, targetHeight = 720, - options._experimental.sessionReplayOptions + options._experimental.replayOptions ) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt index a7094e10745..7dfe26c4785 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/ScreenshotRecorder.kt @@ -23,7 +23,7 @@ import android.view.WindowManager import io.sentry.SentryLevel.DEBUG import io.sentry.SentryLevel.INFO import io.sentry.SentryOptions -import io.sentry.SessionReplayOptions +import io.sentry.SentryReplayOptions import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode import java.lang.ref.WeakReference import java.util.WeakHashMap @@ -228,7 +228,7 @@ internal data class ScreenshotRecorderConfig( val bitRate: Int ) { companion object { - fun from(context: Context, targetHeight: Int, sessionReplayOptions: SessionReplayOptions): ScreenshotRecorderConfig { + fun from(context: Context, targetHeight: Int, sentryReplayOptions: SentryReplayOptions): ScreenshotRecorderConfig { // PixelCopy takes screenshots including system bars, so we have to get the real size here val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val screenBounds = if (VERSION.SDK_INT >= VERSION_CODES.R) { @@ -245,8 +245,8 @@ internal data class ScreenshotRecorderConfig( recordingWidth = (targetHeight / aspectRatio).roundToInt(), recordingHeight = targetHeight, scaleFactor = targetHeight.toFloat() / screenBounds.bottom, - frameRate = sessionReplayOptions.frameRate, - bitRate = sessionReplayOptions.bitRate + frameRate = sentryReplayOptions.frameRate, + bitRate = sentryReplayOptions.bitRate ) } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 756fd592044..fcede26562f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -307,8 +307,8 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun ()V - public fun getSessionReplayOptions ()Lio/sentry/SessionReplayOptions; - public fun setSessionReplayOptions (Lio/sentry/SessionReplayOptions;)V + public fun getReplayOptions ()Lio/sentry/SentryReplayOptions; + public fun setReplayOptions (Lio/sentry/SentryReplayOptions;)V } public final class io/sentry/ExternalOptions { @@ -2540,6 +2540,19 @@ public final class io/sentry/SentryReplayEvent$ReplayType$Deserializer : io/sent public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; } +public final class io/sentry/SentryReplayOptions { + public fun ()V + public fun (Ljava/lang/Double;Ljava/lang/Double;)V + public fun getBitRate ()I + public fun getErrorReplayDuration ()J + public fun getErrorSampleRate ()Ljava/lang/Double; + public fun getFrameRate ()I + public fun getSessionSampleRate ()Ljava/lang/Double; + public fun getSessionSegmentDuration ()J + public fun setErrorSampleRate (Ljava/lang/Double;)V + public fun setSessionSampleRate (Ljava/lang/Double;)V +} + public final class io/sentry/SentrySpanStorage { public fun get (Ljava/lang/String;)Lio/sentry/ISpan; public static fun getInstance ()Lio/sentry/SentrySpanStorage; @@ -2693,19 +2706,6 @@ public final class io/sentry/Session$State : java/lang/Enum { public static fun values ()[Lio/sentry/Session$State; } -public final class io/sentry/SessionReplayOptions { - public fun ()V - public fun (Ljava/lang/Double;Ljava/lang/Double;)V - public fun getBitRate ()I - public fun getErrorReplayDuration ()J - public fun getErrorSampleRate ()Ljava/lang/Double; - public fun getFrameRate ()I - public fun getSessionSampleRate ()Ljava/lang/Double; - public fun getSessionSegmentDuration ()J - public fun setErrorSampleRate (Ljava/lang/Double;)V - public fun setSessionSampleRate (Ljava/lang/Double;)V -} - public final class io/sentry/ShutdownHookIntegration : io/sentry/Integration, java/io/Closeable { public fun ()V public fun (Ljava/lang/Runtime;)V diff --git a/sentry/src/main/java/io/sentry/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 8e7ade8ca93..b22d283724b 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -3,14 +3,14 @@ import org.jetbrains.annotations.NotNull; public final class ExperimentalOptions { - private @NotNull SessionReplayOptions sessionReplayOptions = new SessionReplayOptions(); + private @NotNull SentryReplayOptions replayOptions = new SentryReplayOptions(); @NotNull - public SessionReplayOptions getSessionReplayOptions() { - return sessionReplayOptions; + public SentryReplayOptions getReplayOptions() { + return replayOptions; } - public void setSessionReplayOptions(final @NotNull SessionReplayOptions sessionReplayOptions) { - this.sessionReplayOptions = sessionReplayOptions; + public void setReplayOptions(final @NotNull SentryReplayOptions replayOptions) { + this.replayOptions = replayOptions; } } diff --git a/sentry/src/main/java/io/sentry/SessionReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java similarity index 77% rename from sentry/src/main/java/io/sentry/SessionReplayOptions.java rename to sentry/src/main/java/io/sentry/SentryReplayOptions.java index 88b815a7c06..df98fc384f4 100644 --- a/sentry/src/main/java/io/sentry/SessionReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -1,9 +1,10 @@ package io.sentry; +import io.sentry.util.SampleRateUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -public final class SessionReplayOptions { +public final class SentryReplayOptions { /** * Indicates the percentage in which the replay for the session will be created. Specifying 0 @@ -37,9 +38,9 @@ public final class SessionReplayOptions { /** The maximum duration of the segment of a session replay. */ private long sessionSegmentDuration = 5000L; - public SessionReplayOptions() {} + public SentryReplayOptions() {} - public SessionReplayOptions( + public SentryReplayOptions( final @Nullable Double sessionSampleRate, final @Nullable Double errorSampleRate) { this.sessionSampleRate = sessionSampleRate; this.errorSampleRate = errorSampleRate; @@ -51,6 +52,12 @@ public Double getErrorSampleRate() { } public void setErrorSampleRate(final @Nullable Double errorSampleRate) { + if (!SampleRateUtils.isValidSampleRate(errorSampleRate)) { + throw new IllegalArgumentException( + "The value " + + errorSampleRate + + " is not valid. Use null to disable or values >= 0.0 and <= 1.0."); + } this.errorSampleRate = errorSampleRate; } @@ -60,6 +67,12 @@ public Double getSessionSampleRate() { } public void setSessionSampleRate(final @Nullable Double sessionSampleRate) { + if (!SampleRateUtils.isValidSampleRate(sessionSampleRate)) { + throw new IllegalArgumentException( + "The value " + + sessionSampleRate + + " is not valid. Use null to disable or values >= 0.0 and <= 1.0."); + } this.sessionSampleRate = sessionSampleRate; } From 7854e4fcdc67bf8f77ee4d47374be4a9c76a731f Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 26 Mar 2024 11:11:10 +0100 Subject: [PATCH 4/5] Fix test --- .../test/java/io/sentry/android/replay/ReplayCacheTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt index 88d410fab1a..dc4f5c29b59 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt @@ -36,7 +36,7 @@ class ReplayCacheTest { frameRate: Int, framesToEncode: Int = 0 ): ReplayCache { - val recorderConfig = ScreenshotRecorderConfig(100, 200, 1f, frameRate) + val recorderConfig = ScreenshotRecorderConfig(100, 200, 1f, frameRate = frameRate, bitRate = 20_000) options.run { cacheDirPath = dir?.newFolder()?.absolutePath } @@ -45,9 +45,7 @@ class ReplayCacheTest { options, MuxerConfig( file = videoFile, - recorderConfig = recorderConfig, - frameRate = recorderConfig.frameRate.toFloat(), - bitrate = 20 * 1000 + recorderConfig = recorderConfig ), onClose = { encodeFrame(framesToEncode, frameRate, size = 0, flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM) From 2cddcc441fe575b29a044d3f7759a97df32ab69e Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 26 Mar 2024 11:49:16 +0100 Subject: [PATCH 5/5] Add AndroidManifest options for replays --- .../android/core/ManifestMetadataReader.java | 19 +++++++++ .../core/ManifestMetadataReaderTest.kt | 42 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 93bf928285f..32ad9df290d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -100,6 +100,10 @@ final class ManifestMetadataReader { static final String ENABLE_APP_START_PROFILING = "io.sentry.profiling.enable-app-start"; + static final String REPLAYS_SESSION_SAMPLE_RATE = "io.sentry.replays.session-sample-rate"; + + static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.replays.error-sample-rate"; + /** ManifestMetadataReader ctor */ private ManifestMetadataReader() {} @@ -371,6 +375,21 @@ static void applyMetadata( options.setEnableAppStartProfiling( readBool( metadata, logger, ENABLE_APP_START_PROFILING, options.isEnableAppStartProfiling())); + + if (options.get_experimental().getReplayOptions().getSessionSampleRate() == null) { + final Double sessionSampleRate = + readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE); + if (sessionSampleRate != -1) { + options.get_experimental().getReplayOptions().setSessionSampleRate(sessionSampleRate); + } + } + + if (options.get_experimental().getReplayOptions().getErrorSampleRate() == null) { + final Double errorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE); + if (errorSampleRate != -1) { + options.get_experimental().getReplayOptions().setErrorSampleRate(errorSampleRate); + } + } } options diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 2cff98ed396..03f8e5291fd 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1370,4 +1370,46 @@ class ManifestMetadataReaderTest { // Assert assertFalse(fixture.options.isEnableAppStartProfiling) } + + @Test + fun `applyMetadata reads replays errorSampleRate from metadata`() { + // Arrange + val expectedSampleRate = 0.99f + + val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to expectedSampleRate) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options._experimental.replayOptions.errorSampleRate) + } + + @Test + fun `applyMetadata does not override replays errorSampleRate from options`() { + // Arrange + val expectedSampleRate = 0.99f + fixture.options._experimental.replayOptions.errorSampleRate = expectedSampleRate.toDouble() + val bundle = bundleOf(ManifestMetadataReader.REPLAYS_ERROR_SAMPLE_RATE to 0.1f) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options._experimental.replayOptions.errorSampleRate) + } + + @Test + fun `applyMetadata without specifying replays errorSampleRate, stays null`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertNull(fixture.options._experimental.replayOptions.errorSampleRate) + } }