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(