Faro & grafana improvements
This commit is contained in:
@@ -417,12 +417,12 @@ export default function ProfileScreen() {
|
|||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container}>
|
<ScrollView style={styles.container}>
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
<View style={styles.avatarSection}>
|
<View style={[styles.avatarSection, isTablet && styles.avatarSectionTablet]}>
|
||||||
<TouchableOpacity style={styles.avatarContainer} onPress={() => setShowAvatarModal(true)}>
|
<TouchableOpacity style={[styles.avatarContainer, isTablet && styles.avatarContainerTablet]} onPress={() => setShowAvatarModal(true)}>
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<Image source={{ uri: avatarUrl }} style={styles.avatarImage} />
|
<Image source={{ uri: avatarUrl }} style={[styles.avatarImage, isTablet && styles.avatarImageTablet]} />
|
||||||
) : (
|
) : (
|
||||||
<Ionicons name="person" size={40} color={colors.primary} />
|
<Ionicons name="person" size={isTablet ? 50 : 40} color={colors.primary} />
|
||||||
)}
|
)}
|
||||||
<View style={styles.avatarOverlay}>
|
<View style={styles.avatarOverlay}>
|
||||||
<Ionicons name="camera" size={24} color="white" />
|
<Ionicons name="camera" size={24} color="white" />
|
||||||
@@ -432,7 +432,7 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Read-only Name Section */}
|
{/* Read-only Name Section */}
|
||||||
<View style={styles.infoSection}>
|
<View style={[styles.infoSection, isTablet && styles.infoSectionTablet]}>
|
||||||
<View style={styles.infoCard}>
|
<View style={styles.infoCard}>
|
||||||
<Text style={styles.infoLabel}>Nombre</Text>
|
<Text style={styles.infoLabel}>Nombre</Text>
|
||||||
<Text style={styles.infoValue}>{firstName || '—'}</Text>
|
<Text style={styles.infoValue}>{firstName || '—'}</Text>
|
||||||
@@ -444,7 +444,7 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Menu Section */}
|
{/* Menu Section */}
|
||||||
<View style={styles.menuSection}>
|
<View style={[styles.menuSection, isTablet && styles.menuSectionTablet]}>
|
||||||
<TouchableOpacity style={styles.menuItem} onPress={openConfig}>
|
<TouchableOpacity style={styles.menuItem} onPress={openConfig}>
|
||||||
<Ionicons name="settings" size={24} color={colors.textSecondary} />
|
<Ionicons name="settings" size={24} color={colors.textSecondary} />
|
||||||
<Text style={styles.menuText}>Configuración</Text>
|
<Text style={styles.menuText}>Configuración</Text>
|
||||||
@@ -484,7 +484,7 @@ export default function ProfileScreen() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Logout Button */}
|
{/* Logout Button */}
|
||||||
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
|
<TouchableOpacity style={[styles.logoutButton, isTablet && styles.logoutButtonTablet]} onPress={handleLogout}>
|
||||||
<Ionicons name="log-out" size={20} color={colors.danger} />
|
<Ionicons name="log-out" size={20} color={colors.danger} />
|
||||||
<Text style={styles.logoutText}>Cerrar Sesión</Text>
|
<Text style={styles.logoutText}>Cerrar Sesión</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import AdminView from './views/AdminView';
|
|||||||
import LoginModal from './components/LoginModal';
|
import LoginModal from './components/LoginModal';
|
||||||
import SavedNotifications from './components/SavedNotifications';
|
import SavedNotifications from './components/SavedNotifications';
|
||||||
import BottomNav from './components/BottomNav';
|
import BottomNav from './components/BottomNav';
|
||||||
|
import { getFaro } from './utils/faro';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [screen, setScreen] = useState('home');
|
const [screen, setScreen] = useState('home');
|
||||||
@@ -40,7 +41,10 @@ function App() {
|
|||||||
fetch('/api/auth/check')
|
fetch('/api/auth/check')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => { if (data.authenticated) setCurrentUser(data.user); })
|
.then(data => { if (data.authenticated) setCurrentUser(data.user); })
|
||||||
.catch(() => {})
|
.catch((err) => {
|
||||||
|
console.warn('[auth/check]', err);
|
||||||
|
getFaro()?.pushError(err, { type: 'network', url: '/api/auth/check' });
|
||||||
|
})
|
||||||
.finally(() => setAuthChecked(true));
|
.finally(() => setAuthChecked(true));
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
@@ -57,7 +61,10 @@ function App() {
|
|||||||
const count = (data.global?.length || 0) + (data.pharmacy?.length || 0);
|
const count = (data.global?.length || 0) + (data.pharmacy?.length || 0);
|
||||||
setBadgeCount(count);
|
setBadgeCount(count);
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch((err) => {
|
||||||
|
console.warn('[notifications/mine]', err);
|
||||||
|
getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' });
|
||||||
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [currentUser]);
|
}, [currentUser]);
|
||||||
|
|
||||||
@@ -69,7 +76,10 @@ function App() {
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
setBadgeCount((data.global?.length || 0) + (data.pharmacy?.length || 0));
|
setBadgeCount((data.global?.length || 0) + (data.pharmacy?.length || 0));
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch((err) => {
|
||||||
|
console.warn('[refreshBadge]', err);
|
||||||
|
getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLogin(user) {
|
function handleLogin(user) {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default class ErrorBoundary extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, info) {
|
||||||
|
// Report to Faro if available
|
||||||
|
try {
|
||||||
|
const faro = window.__faro;
|
||||||
|
if (faro?.api?.pushError) {
|
||||||
|
faro.api.pushError(error, { type: 'react-boundary', componentStack: info.componentStack });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Faro reporting must never break the app
|
||||||
|
}
|
||||||
|
console.error('[ErrorBoundary]', error, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
padding: '2rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontFamily: 'system-ui, sans-serif',
|
||||||
|
color: '#333',
|
||||||
|
}}>
|
||||||
|
<h2>Algo salió mal</h2>
|
||||||
|
<p style={{ color: '#666' }}>Ha ocurrido un error inesperado. Por favor, recarga la página.</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
style={{
|
||||||
|
marginTop: '1rem',
|
||||||
|
padding: '0.5rem 1.5rem',
|
||||||
|
backgroundColor: '#27633a',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '6px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Recargar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import ErrorBoundary from './components/ErrorBoundary';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import { initNativeShell } from './utils/native';
|
import { initNativeShell } from './utils/native';
|
||||||
import { initFaro } from './utils/faro';
|
import { initFaro } from './utils/faro';
|
||||||
@@ -9,9 +10,19 @@ import { initFaro } from './utils/faro';
|
|||||||
// No-op if VITE_FARO_ENDPOINT is not configured.
|
// No-op if VITE_FARO_ENDPOINT is not configured.
|
||||||
initFaro();
|
initFaro();
|
||||||
|
|
||||||
|
// Global unhandled promise rejection handler — report to Faro
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
console.warn('[unhandledrejection]', event.reason);
|
||||||
|
try {
|
||||||
|
window.__faro?.api?.pushError(event.reason, { type: 'unhandled-rejection' });
|
||||||
|
} catch { /* Faro must never break the app */ }
|
||||||
|
});
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import { TracingInstrumentation } from '@grafana/faro-web-tracing';
|
|||||||
import { OtlpHttpTransport } from '@grafana/faro-transport-otlp-http';
|
import { OtlpHttpTransport } from '@grafana/faro-transport-otlp-http';
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
let faroApi = null;
|
||||||
|
|
||||||
|
export function getFaro() {
|
||||||
|
return faroApi;
|
||||||
|
}
|
||||||
|
|
||||||
export function initFaro() {
|
export function initFaro() {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
@@ -34,7 +39,7 @@ export function initFaro() {
|
|||||||
const version = import.meta.env.VITE_FARO_APP_VERSION || '1.0.0';
|
const version = import.meta.env.VITE_FARO_APP_VERSION || '1.0.0';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initializeFaro({
|
const faro = initializeFaro({
|
||||||
url: `${endpoint.replace(/\/$/, '')}/v1/traces`,
|
url: `${endpoint.replace(/\/$/, '')}/v1/traces`,
|
||||||
app: {
|
app: {
|
||||||
name: appName,
|
name: appName,
|
||||||
@@ -61,6 +66,9 @@ export function initFaro() {
|
|||||||
url: `${endpoint.replace(/\/$/, '')}/v1/traces`,
|
url: `${endpoint.replace(/\/$/, '')}/v1/traces`,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
faroApi = faro.api;
|
||||||
|
// Expose for ErrorBoundary and manual reporting
|
||||||
|
window.__faro = faro;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Never let Faro init failure break the app.
|
// Never let Faro init failure break the app.
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
Reference in New Issue
Block a user