Skip to content

Commit

Permalink
Add api for pausing/resuming cocoa app hang tracking (#2134)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
buenaflor authored Jun 26, 2024
1 parent 98d9a4a commit 66e0270
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
16 changes: 16 additions & 0 deletions flutter/ios/Classes/SentryFlutterPluginApple.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ public class SentryFlutterPluginApple: NSObject, FlutterPlugin {
case "displayRefreshRate":
displayRefreshRate(result)

case "pauseAppHangTracking":
pauseAppHangTracking(result)

case "resumeAppHangTracking":
resumeAppHangTracking(result)

default:
result(FlutterMethodNotImplemented)
}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions flutter/lib/src/native/sentry_native_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,8 @@ abstract class SentryNativeBinding {
SentryId traceId, int startTimeNs, int endTimeNs);

Future<List<DebugImage>?> loadDebugImages();

Future<void> pauseAppHangTracking();

Future<void> resumeAppHangTracking();
}
8 changes: 8 additions & 0 deletions flutter/lib/src/native/sentry_native_channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,4 +182,12 @@ class SentryNativeChannel
@override
Future<int?> displayRefreshRate() =>
_channel.invokeMethod('displayRefreshRate');

@override
Future<void> pauseAppHangTracking() =>
_channel.invokeMethod('pauseAppHangTracking');

@override
Future<void> resumeAppHangTracking() =>
_channel.invokeMethod('resumeAppHangTracking');
}
28 changes: 28 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ mixin SentryFlutter {
return SentryNavigatorObserver.timeToDisplayTracker?.reportFullyDisplayed();
}

/// Pauses the app hang tracking.
/// Only for iOS and macOS.
static Future<void> 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<void>.value();
}
return _native!.pauseAppHangTracking();
}

/// Resumes the app hang tracking.
/// Only for iOS and macOS
static Future<void> 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<void>.value();
}
return _native!.resumeAppHangTracking();
}

@internal
static SentryNativeBinding? get native => _native;

Expand Down
20 changes: 20 additions & 0 deletions flutter/test/mocks.mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,26 @@ class MockSentryNativeBinding extends _i1.Mock
),
returnValue: _i8.Future<List<_i3.DebugImage>?>.value(),
) as _i8.Future<List<_i3.DebugImage>?>);

@override
_i8.Future<void> pauseAppHangTracking() => (super.noSuchMethod(
Invocation.method(
#pauseAppHangTracking,
[],
),
returnValue: _i8.Future<void>.value(),
returnValueForMissingStub: _i8.Future<void>.value(),
) as _i8.Future<void>);

@override
_i8.Future<void> resumeAppHangTracking() => (super.noSuchMethod(
Invocation.method(
#resumeAppHangTracking,
[],
),
returnValue: _i8.Future<void>.value(),
returnValueForMissingStub: _i8.Future<void>.value(),
) as _i8.Future<void>);
}

/// A class which mocks [Hub].
Expand Down
35 changes: 35 additions & 0 deletions flutter/test/sentry_flutter_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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() {}
Expand Down
18 changes: 18 additions & 0 deletions flutter/test/sentry_native_channel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
});
}
}

0 comments on commit 66e0270

Please sign in to comment.