goon/mobile/src/ErrorBoundary.tsx
goon-foss ad0284585b Initial commit
Goon — self-hosted aggregator for adult-content scene metadata.

Indexes scenes from TPDB, StashDB, and 30+ public adult tube sites.
Cross-source deduplication via perceptual hash + Levenshtein distance.
FastAPI backend + APScheduler worker + React Native (Expo) mobile client.

FOSS, ad-free, donation-funded. See README for details.
2026-05-20 10:10:22 +02:00

80 lines
2.5 KiB
TypeScript

import * as Sentry from '@sentry/react-native';
import React from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import { theme } from './theme';
interface Props {
children: React.ReactNode;
/** Opcjonalny tag wysyłany do Sentry — pomaga rozróżnić top-level boundary od lokalnych. */
scope?: string;
}
interface State {
error: Error | null;
info: string | null;
}
export class ErrorBoundary extends React.Component<Props, State> {
state: State = { error: null, info: null };
static getDerivedStateFromError(error: Error): State {
return { error, info: null };
}
componentDidCatch(error: Error, info: React.ErrorInfo): void {
this.setState({ error, info: info.componentStack || null });
// Sentry.wrap()-owy boundary łapie tylko jeśli root nie jest manuanle wrapped.
// Dodajemy explicit capture żeby ten boundary też wysyłał — z tagiem `scope`
// żebyśmy w UI Sentry odróżnili global crash od per-tab crash.
Sentry.captureException(error, {
tags: { boundary: this.props.scope || 'global' },
contexts: { react: { componentStack: info.componentStack } },
});
// eslint-disable-next-line no-console
console.error('App crashed:', error, info);
}
render() {
if (this.state.error) {
return (
<ScrollView style={styles.container} contentContainerStyle={{ padding: 20 }}>
<Text style={styles.title}>App crashed</Text>
<Text style={styles.label}>Error</Text>
<Text style={styles.body}>{this.state.error.name}: {this.state.error.message}</Text>
{this.state.error.stack ? (
<>
<Text style={styles.label}>Stack</Text>
<Text style={styles.code}>{this.state.error.stack}</Text>
</>
) : null}
{this.state.info ? (
<>
<Text style={styles.label}>Component stack</Text>
<Text style={styles.code}>{this.state.info}</Text>
</>
) : null}
</ScrollView>
);
}
return <>{this.props.children}</>;
}
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: theme.bg },
title: { color: theme.bad, fontSize: 24, fontWeight: '700', marginBottom: 16 },
label: {
color: theme.muted,
fontSize: 12,
textTransform: 'uppercase',
letterSpacing: 0.6,
marginTop: 16,
marginBottom: 4,
},
body: { color: theme.fg, fontSize: 14 },
code: {
color: theme.fg,
fontSize: 11,
fontFamily: 'monospace',
},
});