-
-
Notifications
You must be signed in to change notification settings - Fork 276
enh: refactor captureReplay and setReplayConfig to use FFI/JNI
#3318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
d79f8f9
Add support for captureReplay
buenaflor aadb6b9
Remove method channel methods
buenaflor 74346c0
Update
buenaflor 6764e1c
Update
buenaflor d1b64ef
Update
buenaflor 55603c8
Add test for adjustReplaySizeToBlockSize
buenaflor 8f1b69e
Update
buenaflor b27d058
Add comment
buenaflor 7137617
Update tests
buenaflor 9da5e55
Clean up
buenaflor f572597
Update tests
buenaflor c41e9d1
Update tests
buenaflor 0d578c9
Update tests
buenaflor 244c51c
Update tests
buenaflor 247313b
Update tests
buenaflor 20b100b
Update tests
buenaflor 336e06c
release: 9.8.0
getsentry-bot f196368
Replace Android emulator test step with unit test (#3319)
buenaflor 1753710
build(deps): bump actions/upload-artifact from 4 to 5 (#3315)
dependabot[bot] 0248c07
build(deps): bump ruby/setup-ruby from 1.263.0 to 1.267.0 (#3316)
dependabot[bot] e5f85f1
Update CHANGELOG
buenaflor 34c3af6
Merge branch 'main' into enh/ffi-jni-replay-config
buenaflor daacf58
Use round() instead of toInt()
buenaflor 54a4ef1
Fix CHANGELOG
buenaflor e8ce749
Update
buenaflor b18a997
Bug bot reviews
buenaflor 84910f6
Add test
buenaflor 5891010
Fix test
buenaflor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
228 changes: 203 additions & 25 deletions
228
packages/flutter/example/integration_test/replay_test.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,217 @@ | ||
| // ignore_for_file: invalid_use_of_internal_member | ||
|
|
||
| import 'dart:async'; | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:flutter/widgets.dart'; | ||
| import 'package:flutter_test/flutter_test.dart'; | ||
| import 'package:integration_test/integration_test.dart'; | ||
| import 'package:sentry_flutter/sentry_flutter.dart'; | ||
| import 'package:sentry_flutter_example/main.dart'; | ||
| import 'package:sentry_flutter/src/native/java/sentry_native_java.dart'; | ||
| import 'package:sentry_flutter/src/replay/replay_config.dart'; | ||
| import 'package:sentry_flutter/src/native/cocoa/sentry_native_cocoa.dart'; | ||
| import 'package:sentry_flutter/src/replay/scheduled_recorder_config.dart'; | ||
|
|
||
| void main() { | ||
| IntegrationTestWidgetsFlutterBinding.ensureInitialized(); | ||
| IntegrationTestWidgetsFlutterBinding.instance.framePolicy = | ||
| LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; | ||
|
|
||
| const fakeDsn = 'https://[email protected]/1234567'; | ||
|
|
||
| tearDown(() async { | ||
| await Sentry.close(); | ||
| }); | ||
|
|
||
| Future<void> setupSentryAndApp(WidgetTester tester, {String? dsn}) async { | ||
| await setupSentry( | ||
| () async { | ||
| await tester.pumpWidget(SentryScreenshotWidget( | ||
| child: DefaultAssetBundle( | ||
| bundle: SentryAssetBundle(enableStructuredDataTracing: true), | ||
| child: const MyApp(), | ||
| ))); | ||
| }, | ||
| dsn ?? fakeDsn, | ||
| isIntegrationTest: true, | ||
| ); | ||
| } | ||
|
|
||
| group('Replay recording', () { | ||
| setUp(() async { | ||
| await SentryFlutter.init((options) { | ||
| // ignore: invalid_use_of_internal_member | ||
| options.automatedTestMode = true; | ||
| options.dsn = 'https://[email protected]/1234567'; | ||
| options.debug = true; | ||
| options.replay.sessionSampleRate = 1.0; | ||
| }); | ||
| testWidgets('native binding is initialized', (tester) async { | ||
| await setupSentryAndApp(tester); | ||
| expect(SentryFlutter.native, isNotNull); | ||
| }); | ||
|
|
||
| tearDown(() async { | ||
| await Sentry.close(); | ||
| testWidgets('supportsReplay matches platform', (tester) async { | ||
| await setupSentryAndApp(tester); | ||
| final supports = SentryFlutter.native?.supportsReplay ?? false; | ||
| expect(supports, Platform.isAndroid || Platform.isIOS ? isTrue : isFalse); | ||
| }); | ||
|
|
||
| test('native binding is initialized', () async { | ||
| // ignore: invalid_use_of_internal_member | ||
| expect(SentryFlutter.native, isNotNull); | ||
| testWidgets('captureReplay returns a SentryId', (tester) async { | ||
| if (!(Platform.isAndroid || Platform.isIOS)) return; | ||
| await setupSentryAndApp(tester); | ||
| final id = await SentryFlutter.native?.captureReplay(); | ||
| expect(id, isA<SentryId>()); | ||
| if (Platform.isIOS) { | ||
| final current = SentryFlutter.native?.replayId; | ||
| expect(current?.toString(), equals(id?.toString())); | ||
| } | ||
| }); | ||
|
|
||
| testWidgets('captureReplay sets native replay ID', (tester) async { | ||
| if (!(Platform.isAndroid || Platform.isIOS)) return; | ||
| await setupSentryAndApp(tester); | ||
| final id = await SentryFlutter.native?.captureReplay(); | ||
| expect(id, isA<SentryId>()); | ||
| expect(SentryFlutter.native?.replayId, isNotNull); | ||
| expect(SentryFlutter.native?.replayId, isNot(const SentryId.empty())); | ||
| }); | ||
|
|
||
| test('session replay is captured', () async { | ||
| // TODO add when the beforeSend callback is implemented for replays. | ||
| }, skip: true); | ||
|
|
||
| test('replay is captured on errors', () async { | ||
| // TODO we may need an HTTP server for this because Android sends replays | ||
| // in a separate envelope. | ||
| }, skip: true); | ||
| }, | ||
| skip: Platform.isAndroid | ||
| ? false | ||
| : "Replay recording is not supported on this platform"); | ||
| // We would like to add a test that ensures a native-initiated replay stop | ||
| // clears the replay ID from the scope. Currently we can't add that test | ||
| // because FFI/JNI cannot be mocked in this environment. | ||
| testWidgets('sets replay ID after capturing exception', (tester) async { | ||
| await setupSentryAndApp(tester); | ||
|
|
||
| try { | ||
| throw Exception('boom'); | ||
| } catch (e, st) { | ||
| await Sentry.captureException(e, stackTrace: st); | ||
| } | ||
|
|
||
| // After capture, ReplayEventProcessor should set scope.replayId | ||
| await Sentry.configureScope((scope) async { | ||
| expect( | ||
| scope.replayId == null || scope.replayId == const SentryId.empty(), | ||
| isFalse); | ||
| }); | ||
|
|
||
| final current = SentryFlutter.native?.replayId; | ||
| await Sentry.configureScope((scope) async { | ||
| expect(current?.toString(), equals(scope.replayId?.toString())); | ||
| }); | ||
| }); | ||
|
|
||
| testWidgets( | ||
| 'replay recorder start emits frame and stop silences frames on Android', | ||
| (tester) async { | ||
| await setupSentryAndApp(tester); | ||
| final native = SentryFlutter.native as SentryNativeJava?; | ||
| expect(native, isNotNull); | ||
|
|
||
| await Future.delayed(const Duration(seconds: 2)); | ||
| final recorder = native!.testRecorder; | ||
| expect(recorder, isNotNull); | ||
|
|
||
| await recorder! | ||
| .onConfigurationChanged(const ScheduledScreenshotRecorderConfig( | ||
| width: 800, | ||
| height: 600, | ||
| frameRate: 1, | ||
| )); | ||
|
|
||
| var frameCount = 0; | ||
| final firstFrame = Completer<void>(); | ||
| recorder.onScreenshotAddedForTest = () { | ||
| frameCount++; | ||
| if (!firstFrame.isCompleted) firstFrame.complete(); | ||
| }; | ||
|
|
||
| await tester.pump(); | ||
| await firstFrame.future.timeout(const Duration(seconds: 5)); | ||
|
|
||
| await recorder.stop(); | ||
| await tester.pump(); | ||
| final afterStopCount = frameCount; | ||
| await Future<void>.delayed(const Duration(seconds: 2)); | ||
| expect(frameCount, equals(afterStopCount)); | ||
| }, skip: !Platform.isAndroid); | ||
|
|
||
| testWidgets( | ||
| 'replay recorder pause silences and resume restarts frames on Android', | ||
| (tester) async { | ||
| await setupSentryAndApp(tester); | ||
| final native = SentryFlutter.native as SentryNativeJava?; | ||
| expect(native, isNotNull); | ||
|
|
||
| await Future.delayed(const Duration(seconds: 2)); | ||
| final recorder = native!.testRecorder; | ||
| expect(recorder, isNotNull); | ||
|
|
||
| await recorder! | ||
| .onConfigurationChanged(const ScheduledScreenshotRecorderConfig( | ||
| width: 800, | ||
| height: 600, | ||
| frameRate: 1, | ||
| )); | ||
|
|
||
| var frameCount = 0; | ||
| final firstFrame = Completer<void>(); | ||
| recorder.onScreenshotAddedForTest = () { | ||
| frameCount++; | ||
| if (!firstFrame.isCompleted) firstFrame.complete(); | ||
| }; | ||
|
|
||
| await tester.pump(); | ||
| await firstFrame.future.timeout(const Duration(seconds: 5)); | ||
|
|
||
| await recorder.pause(); | ||
| await tester.pump(); | ||
| final pausedCount = frameCount; | ||
| await Future<void>.delayed(const Duration(seconds: 2)); | ||
| expect(frameCount, equals(pausedCount)); | ||
|
|
||
| await recorder.resume(); | ||
| await tester.pump(); | ||
| final resumedBaseline = frameCount; | ||
| await Future<void>.delayed(const Duration(seconds: 3)); | ||
| expect(frameCount, greaterThan(resumedBaseline)); | ||
|
|
||
| await recorder.stop(); | ||
| await tester.pump(); | ||
| final afterStopCount = frameCount; | ||
| await Future<void>.delayed(const Duration(seconds: 2)); | ||
| expect(frameCount, equals(afterStopCount)); | ||
| }, skip: !Platform.isAndroid); | ||
|
|
||
| testWidgets('setReplayConfig applies without error on Android', | ||
| (tester) async { | ||
| await setupSentryAndApp(tester); | ||
| const config = ReplayConfig( | ||
| windowWidth: 1080, | ||
| windowHeight: 1920, | ||
| width: 800, | ||
| height: 600, | ||
| frameRate: 1, | ||
| ); | ||
| await Future.delayed(const Duration(seconds: 2)); | ||
|
|
||
| // Should not throw | ||
| await SentryFlutter.native?.setReplayConfig(config); | ||
| }, skip: !Platform.isAndroid); | ||
|
|
||
| testWidgets('capture screenshot via test recorder returns metadata on iOS', | ||
| (tester) async { | ||
| await setupSentryAndApp(tester); | ||
| final native = SentryFlutter.native as SentryNativeCocoa?; | ||
| expect(native, isNotNull); | ||
|
|
||
| await Future.delayed(const Duration(seconds: 2)); | ||
| final json = await native!.testRecorder?.captureScreenshot(); | ||
| expect(json, isNotNull); | ||
| expect(json!['length'], isNotNull); | ||
| expect(json['address'], isNotNull); | ||
| expect(json['width'], isNotNull); | ||
| expect(json['height'], isNotNull); | ||
| expect((json['width'] as int) > 0, isTrue); | ||
| expect((json['height'] as int) > 0, isTrue); | ||
|
|
||
| // Capture again to ensure subsequent captures still succeed | ||
| final json2 = await native.testRecorder?.captureScreenshot(); | ||
| expect(json2, isNotNull); | ||
| }, skip: !Platform.isIOS); | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Misnamed Test Skips iOS Functionality Verification
The test 'setReplayConfig applies without error on iOS' is misnamed. Its platform conditions cause it to run exclusively on Android, meaning it doesn't test iOS functionality as the name implies.