1. Czym jest smoke test dla ChatGPT App
W zwykłym świecie tworzenia aplikacji webowych smoke test — to minimalna weryfikacja „czy system w ogóle żyje?”. Strona się otwiera, przyciski nie wywalają błędów, nic krytycznego się nie pali.
W świecie ChatGPT Apps smoke test jest nieco ciekawszy, ponieważ w łańcuchu uczestniczy od razu kilka ogniw:
- Twój kod widżetu (React/Next.js).
- Serwer deweloperski Next.js.
- Tunel (ngrok/Cloudflare).
- ChatGPT, które tworzy iframe i ładuje twój widżet do środka czatu.
Dla nas dobry smoke test to sytuacja, gdy:
- widżet renderuje się w ChatGPT bez błędów;
- działa podstawowa interaktywność (na przykład klik przycisku — otwiera się zewnętrzny link);
- ani w konsoli przeglądarki, ani w logach serwera deweloperskiego nie ma czerwonej lawiny błędów.
Ważne: na tym etapie nie testujemy jeszcze narzędzi MCP, nie robimy testów obciążeniowych i nie liczymy pieniędzy za tokeny. Nasze zadanie jest skromne i bardzo praktyczne: udowodnić, że łańcuch „kod → Next.js → tunel → ChatGPT → użytkownik” w ogóle się domknął.
Wygodnie jest wyobrazić to sobie jako taką tabelkę:
| Co sprawdzamy | Jak poznać, że wszystko jest w porządku |
|---|---|
| Renderowanie widżetu | W ChatGPT widać nasze UI, a nie „zepsuty iframe” |
| Łączność ChatGPT ↔ nasz serwer | Brak błędów „nie mogę załadować aplikacji” |
| Działanie JS w piaskownicy | Obsługiwacze onClick faktycznie się wykonują |
| Możliwość otworzenia zewnętrznego linku | Przycisk otwiera nową kartę/okno z podanym adresem URL |
2. Nasza aplikacja szkoleniowa: proste „Hello GiftGenius”
W tym kursie stopniowo budujemy aplikację GiftGenius — asystenta do doboru prezentów. Na tym etapie nie dobiera jeszcze nic, ale potrafi przynajmniej grzecznie się przywitać i pokazać link „dowiedz się więcej”.
Potrzebujemy minimalnego, ale uczciwego widżetu: bez skomplikowanej logiki, ale z żywym kodem Reacta.
Najprostsza wersja komponentu widżetu może wyglądać tak (nazwę i style możesz dostosować do siebie, ale weźmiemy bazę z planu kursu):
// app/widget/page.tsx
'use client';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
To twój pierwszy ChatGPT App. Dalej nauczymy go dobierać prezenty.
</p>
</main>
);
}
Kilka ważnych uwag.
Po pierwsze, dyrektywa 'use client'; na początku pliku sprawia, że komponent jest kliencki. Bez niej Next.js traktuje plik jako komponent serwerowy i nie będziesz mógł używać window, obsługiwaczy onClick i w ogóle żadnego API przeglądarki.
Po drugie, to zwykły komponent React. Nie widać w nim żadnej „magii Apps SDK” — i to uczciwe. Cała magia tego, że trafia on do wnętrza ChatGPT, ukryta jest w konfiguracji serwera MCP i narzędzia, które zwraca link do URL widżetu. Tym zajmiemy się później, teraz interesuje nas tylko UI.
3. Osadzamy widżet w szablonie i uruchamiamy
W oficjalnym szablonie Next.js dla Apps SDK strona widżetu zwykle już istnieje; albo ją edytujesz, albo tworzysz swoją pod potrzebną ścieżką (na przykład /widget).
Załóżmy, że masz właśnie app/widget/page.tsx i zastępujesz jego zawartość kodem powyżej. Dalej łańcuch wygląda tak:
- Zapisujesz plik.
- Serwer deweloperski Next.js (już uruchomiony przez npm run dev) przeładowuje potrzebne moduły, HMR odświeża stronę.
- Przez tunel twój publiczny adres HTTPS pod tą samą ścieżką /widget zaczyna serwować zaktualizowany UI.
Można to sprawdzić na dwa sposoby.
Najpierw po staroświecku — w lokalnej przeglądarce. Otwierasz:
http://localhost:3000/widget
i widzisz to samo Hello from GiftGenius. Tak, to jeszcze nie ChatGPT, po prostu upewniasz się, że UI twojej aplikacji Next.js żyje.
Następnie — przez tunel. Bierzesz przydzielony URL (coś w rodzaju https://witty-cat.ngrok-free.app), dopisujesz /widget i otwierasz w zwykłej przeglądarce:
https://witty-cat.ngrok-free.app/widget
Jeśli wszystko jest dobrze, strona powinna wyglądać tak samo. To znaczy, że łańcuch „Next.js → tunel → twoja przeglądarka” działa, pozostało wstawić między nie ChatGPT.
4. Sprawdzamy widżet wewnątrz ChatGPT
ChatGPT w Dev Mode, w gruncie rzeczy, robi trzy kroki: tworzy iframe, ustawia w nim src na twój publiczny URL i pozwala temu iframe'owi żyć wewnątrz wiadomości czatu.
Uproszczony przebieg wygląda tak:
sequenceDiagram
participant Dev as Ty (Dev)
participant Next as Dev-serwer Next.js
participant Tun as Tunel (HTTPS)
participant GPT as ChatGPT
participant User as Użytkownik
Dev->>Next: npm run dev (http://localhost:3000)
Dev->>Tun: Uruchomienie tunelu do portu 3000
GPT->>Tun: GET https://.../widget
Tun->>Next: Proxy na http://localhost:3000/widget
Next-->>Tun: HTML + JS widżetu
Tun-->>GPT: Odpowiedź z HTML/JS
GPT->>User: Render iframe z widżetem
Aby zobaczyć wynik, wykonaj:
- Otwórz ChatGPT w przeglądarce, wybierz właściwy model (zwykle GPT‑5.1 lub to, co ustawiono domyślnie dla Dev Mode).
- Wyraźnie wybierz swoją aplikację (przez menu Apps/Developer) lub „przywołaj” ją frazą w rodzaju: „Uruchom aplikację GiftGenius”.
- ChatGPT wywołuje twój App, serwer MCP zwraca odpowiedź zawierającą link do UI (ten właśnie /widget) i w wiadomości czatu pojawia się twój widżet.
Jeśli wszystko jest w porządku, zobaczysz znajomy nagłówek „Hello from GiftGenius” prosto w ChatGPT. Na tym etapie smoke test jest prawie zaliczony: iframe się renderuje, łańcuch „Next.js → tunel → ChatGPT” żyje. Zostało sprawdzić ostatni punkt z naszej tabelki — że widżet potrafi przewidywalnie otwierać zewnętrzny link. Do tego potrzebny będzie openExternal.
Nieco później, gdy zaczniesz zmieniać kod, normalny cykl developerski będzie wyglądał tak:
- Zmieniasz JSX.
- Zapisujesz.
- Albo odświeżasz kartę ChatGPT, albo (czasem) wystarczy po prostu „poruszyć” widżet — na przykład wysłać nową wiadomość lub jeszcze raz uruchomić App (w zależności od tego, jak skonfigurowany jest twój szablon i cache).
Jeśli zmian nie widać, najpierw pomyśl o trzech podejrzanych: serwer dev nie jest uruchomiony, tunel się rozłączył albo ChatGPT podłączony jest do starego URL. W rozdziale „Gdzie szukać błędów, jeśli coś poszło nie tak” rozważymy ten scenariusz dokładniej.
5. Dlaczego nie wystarczy po prostu wstawić <a href> i zapomnieć
Aby zrealizować ostatni punkt naszego smoke testu — przycisk, który otwiera zewnętrzną stronę — musimy zrozumieć openExternal. Pojawia się logiczne pytanie: „Po co w ogóle ten openExternal? Co przeszkadza zrobić zwykły link?”
Problem w tym, że twój widżet żyje nie „po prostu w przeglądarce”, lecz w iframe pod kontrolą ChatGPT. Ten iframe działa w dość restrykcyjnej piaskownicy: mogą obowiązywać ograniczenia Content Security Policy, atrybuty sandbox, dziwności z target="_blank" i blokowaniem wyskakujących okienek. W rezultacie zachowanie <ahref="…"> lub window.open() wewnątrz takiego iframe może okazać się nieprzewidywalne: od całkowitego zignorowania po wyskakujące ostrzeżenia, których twój kod nie kontroluje.
Ponadto, z punktu widzenia UX, OpenAI chce kontrolować, kiedy i jak otwierasz zewnętrzne strony. Dlatego Apps SDK udostępnia ujednolicony most window.openai: twój kod nie wchodzi bezpośrednio w okno nadrzędne, lecz deleguje działanie aplikacji‑gospodarzowi poprzez jasno opisane API.
6. API window.openai.openExternal: co to jest i jak działa
W piaskownicy widżetu dostępny jest globalny obiekt window.openai. To główny „most” między twoim UI a ChatGPT: przez niego można wywoływać narzędzia, wysyłać wiadomości follow‑up, zmieniać tryb wyświetlania, zarządzać stanem widżetu i, rzecz jasna, otwierać zewnętrzne linki.
W tej lekcji interesuje nas jedna konkretna metoda:
window.openai.openExternal({ href: string }): void;
Gdy wywołujesz window.openai.openExternal({ href: 'https://example.com' }), ChatGPT:
- Sprawdza, czy adres URL jest dozwolony przez polityki.
- Może pokazać użytkownikowi ostrzeżenie (na przykład, że to zewnętrzna strona).
- Otwiera link w nowej karcie/oknie przeglądarki użytkownika.
Ważne są dwie rzeczy.
Po pierwsze, to czysto kliencka operacja. Nie wywołuje narzędzi MCP, nie chodzi do twojego backendu i nie zużywa tokenów OpenAI. To po prostu sygnał do aplikacji‑gospodarza: „proszę, otwórz ten URL”.
Po drugie, taki sposób jest zgodny z piaskownicą. ChatGPT sam decyduje, jak dokładnie otworzyć link, nie pozwalając twojemu iframe'owi przesadzić z window.open().
7. Dodajemy przycisk z openExternal do naszego widżetu
Nauczmy się teraz otwierać zewnętrzny link z naszego „Hello GiftGenius”. Najprostszy scenariusz: przycisk „Otwórz link demo”, który prowadzi, na przykład, do dokumentacji lub landing page'a twojej usługi.
Na początek napiszmy mały helper, aby TypeScript nie narzekał i aby widżet nie wywalił się, jeśli otworzysz /widget bezpośrednio w przeglądarce (gdzie window.openai jeszcze nie istnieje):
// app/widget/openExternalSafe.ts
export function openExternalSafe(href: string) {
if (typeof window !== 'undefined' && (window as any).openai?.openExternal) {
(window as any).openai.openExternal({ href });
} else {
// Fallback do lokalnego podglądu bez ChatGPT
window.open(href, '_blank', 'noopener,noreferrer');
}
}
Tutaj celowo używam (window as any), aby nie obciążać cię typizacją window.openai. Nieco później w kursie starannie opiszemy interfejs tego obiektu. Na razie wystarczy nam, żeby kod się kompilował i działał.
Teraz podepniemy helper do naszego widżetu i dodamy przycisk:
// app/widget/page.tsx
'use client';
import { openExternalSafe } from './openExternalSafe';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
To twój pierwszy ChatGPT App. Dalej nauczymy go dobierać prezenty.
</p>
<button
type="button"
onClick={() => openExternalSafe('https://example.com')}
style={{
padding: '8px 16px',
borderRadius: 8,
border: '1px solid #ccc',
cursor: 'pointer',
}}
>
Otwórz link demo
</button>
</main>
);
}
Co będzie się działo po kliknięciu.
Jeśli widżet działa wewnątrz ChatGPT, window.openai.openExternal istnieje i ChatGPT otworzy https://example.com tak, jak przewidują zasady.
Jeśli otworzyłeś http://localhost:3000/widget w zwykłej przeglądarce, window.openai nie ma i zadziała fallback: otworzy się nowa karta zwykłymi środkami przeglądarki. Tutaj window.open jest używany tylko przy bezpośrednim otwarciu /widget w zwykłej przeglądarce, czyli już nie w piaskownicy ChatGPT. W tym kontekście działa jak zwykle i nie sprawia problemów.
Bardziej szczegółowo omówimy openExternal w module 3 (osobna lekcja o widżecie i piaskownicy), więc teraz można śmiało przechodzić do uruchomienia aplikacji.
8. Mini smoke test end‑to‑end
Teraz można zrobić pełny „bojowy” przebieg. Spróbujmy przejść wszystkie kroki:
- Upewnij się, że serwer dev jest uruchomiony (npm run dev) i widzisz Hello from GiftGenius pod adresem http://localhost:3000/widget.
- Upewnij się, że tunel do portu 3000 działa i publiczny URL otwiera się z zewnętrznej przeglądarki.
- Otwórz ChatGPT, włącz Dev Mode i upewnij się, że twój App jest podłączony do właściwego URL (publicznego, a nie localhost).
- Otwórz czat, wybierz App (lub poproś model, aby go uruchomił).
- Upewnij się, że we wbudowanym widżecie widać „Hello from GiftGenius”.
- Kliknij przycisk „Otwórz link demo” i upewnij się, że w przeglądarce otworzył się https://example.com (albo twój adres).
Jeśli to wszystko zadziałało, to znaczy:
- HTML/JS widżetu są poprawnie budowane i serwowane przez serwer Next.
- Tunel HTTPS poprawnie proxy'uje żądania.
- ChatGPT ufa twojemu URL i potrafi załadować widżet.
- window.openai działa i przekazuje polecenie otwarcia zewnętrznego linku.
To dokładnie to, czego chcieliśmy od pierwszego smoke testu.
9. Gdzie szukać błędów, jeśli coś poszło nie tak
W odróżnieniu od „zwykłego” frontendu masz tutaj tylko trzy główne miejsca diagnostyki. Ważne jest szybko rozumieć, w którym z nich dokładnie wszystko się zepsuło:
- Najpierw spójrz na UI w ChatGPT. Jeśli zamiast widżetu widzisz komunikat o błędzie w rodzaju „Error loading app” lub „We had trouble talking to your app”, problem najpewniej leży w tunelu lub w dostępności twojego serwera deweloperskiego. Spróbuj otworzyć publiczny URL bezpośrednio w przeglądarce: jeśli się nie otwiera lub otwiera się z błędem Next.js, to to leczymy w pierwszej kolejności.
- Następnie otwórz DevTools przeglądarki na karcie, gdzie działa ChatGPT. Tam jest osobny iframe dla twojego widżetu, a w środku — znajoma zakładka Console. Jeśli przy kliknięciu przycisku z openExternal nic się nie dzieje, zobacz, czy nie ma błędów typu „window.openai is undefined” lub innych błędów JS. Jeśli taki błąd jest — najpewniej próbujesz widżetu nie w ChatGPT (a bezpośrednio po URL tunelu) albo zapomniałeś o dyrektywie 'use client';.
- Równolegle patrz na terminal z npm run dev. Jeśli sypią się tam błędy budowania (TypeScript, ESLint, kompilacja), to w najlepszym razie ChatGPT zobaczy starą wersję kodu, w najgorszym — nie zobaczy nic. Jeśli błędów nie ma, ale nie widzisz aktualizacji, upewnij się, że tunel wciąż jest aktywny: wiele usług tunelowych zamyka sesje po czasie bezczynności.
Jest jeszcze jeden typowy przypadek: wszystko działa na localhost, ale przy dostępie przez tunel dostajesz 404 lub dziwną stronę. Wtedy dokładnie sprawdź ścieżkę bazową (/widget vs /), ustawienia basePath/assetPrefix (jeśli już je zmieniałeś) i adres wpisany w Dev Mode.
10. Trochę o „porządkach”: zatrzymywanie procesów
To drobiazg, ale w praktyce bardzo przydatny. Nowicjusze często zapominają, że i serwer dev, i tunel — to osobne procesy, które dalej żyją w tle.
Jeśli nagle „port 3000 jest już zajęty”, możliwe, że gdzieś w czeluściach terminali ukrył się stary npm run dev. Na Windows to czasem zamienia się w „tańce” wokół menedżera zadań, na macOS i Linux ratuje Ctrl + C w tym terminalu, w którym proces został uruchomiony.
To samo z tunelem: jeśli poeksperymentowałeś z kilkoma tunelami pod rząd albo zapomniałeś zamknąć stary, łatwo się pomylić, do którego dokładnie URL jest teraz podpięty twój App w Dev Mode. Lepiej wyrobić sobie nawyk: zamierzasz kończyć sesję — rozłącz tunel, zatrzymaj serwer dev i przy następnym uruchomieniu zacznij z czystej kartki.
11. Typowe błędy przy pierwszym smoke teście
Błąd nr 1: używanie localhost zamiast publicznego adresu HTTPS.
Częsta historia: w Dev Mode przypadkowo podajesz http://localhost:3000 albo w ogóle zapominasz o tunelu. Na twojej maszynie wszystko działa, ale ChatGPT, działający w chmurze, fizycznie nie może dostać się do localhost. Lekarstwo jest proste: sprawdzaj, że w ustawieniach App podany jest publiczny adres HTTPS tunelu, i to z właściwą ścieżką (/mcp lub root — w zależności od szablonu).
Błąd nr 2: zapomniana dyrektywa 'use client'; w pliku widżetu.
Piszesz piękny kod Reacta, dodajesz onClick, odwołujesz się do window.openai, a Next.js po cichu robi ze strony komponent serwerowy. W najlepszym przypadku dostaniesz błąd „window is not defined”, w najgorszym — komponent w ogóle się nie zbuduje. Aby mieć dostęp do API przeglądarki, widżet musi być komponentem klienckim, o czym mówi pierwsza linia 'use client';.
Błąd nr 3: bezpośrednie wywołanie window.open() zamiast openExternal.
Czasem wydaje się prościej zrobić window.open('https://example.com'). W zwykłej przeglądarce to jeszcze zadziała, ale w piaskownicy ChatGPT dostaniesz nieprzewidywalne zachowanie: od całkowitego ignorowania po blokadę. Prawidłowa droga dla ChatGPT Apps — window.openai.openExternal({ href }), która deleguje otwarcie linku hostowi i respektuje wszystkie polityki bezpieczeństwa.
Błąd nr 4: TypeScript narzeka na window.openai, a deweloper „leczy” to wyłączeniem typów.
Czasem w desperacji ktoś pisze // @ts-nocheck na początku pliku. To usuwa błędy kompilacji, ale równocześnie wyłącza cały TypeScript w tym pliku. Znacznie bezpieczniej użyć punktowego as any wokół window, albo w osobnym pliku opisać minimalny interfejs dla window.openai. W tym module wybraliśmy mały helper openExternalSafe z (window as any), a staranną typizację dodamy później.
Błąd nr 5: oglądanie wyniku tylko na localhost, ale nie wewnątrz ChatGPT.
Bywa pokusa, by poprzestać na tym, że http://localhost:3000/widget się otwiera i uznać zadanie za wykonane. Ale sens tego modułu jest właśnie taki, by zobaczyć App wewnątrz ChatGPT. To, że w zwykłej przeglądarce wszystko jest dobrze, nie gwarantuje jeszcze, że ChatGPT poprawnie utworzy iframe, pobierze zasoby przez tunel i nie potknie się o CORS/CSP. Pełny smoke test zawsze obejmuje krok z realnym uruchomieniem App w interfejsie ChatGPT.
Błąd nr 6: zapomniany lub zerwany tunel.
Zaktualizowałeś kod, a w ChatGPT wisi stara wersja widżetu albo w ogóle nic się nie ładuje. Często okazuje się, że tunel zamknął się po czasie bezczynności, ale Developer Mode nadal wskazuje na stary URL. Jeśli przy otwieraniu adresu tunelu w zwykłej przeglądarce widzisz błąd — najpierw przywróć tunel, a dopiero potem podejrzewaj Apps SDK.
Błąd nr 7: ignorowanie konsoli w iframe.
Deweloperzy przyzwyczajeni do SPA patrzą na console.log w DevTools swojego aplikacji, ale w ChatGPT to iframe i trzeba wybrać właściwą ramkę w DevTools. Jeśli patrzysz tylko na poziom najwyższy, możesz nie zobaczyć żadnego błędu, choć wewnątrz widżetu wszystko już dawno jest czerwone. Nawyk „otwieraj DevTools właśnie na iframe‑widżecie” bardzo oszczędza nerwy.
GO TO FULL VERSION