From 66e0270957648ac5354fdf20b2b2d9f8a589a8a7 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 26 Jun 2024 15:16:50 +0200 Subject: [PATCH] Add api for pausing/resuming cocoa app hang tracking (#2134) * Add api * Add docs * Format * Update * Add test * Add test * Changelog * Add failure test * Swiftlint * typo fix * Update log message to include the function name --- CHANGELOG.md | 3 ++ .../Classes/SentryFlutterPluginApple.swift | 16 +++++++++ .../lib/src/native/sentry_native_binding.dart | 4 +++ .../lib/src/native/sentry_native_channel.dart | 8 +++++ flutter/lib/src/sentry_flutter.dart | 28 +++++++++++++++ flutter/test/mocks.mocks.dart | 20 +++++++++++ flutter/test/sentry_flutter_test.dart | 35 +++++++++++++++++++ flutter/test/sentry_native_channel_test.dart | 18 ++++++++++ 8 files changed, 132 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f40482e232..66c7f7f8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Features +- Add API for pausing/resuming **iOS** and **macOS** app hang tracking ([#2134](https://github.com/getsentry/sentry-dart/pull/2134)) + - This is useful to prevent the Cocoa SDK from reporting wrongly detected app hangs when the OS shows a system dialog for asking specific permissions. + - Use `SentryFlutter.pauseAppHangTracking()` and `SentryFlutter.resumeAppHangTracking()` - Capture total frames, frames delay, slow & frozen frames and attach to spans ([#2106](https://github.com/getsentry/sentry-dart/pull/2106)) - Support WebAssembly compilation (dart2wasm) ([#2113](https://github.com/getsentry/sentry-dart/pull/2113)) diff --git a/flutter/ios/Classes/SentryFlutterPluginApple.swift b/flutter/ios/Classes/SentryFlutterPluginApple.swift index cee6266564..35249ef5d1 100644 --- a/flutter/ios/Classes/SentryFlutterPluginApple.swift +++ b/flutter/ios/Classes/SentryFlutterPluginApple.swift @@ -168,6 +168,12 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { case "displayRefreshRate": displayRefreshRate(result) + case "pauseAppHangTracking": + pauseAppHangTracking(result) + + case "resumeAppHangTracking": + resumeAppHangTracking(result) + default: result(FlutterMethodNotImplemented) } @@ -713,6 +719,16 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin { result(Int(mode.refreshRate)) } #endif + + private func pauseAppHangTracking(_ result: @escaping FlutterResult) { + SentrySDK.pauseAppHangTracking() + result("") + } + + private func resumeAppHangTracking(_ result: @escaping FlutterResult) { + SentrySDK.resumeAppHangTracking() + result("") + } } // swiftlint:enable function_body_length diff --git a/flutter/lib/src/native/sentry_native_binding.dart b/flutter/lib/src/native/sentry_native_binding.dart index 54223b5561..07ea5969fe 100644 --- a/flutter/lib/src/native/sentry_native_binding.dart +++ b/flutter/lib/src/native/sentry_native_binding.dart @@ -52,4 +52,8 @@ abstract class SentryNativeBinding { SentryId traceId, int startTimeNs, int endTimeNs); Future?> loadDebugImages(); + + Future pauseAppHangTracking(); + + Future resumeAppHangTracking(); } diff --git a/flutter/lib/src/native/sentry_native_channel.dart b/flutter/lib/src/native/sentry_native_channel.dart index e6888eecac..fe00683b0e 100644 --- a/flutter/lib/src/native/sentry_native_channel.dart +++ b/flutter/lib/src/native/sentry_native_channel.dart @@ -182,4 +182,12 @@ class SentryNativeChannel @override Future displayRefreshRate() => _channel.invokeMethod('displayRefreshRate'); + + @override + Future pauseAppHangTracking() => + _channel.invokeMethod('pauseAppHangTracking'); + + @override + Future resumeAppHangTracking() => + _channel.invokeMethod('resumeAppHangTracking'); } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 8cf3d995ba..4b0deb8541 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -236,6 +236,34 @@ mixin SentryFlutter { return SentryNavigatorObserver.timeToDisplayTracker?.reportFullyDisplayed(); } + /// Pauses the app hang tracking. + /// Only for iOS and macOS. + static Future pauseAppHangTracking() { + if (_native == null) { + // ignore: invalid_use_of_internal_member + Sentry.currentHub.options.logger( + SentryLevel.debug, + 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the pauseAppHangTracking API.', + ); + return Future.value(); + } + return _native!.pauseAppHangTracking(); + } + + /// Resumes the app hang tracking. + /// Only for iOS and macOS + static Future resumeAppHangTracking() { + if (_native == null) { + // ignore: invalid_use_of_internal_member + Sentry.currentHub.options.logger( + SentryLevel.debug, + 'Native integration is not available. Make sure SentryFlutter is initialized before accessing the resumeAppHangTracking API.', + ); + return Future.value(); + } + return _native!.resumeAppHangTracking(); + } + @internal static SentryNativeBinding? get native => _native; diff --git a/flutter/test/mocks.mocks.dart b/flutter/test/mocks.mocks.dart index 7e595ab4cf..214b15b588 100644 --- a/flutter/test/mocks.mocks.dart +++ b/flutter/test/mocks.mocks.dart @@ -1320,6 +1320,26 @@ class MockSentryNativeBinding extends _i1.Mock ), returnValue: _i8.Future?>.value(), ) as _i8.Future?>); + + @override + _i8.Future pauseAppHangTracking() => (super.noSuchMethod( + Invocation.method( + #pauseAppHangTracking, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); + + @override + _i8.Future resumeAppHangTracking() => (super.noSuchMethod( + Invocation.method( + #resumeAppHangTracking, + [], + ), + returnValue: _i8.Future.value(), + returnValueForMissingStub: _i8.Future.value(), + ) as _i8.Future); } /// A class which mocks [Hub]. diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 0c9590dc92..3987cdfb7e 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: invalid_use_of_internal_member import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sentry/src/platform/platform.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; @@ -624,6 +625,40 @@ void main() { await Sentry.close(); }); }); + + test('resumeAppHangTracking calls native method when available', () async { + SentryFlutter.native = MockSentryNativeBinding(); + when(SentryFlutter.native?.resumeAppHangTracking()) + .thenAnswer((_) => Future.value()); + + await SentryFlutter.resumeAppHangTracking(); + + verify(SentryFlutter.native?.resumeAppHangTracking()).called(1); + }); + + test('resumeAppHangTracking does nothing when native is null', () async { + SentryFlutter.native = null; + + // This should complete without throwing an error + await expectLater(SentryFlutter.resumeAppHangTracking(), completes); + }); + + test('pauseAppHangTracking calls native method when available', () async { + SentryFlutter.native = MockSentryNativeBinding(); + when(SentryFlutter.native?.pauseAppHangTracking()) + .thenAnswer((_) => Future.value()); + + await SentryFlutter.pauseAppHangTracking(); + + verify(SentryFlutter.native?.pauseAppHangTracking()).called(1); + }); + + test('pauseAppHangTracking does nothing when native is null', () async { + SentryFlutter.native = null; + + // This should complete without throwing an error + await expectLater(SentryFlutter.pauseAppHangTracking(), completes); + }); } void appRunner() {} diff --git a/flutter/test/sentry_native_channel_test.dart b/flutter/test/sentry_native_channel_test.dart index 6a10400d3c..cc2e67dfd5 100644 --- a/flutter/test/sentry_native_channel_test.dart +++ b/flutter/test/sentry_native_channel_test.dart @@ -284,6 +284,24 @@ void main() { expect(data?.map((v) => v.toJson()), json); }); + + test('pauseAppHangTracking', () async { + when(channel.invokeMethod('pauseAppHangTracking')) + .thenAnswer((_) => Future.value()); + + await sut.pauseAppHangTracking(); + + verify(channel.invokeMethod('pauseAppHangTracking')); + }); + + test('resumeAppHangTracking', () async { + when(channel.invokeMethod('resumeAppHangTracking')) + .thenAnswer((_) => Future.value()); + + await sut.resumeAppHangTracking(); + + verify(channel.invokeMethod('resumeAppHangTracking')); + }); }); } }