1. Что такое smoke‑тест для ChatGPT App
В обычном веб‑разработческом мире smoke‑тест — это минимальная проверка «система вообще жива?». Страница открывается, кнопки не падают, ничего критичного не горит.
В мире ChatGPT Apps 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 вызывает ваш App, MCP‑сервер возвращает ответ, включающий ссылку на UI (тот самый /widget), и в сообщении чата появляется ваш виджет.
Если всё хорошо, вы видите знакомый заголовок «Hello from GiftGenius» прямо внутри ChatGPT. На этом этапе smoke‑тест почти пройден: iframe рендерится, цепочка «Next.js → туннель → ChatGPT» жива. Осталось проверить последний пункт из нашей таблички — что виджет умеет предсказуемо открывать внешнюю ссылку. Для этого нам понадобится openExternal.
Чуть позже, когда вы начнёте менять код, нормальный дев‑цикл будет выглядеть так:
- Меняете JSX.
- Сохраняете.
- Либо обновляете вкладку ChatGPT, либо (иногда) достаточно просто «пошевелить» виджет — например, отправить новое сообщение или ещё раз запустить App (в зависимости от того, как настроен ваш шаблон и кэширование).
Если изменения не видны, первым делом подумайте о трёх подозреваемых: Dev‑сервер не запущен, туннель отвалился или ChatGPT подключён к старому URL. В разделе «Где искать ошибки, если что‑то пошло не так» мы разберём этот сценарий подробнее.
5. Почему нельзя просто поставить <a href> и забыть
Чтобы выполнить последний пункт нашего smoke‑теста — кнопку, которая открывает внешнюю страницу, — нам придётся разобраться с openExternal. Логичный вопрос: «А зачем вообще этот openExternal? Что мешает сделать обычную ссылку?»
Проблема в том, что ваш виджет живёт не «просто в браузере», а в iframe под управлением ChatGPT. Этот iframe работает в достаточно жёсткой песочнице: могут действовать ограничения Content Security Policy, атрибуты sandbox, странности с target="_blank" и блокировкой всплывающих окон. В результате поведение <ahref="…"> или window.open() внутри такого iframe может оказаться непредсказуемым: от полного игнора до всплывающих предупреждений, не контролируемых вашим кодом.
Кроме того, с точки зрения UX 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‑инструменты, не ходит на ваш backend и не тратит токены OpenAI. Это просто сигнал хост‑приложению «пожалуйста, открой вот этот URL».
Во‑вторых, такой способ совместим с песочницей. ChatGPT сам решает, как именно открывать ссылку, не давая вашему iframe переусердствовать с window.open().
7. Добавляем кнопку с openExternal в наш виджет
Теперь давайте научимся открывать внешнюю ссылку из нашего «Hello GiftGenius». Самый простой сценарий: кнопка «Открыть демо‑ссылку», которая ведёт, например, на документацию или лендинг вашего сервиса.
Для начала напишем маленький helper, чтобы 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. Чуть позже в курсе мы аккуратно опишем интерфейс этого объекта. Пока нам достаточно, чтобы код компилировался и работал.
Теперь подключим helper в наш виджет и добавим кнопку:
// 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 и убедитесь, что ваш App подключён к правильному URL (публичному, а не localhost).
- Откройте чат, выберите App (или попросите модель запустить его).
- Убедитесь, что во встроенном виджете видно «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 vs /), настройки basePath/assetPrefix (если вы уже их трогали) и адрес, прописанный в Dev Mode.
10. Немного о «приборке»: остановка процессов
Это мелочь, но на практике очень полезная. Новички часто забывают о том, что и dev‑сервер, и туннель — это отдельные процессы, которые продолжают жить в фоновом режиме.
Если у вас внезапно «порт 3000 уже занят», возможно, где‑то в глубине терминалов спрятан старый npm run dev. На Windows это иногда превращается в «танцы с бубном» вокруг диспетчера задач, на macOS и Linux спасает Ctrl + C в том терминале, где процесс запущен.
То же самое с туннелем: если вы поэкспериментировали с несколькими туннелями подряд или забыли закрыть старый, легко запутаться, к какому именно URL сейчас привязан ваш App в Dev Mode. Лучше выработать привычку: собрались завершать сессию — отключите туннель, остановите dev‑сервер и при следующем запуске начинайте с чистого листа.
11. Типичные ошибки при первом smoke‑тесте
Ошибка №1: использовать localhost вместо публичного HTTPS‑URL.
Частая история: в Dev Mode вы случайно указываете http://localhost:3000 или вообще забываете про туннель. На вашей машине всё работает, но ChatGPT, живущий в облаке, физически не может достучаться до localhost. Лекарство простое: проверяйте, что в настройках App указан именно публичный 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. Мы в этом модуле выбрали небольшой helper openExternalSafe с (window as any), а аккуратную типизацию добавим позже.
Ошибка №5: просмотр результата только в localhost, но не внутри ChatGPT.
Бывает соблазн ограничиться тем, что http://localhost:3000/widget открывается, и считать задачу решённой. Но смысл этого модуля как раз в том, чтобы увидеть App внутри ChatGPT. То, что в обычном браузере всё хорошо, ещё не гарантирует, что ChatGPT правильно создаст iframe, подхватит ресурсы через туннель и не упрётся в CORS/CSP. Полноценный smoke‑тест всегда включает шаг с реальным запуском App в интерфейсе ChatGPT.
Ошибка №6: забытый или отвалившийся туннель.
Вы обновили код, а в ChatGPT висит старая версия виджета или вообще ничего не грузится. Часто оказывается, что туннель закрылся по таймауту, но Developer Mode по‑прежнему смотрит на старый URL. Если при открытии туннельного URL в обычном браузере вы видите ошибку — сначала восстановите туннель, а уже потом грешите на Apps SDK.
Ошибка №7: игнорирование консоли в iframe.
Разработчики с опытом SPA привыкли смотреть console.log в DevTools своего приложения, но внутри ChatGPT это iframe, и нужно выбрать правильный фрейм в DevTools. Если смотреть только на верхний уровень, вы можете не увидеть ни одной ошибки, хотя внутри виджета всё давно красное. Привычка «открыть DevTools именно на iframe‑виджете» сильно экономит нервы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ