This commit is contained in:
problematicconsumer
2023-10-04 18:06:48 +03:30
parent 839967d02d
commit 8ec9f7f964
25 changed files with 130 additions and 89 deletions

View File

@@ -57,12 +57,13 @@
"fromClipboard": "Add From Clipboard", "fromClipboard": "Add From Clipboard",
"scanQr": "Scan QR code", "scanQr": "Scan QR code",
"manually": "Manual Entry", "manually": "Manual Entry",
"addingProfileMsg": "Adding Profile" "addingProfileMsg": "Adding Profile",
"failureMsg": "Failed to add profile"
}, },
"update": { "update": {
"buttonTxt": "Update", "buttonTxt": "Update",
"tooltip": "Update Profile", "tooltip": "Update Profile",
"failureMsg": "Update Failed", "failureMsg": "Failed to update profile",
"successMsg": "Profile updated successfully" "successMsg": "Profile updated successfully"
}, },
"edit": { "edit": {
@@ -76,7 +77,8 @@
}, },
"save": { "save": {
"buttonText": "Save", "buttonText": "Save",
"successMsg": "Profile saved successfully" "successMsg": "Profile saved successfully",
"failureMsg": "Failed to save profile"
}, },
"detailsForm": { "detailsForm": {
"nameLabel": "Name", "nameLabel": "Name",
@@ -242,7 +244,7 @@
"invalidConfig": "Invalid Configs" "invalidConfig": "Invalid Configs"
}, },
"connection": { "connection": {
"unexpected": "Unexpected error", "unexpected": "Unexpected connection error",
"timeout": "Connection timeout", "timeout": "Connection timeout",
"badCertificate": "Bad certificate", "badCertificate": "Bad certificate",
"badResponse": "Bad response", "badResponse": "Bad response",

View File

@@ -57,7 +57,8 @@
"fromClipboard": "افزودن از کلیپ‌بورد", "fromClipboard": "افزودن از کلیپ‌بورد",
"scanQr": "اسکن QR کد", "scanQr": "اسکن QR کد",
"manually": "افزودن دستی", "manually": "افزودن دستی",
"addingProfileMsg": "در حال افزودن پروفایل" "addingProfileMsg": "در حال افزودن پروفایل",
"failureMsg": "در افزودن پروفایل خطایی رخ داد"
}, },
"update": { "update": {
"buttonTxt": "بروزرسانی", "buttonTxt": "بروزرسانی",
@@ -76,7 +77,8 @@
}, },
"save": { "save": {
"buttonText": "ذخیره", "buttonText": "ذخیره",
"successMsg": "پروفایل با موفقیت ذخیره شد" "successMsg": "پروفایل با موفقیت ذخیره شد",
"failureMsg": "خطا در ذخیره پروفایل"
}, },
"detailsForm": { "detailsForm": {
"nameLabel": "نام", "nameLabel": "نام",
@@ -242,7 +244,7 @@
"invalidConfig": "کانفیگ غیر معتبر" "invalidConfig": "کانفیگ غیر معتبر"
}, },
"connection": { "connection": {
"unexpected": "خطای غیرمنتظره", "unexpected": " خطای غیرمنتظره در اتصال",
"timeout": "درخواست بیش از حد مجاز زمان برد", "timeout": "درخواست بیش از حد مجاز زمان برد",
"badCertificate": "خطای اعتبار سنجی", "badCertificate": "خطای اعتبار سنجی",
"badResponse": "پاسخ نامعتبر", "badResponse": "پاسخ نامعتبر",

View File

@@ -10,7 +10,6 @@ import 'package:hiddify/core/prefs/prefs.dart';
import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/data/data_providers.dart';
import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/data/repository/app_repository_impl.dart';
import 'package:hiddify/domain/environment.dart'; import 'package:hiddify/domain/environment.dart';
import 'package:hiddify/domain/failures.dart';
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart'; import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
import 'package:hiddify/features/common/window/window_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart';
import 'package:hiddify/features/system_tray/system_tray.dart'; import 'package:hiddify/features/system_tray/system_tray.dart';
@@ -108,7 +107,6 @@ Future<void> _lazyBootstrap(
runApp( runApp(
ProviderScope( ProviderScope(
parent: container, parent: container,
observers: [SentryRiverpodObserver()],
child: SentryUserInteractionWidget( child: SentryUserInteractionWidget(
child: const AppView(), child: const AppView(),
), ),

View File

@@ -12,7 +12,6 @@ import 'package:hiddify/domain/core_facade.dart';
import 'package:hiddify/domain/profiles/profiles.dart'; import 'package:hiddify/domain/profiles/profiles.dart';
import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/services/service_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sentry_dio/sentry_dio.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
part 'data_providers.g.dart'; part 'data_providers.g.dart';
@@ -34,7 +33,7 @@ Dio dio(DioRef ref) => Dio(
"User-Agent": ref.watch(appInfoProvider).userAgent, "User-Agent": ref.watch(appInfoProvider).userAgent,
}, },
), ),
)..addSentry(); );
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao( ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao(

View File

@@ -136,7 +136,7 @@ class ProfilesRepositoryImpl
}, },
); );
} finally { } finally {
if (await File(tempPath).exists()) await File(tempPath).delete(); if (File(tempPath).existsSync()) File(tempPath).deleteSync();
} }
}, },
(error, stackTrace) { (error, stackTrace) {
@@ -277,7 +277,7 @@ class ProfilesRepositoryImpl
}, },
); );
} finally { } finally {
if (await File(tempPath).exists()) await File(tempPath).delete(); if (File(tempPath).existsSync()) File(tempPath).deleteSync();
} }
}, },
); );

View File

@@ -15,9 +15,11 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure {
StackTrace? stackTrace, StackTrace? stackTrace,
]) = UnexpectedConnectionFailure; ]) = UnexpectedConnectionFailure;
@With<ExpectedMeasuredFailure>()
const factory ConnectionFailure.missingVpnPermission([String? message]) = const factory ConnectionFailure.missingVpnPermission([String? message]) =
MissingVpnPermission; MissingVpnPermission;
@With<ExpectedMeasuredFailure>()
const factory ConnectionFailure.missingNotificationPermission([ const factory ConnectionFailure.missingNotificationPermission([
String? message, String? message,
]) = MissingNotificationPermission; ]) = MissingNotificationPermission;
@@ -28,9 +30,9 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure {
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
UnexpectedConnectionFailure(:final error) => ( UnexpectedConnectionFailure() => (
type: t.failure.connectivity.unexpected, type: t.failure.connectivity.unexpected,
message: t.mayPrintError(error), message: null,
), ),
MissingVpnPermission(:final message) => ( MissingVpnPermission(:final message) => (
type: t.failure.connectivity.missingVpnPermission, type: t.failure.connectivity.missingVpnPermission,

View File

@@ -14,7 +14,7 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure {
StackTrace? stackTrace, StackTrace? stackTrace,
) = UnexpectedCoreServiceFailure; ) = UnexpectedCoreServiceFailure;
@With<ExpectedException>() @With<ExpectedFailure>()
const factory CoreServiceFailure.serviceNotRunning([String? message]) = const factory CoreServiceFailure.serviceNotRunning([String? message]) =
CoreServiceNotRunning; CoreServiceNotRunning;
@@ -22,6 +22,7 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure {
String? message, String? message,
]) = InvalidConfigOptions; ]) = InvalidConfigOptions;
@With<ExpectedMeasuredFailure>()
const factory CoreServiceFailure.invalidConfig([ const factory CoreServiceFailure.invalidConfig([
String? message, String? message,
]) = InvalidConfig; ]) = InvalidConfig;
@@ -51,9 +52,9 @@ sealed class CoreServiceFailure with _$CoreServiceFailure, Failure {
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
UnexpectedCoreServiceFailure(:final error) => ( UnexpectedCoreServiceFailure() => (
type: t.failure.singbox.unexpected, type: t.failure.singbox.unexpected,
message: t.mayPrintError(error), message: null,
), ),
CoreServiceNotRunning(:final message) => ( CoreServiceNotRunning(:final message) => (
type: t.failure.singbox.serviceNotRunning, type: t.failure.singbox.serviceNotRunning,

View File

@@ -1,68 +1,73 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:hiddify/core/prefs/prefs.dart'; import 'package:hiddify/core/prefs/prefs.dart';
typedef PresentableError = ({String type, String? message});
mixin Failure { mixin Failure {
({String type, String? message}) present(TranslationsEn t); ({String type, String? message}) present(TranslationsEn t);
} }
/// failures that are not expected to happen but depending on [error] type might not be relevant (eg network errors)
mixin UnexpectedFailure { mixin UnexpectedFailure {
Object? get error; Object? get error;
StackTrace? get stackTrace; StackTrace? get stackTrace;
} }
/// failures that are expected to happen and should be handled by the app
/// and should be logged, eg missing permissions
mixin ExpectedMeasuredFailure {}
/// failures ignored by analytics service etc. /// failures ignored by analytics service etc.
mixin ExpectedException {} mixin ExpectedFailure {}
extension ErrorPresenter on TranslationsEn { extension ErrorPresenter on TranslationsEn {
String? _errorToMessage(Object error) { PresentableError errorToPair(Object error) => switch (error) {
switch (error) { UnexpectedFailure(error: final nestedErr?) => errorToPair(nestedErr),
case Failure(): Failure() => error.present(this),
final err = error.present(this); DioException() => error.present(this),
return err.type + (err.message == null ? "" : ": ${err.message}"); _ => (type: failure.unexpected, message: null),
case DioException(): };
return error.present(this);
default:
return null;
}
}
String printError(Object error) => PresentableError presentError(
_errorToMessage(error) ?? failure.unexpected;
String? mayPrintError(Object? error) =>
error != null ? _errorToMessage(error) : null;
({String type, String? message}) presentError(
Object error, { Object error, {
String? action, String? action,
}) { }) {
final ({String type, String? message}) presentable; final pair = errorToPair(error);
if (error case Failure()) { if (action == null) return pair;
presentable = error.present(this);
} else {
presentable = (type: failure.unexpected, message: null);
}
return ( return (
type: action == null ? presentable.type : "$action: ${presentable.type}", type: action,
message: presentable.message, message: pair.type + (pair.message == null ? "" : "\n${pair.message!}"),
); );
} }
String presentShortError(
Object error, {
String? action,
}) {
final pair = errorToPair(error);
if (action == null) return pair.type;
return "$action: ${pair.type}";
}
} }
extension DioExceptionPresenter on DioException { extension DioExceptionPresenter on DioException {
String presentType(TranslationsEn t) => switch (type) { PresentableError present(TranslationsEn t) => switch (type) {
DioExceptionType.connectionTimeout || DioExceptionType.connectionTimeout ||
DioExceptionType.sendTimeout || DioExceptionType.sendTimeout ||
DioExceptionType.receiveTimeout => DioExceptionType.receiveTimeout =>
t.failure.connection.timeout, (type: t.failure.connection.timeout, message: null),
DioExceptionType.badCertificate => t.failure.connection.badCertificate, DioExceptionType.badCertificate => (
DioExceptionType.badResponse => t.failure.connection.badResponse, type: t.failure.connection.badCertificate,
DioExceptionType.connectionError => message: message,
t.failure.connection.connectionError, ),
_ => t.failure.unexpected, DioExceptionType.badResponse => (
type: t.failure.connection.badResponse,
message: message,
),
DioExceptionType.connectionError => (
type: t.failure.connection.connectionError,
message: message,
),
_ => (type: t.failure.connection.unexpected, message: message),
}; };
String present(TranslationsEn t) {
return presentType(t) + (message == null ? "" : "\n$message");
}
} }

View File

@@ -34,6 +34,7 @@ sealed class Profile with _$Profile {
required DateTime lastUpdate, required DateTime lastUpdate,
}) = LocalProfile; }) = LocalProfile;
// ignore: prefer_constructors_over_static_methods
static RemoteProfile fromResponse( static RemoteProfile fromResponse(
String url, String url,
Map<String, List<String>> headers, Map<String, List<String>> headers,

View File

@@ -16,18 +16,19 @@ sealed class ProfileFailure with _$ProfileFailure, Failure {
const factory ProfileFailure.notFound() = ProfileNotFoundFailure; const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
@With<ExpectedException>() @With<ExpectedFailure>()
const factory ProfileFailure.invalidUrl() = ProfileInvalidUrlFailure; const factory ProfileFailure.invalidUrl() = ProfileInvalidUrlFailure;
@With<ExpectedFailure>()
const factory ProfileFailure.invalidConfig([String? message]) = const factory ProfileFailure.invalidConfig([String? message]) =
ProfileInvalidConfigFailure; ProfileInvalidConfigFailure;
@override @override
({String type, String? message}) present(TranslationsEn t) { ({String type, String? message}) present(TranslationsEn t) {
return switch (this) { return switch (this) {
ProfileUnexpectedFailure(:final error) => ( ProfileUnexpectedFailure() => (
type: t.failure.profiles.unexpected, type: t.failure.profiles.unexpected,
message: t.mayPrintError(error), message: null,
), ),
ProfileNotFoundFailure() => ( ProfileNotFoundFailure() => (
type: t.failure.profiles.notFound, type: t.failure.profiles.notFound,

View File

@@ -31,7 +31,7 @@ class AboutPage extends HookConsumerWidget {
canIgnore: false, canIgnore: false,
).show(context); ).show(context);
case AppUpdateStateError(:final error): case AppUpdateStateError(:final error):
return CustomToast.error(t.printError(error)).show(context); return CustomToast.error(t.presentShortError(error)).show(context);
case AppUpdateStateNotAvailable(): case AppUpdateStateNotAvailable():
return CustomToast.success(t.appUpdate.notAvailableMsg) return CustomToast.success(t.appUpdate.notAvailableMsg)
.show(context); .show(context);

View File

@@ -1,4 +1,3 @@
import 'package:dartx/dartx.dart';
import 'package:hiddify/core/prefs/general_prefs.dart'; import 'package:hiddify/core/prefs/general_prefs.dart';
import 'package:hiddify/features/common/app_update_notifier.dart'; import 'package:hiddify/features/common/app_update_notifier.dart';
import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/connectivity/connectivity_controller.dart';
@@ -20,8 +19,7 @@ void commonControllers(CommonControllersRef ref) {
introCompletedProvider, introCompletedProvider,
(_, completed) async { (_, completed) async {
if (completed) { if (completed) {
await Future.delayed(5.seconds) await ref.read(cronServiceProvider).startScheduler();
.then((_) async => ref.read(cronServiceProvider).startScheduler());
} }
}, },
fireImmediately: true, fireImmediately: true,

View File

@@ -7,23 +7,30 @@ import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
// TODO add release notes // TODO add release notes
class NewVersionDialog extends HookConsumerWidget { class NewVersionDialog extends HookConsumerWidget with PresLogger {
const NewVersionDialog( NewVersionDialog(
this.currentVersion, this.currentVersion,
this.newVersion, { this.newVersion, {
super.key, // super.key,
this.canIgnore = true, this.canIgnore = true,
}); }) : super(key: _dialogKey);
final String currentVersion; final String currentVersion;
final RemoteVersionInfo newVersion; final RemoteVersionInfo newVersion;
final bool canIgnore; final bool canIgnore;
Future<void> show(BuildContext context) { static final _dialogKey = GlobalKey(debugLabel: 'new version dialog');
return showDialog(
context: context, Future<void> show(BuildContext context) async {
builder: (context) => this, if (_dialogKey.currentContext == null) {
); return showDialog(
context: context,
useRootNavigator: true,
builder: (context) => this,
);
} else {
loggy.warning("new version dialog is already open");
}
} }
@override @override

View File

@@ -32,7 +32,7 @@ class ProfileTile extends HookConsumerWidget {
final selectActiveMutation = useMutation( final selectActiveMutation = useMutation(
initialOnFailure: (err) { initialOnFailure: (err) {
CustomToast.error(t.printError(err)).show(context); CustomToast.error(t.presentShortError(err)).show(context);
}, },
initialOnSuccess: () { initialOnSuccess: () {
if (context.mounted) context.pop(); if (context.mounted) context.pop();

View File

@@ -68,7 +68,7 @@ class HomePage extends HookConsumerWidget {
_ => const EmptyProfilesHomeBody(), _ => const EmptyProfilesHomeBody(),
}, },
AsyncError(:final error) => AsyncError(:final error) =>
SliverErrorBodyPlaceholder(t.printError(error)), SliverErrorBodyPlaceholder(t.presentShortError(error)),
_ => const SliverToBoxAdapter(), _ => const SliverToBoxAdapter(),
}, },
], ],

View File

@@ -69,8 +69,16 @@ class IntroPage extends HookConsumerWidget with PresLogger {
child: FilledButton( child: FilledButton(
onPressed: () async { onPressed: () async {
if (!ref.read(enableAnalyticsProvider)) { if (!ref.read(enableAnalyticsProvider)) {
loggy.debug("disabling analytics per user request"); loggy.info("disabling analytics per user request");
await Sentry.close(); try {
await Sentry.close();
} catch (error, stackTrace) {
loggy.warning(
"could not disable analytics",
error,
stackTrace,
);
}
} }
await ref await ref
.read(introCompletedProvider.notifier) .read(introCompletedProvider.notifier)

View File

@@ -138,7 +138,7 @@ class LogsPage extends HookConsumerWidget with PresLogger {
NestedTabAppBar( NestedTabAppBar(
title: Text(t.logs.pageTitle), title: Text(t.logs.pageTitle),
), ),
SliverErrorBodyPlaceholder(t.printError(error)), SliverErrorBodyPlaceholder(t.presentShortError(error)),
], ],
), ),
); );

View File

@@ -29,7 +29,15 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
if (asyncSave case AsyncData(value: final save)) { if (asyncSave case AsyncData(value: final save)) {
switch (save) { switch (save) {
case MutationFailure(:final failure): case MutationFailure(:final failure):
CustomAlertDialog.fromErr(t.presentError(failure)).show(context); final String action;
if (ref.read(provider) case AsyncData(value: final data)
when data.isEditing) {
action = t.profile.save.failureMsg;
} else {
action = t.profile.add.failureMsg;
}
CustomAlertDialog.fromErr(t.presentError(failure, action: action))
.show(context);
case MutationSuccess(): case MutationSuccess():
CustomToast.success(t.profile.save.successMsg).show(context); CustomToast.success(t.profile.save.successMsg).show(context);
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
@@ -62,7 +70,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
if (asyncDelete case AsyncData(value: final delete)) { if (asyncDelete case AsyncData(value: final delete)) {
switch (delete) { switch (delete) {
case MutationFailure(:final failure): case MutationFailure(:final failure):
CustomToast.error(t.printError(failure)).show(context); CustomToast.error(t.presentShortError(failure)).show(context);
case MutationSuccess(): case MutationSuccess():
CustomToast.success(t.profile.delete.successMsg).show(context); CustomToast.success(t.profile.delete.successMsg).show(context);
WidgetsBinding.instance.addPostFrameCallback( WidgetsBinding.instance.addPostFrameCallback(
@@ -261,7 +269,7 @@ class ProfileDetailPage extends HookConsumerWidget with PresLogger {
title: Text(t.profile.detailsPageTitle), title: Text(t.profile.detailsPageTitle),
pinned: true, pinned: true,
), ),
SliverErrorBodyPlaceholder(t.printError(error)), SliverErrorBodyPlaceholder(t.presentShortError(error)),
], ],
), ),
); );

View File

@@ -35,7 +35,9 @@ class AddProfileModal extends HookConsumerWidget {
t.failure.profiles.invalidUrl, t.failure.profiles.invalidUrl,
).show(context); ).show(context);
} else { } else {
CustomAlertDialog.fromErr(t.presentError(err)).show(context); CustomAlertDialog.fromErr(
t.presentError(err, action: t.profile.add.failureMsg),
).show(context);
} }
}, },
initialOnSuccess: () { initialOnSuccess: () {

View File

@@ -37,7 +37,7 @@ class ProfilesModal extends HookConsumerWidget {
itemCount: profiles.length, itemCount: profiles.length,
), ),
AsyncError(:final error) => SliverErrorBodyPlaceholder( AsyncError(:final error) => SliverErrorBodyPlaceholder(
t.printError(error), t.presentShortError(error),
), ),
AsyncLoading() => const SliverLoadingBodyPlaceholder(), AsyncLoading() => const SliverLoadingBodyPlaceholder(),
_ => const SliverToBoxAdapter(), _ => const SliverToBoxAdapter(),

View File

@@ -20,7 +20,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
final selectActiveProxyMutation = useMutation( final selectActiveProxyMutation = useMutation(
initialOnFailure: (error) => initialOnFailure: (error) =>
CustomToast.error(t.printError(error)).show(context), CustomToast.error(t.presentShortError(error)).show(context),
); );
switch (asyncProxies) { switch (asyncProxies) {
@@ -144,7 +144,7 @@ class ProxiesPage extends HookConsumerWidget with PresLogger {
title: Text(t.proxies.pageTitle), title: Text(t.proxies.pageTitle),
), ),
SliverErrorBodyPlaceholder( SliverErrorBodyPlaceholder(
t.printError(error), t.presentShortError(error),
icon: null, icon: null,
), ),
], ],

View File

@@ -55,9 +55,9 @@ class CronService with InfraLogger {
int runCount = 0; int runCount = 0;
_scheduler = NeatPeriodicTaskScheduler( _scheduler = NeatPeriodicTaskScheduler(
name: "cron job scheduler", name: "cron job scheduler",
interval: const Duration(minutes: 5), interval: const Duration(minutes: 10),
timeout: const Duration(seconds: 15), timeout: const Duration(minutes: 5),
minCycle: const Duration(minutes: 1), minCycle: const Duration(minutes: 2),
task: () { task: () {
loggy.debug("in run ${runCount++}"); loggy.debug("in run ${runCount++}");
return Future.wait(jobs.values.map(run)); return Future.wait(jobs.values.map(run));

View File

@@ -20,6 +20,7 @@ class CustomAlertDialog extends StatelessWidget {
Future<void> show(BuildContext context) async { Future<void> show(BuildContext context) async {
await showDialog( await showDialog(
context: context, context: context,
useRootNavigator: true,
builder: (context) => this, builder: (context) => this,
); );
} }

View File

@@ -34,7 +34,7 @@ class SentryLoggyIntegration extends LoggyPrinter
@override @override
Future<void> onLog(LogRecord record) async { Future<void> onLog(LogRecord record) async {
if (!canSendEvent(record.error)) return; if (!canLogEvent(record.error)) return;
if (_shouldLog(record.level, _minEventLevel)) { if (_shouldLog(record.level, _minEventLevel)) {
await _hub.captureEvent( await _hub.captureEvent(

View File

@@ -15,7 +15,13 @@ bool canSendEvent(dynamic throwable) {
UnexpectedFailure(:final error) => canSendEvent(error), UnexpectedFailure(:final error) => canSendEvent(error),
DioException _ => false, DioException _ => false,
SocketException _ => false, SocketException _ => false,
ExpectedException _ => false, ExpectedFailure _ => false,
ExpectedMeasuredFailure _ => false,
_ => true, _ => true,
}; };
} }
bool canLogEvent(dynamic throwable) => switch (throwable) {
ExpectedMeasuredFailure _ => true,
_ => canSendEvent(throwable),
};