Files
Melo/lib/services/logger_service.dart
T
Hermes (Server)andClaude Opus 4.8 d83f8c8c54 Datenschutz: Log-Bereinigung vor Server-Upload (Scrubber + Quelle)
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
2026-08-20 01:38:56 +02:00

177 lines
5.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();