Diagnose-Logs werden zentral bereinigt, bevor sie an logs.baka-net.de gehen. Lokaler debugPrint bleibt vollständig. - Freitext-Scrubber (LoggerService.scrubText) für message + stackTrace: maskiert user=/pw=/password=/token=/secret=/url= sowie Datei-Pfade, nackte URLs, Bearer-Token, JWTs und lange Hex-IDs. - stackTrace auf 8 Zeilen gekürzt (capFrames). - deviceModel nur noch grobe Plattform (android/ios) statt Hersteller+Modell; device_info_plus im Logger entfernt. - Quelle entschärft: settings_screen loggte Navidrome-Login mit Klartext user=/url= — jetzt nur neutraler Status inkl. pwLeer-Flag. - Neue Tests: test/logger_service_test.dart (9). 79/79 grün, analyze sauber. Opt-in/Einwilligung bleibt separater Folgeschritt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tr2UwurtxyJUpWpBSeVmBm
177 lines
5.3 KiB
Dart
177 lines
5.3 KiB
Dart
import 'package:flutter/foundation.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'dart:convert';
|
||
import 'dart:io';
|
||
|
||
class LogEntry {
|
||
final String level; // DEBUG, INFO, WARNING, ERROR
|
||
final String message;
|
||
final String? stackTrace;
|
||
final DateTime timestamp;
|
||
final String? userAgent;
|
||
final String? deviceModel;
|
||
|
||
LogEntry({
|
||
required this.level,
|
||
required this.message,
|
||
this.stackTrace,
|
||
DateTime? timestamp,
|
||
this.userAgent,
|
||
this.deviceModel,
|
||
}) : timestamp = timestamp ?? DateTime.now();
|
||
|
||
Map<String, dynamic> toJson() => {
|
||
'level': level,
|
||
'message': message,
|
||
'stackTrace': stackTrace,
|
||
'timestamp': timestamp.toIso8601String(),
|
||
'userAgent': userAgent,
|
||
'deviceModel': deviceModel,
|
||
};
|
||
}
|
||
|
||
/// Centralized Logging Service mit Auto-Upload zu Server
|
||
class LoggerService extends ChangeNotifier {
|
||
static const String _logServerUrl = 'https://logs.baka-net.de/api/logs';
|
||
static const int _batchSize = 5;
|
||
|
||
final List<LogEntry> _logs = [];
|
||
late final String _deviceModel;
|
||
bool _initialized = false;
|
||
|
||
Future<void> init() async {
|
||
if (_initialized) return;
|
||
// Datenschutz: nur grobe Plattform senden statt Hersteller+Modell
|
||
// (das war ein Geräte-Fingerprint). Reicht für Bug-Triage.
|
||
_deviceModel = Platform.isAndroid
|
||
? 'android'
|
||
: (Platform.isIOS ? 'ios' : Platform.operatingSystem);
|
||
_initialized = true;
|
||
debugPrint('✅ LoggerService initialized for $_deviceModel');
|
||
}
|
||
|
||
// ─── Datenschutz: Bereinigung vor dem Server-Upload ───
|
||
|
||
static const int _maxFrames = 8;
|
||
|
||
/// Maskiert sensible Fragmente in Log-Freitext (message/stackTrace), bevor
|
||
/// er an den Server geht: sensible `key=value`-Fragmente, Datei-Pfade,
|
||
/// nackte URLs, Bearer-Token, JWTs und lange Hex-IDs.
|
||
@visibleForTesting
|
||
static String scrubText(String? roh) {
|
||
if (roh == null || roh.isEmpty) return roh ?? '';
|
||
var s = roh.replaceAllMapped(
|
||
RegExp(r'\b(user|username|benutzer|pw|pass|passwort|password|token|secret|url|serverurl)=([^\s&]+)',
|
||
caseSensitive: false),
|
||
(m) => '${m.group(1)}=<redacted>',
|
||
);
|
||
s = s
|
||
.replaceAll(RegExp(r'/(storage|data|home|Users|var)/\S+'), '<path>')
|
||
.replaceAll(RegExp(r'https?://\S+'), '<url>')
|
||
.replaceAll(RegExp(r'Bearer\s+[A-Za-z0-9._-]+'), 'Bearer <token>')
|
||
.replaceAll(RegExp(r'eyJ[A-Za-z0-9._-]{10,}'), '<jwt>')
|
||
.replaceAll(RegExp(r'\b[A-Fa-f0-9]{16,}\b'), '<hex>');
|
||
return s;
|
||
}
|
||
|
||
/// Kürzt einen (bereits gescrubbten) Stack-Trace auf wenige Zeilen.
|
||
@visibleForTesting
|
||
static String capFrames(String s) {
|
||
final zeilen = s.split('\n');
|
||
if (zeilen.length <= _maxFrames) return s;
|
||
final rest = zeilen.length - _maxFrames;
|
||
return '${zeilen.take(_maxFrames).join('\n')}\n… ($rest weitere Zeilen entfernt)';
|
||
}
|
||
|
||
/// Bereinigter Upload-Payload eines Eintrags. Der lokale Puffer/debugPrint
|
||
/// bleibt unberührt — die Maskierung greift nur auf dem Versand-Pfad.
|
||
@visibleForTesting
|
||
static Map<String, dynamic> sanitizedJson(LogEntry l) => {
|
||
'level': l.level,
|
||
'message': scrubText(l.message),
|
||
'stackTrace':
|
||
l.stackTrace == null ? null : capFrames(scrubText(l.stackTrace)),
|
||
'timestamp': l.timestamp.toIso8601String(),
|
||
'userAgent': l.userAgent,
|
||
'deviceModel': l.deviceModel,
|
||
};
|
||
|
||
/// Logge einen Fehler (wird sofort zum Server geschickt)
|
||
Future<void> error(String message, [Object? error, StackTrace? stackTrace]) async {
|
||
final entry = LogEntry(
|
||
level: 'ERROR',
|
||
message: message,
|
||
stackTrace: '$error\n$stackTrace',
|
||
deviceModel: _deviceModel,
|
||
);
|
||
_logs.add(entry);
|
||
debugPrint('❌ ERROR: $message');
|
||
await _uploadIfError(entry);
|
||
}
|
||
|
||
/// Logge eine Warnung
|
||
void warning(String message) {
|
||
final entry = LogEntry(
|
||
level: 'WARNING',
|
||
message: message,
|
||
deviceModel: _deviceModel,
|
||
);
|
||
_logs.add(entry);
|
||
debugPrint('⚠️ WARNING: $message');
|
||
_checkAndFlush();
|
||
}
|
||
|
||
/// Logge Info-Event
|
||
void info(String message) {
|
||
final entry = LogEntry(
|
||
level: 'INFO',
|
||
message: message,
|
||
deviceModel: _deviceModel,
|
||
);
|
||
_logs.add(entry);
|
||
debugPrint('ℹ️ INFO: $message');
|
||
_checkAndFlush();
|
||
}
|
||
|
||
void _checkAndFlush() {
|
||
if (_logs.length >= _batchSize) {
|
||
_uploadLogs();
|
||
}
|
||
}
|
||
|
||
Future<void> _uploadIfError(LogEntry entry) async {
|
||
debugPrint('📤 Uploading ERROR immediately: ${entry.message}');
|
||
await _uploadLogs();
|
||
}
|
||
|
||
/// Hochladen der Logs zum Server
|
||
Future<void> _uploadLogs() async {
|
||
if (_logs.isEmpty) return;
|
||
try {
|
||
final logsToUpload = List<LogEntry>.from(_logs);
|
||
await http.post(
|
||
Uri.parse(_logServerUrl),
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: jsonEncode({
|
||
'logs': logsToUpload.map(sanitizedJson).toList(),
|
||
'timestamp': DateTime.now().toIso8601String(),
|
||
}),
|
||
).timeout(const Duration(seconds: 10));
|
||
_logs.clear();
|
||
debugPrint('✅ Logs uploaded successfully (${logsToUpload.length} entries)');
|
||
} catch (e) {
|
||
debugPrint('⚠️ Failed to upload logs: $e');
|
||
}
|
||
}
|
||
|
||
/// Manuelles Flushen (z.B. vor App-Exit)
|
||
Future<void> flush() async {
|
||
await _uploadLogs();
|
||
}
|
||
|
||
int get logCount => _logs.length;
|
||
}
|
||
|
||
/// Global Logger Instance
|
||
final logger = LoggerService();
|