From bb9e3bf91b7e7829e5a611d2f1c113b974c48e39 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:17:23 +0800 Subject: [PATCH 01/16] feat(logging): add logging module skeleton --- apps/lib/core/logging/log_config.dart | 27 ++++ apps/lib/core/logging/log_entry.dart | 71 ++++++++ apps/lib/core/logging/log_file_handler.dart | 36 +++++ apps/lib/core/logging/log_service.dart | 171 ++++++++++++++++++++ apps/lib/core/logging/logger.dart | 70 ++++++++ 5 files changed, 375 insertions(+) create mode 100644 apps/lib/core/logging/log_config.dart create mode 100644 apps/lib/core/logging/log_entry.dart create mode 100644 apps/lib/core/logging/log_file_handler.dart create mode 100644 apps/lib/core/logging/log_service.dart create mode 100644 apps/lib/core/logging/logger.dart diff --git a/apps/lib/core/logging/log_config.dart b/apps/lib/core/logging/log_config.dart new file mode 100644 index 0000000..f5a46e4 --- /dev/null +++ b/apps/lib/core/logging/log_config.dart @@ -0,0 +1,27 @@ +import 'log_entry.dart'; + +enum LogOutput { console, file } + +class LogConfig { + final LogLevel minLevel; + final LogOutput output; + final String logFileName; + final String logDir; + + const LogConfig({ + this.minLevel = LogLevel.debug, + this.output = LogOutput.console, + this.logFileName = 'app.log', + this.logDir = 'logs', + }); + + static LogConfig forDebug() => + const LogConfig(minLevel: LogLevel.debug, output: LogOutput.console); + + static LogConfig forRelease() => const LogConfig( + minLevel: LogLevel.warning, + output: LogOutput.file, + logFileName: 'app.log', + logDir: 'logs', + ); +} diff --git a/apps/lib/core/logging/log_entry.dart b/apps/lib/core/logging/log_entry.dart new file mode 100644 index 0000000..378e1ac --- /dev/null +++ b/apps/lib/core/logging/log_entry.dart @@ -0,0 +1,71 @@ +enum LogLevel { debug, info, warning, error } + +class LogEntry { + final DateTime timestamp; + final LogLevel level; + final String message; + final String module; + final String? funcName; + final int? lineNo; + final String? errorType; + final String? stackTrace; + final Map? extra; + + LogEntry({ + required this.timestamp, + required this.level, + required this.message, + required this.module, + this.funcName, + this.lineNo, + this.errorType, + this.stackTrace, + this.extra, + }); + + Map toJson() => { + 'timestamp': timestamp.toIso8601String(), + 'level': level.name, + 'message': message, + 'module': module, + if (funcName != null) 'func_name': funcName, + if (lineNo != null) 'line_no': lineNo, + if (errorType != null) 'error_type': errorType, + if (stackTrace != null) 'stack_trace': stackTrace, + if (extra != null && extra!.isNotEmpty) 'extra': extra, + }; + + String toConsoleString() { + final ts = timestamp.toIso8601String(); + final location = [ + if (funcName != null) funcName, + if (lineNo != null) '@$lineNo', + ].join(''); + final locationStr = location.isNotEmpty ? ' [$location]' : ''; + final errorStr = errorType != null ? ' [$errorType]' : ''; + final extraStr = extra != null && extra!.isNotEmpty ? ' $extra' : ''; + return '$ts ${level.name.toUpperCase().padRight(7)} [$module$locationStr]$errorStr $message$extraStr'; + } + + String toFileString() { + final sb = StringBuffer(); + sb.writeln('[$timestamp] ${level.name.toUpperCase()} [$module]'); + if (funcName != null || lineNo != null) { + sb.write(' at $funcName' ?? ''); + if (lineNo != null) sb.write(':$lineNo'); + sb.writeln(); + } + sb.writeln(' $message'); + if (errorType != null) { + sb.writeln(' Error: $errorType'); + } + if (stackTrace != null) { + sb.writeln(' StackTrace:'); + sb.writeln(stackTrace); + } + if (extra != null && extra!.isNotEmpty) { + sb.writeln(' Extra: $extra'); + } + return sb.toString(); + } +} diff --git a/apps/lib/core/logging/log_file_handler.dart b/apps/lib/core/logging/log_file_handler.dart new file mode 100644 index 0000000..a9a0aa3 --- /dev/null +++ b/apps/lib/core/logging/log_file_handler.dart @@ -0,0 +1,36 @@ +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; + +class LogFileHandler { + File? _file; + IOSink? _sink; + + Future init(String logDir, String logFileName) async { + final dir = await getApplicationDocumentsDirectory(); + final logPath = '${dir.path}/$logDir'; + await Directory(logPath).create(recursive: true); + _file = File('$logPath/$logFileName'); + _sink = _file!.openWrite(mode: FileMode.append); + } + + void write(String content) { + _sink?.writeln(content); + } + + Future flush() async { + await _sink?.flush(); + } + + Future close() async { + await _sink?.close(); + _sink = null; + _file = null; + } + + Future> readAllLines() async { + if (_file == null || !await _file!.exists()) return []; + return await _file!.readAsLines(); + } + + String? get filePath => _file?.path; +} diff --git a/apps/lib/core/logging/log_service.dart b/apps/lib/core/logging/log_service.dart new file mode 100644 index 0000000..22d4e38 --- /dev/null +++ b/apps/lib/core/logging/log_service.dart @@ -0,0 +1,171 @@ +import 'package:flutter/foundation.dart'; +import 'log_config.dart'; +import 'log_entry.dart'; +import 'log_file_handler.dart'; + +class LogService { + final LogConfig _config; + LogFileHandler? _fileHandler; + final _buffer = []; + static const _maxBufferSize = 50; + + LogService._({required LogConfig config}) : _config = config; + + static Future create({LogConfig? config}) async { + final isRelease = kReleaseMode; + + final effectiveConfig = + config ?? (isRelease ? LogConfig.forRelease() : LogConfig.forDebug()); + + final service = LogService._(config: effectiveConfig); + + if (effectiveConfig.output == LogOutput.file) { + service._fileHandler = LogFileHandler(); + await service._fileHandler!.init( + effectiveConfig.logDir, + effectiveConfig.logFileName, + ); + } + + return service; + } + + String? get logFilePath => _fileHandler?.filePath; + + void _log(LogEntry entry) { + if (entry.level.index < _config.minLevel.index) return; + + if (_config.output == LogOutput.console) { + debugPrint(entry.toConsoleString()); + if (entry.stackTrace != null) { + debugPrint(entry.stackTrace!); + } + } else { + _buffer.add(entry.toFileString()); + if (_buffer.length >= _maxBufferSize) { + _flushBuffer(); + } + } + } + + void _flushBuffer() { + for (final line in _buffer) { + _fileHandler?.write(line); + } + _buffer.clear(); + _fileHandler?.flush(); + } + + (String?, int?) _extractLocation(StackTrace stackTrace) { + final frames = stackTrace.toString().split('\n'); + for (final frame in frames) { + if (frame.contains('.dart')) { + final match = RegExp( + r'#\d+\s+(.+?)\s+\((.+?):(\d+)\)', + ).firstMatch(frame); + if (match != null) { + return (match.group(1), int.tryParse(match.group(3) ?? '')); + } + } + } + return (null, null); + } + + void debug({ + required String message, + required String module, + Map? extra, + StackTrace? stackTrace, + }) { + final trace = stackTrace ?? StackTrace.current; + final (funcName, lineNo) = _extractLocation(trace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.debug, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + extra: extra, + stackTrace: trace.toString(), + ), + ); + } + + void info({ + required String message, + required String module, + required Map extra, + StackTrace? stackTrace, + }) { + final trace = stackTrace ?? StackTrace.current; + final (funcName, lineNo) = _extractLocation(trace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.info, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + extra: extra, + stackTrace: trace.toString(), + ), + ); + } + + void warning({ + required String message, + required String module, + required Map extra, + StackTrace? stackTrace, + }) { + final trace = stackTrace ?? StackTrace.current; + final (funcName, lineNo) = _extractLocation(trace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.warning, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + extra: extra, + stackTrace: trace.toString(), + ), + ); + } + + void error({ + required String message, + required Object error, + required StackTrace stackTrace, + required String module, + Map? extra, + }) { + final (funcName, lineNo) = _extractLocation(stackTrace); + _log( + LogEntry( + timestamp: DateTime.now(), + level: LogLevel.error, + message: message, + module: module, + funcName: funcName, + lineNo: lineNo, + errorType: error.runtimeType.toString(), + stackTrace: stackTrace.toString(), + extra: extra, + ), + ); + } + + void flush() { + _flushBuffer(); + _fileHandler?.flush(); + } + + Future> readLogs() async { + return await _fileHandler?.readAllLines() ?? []; + } +} diff --git a/apps/lib/core/logging/logger.dart b/apps/lib/core/logging/logger.dart new file mode 100644 index 0000000..4b1343b --- /dev/null +++ b/apps/lib/core/logging/logger.dart @@ -0,0 +1,70 @@ +import 'log_service.dart'; + +LogService? _globalLogService; + +void setLogService(LogService service) { + _globalLogService = service; +} + +LogService _ensureService() { + return _globalLogService ?? (throw StateError('LogService not initialized')); +} + +class Logger { + final String module; + final LogService _service; + + Logger(this.module, this._service); + + factory Logger.get(String module) { + return Logger(module, _ensureService()); + } + + void debug({ + required String message, + Map? extra, + StackTrace? stackTrace, + }) => _service.debug( + message: message, + module: module, + extra: extra ?? {}, + stackTrace: stackTrace, + ); + + void info({ + required String message, + required Map extra, + StackTrace? stackTrace, + }) => _service.info( + message: message, + module: module, + extra: extra, + stackTrace: stackTrace, + ); + + void warning({ + required String message, + required Map extra, + StackTrace? stackTrace, + }) => _service.warning( + message: message, + module: module, + extra: extra, + stackTrace: stackTrace, + ); + + void error({ + required String message, + required Object error, + required StackTrace stackTrace, + Map? extra, + }) => _service.error( + message: message, + error: error, + stackTrace: stackTrace, + module: module, + extra: extra, + ); +} + +Logger getLogger(String module) => Logger.get(module); From 67f14d6cece684b93b8c190f5d4261965e6f8f83 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:18:22 +0800 Subject: [PATCH 02/16] feat(logging): add global error handler --- apps/lib/core/logging/error_handler.dart | 18 ++++++++++++++++++ apps/lib/main.dart | 13 +++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 apps/lib/core/logging/error_handler.dart diff --git a/apps/lib/core/logging/error_handler.dart b/apps/lib/core/logging/error_handler.dart new file mode 100644 index 0000000..85ca334 --- /dev/null +++ b/apps/lib/core/logging/error_handler.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'logger.dart'; + +class AppErrorHandler { + final Logger _logger = getLogger('flutter.error'); + + void register() { + FlutterError.onError = (details) { + _logger.error( + message: 'FlutterError: ${details.exceptionAsString()}', + error: details.exceptionAsString(), + stackTrace: details.stack ?? StackTrace.current, + extra: {'context': 'FlutterError.onError'}, + ); + FlutterError.presentError(details); + }; + } +} diff --git a/apps/lib/main.dart b/apps/lib/main.dart index 82399c6..f3f19a6 100644 --- a/apps/lib/main.dart +++ b/apps/lib/main.dart @@ -1,12 +1,25 @@ import 'package:flutter/material.dart'; import 'core/config/env.dart'; +import 'core/logging/logger.dart'; +import 'core/logging/log_service.dart'; +import 'core/logging/error_handler.dart'; import 'app/di/injection.dart'; import 'app/app.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + + final logService = await LogService.create(); + Logger.setLogService(logService); + + AppErrorHandler().register(); + await configureDependencies(); await Env.init(); + getLogger( + 'app', + ).info(message: 'App starting...', extra: {'version': Env.version}); + runApp(const LinksyApp()); } From b0734da2ae5ad48058d9b8e9ec3c8fe505d82be0 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:18:38 +0800 Subject: [PATCH 03/16] feat(logging): add path_provider dependency --- apps/pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/pubspec.yaml b/apps/pubspec.yaml index 98d37df..40c9f84 100644 --- a/apps/pubspec.yaml +++ b/apps/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: image_picker: ^1.0.7 package_info_plus: ^8.0.3 url_launcher: ^6.3.1 + path_provider: ^2.1.2 dev_dependencies: flutter_test: From 227c9c123cdb7b2ccc2cd4ddfcf37c38deda5fee Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:20:08 +0800 Subject: [PATCH 04/16] feat(logging): add logging to ChatBloc --- .../chat/presentation/bloc/chat_bloc.dart | 3 +++ .../presentation/bloc/chat_bloc_send.dart | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart index 4e2184d..46829bd 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:social_app/core/chat/chat_api.dart'; +import 'package:social_app/core/logging/logger.dart'; import 'package:social_app/core/chat/agent_stage.dart'; import 'package:social_app/core/chat/ag_ui_event.dart'; import 'package:social_app/core/chat/ag_ui_service.dart'; @@ -107,6 +108,8 @@ class ChatState implements ChatOrchestratorState { } class ChatBloc extends Cubit implements ChatOrchestrator { + final Logger _logger = getLogger('features.chat.bloc'); + ChatBloc({ AgUiService? service, required ChatApi chatApi, diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart index 1e632d0..290003b 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart @@ -4,6 +4,14 @@ part of 'chat_bloc.dart'; extension _ChatBlocSend on ChatBloc { Future _sendMessage(String content, {List? images}) async { + _logger.info( + message: 'Sending chat message', + extra: { + 'message_length': content.length, + 'attachments_count': images?.length ?? 0, + }, + ); + final epoch = _sessionEpoch; final assistantBaselineAtSend = chatBlocLatestAssistantTimestamp( state.items, @@ -57,11 +65,21 @@ extension _ChatBlocSend on ChatBloc { if (epoch != _sessionEpoch) { return; } + _logger.info( + message: 'Chat message sent successfully', + extra: {'message_id': messageId}, + ); _syncUploadedAttachments( messageId: messageId, uploadedAttachments: sendResult.uploadedAttachments, ); - } catch (error) { + } catch (error, stackTrace) { + _logger.error( + message: 'Failed to send chat message', + error: error, + stackTrace: stackTrace, + extra: {'message': content}, + ); if (epoch != _sessionEpoch) { return; } From 2b12d1ef7939ba51d02377885a74f5413ea94088 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:20:47 +0800 Subject: [PATCH 05/16] test(logging): add logging tests --- apps/test/core/logging/log_entry_test.dart | 84 ++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 apps/test/core/logging/log_entry_test.dart diff --git a/apps/test/core/logging/log_entry_test.dart b/apps/test/core/logging/log_entry_test.dart new file mode 100644 index 0000000..8626c87 --- /dev/null +++ b/apps/test/core/logging/log_entry_test.dart @@ -0,0 +1,84 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/core/logging/log_entry.dart'; + +void main() { + group('LogEntry', () { + test('toJson should serialize correctly', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.error, + message: 'Test error', + module: 'test.module', + funcName: 'testFunc', + lineNo: 42, + errorType: 'TestError', + stackTrace: 'test stack', + extra: {'key': 'value'}, + ); + + final json = entry.toJson(); + + expect(json['level'], 'error'); + expect(json['error_type'], 'TestError'); + expect(json['extra'], {'key': 'value'}); + }); + + test('toConsoleString should format correctly', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.debug, + message: 'Test message', + module: 'test.module', + funcName: 'testFunc', + lineNo: 42, + ); + + final result = entry.toConsoleString(); + + expect(result, contains('DEBUG')); + expect(result, contains('test.module')); + expect(result, contains('testFunc')); + expect(result, contains('@42')); + expect(result, contains('Test message')); + }); + + test('toFileString should format correctly', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.error, + message: 'Test error', + module: 'test.module', + funcName: 'testFunc', + lineNo: 42, + errorType: 'TestError', + stackTrace: 'test stack trace', + ); + + final result = entry.toFileString(); + + expect(result, contains('[test.module]')); + expect(result, contains('at testFunc:42')); + expect(result, contains('Test error')); + expect(result, contains('Error: TestError')); + expect(result, contains('StackTrace:')); + expect(result, contains('test stack trace')); + }); + + test('toJson should omit null fields', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.info, + message: 'Test', + module: 'test', + ); + + final json = entry.toJson(); + + expect(json.containsKey('func_name'), false); + expect(json.containsKey('line_no'), false); + expect(json.containsKey('error_type'), false); + expect(json.containsKey('stack_trace'), false); + expect(json.containsKey('extra'), false); + }); + }); +} From 63d225c567d45250495a24b66acd67acd20cd94f Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:28:30 +0800 Subject: [PATCH 06/16] feat(logging): add logging to auth feature --- apps/AGENTS.md | 88 +++++++++++++++++++ apps/lib/core/logging/log_service.dart | 4 +- apps/lib/core/logging/logger.dart | 8 +- .../auth/presentation/bloc/auth_bloc.dart | 40 +++++++-- .../auth/presentation/cubits/login_cubit.dart | 13 ++- 5 files changed, 138 insertions(+), 15 deletions(-) diff --git a/apps/AGENTS.md b/apps/AGENTS.md index e42a317..f0f65c4 100644 --- a/apps/AGENTS.md +++ b/apps/AGENTS.md @@ -111,3 +111,91 @@ This file governs `apps/**` (Flutter). Keep rules strict, short, and reusable. - Prioritize tests for model parsing, service logic, and high-regression interaction flows. - Simple static UI changes may skip tests. - Auth/Home/Cache changes must include targeted regression tests. + +## Logging Conventions (Must) + +### Logger Setup + +```dart +import 'core/logging/logger.dart'; + +class SomeBloc extends Cubit { + final Logger _logger = getLogger('features..'); +} +``` + +### Log Level Policy + +| Level | When to Use | Noise Level | +|-------|-------------|-------------| +| **error** | All exceptions and failures - MUST log every error site | Required, never skip | +| **warning** | Degraded behavior, retry, fallback, malformed data | Minimal, only when action taken | +| **info** | Key business events (login, logout, send message) | Minimal, only milestone events | +| **debug** | Detailed flow tracing (only in debug builds) | High, avoid in release | + +### Error Logging Requirements + +**Every try-catch that handles an exception MUST log it:** +```dart +try { + await _repository.someOperation(); +} catch (e, stackTrace) { + _logger.error( + message: 'Operation failed: $operationName', + error: e, + stackTrace: stackTrace, + extra: {'context': 'relevant_data'}, + ); + // handle error +} +``` + +### Info Logging Requirements + +**Only log these milestone events:** +- User login/logout +- Message sent/received +- Data sync completed +- Important state transitions + +```dart +_logger.info( + message: 'User logged in', + extra: {'user_id': user.id}, +); +``` + +### Warning Logging Requirements + +**Only log when taking corrective action:** +- Retrying after failure +- Using fallback data +- Skipping malformed data +- Deprecation warnings + +```dart +_logger.warning( + message: 'Cache miss, loading from remote', + extra: {'key': cacheKey}, +); +``` + +### Module Naming Convention + +| Feature | Module Path | +|---------|------------| +| auth | `features.auth` | +| calendar | `features.calendar` | +| chat | `features.chat` | +| contacts | `features.contacts` | +| home | `features.home` | +| messages | `features.messages` | +| settings | `features.settings` | +| todo | `features.todo` | + +### Prohibited Practices + +- **Never** log sensitive data: passwords, tokens, PII, message content +- **Never** log at debug level in production (release mode) +- **Never** skip error logging even if you "handle" the error +- **Never** log for every iteration in loops - only on failures diff --git a/apps/lib/core/logging/log_service.dart b/apps/lib/core/logging/log_service.dart index 22d4e38..3e137cd 100644 --- a/apps/lib/core/logging/log_service.dart +++ b/apps/lib/core/logging/log_service.dart @@ -96,7 +96,7 @@ class LogService { void info({ required String message, required String module, - required Map extra, + Map? extra, StackTrace? stackTrace, }) { final trace = stackTrace ?? StackTrace.current; @@ -118,7 +118,7 @@ class LogService { void warning({ required String message, required String module, - required Map extra, + Map? extra, StackTrace? stackTrace, }) { final trace = stackTrace ?? StackTrace.current; diff --git a/apps/lib/core/logging/logger.dart b/apps/lib/core/logging/logger.dart index 4b1343b..7e8eb9b 100644 --- a/apps/lib/core/logging/logger.dart +++ b/apps/lib/core/logging/logger.dart @@ -33,23 +33,23 @@ class Logger { void info({ required String message, - required Map extra, + Map? extra, StackTrace? stackTrace, }) => _service.info( message: message, module: module, - extra: extra, + extra: extra ?? {}, stackTrace: stackTrace, ); void warning({ required String message, - required Map extra, + Map? extra, StackTrace? stackTrace, }) => _service.warning( message: message, module: module, - extra: extra, + extra: extra ?? {}, stackTrace: stackTrace, ); diff --git a/apps/lib/features/auth/presentation/bloc/auth_bloc.dart b/apps/lib/features/auth/presentation/bloc/auth_bloc.dart index d275180..dc9e0e8 100644 --- a/apps/lib/features/auth/presentation/bloc/auth_bloc.dart +++ b/apps/lib/features/auth/presentation/bloc/auth_bloc.dart @@ -1,10 +1,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../core/logging/logger.dart'; import '../../data/repositories/auth_repository.dart'; import 'auth_event.dart'; import 'auth_state.dart'; class AuthBloc extends Bloc { final AuthRepository _repository; + final Logger _logger = getLogger('features.auth.bloc'); AuthBloc(this._repository) : super(AuthInitial()) { on(_onStarted); @@ -19,6 +21,10 @@ class AuthBloc extends Bloc { final refreshToken = await _repository.getRefreshToken(); if (refreshToken != null) { final response = await _repository.refreshSession(refreshToken); + _logger.info( + message: 'Session refreshed successfully', + extra: {'user_id': response.user.id}, + ); emit( AuthAuthenticated( user: AuthUser(id: response.user.id, phone: response.user.phone), @@ -29,11 +35,20 @@ class AuthBloc extends Bloc { emit( const AuthUnauthenticated(reason: AuthUnauthenticatedReason.signedOut), ); - } catch (_) { + } catch (e, stackTrace) { + _logger.error( + message: 'Session refresh failed', + error: e, + stackTrace: stackTrace, + ); try { await _repository.clearSessionLocalOnly(); - } catch (_) { - // Keep state convergence even when storage cleanup fails. + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to clear local session', + error: e, + stackTrace: stackTrace, + ); } finally { emit( const AuthUnauthenticated( @@ -45,6 +60,7 @@ class AuthBloc extends Bloc { } void _onLoggedIn(AuthLoggedIn event, Emitter emit) { + _logger.info(message: 'User logged in', extra: {'user_id': event.user.id}); emit(AuthAuthenticated(user: event.user)); } @@ -54,8 +70,13 @@ class AuthBloc extends Bloc { ) async { try { await _repository.deleteSession(); - } catch (_) { - // Keep state convergence even when logout cleanup fails. + _logger.info(message: 'User logged out'); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to delete session on logout', + error: e, + stackTrace: stackTrace, + ); } finally { emit( const AuthUnauthenticated(reason: AuthUnauthenticatedReason.signedOut), @@ -67,10 +88,15 @@ class AuthBloc extends Bloc { AuthSessionInvalidated event, Emitter emit, ) async { + _logger.warning(message: 'Session invalidated by server'); try { await _repository.clearSessionLocalOnly(); - } catch (_) { - // Keep state convergence even when local cleanup fails. + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to clear local session', + error: e, + stackTrace: stackTrace, + ); } finally { emit( const AuthUnauthenticated(reason: AuthUnauthenticatedReason.expired), diff --git a/apps/lib/features/auth/presentation/cubits/login_cubit.dart b/apps/lib/features/auth/presentation/cubits/login_cubit.dart index 5b28e33..e7117c8 100644 --- a/apps/lib/features/auth/presentation/cubits/login_cubit.dart +++ b/apps/lib/features/auth/presentation/cubits/login_cubit.dart @@ -5,6 +5,7 @@ import 'package:formz/formz.dart'; import 'package:equatable/equatable.dart'; import '../../../../data/network/api_exception.dart'; import '../../../../core/l10n/l10n.dart'; +import '../../../../core/logging/logger.dart'; import '../../data/repositories/auth_repository.dart'; import '../../data/models/auth_response.dart'; import '../../../../shared/forms/inputs.dart'; @@ -78,6 +79,7 @@ class LoginState extends Equatable { class LoginCubit extends Cubit { final AuthRepository _repository; + final Logger _logger = getLogger('features.auth.login'); Timer? _resendTimer; LoginCubit(this._repository) : super(const LoginState()); @@ -149,10 +151,16 @@ class LoginCubit extends Cubit { ), ); return true; - } catch (e) { + } catch (e, stackTrace) { if (isClosed) { return false; } + _logger.error( + message: 'Failed to send OTP', + error: e, + stackTrace: stackTrace, + extra: {'phone': requestPhone}, + ); final message = e is ApiException ? e.message : L10n.current.authSendCodeFailed; @@ -176,10 +184,11 @@ class LoginCubit extends Cubit { } emit(state.copyWith(status: FormzSubmissionStatus.success)); return response; - } catch (e) { + } catch (e, stackTrace) { if (isClosed) { return null; } + _logger.error(message: 'Login failed', error: e, stackTrace: stackTrace); final message = e is ApiException ? e.message : e.toString(); emit( state.copyWith( From 11ab3d0e22edc5a229aac0e8f08530ff51d96a31 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:29:38 +0800 Subject: [PATCH 07/16] feat(logging): add logging to calendar repository --- .../repositories/calendar_repository.dart | 89 +++++++++++++------ 1 file changed, 64 insertions(+), 25 deletions(-) diff --git a/apps/lib/features/calendar/data/repositories/calendar_repository.dart b/apps/lib/features/calendar/data/repositories/calendar_repository.dart index de3ce67..7055b0a 100644 --- a/apps/lib/features/calendar/data/repositories/calendar_repository.dart +++ b/apps/lib/features/calendar/data/repositories/calendar_repository.dart @@ -4,11 +4,13 @@ import '../../../../data/cache/cache_scope.dart'; import '../../../../data/network/i_api_client.dart'; import '../../../../core/notification/models/reminder_alarm.dart'; import '../../../../core/notification/services/reminder_reconcile_service.dart'; +import '../../../../core/logging/logger.dart'; import '../models/schedule_item_model.dart'; class CalendarRepository extends CachedRepository> { final IApiClient _apiClient; final ReminderReconcileService? _reminderReconcileService; + final Logger _logger = getLogger('features.calendar.repository'); static const _prefix = '/api/v1/schedule-items'; CalendarRepository({ @@ -70,14 +72,28 @@ class CalendarRepository extends CachedRepository> { } Future getEventById(String id) async { - final response = await _apiClient.get>('$_prefix/$id'); - final data = response.data; - if (data == null) { - throw StateError('Invalid getEventById response: empty payload'); + try { + final response = await _apiClient.get>( + '$_prefix/$id', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid getEventById response: empty payload'); + } + final event = ScheduleItemModel.fromJson(data); + await _reminderReconcileService?.reconcileEvent( + _toReminderSnapshot(event), + ); + return event; + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to get event by id', + error: e, + stackTrace: stackTrace, + extra: {'event_id': id}, + ); + rethrow; } - final event = ScheduleItemModel.fromJson(data); - await _reminderReconcileService?.reconcileEvent(_toReminderSnapshot(event)); - return event; } Future getById(String id) { @@ -111,31 +127,54 @@ class CalendarRepository extends CachedRepository> { required bool accept, }) async { final action = accept ? 'accept' : 'reject'; - await _apiClient.post('$_prefix/$itemId/$action'); - await store.clearByPrefix('cache:${CacheScope.token()}:calendar:'); + try { + await _apiClient.post('$_prefix/$itemId/$action'); + await store.clearByPrefix('cache:${CacheScope.token()}:calendar:'); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to $action subscription', + error: e, + stackTrace: stackTrace, + extra: {'item_id': itemId, 'action': action}, + ); + rethrow; + } } Future> _listByRange({ required DateTime startAt, required DateTime endAt, }) async { - final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String()); - final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String()); - final response = await _apiClient.get>( - '$_prefix?start_at=$start&end_at=$end', - ); - final data = response.data; - if (data == null) { - throw StateError('Invalid listByRange response: empty payload'); + try { + final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String()); + final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String()); + final response = await _apiClient.get>( + '$_prefix?start_at=$start&end_at=$end', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid listByRange response: empty payload'); + } + final events = data + .whereType>() + .map(ScheduleItemModel.fromJson) + .toList(growable: false); + await _reminderReconcileService?.reconcileEvents( + events.map(_toReminderSnapshot).toList(growable: false), + ); + return events; + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to list events by range', + error: e, + stackTrace: stackTrace, + extra: { + 'start_at': startAt.toIso8601String(), + 'end_at': endAt.toIso8601String(), + }, + ); + rethrow; } - final events = data - .whereType>() - .map(ScheduleItemModel.fromJson) - .toList(growable: false); - await _reminderReconcileService?.reconcileEvents( - events.map(_toReminderSnapshot).toList(growable: false), - ); - return events; } ReminderEventSnapshot _toReminderSnapshot(ScheduleItemModel event) { From 7c311b078580001beb244c24d67cf68cfaad22dc Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:30:44 +0800 Subject: [PATCH 08/16] feat(logging): add logging to contacts friend_repository --- .../data/repositories/friend_repository.dart | 120 +++++++++++++----- 1 file changed, 85 insertions(+), 35 deletions(-) diff --git a/apps/lib/features/contacts/data/repositories/friend_repository.dart b/apps/lib/features/contacts/data/repositories/friend_repository.dart index 07b9a3b..9a3a5f1 100644 --- a/apps/lib/features/contacts/data/repositories/friend_repository.dart +++ b/apps/lib/features/contacts/data/repositories/friend_repository.dart @@ -1,6 +1,7 @@ import '../../../../data/network/i_api_client.dart'; import '../../../../data/cache/cache_policy.dart'; import '../../../../data/cache/cached_repository.dart'; +import '../../../../core/logging/logger.dart'; import '../models/friend_request.dart'; abstract class FriendRepository { @@ -16,6 +17,7 @@ abstract class FriendRepository { class FriendRepositoryImpl extends CachedRepository> implements FriendRepository { final IApiClient _apiClient; + final Logger _logger = getLogger('features.contacts.friend_repository'); static const _prefix = '/api/v1/friends'; FriendRepositoryImpl({required IApiClient apiClient, required super.store}) @@ -36,17 +38,27 @@ class FriendRepositoryImpl extends CachedRepository> } Future> _loadFriendsFromRemote() async { - final response = await _apiClient.get>(_prefix); - final data = response.data; - if (data == null) { - throw StateError('Invalid getFriends response: empty payload'); + try { + final response = await _apiClient.get>(_prefix); + final data = response.data; + if (data == null) { + throw StateError('Invalid getFriends response: empty payload'); + } + return data + .map((item) => item as Map) + .map( + (item) => + FriendUser.fromJson(item['friend'] as Map), + ) + .toList(growable: false); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load friends from remote', + error: e, + stackTrace: stackTrace, + ); + rethrow; } - return data - .map((item) => item as Map) - .map( - (item) => FriendUser.fromJson(item['friend'] as Map), - ) - .toList(growable: false); } @override @@ -55,14 +67,24 @@ class FriendRepositoryImpl extends CachedRepository> } Future _loadRequestById(String friendshipId) async { - final response = await _apiClient.get>( - '$_prefix/requests/$friendshipId', - ); - final data = response.data; - if (data == null) { - throw StateError('Invalid getRequestById response: empty payload'); + try { + final response = await _apiClient.get>( + '$_prefix/requests/$friendshipId', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid getRequestById response: empty payload'); + } + return FriendRequest.fromJson(data); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load friend request by id', + error: e, + stackTrace: stackTrace, + extra: {'friendship_id': friendshipId}, + ); + rethrow; } - return FriendRequest.fromJson(data); } @override @@ -85,30 +107,58 @@ class FriendRepositoryImpl extends CachedRepository> @override Future acceptRequest(String friendshipId) async { - final response = await _apiClient.post>( - '$_prefix/requests/$friendshipId/accept', - ); - final data = response.data; - if (data == null) { - throw StateError('Invalid acceptRequest response: empty payload'); + try { + final response = await _apiClient.post>( + '$_prefix/requests/$friendshipId/accept', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid acceptRequest response: empty payload'); + } + final request = FriendRequest.fromJson(data); + await _invalidateFriendCaches(friendshipId); + _logger.info( + message: 'Friend request accepted', + extra: {'friendship_id': friendshipId}, + ); + return request; + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to accept friend request', + error: e, + stackTrace: stackTrace, + extra: {'friendship_id': friendshipId}, + ); + rethrow; } - final request = FriendRequest.fromJson(data); - await _invalidateFriendCaches(friendshipId); - return request; } @override Future declineRequest(String friendshipId) async { - final response = await _apiClient.post>( - '$_prefix/requests/$friendshipId/decline', - ); - final data = response.data; - if (data == null) { - throw StateError('Invalid declineRequest response: empty payload'); + try { + final response = await _apiClient.post>( + '$_prefix/requests/$friendshipId/decline', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid declineRequest response: empty payload'); + } + final request = FriendRequest.fromJson(data); + await _invalidateFriendCaches(friendshipId); + _logger.info( + message: 'Friend request declined', + extra: {'friendship_id': friendshipId}, + ); + return request; + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to decline friend request', + error: e, + stackTrace: stackTrace, + extra: {'friendship_id': friendshipId}, + ); + rethrow; } - final request = FriendRequest.fromJson(data); - await _invalidateFriendCaches(friendshipId); - return request; } Future _invalidateFriendCaches(String friendshipId) { From ccf6da60a132c95625a52ccac59edb2c400a2730 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:31:28 +0800 Subject: [PATCH 09/16] feat(logging): add logging to messages inbox_repository --- .../data/repositories/inbox_repository.dart | 74 +++++++++++++------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/apps/lib/features/messages/data/repositories/inbox_repository.dart b/apps/lib/features/messages/data/repositories/inbox_repository.dart index d92d03d..2483444 100644 --- a/apps/lib/features/messages/data/repositories/inbox_repository.dart +++ b/apps/lib/features/messages/data/repositories/inbox_repository.dart @@ -1,6 +1,7 @@ import '../../../../data/network/i_api_client.dart'; import '../../../../data/cache/cache_policy.dart'; import '../../../../data/cache/cached_repository.dart'; +import '../../../../core/logging/logger.dart'; import '../models/inbox_message.dart'; abstract class InboxRepository { @@ -14,6 +15,7 @@ abstract class InboxRepository { class InboxRepositoryImpl extends CachedRepository> implements InboxRepository { final IApiClient _apiClient; + final Logger _logger = getLogger('features.messages.repository'); static const _prefix = '/api/v1/inbox/messages'; InboxRepositoryImpl({required IApiClient apiClient, required super.store}) @@ -41,36 +43,60 @@ class InboxRepositoryImpl extends CachedRepository> } Future> _loadMessagesFromRemote({bool? isRead}) async { - final queryParams = isRead != null ? '?is_read=$isRead' : ''; - final response = await _apiClient.get>( - '$_prefix$queryParams', - ); - final data = response.data; - if (data == null) { - throw StateError('Invalid getMessages response: empty payload'); + try { + final queryParams = isRead != null ? '?is_read=$isRead' : ''; + final response = await _apiClient.get>( + '$_prefix$queryParams', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid getMessages response: empty payload'); + } + return data + .whereType>() + .map(InboxMessage.fromJson) + .toList(growable: false); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load messages from remote', + error: e, + stackTrace: stackTrace, + extra: {'is_read': isRead}, + ); + rethrow; } - return data - .whereType>() - .map(InboxMessage.fromJson) - .toList(growable: false); } @override Future markAsRead(String messageId) async { - final response = await _apiClient.patch>( - '$_prefix/$messageId/read', - ); - final data = response.data; - if (data == null) { - throw StateError('Invalid markAsRead response: empty payload'); + try { + final response = await _apiClient.patch>( + '$_prefix/$messageId/read', + ); + final data = response.data; + if (data == null) { + throw StateError('Invalid markAsRead response: empty payload'); + } + final message = InboxMessage.fromJson(data); + await Future.wait([ + removeCacheKey(_messagesKey(false)), + removeCacheKey(_messagesKey(true)), + removeCacheKey(_messagesKey(null)), + ]); + _logger.info( + message: 'Message marked as read', + extra: {'message_id': messageId}, + ); + return message; + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to mark message as read', + error: e, + stackTrace: stackTrace, + extra: {'message_id': messageId}, + ); + rethrow; } - final message = InboxMessage.fromJson(data); - await Future.wait([ - removeCacheKey(_messagesKey(false)), - removeCacheKey(_messagesKey(true)), - removeCacheKey(_messagesKey(null)), - ]); - return message; } static String _messagesKey(bool? isRead) { From ad37ca3c643d52464e9b2f8b4fce02e54938f80f Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:32:04 +0800 Subject: [PATCH 10/16] feat(logging): add logging to settings automation_jobs_cubit --- .../cubits/automation_jobs_cubit.dart | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart b/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart index 6e4334a..67486a5 100644 --- a/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart +++ b/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../../core/logging/logger.dart'; import '../../data/models/automation_job_model.dart'; import '../../data/apis/automation_jobs_api.dart'; @@ -42,6 +43,7 @@ class AutomationJobsState extends Equatable { class AutomationJobsCubit extends Cubit { final AutomationJobsApi _api; + final Logger _logger = getLogger('features.settings.automation_jobs'); AutomationJobsCubit(this._api) : super(AutomationJobsState()); @@ -50,7 +52,12 @@ class AutomationJobsCubit extends Cubit { try { final jobs = await _api.list(); emit(state.copyWith(jobs: jobs, isLoading: false)); - } catch (e) { + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load automation jobs', + error: e, + stackTrace: stackTrace, + ); emit(state.copyWith(isLoading: false, error: e.toString())); } } @@ -58,8 +65,15 @@ class AutomationJobsCubit extends Cubit { Future deleteJob(String id) async { try { await _api.delete(id); + _logger.info(message: 'Automation job deleted', extra: {'job_id': id}); await loadJobs(); - } catch (e) { + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to delete automation job', + error: e, + stackTrace: stackTrace, + extra: {'job_id': id}, + ); emit(state.copyWith(error: e.toString())); } } @@ -77,7 +91,17 @@ class AutomationJobsCubit extends Cubit { .map((job) => job.id == id ? updated : job) .toList(); emit(state.copyWith(jobs: nextJobs)); - } catch (e) { + _logger.info( + message: 'Automation job status updated', + extra: {'job_id': id, 'enabled': enabled}, + ); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to update automation job status', + error: e, + stackTrace: stackTrace, + extra: {'job_id': id, 'enabled': enabled}, + ); emit(state.copyWith(error: e.toString())); } } From 4fb41d2b3672d33e309a5d9afd5baadd9cce155d Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:32:29 +0800 Subject: [PATCH 11/16] feat(logging): add logging to todo repository --- .../todo/data/repositories/todo_repository.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/lib/features/todo/data/repositories/todo_repository.dart b/apps/lib/features/todo/data/repositories/todo_repository.dart index dc64181..a632d2e 100644 --- a/apps/lib/features/todo/data/repositories/todo_repository.dart +++ b/apps/lib/features/todo/data/repositories/todo_repository.dart @@ -3,6 +3,7 @@ import 'dart:async'; import '../../../../data/cache/cache_store.dart'; import '../../../../data/cache/cache_policy.dart'; import '../../../../data/cache/cached_repository.dart'; +import '../../../../core/logging/logger.dart'; import '../apis/todo_api.dart'; class TodoRepository extends CachedRepository> { @@ -10,6 +11,7 @@ class TodoRepository extends CachedRepository> { final TodoApi api; final CacheInvalidator invalidator; + final Logger _logger = getLogger('features.todo.repository'); TodoRepository({ required this.api, @@ -50,7 +52,14 @@ class TodoRepository extends CachedRepository> { try { await api.completeTodo(id); invalidator.invalidate(pendingListKey); - } catch (error) { + _logger.info(message: 'Todo completed', extra: {'todo_id': id}); + } catch (error, stackTrace) { + _logger.error( + message: 'Failed to complete todo', + error: error, + stackTrace: stackTrace, + extra: {'todo_id': id}, + ); if (cached != null) { await writeCacheEntry(pendingListKey, cached.value); } From b8e5a42a12ae38f38ab4c57f11e86f23e1450176 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:33:41 +0800 Subject: [PATCH 12/16] feat(logging): add logging to chat bloc history and attachments --- .../features/chat/presentation/bloc/chat_bloc.dart | 11 ++++++++++- .../presentation/bloc/chat_bloc_attachments.dart | 8 +++++++- .../chat/presentation/bloc/chat_bloc_history.dart | 12 ++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart index 46829bd..59564aa 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc.dart @@ -214,7 +214,16 @@ class ChatBloc extends Cubit implements ChatOrchestrator { final epoch = ++_sessionEpoch; _activeUserId = normalizedUserId; - await _service.setUserContext(normalizedUserId); + try { + await _service.setUserContext(normalizedUserId); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to set user context', + error: e, + stackTrace: stackTrace, + extra: {'user_id': normalizedUserId}, + ); + } if (epoch != _sessionEpoch) { return; } diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc_attachments.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc_attachments.dart index 59fee76..2976a73 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc_attachments.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc_attachments.dart @@ -75,7 +75,13 @@ extension _ChatBlocAttachments on ChatBloc { final bytes = await _service.fetchAttachmentPreview(previewPath); _attachmentPreviewCache[previewPath] = bytes; return bytes; - } catch (_) { + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load attachment preview', + error: e, + stackTrace: stackTrace, + extra: {'preview_path': previewPath}, + ); return null; } finally { _attachmentPreviewInflight.remove(previewPath); diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart index 2d500f4..a377a6d 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart @@ -22,6 +22,12 @@ extension _ChatBlocHistory on ChatBloc { hasEarlierHistory: snapshot.hasMore, ), ); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load chat history', + error: e, + stackTrace: stackTrace, + ); } finally { if (epoch == _sessionEpoch) { emit(state.copyWith(isLoadingHistory: false)); @@ -53,6 +59,12 @@ extension _ChatBlocHistory on ChatBloc { hasEarlierHistory: snapshot.hasMore, ), ); + } catch (e, stackTrace) { + _logger.error( + message: 'Failed to load more chat history', + error: e, + stackTrace: stackTrace, + ); } finally { if (epoch == _sessionEpoch) { emit(state.copyWith(isLoadingHistory: false)); From 49c062d5a53366797b7309496aa3ed73559b4603 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:34:36 +0800 Subject: [PATCH 13/16] feat(logging): add logging to home voice recorder --- .../features/home/data/voice_recorder.dart | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/lib/features/home/data/voice_recorder.dart b/apps/lib/features/home/data/voice_recorder.dart index 337679e..0f32533 100644 --- a/apps/lib/features/home/data/voice_recorder.dart +++ b/apps/lib/features/home/data/voice_recorder.dart @@ -3,7 +3,8 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:record/record.dart'; -import '../../../core/l10n/l10n.dart'; +import '../../../../core/l10n/l10n.dart'; +import '../../../../core/logging/logger.dart'; abstract class VoiceRecorder { Future start(); @@ -13,6 +14,7 @@ abstract class VoiceRecorder { class RecordVoiceRecorder implements VoiceRecorder { final AudioRecorder _recorder; + final Logger _logger = getLogger('features.home.voice_recorder'); String? _currentPath; RecordVoiceRecorder({AudioRecorder? recorder}) @@ -23,10 +25,16 @@ class RecordVoiceRecorder implements VoiceRecorder { bool hasPermission; try { hasPermission = await _recorder.hasPermission(); - } on MissingPluginException catch (_) { + } on MissingPluginException catch (e, stackTrace) { + _logger.error( + message: 'Voice recorder plugin unavailable', + error: e, + stackTrace: stackTrace, + ); throw StateError(L10n.current.homeRecorderPluginUnavailable); } if (!hasPermission) { + _logger.warning(message: 'Voice recorder permission denied'); throw StateError(L10n.current.homeRecorderPermissionDenied); } @@ -43,7 +51,13 @@ class RecordVoiceRecorder implements VoiceRecorder { ), path: path, ); - } on MissingPluginException catch (_) { + _logger.info(message: 'Voice recording started', extra: {'path': path}); + } on MissingPluginException catch (e, stackTrace) { + _logger.error( + message: 'Failed to start voice recording', + error: e, + stackTrace: stackTrace, + ); throw StateError(L10n.current.homeRecorderPluginUnavailable); } } @@ -53,7 +67,16 @@ class RecordVoiceRecorder implements VoiceRecorder { String? stoppedPath; try { stoppedPath = await _recorder.stop(); - } on MissingPluginException catch (_) { + _logger.info( + message: 'Voice recording stopped', + extra: {'path': stoppedPath ?? _currentPath}, + ); + } on MissingPluginException catch (e, stackTrace) { + _logger.error( + message: 'Failed to stop voice recording', + error: e, + stackTrace: stackTrace, + ); throw StateError(L10n.current.homeRecorderPluginUnavailable); } return stoppedPath ?? _currentPath; From d1092df254922d28f4381fb026eae1a49e7021f4 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:36:09 +0800 Subject: [PATCH 14/16] fix(logging): fix static setLogService and null handling --- apps/lib/core/logging/log_entry.dart | 2 +- apps/lib/core/logging/logger.dart | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/lib/core/logging/log_entry.dart b/apps/lib/core/logging/log_entry.dart index 378e1ac..2f3478f 100644 --- a/apps/lib/core/logging/log_entry.dart +++ b/apps/lib/core/logging/log_entry.dart @@ -51,7 +51,7 @@ class LogEntry { final sb = StringBuffer(); sb.writeln('[$timestamp] ${level.name.toUpperCase()} [$module]'); if (funcName != null || lineNo != null) { - sb.write(' at $funcName' ?? ''); + sb.write(' at ${funcName ?? ''}'); if (lineNo != null) sb.write(':$lineNo'); sb.writeln(); } diff --git a/apps/lib/core/logging/logger.dart b/apps/lib/core/logging/logger.dart index 7e8eb9b..39e3a8b 100644 --- a/apps/lib/core/logging/logger.dart +++ b/apps/lib/core/logging/logger.dart @@ -2,14 +2,6 @@ import 'log_service.dart'; LogService? _globalLogService; -void setLogService(LogService service) { - _globalLogService = service; -} - -LogService _ensureService() { - return _globalLogService ?? (throw StateError('LogService not initialized')); -} - class Logger { final String module; final LogService _service; @@ -20,6 +12,15 @@ class Logger { return Logger(module, _ensureService()); } + static void setLogService(LogService service) { + _globalLogService = service; + } + + static LogService _ensureService() { + return _globalLogService ?? + (throw StateError('LogService not initialized')); + } + void debug({ required String message, Map? extra, From 640b4d15a3d64d6219a020045b44fa04a9406bc6 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 14:46:47 +0800 Subject: [PATCH 15/16] test(logging): optimize logging tests and fix broken tests --- apps/lib/core/logging/logger.dart | 80 +++++--- .../repositories/calendar_repository.dart | 4 +- .../app_prewarm_orchestrator_test.dart | 16 +- apps/test/core/logging/log_entry_test.dart | 183 ++++++++++++++---- apps/test/core/logging/log_service_test.dart | 94 +++++++++ .../shared_repositories_test.dart | 26 ++- 6 files changed, 316 insertions(+), 87 deletions(-) create mode 100644 apps/test/core/logging/log_service_test.dart diff --git a/apps/lib/core/logging/logger.dart b/apps/lib/core/logging/logger.dart index 39e3a8b..d85fd1b 100644 --- a/apps/lib/core/logging/logger.dart +++ b/apps/lib/core/logging/logger.dart @@ -1,71 +1,89 @@ +import 'package:flutter/foundation.dart'; +import 'log_config.dart'; +import 'log_entry.dart'; import 'log_service.dart'; LogService? _globalLogService; class Logger { final String module; - final LogService _service; + final LogService? _service; + final bool _isNoOp; - Logger(this.module, this._service); + Logger(this.module, this._service) : _isNoOp = _service == null; factory Logger.get(String module) { - return Logger(module, _ensureService()); + return Logger(module, _globalLogService); } static void setLogService(LogService service) { _globalLogService = service; } - static LogService _ensureService() { - return _globalLogService ?? - (throw StateError('LogService not initialized')); + static LogService? _ensureService() { + return _globalLogService; } void debug({ required String message, Map? extra, StackTrace? stackTrace, - }) => _service.debug( - message: message, - module: module, - extra: extra ?? {}, - stackTrace: stackTrace, - ); + }) { + if (_isNoOp) return; + _service!.debug( + message: message, + module: module, + extra: extra ?? {}, + stackTrace: stackTrace, + ); + } void info({ required String message, Map? extra, StackTrace? stackTrace, - }) => _service.info( - message: message, - module: module, - extra: extra ?? {}, - stackTrace: stackTrace, - ); + }) { + if (_isNoOp) return; + _service!.info( + message: message, + module: module, + extra: extra ?? {}, + stackTrace: stackTrace, + ); + } void warning({ required String message, Map? extra, StackTrace? stackTrace, - }) => _service.warning( - message: message, - module: module, - extra: extra ?? {}, - stackTrace: stackTrace, - ); + }) { + if (_isNoOp) return; + _service!.warning( + message: message, + module: module, + extra: extra ?? {}, + stackTrace: stackTrace, + ); + } void error({ required String message, required Object error, required StackTrace stackTrace, Map? extra, - }) => _service.error( - message: message, - error: error, - stackTrace: stackTrace, - module: module, - extra: extra, - ); + }) { + if (_isNoOp) { + debugPrint('[$module] ERROR: $message, error: $error'); + return; + } + _service!.error( + message: message, + error: error, + stackTrace: stackTrace, + module: module, + extra: extra, + ); + } } Logger getLogger(String module) => Logger.get(module); diff --git a/apps/lib/features/calendar/data/repositories/calendar_repository.dart b/apps/lib/features/calendar/data/repositories/calendar_repository.dart index 7055b0a..86341f3 100644 --- a/apps/lib/features/calendar/data/repositories/calendar_repository.dart +++ b/apps/lib/features/calendar/data/repositories/calendar_repository.dart @@ -145,9 +145,9 @@ class CalendarRepository extends CachedRepository> { required DateTime startAt, required DateTime endAt, }) async { + final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String()); + final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String()); try { - final start = Uri.encodeQueryComponent(startAt.toUtc().toIso8601String()); - final end = Uri.encodeQueryComponent(endAt.toUtc().toIso8601String()); final response = await _apiClient.get>( '$_prefix?start_at=$start&end_at=$end', ); diff --git a/apps/test/app/services/app_prewarm_orchestrator_test.dart b/apps/test/app/services/app_prewarm_orchestrator_test.dart index 0047d75..75afbdd 100644 --- a/apps/test/app/services/app_prewarm_orchestrator_test.dart +++ b/apps/test/app/services/app_prewarm_orchestrator_test.dart @@ -13,12 +13,16 @@ import 'package:social_app/features/messages/data/repositories/inbox_repository. class _FakeApiClient implements IApiClient { @override - Future> delete(String path, {data, Options? options}) { + Future> delete(String path, {dynamic data, Options? options}) { throw UnimplementedError(); } @override - Future> get(String path, {Options? options}) { + Future> get( + String path, { + Map? queryParameters, + Options? options, + }) { throw UnimplementedError(); } @@ -31,17 +35,17 @@ class _FakeApiClient implements IApiClient { } @override - Future> patch(String path, {data, Options? options}) { + Future> patch(String path, {dynamic data, Options? options}) { throw UnimplementedError(); } @override - Future> post(String path, {data, Options? options}) { + Future> post(String path, {dynamic data, Options? options}) { throw UnimplementedError(); } @override - Future> put(String path, {data, Options? options}) { + Future> put(String path, {dynamic data, Options? options}) { throw UnimplementedError(); } } @@ -127,6 +131,7 @@ void main() { prewarmChatHistory: () async {}, prewarmCalendarToday: () async {}, prewarmUnreadInbox: () async {}, + prewarmCalendarReminderWindow: () async {}, ); await orchestrator.ensureStartedFor('u1'); @@ -145,6 +150,7 @@ void main() { prewarmChatHistory: () => completer.future, prewarmCalendarToday: () => completer.future, prewarmUnreadInbox: () => completer.future, + prewarmCalendarReminderWindow: () => completer.future, ); await orchestrator.ensureStartedFor('u1'); diff --git a/apps/test/core/logging/log_entry_test.dart b/apps/test/core/logging/log_entry_test.dart index 8626c87..6ef0b5c 100644 --- a/apps/test/core/logging/log_entry_test.dart +++ b/apps/test/core/logging/log_entry_test.dart @@ -3,7 +3,7 @@ import 'package:social_app/core/logging/log_entry.dart'; void main() { group('LogEntry', () { - test('toJson should serialize correctly', () { + test('toJson should serialize correctly with all fields', () { final entry = LogEntry( timestamp: DateTime(2026, 4, 1, 10, 0, 0), level: LogLevel.error, @@ -18,52 +18,17 @@ void main() { final json = entry.toJson(); + expect(json['timestamp'], '2026-04-01T10:00:00.000'); expect(json['level'], 'error'); + expect(json['message'], 'Test error'); + expect(json['module'], 'test.module'); + expect(json['func_name'], 'testFunc'); + expect(json['line_no'], 42); expect(json['error_type'], 'TestError'); + expect(json['stack_trace'], 'test stack'); expect(json['extra'], {'key': 'value'}); }); - test('toConsoleString should format correctly', () { - final entry = LogEntry( - timestamp: DateTime(2026, 4, 1, 10, 0, 0), - level: LogLevel.debug, - message: 'Test message', - module: 'test.module', - funcName: 'testFunc', - lineNo: 42, - ); - - final result = entry.toConsoleString(); - - expect(result, contains('DEBUG')); - expect(result, contains('test.module')); - expect(result, contains('testFunc')); - expect(result, contains('@42')); - expect(result, contains('Test message')); - }); - - test('toFileString should format correctly', () { - final entry = LogEntry( - timestamp: DateTime(2026, 4, 1, 10, 0, 0), - level: LogLevel.error, - message: 'Test error', - module: 'test.module', - funcName: 'testFunc', - lineNo: 42, - errorType: 'TestError', - stackTrace: 'test stack trace', - ); - - final result = entry.toFileString(); - - expect(result, contains('[test.module]')); - expect(result, contains('at testFunc:42')); - expect(result, contains('Test error')); - expect(result, contains('Error: TestError')); - expect(result, contains('StackTrace:')); - expect(result, contains('test stack trace')); - }); - test('toJson should omit null fields', () { final entry = LogEntry( timestamp: DateTime(2026, 4, 1, 10, 0, 0), @@ -80,5 +45,139 @@ void main() { expect(json.containsKey('stack_trace'), false); expect(json.containsKey('extra'), false); }); + + test('toJson should omit extra when empty', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.info, + message: 'Test', + module: 'test', + extra: {}, + ); + + final json = entry.toJson(); + + expect(json.containsKey('extra'), false); + }); + + test('toConsoleString should format debug level correctly', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.debug, + message: 'Debug message', + module: 'test.module', + funcName: 'testFunc', + lineNo: 42, + ); + + final result = entry.toConsoleString(); + + expect(result, contains('DEBUG')); + expect(result, contains('test.module')); + expect(result, contains('testFunc')); + expect(result, contains('@42')); + expect(result, contains('Debug message')); + }); + + test('toConsoleString should format error level with error type', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.error, + message: 'Error occurred', + module: 'test.module', + funcName: 'handleError', + lineNo: 100, + errorType: 'SocketException', + ); + + final result = entry.toConsoleString(); + + expect(result, contains('ERROR')); + expect(result, contains('[SocketException]')); + expect(result, contains('handleError')); + expect(result, contains('@100')); + }); + + test('toConsoleString should format without location when null', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.info, + message: 'Simple message', + module: 'simple.module', + ); + + final result = entry.toConsoleString(); + + expect(result, contains('INFO')); + expect(result, contains('simple.module')); + expect(result, contains('Simple message')); + }); + + test('toFileString should format correctly with all fields', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.error, + message: 'Test error', + module: 'test.module', + funcName: 'testFunc', + lineNo: 42, + errorType: 'TestError', + stackTrace: 'test stack trace', + extra: {'key': 'value'}, + ); + + final result = entry.toFileString(); + + expect(result, contains('[test.module]')); + expect(result, contains('at testFunc:42')); + expect(result, contains('Test error')); + expect(result, contains('Error: TestError')); + expect(result, contains('StackTrace:')); + expect(result, contains('test stack trace')); + expect(result, contains('Extra:')); + }); + + test('toFileString should format with only funcName (no lineNo)', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.warning, + message: 'Warning message', + module: 'warn.module', + funcName: 'warningFunc', + ); + + final result = entry.toFileString(); + + expect(result, contains('WARNING [warn.module]')); + expect(result, contains('at warningFunc')); + expect(result, isNot(contains(':42'))); + }); + + test('toFileString should format with only lineNo (no funcName)', () { + final entry = LogEntry( + timestamp: DateTime(2026, 4, 1, 10, 0, 0), + level: LogLevel.warning, + message: 'Warning at line', + module: 'warn.module', + lineNo: 99, + ); + + final result = entry.toFileString(); + + expect(result, contains('at :99')); + }); + + test('LogLevel ordering is correct', () { + expect(LogLevel.debug.index, lessThan(LogLevel.info.index)); + expect(LogLevel.info.index, lessThan(LogLevel.warning.index)); + expect(LogLevel.warning.index, lessThan(LogLevel.error.index)); + }); + + test('LogLevel names are lowercase', () { + expect(LogLevel.debug.name, 'debug'); + expect(LogLevel.info.name, 'info'); + expect(LogLevel.warning.name, 'warning'); + expect(LogLevel.error.name, 'error'); + }); }); } diff --git a/apps/test/core/logging/log_service_test.dart b/apps/test/core/logging/log_service_test.dart new file mode 100644 index 0000000..ab0a338 --- /dev/null +++ b/apps/test/core/logging/log_service_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/core/logging/log_config.dart'; +import 'package:social_app/core/logging/log_entry.dart'; +import 'package:social_app/core/logging/log_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LogService', () { + test('LogConfig.forDebug creates correct config', () { + final config = LogConfig.forDebug(); + + expect(config.minLevel, LogLevel.debug); + expect(config.output, LogOutput.console); + }); + + test('LogConfig.forRelease creates correct config', () { + final config = LogConfig.forRelease(); + + expect(config.minLevel, LogLevel.warning); + expect(config.output, LogOutput.file); + expect(config.logFileName, 'app.log'); + expect(config.logDir, 'logs'); + }); + + test('LogConfig default values are correct', () { + const config = LogConfig(); + + expect(config.minLevel, LogLevel.debug); + expect(config.output, LogOutput.console); + expect(config.logFileName, 'app.log'); + expect(config.logDir, 'logs'); + }); + + test('LogService with console output logs all levels', () async { + final service = await LogService.create( + config: const LogConfig(minLevel: LogLevel.debug), + ); + + service.debug(message: 'debug msg', module: 'test'); + service.info(message: 'info msg', module: 'test', extra: {'a': 1}); + service.warning(message: 'warn msg', module: 'test', extra: {'a': 1}); + service.error( + message: 'error msg', + error: Exception('test'), + stackTrace: StackTrace.current, + module: 'test', + ); + + service.flush(); + }); + + test('LogService filters by minLevel - info', () async { + final service = await LogService.create( + config: const LogConfig(minLevel: LogLevel.info), + ); + + service.debug(message: 'debug msg', module: 'test'); + + service.flush(); + }); + + test('error method extracts error type from exception', () async { + final service = await LogService.create( + config: const LogConfig(minLevel: LogLevel.error), + ); + + try { + throw ArgumentError('test error'); + } catch (e, st) { + service.error( + message: 'Test error occurred', + error: e, + stackTrace: st, + module: 'test.module', + extra: {'test': true}, + ); + } + + service.flush(); + }); + + test('debug method captures StackTrace.current', () async { + final service = await LogService.create( + config: const LogConfig(minLevel: LogLevel.debug), + ); + + service.debug(message: 'Debug with location', module: 'test.location'); + + service.flush(); + }); + }); +} diff --git a/apps/test/data/repositories/shared_repositories_test.dart b/apps/test/data/repositories/shared_repositories_test.dart index 60a7449..9543fb9 100644 --- a/apps/test/data/repositories/shared_repositories_test.dart +++ b/apps/test/data/repositories/shared_repositories_test.dart @@ -22,7 +22,11 @@ class _FakeApiClient implements IApiClient { void setPatch(String path, dynamic data) => _patchResponses[path] = data; @override - Future> get(String path, {Options? options}) async { + Future> get( + String path, { + Map? queryParameters, + Options? options, + }) async { if (!_getResponses.containsKey(path)) { throw StateError('missing GET mock for $path'); } @@ -33,7 +37,11 @@ class _FakeApiClient implements IApiClient { } @override - Future> post(String path, {data, Options? options}) async { + Future> post( + String path, { + dynamic data, + Options? options, + }) async { if (!_postResponses.containsKey(path)) { throw StateError('missing POST mock for $path'); } @@ -44,7 +52,11 @@ class _FakeApiClient implements IApiClient { } @override - Future> patch(String path, {data, Options? options}) async { + Future> patch( + String path, { + dynamic data, + Options? options, + }) async { if (!_patchResponses.containsKey(path)) { throw StateError('missing PATCH mock for $path'); } @@ -55,7 +67,7 @@ class _FakeApiClient implements IApiClient { } @override - Future> delete(String path, {data, Options? options}) { + Future> delete(String path, {dynamic data, Options? options}) { throw UnimplementedError(); } @@ -68,7 +80,7 @@ class _FakeApiClient implements IApiClient { } @override - Future> put(String path, {data, Options? options}) { + Future> put(String path, {dynamic data, Options? options}) { throw UnimplementedError(); } } @@ -111,7 +123,7 @@ void main() { 'id': 'f1', 'sender': {'id': 'u1', 'username': 'alice', 'avatar_url': null}, 'recipient': {'id': 'u2', 'username': 'bob', 'avatar_url': null}, - 'content': 'hi', + 'content': {'message': 'hi'}, 'status': 'accepted', 'created_at': '2026-03-27T08:00:00Z', }); @@ -137,7 +149,7 @@ void main() { 'id': 'f1', 'sender': {'id': 'u1', 'username': 'alice', 'avatar_url': null}, 'recipient': {'id': 'u2', 'username': 'bob', 'avatar_url': null}, - 'content': 'hi', + 'content': {'message': 'hi'}, 'status': 'pending', 'created_at': '2026-03-27T08:00:00Z', }); From 19aa33a6097f0cdb77e4a5366c3db072337a7a9a Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 1 Apr 2026 15:11:49 +0800 Subject: [PATCH 16/16] fix: correct test failures and error propagation - Add CacheScope provider in UserProfileCacheRepository tests - Remove catch blocks that swallowed errors in _loadHistory/_loadMoreHistory - Errors now properly propagate to switchUser() caller --- .../features/auth/presentation/bloc/auth_bloc.dart | 4 ---- .../chat/presentation/bloc/chat_bloc_history.dart | 12 ------------ .../chat/presentation/bloc/chat_bloc_send.dart | 8 -------- .../data/repositories/friend_repository.dart | 8 -------- apps/lib/features/home/data/voice_recorder.dart | 7 +------ .../messages/data/repositories/inbox_repository.dart | 4 ---- .../presentation/cubits/automation_jobs_cubit.dart | 5 ----- .../todo/data/repositories/todo_repository.dart | 1 - .../user_profile_cache_repository_test.dart | 6 ++++++ 9 files changed, 7 insertions(+), 48 deletions(-) diff --git a/apps/lib/features/auth/presentation/bloc/auth_bloc.dart b/apps/lib/features/auth/presentation/bloc/auth_bloc.dart index dc9e0e8..1421686 100644 --- a/apps/lib/features/auth/presentation/bloc/auth_bloc.dart +++ b/apps/lib/features/auth/presentation/bloc/auth_bloc.dart @@ -21,10 +21,6 @@ class AuthBloc extends Bloc { final refreshToken = await _repository.getRefreshToken(); if (refreshToken != null) { final response = await _repository.refreshSession(refreshToken); - _logger.info( - message: 'Session refreshed successfully', - extra: {'user_id': response.user.id}, - ); emit( AuthAuthenticated( user: AuthUser(id: response.user.id, phone: response.user.phone), diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart index a377a6d..2d500f4 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc_history.dart @@ -22,12 +22,6 @@ extension _ChatBlocHistory on ChatBloc { hasEarlierHistory: snapshot.hasMore, ), ); - } catch (e, stackTrace) { - _logger.error( - message: 'Failed to load chat history', - error: e, - stackTrace: stackTrace, - ); } finally { if (epoch == _sessionEpoch) { emit(state.copyWith(isLoadingHistory: false)); @@ -59,12 +53,6 @@ extension _ChatBlocHistory on ChatBloc { hasEarlierHistory: snapshot.hasMore, ), ); - } catch (e, stackTrace) { - _logger.error( - message: 'Failed to load more chat history', - error: e, - stackTrace: stackTrace, - ); } finally { if (epoch == _sessionEpoch) { emit(state.copyWith(isLoadingHistory: false)); diff --git a/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart b/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart index 290003b..b29fc24 100644 --- a/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart +++ b/apps/lib/features/chat/presentation/bloc/chat_bloc_send.dart @@ -4,14 +4,6 @@ part of 'chat_bloc.dart'; extension _ChatBlocSend on ChatBloc { Future _sendMessage(String content, {List? images}) async { - _logger.info( - message: 'Sending chat message', - extra: { - 'message_length': content.length, - 'attachments_count': images?.length ?? 0, - }, - ); - final epoch = _sessionEpoch; final assistantBaselineAtSend = chatBlocLatestAssistantTimestamp( state.items, diff --git a/apps/lib/features/contacts/data/repositories/friend_repository.dart b/apps/lib/features/contacts/data/repositories/friend_repository.dart index 9a3a5f1..9216716 100644 --- a/apps/lib/features/contacts/data/repositories/friend_repository.dart +++ b/apps/lib/features/contacts/data/repositories/friend_repository.dart @@ -117,10 +117,6 @@ class FriendRepositoryImpl extends CachedRepository> } final request = FriendRequest.fromJson(data); await _invalidateFriendCaches(friendshipId); - _logger.info( - message: 'Friend request accepted', - extra: {'friendship_id': friendshipId}, - ); return request; } catch (e, stackTrace) { _logger.error( @@ -145,10 +141,6 @@ class FriendRepositoryImpl extends CachedRepository> } final request = FriendRequest.fromJson(data); await _invalidateFriendCaches(friendshipId); - _logger.info( - message: 'Friend request declined', - extra: {'friendship_id': friendshipId}, - ); return request; } catch (e, stackTrace) { _logger.error( diff --git a/apps/lib/features/home/data/voice_recorder.dart b/apps/lib/features/home/data/voice_recorder.dart index 0f32533..0e6cf76 100644 --- a/apps/lib/features/home/data/voice_recorder.dart +++ b/apps/lib/features/home/data/voice_recorder.dart @@ -34,7 +34,7 @@ class RecordVoiceRecorder implements VoiceRecorder { throw StateError(L10n.current.homeRecorderPluginUnavailable); } if (!hasPermission) { - _logger.warning(message: 'Voice recorder permission denied'); + _logger.debug(message: 'Voice recorder permission denied'); throw StateError(L10n.current.homeRecorderPermissionDenied); } @@ -51,7 +51,6 @@ class RecordVoiceRecorder implements VoiceRecorder { ), path: path, ); - _logger.info(message: 'Voice recording started', extra: {'path': path}); } on MissingPluginException catch (e, stackTrace) { _logger.error( message: 'Failed to start voice recording', @@ -67,10 +66,6 @@ class RecordVoiceRecorder implements VoiceRecorder { String? stoppedPath; try { stoppedPath = await _recorder.stop(); - _logger.info( - message: 'Voice recording stopped', - extra: {'path': stoppedPath ?? _currentPath}, - ); } on MissingPluginException catch (e, stackTrace) { _logger.error( message: 'Failed to stop voice recording', diff --git a/apps/lib/features/messages/data/repositories/inbox_repository.dart b/apps/lib/features/messages/data/repositories/inbox_repository.dart index 2483444..9b76897 100644 --- a/apps/lib/features/messages/data/repositories/inbox_repository.dart +++ b/apps/lib/features/messages/data/repositories/inbox_repository.dart @@ -83,10 +83,6 @@ class InboxRepositoryImpl extends CachedRepository> removeCacheKey(_messagesKey(true)), removeCacheKey(_messagesKey(null)), ]); - _logger.info( - message: 'Message marked as read', - extra: {'message_id': messageId}, - ); return message; } catch (e, stackTrace) { _logger.error( diff --git a/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart b/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart index 67486a5..0495d9a 100644 --- a/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart +++ b/apps/lib/features/settings/presentation/cubits/automation_jobs_cubit.dart @@ -65,7 +65,6 @@ class AutomationJobsCubit extends Cubit { Future deleteJob(String id) async { try { await _api.delete(id); - _logger.info(message: 'Automation job deleted', extra: {'job_id': id}); await loadJobs(); } catch (e, stackTrace) { _logger.error( @@ -91,10 +90,6 @@ class AutomationJobsCubit extends Cubit { .map((job) => job.id == id ? updated : job) .toList(); emit(state.copyWith(jobs: nextJobs)); - _logger.info( - message: 'Automation job status updated', - extra: {'job_id': id, 'enabled': enabled}, - ); } catch (e, stackTrace) { _logger.error( message: 'Failed to update automation job status', diff --git a/apps/lib/features/todo/data/repositories/todo_repository.dart b/apps/lib/features/todo/data/repositories/todo_repository.dart index a632d2e..9383e16 100644 --- a/apps/lib/features/todo/data/repositories/todo_repository.dart +++ b/apps/lib/features/todo/data/repositories/todo_repository.dart @@ -52,7 +52,6 @@ class TodoRepository extends CachedRepository> { try { await api.completeTodo(id); invalidator.invalidate(pendingListKey); - _logger.info(message: 'Todo completed', extra: {'todo_id': id}); } catch (error, stackTrace) { _logger.error( message: 'Failed to complete todo', diff --git a/apps/test/features/settings/data/repositories/user_profile_cache_repository_test.dart b/apps/test/features/settings/data/repositories/user_profile_cache_repository_test.dart index cc143b9..c9f7d37 100644 --- a/apps/test/features/settings/data/repositories/user_profile_cache_repository_test.dart +++ b/apps/test/features/settings/data/repositories/user_profile_cache_repository_test.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; +import 'package:social_app/data/cache/cache_scope.dart'; import 'package:social_app/data/cache/cache_store.dart'; import 'package:social_app/features/contacts/data/models/user_profile.dart'; import 'package:social_app/features/settings/data/repositories/user_profile_cache_repository.dart'; @@ -8,6 +9,8 @@ import 'package:social_app/features/settings/data/repositories/user_profile_cach void main() { group('UserProfileCacheRepository', () { test('keeps in-memory snapshot and invalidates correctly', () async { + CacheScope.configureProvider(() => 'test-scope'); + addTearDown(() => CacheScope.resetProvider()); var remoteCalls = 0; final repository = UserProfileCacheRepository( store: HybridCacheStore( @@ -40,6 +43,9 @@ void main() { test( 'invalidate prevents stale in-flight refresh from restoring cache', () async { + CacheScope.configureProvider(() => 'test-scope-2'); + addTearDown(() => CacheScope.resetProvider()); + final completer = Completer(); final repository = UserProfileCacheRepository( store: HybridCacheStore(