docs: 更新 HTTP 错误码、用户积分、占卜运行及用户资料协议文档
This commit is contained in:
@@ -0,0 +1,542 @@
|
||||
# Logging Guidelines
|
||||
|
||||
> How logging is done in Flutter app.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This app uses **structured logging** with custom `Logger` class:
|
||||
|
||||
- **Library**: Custom `Logger` class in `core/logging/`
|
||||
- **Interface**: `getLogger(module)` from `core/logging/logger.dart`
|
||||
- **Log levels**: `debug`, `info`, `warning`, `error`
|
||||
- **Sensitive fields**: Never log passwords, tokens, PII
|
||||
|
||||
---
|
||||
|
||||
## Logger Setup
|
||||
|
||||
### Import and Initialize
|
||||
|
||||
```dart
|
||||
import 'core/logging/logger.dart';
|
||||
|
||||
class SomeBloc extends ChangeNotifier {
|
||||
final Logger _logger = getLogger('features.auth.bloc');
|
||||
}
|
||||
```
|
||||
|
||||
### Module Naming Convention
|
||||
|
||||
| Feature | Module Path |
|
||||
|---------|------------|
|
||||
| auth | `features.auth` |
|
||||
| home | `features.home` |
|
||||
| divination | `features.divination` |
|
||||
| settings | `features.settings` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```dart
|
||||
// features/auth/presentation/bloc/auth_bloc.dart
|
||||
final Logger _logger = getLogger('features.auth.bloc');
|
||||
|
||||
// features/home/data/repositories/home_repository.dart
|
||||
final Logger _logger = getLogger('features.home.repository');
|
||||
|
||||
// core/network/http_client.dart
|
||||
final Logger _logger = getLogger('core.network.http_client');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Levels
|
||||
|
||||
| Level | When to Use | Noise Level | Required |
|
||||
|-------|-------------|-------------|----------|
|
||||
| **error** | All exceptions and failures | Required | Never skip |
|
||||
| **warning** | Degraded behavior, retry, fallback | Minimal | Only when action taken |
|
||||
| **info** | Key business events | Minimal | Only milestones |
|
||||
| **debug** | Detailed flow tracing (dev only) | High | Avoid in release |
|
||||
|
||||
---
|
||||
|
||||
## Error Logging Requirements
|
||||
|
||||
### Every try-catch MUST log the exception:
|
||||
|
||||
```dart
|
||||
try {
|
||||
await _repository.someOperation();
|
||||
} catch (e, stackTrace) {
|
||||
_logger.error(
|
||||
message: 'Operation failed: $operationName',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
extra: {'context': 'relevant_data'},
|
||||
);
|
||||
// handle error
|
||||
}
|
||||
```
|
||||
|
||||
### Error Logging Pattern
|
||||
|
||||
```dart
|
||||
// features/auth/presentation/bloc/auth_bloc.dart
|
||||
Future<void> start() async {
|
||||
try {
|
||||
final user = await _repository.recoverSession();
|
||||
// ...
|
||||
} catch (error, stackTrace) {
|
||||
_logger.error(
|
||||
message: 'Session recovery failed: ${error.runtimeType}',
|
||||
error: error.runtimeType.toString(),
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
await _repository.clearLocalSession();
|
||||
_state = AuthState(
|
||||
status: AuthStatus.unauthenticated,
|
||||
errorMessage: _toSafeMessage(error),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Info Logging Requirements
|
||||
|
||||
### Only log these milestone events:
|
||||
|
||||
- User login/logout
|
||||
- Message sent/received
|
||||
- Data sync completed
|
||||
- Important state transitions
|
||||
|
||||
### Info Logging Pattern
|
||||
|
||||
```dart
|
||||
// Login success
|
||||
_logger.info(
|
||||
message: 'User logged in',
|
||||
extra: {'user_id': user.id},
|
||||
);
|
||||
|
||||
// Run success
|
||||
_logger.info(
|
||||
message: 'Run completed',
|
||||
extra: {
|
||||
'run_id': runId,
|
||||
'thread_id': threadId,
|
||||
'duration_ms': duration.inMilliseconds,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### ❌ DO NOT log for every operation:
|
||||
|
||||
```dart
|
||||
// WRONG: Logging every keystroke
|
||||
onChanged: (value) {
|
||||
_logger.info('Input changed: $value'); // Too noisy
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Warning Logging Requirements
|
||||
|
||||
### Only log when taking corrective action:
|
||||
|
||||
- Retrying after failure
|
||||
- Using fallback data
|
||||
- Skipping malformed data
|
||||
- Deprecation warnings
|
||||
|
||||
### Warning Logging Pattern
|
||||
|
||||
```dart
|
||||
// Cache miss with fallback
|
||||
_logger.warning(
|
||||
message: 'Cache miss, loading from remote',
|
||||
extra: {'key': cacheKey},
|
||||
);
|
||||
|
||||
// Retry attempt
|
||||
_logger.warning(
|
||||
message: 'Retry attempt',
|
||||
extra: {'attempt': 2, 'max_attempts': 3},
|
||||
);
|
||||
|
||||
// Fallback data
|
||||
_logger.warning(
|
||||
message: 'Using fallback data due to network timeout',
|
||||
extra: {'timeout_ms': 5000},
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debug Logging
|
||||
|
||||
### Use sparingly, only in debug builds:
|
||||
|
||||
```dart
|
||||
if (kDebugMode) {
|
||||
_logger.debug(
|
||||
message: 'Variable value',
|
||||
extra: {'variable': expensiveObject.toString()},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Debug logs are automatically filtered in release builds.
|
||||
|
||||
---
|
||||
|
||||
## Prohibited Practices
|
||||
|
||||
### ❌ Never log sensitive data
|
||||
|
||||
```dart
|
||||
// WRONG: Logging password
|
||||
_logger.info(message: 'User login', extra: {'password': password});
|
||||
|
||||
// WRONG: Logging token
|
||||
_logger.debug(message: 'API call', extra: {'token': accessToken});
|
||||
|
||||
// WRONG: Logging PII
|
||||
_logger.info(message: 'User profile', extra: {'email': userEmail});
|
||||
```
|
||||
|
||||
### ❌ Never log at debug level in production
|
||||
|
||||
```dart
|
||||
// WRONG: Will log in release build
|
||||
_logger.debug(message: 'Debug info: $sensitiveData');
|
||||
```
|
||||
|
||||
**Right way:** Use `kDebugMode` guard:
|
||||
|
||||
```dart
|
||||
if (kDebugMode) {
|
||||
_logger.debug(message: 'Variable value', extra: {'var': value});
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Never skip error logging
|
||||
|
||||
```dart
|
||||
// WRONG: Exception is caught but not logged
|
||||
try {
|
||||
await operation();
|
||||
} catch (e) {
|
||||
state = ErrorState();
|
||||
// Missing error log!
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Never log in every iteration
|
||||
|
||||
```dart
|
||||
// WRONG: Log every iteration
|
||||
for (item in items) {
|
||||
_logger.debug('Processing item: ${item.id}'); // Too noisy
|
||||
process(item);
|
||||
}
|
||||
```
|
||||
|
||||
**Right: Log only failures:**
|
||||
|
||||
```dart
|
||||
for (item in items) {
|
||||
try {
|
||||
process(item);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.error(
|
||||
message: 'Item processing failed',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
extra: {'item_id': item.id},
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Logger Implementation
|
||||
|
||||
### Core Logger Class
|
||||
|
||||
```dart
|
||||
// core/logging/logger.dart
|
||||
import 'log_entry.dart';
|
||||
import 'log_service.dart';
|
||||
|
||||
class Logger {
|
||||
final String module;
|
||||
final LogService? _service;
|
||||
final bool _isNoOp;
|
||||
|
||||
Logger(this.module, this._service) : _isNoOp = _service == null;
|
||||
|
||||
factory Logger.get(String module) {
|
||||
return Logger(module, _globalLogService);
|
||||
}
|
||||
|
||||
void error({
|
||||
required String message,
|
||||
required Object error,
|
||||
required StackTrace stackTrace,
|
||||
Map<String, dynamic>? extra,
|
||||
}) {
|
||||
if (_isNoOp) {
|
||||
debugPrint(LogEntry(
|
||||
message: message,
|
||||
module: module,
|
||||
errorType: error.runtimeType.toString(),
|
||||
).toConsoleString());
|
||||
return;
|
||||
}
|
||||
_service!.error(
|
||||
message: message,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
void info({
|
||||
required String message,
|
||||
Map<String, dynamic>? extra,
|
||||
}) {
|
||||
if (_isNoOp) return;
|
||||
_service!.info(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
void warning({
|
||||
required String message,
|
||||
Map<String, dynamic>? extra,
|
||||
}) {
|
||||
if (_isNoOp) return;
|
||||
_service!.warning(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
void debug({
|
||||
required String message,
|
||||
Map<String, dynamic>? extra,
|
||||
}) {
|
||||
if (_isNoOp || !kDebugMode) return;
|
||||
_service!.debug(
|
||||
message: message,
|
||||
module: module,
|
||||
extra: extra ?? {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Logger getLogger(String module) => Logger.get(module);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Log Entry Structure
|
||||
|
||||
### LogEntry Fields
|
||||
|
||||
```dart
|
||||
// core/logging/log_entry.dart
|
||||
class LogEntry {
|
||||
final DateTime timestamp;
|
||||
final LogLevel level;
|
||||
final String message;
|
||||
final String module;
|
||||
final String? errorType;
|
||||
final String? errorMessage;
|
||||
final String? stackTrace;
|
||||
final Map<String, dynamic>? extra;
|
||||
}
|
||||
|
||||
enum LogLevel { debug, info, warning, error }
|
||||
```
|
||||
|
||||
### Log Format
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-04-10T12:34:56.789Z",
|
||||
"level": "error",
|
||||
"module": "features.auth.bloc",
|
||||
"message": "Session recovery failed: SocketException",
|
||||
"error_type": "SocketException",
|
||||
"error_message": "Connection refused",
|
||||
"stack_trace": "...",
|
||||
"extra": {
|
||||
"user_id": "123e4567-e89b-12d3-a456-426614174000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Using `print()` instead of Logger
|
||||
|
||||
```dart
|
||||
// WRONG: Never use print in production code
|
||||
print('User logged in: $userId');
|
||||
```
|
||||
|
||||
**Right:** Use `Logger`:
|
||||
|
||||
```dart
|
||||
_logger.info(message: 'User logged in', extra: {'user_id': userId});
|
||||
```
|
||||
|
||||
### ❌ Logging sensitive data
|
||||
|
||||
```dart
|
||||
// WRONG: Logging PII
|
||||
_logger.info(message: 'User profile', extra: {
|
||||
'email': userEmail,
|
||||
'phone': userPhone,
|
||||
});
|
||||
```
|
||||
|
||||
**Right:** Exclude sensitive fields:
|
||||
|
||||
```dart
|
||||
_logger.info(message: 'User profile loaded', extra: {'user_id': userId});
|
||||
```
|
||||
|
||||
### ❌ Catching without logging
|
||||
|
||||
```dart
|
||||
// WRONG: Silent failure
|
||||
try {
|
||||
await service.doSomething();
|
||||
} catch (e) {
|
||||
// No logging, no re-raise
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
**Right:** Log and re-raise:
|
||||
|
||||
```dart
|
||||
try {
|
||||
await service.doSomething();
|
||||
} catch (e, stackTrace) {
|
||||
_logger.error(
|
||||
message: 'Operation failed',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples from Codebase
|
||||
|
||||
### AuthBloc Error Handling
|
||||
|
||||
```dart
|
||||
// features/auth/presentation/bloc/auth_bloc.dart
|
||||
class AuthBloc extends ChangeNotifier {
|
||||
final Logger _logger = getLogger('features.auth.bloc');
|
||||
|
||||
Future<void> start() async {
|
||||
try {
|
||||
final user = await _repository.recoverSession();
|
||||
if (user == null) {
|
||||
_state = const AuthState(status: AuthStatus.unauthenticated);
|
||||
} else {
|
||||
_state = AuthState(status: AuthStatus.authenticated, user: user);
|
||||
}
|
||||
notifyListeners();
|
||||
} catch (error, stackTrace) {
|
||||
_logger.error(
|
||||
message: 'Session recovery failed: ${error.runtimeType}',
|
||||
error: error.runtimeType.toString(),
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
await _repository.clearLocalSession();
|
||||
_state = AuthState(
|
||||
status: AuthStatus.unauthenticated,
|
||||
errorMessage: _toSafeMessage(error),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_logger.info(message: 'User logged out');
|
||||
_state = const AuthState(status: AuthStatus.unauthenticated);
|
||||
notifyListeners();
|
||||
|
||||
unawaited(
|
||||
_repository.logout().catchError((Object error, StackTrace stackTrace) {
|
||||
_logger.error(
|
||||
message: 'User logout failed: ${error.runtimeType}',
|
||||
error: error.runtimeType.toString(),
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Network Error Handling
|
||||
|
||||
```dart
|
||||
// core/network/http_client.dart
|
||||
final Logger _logger = getLogger('core.network.http_client');
|
||||
|
||||
Future<Response> get(String path) async {
|
||||
try {
|
||||
final response = await _innerClient.get(path);
|
||||
final problem = ApiProblemMapper.tryParse(response);
|
||||
|
||||
if (problem != null && response.statusCode != 401) {
|
||||
_logger.warning(
|
||||
message: 'HTTP error response',
|
||||
extra: {
|
||||
'status': response.statusCode,
|
||||
'path': path,
|
||||
'code': problem.code,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
} on SocketException catch (e, stackTrace) {
|
||||
_logger.error(
|
||||
message: 'Network error',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
extra: {'path': path},
|
||||
);
|
||||
throw ApiProblem(
|
||||
status: 0,
|
||||
title: 'Network Error',
|
||||
detail: 'No internet connection',
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user