Kurs JavaScript i React · Moduł 16: Obsługa błędów i Suspense
Logowanie i monitoring błędów
W tej lekcji5
Na statku kosmicznym każda awaria jest rejestrowana w czarnej skrzynce. Bez niej załoga nie mogłaby analizować przyczyn problemów po fakcie. W aplikacjach React mamy dokładnie ten sam problem -- jeśli błąd wystąpi u użytkownika na produkcji, a my go nie zalogujemy, nigdy się o nim nie dowiemy. Logowanie i monitoring błędów to Twoja czarna skrzynka produkcyjna.
Strategie logowania błędów
W środowisku produkcyjnym potrzebujemy kilku warstw logowania:
1. console.error -- tylko do developmentu
console.error jest przydatny podczas pracy lokalnej, ale na produkcji użytkownik go nie widzi, a Ty nie masz do niego dostępu.
1// Dobre do developmentu, bezużyteczne na produkcji
2componentDidCatch(error, errorInfo) {
3 console.error('Error caught:', error);
4 console.error('Component stack:', errorInfo.componentStack);
5}2. Logowanie do zewnętrznego serwisu
Na produkcji błędy muszą być wysyłane do serwisu monitorującego:
1class ErrorBoundary extends React.Component {
2 componentDidCatch(error, errorInfo) {
3 // Wysłanie do serwisu monitorującego
4 logErrorToService({
5 message: error.message,
6 stack: error.stack,
7 componentStack: errorInfo.componentStack,
8 timestamp: new Date().toISOString(),
9 url: window.location.href,
10 userAgent: navigator.userAgent,
11 });
12 }
13
14 // ...render z fallback
15}
16
17async function logErrorToService(errorData) {
18 try {
19 await fetch('/api/errors', {
20 method: 'POST',
21 headers: { 'Content-Type': 'application/json' },
22 body: JSON.stringify(errorData),
23 });
24 } catch (e) {
25 // Fallback -- nie blokuj aplikacji jeśli logowanie się nie powiedzie
26 console.error('Failed to log error:', e);
27 }
28}Sentry i serwisy error tracking
Sentry to najpopularniejszy serwis do monitorowania błędów w aplikacjach JavaScript i React. Inne popularne narzędzia to LogRocket, Bugsnag, Datadog i New Relic.
Podstawowa integracja z Sentry
1import * as Sentry from '@sentry/react';
2
3// Inicjalizacja w main/index.js
4Sentry.init({
5 dsn: 'https://klucz@sentry.io/projekt',
6 environment: 'production',
7 // Procent sesji do rejestrowania (performance)
8 tracesSampleRate: 0.2,
9});
10
11// Sentry automatycznie przechwytuje:
12// - Nieobsłużone wyjątki (uncaught exceptions)
13// - Nieobsłużone rejection Promise
14// - Błędy siecioweSentry z Error Boundary
1import * as Sentry from '@sentry/react';
2
3// Sentry dostarcza gotowy Error Boundary
4function App() {
5 return (
6 <Sentry.ErrorBoundary
7 fallback={({ error }) => (
8 <div className="error-page">
9 <h2>Coś poszło nie tak</h2>
10 <p>Błąd został automatycznie zgłoszony</p>
11 <button onClick={() => window.location.reload()}>
12 Odśwież stronę
13 </button>
14 </div>
15 )}
16 >
17 <SpaceStation />
18 </Sentry.ErrorBoundary>
19 );
20}Kontekst i breadcrumbs w Sentry
Sentry pozwala dodawać kontekst do błędów -- informacje o użytkowniku, ostatnich akcjach (breadcrumbs) i dodatkowe dane:
1// Ustawienie kontekstu użytkownika
2Sentry.setUser({
3 id: user.id,
4 email: user.email,
5 username: user.callsign,
6});
7
8// Ręczne dodawanie breadcrumbs (ślad akcji użytkownika)
9Sentry.addBreadcrumb({
10 category: 'navigation',
11 message: 'Użytkownik przeszedł do panelu misji',
12 level: 'info',
13});
14
15// Ręczne raportowanie błędu z dodatkowym kontekstem
16Sentry.captureException(error, {
17 extra: {
18 missionId: currentMission.id,
19 moduleState: moduleStatus,
20 },
21});Customowy hook do logowania błędów
Możesz stworzyć własny hook, który centralizuje logikę logowania:
1function useErrorLogger() {
2 const logError = useCallback((error, context = {}) => {
3 const errorReport = {
4 message: error.message,
5 stack: error.stack,
6 timestamp: new Date().toISOString(),
7 url: window.location.href,
8 ...context,
9 };
10
11 // W produkcji wyślij do serwisu
12 if (process.env.NODE_ENV === 'production') {
13 sendToErrorService(errorReport);
14 }
15
16 // W development loguj do konsoli
17 console.error('[ErrorLogger]', errorReport);
18 }, []);
19
20 return { logError };
21}
22
23// Użycie w komponencie
24function MissionControl() {
25 const { logError } = useErrorLogger();
26
27 const handleDataFetch = async () => {
28 try {
29 const data = await fetchMissionData();
30 setMissions(data);
31 } catch (error) {
32 logError(error, {
33 component: 'MissionControl',
34 action: 'fetchMissionData',
35 });
36 setError('Nie udało się pobrać danych misji');
37 }
38 };
39}Raportowanie błędów na produkcji
Global error handlers
Oprócz Error Boundaries i try-catch, warto dodać globalne handlery, które przechwycą błędy wymykające się innym mechanizmom:
1// Globalne przechwytywanie nieprzechwyconych błędów
2window.addEventListener('error', (event) => {
3 logErrorToService({
4 type: 'uncaught_error',
5 message: event.message,
6 filename: event.filename,
7 lineno: event.lineno,
8 colno: event.colno,
9 });
10});
11
12// Globalne przechwytywanie nieobsłużonych Promise rejections
13window.addEventListener('unhandledrejection', (event) => {
14 logErrorToService({
15 type: 'unhandled_rejection',
16 message: event.reason?.message || 'Unknown rejection',
17 stack: event.reason?.stack,
18 });
19});Poziomy ważności błędów
Nie wszystkie błędy są równie ważne. Serwisy error tracking pozwalają na klasyfikację:
1// Krytyczny błąd - aplikacja nie działa
2Sentry.captureException(error, { level: 'fatal' });
3
4// Błąd - funkcjonalność nie działa, ale aplikacja tak
5Sentry.captureException(error, { level: 'error' });
6
7// Ostrzeżenie - coś może być nie tak
8Sentry.captureMessage('API odpowiada wolno', 'warning');
9
10// Informacja - przydatna do debugowania
11Sentry.captureMessage('Użytkownik zmienił język', 'info');Source maps dla debugowania produkcji
Kod produkcyjny jest zminifikowany -- stack trace jest nieczytelny bez source maps. Source maps to jak mapa dekodująca -- pozwalają przetłumaczyć pozycje w zminifikowanym kodzie na oryginalne pliki źródłowe.
1// Bez source maps stack trace wygląda tak:
2// Error at e.render (main.a1b2c3.js:1:45678)
3
4// Z source maps:
5// Error at MissionControl.render (MissionControl.jsx:42:15)Konfiguracja source maps
1// webpack.config.js
2module.exports = {
3 devtool: 'source-map', // Generuj source maps
4 // ...
5};
6
7// Ważne: source maps NIE powinny być publiczne!
8// Uploaduj je do Sentry, ale nie serwuj użytkownikom:
9// sentry-cli sourcemaps upload ./buildSource maps pozwalają Ci zobaczyć dokładnie w której linii oryginalnego kodu wystąpił błąd, nawet jeśli produkcyjny bundle jest zminifikowany do jednej linii. To jak posiadanie schematu technicznego statku kosmicznego -- zamiast patrzeć na niezrozumiały zminifikowany panel, widzisz dokładnie który moduł zawiódł i dlaczego. Pamiętaj jednak, żeby nigdy nie udostępniać source maps publicznie -- to byłby jak udostępnienie planów statku potencjalnym najeźdźcom.
Kod do tej lekcji: App.jsx
1import React, { useState, useCallback, useEffect } from 'react';
2import './styles.css';
3
4// Customowy hook do logowania bledow
5function useErrorLogger() {
6 const [errorLog, setErrorLog] = useState([]);
7
8 const logError = useCallback((error, context = {}) => {
9 const report = {
10 id: Date.now(),
11 message: error.message || String(error),
12 stack: error.stack ? error.stack.split('\n').slice(0, 3).join('\n') : 'brak',
13 timestamp: new Date().toLocaleTimeString(),
14 url: window.location.href,
15 ...context,
16 };
17
18 setErrorLog(prev => [report, ...prev].slice(0, 10));
19
20 // W produkcji wyslalibysmy do serwisu:
21 // sendToSentry(report);
22 console.error('[ErrorLogger]', report);
23 }, []);
24
25 return { logError, errorLog };
26}
27
28// Error Boundary z logowaniem
29class LoggingErrorBoundary extends React.Component {
30 constructor(props) {
31 super(props);
32 this.state = { hasError: false, error: null };
33 }
34
35 static getDerivedStateFromError(error) {
36 return { hasError: true, error };
37 }
38
39 componentDidCatch(error, errorInfo) {
40 // Logowanie do "serwisu"
41 if (this.props.onError) {
42 this.props.onError(error, {
43 component: this.props.label || 'Unknown',
44 type: 'render_error',
45 componentStack: errorInfo.componentStack,
46 });
47 }
48 }
49
50 render() {
51 if (this.state.hasError) {
52 return (
53 <div className="error-card">
54 <strong>{this.props.label}: Awaria</strong>
55 <p>{this.state.error?.message}</p>
56 <button onClick={() => this.setState({ hasError: false })}>
57 Retry
58 </button>
59 </div>
60 );
61 }
62 return this.props.children;
63 }
64}
65
66// Modul ktory moze rzucic blad
67function UnstableModule({ name, failRate = 0.3 }) {
68 if (Math.random() < failRate) {
69 throw new Error(name + ': krytyczna awaria systemu!');
70 }
71 return (
72 <div className="module-ok">
73 <span className="dot green" /> {name}: Online
74 </div>
75 );
76}
77
78// Komponent z async bledem
79function DataPanel({ logError }) {
80 const [data, setData] = useState(null);
81 const [loading, setLoading] = useState(false);
82
83 const fetchData = async () => {
84 setLoading(true);
85 try {
86 await new Promise(r => setTimeout(r, 800));
87 if (Math.random() > 0.5) {
88 throw new Error('Timeout polaczenia z baza danych');
89 }
90 setData({ missions: 42, crew: 7, fuel: '87%' });
91 } catch (error) {
92 logError(error, {
93 component: 'DataPanel',
94 action: 'fetchData',
95 type: 'async_error',
96 });
97 setData(null);
98 } finally {
99 setLoading(false);
100 }
101 };
102
103 return (
104 <div className="data-panel">
105 <h3>Dane stacji</h3>
106 <button onClick={fetchData} className="fetch-btn">
107 {loading ? 'Ladowanie...' : 'Pobierz dane'}
108 </button>
109 {data && (
110 <ul>
111 <li>Misje: {data.missions}</li>
112 <li>Zaloga: {data.crew}</li>
113 <li>Paliwo: {data.fuel}</li>
114 </ul>
115 )}
116 </div>
117 );
118}
119
120export default function App() {
121 const { logError, errorLog } = useErrorLogger();
122 const [key, setKey] = useState(0);
123
124 // Globalny handler (symulacja)
125 useEffect(() => {
126 const handler = (event) => {
127 logError(new Error(event.reason?.message || 'Unhandled rejection'), {
128 type: 'unhandled_rejection',
129 });
130 };
131 window.addEventListener('unhandledrejection', handler);
132 return () => window.removeEventListener('unhandledrejection', handler);
133 }, [logError]);
134
135 return (
136 <div className="app">
137 <h1>Error Logging & Monitoring</h1>
138 <p className="subtitle">
139 Kazdy blad jest rejestrowany w logu (czarnej skrzynce)
140 </p>
141 <button onClick={() => setKey(k => k + 1)} className="reset-btn">
142 Reset modulow
143 </button>
144
145 <div className="modules" key={key}>
146 <LoggingErrorBoundary label="Nawigacja" onError={logError}>
147 <UnstableModule name="Nawigacja" />
148 </LoggingErrorBoundary>
149
150 <LoggingErrorBoundary label="Komunikacja" onError={logError}>
151 <UnstableModule name="Komunikacja" />
152 </LoggingErrorBoundary>
153
154 <DataPanel logError={logError} />
155 </div>
156
157 <div className="log-panel">
158 <h3>Log bledow ({errorLog.length})</h3>
159 {errorLog.length === 0 ? (
160 <p className="empty-log">Brak zarejestrowanych bledow</p>
161 ) : (
162 errorLog.map(entry => (
163 <div key={entry.id} className="log-entry">
164 <span className="log-time">{entry.timestamp}</span>
165 <span className="log-type">[{entry.type || 'error'}]</span>
166 <span className="log-msg">{entry.message}</span>
167 {entry.component && (
168 <span className="log-comp">({entry.component})</span>
169 )}
170 </div>
171 ))
172 )}
173 </div>
174 </div>
175 );
176}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Dlaczego console.error jest niewystarczające do monitorowania błędów na produkcji?
2. W jaki sposób Sentry integruje się z Error Boundaries w React?
Zadania praktyczne w grze
- Układanie w pionie
Uporządkuj poziomy ważności błędów od najwyższego do najniższego:
- Edytor kodu
Zaimplementuj hook useErrorLogger
- Klikanie w kolejności
Kliknij elementy w kolejności budowania logowania w componentDidCatch:
- Układanie w poziomie
Ułóż poprawną składnię globalnego handlera Promise rejection: