Skip to content
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

Add api for pausing/resuming cocoa app hang tracking #2134

Merged
merged 11 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'));
});
});
}
}
Loading