From 6612c8b10cb250b6ef2e762519258181e31fffa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Tue, 7 Jul 2026 23:40:04 +0200 Subject: [PATCH] Faro & grafana improvements --- apps/frontend-mobile/app/(tabs)/profile.tsx | 16 +++--- apps/frontend/src/App.jsx | 16 +++++- .../frontend/src/components/ErrorBoundary.jsx | 57 +++++++++++++++++++ apps/frontend/src/main.jsx | 13 ++++- apps/frontend/src/utils/faro.js | 10 +++- 5 files changed, 99 insertions(+), 13 deletions(-) create mode 100644 apps/frontend/src/components/ErrorBoundary.jsx diff --git a/apps/frontend-mobile/app/(tabs)/profile.tsx b/apps/frontend-mobile/app/(tabs)/profile.tsx index 17ab6cf..8fac40d 100644 --- a/apps/frontend-mobile/app/(tabs)/profile.tsx +++ b/apps/frontend-mobile/app/(tabs)/profile.tsx @@ -417,12 +417,12 @@ export default function ProfileScreen() { return ( {/* Avatar Section */} - - setShowAvatarModal(true)}> + + setShowAvatarModal(true)}> {avatarUrl ? ( - + ) : ( - + )} @@ -432,7 +432,7 @@ export default function ProfileScreen() { {/* Read-only Name Section */} - + Nombre {firstName || '—'} @@ -444,7 +444,7 @@ export default function ProfileScreen() { {/* Menu Section */} - + Configuración @@ -463,7 +463,7 @@ export default function ProfileScreen() { {searchHistory.map((item) => ( {item.address} - handleDeleteSearch(item.id)} style={styles.searchDelete} > @@ -484,7 +484,7 @@ export default function ProfileScreen() { {/* Logout Button */} - + Cerrar Sesión diff --git a/apps/frontend/src/App.jsx b/apps/frontend/src/App.jsx index 5b4d14e..768b5b6 100644 --- a/apps/frontend/src/App.jsx +++ b/apps/frontend/src/App.jsx @@ -9,6 +9,7 @@ import AdminView from './views/AdminView'; import LoginModal from './components/LoginModal'; import SavedNotifications from './components/SavedNotifications'; import BottomNav from './components/BottomNav'; +import { getFaro } from './utils/faro'; function App() { const [screen, setScreen] = useState('home'); @@ -40,7 +41,10 @@ function App() { fetch('/api/auth/check') .then(r => r.json()) .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)); window.addEventListener('resize', handleResize); @@ -57,7 +61,10 @@ function App() { const count = (data.global?.length || 0) + (data.pharmacy?.length || 0); setBadgeCount(count); }) - .catch(() => {}); + .catch((err) => { + console.warn('[notifications/mine]', err); + getFaro()?.pushError(err, { type: 'network', url: '/api/notifications/mine' }); + }); return () => { cancelled = true; }; }, [currentUser]); @@ -69,7 +76,10 @@ function App() { if (!data) return; 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) { diff --git a/apps/frontend/src/components/ErrorBoundary.jsx b/apps/frontend/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..97044b6 --- /dev/null +++ b/apps/frontend/src/components/ErrorBoundary.jsx @@ -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 ( +
+

Algo salió mal

+

Ha ocurrido un error inesperado. Por favor, recarga la página.

+ +
+ ); + } + return this.props.children; + } +} diff --git a/apps/frontend/src/main.jsx b/apps/frontend/src/main.jsx index 4ed469c..e0341e3 100644 --- a/apps/frontend/src/main.jsx +++ b/apps/frontend/src/main.jsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import ErrorBoundary from './components/ErrorBoundary'; import './index.css'; import { initNativeShell } from './utils/native'; import { initFaro } from './utils/faro'; @@ -9,9 +10,19 @@ import { initFaro } from './utils/faro'; // No-op if VITE_FARO_ENDPOINT is not configured. 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( - + + + ); diff --git a/apps/frontend/src/utils/faro.js b/apps/frontend/src/utils/faro.js index 50bc81e..43c5ed4 100644 --- a/apps/frontend/src/utils/faro.js +++ b/apps/frontend/src/utils/faro.js @@ -17,6 +17,11 @@ import { TracingInstrumentation } from '@grafana/faro-web-tracing'; import { OtlpHttpTransport } from '@grafana/faro-transport-otlp-http'; let initialized = false; +let faroApi = null; + +export function getFaro() { + return faroApi; +} export function initFaro() { if (initialized) return; @@ -34,7 +39,7 @@ export function initFaro() { const version = import.meta.env.VITE_FARO_APP_VERSION || '1.0.0'; try { - initializeFaro({ + const faro = initializeFaro({ url: `${endpoint.replace(/\/$/, '')}/v1/traces`, app: { name: appName, @@ -61,6 +66,9 @@ export function initFaro() { url: `${endpoint.replace(/\/$/, '')}/v1/traces`, }), }); + faroApi = faro.api; + // Expose for ErrorBoundary and manual reporting + window.__faro = faro; } catch (err) { // Never let Faro init failure break the app. // eslint-disable-next-line no-console