Skip to content

Commit 66e0270

Browse files
authored
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
1 parent 98d9a4a commit 66e0270

8 files changed

+132
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
### Features
66

7+
- Add API for pausing/resuming **iOS** and **macOS** app hang tracking ([#2134](https://github.com/getsentry/sentry-dart/pull/2134))
8+
- 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.
9+
- Use `SentryFlutter.pauseAppHangTracking()` and `SentryFlutter.resumeAppHangTracking()`
710
- Capture total frames, frames delay, slow & frozen frames and attach to spans ([#2106](https://github.com/getsentry/sentry-dart/pull/2106))
811
- Support WebAssembly compilation (dart2wasm) ([#2113](https://github.com/getsentry/sentry-dart/pull/2113))
912

flutter/ios/Classes/SentryFlutterPluginApple.swift

+16
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,12 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
168168
case "displayRefreshRate":
169169
displayRefreshRate(result)
170170

171+
case "pauseAppHangTracking":
172+
pauseAppHangTracking(result)
173+
174+
case "resumeAppHangTracking":
175+
resumeAppHangTracking(result)
176+
171177
default:
172178
result(FlutterMethodNotImplemented)
173179
}
@@ -713,6 +719,16 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
713719
result(Int(mode.refreshRate))
714720
}
715721
#endif
722+
723+
private func pauseAppHangTracking(_ result: @escaping FlutterResult) {
724+
SentrySDK.pauseAppHangTracking()
725+
result("")
726+
}
727+
728+
private func resumeAppHangTracking(_ result: @escaping FlutterResult) {
729+
SentrySDK.resumeAppHangTracking()
730+
result("")
731+
}
716732
}
717733

718734
// swiftlint:enable function_body_length

flutter/lib/src/native/sentry_native_binding.dart

+4
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ abstract class SentryNativeBinding {
5252
SentryId traceId, int startTimeNs, int endTimeNs);
5353

5454
Future<List<DebugImage>?> loadDebugImages();
55+
56+
Future<void> pauseAppHangTracking();
57+
58+
Future<void> resumeAppHangTracking();
5559
}

flutter/lib/src/native/sentry_native_channel.dart

+8
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,12 @@ class SentryNativeChannel
182182
@override
183183
Future<int?> displayRefreshRate() =>
184184
_channel.invokeMethod('displayRefreshRate');
185+
186+
@override
187+
Future<void> pauseAppHangTracking() =>
188+
_channel.invokeMethod('pauseAppHangTracking');
189+
190+
@override
191+
Future<void> resumeAppHangTracking() =>
192+
_channel.invokeMethod('resumeAppHangTracking');
185193
}

flutter/lib/src/sentry_flutter.dart

+28
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,34 @@ mixin SentryFlutter {
236236
return SentryNavigatorObserver.timeToDisplayTracker?.reportFullyDisplayed();
237237
}
238238

239+
/// Pauses the app hang tracking.
240+
/// Only for iOS and macOS.
241+
static Future<void> pauseAppHangTracking() {
242+
if (_native == null) {
243+
// ignore: invalid_use_of_internal_member
244+
Sentry.currentHub.options.logger(
245+
SentryLevel.debug,
246+
'Native integration is not available. Make sure SentryFlutter is initialized before accessing the pauseAppHangTracking API.',
247+
);
248+
return Future<void>.value();
249+
}
250+
return _native!.pauseAppHangTracking();
251+
}
252+
253+
/// Resumes the app hang tracking.
254+
/// Only for iOS and macOS
255+
static Future<void> resumeAppHangTracking() {
256+
if (_native == null) {
257+
// ignore: invalid_use_of_internal_member
258+
Sentry.currentHub.options.logger(
259+
SentryLevel.debug,
260+
'Native integration is not available. Make sure SentryFlutter is initialized before accessing the resumeAppHangTracking API.',
261+
);
262+
return Future<void>.value();
263+
}
264+
return _native!.resumeAppHangTracking();
265+
}
266+
239267
@internal
240268
static SentryNativeBinding? get native => _native;
241269

flutter/test/mocks.mocks.dart

+20
Original file line numberDiff line numberDiff line change
@@ -1320,6 +1320,26 @@ class MockSentryNativeBinding extends _i1.Mock
13201320
),
13211321
returnValue: _i8.Future<List<_i3.DebugImage>?>.value(),
13221322
) as _i8.Future<List<_i3.DebugImage>?>);
1323+
1324+
@override
1325+
_i8.Future<void> pauseAppHangTracking() => (super.noSuchMethod(
1326+
Invocation.method(
1327+
#pauseAppHangTracking,
1328+
[],
1329+
),
1330+
returnValue: _i8.Future<void>.value(),
1331+
returnValueForMissingStub: _i8.Future<void>.value(),
1332+
) as _i8.Future<void>);
1333+
1334+
@override
1335+
_i8.Future<void> resumeAppHangTracking() => (super.noSuchMethod(
1336+
Invocation.method(
1337+
#resumeAppHangTracking,
1338+
[],
1339+
),
1340+
returnValue: _i8.Future<void>.value(),
1341+
returnValueForMissingStub: _i8.Future<void>.value(),
1342+
) as _i8.Future<void>);
13231343
}
13241344

13251345
/// A class which mocks [Hub].

flutter/test/sentry_flutter_test.dart

+35
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// ignore_for_file: invalid_use_of_internal_member
22

33
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:mockito/mockito.dart';
45
import 'package:package_info_plus/package_info_plus.dart';
56
import 'package:sentry/src/platform/platform.dart';
67
import 'package:sentry_flutter/sentry_flutter.dart';
@@ -624,6 +625,40 @@ void main() {
624625
await Sentry.close();
625626
});
626627
});
628+
629+
test('resumeAppHangTracking calls native method when available', () async {
630+
SentryFlutter.native = MockSentryNativeBinding();
631+
when(SentryFlutter.native?.resumeAppHangTracking())
632+
.thenAnswer((_) => Future.value());
633+
634+
await SentryFlutter.resumeAppHangTracking();
635+
636+
verify(SentryFlutter.native?.resumeAppHangTracking()).called(1);
637+
});
638+
639+
test('resumeAppHangTracking does nothing when native is null', () async {
640+
SentryFlutter.native = null;
641+
642+
// This should complete without throwing an error
643+
await expectLater(SentryFlutter.resumeAppHangTracking(), completes);
644+
});
645+
646+
test('pauseAppHangTracking calls native method when available', () async {
647+
SentryFlutter.native = MockSentryNativeBinding();
648+
when(SentryFlutter.native?.pauseAppHangTracking())
649+
.thenAnswer((_) => Future.value());
650+
651+
await SentryFlutter.pauseAppHangTracking();
652+
653+
verify(SentryFlutter.native?.pauseAppHangTracking()).called(1);
654+
});
655+
656+
test('pauseAppHangTracking does nothing when native is null', () async {
657+
SentryFlutter.native = null;
658+
659+
// This should complete without throwing an error
660+
await expectLater(SentryFlutter.pauseAppHangTracking(), completes);
661+
});
627662
}
628663

629664
void appRunner() {}

flutter/test/sentry_native_channel_test.dart

+18
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,24 @@ void main() {
284284

285285
expect(data?.map((v) => v.toJson()), json);
286286
});
287+
288+
test('pauseAppHangTracking', () async {
289+
when(channel.invokeMethod('pauseAppHangTracking'))
290+
.thenAnswer((_) => Future.value());
291+
292+
await sut.pauseAppHangTracking();
293+
294+
verify(channel.invokeMethod('pauseAppHangTracking'));
295+
});
296+
297+
test('resumeAppHangTracking', () async {
298+
when(channel.invokeMethod('resumeAppHangTracking'))
299+
.thenAnswer((_) => Future.value());
300+
301+
await sut.resumeAppHangTracking();
302+
303+
verify(channel.invokeMethod('resumeAppHangTracking'));
304+
});
287305
});
288306
}
289307
}

0 commit comments

Comments
 (0)