Portfolio

Airhelp - Projekt WordPress | WPPoland

AirHelp powstało w 2013 roku jako start-up, by stać się globalnym liderem w obronie praw pasażerów lotniczych, pomagając ponad 13 milionom osób zrozumieć ich...

#Strony www
Airhelp - Projekt WordPress | WPPoland

#AirHelp, technologia dla największego obrońcy praw pasażerów

Ruch na serwisie odszkodowawczym nie rozkłada się równomiernie. Przez większość roku jest przewidywalny i płaski, a potem w jedno popołudnie strajk kontrolerów we Francji albo awaria systemu odpraw u dużego przewoźnika unieruchamia kilkaset lotów, i w ciągu godziny na formularz zgłoszeniowy wchodzi ruch kilkudziesięciokrotnie większy od średniej. To jest podstawowa właściwość tego projektu i z niej wynika większość decyzji technicznych opisanych niżej. Serwis, który ma dobre wyniki w teście syntetycznym o drugiej w nocy, nie mówi nic o tym, co się stanie, gdy dziesięć tysięcy osób jednocześnie wpisuje numer rejsu.

AirHelp powstało w styczniu 2013 roku, założone przez Henrika Zillmera, z siedzibą w Berlinie. Firma działa na podstawie unijnego rozporządzenia 261/2004, a poza Unią opiera się na brytyjskim UK261, konwencji montrealskiej, brazylijskim ANAC 400 i kilku kolejnych porządkach prawnych. Pomogła ponad 13 milionom osób zrozumieć swoje prawa i uzyskać odszkodowania za opóźnione, odwołane lub przepełnione loty. Firma nie tylko wspiera pasażerów w procesie odszkodowawczym, ale także reprezentuje ich w sporach sądowych z liniami lotniczymi i lobbuje za sprawiedliwymi regulacjami na szczeblu rządowym.

Jako programista zaprojektowałem i wdrożyłem witrynę AirHelp, prowadząc pięcioosobowy zespół w roli Team Pilot. Poniżej opisuję, co z tego wynikało dla architektury, gdzie wdrożenie napotykało opór i jakim kosztem został on rozwiązany. Wdrożenie zajęło około sześciu tygodni, licząc od analizy zakresu do publikacji, a układ i rozmieszczenie elementów dostarczył klient.

#Cel AirHelp i jego odbiorcy

Odbiorcą tego serwisu jest osoba w nietypowym stanie. Siedzi na lotnisku albo wraca do domu po odwołanym locie, korzysta z telefonu, często w roamingu i na słabym łączu, jest zirytowana i ma przy sobie tylko kartę pokładową ze zdjęcia. Nie przyszła poczytać o firmie. Przyszła sprawdzić, czy jej się coś należy, i chce odpowiedzi w dwóch krokach.

Ta sylwetka użytkownika unieważnia sporą część typowych decyzji projektowych dla serwisu korporacyjnego. Nie ma sensu bogata strona główna, skoro większość wejść trafia od razu na formularz albo na artykuł prawny z wyszukiwarki. Nie ma sensu ładowanie skryptów analitycznych przed treścią, skoro pierwsze dwie sekundy decydują o tym, czy ktokolwiek zobaczy formularz. Ma za to sens rzecz rzadko doceniana: formularz musi przeżyć utratę połączenia w połowie wypełniania, bo na lotnisku to nie jest przypadek brzegowy, tylko norma.

Druga grupa odbiorców to osoby, które trafiają tu z wyszukiwarki miesiące po locie, szukając konkretnej informacji prawnej. Ten ruch ma zupełnie inny profil. Jest stały, dobrze cache’owalny, czyta dłuższe teksty i wchodzi na strony w 24 językach. Jedna platforma obsługuje więc dwa niepodobne do siebie wzorce użycia, a większość napięć architektonicznych bierze się z tego, że muszą współistnieć pod jednym adresem.

Zaplecze merytoryczne stanowi współpraca z kancelariami w 30 krajach oraz zespół 700 pracowników, w tym największa na świecie grupa prawników wyspecjalizowanych w prawie lotniczym. Z punktu widzenia serwisu to oznacza, że treść prawna ma realnego właściciela po stronie klienta i podlega zatwierdzeniu, zanim trafi na produkcję. Model treści musiał to unieść.

#Dlaczego 24 języki to nie jest problem tłumaczeniowy

Najczęstsze nieporozumienie przy takim projekcie polega na potraktowaniu wielojęzyczności jako warstwy tłumaczeń nałożonej na jeden serwis. Tutaj to nie działa, bo różnica między rynkami nie jest różnicą językową, tylko prawną. Pasażer lecący z Warszawy do Londynu po brexicie podlega innemu reżimowi niż ten sam pasażer w locie wewnątrzunijnym, a odszkodowanie za lot z Sao Paulo liczy się według jeszcze innych przepisów. Zdanie o wysokości świadczenia nie jest więc tym samym zdaniem w dwudziestu czterech wersjach. To dwadzieścia kilka odrębnych twierdzeń prawnych, z których część jest prawdziwa tylko w swoim kraju.

Konsekwencja dla modelu treści jest taka, że wariant językowy musi być pełnoprawnym dokumentem z własnym cyklem zatwierdzania, a nie polem tłumaczenia przypiętym do oryginału. Wariant, który nie doczekał się przeglądu prawnika z danej jurysdykcji, nie może wypaść do wersji angielskiej, bo wtedy serwis podaje obcy stan prawny jako własny. Musi zniknąć z nawigacji i z hreflang, i to jest jedyne bezpieczne zachowanie domyślne.

Architektura frontendu opiera się na Next.js z renderowaniem po stronie serwera i obsługą 24 języków przez i18n, zgodna z WCAG 2.1 i przygotowana pod urządzenia mobilne. Renderowanie po stronie serwera nie jest tu wyborem estetycznym. Przy treści prawnej, która ma rankować w kilkudziesięciu rynkach naraz, i przy urządzeniach mobilnych na słabym łączu, przerzucenie składania strony na przeglądarkę odbiorcy oznaczałoby, że najsłabszy telefon w najgorszej sieci wykonuje najwięcej pracy.

#Techniczne funkcjonalności AirHelp

Proces odszkodowawczy to formularz, który pobiera dane lotu dynamicznie przez GraphQL, łączy się z API przewoźników i zapisuje zgłoszenie w PostgreSQL z szyfrowaniem AES-256. Wybór GraphQL zamiast kilku wywołań REST ma tu konkretne uzasadnienie: formularz jest wielokrokowy i każdy krok potrzebuje innego wycinka tych samych danych o rejsie. Przy REST kończy się to albo nadmiarowym pobieraniem całości na starcie, albo serią żądań rosnącą z liczbą kroków. Zapytanie o dokładnie te pola, które renderuje bieżący krok, skraca to do jednego przejścia przez sieć na krok, co przy telefonie w roamingu jest różnicą odczuwalną, a nie kosmetyczną.

Sekcja edukacyjna z artykułami prawnymi ładuje się przez REST API z cache’owaniem w Redis i renderuje w React. Tutaj rozdział jest świadomy: treść redakcyjna jest wspólna dla wszystkich odwiedzających i nadaje się do agresywnego buforowania, dane zgłoszenia nie nadają się do buforowania w ogóle. Trzymanie ich za jednym interfejsem zaoszczędziłoby trochę kodu i kosztowało możliwość odrębnego ustawienia czasu życia dla każdego z nich.

Panel śledzenia statusu wniosku pokazuje dane w czasie rzeczywistym przez WebSocket, buforowane w Memcached dla niskich opóźnień. Warto nazwać kompromis, który za tym stoi. Stałe połączenie zużywa zasoby serwera przez cały czas, gdy karta jest otwarta, a status sprawy odszkodowawczej zmienia się średnio raz na kilka tygodni. Odpytywanie co minutę byłoby tańsze w utrzymaniu i wystarczające dla samej informacji. Uzasadnieniem dla połączenia stałego jest zachowanie w dniu, w którym status faktycznie się zmienia, bo wtedy użytkownik siedzi na tej stronie i odświeża ją ręcznie, a każde ręczne odświeżenie kosztuje więcej niż utrzymanie kanału.

Kopie zapasowe trafiają automatycznie na Amazon S3 z replikacją między regionami, wersjonowaniem i kompresją Zstandard. Wersjonowanie jest tu ważniejsze od samej replikacji. Replikacja chroni przed utratą centrum danych, czyli przed zdarzeniem rzadkim. Wersjonowanie chroni przed nadpisaniem danych przez własny błąd we wdrożeniu, czyli przed zdarzeniem znacznie częstszym, a kopia zreplikowana do trzech regionów jest wtedy po prostu błędem w trzech kopiach.

Optymalizacja pod wyszukiwarki obejmuje frazy w rodzaju „odszkodowanie za opóźniony lot”, dynamiczne mapy XML i przyspieszone indeksowanie. Przy treści prawnej, która zmienia się po każdym istotnym wyroku, mapa generowana statycznie przy wdrożeniu przestaje odzwierciedlać stan serwisu w ciągu tygodnia, więc jest budowana z bieżącego stanu bazy.

#Wyzwania techniczne związane z wydajnością i ich rozwiązania

Obciążenie bazy przy milionach użytkowników wychodziło na PostgreSQL. Rozwiązaniem był Redis z trwałym zapisem dla najczęstszych zapytań oraz podział bazy z replikami odczytu na Amazon RDS. Repliki odczytu mają jednak cenę, o której łatwo zapomnieć: replikacja jest asynchroniczna, więc zaraz po wysłaniu zgłoszenia odczyt z repliki może go jeszcze nie widzieć. Użytkownik składa wniosek i trafia na pustą listę spraw, co wygląda jak utrata danych. Dlatego odczyt bezpośrednio po zapisie musi iść do instancji głównej, a rozproszenie obejmuje tylko te ścieżki, które tolerują opóźnienie kilkuset milisekund.

Wolne ładowanie formularza brało się z integracji z API przewoźników, zwłaszcza po masowych odwołaniach lotów. Zapytania API przeszły na asynchroniczne przetwarzanie przez RabbitMQ, z zapasem w postaci statycznych danych buforowanych w Elasticsearch przy przekroczeniu czasu oczekiwania. Najważniejsza jest tu wartość zapasowa. Systemy przewoźników bywają niedostępne dokładnie wtedy, gdy są najbardziej potrzebne, bo ta sama awaria, która odwołała loty, obciąża ich infrastrukturę. Formularz, który w takiej chwili pokazuje błąd, traci zgłoszenie od osoby, która ma pełne prawo do odszkodowania. Formularz, który przyjmuje numer rejsu bez natychmiastowego potwierdzenia i weryfikuje go później, tego zgłoszenia nie traci.

Wysoka latencja multimediów uderzała w telefony w regionach o słabej łączności. Fastly z kompresją Brotli, formatem WebP i leniwym ładowaniem przez Intersection Observer API skróciło dystrybucję, a geo-optymalizacja skróciła drogę. Zysk z formatu obrazu jest tu jednak wtórny wobec zysku z tego, że obraz poniżej linii zgięcia nie konkuruje o pasmo z treścią, którą użytkownik właśnie czyta.

Opóźnienia w panelu czasu rzeczywistego pojawiały się przy skali 13 milionów użytkowników. Kafka przejęła strumieniowe przesyłanie danych, z ograniczaniem tempa po stronie serwera i load balancerem AWS ALB. Ograniczanie tempa jest w tym zestawie elementem najmniej efektownym i najbardziej potrzebnym, bo bez niego pojedynczy klient z błędem w pętli potrafi zająć kanał należący do reszty.

Nieaktualny cache przy zmianach rozwiązał Varnish z niestandardowym VCL, unieważnianiem przez webhooki i Edge Side Includes dla sekcji dynamicznych, uzupełniony wersjonowaniem adresów. Edge Side Includes są tu odpowiedzią na konkretne napięcie: strona artykułu prawnego jest w 95 procentach identyczna dla wszystkich, a w 5 procentach zależy od tego, czy patrzy na nią osoba z otwartą sprawą. Bez podziału na fragmenty te pięć procent unieważnia buforowanie całej strony.

Zapotrzebowanie na zasoby w godzinach szczytu obsługuje auto-scaling na AWS EC2 z CloudWatch, uzupełniony o Cloudflare Rate Limiting przeciw nadmiernemu ruchowi botów. Auto-scaling ma tu wadę wynikającą wprost z kształtu ruchu opisanego na początku: reaguje na obciążenie, które już wystąpiło, a fala po odwołaniu lotów narasta w kilka minut. Dlatego progi są ustawione niżej, niż wynikałoby to z rachunku kosztów w spokojny dzień, i to jest świadomie kupiony zapas, a nie niedopatrzenie.

#Dostępność i odporność formularza

Zgodność z WCAG 2.1 w serwisie odszkodowawczym nie sprowadza się do kontrastu i tekstów alternatywnych. Najważniejszym elementem dostępnym jest tu formularz wielokrokowy, a w nim rzeczy, których audyt automatyczny nie wychwyci: czy komunikat o błędzie jest powiązany z polem, którego dotyczy, czy zmiana kroku przenosi fokus na nagłówek nowego kroku, czy czytnik ekranu ogłasza postęp. Formularz, który po błędzie walidacji przewija stronę na górę bez przesunięcia fokusu, jest dla osoby korzystającej z klawiatury pętlą bez wyjścia, a przechodzi każdy automatyczny test.

Odporność na utratę połączenia potraktowałem jako wymaganie funkcjonalne, nie jako udogodnienie. Stan formularza zapisuje się lokalnie po każdym kroku, więc zerwanie sesji w hali odlotów nie kasuje piętnastu minut pracy. Koszt tej decyzji jest realny i trzeba go nazwać: dane zgłoszenia zawierają numer rejsu i dane osobowe, a ich przechowywanie po stronie przeglądarki wymaga własnego czyszczenia po wysłaniu wniosku i jasnej reguły, po jakim czasie porzucony szkic znika. Bez tej reguły wygoda zamienia się w zaległość zgodnościową.

Trzecia rzecz dotyczy walidacji. Kusi, żeby odrzucać numer rejsu, który nie pasuje do wzorca, natychmiast i po stronie przeglądarki. W praktyce wzorce różnią się między przewoźnikami, a pasażer przepisuje numer z fotografii karty pokładowej zrobionej pod kątem. Ostrzeżenie, które nie blokuje wysyłki, przyjmuje zgłoszenie od osoby, która pomyliła zero z literą, a twarda walidacja to zgłoszenie odrzuca i nikt się o tym nie dowiaduje.

#Zastosowane technologie

Yoast SEO odpowiada za metadane, dynamiczne mapy XML i powiadamianie wyszukiwarek o aktualizacjach. UpdraftPlus prowadzi kopie na Amazon S3 z replikacją i szyfrowaniem AES-256. Cloudflare zapewnia warstwę brzegową z Argo Smart Routing, kompresją Brotli i ochroną przed DDoS przez ograniczanie tempa żądań. Redis buforuje w pamięci, z podziałem na sesje, formularze i panel. Varnish buforuje po stronie serwera, z własnym VCL, trybem grace i ESI dla bloków dynamicznych. Tryb grace zasługuje na osobne zdanie, bo pozwala oddać nieco nieświeżą wersję strony w chwili, gdy backend nie odpowiada, zamiast oddać błąd. Przy ruchu, który potrafi wejść falą, to różnica między serwisem wolniejszym a serwisem niedostępnym.

Lighthouse prowadzi audyty Core Web Vitals wpięte w potok CI/CD w Jenkinsie. RabbitMQ kolejkuje zadania takie jak przetwarzanie API i wysyłka maili, z ponowieniami i kolejką martwych wiadomości. Elasticsearch obsługuje wyszukiwanie lotów i treści z dopasowaniem rozmytym oraz agregacją. Dopasowanie rozmyte nie jest tu udogodnieniem, tylko koniecznością: numer rejsu bywa przepisany z fotografii karty pokładowej, a nazwa lotniska zapisana na dwadzieścia sposobów w dwudziestu czterech językach. Fastly rozkłada dystrybucję multimediów geograficznie, a Kafka obsługuje strumień danych czasu rzeczywistego z partycjonowaniem.

#Zarządzanie i wsparcie techniczne

AirHelp wymaga ciągłej optymalizacji i wsparcia. Regularnie aktualizuję system i wtyczki, testując zmiany na środowisku testowym z kopiami na Amazon S3. Środowisko testowe jest tu kopią produkcji, nie czystą instalacją, i to rozróżnienie decyduje o wartości testu. Zapytanie, które na dwustu zgłoszeniach zwraca się natychmiast, przy milionach rekordów potrafi urosnąć o dwa rzędy wielkości, a na pustej bazie nikt tego nie zobaczy. To samo dotyczy skuteczności cache, mierzalnej wyłącznie na realnym rozkładzie wejść.

Cloudflare, Redis i Fastly odpowiadają za wydajność pod ruchem globalnym, a Varnish, RabbitMQ i Kafka stabilizują procesy dynamiczne. Monitoring opiera się na Elasticsearch i CloudWatch, zapytania SQL i NoSQL są przeglądane pod kątem indeksów, a cache unieważniany przy zmianach treści. Najważniejszym sygnałem w tym monitoringu nie jest czas odpowiedzi, tylko skuteczność cache, bo przy serwisie tego kształtu chybienie prowadzi żądanie do źródła niezależnie od tego, jak dobrze źródło zostało dostrojone.

Platforma może zostać rozbudowana o integracje z systemami ERP, moduł analizy lotów czy sekcję raportów prawnych. Każde z tych rozszerzeń wchodzi jednak w ten sam konflikt, który przewija się przez cały projekt: im więcej treści zależy od tożsamości odwiedzającego, tym mniej da się obsłużyć na brzegu sieci, a wtedy decyzje z tego wdrożenia trzeba przeliczyć od nowa, zamiast przenieść.

Planujesz witrynę dla swojej firmy usługowej? Potrzebujesz skalowalnej platformy z zaawansowanym wsparciem technicznym? Jako specjalista WordPress pomagam w realizacji złożonych projektów. Napisz z założeniami projektu i opisz cel, ograniczenia oraz aktualny stan projektu.

FAQ do artykułu

Często zadawane pytania

Najważniejsze odpowiedzi, które pomagają wdrożyć temat w praktyce.

SEO-readyGEO-readyAEO-ready4 Q&A
Jakiego zakresu dotyczył projekt Airhelp?#
Airhelp to realizacja z kategorii Strony www, oddana w 2019 roku. Stoi za nią: WordPress, React, Next.js, GraphQL oraz Redis.
Jak wyglądał przebieg prac przy Airhelp?#
Wdrożenie zajęło około sześciu tygodni i ruszyło w 2019 roku. Stoi na: WordPress, React, Next.js, GraphQL oraz Redis. Układ dostarczył klient. Na jego podstawie zbudowałem szablony i model treści, a ścieżki, którymi idzie ruch, sprawdziłem na kopii produkcji, nie na pustej instalacji.
Co było najtrudniejsze technicznie w Airhelp?#
Najwięcej uwagi pochłonęło spięcie: WordPress, React, Next.js, GraphQL oraz Redis. Treść, konfiguracja i kod siedzą w osobnych warstwach, więc wycofanie zmian po starcie rusza jedną z nich, a nie wszystkie trzy. Przypadki brzegowe wychodzą na kopii produkcji i tam idą testy.
Co z projektu Airhelp da się wykorzystać przy kolejnym wdrożeniu?#
Przenosi się warstwa techniczna: WordPress, React, Next.js, GraphQL oraz Redis. Na kolejnym wdrożeniu wygląda podobnie. Nie przenosi się model treści tego projektu ani jego integracje, bo powstały pod dane jednego klienta i pod brief z kategorii Strony www. Kolejne wdrożenie zaczyna się od analizy zakresu, a wycena idzie po niej.

Potrzebujesz FAQ dopasowanego do branży i rynku? Przygotujemy wersję pod Twoje cele biznesowe.

Porozmawiajmy