CodeGym /Kursy /ChatGPT Apps /Pierwszy smoke test: „Hello widget” i openExternal

Pierwszy smoke test: „Hello widget” i openExternal

ChatGPT Apps
Poziom 2 , Lekcja 4
Dostępny

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:

  1. Twój kod widżetu (React/Next.js).
  2. Serwer deweloperski Next.js.
  3. Tunel (ngrok/Cloudflare).
  4. 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:

  1. Zapisujesz plik.
  2. Serwer deweloperski Next.js (już uruchomiony przez npm run dev) przeładowuje potrzebne moduły, HMR odświeża stronę.
  3. 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:

  1. Otwórz ChatGPT w przeglądarce, wybierz właściwy model (zwykle GPT‑5.1 lub to, co ustawiono domyślnie dla Dev Mode).
  2. Wyraźnie wybierz swoją aplikację (przez menu Apps/Developer) lub „przywołaj” ją frazą w rodzaju: „Uruchom aplikację GiftGenius”.
  3. 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:

  1. Zmieniasz JSX.
  2. Zapisujesz.
  3. 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:

  1. Sprawdza, czy adres URL jest dozwolony przez polityki.
  2. Może pokazać użytkownikowi ostrzeżenie (na przykład, że to zewnętrzna strona).
  3. 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:

  1. Upewnij się, że serwer dev jest uruchomiony (npm run dev) i widzisz Hello from GiftGenius pod adresem http://localhost:3000/widget.
  2. Upewnij się, że tunel do portu 3000 działa i publiczny URL otwiera się z zewnętrznej przeglądarki.
  3. 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).
  4. Otwórz czat, wybierz App (lub poproś model, aby go uruchomił).
  5. Upewnij się, że we wbudowanym widżecie widać „Hello from GiftGenius”.
  6. 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:

  1. 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.
  2. 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';.
  3. 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.

1
Ankieta/quiz
Pierwsza aplikacja ChatGPT, poziom 2, lekcja 4
Niedostępny
Pierwsza aplikacja ChatGPT
Pierwsza aplikacja ChatGPT: szablon, Dev Mode, tunel
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION