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>
249 lines
9.3 KiB
TypeScript
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
|
|
}
|
|
}
|