1. Що таке smoke‑тест для ChatGPT App
У звичній веброзробці smoke‑тест — це мінімальна перевірка на кшталт «чи взагалі жива система?». Сторінка відкривається, кнопки не «падають», нічого критичного не ламається.
У світі застосунків ChatGPT smoke‑тест трохи цікавіший, адже в ланцюжку беруть участь одразу кілька ланок:
- Ваш код віджета (React/Next.js).
- dev‑сервер Next.js.
- Тунель (ngrok/Cloudflare).
- 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, і ви замінюєте його вміст на код вище. Далі ланцюжок такий:
- Ви зберігаєте файл.
- dev‑сервер Next.js (уже запущений через npm run dev) перезапускає потрібні модулі, а HMR оновлює сторінку.
- Через тунель ваш публічний 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 з віджетом
Щоб побачити результат, ви:
- Відкриваєте ChatGPT у браузері, обираєте потрібну модель (зазвичай GPT‑5.1 або ту, що задано за замовчуванням для Dev Mode).
- Явно обираєте свій застосунок (через меню Apps/Developer) або запускаєте його фразою на кшталт: «Запусти застосунок GiftGenius».
- ChatGPT викликає ваш застосунок, MCP‑сервер повертає відповідь, що містить посилання на UI (той самий /widget), і в повідомленні чату з’являється ваш віджет.
Якщо все гаразд, ви бачите знайомий заголовок «Hello from GiftGenius» прямо всередині ChatGPT. На цьому етапі smoke‑тест майже пройдено: iframe рендериться, а ланцюжок «Next.js → тунель → ChatGPT» працює. Залишилося перевірити останній пункт із нашої таблиці — чи вміє віджет передбачувано відкривати зовнішнє посилання. Для цього нам знадобиться openExternal.
Трохи пізніше, коли ви почнете змінювати код, звичний цикл розробки виглядатиме так:
- Змінюєте JSX.
- Зберігаєте.
- Оновлюєте вкладку 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:
- Перевіряє, що URL дозволений політиками.
- Може показати користувачеві попередження (наприклад, що це зовнішній сайт).
- Відкриває посилання в новій вкладці/вікні браузера користувача.
Важливо зрозуміти дві речі.
По‑перше, це суто клієнтська операція. Вона не викликає 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
Тепер можна зробити повноцінний «бойовий» прогін. Спробуйте пройти всі кроки:
- Переконайтеся, що dev‑сервер запущено (npm run dev) і ви бачите Hello from GiftGenius на http://localhost:3000/widget.
- Переконайтеся, що тунель до порту 3000 піднято, а публічний URL відкривається у звичайному браузері.
- Відкрийте ChatGPT, увімкніть Dev Mode і переконайтеся, що ваш застосунок підключено до правильного URL (публічного, а не localhost).
- Відкрийте чат, оберіть застосунок (або попросіть модель запустити його).
- Переконайтеся, що у вбудованому віджеті видно «Hello from GiftGenius».
- Натисніть кнопку «Відкрити демо‑посилання» і переконайтеся, що в браузері відкрився https://example.com (або ваша адреса).
Якщо все це спрацювало, то:
- HTML/JS віджета правильно збирається й віддається Next‑сервером.
- HTTPS‑тунель коректно проксіює запити.
- ChatGPT довіряє вашому URL і вміє завантажувати віджет.
- window.openai працює й передає команду на відкриття зовнішнього посилання.
Це саме те, чого ми й хотіли від першого smoke‑тесту.
9. Де шукати помилки, якщо щось пішло не так
На відміну від «звичайного» фронтенду, тут у вас є лише три основні місця для діагностики. Важливо швидко розуміти, у якому з них саме все зламалося:
- Спершу подивіться на UI в ChatGPT. Якщо замість віджета ви бачите повідомлення про помилку на кшталт «Error loading app» або «We had trouble talking to your app», проблема, найімовірніше, у тунелі або в доступності вашого dev‑сервера. Спробуйте відкрити публічний URL напряму в браузері. Якщо він не відкривається або відкривається з помилкою Next.js, передусім виправляйте саме це.
- Потім відкривайте DevTools браузера на вкладці, де працює ChatGPT. Там є окремий iframe під ваш віджет, а всередині нього — знайома вкладка Console. Якщо під час натискання кнопки з openExternal нічого не відбувається, перевірте, чи немає помилок на кшталт «window.openai is undefined» або інших JS‑помилок. Якщо така помилка є, то, найімовірніше, ви тестуєте віджет не в ChatGPT, а напряму за URL тунелю, або забули директиву 'use client';.
- Паралельно дивіться в термінал із 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‑віджеті» дуже економить нерви.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ