feat: namedTimeout for Sentry context

This commit is contained in:
edde746
2026-04-16 07:27:05 +02:00
parent a60e1e7f42
commit 6c61e0e4c9
6 changed files with 35 additions and 10 deletions
+20
View File
@@ -256,6 +256,26 @@ FutureOr<SentryEvent?> _beforeSend(SentryEvent event, Hint _) {
} }
} }
// Enrich TimeoutException with operation name + duration as tags/fingerprint.
// value format: "TimeoutException after 0:00:05.000000: <operation> 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 // Scrub breadcrumb messages and data
final breadcrumbs = event.breadcrumbs; final breadcrumbs = event.breadcrumbs;
if (breadcrumbs != null) { if (breadcrumbs != null) {
+3 -2
View File
@@ -5,8 +5,9 @@ import 'package:dart_discord_presence/dart_discord_presence.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../utils/plex_http_client.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/future_extensions.dart';
import '../utils/plex_http_client.dart';
import 'plex_client.dart'; import 'plex_client.dart';
import 'settings_service.dart'; import 'settings_service.dart';
@@ -317,7 +318,7 @@ class DiscordRPCService {
final uploadStreamed = await httpClient.inner final uploadStreamed = await httpClient.inner
.send(uploadRequest) .send(uploadRequest)
.timeout(const Duration(seconds: 15)); .namedTimeout(const Duration(seconds: 15), operation: 'Litterbox upload');
final uploadedUrl = (await uploadStreamed.stream.bytesToString()).trim(); final uploadedUrl = (await uploadStreamed.stream.bytesToString()).trim();
if (uploadedUrl.startsWith('http')) { if (uploadedUrl.startsWith('http')) {
+2 -1
View File
@@ -6,6 +6,7 @@ import 'plex_client.dart';
import '../models/plex_config.dart'; import '../models/plex_config.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/connection_constants.dart'; import '../utils/connection_constants.dart';
import '../utils/future_extensions.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'plex_auth_service.dart'; import 'plex_auth_service.dart';
import 'settings_service.dart'; import 'settings_service.dart';
@@ -207,7 +208,7 @@ class MultiServerManager {
try { try {
appLogger.d('Attempting connection to server: ${server.name}'); 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 // Store the client and server info
_clients[serverId]?.close(); _clients[serverId]?.close();
+2 -1
View File
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:saf_util/saf_util.dart'; import 'package:saf_util/saf_util.dart';
import '../utils/future_extensions.dart';
import '../utils/platform_detector.dart'; import '../utils/platform_detector.dart';
import 'package:saf_util/saf_util_platform_interface.dart'; import 'package:saf_util/saf_util_platform_interface.dart';
import 'package:saf_stream/saf_stream.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 // This is much more efficient for large files and avoids hangs
final result = await _safStream final result = await _safStream
.pasteLocalFile(sourceFilePath, targetDirectoryUri, fileName, mimeType, overwrite: true) .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}'); debugPrint('SAF copyFileToSaf: successfully copied to ${result.uri}');
return result.uri.toString(); return result.uri.toString();
+2 -1
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import '../mpv/mpv.dart'; import '../mpv/mpv.dart';
import '../models/plex_media_info.dart'; import '../models/plex_media_info.dart';
import '../utils/future_extensions.dart';
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../models/plex_user_profile.dart'; import '../models/plex_user_profile.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
@@ -686,7 +687,7 @@ class TrackSelectionService {
await player.streams.tracks await player.streams.tracks
.where((t) => t.audio.isNotEmpty || t.subtitle.isNotEmpty) .where((t) => t.audio.isNotEmpty || t.subtitle.isNotEmpty)
.first .first
.timeout(const Duration(seconds: 10)); .namedTimeout(const Duration(seconds: 10), operation: 'track loading');
} catch (_) { } catch (_) {
// Timeout or stream closed — proceed with whatever state we have // Timeout or stream closed — proceed with whatever state we have
} }
+6 -5
View File
@@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'app_logger.dart'; import 'app_logger.dart';
import 'future_extensions.dart';
import 'isolate_helper.dart'; import 'isolate_helper.dart';
import 'log_redaction_manager.dart'; import 'log_redaction_manager.dart';
import 'plex_http_exception.dart'; import 'plex_http_exception.dart';
@@ -145,11 +146,11 @@ class PlexHttpClient {
try { try {
final streamed = await _client final streamed = await _client
.send(request) .send(request)
.timeout(timeout ?? connectTimeout); .namedTimeout(timeout ?? connectTimeout, operation: 'GET ${uri.path} connect');
final bytes = await streamed.stream final bytes = await streamed.stream
.toBytes() .toBytes()
.timeout(timeout ?? receiveTimeout); .namedTimeout(timeout ?? receiveTimeout, operation: 'GET ${uri.path} receive');
sw.stop(); sw.stop();
_logResponse('GET', uri, streamed.statusCode, sw.elapsedMilliseconds); _logResponse('GET', uri, streamed.statusCode, sw.elapsedMilliseconds);
@@ -174,7 +175,7 @@ class PlexHttpClient {
try { try {
final streamed = await _client final streamed = await _client
.send(request) .send(request)
.timeout(timeout ?? connectTimeout); .namedTimeout(timeout ?? connectTimeout, operation: 'download ${uri.path} connect');
final file = File(filePath); final file = File(filePath);
final sink = file.openWrite(); final sink = file.openWrite();
@@ -231,12 +232,12 @@ class PlexHttpClient {
// Phase 1: send + receive headers (connect timeout) // Phase 1: send + receive headers (connect timeout)
final streamed = await _client final streamed = await _client
.send(request) .send(request)
.timeout(timeout ?? connectTimeout); .namedTimeout(timeout ?? connectTimeout, operation: '$method ${uri.path} connect');
// Phase 2: consume body (receive timeout) // Phase 2: consume body (receive timeout)
final bytes = await streamed.stream final bytes = await streamed.stream
.toBytes() .toBytes()
.timeout(timeout ?? receiveTimeout); .namedTimeout(timeout ?? receiveTimeout, operation: '$method ${uri.path} receive');
sw.stop(); sw.stop();
_logResponse(method, uri, streamed.statusCode, sw.elapsedMilliseconds); _logResponse(method, uri, streamed.statusCode, sw.elapsedMilliseconds);