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 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 _logs = []; late final String _deviceModel; bool _initialized = false; Future 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)}=', ); s = s .replaceAll(RegExp(r'/(storage|data|home|Users|var)/\S+'), '') .replaceAll(RegExp(r'https?://\S+'), '') .replaceAll(RegExp(r'Bearer\s+[A-Za-z0-9._-]+'), 'Bearer ') .replaceAll(RegExp(r'eyJ[A-Za-z0-9._-]{10,}'), '') .replaceAll(RegExp(r'\b[A-Fa-f0-9]{16,}\b'), ''); 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 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 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 _uploadIfError(LogEntry entry) async { debugPrint('📤 Uploading ERROR immediately: ${entry.message}'); await _uploadLogs(); } /// Hochladen der Logs zum Server Future _uploadLogs() async { if (_logs.isEmpty) return; try { final logsToUpload = List.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 flush() async { await _uploadLogs(); } int get logCount => _logs.length; } /// Global Logger Instance final logger = LoggerService();