diff --git a/lib/main.dart b/lib/main.dart index 4abb3c20..ced3a77b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -256,6 +256,26 @@ FutureOr _beforeSend(SentryEvent event, Hint _) { } } + // Enrich TimeoutException with operation name + duration as tags/fingerprint. + // value format: "TimeoutException after 0:00:05.000000: timed out" + if (exceptions != null) { + final timeoutException = exceptions.where((e) => e.type == 'TimeoutException').firstOrNull; + if (timeoutException != null) { + final value = timeoutException.value ?? ''; + final colonIdx = value.indexOf(': '); + final message = colonIdx >= 0 ? value.substring(colonIdx + 2) : value; + final operation = message.endsWith(' timed out') + ? message.substring(0, message.length - ' timed out'.length) + : null; + final durationMatch = RegExp(r'after (\d+:\d{2}:\d{2}\.\d+)').firstMatch(value); + + final tags = event.tags ??= {}; + if (operation != null) tags['timeout.operation'] = operation; + if (durationMatch != null) tags['timeout.duration'] = durationMatch.group(1)!; + event.fingerprint = ['TimeoutException', ?operation]; + } + } + // Scrub breadcrumb messages and data final breadcrumbs = event.breadcrumbs; if (breadcrumbs != null) { diff --git a/lib/services/discord_rpc_service.dart b/lib/services/discord_rpc_service.dart index f87bcd9e..34e02798 100644 --- a/lib/services/discord_rpc_service.dart +++ b/lib/services/discord_rpc_service.dart @@ -5,8 +5,9 @@ import 'package:dart_discord_presence/dart_discord_presence.dart'; import 'package:http/http.dart' as http; import '../models/plex_metadata.dart'; -import '../utils/plex_http_client.dart'; import '../utils/app_logger.dart'; +import '../utils/future_extensions.dart'; +import '../utils/plex_http_client.dart'; import 'plex_client.dart'; import 'settings_service.dart'; @@ -317,7 +318,7 @@ class DiscordRPCService { final uploadStreamed = await httpClient.inner .send(uploadRequest) - .timeout(const Duration(seconds: 15)); + .namedTimeout(const Duration(seconds: 15), operation: 'Litterbox upload'); final uploadedUrl = (await uploadStreamed.stream.bytesToString()).trim(); if (uploadedUrl.startsWith('http')) { diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index bb10bc6b..4e64e948 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 '../utils/future_extensions.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'plex_auth_service.dart'; import 'settings_service.dart'; @@ -207,7 +208,7 @@ class MultiServerManager { try { appLogger.d('Attempting connection to server: ${server.name}'); - final client = await _createClientForServer(server: server, clientIdentifier: effectiveClientId).timeout(timeout); + final client = await _createClientForServer(server: server, clientIdentifier: effectiveClientId).namedTimeout(timeout, operation: 'connect to ${server.name}'); // Store the client and server info _clients[serverId]?.close(); diff --git a/lib/services/saf_storage_service.dart b/lib/services/saf_storage_service.dart index a1f2e7c7..7d1db297 100644 --- a/lib/services/saf_storage_service.dart +++ b/lib/services/saf_storage_service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:saf_util/saf_util.dart'; +import '../utils/future_extensions.dart'; import '../utils/platform_detector.dart'; import 'package:saf_util/saf_util_platform_interface.dart'; import 'package:saf_stream/saf_stream.dart'; @@ -150,7 +151,7 @@ class SafStorageService { // This is much more efficient for large files and avoids hangs final result = await _safStream .pasteLocalFile(sourceFilePath, targetDirectoryUri, fileName, mimeType, overwrite: true) - .timeout(const Duration(minutes: 30), onTimeout: () => throw TimeoutException('SAF copy timed out')); + .namedTimeout(const Duration(minutes: 30), operation: 'SAF copy'); debugPrint('SAF copyFileToSaf: successfully copied to ${result.uri}'); return result.uri.toString(); diff --git a/lib/services/track_selection_service.dart b/lib/services/track_selection_service.dart index 77fd2ae6..44cfb0d5 100644 --- a/lib/services/track_selection_service.dart +++ b/lib/services/track_selection_service.dart @@ -3,6 +3,7 @@ import 'dart:async'; import '../mpv/mpv.dart'; import '../models/plex_media_info.dart'; +import '../utils/future_extensions.dart'; import '../models/plex_metadata.dart'; import '../models/plex_user_profile.dart'; import '../utils/app_logger.dart'; @@ -686,7 +687,7 @@ class TrackSelectionService { await player.streams.tracks .where((t) => t.audio.isNotEmpty || t.subtitle.isNotEmpty) .first - .timeout(const Duration(seconds: 10)); + .namedTimeout(const Duration(seconds: 10), operation: 'track loading'); } catch (_) { // Timeout or stream closed — proceed with whatever state we have } diff --git a/lib/utils/plex_http_client.dart b/lib/utils/plex_http_client.dart index 40afa581..d422fe70 100644 --- a/lib/utils/plex_http_client.dart +++ b/lib/utils/plex_http_client.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'app_logger.dart'; +import 'future_extensions.dart'; import 'isolate_helper.dart'; import 'log_redaction_manager.dart'; import 'plex_http_exception.dart'; @@ -145,11 +146,11 @@ class PlexHttpClient { try { final streamed = await _client .send(request) - .timeout(timeout ?? connectTimeout); + .namedTimeout(timeout ?? connectTimeout, operation: 'GET ${uri.path} connect'); final bytes = await streamed.stream .toBytes() - .timeout(timeout ?? receiveTimeout); + .namedTimeout(timeout ?? receiveTimeout, operation: 'GET ${uri.path} receive'); sw.stop(); _logResponse('GET', uri, streamed.statusCode, sw.elapsedMilliseconds); @@ -174,7 +175,7 @@ class PlexHttpClient { try { final streamed = await _client .send(request) - .timeout(timeout ?? connectTimeout); + .namedTimeout(timeout ?? connectTimeout, operation: 'download ${uri.path} connect'); final file = File(filePath); final sink = file.openWrite(); @@ -231,12 +232,12 @@ class PlexHttpClient { // Phase 1: send + receive headers (connect timeout) final streamed = await _client .send(request) - .timeout(timeout ?? connectTimeout); + .namedTimeout(timeout ?? connectTimeout, operation: '$method ${uri.path} connect'); // Phase 2: consume body (receive timeout) final bytes = await streamed.stream .toBytes() - .timeout(timeout ?? receiveTimeout); + .namedTimeout(timeout ?? receiveTimeout, operation: '$method ${uri.path} receive'); sw.stop(); _logResponse(method, uri, streamed.statusCode, sw.elapsedMilliseconds);