diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dffe87b4c..8649137bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - Session Replay: Fix memory leak when masking Compose screens ([#3985](https://github.com/getsentry/sentry-java/pull/3985)) - Session Replay: Fix potential ANRs in `GestureRecorder` ([#4001](https://github.com/getsentry/sentry-java/pull/4001)) +### Internal + +- Session Replay: Flutter improvements ([#4007](https://github.com/getsentry/sentry-java/pull/4007)) + ## 7.19.0 ### Fixes diff --git a/sentry-android-replay/api/sentry-android-replay.api b/sentry-android-replay/api/sentry-android-replay.api index 7e2db5248f..33043e69b6 100644 --- a/sentry-android-replay/api/sentry-android-replay.api +++ b/sentry-android-replay/api/sentry-android-replay.api @@ -43,12 +43,12 @@ public abstract interface class io/sentry/android/replay/Recorder : java/io/Clos public final class io/sentry/android/replay/ReplayCache : java/io/Closeable { public static final field $stable I public static final field Companion Lio/sentry/android/replay/ReplayCache$Companion; - public fun (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;Lio/sentry/android/replay/ScreenshotRecorderConfig;)V + public fun (Lio/sentry/SentryOptions;Lio/sentry/protocol/SentryId;)V public final fun addFrame (Ljava/io/File;JLjava/lang/String;)V public static synthetic fun addFrame$default (Lio/sentry/android/replay/ReplayCache;Ljava/io/File;JLjava/lang/String;ILjava/lang/Object;)V public fun close ()V - public final fun createVideoOf (JJIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo; - public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo; + public final fun createVideoOf (JJIIIIILjava/io/File;)Lio/sentry/android/replay/GeneratedVideo; + public static synthetic fun createVideoOf$default (Lio/sentry/android/replay/ReplayCache;JJIIIIILjava/io/File;ILjava/lang/Object;)Lio/sentry/android/replay/GeneratedVideo; public final fun persistSegmentValues (Ljava/lang/String;Ljava/lang/String;)V public final fun rotate (J)Ljava/lang/String; } @@ -60,8 +60,8 @@ public final class io/sentry/android/replay/ReplayCache$Companion { 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 { public static final field $stable I public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;)V - public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Landroid/content/Context;Lio/sentry/transport/ICurrentDateProvider;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun captureReplay (Ljava/lang/Boolean;)V public fun close ()V public fun getBreadcrumbConverter ()Lio/sentry/ReplayBreadcrumbConverter; 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 3db92ea5d8..a757c4b455 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 @@ -38,8 +38,7 @@ import java.util.concurrent.atomic.AtomicBoolean */ public class ReplayCache( private val options: SentryOptions, - private val replayId: SentryId, - private val recorderConfig: ScreenshotRecorderConfig + private val replayId: SentryId ) : Closeable { private val isClosed = AtomicBoolean(false) @@ -133,6 +132,8 @@ public class ReplayCache( segmentId: Int, height: Int, width: Int, + frameRate: Int, + bitRate: Int, videoFile: File = File(replayCacheDir, "$segmentId.mp4") ): GeneratedVideo? { if (videoFile.exists() && videoFile.length() > 0) { @@ -146,7 +147,6 @@ public class ReplayCache( return null } - // TODO: reuse instance of encoder and just change file path to create a different muxer encoder = synchronized(encoderLock) { SimpleVideoEncoder( options, @@ -154,13 +154,13 @@ public class ReplayCache( file = videoFile, recordingHeight = height, recordingWidth = width, - frameRate = recorderConfig.frameRate, - bitRate = recorderConfig.bitRate + frameRate = frameRate, + bitRate = bitRate ) ).also { it.start() } } - val step = 1000 / recorderConfig.frameRate.toLong() + val step = 1000 / frameRate.toLong() var frameCount = 0 var lastFrame: ReplayFrame = frames.first() for (timestamp in from until (from + (duration)) step step) { @@ -306,7 +306,7 @@ public class ReplayCache( } } - internal fun fromDisk(options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null): LastSegmentData? { + internal fun fromDisk(options: SentryOptions, replayId: SentryId, replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null): LastSegmentData? { val replayCacheDir = makeReplayCacheDir(options, replayId) val lastSegmentFile = File(replayCacheDir, ONGOING_SEGMENT) if (!lastSegmentFile.exists()) { @@ -360,7 +360,7 @@ public class ReplayCache( scaleFactorY = 1.0f ) - val cache = replayCacheProvider?.invoke(replayId, recorderConfig) ?: ReplayCache(options, replayId, recorderConfig) + val cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId) cache.replayCacheDir?.listFiles { dir, name -> if (name.endsWith(".jpg")) { val file = File(dir, name) 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 5b7e3ecae6..4148fbb26c 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 @@ -56,7 +56,7 @@ public class ReplayIntegration( private val dateProvider: ICurrentDateProvider, private val recorderProvider: (() -> Recorder)? = null, private val recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)? = null, - private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null + private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null ) : Integration, Closeable, ScreenshotRecorderCallback, @@ -80,7 +80,7 @@ public class ReplayIntegration( dateProvider: ICurrentDateProvider, recorderProvider: (() -> Recorder)?, recorderConfigProvider: ((configChanged: Boolean) -> ScreenshotRecorderConfig)?, - replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)?, + replayCacheProvider: ((replayId: SentryId) -> ReplayCache)?, replayCaptureStrategyProvider: ((isFullSession: Boolean) -> CaptureStrategy)? = null, mainLooperHandler: MainLooperHandler? = null, gestureRecorderProvider: (() -> GestureRecorder)? = null @@ -110,8 +110,6 @@ public class ReplayIntegration( private var mainLooperHandler: MainLooperHandler = MainLooperHandler() private var gestureRecorderProvider: (() -> GestureRecorder)? = null - private lateinit var recorderConfig: ScreenshotRecorderConfig - override fun register(hub: IHub, options: SentryOptions) { this.options = options @@ -134,10 +132,16 @@ public class ReplayIntegration( options.connectionStatusProvider.addConnectionStatusObserver(this) hub.rateLimiter?.addRateLimitObserver(this) - try { - context.registerComponentCallbacks(this) - } catch (e: Throwable) { - options.logger.log(INFO, "ComponentCallbacks is not available, orientation changes won't be handled by Session replay", e) + if (options.experimental.sessionReplay.isTrackOrientationChange) { + try { + context.registerComponentCallbacks(this) + } catch (e: Throwable) { + options.logger.log( + INFO, + "ComponentCallbacks is not available, orientation changes won't be handled by Session replay", + e + ) + } } addIntegrationToSdkVersion("Replay") @@ -169,7 +173,7 @@ public class ReplayIntegration( return } - recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay) + val recorderConfig = recorderConfigProvider?.invoke(false) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay) captureStrategy = replayCaptureStrategyProvider?.invoke(isFullSession) ?: if (isFullSession) { SessionCaptureStrategy(options, hub, dateProvider, replayExecutor, replayCacheProvider) } else { @@ -260,9 +264,11 @@ public class ReplayIntegration( options.connectionStatusProvider.removeConnectionStatusObserver(this) hub?.rateLimiter?.removeRateLimitObserver(this) - try { - context.unregisterComponentCallbacks(this) - } catch (ignored: Throwable) { + if (options.experimental.sessionReplay.isTrackOrientationChange) { + try { + context.unregisterComponentCallbacks(this) + } catch (ignored: Throwable) { + } } stop() recorder?.close() @@ -279,7 +285,7 @@ public class ReplayIntegration( recorder?.stop() // refresh config based on new device configuration - recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay) + val recorderConfig = recorderConfigProvider?.invoke(true) ?: ScreenshotRecorderConfig.from(context, options.experimental.sessionReplay) captureStrategy?.onConfigurationChanged(recorderConfig) recorder?.start(recorderConfig) @@ -392,6 +398,7 @@ public class ReplayIntegration( height = lastSegment.recorderConfig.recordingHeight, width = lastSegment.recorderConfig.recordingWidth, frameRate = lastSegment.recorderConfig.frameRate, + bitRate = lastSegment.recorderConfig.bitRate, cache = lastSegment.cache, replayType = lastSegment.replayType, screenAtStart = lastSegment.screenAtStart, diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt index df0b7575a3..fbc80565b1 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt @@ -46,7 +46,7 @@ internal abstract class BaseCaptureStrategy( private val hub: IHub?, private val dateProvider: ICurrentDateProvider, protected val replayExecutor: ScheduledExecutorService, - private val replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null + private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null ) : CaptureStrategy { internal companion object { @@ -89,7 +89,7 @@ internal abstract class BaseCaptureStrategy( replayId: SentryId, replayType: ReplayType? ) { - cache = replayCacheProvider?.invoke(replayId, recorderConfig) ?: ReplayCache(options, replayId, recorderConfig) + cache = replayCacheProvider?.invoke(replayId) ?: ReplayCache(options, replayId) this.currentReplayId = replayId this.currentSegment = segmentId @@ -124,6 +124,7 @@ internal abstract class BaseCaptureStrategy( replayType: ReplayType = this.replayType, cache: ReplayCache? = this.cache, frameRate: Int = recorderConfig.frameRate, + bitRate: Int = recorderConfig.bitRate, screenAtStart: String? = this.screenAtStart, breadcrumbs: List? = null, events: Deque = this.currentEvents @@ -140,6 +141,7 @@ internal abstract class BaseCaptureStrategy( replayType, cache, frameRate, + bitRate, screenAtStart, breadcrumbs, events 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 0418283dda..8ef346f8a3 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 @@ -31,7 +31,7 @@ internal class BufferCaptureStrategy( private val dateProvider: ICurrentDateProvider, private val random: Random, executor: ScheduledExecutorService, - replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null + replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null ) : BaseCaptureStrategy(options, hub, dateProvider, executor, replayCacheProvider = replayCacheProvider) { // TODO: capture envelopes for buffered segments instead, but don't send them until buffer is triggered diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt index f4e6215710..2f7a5bef14 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt @@ -69,6 +69,7 @@ internal interface CaptureStrategy { replayType: ReplayType, cache: ReplayCache?, frameRate: Int, + bitRate: Int, screenAtStart: String?, breadcrumbs: List?, events: Deque @@ -78,7 +79,9 @@ internal interface CaptureStrategy { currentSegmentTimestamp.time, segmentId, height, - width + width, + frameRate, + bitRate ) ?: return ReplaySegment.Failed val (video, frameCount, videoDuration) = generatedVideo diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt index 1a6dbc8c89..a8c8f1387c 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt @@ -21,7 +21,7 @@ internal class SessionCaptureStrategy( private val hub: IHub?, private val dateProvider: ICurrentDateProvider, executor: ScheduledExecutorService, - replayCacheProvider: ((replayId: SentryId, recorderConfig: ScreenshotRecorderConfig) -> ReplayCache)? = null + replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null ) : BaseCaptureStrategy(options, hub, dateProvider, executor, replayCacheProvider) { internal companion object { 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 91a17f5192..c7529ac1f6 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 @@ -47,14 +47,12 @@ class ReplayCacheTest { val options = SentryOptions() fun getSut( dir: TemporaryFolder?, - replayId: SentryId = SentryId(), - frameRate: Int + replayId: SentryId = SentryId() ): ReplayCache { - val recorderConfig = ScreenshotRecorderConfig(100, 200, 1f, 1f, frameRate = frameRate, bitRate = 20_000) options.run { cacheDirPath = dir?.newFolder()?.absolutePath } - return ReplayCache(options, replayId, recorderConfig) + return ReplayCache(options, replayId) } } @@ -70,8 +68,7 @@ class ReplayCacheTest { val replayId = SentryId() val replayCache = fixture.getSut( null, - replayId, - frameRate = 1 + replayId ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) @@ -85,8 +82,7 @@ class ReplayCacheTest { val replayId = SentryId() val replayCache = fixture.getSut( tmpDir, - replayId, - frameRate = 1 + replayId ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) @@ -101,11 +97,10 @@ class ReplayCacheTest { @Test fun `when no frames are provided, returns nothing`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) - val video = replayCache.createVideoOf(5000L, 0, 0, 100, 200) + val video = replayCache.createVideoOf(5000L, 0, 0, 100, 200, 1, 20_000) assertNull(video) } @@ -114,8 +109,7 @@ class ReplayCacheTest { fun `deletes frames after creating a video`() { ReplayShadowMediaCodec.framesToEncode = 3 val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) @@ -123,7 +117,7 @@ class ReplayCacheTest { replayCache.addFrame(bitmap, 1001) replayCache.addFrame(bitmap, 2001) - val segment0 = replayCache.createVideoOf(3000L, 0, 0, 100, 200) + val segment0 = replayCache.createVideoOf(3000L, 0, 0, 100, 200, 1, 20_000) assertEquals(3, segment0!!.frameCount) assertEquals(3000, segment0.duration) assertTrue { segment0.video.exists() && segment0.video.length() > 0 } @@ -136,14 +130,13 @@ class ReplayCacheTest { @Test fun `repeats last known frame for the segment duration`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) replayCache.addFrame(bitmap, 1) - val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200) + val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200, 1, 20_000) assertEquals(5, segment0!!.frameCount) assertEquals(5000, segment0.duration) assertTrue { segment0.video.exists() && segment0.video.length() > 0 } @@ -153,15 +146,14 @@ class ReplayCacheTest { @Test fun `repeats last known frame for the segment duration for each timespan`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) replayCache.addFrame(bitmap, 1) replayCache.addFrame(bitmap, 3001) - val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200) + val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200, 1, 20_000) assertEquals(5, segment0!!.frameCount) assertEquals(5000, segment0.duration) assertTrue { segment0.video.exists() && segment0.video.length() > 0 } @@ -171,20 +163,19 @@ class ReplayCacheTest { @Test fun `repeats last known frame for each segment`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) replayCache.addFrame(bitmap, 1) replayCache.addFrame(bitmap, 5001) - val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200) + val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200, 1, 20_000) assertEquals(5, segment0!!.frameCount) assertEquals(5000, segment0.duration) assertEquals(File(replayCache.replayCacheDir, "0.mp4"), segment0.video) - val segment1 = replayCache.createVideoOf(5000L, 5000L, 1, 100, 200) + val segment1 = replayCache.createVideoOf(5000L, 5000L, 1, 100, 200, 1, 20_000) assertEquals(5, segment1!!.frameCount) assertEquals(5000, segment1.duration) assertTrue { segment0.video.exists() && segment0.video.length() > 0 } @@ -196,8 +187,7 @@ class ReplayCacheTest { ReplayShadowMediaCodec.framesToEncode = 6 val replayCache = fixture.getSut( - tmpDir, - frameRate = 2 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) @@ -205,7 +195,7 @@ class ReplayCacheTest { replayCache.addFrame(bitmap, 1001) replayCache.addFrame(bitmap, 1501) - val segment0 = replayCache.createVideoOf(3000L, 0, 0, 100, 200) + val segment0 = replayCache.createVideoOf(3000L, 0, 0, 100, 200, 2, 20_000) assertEquals(6, segment0!!.frameCount) assertEquals(3000, segment0.duration) assertTrue { segment0.video.exists() && segment0.video.length() > 0 } @@ -215,8 +205,7 @@ class ReplayCacheTest { @Test fun `does not add frame when bitmap is recycled`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888).also { it.recycle() } @@ -228,8 +217,7 @@ class ReplayCacheTest { @Test fun `addFrame with File path works`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val flutterCacheDir = @@ -240,7 +228,7 @@ class ReplayCacheTest { val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888).also { it.recycle() } replayCache.addFrame(screenshot, frameTimestamp = 1) - val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200, videoFile = video) + val segment0 = replayCache.createVideoOf(5000L, 0, 0, 100, 200, 1, 20_000, videoFile = video) assertEquals(5, segment0!!.frameCount) assertEquals(5000, segment0.duration) @@ -251,8 +239,7 @@ class ReplayCacheTest { @Test fun `rotates frames`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) @@ -269,8 +256,7 @@ class ReplayCacheTest { @Test fun `rotate returns first screen in buffer`() { val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val bitmap = Bitmap.createBitmap(1, 1, ARGB_8888) @@ -288,8 +274,7 @@ class ReplayCacheTest { val replayId = SentryId() val replayCache = fixture.getSut( tmpDir, - replayId, - frameRate = 1 + replayId ) replayCache.close() @@ -303,8 +288,7 @@ class ReplayCacheTest { val replayId = SentryId() val replayCache = fixture.getSut( tmpDir, - replayId, - frameRate = 1 + replayId ) replayCache.persistSegmentValues("key1", "value1") @@ -320,8 +304,7 @@ class ReplayCacheTest { val replayId = SentryId() val replayCache = fixture.getSut( tmpDir, - replayId, - frameRate = 1 + replayId ) replayCache.persistSegmentValues("key1", "value1") @@ -467,8 +450,7 @@ class ReplayCacheTest { ReplayShadowMediaCodec.framesToEncode = 3 val replayCache = fixture.getSut( - tmpDir, - frameRate = 1 + tmpDir ) val oldVideoFile = File(replayCache.replayCacheDir, "0.mp4").also { @@ -480,7 +462,7 @@ class ReplayCacheTest { replayCache.addFrame(bitmap, 1001) replayCache.addFrame(bitmap, 2001) - val segment0 = replayCache.createVideoOf(3000L, 0, 0, 100, 200, oldVideoFile) + val segment0 = replayCache.createVideoOf(3000L, 0, 0, 100, 200, 1, 20_000, oldVideoFile) assertEquals(3, segment0!!.frameCount) assertEquals(3000, segment0.duration) assertTrue { segment0.video.exists() && segment0.video.length() > 0 } diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt index 95380deaa7..f375136149 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt @@ -96,7 +96,7 @@ class ReplayIntegrationTest { val replayCache = mock { on { frames }.thenReturn(mutableListOf(ReplayFrame(File("1720693523997.jpg"), 1720693523997))) - on { createVideoOf(anyLong(), anyLong(), anyInt(), anyInt(), anyInt(), any()) } + on { createVideoOf(anyLong(), anyLong(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any()) } .thenReturn(GeneratedVideo(File("0.mp4"), 5, VIDEO_DURATION)) } @@ -127,7 +127,7 @@ class ReplayIntegrationTest { dateProvider, recorderProvider, recorderConfigProvider = recorderConfigProvider, - replayCacheProvider = { _, _ -> replayCache }, + replayCacheProvider = { _ -> replayCache }, replayCaptureStrategyProvider = replayCaptureStrategyProvider, gestureRecorderProvider = gestureRecorderProvider ) 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 625306cb8e..1fdb41386a 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 @@ -70,7 +70,7 @@ class BufferCaptureStrategyTest { on { persistSegmentValues(any(), anyOrNull()) }.then { persistedSegment.put(it.arguments[0].toString(), it.arguments[1]?.toString()) } - on { createVideoOf(anyLong(), anyLong(), anyInt(), anyInt(), anyInt(), any()) } + on { createVideoOf(anyLong(), anyLong(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any()) } .thenReturn(GeneratedVideo(File("0.mp4"), 5, VIDEO_DURATION)) } val recorderConfig = ScreenshotRecorderConfig( @@ -104,7 +104,7 @@ class BufferCaptureStrategyTest { null }.whenever(it).submit(any()) } - ) { _, _ -> replayCache } + ) { _ -> replayCache } } fun mockedMotionEvent(action: Int): MotionEvent = mock { diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt index 12eb10c3f4..50adeae3a1 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt @@ -76,7 +76,7 @@ class SessionCaptureStrategyTest { on { persistSegmentValues(any(), anyOrNull()) }.then { persistedSegment.put(it.arguments[0].toString(), it.arguments[1]?.toString()) } - on { createVideoOf(anyLong(), anyLong(), anyInt(), anyInt(), anyInt(), any()) } + on { createVideoOf(anyLong(), anyLong(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any()) } .thenReturn(GeneratedVideo(File("0.mp4"), 5, VIDEO_DURATION)) } val recorderConfig = ScreenshotRecorderConfig( @@ -105,7 +105,7 @@ class SessionCaptureStrategyTest { null }.whenever(it).submit(any()) } - ) { _, _ -> replayCache } + ) { _ -> replayCache } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index fc2f4ba51e..ef922d4f59 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2741,12 +2741,14 @@ public final class io/sentry/SentryReplayOptions { public fun getUnmaskViewContainerClass ()Ljava/lang/String; public fun isSessionReplayEnabled ()Z public fun isSessionReplayForErrorsEnabled ()Z + public fun isTrackOrientationChange ()Z public fun setMaskAllImages (Z)V public fun setMaskAllText (Z)V public fun setMaskViewContainerClass (Ljava/lang/String;)V public fun setOnErrorSampleRate (Ljava/lang/Double;)V public fun setQuality (Lio/sentry/SentryReplayOptions$SentryReplayQuality;)V public fun setSessionSampleRate (Ljava/lang/Double;)V + public fun setTrackOrientationChange (Z)V public fun setUnmaskViewContainerClass (Ljava/lang/String;)V } diff --git a/sentry/src/main/java/io/sentry/SentryReplayOptions.java b/sentry/src/main/java/io/sentry/SentryReplayOptions.java index fd492213ac..f9e82c8600 100644 --- a/sentry/src/main/java/io/sentry/SentryReplayOptions.java +++ b/sentry/src/main/java/io/sentry/SentryReplayOptions.java @@ -108,6 +108,12 @@ public enum SentryReplayQuality { /** The maximum duration of a full session replay, defaults to 1h. */ private long sessionDuration = 60 * 60 * 1000L; + /** + * Whether to track orientation changes in session replay. Used in Flutter as it has its own + * callbacks to determine the orientation change. + */ + private boolean trackOrientationChange = true; + public SentryReplayOptions(final boolean empty) { if (!empty) { setMaskAllText(true); @@ -266,4 +272,14 @@ public void setUnmaskViewContainerClass(@NotNull String containerClass) { public @Nullable String getUnmaskViewContainerClass() { return unmaskViewContainerClass; } + + @ApiStatus.Internal + public boolean isTrackOrientationChange() { + return trackOrientationChange; + } + + @ApiStatus.Internal + public void setTrackOrientationChange(final boolean trackOrientationChange) { + this.trackOrientationChange = trackOrientationChange; + } }