Files
ARIA-AGENT/android/src/services/logger.ts
T
duffyduckandClaude Opus 4.8 e2b1eced3c feat(logger): native App-Crashes ueber RVS erkennen (ohne adb)
Native Crashes/OOM schreiben keinen JS-Fehler → tauchten in app.log
nirgends auf (nur der Whisper-Reboot als Symptom). Jetzt: ein RUN_MARKER
bleibt gesetzt solange die App aktiv laeuft, wird bei sauberem Hintergrund-
Wechsel geloescht. Ist er beim naechsten Start noch da, ist der vorige Lauf
unsauber gestorben → Report via RVS ('app.crash-detected') mit dem letzten
Breadcrumb (was die App zuletzt tat) + Zeitabstand. Breadcrumbs kommen aus
reportAppError/reportAppDebug, throttled in AsyncStorage persistiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-08 16:51:18 +02:00

249 lines
9.3 KiB
TypeScript

/**
* Verbose-Logging-Toggle: console.log laesst sich global stummschalten.
* console.warn/console.error bleiben immer an — Fehler will man immer sehen.
*
* Default: an (true). Toggle ueber Settings → Protokoll → Verbose Logging.
* Beim Start wird der gespeicherte Wert geladen, vorher loggen wir normal.
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform, DeviceEventEmitter, AppState } from 'react-native';
import rvs from './rvs';
// Lokales Event damit die SettingsScreen Live Logs / Events Tabs
// auch das sehen was die App SELBST loggt (reportAppDebug/Error).
// Bisher gingen die nur via RVS an die Bridge. Lokal sichtbar = Mama-
// tauglich Debug ohne curl.
export const APP_LOG_EVENT = 'AriaLocalAppLog';
interface LocalLogEntry {
ts: number;
level: 'info' | 'warn' | 'error';
scope: string;
message: string;
}
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
// Eigener Toggle fuer Debug-Logs die ueber RVS an die Bridge gehen
// (/shared/logs/app.log → Diagnostic /api/app-log). Damit der Default-User
// nicht stuendlich Traffic + Disk-Schreiben hat, dieser ist DEFAULT AUS.
// Stefan schaltet's nur ein wenn er ein konkretes Problem debuggen muss.
export const DEBUG_LOGS_TO_BRIDGE_KEY = 'aria_debug_logs_to_bridge';
// Original-console.log retten, damit wir die Wrapper jederzeit wieder
// "scharf" stellen koennen (sonst waere ein Toggle-an nach -aus tot).
const originalLog = console.log.bind(console);
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;
}
/** Wert aus AsyncStorage laden und anwenden. Beim App-Start aufrufen. */
export async function initLogger(): Promise<void> {
try {
const v = await AsyncStorage.getItem(VERBOSE_LOGGING_KEY);
_verbose = v !== 'false'; // default: true
} catch {}
try {
const d = await AsyncStorage.getItem(DEBUG_LOGS_TO_BRIDGE_KEY);
_debugLogsToBridge = d === 'true'; // default: false
} catch {}
applyState();
await _initCrashDetection();
}
// Native-Crash-Erkennung (ohne adb) — siehe RUN_MARKER-Kommentar oben.
async function _initCrashDetection(): Promise<void> {
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 {
return _verbose;
}
export function setVerboseLogging(verbose: boolean): void {
_verbose = verbose;
applyState();
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).catch(() => {});
}
export function isDebugLogsToBridge(): boolean {
return _debugLogsToBridge;
}
export function setDebugLogsToBridge(enabled: boolean): void {
_debugLogsToBridge = enabled;
AsyncStorage.setItem(DEBUG_LOGS_TO_BRIDGE_KEY, String(enabled)).catch(() => {});
}
// ─── App-Crash-Reporting via RVS ────────────────────────────────────
//
// Wenn die App crasht — egal ob React-Render-Fehler (ErrorBoundary) oder
// ungefangener JS-Error (ErrorUtils-Handler) — schicken wir den Crash
// als RVS-Message vom Typ "app_log" an die Bridge. Die schreibt in
// /shared/logs/app.log, sodass wir/Diagnostic die Crashes mitlesen
// koennen ohne ADB.
interface AppErrorEvent {
scope: string;
message: string;
stack?: string;
level?: 'error' | 'warn' | 'info';
}
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,
platform: Platform.OS,
level: ev.level || 'error',
scope: ev.scope,
message: ev.message,
stack: (ev.stack || '').slice(0, 8000),
});
} catch {
// RVS noch nicht connected — Fehler geht im console weiter.
}
// Lokal in den App-Logs-Tab emitten — Errors gehen IMMER durch
// (unabhaengig vom Debug-Toggle).
try {
const entry: LocalLogEntry = {
ts, level: ev.level || 'error', scope: ev.scope, message: ev.message,
};
DeviceEventEmitter.emit(APP_LOG_EVENT, entry);
} catch {}
// Plus lokal: console.error, damit Stefan's adb (wenn doch mal verfuegbar)
// den Crash sieht.
console.error(`[app-error scope=${ev.scope}]`, ev.message, '\n', ev.stack || '');
}
/** Schickt eine Debug-/Info-Message via RVS an die Bridge. Landet ebenfalls
* in /shared/logs/app.log — abrufbar via `curl /api/app-log?lines=N`.
* Im Gegensatz zu reportAppError: keine Stacktrace, level=info, kein
* console.error. Fuer Live-Diagnose im Hintergrund wenn ADB nicht da ist.
*
* Nur aktiv wenn Settings → Protokoll → Debug-Logs an Bridge AN ist.
* 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);
try {
rvs.send('app_log' as any, {
ts,
platform: Platform.OS,
level: 'info',
scope,
message: trimmed,
});
} catch {}
// Plus lokal in den App-Logs-Tab emitten — damit Stefan in der App
// selbst (Settings → Protokoll → Live Logs) sieht was passiert,
// ohne curl gegen Bridge.
try {
const entry: LocalLogEntry = { ts, level: 'info', scope, message: trimmed };
DeviceEventEmitter.emit(APP_LOG_EVENT, entry);
} catch {}
}
/** Installiert einen globalen JS-Error-Handler der ungefangene Errors via
* RVS an die Bridge schickt. Beim App-Start aufrufen. */
export function installGlobalCrashReporter(): void {
if (_reportingInstalled) return;
_reportingInstalled = true;
try {
const g: any = global as any;
const prev = g.ErrorUtils?.getGlobalHandler?.();
g.ErrorUtils?.setGlobalHandler?.((err: any, isFatal: boolean) => {
reportAppError({
scope: isFatal ? 'global-fatal' : 'global-nonfatal',
message: (err && err.message) || String(err),
stack: err && err.stack,
});
// Original-Handler weiterhin aufrufen damit React-Native das System-
// Crash-Overlay zeigt (im Dev-Build) bzw. in Production sauber stirbt.
if (typeof prev === 'function') {
try { prev(err, isFatal); } catch {}
}
});
// unhandled Promise-Rejections — manche RN-Versionen haben das nicht
// automatisch im ErrorUtils.
g.HermesInternal?.enablePromiseRejectionTracker?.({
allRejections: true,
onUnhandled: (id: number, err: any) => {
reportAppError({
scope: 'promise-unhandled',
level: 'warn',
message: (err && err.message) || String(err),
stack: err && err.stack,
});
},
});
} catch {
// ErrorUtils nicht da → nix machen
}
}