diff --git a/CHANGELOG.md b/CHANGELOG.md index 11cdde82ea..daf225cb7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ ### Dependencies +- Bump Android SDK from v8.14.0 to v8.16.0 ([#2977](https://github.com/getsentry/sentry-dart/pull/2977)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8132) + - [diff](https://github.com/getsentry/sentry-java/compare/8.13.2...8.16.0) - Bump Native SDK from v0.9.0 to v0.9.1 ([#3018](https://github.com/getsentry/sentry-dart/pull/3018)) - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#091) - [diff](https://github.com/getsentry/sentry-native/compare/0.9.0...0.9.1) diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index 8c5b6a6cdf..3d53f4395f 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -60,7 +60,7 @@ android { } dependencies { - api 'io.sentry:sentry-android:8.13.2' + api 'io.sentry:sentry-android:8.16.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" // Required -- JUnit 4 framework diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt index 47faae28cd..18ee9a8137 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutter.kt @@ -201,10 +201,10 @@ class SentryFlutter { replayOptions.sessionSampleRate = (data["sessionSampleRate"] as? Number)?.toDouble() replayOptions.onErrorSampleRate = (data["onErrorSampleRate"] as? Number)?.toDouble() - // Disable native tracking of orientation change (causes replay restart) + // Disable native tracking of window sizes // because we don't have the new size from Flutter yet. Instead, we'll // trigger onConfigurationChanged() manually in setReplayConfig(). - replayOptions.setTrackOrientationChange(false) + replayOptions.isTrackConfiguration = false @Suppress("UNCHECKED_CAST") val tags = (data["tags"] as? Map) ?: mapOf() diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt index c3e7c74c9c..8fd189bc5b 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterPlugin.kt @@ -3,15 +3,9 @@ package io.sentry.flutter import android.annotation.SuppressLint import android.app.Activity import android.content.Context -import android.content.res.Configuration -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.os.Looper import android.util.Log -import android.view.WindowManager import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -49,18 +43,6 @@ class SentryFlutterPlugin : private lateinit var context: Context private lateinit var sentryFlutter: SentryFlutter - // Note: initial config because we don't yet have the numbers of the actual Flutter widget. - // See how SentryFlutterReplayRecorder.start() handles it. New settings will be set by setReplayConfig() method below. - private var replayConfig = - ScreenshotRecorderConfig( - recordingWidth = VIDEO_BLOCK_SIZE, - recordingHeight = VIDEO_BLOCK_SIZE, - scaleFactorX = 1.0f, - scaleFactorY = 1.0f, - frameRate = 1, - bitRate = 75000, - ) - private var activity: WeakReference? = null private var pluginRegistrationTime: Long? = null @@ -160,18 +142,6 @@ class SentryFlutterPlugin : context.applicationContext, dateProvider = CurrentDateProvider.getInstance(), recorderProvider = { SentryFlutterReplayRecorder(channel, replay!!) }, - recorderConfigProvider = { - Log.i( - "Sentry", - "Replay configuration requested. Returning: %dx%d at %d FPS, %d BPS".format( - replayConfig.recordingWidth, - replayConfig.recordingHeight, - replayConfig.frameRate, - replayConfig.bitRate, - ), - ) - replayConfig - }, replayCacheProvider = null, ) replay!!.breadcrumbConverter = SentryFlutterReplayBreadcrumbConverter() @@ -473,7 +443,8 @@ class SentryFlutterPlugin : private const val NATIVE_CRASH_WAIT_TIME = 500L - @JvmStatic fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay + @JvmStatic + fun privateSentryGetReplayIntegration(): ReplayIntegration? = replay private fun crash() { val exception = RuntimeException("FlutterSentry Native Integration: Sample RuntimeException") @@ -515,8 +486,21 @@ class SentryFlutterPlugin : // Since codec block size is 16, so we have to adjust the width and height to it, // otherwise the codec might fail to configure on some devices, see // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/media/java/android/media/MediaCodecInfo.java;l=1999-2001 + val windowWidth = call.argument("windowWidth") as? Double ?: 0.0 + val windowHeight = call.argument("windowHeight") as? Double ?: 0.0 + var width = call.argument("width") as? Double ?: 0.0 var height = call.argument("height") as? Double ?: 0.0 + + if (width == 0.0 || height == 0.0 || windowWidth == 0.0 || windowHeight == 0.0) { + result.error( + "5", + "Replay config is not valid: width: $width, height: $height, windowWidth: $windowWidth, windowHeight: $windowHeight", + null, + ) + return + } + // First update the smaller dimension, as changing that will affect the screen ratio more. if (width < height) { val newWidth = width.adjustReplaySizeToBlockSize() @@ -528,23 +512,12 @@ class SentryFlutterPlugin : height = newHeight } - 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) - } - - replayConfig = + val replayConfig = ScreenshotRecorderConfig( recordingWidth = width.roundToInt(), recordingHeight = height.roundToInt(), - scaleFactorX = width.toFloat() / screenBounds.width().toFloat(), - scaleFactorY = height.toFloat() / screenBounds.height().toFloat(), + scaleFactorX = width.toFloat() / windowWidth.toFloat(), + scaleFactorY = height.toFloat() / windowHeight.toFloat(), frameRate = call.argument("frameRate") as? Int ?: 0, bitRate = call.argument("bitRate") as? Int ?: 0, ) @@ -557,7 +530,7 @@ class SentryFlutterPlugin : replayConfig.bitRate, ), ) - replay!!.onConfigurationChanged(Configuration()) + replay?.onConfigurationChanged(replayConfig) result.success("") } diff --git a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt index fb86ed4440..a7897c002b 100644 --- a/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt +++ b/flutter/android/src/main/kotlin/io/sentry/flutter/SentryFlutterReplayRecorder.kt @@ -12,21 +12,12 @@ internal class SentryFlutterReplayRecorder( private val channel: MethodChannel, private val integration: ReplayIntegration, ) : Recorder { - override fun start(recorderConfig: ScreenshotRecorderConfig) { - // Ignore if this is the initial call before we actually got the configuration from Flutter. - // We'll get another call here when the configuration is changed according to the widget size. - if (recorderConfig.recordingHeight <= VIDEO_BLOCK_SIZE && recorderConfig.recordingWidth <= VIDEO_BLOCK_SIZE) { - return - } - + override fun start() { Handler(Looper.getMainLooper()).post { try { channel.invokeMethod( "ReplayRecorder.start", mapOf( - "width" to recorderConfig.recordingWidth, - "height" to recorderConfig.recordingHeight, - "frameRate" to recorderConfig.frameRate, "replayId" to integration.getReplayId().toString(), ), ) @@ -46,6 +37,33 @@ internal class SentryFlutterReplayRecorder( } } + override fun onConfigurationChanged(config: ScreenshotRecorderConfig) { + Handler(Looper.getMainLooper()).post { + try { + channel.invokeMethod( + "ReplayRecorder.onConfigurationChanged", + mapOf( + "width" to config.recordingWidth, + "height" to config.recordingHeight, + "frameRate" to config.frameRate, + ), + ) + } catch (ignored: Exception) { + Log.w("Sentry", "Failed to propagate configuration change to Flutter", ignored) + } + } + } + + override fun reset() { + Handler(Looper.getMainLooper()).post { + try { + channel.invokeMethod("ReplayRecorder.reset", null) + } catch (ignored: Exception) { + Log.w("Sentry", "Failed to reset replay recorder", ignored) + } + } + } + override fun pause() { Handler(Looper.getMainLooper()).post { try { diff --git a/flutter/lib/src/event_processor/screenshot_event_processor.dart b/flutter/lib/src/event_processor/screenshot_event_processor.dart index 34104d88be..ece03dbb37 100644 --- a/flutter/lib/src/event_processor/screenshot_event_processor.dart +++ b/flutter/lib/src/event_processor/screenshot_event_processor.dart @@ -17,13 +17,11 @@ class ScreenshotEventProcessor implements EventProcessor { ScreenshotEventProcessor(this._options) { final targetResolution = _options.screenshotQuality.targetResolution(); - _recorder = ScreenshotRecorder( - ScreenshotRecorderConfig( - width: targetResolution, - height: targetResolution, - ), - _options, - ); + _recorder = ScreenshotRecorder(_options, + config: ScreenshotRecorderConfig( + width: targetResolution, + height: targetResolution, + )); _debouncer = Debouncer( // ignore: invalid_use_of_internal_member _options.clock, diff --git a/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart b/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart index d822f6beb5..11da563816 100644 --- a/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart +++ b/flutter/lib/src/native/cocoa/cocoa_replay_recorder.dart @@ -15,11 +15,12 @@ class CocoaReplayRecorder { CocoaReplayRecorder(this._options) : _recorder = ReplayScreenshotRecorder( - ScreenshotRecorderConfig( - pixelRatio: _options.replay.quality.resolutionScalingFactor, - ), _options, - ); + ) { + _recorder.config = ScreenshotRecorderConfig( + pixelRatio: _options.replay.quality.resolutionScalingFactor, + ); + } Future?> captureScreenshot() async { return _recorder.capture((screenshot) async { diff --git a/flutter/lib/src/native/java/android_replay_recorder.dart b/flutter/lib/src/native/java/android_replay_recorder.dart index 4a9253e83c..82ed267f3d 100644 --- a/flutter/lib/src/native/java/android_replay_recorder.dart +++ b/flutter/lib/src/native/java/android_replay_recorder.dart @@ -7,7 +7,6 @@ import 'package:meta/meta.dart'; import '../../../sentry_flutter.dart'; import '../../replay/scheduled_recorder.dart'; -import '../../replay/scheduled_recorder_config.dart'; import '../../screenshot/screenshot.dart'; import 'binding.dart' as native; @@ -18,11 +17,10 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder { _AndroidNativeReplayWorker? _worker; @internal // visible for testing, used by SentryNativeJava - static AndroidReplayRecorder Function( - ScheduledScreenshotRecorderConfig, SentryFlutterOptions) factory = + static AndroidReplayRecorder Function(SentryFlutterOptions) factory = AndroidReplayRecorder.new; - AndroidReplayRecorder(super.config, super.options) { + AndroidReplayRecorder(super.options) { super.callback = _addReplayScreenshot; } diff --git a/flutter/lib/src/native/java/sentry_native_java.dart b/flutter/lib/src/native/java/sentry_native_java.dart index 7ceac09edc..9301dedb37 100644 --- a/flutter/lib/src/native/java/sentry_native_java.dart +++ b/flutter/lib/src/native/java/sentry_native_java.dart @@ -24,19 +24,20 @@ class SentryNativeJava extends SentryNativeChannel { final replayId = SentryId.fromId(call.arguments['replayId'] as String); - final config = ScheduledScreenshotRecorderConfig( - width: (call.arguments['width'] as num).toDouble(), - height: (call.arguments['height'] as num).toDouble(), - frameRate: call.arguments['frameRate'] as int); - - _replayRecorder = AndroidReplayRecorder.factory(config, options); + _replayRecorder = AndroidReplayRecorder.factory(options); await _replayRecorder!.start(); - hub.configureScope((s) { // ignore: invalid_use_of_internal_member s.replayId = replayId; }); + break; + case 'ReplayRecorder.onConfigurationChanged': + final config = ScheduledScreenshotRecorderConfig( + width: (call.arguments['width'] as num).toDouble(), + height: (call.arguments['height'] as num).toDouble(), + frameRate: call.arguments['frameRate'] as int); + _replayRecorder?.onConfigurationChanged(config); break; case 'ReplayRecorder.stop': hub.configureScope((s) { @@ -55,6 +56,9 @@ class SentryNativeJava extends SentryNativeChannel { case 'ReplayRecorder.resume': await _replayRecorder?.resume(); break; + case 'ReplayRecorder.reset': + // ignored + break; default: throw UnimplementedError('Method ${call.method} not implemented'); } diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index e17f1d416c..7e4450c547 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -252,6 +252,8 @@ class SentryNativeChannel @override FutureOr setReplayConfig(ReplayConfig config) => channel.invokeMethod('setReplayConfig', { + 'windowWidth': config.windowWidth, + 'windowHeight': config.windowHeight, 'width': config.width, 'height': config.height, 'frameRate': config.frameRate, diff --git a/flutter/lib/src/replay/integration.dart b/flutter/lib/src/replay/integration.dart index 5ec5d9caa6..02430fccb4 100644 --- a/flutter/lib/src/replay/integration.dart +++ b/flutter/lib/src/replay/integration.dart @@ -35,6 +35,8 @@ class ReplayIntegration extends Integration { SentryScreenshotWidget.onBuild((status, prevStatus) { if (status != prevStatus) { _native.setReplayConfig(ReplayConfig( + windowWidth: status.size?.width ?? 0.0, + windowHeight: status.size?.height ?? 0.0, width: replayOptions.quality.resolutionScalingFactor * (status.size?.width ?? 0.0), height: replayOptions.quality.resolutionScalingFactor * diff --git a/flutter/lib/src/replay/replay_config.dart b/flutter/lib/src/replay/replay_config.dart index fb9233d7e1..0929e68d91 100644 --- a/flutter/lib/src/replay/replay_config.dart +++ b/flutter/lib/src/replay/replay_config.dart @@ -5,6 +5,10 @@ import 'scheduled_recorder_config.dart'; @immutable @internal class ReplayConfig extends ScheduledScreenshotRecorderConfig { + final double windowWidth; + + final double windowHeight; + @override double get width => super.width!; @@ -12,6 +16,8 @@ class ReplayConfig extends ScheduledScreenshotRecorderConfig { double get height => super.height!; const ReplayConfig({ + required this.windowWidth, + required this.windowHeight, required double super.width, required double super.height, super.frameRate = 1, diff --git a/flutter/lib/src/replay/replay_recorder.dart b/flutter/lib/src/replay/replay_recorder.dart index 0deeda52bd..75e704e9c3 100644 --- a/flutter/lib/src/replay/replay_recorder.dart +++ b/flutter/lib/src/replay/replay_recorder.dart @@ -9,7 +9,7 @@ var _instanceCounter = 0; @internal class ReplayScreenshotRecorder extends ScreenshotRecorder { - ReplayScreenshotRecorder(super.config, super.options) + ReplayScreenshotRecorder(super.options) : super( privacyOptions: options.privacy, logName: 'ReplayRecorder #${++_instanceCounter}'); diff --git a/flutter/lib/src/replay/scheduled_recorder.dart b/flutter/lib/src/replay/scheduled_recorder.dart index 4aeccf7d07..26a0467c8f 100644 --- a/flutter/lib/src/replay/scheduled_recorder.dart +++ b/flutter/lib/src/replay/scheduled_recorder.dart @@ -15,10 +15,9 @@ typedef ScheduledScreenshotRecorderCallback = Future Function( @internal class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { - late final Scheduler _scheduler; late final ScheduledScreenshotRecorderCallback _callback; - var _status = _Status.running; - late final Duration _frameDuration; + var _status = _Status.stopped; + Scheduler? _scheduler; // late final _idleFrameFiller = _IdleFrameFiller(_frameDuration, _onScreenshot); @override @@ -26,20 +25,8 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { ScheduledScreenshotRecorderConfig get config => super.config as ScheduledScreenshotRecorderConfig; - ScheduledScreenshotRecorder( - ScheduledScreenshotRecorderConfig config, SentryFlutterOptions options, - [ScheduledScreenshotRecorderCallback? callback]) - : super(config, options) { - assert(config.frameRate > 0); - _frameDuration = Duration(milliseconds: 1000 ~/ config.frameRate); - assert(_frameDuration.inMicroseconds > 0); - - _scheduler = Scheduler( - _frameDuration, - (_) => capture(_onImageCaptured), - _addPostFrameCallback, - ); - + ScheduledScreenshotRecorder(super.options, + [ScheduledScreenshotRecorderCallback? callback]) { if (callback != null) { _callback = callback; } @@ -55,7 +42,7 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { _callback = callback; } - void start() { + void start() async { assert(() { // The following fails if callback hasn't been provided // in the constructor nor set with a setter. @@ -63,24 +50,49 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { return true; }()); - options.log(SentryLevel.debug, - "$logName: starting capture (${config.width}x${config.height} @ ${config.frameRate} Hz)."); + options.log(SentryLevel.debug, "$logName: starting capture"); _status = _Status.running; - _startScheduler(); + _restartScheduler(); + } + + void onConfigurationChanged(ScheduledScreenshotRecorderConfig config) async { + super.config = config; + options.log(SentryLevel.debug, + "$logName: onConfigurationChanged (${config.width}x${config.height} @ ${config.frameRate} Hz)."); + + _restartScheduler(); } Future _stopScheduler() { - return _scheduler.stop(); + return _scheduler?.stop() ?? Future.value(); } - void _startScheduler() { - _scheduler.start(); + void _restartScheduler() async { + await _stopScheduler(); - // We need to schedule a frame because if this happens in-between user - // actions, there may not be any frame captured for a long time so even - // the IdleFrameFiller won't have anything to repeat. This would appear - // as if the replay was broken. - options.bindingUtils.instance!.ensureVisualUpdate(); + if (super.config == null) { + return; + } + + assert(config.frameRate > 0); + var frameDuration = Duration(milliseconds: 1000 ~/ config.frameRate); + assert(frameDuration.inMicroseconds > 0); + + _scheduler = Scheduler( + frameDuration, + (_) => capture(_onImageCaptured), + _addPostFrameCallback, + ); + + if (_status == _Status.running) { + _scheduler!.start(); + + // We need to schedule a frame because if this happens in-between user + // actions, there may not be any frame captured for a long time so even + // the IdleFrameFiller won't have anything to repeat. This would appear + // as if the replay was broken. + options.bindingUtils.instance!.ensureVisualUpdate(); + } } Future stop() async { @@ -102,7 +114,7 @@ class ScheduledScreenshotRecorder extends ReplayScreenshotRecorder { Future resume() async { if (_status == _Status.paused) { _status = _Status.running; - _startScheduler(); + _restartScheduler(); // _idleFrameFiller.resume(); } } diff --git a/flutter/lib/src/screenshot/recorder.dart b/flutter/lib/src/screenshot/recorder.dart index 2ae1ae28aa..4b0d466d24 100644 --- a/flutter/lib/src/screenshot/recorder.dart +++ b/flutter/lib/src/screenshot/recorder.dart @@ -16,22 +16,19 @@ import 'widget_filter.dart'; @internal class ScreenshotRecorder { - @protected - final ScreenshotRecorderConfig config; - @protected final SentryFlutterOptions options; + ScreenshotRecorderConfig? config; + final String logName; bool _warningLogged = false; late final SentryMaskingConfig? _maskingConfig; - ScreenshotRecorder( - this.config, - this.options, { - SentryPrivacyOptions? privacyOptions, - this.logName = 'ScreenshotRecorder', - }) { + ScreenshotRecorder(this.options, + {SentryPrivacyOptions? privacyOptions, + this.logName = 'ScreenshotRecorder', + this.config}) { privacyOptions ??= options.privacy; final maskingConfig = @@ -71,7 +68,13 @@ class ScreenshotRecorder { return Future.value(null); } - final capture = _Capture.create(renderObject, config, context); + if (config == null) { + _log(SentryLevel.warning, + "Capture config is not set, skipping capture."); + return Future.value(null); + } + + final capture = _Capture.create(renderObject, config!, context); Timeline.startSync('Sentry::captureScreenshot:RenderObjectToImage', flow: flow); diff --git a/flutter/test/replay/android_replay_recorder_web.dart b/flutter/test/replay/android_replay_recorder_web.dart index ab8bc199bb..add5eaae30 100644 --- a/flutter/test/replay/android_replay_recorder_web.dart +++ b/flutter/test/replay/android_replay_recorder_web.dart @@ -5,14 +5,12 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/replay/scheduled_recorder.dart'; -import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; class AndroidReplayRecorder extends ScheduledScreenshotRecorder { - static AndroidReplayRecorder Function( - ScheduledScreenshotRecorderConfig, SentryFlutterOptions) factory = + static AndroidReplayRecorder Function(SentryFlutterOptions) factory = AndroidReplayRecorder.new; - AndroidReplayRecorder(super.config, super.options); + AndroidReplayRecorder(super.options); @override Future start() async { diff --git a/flutter/test/replay/replay_native_test.dart b/flutter/test/replay/replay_native_test.dart index b57e767d87..418704fa81 100644 --- a/flutter/test/replay/replay_native_test.dart +++ b/flutter/test/replay/replay_native_test.dart @@ -36,7 +36,6 @@ void main() { late NativeChannelFixture native; late SentryFlutterOptions options; late MockHub hub; - late Map replayConfig; late _MockAndroidReplayRecorder mockAndroidRecorder; setUp(() { @@ -50,19 +49,9 @@ void main() { sut = createBinding(options); - if (mockPlatform.isIOS) { - replayConfig = { - 'replayId': '123', - }; - } else if (mockPlatform.isAndroid) { - replayConfig = { - 'replayId': '123', - 'width': 800, - 'height': 600, - 'frameRate': 1000, - }; - AndroidReplayRecorder.factory = (config, options) { - mockAndroidRecorder = _MockAndroidReplayRecorder(config, options); + if (mockPlatform.isAndroid) { + AndroidReplayRecorder.factory = (options) { + mockAndroidRecorder = _MockAndroidReplayRecorder(options); return mockAndroidRecorder; }; } @@ -79,13 +68,17 @@ void main() { await sut.init(hub); }); - testWidgets('sets replayID to context', (tester) async { + testWidgets( + 'sets replayID to context on ${mockPlatform.operatingSystem.name}', + (tester) async { await tester.runAsync(() async { await pumpTestElement(tester); // verify there was no scope configured before verifyNever(hub.configureScope(any)); when(hub.configureScope(captureAny)).thenReturn(null); + final replayConfig = {'replayId': '123'}; + // emulate the native platform invoking the method final future = native.invokeFromNative( mockPlatform.isAndroid @@ -106,15 +99,16 @@ void main() { if (mockPlatform.isAndroid) { await native.invokeFromNative('ReplayRecorder.stop'); AndroidReplayRecorder.factory = AndroidReplayRecorder.new; - - // Workaround for "A Timer is still pending even after the widget tree was disposed." - await tester.pumpWidget(Container()); - await tester.pumpAndSettle(); } + // Workaround for "A Timer is still pending even after the widget tree was disposed." + await tester.pumpWidget(Container()); + await tester.pumpAndSettle(); }); }); - test('clears replay ID from context', () async { + test( + 'clears replay ID from context on ${mockPlatform.operatingSystem.name}', + () async { // verify there was no scope configured before verifyNever(hub.configureScope(any)); when(hub.configureScope(captureAny)).thenReturn(null); @@ -132,7 +126,8 @@ void main() { expect(scope.replayId, isNull); }, skip: mockPlatform.isIOS ? 'iOS does not clear replay ID' : false); - testWidgets('captures images', (tester) async { + testWidgets('captures images on ${mockPlatform.operatingSystem.name}', + (tester) async { await tester.runAsync(() async { when(hub.configureScope(captureAny)).thenReturn(null); @@ -143,14 +138,23 @@ void main() { await tester.pumpAndWaitUntil(future, requiredToComplete: wait); } + final Map replayConfig = {'replayId': '123'}; + final configuration = { + 'width': 800, + 'height': 600, + 'frameRate': 1, + }; await native.invokeFromNative( 'ReplayRecorder.start', replayConfig); + await native.invokeFromNative( + 'ReplayRecorder.onConfigurationChanged', configuration); + await nextFrame(); expect(mockAndroidRecorder.captured, isNotEmpty); final screenshot = mockAndroidRecorder.captured.first; - expect(screenshot.width, replayConfig['width']); - expect(screenshot.height, replayConfig['height']); + expect(screenshot.width, configuration['width']); + expect(screenshot.height, configuration['height']); await native.invokeFromNative('ReplayRecorder.pause'); var count = mockAndroidRecorder.captured.length; @@ -173,6 +177,8 @@ void main() { await nextFrame(wait: false); expect(mockAndroidRecorder.captured.length, equals(count)); } else if (mockPlatform.isIOS) { + final Map replayConfig = {'replayId': '123'}; + Future captureAndVerify() async { final future = native.invokeFromNative( 'captureReplayScreenshot', replayConfig); @@ -207,7 +213,7 @@ class _MockAndroidReplayRecorder extends ScheduledScreenshotRecorder final captured = []; var completer = Completer(); - _MockAndroidReplayRecorder(super.config, super.options) { + _MockAndroidReplayRecorder(super.options) { super.callback = (screenshot, _) async { captured.add(screenshot); completer.complete(); diff --git a/flutter/test/replay/scheduled_recorder_test.dart b/flutter/test/replay/scheduled_recorder_test.dart index f2e672a208..a14945fe76 100644 --- a/flutter/test/replay/scheduled_recorder_test.dart +++ b/flutter/test/replay/scheduled_recorder_test.dart @@ -42,17 +42,18 @@ class _Fixture { _Fixture._(this._tester) { _sut = ScheduledScreenshotRecorder( - ScheduledScreenshotRecorderConfig( - width: 1000, - height: 1000, - frameRate: 1000, - ), defaultTestOptions()..bindingUtils = TestBindingWrapper(), (image, isNewlyCaptured) async { capturedImages.add('${image.width}x${image.height}'); _completer.complete(); }, ); + + _sut.onConfigurationChanged(ScheduledScreenshotRecorderConfig( + width: 1000, + height: 1000, + frameRate: 1000, + )); } static Future<_Fixture> create(WidgetTester tester) async { diff --git a/flutter/test/screenshot/recorder_test.dart b/flutter/test/screenshot/recorder_test.dart index b89869cd6f..074664097d 100644 --- a/flutter/test/screenshot/recorder_test.dart +++ b/flutter/test/screenshot/recorder_test.dart @@ -173,10 +173,12 @@ void main() async { } class _Fixture { - late final ScreenshotRecorder sut = ScreenshotRecorder( - ScreenshotRecorderConfig(width: width, height: height), options); + late final ScreenshotRecorder sut = + ScreenshotRecorder(options, config: recorderConfig); late final options = defaultTestOptions() ..bindingUtils = TestBindingWrapper(); + late final recorderConfig = + ScreenshotRecorderConfig(width: width, height: height); final double? width; final double? height; diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index e742950893..86985ecfb7 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -323,11 +323,18 @@ void main() { when(channel.invokeMethod('setReplayConfig', any)) .thenAnswer((_) => Future.value()); - final config = ReplayConfig(width: 1.1, height: 2.2, frameRate: 3); + final config = ReplayConfig( + windowWidth: 110, + windowHeight: 220, + width: 1.1, + height: 2.2, + frameRate: 3); await sut.setReplayConfig(config); if (mockPlatform.isAndroid) { verify(channel.invokeMethod('setReplayConfig', { + 'windowWidth': config.windowWidth, + 'windowHeight': config.windowHeight, 'width': config.width, 'height': config.height, 'frameRate': config.frameRate,