diff --git a/android/src/services/logger.ts b/android/src/services/logger.ts index 08aebfc..54ab10e 100644 --- a/android/src/services/logger.ts +++ b/android/src/services/logger.ts @@ -7,7 +7,7 @@ */ import AsyncStorage from '@react-native-async-storage/async-storage'; -import { Platform, DeviceEventEmitter } from 'react-native'; +import { Platform, DeviceEventEmitter, AppState } from 'react-native'; import rvs from './rvs'; // Lokales Event damit die SettingsScreen Live Logs / Events Tabs @@ -38,6 +38,23 @@ const noop = () => {}; let _verbose = true; let _debugLogsToBridge = false; +// ─── Crash-Kontext ohne adb ───────────────────────────────────────── +// Ein RUN_MARKER bleibt gesetzt, solange die App AKTIV laeuft; bei sauberem +// Wechsel in den Hintergrund wird er geloescht. Ist er beim naechsten Start +// noch da, ist der vorige Lauf unsauber gestorben (nativer Crash/OOM — der +// schreibt KEINEN JS-Fehler, taucht also sonst nirgends auf). Wir melden das +// dann via RVS mit dem letzten Breadcrumb (was die App zuletzt tat). +const RUN_MARKER_KEY = 'aria_run_marker'; +const BREADCRUMB_KEY = 'aria_last_breadcrumb'; +let _breadcrumb: { ts: number; scope: string; message: string } = { ts: 0, scope: '', message: '' }; +let _breadcrumbDirty = false; + +/** Letzte App-Aktivitaet merken — Crash-Kontext fuer den naechsten Boot. */ +export function noteBreadcrumb(scope: string, message: string): void { + _breadcrumb = { ts: Date.now(), scope: scope || '', message: String(message || '').slice(0, 120) }; + _breadcrumbDirty = true; +} + function applyState(): void { console.log = _verbose ? originalLog : noop; } @@ -53,6 +70,45 @@ export async function initLogger(): Promise { _debugLogsToBridge = d === 'true'; // default: false } catch {} applyState(); + await _initCrashDetection(); +} + +// Native-Crash-Erkennung (ohne adb) — siehe RUN_MARKER-Kommentar oben. +async function _initCrashDetection(): Promise { + try { + const marker = await AsyncStorage.getItem(RUN_MARKER_KEY); + if (marker) { + let bc: any = {}; + try { bc = JSON.parse((await AsyncStorage.getItem(BREADCRUMB_KEY)) || '{}'); } catch {} + const gap = bc && bc.ts ? Math.round((Date.now() - bc.ts) / 1000) : -1; + // Verzoegert melden — RVS ist beim Boot oft noch nicht verbunden. + setTimeout(() => { + reportAppError({ + scope: 'app.crash-detected', + level: 'warn', + message: `Voriger Lauf ohne sauberes Shutdown beendet (nativer Crash/OOM?). ` + + `Letzte Aktivitaet: [${(bc && bc.scope) || '?'}] ${(bc && bc.message) || '?'}` + + (gap >= 0 ? ` (vor ~${gap}s)` : ''), + }); + }, 6000); + } + await AsyncStorage.setItem(RUN_MARKER_KEY, String(Date.now())); + } catch {} + // Breadcrumb throttled persistieren (alle 5s, nur wenn geaendert). + setInterval(() => { + if (_breadcrumbDirty) { + _breadcrumbDirty = false; + AsyncStorage.setItem(BREADCRUMB_KEY, JSON.stringify(_breadcrumb)).catch(() => {}); + } + }, 5000); + // Sauberer Hintergrund-Wechsel → Marker weg (kein Crash). Rueckkehr → wieder + // scharf. So melden nur echte Aktiv-Crashes, kein normales Backgrounden. + try { + AppState.addEventListener('change', (s) => { + if (s === 'background') AsyncStorage.removeItem(RUN_MARKER_KEY).catch(() => {}); + else if (s === 'active') AsyncStorage.setItem(RUN_MARKER_KEY, String(Date.now())).catch(() => {}); + }); + } catch {} } export function isVerboseLogging(): boolean { @@ -94,6 +150,7 @@ let _reportingInstalled = false; /** Schickt einen App-Fehler via RVS an die Bridge. */ export function reportAppError(ev: AppErrorEvent): void { const ts = Date.now(); + noteBreadcrumb(ev.scope, ev.message); try { rvs.send('app_log' as any, { ts, @@ -128,6 +185,9 @@ export function reportAppError(ev: AppErrorEvent): void { * Default aus damit Mama-Modus keine Disk-Schreiblast hat. Error-Reports * (reportAppError) gehen weiterhin IMMER durch. */ export function reportAppDebug(scope: string, message: string): void { + // Breadcrumb IMMER aktualisieren (auch wenn Debug-Logs-an-Bridge aus ist) — + // fuer den Crash-Kontext beim naechsten Boot. + noteBreadcrumb(scope, message); if (!_debugLogsToBridge) return; const ts = Date.now(); const trimmed = String(message).slice(0, 2000);