"Loading indefinitely". Jak Apple odrzucił moją aplikację
· Nie jednym promptem · odcinek 2
Dwa słowa od recenzenta Apple: "loading indefinitely". Czyli kręci się i nic.
To było w połowie lipca. Zakupomat właśnie przestawał być aplikacją tylko dla nas i szedł do App Store - po raz pierwszy! Na moim telefonie wszystko działało. U Marty też. U recenzenta Apple tworzenie nowej rodziny kończyło się niekończącym się spinnerem.
W poprzednim odcinku napisałem, że przez chwilę nie wiedziałem dlaczego. Dziś opowiem o szczegółach. Bo to jest dokładnie ta część budowania aplikacji, której nie zobaczysz w filmiku "apka w 10 minut".
Pierwsza hipoteza: recenzent wyłączył internet
Pierwsza myśl była bardzo wygodna - recenzent wyłączył internet w tablecie, żeby sprawdzić, jak aplikacja zachowa się bez sieci. Klasyka testowania. A spinner, który kręci się w nieskończoność, podpowiadał resztę - żądanie wisi i nikt nie powiedział mu, kiedy ma się poddać. I rzeczywiście - żądanie utworzenia rodziny nigdy nie dotarło do serwera, a klient HTTP w aplikacji nie miał ustawionego timeoutu.
Plan był gotowy: timeout na żądaniach, komunikat "brak połączenia" i nowa wersja do Apple. Kilka minut roboty. Każde wywołanie API dostało limit czasu, 25 sekund. Build 10.
Mało brakowało, a na tym byśmy skończyli. Tylko że to nie była naprawa, tylko połowa naprawy. Aplikacja przestała wisieć w nieskończoność - teraz po 25 sekundach pokazywała "brak połączenia". My bylibyśmy przekonani, że sprawa załatwiona, a rodziny dalej nie dało się utworzyć.
I tu jest pierwsza lekcja, która dziś siedzi w pamięci projektu: brak timeoutu nie wygląda jak błąd sieci. Wygląda jak "aplikacja nie działa". Użytkownik ani recenzent nie zgłosi Ci "timeout". Zgłosi, że wisi.
Logi nie kłamią
Więc zajrzałem w logi serwera. I tu zrobiło się ciekawie.
Recenzent zalogował się i normalnie przeglądał aplikację. Serwer widział każde jego żądanie. I tylko żądanie utworzenia rodziny w ogóle nie dotarło do backendu. Dziwny moment na testowanie timeoutu przez recenzenta Apple. To mi dało do myślenia.
Zaczęliśmy sprawdzać dokładnie taką ścieżkę, jaką przechodził recenzent... i BUM! Aplikacja wisi!
Problem pojawiał się TYLKO w jednej, konkretnej sekwencji: zaloguj się → wyloguj → utwórz nową rodzinę. Kto normalnie robi coś takiego? Nikt! Recenzent Apple - zawsze! ;-)
Kasa zajęta przez klienta, który nigdy nie kończy zakupów
Zakupomat synchronizuje listę na żywo. Marta dopisuje mleko, a mi w sklepie od razu pojawia się na liście. Robi to długo otwarte połączenie z serwerem (SSE), które przez cały czas działania aplikacji czeka na zmiany (swoją drogą może w następnej wersji będę je ubijał, jak aplikacja jest w tle ;-)).
I to połączenie dzieliło z resztą aplikacji jednego klienta HTTP. Na iOS oznacza to jedną pulę połączeń. Długo żyjący strumień zajął ją na stałe, a żądanie utworzenia rodziny stało w kolejce, która nigdy się nie ruszała.
Najprościej - w sklepie jest jedna kasa, a przy niej stoi klient, który nigdy nie kończy zakupów. Wszyscy za nim czekają. W nieskończoność.
Poprawka? Dwa osobne klienty: jeden dla API, drugi dla strumienia na żywo. Build 11. A w buildzie 12 jeszcze ponawianie logowania i rejestracji, gdy połączenie się zatnie.
Trzy buildy naprawcze. Pierwszy o 17:45, ostatni o 18:22, tego samego dnia. Z Klaudiuszem da się tak pracować - ale tylko wtedy, gdy ktoś wie, czego szukać i zechce wyjść poza to, co sugeruje sam agent.
Czego nie da się złapać testem
Najbardziej niepokojące w tej historii jest to, że żaden test automatyczny tego nie złapał. I nie złapie! Testy klienta HTTP używają atrapy sieci, a atrapa nie ma puli połączeń. W testach wszystko było zielone.
Więc zamiast udawać, że test to załatwi, zrobiliśmy dwie rzeczy:
- Ręczny smoke test przed każdym wysłaniem do sklepu. Jedna z pozycji checklisty brzmi dokładnie: "login → logout → utwórz rodzinę". Sekwencja, która nas odrzuciła, jest teraz sprawdzana za każdym razem.
- Zapis w pamięci projektu. Decyzja "strumień na żywo ma własnego klienta HTTP" jest oznaczona jako krytyczna, a w polu "co by tę decyzję odwróciło" stoi: "Nic sensownego. Łączenie tych klientów dla oszczędności to regresja."
Ten drugi punkt jest ważniejszy, niż wygląda.
Dlaczego to zapisuję
Za pół roku ja tego nie będę pamiętał. Claude nie pamięta niczego między sesjami (no dobra! teraz już jest wspólna pamięć dla projektów w Claude Code, ale działa jak działa). A dwa klienty HTTP tam, gdzie mógłby być jeden, wyglądają jak oczywisty kandydat do "uproszczenia". Każdy porządny agent AI, który zobaczy taki kod bez kontekstu, zaproponuje, żeby je połączyć. I będzie brzmiał bardzo przekonująco.
Pamięć projektu jest po to, żeby agent zobaczył kontekst, zanim napisze pierwszą linijkę. Żeby powiedział: "to łamie decyzję D-007", zamiast po cichu wprowadzić z powrotem błąd, przez który Apple odrzucił aplikację.
To jest właśnie różnica między promptem a produktem. Prompt naprawia objaw. Produkt pamięta, dlaczego objaw się pojawił i bierze to pod uwagę.
Co to znaczy dla Twojego zespołu
Agent AI bardzo szybko naprawi to, co widać. Dodanie timeoutu to była dosłownie chwila. Ale objaw ("wisi") w niczym nie przypominał przyczyny ("dwa rodzaje ruchu dzielą jedną pulę połączeń"), a do przyczyny doprowadziły logi serwera i zrozumienie, jak działa platforma. Zespoły, które pracują z agentami, potrzebują nie tylko szybszego pisania kodu, ale też miejsca, w którym zapisują, dlaczego kod jest taki, jaki jest. I jednocześnie nie zapisują niczego, co można odczytać z kodu i szybko może się zmienić.
W ANSLAN pomagamy zbudować second brain w istniejących produktach - odtwarzamy wiedzę o systemie i zapisujemy ją w taki sposób, żeby korzystali z niej i ludzie, i agenty AI.
A sam Zakupomat możesz po prostu wziąć i używać: zakupomat.app. Za darmo, bez reklam, bez sprzedaży danych i bez abonamentu. Android, iPhone i przeglądarka.
Do zobaczenia za tydzień :-)