feat: namedTimeout for Sentry context
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
Reference in New Issue
Block a user