Kurs JavaScript i React · Moduł 16: Obsługa błędów i Suspense

Logowanie i monitoring błędów

5 min czytania
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 sieciowe

Sentry 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 ./build

Source 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. 1. Dlaczego console.error jest niewystarczające do monitorowania błędów na produkcji?

  2. 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:

Przydatne artykuły