JavaRush /Курсы /ChatGPT Apps /Первый smoke‑тест: «Hello widget» и openExternal

Первый smoke‑тест: «Hello widget» и openExternal

ChatGPT Apps
2 уровень , 4 лекция
Открыта

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

В обычном веб‑разработческом мире smoke‑тест — это минимальная проверка «система вообще жива?». Страница открывается, кнопки не падают, ничего критичного не горит.

В мире ChatGPT Apps 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 вызывает ваш App, MCP‑сервер возвращает ответ, включающий ссылку на UI (тот самый /widget), и в сообщении чата появляется ваш виджет.

Если всё хорошо, вы видите знакомый заголовок «Hello from GiftGenius» прямо внутри ChatGPT. На этом этапе smoke‑тест почти пройден: iframe рендерится, цепочка «Next.js → туннель → ChatGPT» жива. Осталось проверить последний пункт из нашей таблички — что виджет умеет предсказуемо открывать внешнюю ссылку. Для этого нам понадобится openExternal.

Чуть позже, когда вы начнёте менять код, нормальный дев‑цикл будет выглядеть так:

  1. Меняете JSX.
  2. Сохраняете.
  3. Либо обновляете вкладку 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:

  1. Проверяет, что URL разрешён политиками.
  2. Может показать пользователю предупреждение (например, что это внешний сайт).
  3. Открывает ссылку в новой вкладке/окне браузера пользователя.

Важно понять две вещи.

Во‑первых, это чисто клиентская операция. Она не вызывает 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

Теперь можно сделать полноценный «боевой» прогон. Попробуем пройти все шаги:

  1. Убедитесь, что dev‑сервер запущен (npm run dev) и вы видите Hello from GiftGenius на http://localhost:3000/widget.
  2. Убедитесь, что туннель к порту 3000 поднят, и публичный URL открывается из внешнего браузера.
  3. Откройте ChatGPT, включите Dev Mode и убедитесь, что ваш App подключён к правильному URL (публичному, а не localhost).
  4. Откройте чат, выберите App (или попросите модель запустить его).
  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 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‑виджете» сильно экономит нервы.

1
Задача
ChatGPT Apps, 2 уровень, 4 лекция
Недоступна
Hello GiftGenius — минимальный “Hello widget” для smoke‑теста
Hello GiftGenius — минимальный “Hello widget” для smoke‑теста
1
Задача
ChatGPT Apps, 2 уровень, 4 лекция
Недоступна
Кнопка “Открыть ссылку” через openExternalSafe (с безопасным fallback)
Кнопка “Открыть ссылку” через openExternalSafe (с безопасным fallback)
1
Опрос
Первое ChatGPT-приложение, 2 уровень, 4 лекция
Недоступен
Первое ChatGPT-приложение
Первое ChatGPT-приложение: шаблон, Dev Mode, туннель
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ