From 79888168aee901a19a4c9d32ca625421b4b4a948 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 15 Mar 2026 05:06:16 +0100 Subject: [PATCH] fix: add timeout diagnostics --- lib/main.dart | 4 ++++ .../companion_remote_peer_service.dart | 3 ++- lib/services/download_manager_service.dart | 4 ++++ lib/services/multi_server_manager.dart | 3 +++ lib/utils/future_extensions.dart | 11 +++++++++++ lib/watch_together/screens/watch_together_screen.dart | 3 ++- .../services/watch_together_peer_service.dart | 3 ++- 7 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 lib/utils/future_extensions.dart diff --git a/lib/main.dart b/lib/main.dart index 646f07e2..f1c13a73 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -501,6 +501,7 @@ class _SetupScreenState extends State { // Check network connectivity early to fast-path airplane mode. // Timeout guards against connectivity_plus hanging on some Android TV devices after force-close. bool hasNetwork; + Sentry.addBreadcrumb(Breadcrumb(message: 'Checking network connectivity', category: 'setup')); try { final connectivityResult = await Connectivity().checkConnectivity().timeout( const Duration(seconds: 3), @@ -512,6 +513,8 @@ class _SetupScreenState extends State { hasNetwork = true; } + Sentry.addBreadcrumb(Breadcrumb(message: 'Network check done: hasNetwork=$hasNetwork', category: 'setup')); + if (hasNetwork) { _setStatus(t.common.refreshingServers); @@ -551,6 +554,7 @@ class _SetupScreenState extends State { return; } + Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'setup')); _setStatus(t.common.connectingToServers); // Populate per-server status for splash display diff --git a/lib/services/companion_remote/companion_remote_peer_service.dart b/lib/services/companion_remote/companion_remote_peer_service.dart index 1d3a7ce7..93a0b39b 100644 --- a/lib/services/companion_remote/companion_remote_peer_service.dart +++ b/lib/services/companion_remote/companion_remote_peer_service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math'; import 'package:web_socket_channel/io.dart'; +import '../../utils/future_extensions.dart'; import '../../models/companion_remote/remote_command.dart'; import '../../models/companion_remote/remote_session.dart'; @@ -506,7 +507,7 @@ class CompanionRemotePeerService with KeepaliveMixin { } try { - final winner = await completer.future.timeout(const Duration(seconds: 10)); + final winner = await completer.future.namedTimeout(const Duration(seconds: 10), operation: 'CompanionRemote race connect'); cleanup(); // Now set up the proper managed connection on the winning address diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 32a2fc43..48a6f9db 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -20,6 +20,7 @@ import '../utils/app_logger.dart'; import '../utils/codec_utils.dart'; import '../utils/global_key_utils.dart'; import '../utils/plex_cache_parser.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; /// Context for a download that's been enqueued with background_downloader. /// Carries metadata needed between enqueue and completion callback. @@ -158,9 +159,11 @@ class DownloadManagerService { /// then scans drift for orphaned items. Future recoverInterruptedDownloads() async { try { + Sentry.addBreadcrumb(Breadcrumb(message: 'Initializing FileDownloader', category: 'downloads')); await _initializeFileDownloader(); // Let background_downloader re-enqueue tasks killed by the OS + Sentry.addBreadcrumb(Breadcrumb(message: 'Rescheduling killed tasks', category: 'downloads')); final (rescheduled, _) = await FileDownloader().rescheduleKilledTasks(); if (rescheduled.isNotEmpty) { appLogger.i('Rescheduled ${rescheduled.length} killed download task(s)'); @@ -206,6 +209,7 @@ class DownloadManagerService { } // Scan drift for orphaned items stuck in 'downloading' + Sentry.addBreadcrumb(Breadcrumb(message: 'Scanning for orphaned downloads', category: 'downloads')); final allDownloads = await _database.select(_database.downloadedMedia).get(); for (final item in allDownloads) { if (item.status == DownloadStatus.downloading.index) { diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index 7c637017..3a38f42b 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -6,6 +6,7 @@ import 'plex_client.dart'; import '../models/plex_config.dart'; import '../utils/app_logger.dart'; import '../utils/connection_constants.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'plex_auth_service.dart'; import 'storage_service.dart'; @@ -183,6 +184,7 @@ class MultiServerManager { } appLogger.i('Connecting to ${servers.length} servers...'); + Sentry.addBreadcrumb(Breadcrumb(message: 'Connecting to ${servers.length} server(s)', category: 'servers')); // Use provided client ID or generate a unique one for this app instance final effectiveClientId = clientIdentifier ?? DateTime.now().millisecondsSinceEpoch.toString(); @@ -486,6 +488,7 @@ class MultiServerManager { if (offline.isEmpty) return; appLogger.d('Attempting reconnection for ${offline.length} offline servers'); + Sentry.addBreadcrumb(Breadcrumb(message: 'Reconnecting ${offline.length} offline server(s)', category: 'servers')); final futures = offline.map((serverId) { final server = _servers[serverId]; diff --git a/lib/utils/future_extensions.dart b/lib/utils/future_extensions.dart new file mode 100644 index 00000000..f76492e8 --- /dev/null +++ b/lib/utils/future_extensions.dart @@ -0,0 +1,11 @@ +import 'dart:async'; + +extension NamedTimeoutExtension on Future { + /// Like [Future.timeout], but the [TimeoutException] includes [operation] + /// so crash reports identify which call timed out. + Future namedTimeout(Duration timeLimit, {required String operation}) { + return timeout(timeLimit, onTimeout: () { + throw TimeoutException('$operation timed out', timeLimit); + }); + } +} diff --git a/lib/watch_together/screens/watch_together_screen.dart b/lib/watch_together/screens/watch_together_screen.dart index 51eedcd0..da6a22b8 100644 --- a/lib/watch_together/screens/watch_together_screen.dart +++ b/lib/watch_together/screens/watch_together_screen.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import '../../utils/future_extensions.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; @@ -91,7 +92,7 @@ class _NotInSessionViewState extends State<_NotInSessionView> { final client = HttpClient(); client.connectionTimeout = const Duration(seconds: 5); final request = await client.getUrl(Uri.parse(WatchTogetherPeerService.healthUrl)); - final response = await request.close().timeout(const Duration(seconds: 5)); + final response = await request.close().namedTimeout(const Duration(seconds: 5), operation: 'WatchTogether health check'); final body = await response.transform(const SystemEncoding().decoder).join(); client.close(); if (!mounted) return; diff --git a/lib/watch_together/services/watch_together_peer_service.dart b/lib/watch_together/services/watch_together_peer_service.dart index 9f5f41d6..decb2afa 100644 --- a/lib/watch_together/services/watch_together_peer_service.dart +++ b/lib/watch_together/services/watch_together_peer_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:uuid/uuid.dart'; +import '../../utils/future_extensions.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../../services/base_peer_service.dart'; @@ -280,7 +281,7 @@ class WatchTogetherPeerService with KeepaliveMixin { _sendRaw({'type': 'join', 'sessionId': _sessionId, 'peerId': _myPeerId}); } - await completer.future.timeout(const Duration(seconds: 10)); + await completer.future.namedTimeout(const Duration(seconds: 10), operation: 'WatchTogether reconnect'); appLogger.d('WatchTogether: Reconnected successfully'); } catch (e) { appLogger.e('WatchTogether: Reconnect failed', error: e);