feat(debug): App-Crash-Reporting via RVS — Logs in der Diagnostic-UI
Stefan ist unterwegs, ADB-Zugriff nicht moeglich. Loesung: die App
loggt ihre eigenen Crashes via RVS, Bridge sammelt sie in
/shared/logs/app.log, Diagnostic-Server liefert sie als JSON.
Damit braucht's keinen ADB mehr — Crashes sind sofort vom Browser
(oder Claude per curl) lesbar.
Komponenten:
1. App components/ErrorBoundary.tsx
- React-ErrorBoundary fuer kritische Sections
- componentDidCatch → reportAppError (RVS-Send)
- UI zeigt Error-Box statt White-Screen + Reset-Button
2. App services/logger.ts
- reportAppError(scope, message, stack) → rvs.send('app_log', ...)
- installGlobalCrashReporter() haengt sich an ErrorUtils.setGlobalHandler
UND HermesInternal.enablePromiseRejectionTracker — fangt sowohl
ungefangene Errors als auch unhandled Promise-Rejections
- Konsole bleibt parallel aktiv (damit ADB im Dev-Build weiter
was sieht)
3. App App.tsx: installGlobalCrashReporter() im useEffect zusammen
mit initLogger.
4. App ChatScreen.tsx:
- Inbox-Modal mit ErrorBoundary umschlossen (scope: InboxModal,
onReset schliesst Modal)
- MemoryDetailModal mit ErrorBoundary umschlossen
- DetailModal wird nur noch konditional gerendert (memoryDetailId
!= null) statt immer visible-toggle — vermeidet potentielles
Modal-Stacking-Problem
5. RVS server.js: ALLOWED_TYPES += "app_log"
6. Bridge aria_bridge.py:
- elif msg_type == "app_log": haengt eine Zeile an
/shared/logs/app.log (JSONL, jedes Item {ts, platform, level,
scope, message, stack})
- Plus log.info Hinweis fuer das normale Bridge-Log
7. Diagnostic server.js:
- GET /api/app-log[?limit=N] → letzte N Eintraege als JSON
- POST /api/app-log/clear → log-Datei loeschen
Workflow zum Debuggen des Inbox-Crashes:
Stefan rebuilded App → drueckt Inbox → ErrorBoundary fangt den
Crash (oder Global-Handler bei ungefangenem Error) → reportAppError
→ RVS → Bridge schreibt nach /shared/logs/app.log → Stefan
oder Claude rufen GET /api/app-log auf → sehen Stacktrace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -13,7 +13,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import ChatScreen from './src/screens/ChatScreen';
|
||||
import SettingsScreen from './src/screens/SettingsScreen';
|
||||
import rvs from './src/services/rvs';
|
||||
import { initLogger } from './src/services/logger';
|
||||
import { initLogger, installGlobalCrashReporter } from './src/services/logger';
|
||||
|
||||
// --- Navigation ---
|
||||
|
||||
@@ -49,6 +49,9 @@ const App: React.FC = () => {
|
||||
// initLogger ist async aber blockt nichts — solange er noch laueft,
|
||||
// loggen wir normal (Default an), danach respektiert console.log das Setting.
|
||||
initLogger().catch(() => {});
|
||||
// Crash-Reporter installieren — ungefangene JS-Errors landen via RVS
|
||||
// bei der Bridge (sichtbar in /shared/logs/app.log + Diagnostic-API)
|
||||
installGlobalCrashReporter();
|
||||
const initConnection = async () => {
|
||||
const config = await rvs.loadConfig();
|
||||
if (config) {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* ErrorBoundary — fängt React-Render-Fehler und zeigt eine Error-Box
|
||||
* statt White-Screen-of-Death. Plus: Crash wird zum logger geschickt,
|
||||
* der das ueber RVS an die Bridge weiterleitet.
|
||||
*
|
||||
* Einsatz: kritische Komponenten/Modals damit ein Bug nicht die ganze
|
||||
* App killt.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
|
||||
import { reportAppError } from '../services/logger';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
/** Optional: Bezeichnung der eingegrenzten Section fuer's Log. */
|
||||
scope?: string;
|
||||
/** Optional: Reset-Callback (z.B. Modal schliessen) — Button ist dann sichtbar. */
|
||||
onReset?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
err: Error | null;
|
||||
info: string;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { err: null, info: '' };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(err: Error): Partial<State> {
|
||||
return { err };
|
||||
}
|
||||
|
||||
componentDidCatch(err: Error, info: any) {
|
||||
const stack = info?.componentStack || '';
|
||||
this.setState({ info: stack });
|
||||
reportAppError({
|
||||
scope: this.props.scope || 'ErrorBoundary',
|
||||
message: err?.message || String(err),
|
||||
stack: (err?.stack || '') + '\n--- componentStack ---\n' + stack,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.err) {
|
||||
return (
|
||||
<View style={s.box}>
|
||||
<Text style={s.title}>⚠️ Etwas ist schiefgegangen</Text>
|
||||
<Text style={s.scope}>{this.props.scope || 'unbekannte Komponente'}</Text>
|
||||
<ScrollView style={s.scroll}>
|
||||
<Text style={s.msg}>{this.state.err.message || String(this.state.err)}</Text>
|
||||
{this.state.info ? <Text style={s.stack}>{this.state.info}</Text> : null}
|
||||
</ScrollView>
|
||||
{this.props.onReset ? (
|
||||
<TouchableOpacity style={s.btn} onPress={() => { this.setState({err:null,info:''}); this.props.onReset?.(); }}>
|
||||
<Text style={s.btnText}>Schliessen + zurueck</Text>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity style={s.btn} onPress={() => this.setState({err:null,info:''})}>
|
||||
<Text style={s.btnText}>Erneut versuchen</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={s.hint}>
|
||||
Crash wurde an die Bridge gemeldet — sichtbar in der Diagnostic-Web-UI unter /api/app-log
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
const s = StyleSheet.create({
|
||||
box: { flex:1, padding:16, backgroundColor:'#1A0A0A' },
|
||||
title: { color:'#FF6B6B', fontWeight:'bold', fontSize:16, marginBottom:6 },
|
||||
scope: { color:'#FF9500', fontSize:12, marginBottom:10 },
|
||||
scroll: { flex:1, backgroundColor:'#0D0D1A', borderRadius:6, padding:10, marginBottom:10 },
|
||||
msg: { color:'#FF6B6B', fontSize:13, marginBottom:8 },
|
||||
stack: { color:'#8888AA', fontSize:11, fontFamily:'monospace' },
|
||||
btn: { backgroundColor:'#0096FF', paddingVertical:10, borderRadius:6, alignItems:'center' },
|
||||
btnText: { color:'#fff', fontWeight:'600' },
|
||||
hint: { color:'#555570', fontSize:10, marginTop:8, textAlign:'center' },
|
||||
});
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -30,6 +30,7 @@ import { Dimensions } from 'react-native';
|
||||
import ZoomableImage from '../components/ZoomableImage';
|
||||
import MemoryDetailModal from '../components/MemoryDetailModal';
|
||||
import MemoryBrowser from '../components/MemoryBrowser';
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
import rvs, { RVSMessage, ConnectionState } from '../services/rvs';
|
||||
import audioService from '../services/audio';
|
||||
import wakeWordService from '../services/wakeword';
|
||||
@@ -1839,17 +1840,22 @@ const ChatScreen: React.FC = () => {
|
||||
</View>
|
||||
|
||||
{/* Memory-Detail/Edit-Modal — wird durch Tap auf eine memorySaved-Bubble geoeffnet */}
|
||||
<MemoryDetailModal
|
||||
memoryId={memoryDetailId}
|
||||
visible={!!memoryDetailId}
|
||||
onClose={() => setMemoryDetailId(null)}
|
||||
onDeleted={() => setMemoryDetailId(null)}
|
||||
/>
|
||||
{memoryDetailId ? (
|
||||
<ErrorBoundary scope="ChatScreen.MemoryDetailModal" onReset={() => setMemoryDetailId(null)}>
|
||||
<MemoryDetailModal
|
||||
memoryId={memoryDetailId}
|
||||
visible={!!memoryDetailId}
|
||||
onClose={() => setMemoryDetailId(null)}
|
||||
onDeleted={() => setMemoryDetailId(null)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : null}
|
||||
|
||||
{/* Notizen-Inbox — Listet alle Memories aus dem aktuellen Chat (Special-Bubbles).
|
||||
Bestes-Aus-beiden-Welten: nur die Memory-IDs aus den memorySaved-Bubbles
|
||||
des aktuellen Chats, plus den vollen Browser darunter wenn der User mehr will. */}
|
||||
<Modal visible={inboxVisible} animationType="slide" onRequestClose={() => setInboxVisible(false)}>
|
||||
<ErrorBoundary scope="ChatScreen.InboxModal" onReset={() => setInboxVisible(false)}>
|
||||
<View style={{flex:1, backgroundColor:'#0D0D1A'}}>
|
||||
<View style={{flexDirection:'row', alignItems:'center', padding:14, borderBottomWidth:1, borderBottomColor:'#1E1E2E'}}>
|
||||
<Text style={{color:'#FFD60A', fontWeight:'bold', fontSize:16, flex:1}}>{'🗂️'} Notizen-Inbox</Text>
|
||||
@@ -1935,6 +1941,7 @@ const ChatScreen: React.FC = () => {
|
||||
</Text>
|
||||
<MemoryBrowser onOpenMemory={(id) => { setInboxVisible(false); setMemoryDetailId(id); }} />
|
||||
</View>
|
||||
</ErrorBoundary>
|
||||
</Modal>
|
||||
|
||||
{/* Bild-Vollbild Modal */}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Platform } from 'react-native';
|
||||
import rvs from './rvs';
|
||||
|
||||
export const VERBOSE_LOGGING_KEY = 'aria_verbose_logging';
|
||||
|
||||
@@ -39,3 +41,77 @@ export function setVerboseLogging(verbose: boolean): void {
|
||||
applyState();
|
||||
AsyncStorage.setItem(VERBOSE_LOGGING_KEY, String(verbose)).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 {
|
||||
try {
|
||||
rvs.send('app_log' as any, {
|
||||
ts: Date.now(),
|
||||
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.
|
||||
}
|
||||
// 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 || '');
|
||||
}
|
||||
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user