Podziemie Analityczne

Jak to działa

Strona o tym, jak pilnujemy liczb.

Audyt, który skasował nasze własne dane

Podczas budowy warstwy danych sprawdziliśmy mapę zmiennych GUS odziedziczoną po wcześniejszej iteracji projektu, opartą na API BDL — zapytanie po zapytaniu, wobec żywego API i opublikowanych danych. 15 z 17 identyfikatorów było błędnych lub nie istniało: identyfikator opisany jako „CPI wskazywał na pozwolenia na budowę mieszkań, a „sprzedaż detaliczna — na przepusty drogowe. Dwa poprawne zwracały wyłącznie wartości roczne, bo BDL jest hurtownią statystyk regionalnych — miesięczne szeregi makro mieszkają w innym API.

Potem zweryfikowaliśmy — wartość po wartości — ręcznie przepisany archiwalny zestaw danych, który strona serwowała jako zapasowy: 98 par (okres, wartość) w dwóch szeregach. 47 z nich nie zgadzało się z publikacjami GUS — inflacja CPI była błędna w 18 z 49 miesięcy, stopa bezrobocia w 29 z 49, a końcówka 2025–2026 nie odpowiadała żadnej opublikowanej wersji danych. Oba szeregi usunęliśmy tego samego dnia (commit z 2026-08-02); w ich miejsce na /wykresy pojawił się jawny komunikat o błędzie. Zasada jest prosta: weryfikuj albo usuń — wolimy pustą stronę niż liczby, których nie umiemy obronić.

Nową mapę zbudowaliśmy na API DBW GUS i sprawdziliśmy na ślepo: najpierw pobraliśmy wartości z API, potem osobna osoba wyszukała odpowiadające im wartości w oficjalnych publikacjach GUS — nie znając odczytów z API. Liczba porównań zgodnych co do wartości: 74 z 74. Dopiero wtedy mapa trafiła do kodu — a licznik rośnie z każdym późniejszym rozszerzeniem mapy, sprawdzanym tą samą procedurą, zanim wejdzie do katalogu. Szeregi handlu zagranicznego przeszły później osobną weryfikację wobec publikacji narastającej — łącznie z tożsamością saldo = eksport − import, zgodną co do złotówki.

Ta sama zasada obowiązuje dalej — ale porównania co do wartości nie wykonaliśmy jeszcze dla każdej serii. Na stronie każdej serii piszemy wprost, czy zostało wykonane.

Pułapki znalezione po drodze zostały zaobserwowane na żywym API, nie wyczytane z dokumentacji — wszystkie są udokumentowane w docs/data-sources.md i obsłużone w kliencie:

  • pole page-count raportuje mniej stron, niż API faktycznie zwraca — stronicujemy, aż strona wróci niepełna;
  • wiersz nagłówkowy CPI (OGÓŁEM) to pozycja 19 462 z 19 944 — na ostatniej stronie odpowiedzi; klient, który czyta tylko pierwszą, nie zobaczy nic;
  • okresy narastające mieszkają w tym samym przekroju co miesiące i niosą wiarygodnie wyglądające wartości — bez jawnej białej listy okresów wykres przeplata dane narastające z miesięcznymi;
  • HTTP 404 znaczy „brak danych dla tego miesiąca”, nie „błąd”;
  • NBP nie serwuje stóp procentowych przez api.nbp.pl — jedynym źródłem jest XML z przecinkiem dziesiętnym, a prośba o JSON (Accept: application/json) dostaje 406.

Kontrakt serwowania: trzy stany

Wszystko, co /wykresy renderuje, podróżuje w kopercie z jawnym statusem: live (dane z bazy), archival (oznaczony zapas) albo error (jawny komunikat). Czwartej, nieoznaczonej ścieżki nie ma — komentarz w src/lib/metrics/data-status.ts formułuje to wprost: nieoznaczony fallback to błąd, nie funkcja.

Gdy jeden widok łączy kilka źródeł, wygrywa najgorszy status, a przy remisie — najstarszy stan danych: strona nigdy nie ogłasza się świeższą niż jej najstarszy element. Porównanie okresów jest kalendarzowe, nie tekstowe, bo „2026-Q1 posortowany jako napis wypada po „2026-08”.

Oznaczenie danych archiwalnych wędruje całym łańcuchem: baner na stronie → plakietka przy wykresie → podpis tabeli → stopka CSV → stempel na eksportowanym PNG. Plik, który ktoś przekaże dalej, niesie swój status ze sobą.

Testy, które blokują całe klasy błędów

Zasady, które dają się złamać przypadkiem, są zakodowane jako testy — bun test uruchamia się w CI przy każdym pushu. W chwili publikacji tej strony liczba plików z testami wynosi 242. Kilka istnieje wyłącznie po to, żeby całe klasy błędów nie dały się wdrożyć:

  • Polszczyzna bez znaków diakrytycznych nie przejdzie builda. Drzewo /wykresy raz wyrenderowało „Wybierz wskazniki. Test tokenizuje każdy plik drzewa — łącznie z generowanymi napisami, które nie mieszkają w żadnym komponencie — i zna ASCII-owe formy słów wymagających znaku. Porównuje całe słowa, nie fragmenty: „usunięty” przechodzi, „usun” oblewa.
  • Żadna migracja nie może zniszczyć tego, co strona serwuje. DROP TABLE, TRUNCATE i DELETE bez WHERE na tabelach czytanych przez działającą stronę oblewają test — łącznie z SQL-em ukrytym w blokach DO. Osobne sprawdzenie: łańcuch migracji musi tworzyć każdą kolumnę, którą czyta kod serwujący, a listę kolumn test parsuje z kodu, zamiast trzymać własną kopię.
  • Dokumentacja źródeł jest normatywna i czytana przez test. Mapa zmiennych w docs/data-sources.md zawiera blok maszynowy (JSON); test porównuje go z kodem co do zmiennej, przekroju, sposobu prezentacji i okna pokrycia. Rozjazd opisu z kodem to czerwony build — dokładnie taki dryf wyhodował 15 błędnych identyfikatorów w poprzedniej mapie.
  • Metadane każdej podstrony przechodzą przez jeden helper. Test skanuje źródła wszystkich podstron i odrzuca ręcznie sklecony blok Open Graph — regresję, która raz zdążyła wejść na produkcję.

Testy nie wykonują żadnych zapytań do żywych API: surowe odpowiedzi DBW i NBP z weryfikacji są zapisane w repozytorium jako fixtures.

Procedurę odtworzenia bazy danych przećwiczyliśmy, zanim była potrzebna: cały łańcuch migracji zastosowany na prawdziwym PostgreSQL 16 — raz na pustej bazie, raz na bazie odtworzonej do kształtu produkcyjnego — i powtórzony, żeby potwierdzić, że drugie uruchomienie niczego nie psuje.

Stos

Next.js 16 (App Router), React 19, TypeScript 5, Tailwind CSS 4 i ECharts 5; testy i build przez Bun; hosting na Vercelu. Artykuły to pliki MDX w repozytorium — bez CMS-a.

Dane pochodzą z API DBW GUS i z NBP. Pochodzenie każdej metryki — zmienna, przekrój, sposób prezentacji, okno pokrycia — jest opisane na stronie źródeł danych.