Skip to content

Commit

Permalink
Merge pull request #3283 from getsentry/rz/feat/session-replay-options
Browse files Browse the repository at this point in the history
[SR] Add SessionReplayOptions
  • Loading branch information
romtsn authored Apr 3, 2024
2 parents b04aaf2 + 5278c86 commit 023cb5f
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.session-replay.session-sample-rate";

static final String REPLAYS_ERROR_SAMPLE_RATE = "io.sentry.session-replay.error-sample-rate";

/** ManifestMetadataReader ctor */
private ManifestMetadataReader() {}

Expand Down Expand Up @@ -371,6 +375,24 @@ static void applyMetadata(
options.setEnableAppStartProfiling(
readBool(
metadata, logger, ENABLE_APP_START_PROFILING, options.isEnableAppStartProfiling()));

if (options.getExperimental().getSessionReplayOptions().getSessionSampleRate() == null) {
final Double sessionSampleRate =
readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE);
if (sessionSampleRate != -1) {
options
.getExperimental()
.getSessionReplayOptions()
.setSessionSampleRate(sessionSampleRate);
}
}

if (options.getExperimental().getSessionReplayOptions().getErrorSampleRate() == null) {
final Double errorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE);
if (errorSampleRate != -1) {
options.getExperimental().getSessionReplayOptions().setErrorSampleRate(errorSampleRate);
}
}
}

options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.sessionReplayOptions.errorSampleRate)
}

@Test
fun `applyMetadata does not override replays errorSampleRate from options`() {
// Arrange
val expectedSampleRate = 0.99f
fixture.options.experimental.sessionReplayOptions.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.sessionReplayOptions.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.sessionReplayOptions.errorSampleRate)
}
}
14 changes: 10 additions & 4 deletions sentry-android-replay/api/sentry-android-replay.api
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@ public abstract interface class io/sentry/android/replay/ScreenshotRecorderCallb
}

public final class io/sentry/android/replay/ScreenshotRecorderConfig {
public fun <init> (IIFI)V
public synthetic fun <init> (IIFIILkotlin/jvm/internal/DefaultConstructorMarker;)V
public static final field Companion Lio/sentry/android/replay/ScreenshotRecorderConfig$Companion;
public fun <init> (IIFII)V
public final fun component1 ()I
public final fun component2 ()I
public final fun component3 ()F
public final fun component4 ()I
public final fun copy (IIFI)Lio/sentry/android/replay/ScreenshotRecorderConfig;
public static synthetic fun copy$default (Lio/sentry/android/replay/ScreenshotRecorderConfig;IIFIILjava/lang/Object;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
public final fun component5 ()I
public final fun copy (IIFII)Lio/sentry/android/replay/ScreenshotRecorderConfig;
public static synthetic fun copy$default (Lio/sentry/android/replay/ScreenshotRecorderConfig;IIFIIILjava/lang/Object;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getBitRate ()I
public final fun getFrameRate ()I
public final fun getRecordingHeight ()I
public final fun getRecordingWidth ()I
Expand All @@ -74,6 +76,10 @@ public final class io/sentry/android/replay/ScreenshotRecorderConfig {
public fun toString ()Ljava/lang/String;
}

public final class io/sentry/android/replay/ScreenshotRecorderConfig$Companion {
public final fun from (Landroid/content/Context;ILio/sentry/SentryReplayOptions;)Lio/sentry/android/replay/ScreenshotRecorderConfig;
}

public abstract interface class io/sentry/android/replay/video/SimpleFrameMuxer {
public abstract fun getVideoTime ()J
public abstract fun isStarted ()Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,7 @@ public class ReplayCache internal constructor(
) : this(options, replayId, recorderConfig, encoderCreator = { videoFile ->
SimpleVideoEncoder(
options,
MuxerConfig(
file = videoFile,
recorderConfig = recorderConfig,
frameRate = recorderConfig.frameRate.toFloat(),
bitrate = 20 * 1000
)
MuxerConfig(file = videoFile, recorderConfig = recorderConfig)
).also { it.start() }
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@ 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
Expand All @@ -33,7 +28,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,
Expand All @@ -59,28 +53,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.height().toFloat() / screenBounds.width().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
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
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
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.SentryLevel.WARNING
import io.sentry.SentryOptions
import io.sentry.SentryReplayOptions
import io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.roundToInt
import kotlin.system.measureTimeMillis

@TargetApi(26)
Expand Down Expand Up @@ -237,8 +244,33 @@ public 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, 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) {
wm.currentWindowMetrics.bounds
} else {
val screenBounds = Point()
@Suppress("DEPRECATION")
wm.defaultDisplay.getRealSize(screenBounds)
Rect(0, 0, screenBounds.x, screenBounds.y)
}
val aspectRatio = screenBounds.height().toFloat() / screenBounds.width().toFloat()

return ScreenshotRecorderConfig(
recordingWidth = (targetHeight / aspectRatio).roundToInt(),
recordingHeight = targetHeight,
scaleFactor = targetHeight.toFloat() / screenBounds.height(),
frameRate = sentryReplayOptions.frameRate,
bitRate = sentryReplayOptions.bitRate
)
}
}
}

interface ScreenshotRecorderCallback {
fun onScreenshotRecorded(bitmap: Bitmap)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,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
}
Expand All @@ -46,9 +46,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)
Expand Down
20 changes: 20 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> ()V
public fun getSessionReplayOptions ()Lio/sentry/SentryReplayOptions;
public fun setSessionReplayOptions (Lio/sentry/SentryReplayOptions;)V
}

public final class io/sentry/ExternalOptions {
public fun <init> ()V
public fun addBundleId (Ljava/lang/String;)V
Expand Down Expand Up @@ -2262,6 +2268,7 @@ public class io/sentry/SentryOptions {
public fun getEnvironment ()Ljava/lang/String;
public fun getEventProcessors ()Ljava/util/List;
public fun getExecutorService ()Lio/sentry/ISentryExecutorService;
public fun getExperimental ()Lio/sentry/ExperimentalOptions;
public fun getFlushTimeoutMillis ()J
public fun getFullyDisplayedReporter ()Lio/sentry/FullyDisplayedReporter;
public fun getGestureTargetLocators ()Ljava/util/List;
Expand Down Expand Up @@ -2533,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 <init> ()V
public fun <init> (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;
Expand Down
20 changes: 20 additions & 0 deletions sentry/src/main/java/io/sentry/ExperimentalOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.sentry;

import org.jetbrains.annotations.NotNull;

/**
* Experimental options for new features, these options are going to be promoted to SentryOptions
* before GA
*/
public final class ExperimentalOptions {
private @NotNull SentryReplayOptions sessionReplayOptions = new SentryReplayOptions();

@NotNull
public SentryReplayOptions getSessionReplayOptions() {
return sessionReplayOptions;
}

public void setSessionReplayOptions(final @NotNull SentryReplayOptions sessionReplayOptions) {
this.sessionReplayOptions = sessionReplayOptions;
}
}
7 changes: 7 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,8 @@ public class SentryOptions {
*/
private int profilingTracesHz = 101;

private final @NotNull ExperimentalOptions experimental = new ExperimentalOptions();

/**
* Adds an event processor
*
Expand Down Expand Up @@ -2274,6 +2276,11 @@ public void setSessionFlushTimeoutMillis(final long sessionFlushTimeoutMillis) {
this.sessionFlushTimeoutMillis = sessionFlushTimeoutMillis;
}

@NotNull
public ExperimentalOptions getExperimental() {
return experimental;
}

/** The BeforeSend callback */
public interface BeforeSendCallback {

Expand Down
Loading

0 comments on commit 023cb5f

Please sign in to comment.