JavaRush /Курси /ChatGPT Apps /Перший smoke‑тест: «Hello Widget» і openExternal

Перший smoke‑тест: «Hello Widget» і openExternal

ChatGPT Apps
Рівень 2 , Лекція 4
Відкрита

1. Що таке smoke‑тест для ChatGPT App

У звичній веброзробці smoke‑тест — це мінімальна перевірка на кшталт «чи взагалі жива система?». Сторінка відкривається, кнопки не «падають», нічого критичного не ламається.

У світі застосунків ChatGPT smoke‑тест трохи цікавіший, адже в ланцюжку беруть участь одразу кілька ланок:

  1. Ваш код віджета (React/Next.js).
  2. dev‑сервер Next.js.
  3. Тунель (ngrok/Cloudflare).
  4. ChatGPT, що створює iframe і завантажує ваш віджет усередину чату.

Для нас добрий smoke‑тест — це ситуація, коли:

  • віджет рендериться всередині ChatGPT без помилок;
  • базова інтерактивність працює (наприклад, натиснули кнопку — і відкрили зовнішнє посилання);
  • ні в консолі браузера, ні в логах dev‑сервера немає «червоної лавини» помилок.

Важливо: на цьому етапі ми ще не перевіряємо MCP‑інструменти, не проводимо навантажувального тестування й не рахуємо гроші за токени. Наше завдання скромне й дуже практичне: довести, що ланцюжок «код → Next.js → тунель → ChatGPT → користувач» узагалі замкнувся.

Зручно уявити це як таблицю:

Що перевіряємо Як зрозуміти, що все гаразд
Рендеринг віджета У ChatGPT видно наш UI, а не «зламаний» iframe
Зв’язок ChatGPT ↔ наш сервер Немає помилок на кшталт «не можу завантажити застосунок»
Робота JS у пісочниці Обробники onClick справді виконуються
Можливість відкрити зовнішнє посилання Кнопка відкриває нову вкладку або вікно із зазначеним URL

2. Наш навчальний App: простий «Hello GiftGenius»

У цьому курсі ми поступово будуємо застосунок GiftGenius — помічник із добору подарунків. На цьому кроці він ще нічого не добирає, зате вже вміє бодай чемно привітатися й показати посилання «дізнатися більше».

Нам потрібен мінімальний, але «чесний» віджет: без складної логіки, зате з живим React‑кодом.

Найпростіший варіант компонента віджета може мати такий вигляд (імʼя і стилі ви можете підлаштувати під себе, але ми візьмемо базу з плану курсу):


// 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 }}>
        Це ваш перший ChatGPT App. Далі ми навчимо його підбирати подарунки.
      </p>
    </main>
  );
}

Кілька важливих моментів.

По‑перше, директива 'use client'; на початку файлу робить компонент клієнтським. Без неї Next.js трактує файл як серверний компонент, і ви не зможете використовувати window, обробники onClick та взагалі будь‑який браузерний API.

По‑друге, це звичайний React‑компонент. Жодної «магії Apps SDK» у ньому не видно — і це нормально. Уся магія того, що він опиняється всередині ChatGPT, схована в конфігурації MCP‑сервера та інструменті, який повертає посилання на URL віджета. Цим ми займемося пізніше, а зараз нас цікавить лише UI.

3. Вбудовуємо віджет у шаблон і запускаємо

В офіційному шаблоні Next.js для Apps SDK сторінка віджета зазвичай уже існує. Ви або редагуєте її, або створюєте власну — за потрібним маршрутом (наприклад, /widget).

Припустімо, що у вас уже є app/widget/page.tsx, і ви замінюєте його вміст на код вище. Далі ланцюжок такий:

  1. Ви зберігаєте файл.
  2. dev‑сервер Next.js (уже запущений через npm run dev) перезапускає потрібні модулі, а HMR оновлює сторінку.
  3. Через тунель ваш публічний HTTPS‑URL за тим самим шляхом /widget починає віддавати оновлений UI.

Перевірити це можна двома способами.

Спершу — «по‑старому», у локальному браузері. Відкрийте:

http://localhost:3000/widget

і побачите той самий Hello from GiftGenius. Так, це ще не ChatGPT: ви просто переконуєтеся, що UI вашого Next.js‑застосунку живий.

Потім — через тунель. Берете виданий URL (щось на кшталт https://witty-cat.ngrok-free.app), дописуєте /widget й відкриваєте у звичайному браузері:

https://witty-cat.ngrok-free.app/widget

Якщо все гаразд, сторінка має виглядати так само. Отже, ланцюжок «Next.js → тунель → ваш браузер» працює — лишилося «вставити» ChatGPT між ними.

4. Перевіряємо віджет усередині ChatGPT

ChatGPT у режимі розробника (Dev Mode) фактично робить три кроки: створює iframe, встановлює в ньому src на ваш публічний URL і вбудовує цей iframe в повідомлення чату.

У спрощеному вигляді це так:

sequenceDiagram
    participant Dev as Ви (Dev)
    participant Next as Next.js dev-сервер
    participant Tun as Тунель (HTTPS)
    participant GPT as ChatGPT
    participant User as Користувач

    Dev->>Next: npm run dev (http://localhost:3000)
    Dev->>Tun: Запуск тунелю до порту 3000
    GPT->>Tun: GET https://.../widget
    Tun->>Next: Проксіювання на http://localhost:3000/widget
    Next-->>Tun: HTML + JS віджета
    Tun-->>GPT: Відповідь з HTML/JS
    GPT->>User: Рендер iframe з віджетом

Щоб побачити результат, ви:

  1. Відкриваєте ChatGPT у браузері, обираєте потрібну модель (зазвичай GPT‑5.1 або ту, що задано за замовчуванням для Dev Mode).
  2. Явно обираєте свій застосунок (через меню Apps/Developer) або запускаєте його фразою на кшталт: «Запусти застосунок GiftGenius».
  3. ChatGPT викликає ваш застосунок, MCP‑сервер повертає відповідь, що містить посилання на UI (той самий /widget), і в повідомленні чату з’являється ваш віджет.

Якщо все гаразд, ви бачите знайомий заголовок «Hello from GiftGenius» прямо всередині ChatGPT. На цьому етапі smoke‑тест майже пройдено: iframe рендериться, а ланцюжок «Next.js → тунель → ChatGPT» працює. Залишилося перевірити останній пункт із нашої таблиці — чи вміє віджет передбачувано відкривати зовнішнє посилання. Для цього нам знадобиться openExternal.

Трохи пізніше, коли ви почнете змінювати код, звичний цикл розробки виглядатиме так:

  1. Змінюєте JSX.
  2. Зберігаєте.
  3. Оновлюєте вкладку ChatGPT або (іноді) просто взаємодієте з віджетом — наприклад, надсилаєте нове повідомлення чи ще раз запускаєте застосунок (це залежить від того, як налаштовано ваш шаблон і кешування).

Якщо змін не видно, передусім згадайте про трьох «підозрюваних»: dev‑сервер не запущено, тунель обірвався або ChatGPT підʼєднаний до старого URL. У розділі «Де шукати помилки, якщо щось пішло не так» ми розберемо цей сценарій докладніше.

5. Чому не можна просто додати <a href> і забути

Щоб виконати останній пункт нашого smoke‑тесту — кнопку, яка відкриває зовнішню сторінку, — нам потрібно розібратися з openExternal. Цілком логічне питання: «А навіщо взагалі цей openExternal? Що заважає зробити звичайне посилання?»

Проблема в тому, що ваш віджет працює не «просто в браузері», а в iframe під керуванням ChatGPT. Цей iframe працює в досить суворій пісочниці: можуть діяти обмеження Content Security Policy, атрибути sandbox, нюанси з target="_blank" і блокування спливаючих вікон. У результаті поведінка <ahref="…"> або window.open() усередині такого iframe може виявитися непередбачуваною — від повного ігнорування до спливаючих попереджень, які не контролюються вашим кодом.

Крім того, з погляду користувацького досвіду OpenAI хоче контролювати, коли й як ви відкриваєте зовнішні сторінки. Тому Apps SDK надає уніфікований міст window.openai: ваш код не звертається напряму до батьківського вікна, а делегує дію хост‑застосунку за чітко описаним API.

6. API window.openai.openExternal: що це і як працює

У пісочниці віджета доступний глобальний об’єкт window.openai. Це основний «міст» між вашим UI і ChatGPT: через нього можна викликати інструменти, надсилати follow-up повідомлення, змінювати режим відображення, керувати станом віджета і, звісно, відкривати зовнішні посилання.

Нас у цій лекції цікавить один конкретний метод:

window.openai.openExternal({ href: string }): void;

Коли ви викликаєте window.openai.openExternal({ href: 'https://example.com' }), ChatGPT:

  1. Перевіряє, що URL дозволений політиками.
  2. Може показати користувачеві попередження (наприклад, що це зовнішній сайт).
  3. Відкриває посилання в новій вкладці/вікні браузера користувача.

Важливо зрозуміти дві речі.

По‑перше, це суто клієнтська операція. Вона не викликає MCP‑інструменти, не звертається до вашого бекенду й не витрачає токени OpenAI. Це просто сигнал хост‑застосунку: «будь ласка, відкрий ось цей URL».

По‑друге, такий спосіб сумісний із пісочницею. ChatGPT сам вирішує, як саме відкривати посилання і не дозволяє вашому iframe надміру покладатися на window.open().

7. Додаємо кнопку з openExternal у наш віджет

Тепер навчимося відкривати зовнішнє посилання з нашого «Hello GiftGenius». Найпростіший сценарій — кнопка «Відкрити демо‑посилання», яка веде, наприклад, на документацію або лендінг вашого сервісу.

Для початку напишемо невелику допоміжну функцію, щоб TypeScript не скаржився й щоб віджет не ламався, якщо ви раптом відкриєте /widget напряму в браузері (де window.openai ще немає):

// 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 {
    // Запасний варіант для локального перегляду без ChatGPT
    window.open(href, '_blank', 'noopener,noreferrer');
  }
}

Тут ми свідомо використовуємо (window as any), щоб не перевантажувати вас типізацією window.openai. Трохи пізніше в курсі ми акуратно опишемо інтерфейс цього об’єкта. Поки що нам достатньо, щоб код компілювався й працював.

Тепер підключимо цю функцію в наш віджет і додамо кнопку:

// 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 }}>
        Це ваш перший ChatGPT App. Далі ми навчимо його підбирати подарунки.
      </p>
      <button
        type="button"
        onClick={() => openExternalSafe('https://example.com')}
        style={{
          padding: '8px 16px',
          borderRadius: 8,
          border: '1px solid #ccc',
          cursor: 'pointer',
        }}
      >
        Відкрити демо‑посилання
      </button>
    </main>
  );
}

Що відбуватиметься під час натискання кнопки.

Якщо віджет запущено всередині ChatGPT, window.openai.openExternal існує, і ChatGPT відкриє https://example.com так, як це передбачено правилами.

Якщо ви відкрили http://localhost:3000/widget у звичайному браузері, window.openai немає, і спрацює запасний варіант (fallback): відкриється нова вкладка звичайними засобами браузера. Тут window.open використовується лише за прямого відкриття /widget у звичайному браузері, тобто вже поза пісочницею ChatGPT. У цьому контексті він працює як завжди й не створює жодних проблем.

Детальніше ми розберемо openExternal у модулі 3 (окрема лекція про віджет і пісочницю). Тож зараз можна сміливо переходити до запуску застосунку.

8. Міні‑smoke‑тест end‑to‑end

Тепер можна зробити повноцінний «бойовий» прогін. Спробуйте пройти всі кроки:

  1. Переконайтеся, що dev‑сервер запущено (npm run dev) і ви бачите Hello from GiftGenius на http://localhost:3000/widget.
  2. Переконайтеся, що тунель до порту 3000 піднято, а публічний URL відкривається у звичайному браузері.
  3. Відкрийте ChatGPT, увімкніть Dev Mode і переконайтеся, що ваш застосунок підключено до правильного URL (публічного, а не localhost).
  4. Відкрийте чат, оберіть застосунок (або попросіть модель запустити його).
  5. Переконайтеся, що у вбудованому віджеті видно «Hello from GiftGenius».
  6. Натисніть кнопку «Відкрити демо‑посилання» і переконайтеся, що в браузері відкрився https://example.com (або ваша адреса).

Якщо все це спрацювало, то:

  • HTML/JS віджета правильно збирається й віддається Next‑сервером.
  • HTTPS‑тунель коректно проксіює запити.
  • ChatGPT довіряє вашому URL і вміє завантажувати віджет.
  • window.openai працює й передає команду на відкриття зовнішнього посилання.

Це саме те, чого ми й хотіли від першого smoke‑тесту.

9. Де шукати помилки, якщо щось пішло не так

На відміну від «звичайного» фронтенду, тут у вас є лише три основні місця для діагностики. Важливо швидко розуміти, у якому з них саме все зламалося:

  1. Спершу подивіться на UI в ChatGPT. Якщо замість віджета ви бачите повідомлення про помилку на кшталт «Error loading app» або «We had trouble talking to your app», проблема, найімовірніше, у тунелі або в доступності вашого dev‑сервера. Спробуйте відкрити публічний URL напряму в браузері. Якщо він не відкривається або відкривається з помилкою Next.js, передусім виправляйте саме це.
  2. Потім відкривайте DevTools браузера на вкладці, де працює ChatGPT. Там є окремий iframe під ваш віджет, а всередині нього — знайома вкладка Console. Якщо під час натискання кнопки з openExternal нічого не відбувається, перевірте, чи немає помилок на кшталт «window.openai is undefined» або інших JS‑помилок. Якщо така помилка є, то, найімовірніше, ви тестуєте віджет не в ChatGPT, а напряму за URL тунелю, або забули директиву 'use client';.
  3. Паралельно дивіться в термінал із npm run dev. Якщо туди надходять помилки збірки (TypeScript, ESLint, компіляція), ChatGPT у кращому разі бачитиме стару версію коду, а в гіршому — не побачить нічого. Якщо помилок немає, але ви не бачите оновлення, переконайтеся, що тунель усе ще активний: багато сервісів тунелювання закривають сесії через тайм-аут бездіяльності.

Є ще один типовий випадок: усе працює на localhost, але під час звернення через тунель ви отримуєте 404 або незвичну сторінку. Тоді уважно перевірте базовий шлях (/widget проти /), налаштування basePath/assetPrefix (якщо ви вже їх змінювали) і адресу, прописану в Dev Mode.

10. Трохи про «прибирання»: зупинка процесів

Це дрібниця, але на практиці дуже корисна. Новачки часто забувають, що і dev‑сервер, і тунель — це окремі процеси, які продовжують працювати у фоновому режимі.

Якщо у вас раптом «порт 3000 уже зайнятий», можливо, десь у вкладках термінала залишився старий npm run dev. На Windows це інколи перетворюється на зайві клопоти з диспетчером завдань, а на macOS і Linux виручає Ctrl + C у тому терміналі, де процес запущено.

Те саме з тунелем: якщо ви поекспериментували з кількома тунелями поспіль або забули закрити старий, легко заплутатися, до якого саме URL зараз привʼязано ваш застосунок у Dev Mode. Краще виробити звичку: зібралися завершувати сесію — вимкніть тунель, зупиніть dev‑сервер, а під час наступного запуску починайте з чистого аркуша.

11. Типові помилки під час першого smoke‑тесту

Помилка № 1: використовувати localhost замість публічного HTTPS‑URL.
Поширена історія: у Dev Mode ви випадково вказуєте http://localhost:3000 або взагалі забуваєте про тунель. На вашій машині все працює, але ChatGPT, що живе в хмарі, фізично не може дістатися до localhost. Рішення просте: перевіряйте, що в налаштуваннях застосунку зазначено саме публічну HTTPS‑адресу тунелю — і з правильним шляхом (/mcp або корінь, залежно від шаблону).

Помилка № 2: забути директиву 'use client' у файлі віджета.
Ви пишете гарний React‑код, додаєте onClick, звертаєтеся до window.openai, а Next.js мовчки робить сторінку серверним компонентом. У кращому разі ви отримаєте помилку «window is not defined», у гіршому — компонент узагалі не збереться. Щоб мати доступ до браузерних API, віджет має бути клієнтським компонентом — про це й говорить перший рядок 'use client';.

Помилка № 3: прямий виклик window.open() замість openExternal.
Іноді здається простіше зробити window.open('https://example.com'). У звичайному браузері це ще може спрацювати, але всередині пісочниці ChatGPT ви отримаєте непередбачувану поведінку: від повного ігнорування до блокування. Правильний шлях для ChatGPT Apps — window.openai.openExternal({ href }), який делегує відкриття посилання хосту й дотримується всіх політик безпеки.

Помилка № 4: TypeScript нарікає на window.openai, і розробник «лікує» це вимкненням типів.
Іноді у відчаї люди пишуть // @ts-nocheck на початку файла. Це прибирає помилки компіляції, але водночас вимикає TypeScript у цьому файлі повністю. Значно безпечніше або використати точковий as any навколо window, або в окремому файлі описати мінімальний інтерфейс для window.openai. У цьому модулі ми обрали невелику допоміжну функцію openExternalSafe з (window as any), а акуратну типізацію додамо пізніше.

Помилка № 5: перегляд результату лише в localhost, але не всередині ChatGPT.
Буває спокуса обмежитися тим, що http://localhost:3000/widget відкривається, і вважати завдання виконаним. Але сенс цього модуля якраз у тому, щоб побачити застосунок усередині ChatGPT. Те, що в звичайному браузері все добре, ще не гарантує, що ChatGPT правильно створить iframe, підхопить ресурси через тунель і не упреться в CORS/CSP. Повноцінний smoke‑тест завжди включає крок із реальним запуском застосунку в інтерфейсі ChatGPT.

Помилка № 6: забутий або «обірвався» тунель.
Ви оновили код, а в ChatGPT висить стара версія віджета або взагалі нічого не завантажується. Часто виявляється, що тунель закрився через тайм-аут, але Developer Mode досі дивиться на старий URL. Якщо під час відкриття тунельного URL у звичайному браузері ви бачите помилку, спочатку відновіть тунель, а вже потім підозрюйте проблеми з Apps SDK.

Помилка № 7: ігнорування консолі в iframe.
Розробники з досвідом SPA звикли дивитися console.log у DevTools свого застосунку, але всередині ChatGPT це iframe, і потрібно обрати правильний фрейм у DevTools. Якщо дивитися лише на верхній рівень, ви можете не побачити жодної помилки, хоча всередині віджета все давно «червоне». Звичка «відкрити DevTools саме на iframe‑віджеті» дуже економить нерви.

1
Опитування
Перший застосунок ChatGPT, рівень 2, лекція 4
Недоступний
Перший застосунок ChatGPT
Перший застосунок ChatGPT: шаблон, Dev Mode, тунель
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ