1. ChatGPT App におけるスモークテストとは
一般的な Web 開発の世界でスモークテストとは、「システムがそもそも生きているか?」を確認する最小限のチェックです。ページが開く、ボタンでエラーが出ない、致命的な問題が起きていない。
ChatGPT Apps の世界ではスモークテストは少しだけ複雑です。というのも、チェーンに複数の要素が同時に関与するからです。
- あなたのウィジェットのコード(React/Next.js)。
- Next.js の Dev サーバー。
- トンネル(ngrok/Cloudflare)。
- ChatGPT(iframe を作成し、その中にあなたのウィジェットを読み込む)。
良いスモークテストとは、次の状況を指します。
- ウィジェットがエラーなく ChatGPT 内にレンダリングされる;
- 基本的なインタラクションが機能する(例: ボタンを押したら外部リンクが開く);
- ブラウザーのコンソールや Dev サーバーのログに、真っ赤なエラーの雪崩がない。
重要: この段階ではまだ MCP ツールの検証や負荷テスト、トークンのコスト計算は行いません。今の私たちの控えめで実用的な目標は、「コード → Next.js → トンネル → ChatGPT → ユーザー」というチェーンがきちんと閉じていることを証明することです。
頭の中で次のような表として捉えると便利です。
| 検証項目 | 良好と判断する基準 |
|---|---|
| ウィジェットのレンダリング | ChatGPT 上で UI が見える(「壊れた iframe」ではない) |
| ChatGPT ↔ 自サーバーの接続 | 「アプリを読み込めません」系のエラーが出ない |
| サンドボックス内の JS の動作 | ハンドラー onClick が実際に実行される |
| 外部リンクを開けること | ボタンが指定した URL を新しいタブ/ウィンドウで開く |
2. 学習用アプリ: シンプルな「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>
);
}
重要なポイントが2つあります。
まず、ファイルの先頭にあるディレクティブ 'use client'; は、このコンポーネントをクライアントコンポーネントにします。これがないと Next.js はファイルをサーバーコンポーネントとして扱うため、window や onClick、その他のブラウザー API を使えません。
次に、これは普通の React コンポーネントです。「Apps SDK の魔法」のようなものは見えません——それで正しいのです。ChatGPT 内に現れる魔法の部分は、MCP サーバーの設定と、ウィジェットの URL を返すツール側に隠れています。そこは後で扱います。今は UI のみを対象とします。
3. ウィジェットをテンプレートに組み込み、起動する
Apps SDK 用の公式 Next.js テンプレートでは、ウィジェット用のページが既に存在するのが普通です。あなたはそれを編集するか、必要なルート用に自分のページ(例: /widget)を作成します。
ここでは app/widget/page.tsx がある前提で、その中身を上のコードに差し替えるとします。この後の流れは次の通りです。
- ファイルを保存する。
- Next.js の Dev サーバー(npm run dev で既に起動中)が該当モジュールを再読み込みし、HMR がページを更新する。
- トンネル経由で、同じパス /widget の公開 HTTPS URL が更新後の UI を返すようになる。
これを確認する方法は2つあります。
まずは昔ながらにローカルブラウザーで開きます。開く URL は次の通りです。
http://localhost:3000/widget
そして同じ Hello from GiftGenius が見えるはずです。そう、これはまだ ChatGPT ではありません。Next.js アプリの UI が生きていることを確認しているだけです。
次に、トンネル経由で確認します。払い出された URL(https://witty-cat.ngrok-free.app のようなもの)に /widget を付けて、通常のブラウザーで開きます。
https://witty-cat.ngrok-free.app/widget
うまくいけば、ページの見た目は同じです。つまり「Next.js → トンネル → あなたのブラウザー」というチェーンは動作しているので、あとはこの間に ChatGPT を挟むだけです。
4. ChatGPT 内でウィジェットを確認する
Dev Mode の ChatGPT は本質的に3つのステップを踏みます。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)を含む応答を返し、チャットメッセージ内にウィジェットが表示されます。
うまくいけば、ChatGPT の中に「Hello from GiftGenius」という見慣れた見出しが見えるはずです。この時点でスモークテストはほぼ合格です。iframe がレンダリングされ、「Next.js → トンネル → ChatGPT」のチェーンが生きているからです。残すは表の最後の項目——ウィジェットが予測可能に外部リンクを開けることの確認です。そのために openExternal が必要になります。
この先コードを変更し始めると、通常の開発サイクルは次のようになります。
- JSX を変更する。
- 保存する。
- ChatGPT のタブを更新するか、(場合によっては)ウィジェットを軽く「動かす」——新しいメッセージを送る、もう一度 App を起動する、など(テンプレートやキャッシュの設定に依存)。
変更が反映されない場合、まず3つの容疑者を考えましょう。Dev サーバーが動いていない、トンネルが切れている、または ChatGPT が古い URL に接続している。詳細は「うまくいかないときの調べ方」のセクションで解説します。
5. なぜ単に <a href> を置いて終わり、ではいけないのか
スモークテストの最後の項目——外部ページを開くボタン——を実現するには、openExternal を理解する必要があります。もっともな疑問として「そもそもなぜ openExternal が要るの?普通のリンクじゃダメなの?」があります。
問題は、あなたのウィジェットが「単なるブラウザー」ではなく、ChatGPT 管理下の iframe 内で動作していることです。この iframe はかなり厳しいサンドボックスで動きます。Content Security Policy の制約、sandbox 属性、target="_blank" の挙動やポップアップブロックの癖などが影響し得ます。その結果、<ahref="…"> や window.open() の挙動は予測不能になり、完全に無視されたり、あなたのコードでは制御できない警告が出ることもあります。
さらに UX の観点から、OpenAI はあなたが外部ページをいつどのように開くかをコントロールしたいと考えています。そこで Apps SDK は統一的なブリッジ window.openai を提供します。あなたのコードは親ウィンドウに直接手を出さず、定義済みの API に従ってホストアプリにアクションを委譲します。
6. API window.openai.openExternal: 何か、どう動くか
ウィジェットのサンドボックスでは、グローバルオブジェクト window.openai にアクセスできます。これはあなたの UI と ChatGPT をつなぐ主要な「ブリッジ」です。これを通じてツール呼び出し、フォローアップメッセージ送信、表示モードの変更、ウィジェット状態の管理、そしてもちろん外部リンクのオープンが可能です。
この講義で扱うのは、ひとつのメソッドです。
window.openai.openExternal({ href: string }): void;
window.openai.openExternal({ href: 'https://example.com' }) を呼び出すと、ChatGPT は次を行います。
- URL がポリシーで許可されているかを確認する。
- ユーザーに警告を表示することがある(例: 外部サイトである旨)。
- ユーザーのブラウザーで、新しいタブ/ウィンドウとしてリンクを開く。
重要なポイントが2つあります。
第一に、これは純粋にクライアント側の操作です。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 が存在しないため、フォールバックが動作します。つまりブラウザーの通常機能で新しいタブが開きます。ここでの window.open は ChatGPT のサンドボックス外(すなわち通常のブラウザー)でのみ使われるため、一般的な挙動になり問題を引き起こしません。
openExternal についてはモジュール 3(ウィジェットとサンドボックスの講義)で詳しく扱います。今はアプリの実行に進んで構いません。
8. ミニ E2E スモークテスト
では「本番さながら」の一連の流れを通してみましょう。次の手順を実行します。
- Dev サーバーが起動済み(npm run dev)で、http://localhost:3000/widget に Hello from GiftGenius が表示されることを確認します。
- 3000 番ポートへのトンネルが立ち上がっており、公開 URL が外部ブラウザーから開けることを確認します。
- ChatGPT を開いて Dev Mode を有効化し、App が正しい URL(localhost ではなく公開 URL)に接続されていることを確認します。
- チャットを開き、App を選択(またはモデルに起動させるよう依頼)します。
- 組み込みウィジェット内に「Hello from GiftGenius」が見えることを確認します。
- 「デモリンクを開く」をクリックし、https://example.com(またはあなたの URL)がブラウザーで開くことを確認します。
すべて問題なければ、次が確認できました。
- ウィジェットの HTML/JS が Next サーバーで正しくビルド・配信されている。
- HTTPS トンネルが正しくプロキシしている。
- ChatGPT があなたの URL を信頼し、ウィジェットを読み込めている。
- window.openai が動作し、外部リンクを開くコマンドが渡っている。
これが、最初のスモークテストで目指していたことそのものです。
9. うまくいかないときの調べ方
「普通の」フロントエンドと違い、ここでの主な診断ポイントは3つだけです。どこで壊れているのかを素早く切り分けることが重要です。
- まず ChatGPT の UI を確認します。ウィジェットの代わりに「Error loading app」や「We had trouble talking to your app」といったエラーメッセージが出る場合、問題はトンネルか Dev サーバーの到達性にある可能性が高いです。公開 URL をブラウザーで直接開いてください。開けない、あるいは Next.js のエラーが出るなら、まずそこを直しましょう。
- 次に、ChatGPT が動いているタブのブラウザー DevTools を開きます。あなたのウィジェット用に専用の iframe があり、その中にいつもの Console タブがあります。openExternal のボタンをクリックしても何も起きないなら、「window.openai is undefined」のようなエラーが出ていないか確認してください。このエラーがある場合、ChatGPT 内ではなく(トンネルの URL を直接)開いて試しているか、'use client'; を忘れている可能性が高いです。
- 並行して npm run dev を実行しているターミナルを見ます。ビルドエラー(TypeScript、ESLint、コンパイル)が出ていると、ChatGPT はよくて古いコード、悪ければ何も見えません。エラーがないのに更新が見えない場合は、トンネルがまだアクティブか確認してください。多くのトンネルサービスはアイドルタイムアウトでセッションを閉じます。
もう1つ典型的なケースがあります。localhost では動くのに、トンネル経由では 404 やおかしなページが返ってくる場合です。そのときはベースパス(/widget と / の違い)、basePath/assetPrefix の設定(すでに変更している場合)、そして Dev Mode に設定したアドレスを注意深く確認してください。
10. 少し「片付け」の話: プロセスの停止
細かいことですが、実務ではとても役に立ちます。初心者は Dev サーバーとトンネルが独立したプロセスであり、バックグラウンドで動き続けることを忘れがちです。
突然「ポート 3000 はすでに使用中」となったら、どこかのターミナルに古い npm run dev が潜んでいるのかもしれません。Windows ではタスク マネージャーで右往左往する事態になりがちですが、macOS と Linux ならそのプロセスを起動したターミナルで Ctrl + C が助けになります。
トンネルも同様です。いくつもトンネルを試したり、古いものを閉じ忘れたりすると、Dev Mode の App がどの URL にぶら下がっているのか混乱しがちです。習慣化しましょう。セッションを終えるときは、トンネルを切断し、Dev サーバーを停止し、次回はクリーンな状態から始めるのがベストです。
11. 初回スモークテストでありがちなミス
ミス 1: localhost を公開 HTTPS URL の代わりに使ってしまう。
ありがちなのは、Dev Mode に誤って http://localhost:3000 を指定する、あるいはトンネルの存在自体を忘れるパターンです。あなたのマシンでは動いても、クラウド上の ChatGPT からは localhost に届きません。解決策はシンプル。App の設定に、正しいパス(テンプレートによって /mcp かルートかが異なる)を含む公開 HTTPS トンネルのアドレスが設定されているか確認しましょう。
ミス 2: ウィジェットのファイルで 'use client'; を書き忘れる。
きれいな React コードを書き、onClick を追加し、window.openai にアクセスしているのに、Next.js が黙ってページをサーバーコンポーネントにします。よくて「window is not defined」、悪ければコンポーネントがビルドされません。ブラウザー API にアクセスするには、ウィジェットはクライアントコンポーネントである必要があり、その合図が先頭行の 'use client'; です。
ミス 3: openExternal の代わりに window.open() を直接呼ぶ。
window.open('https://example.com') の方が簡単に見えることがあります。通常のブラウザーでは動くこともありますが、ChatGPT のサンドボックス内では挙動が予測不能(無視される、ブロックされる等)です。ChatGPT Apps での正しい方法は、window.openai.openExternal({ href }) を使ってホストにリンクオープンを委譲し、セキュリティポリシーを順守することです。
ミス 4: TypeScript が window.openai に文句を言い、型を無効化してしまう。
やけになってファイルの先頭に // @ts-nocheck と書く人がいます。これはコンパイルエラーを消しますが、そのファイルの TypeScript を丸ごと無効にしてしまいます。はるかに安全なのは、window の周りでピンポイントに as any を使うか、別ファイルで window.openai の最小インターフェースを定義することです。このモジュールでは小さなヘルパー openExternalSafe と (window as any) を採用し、厳密な型付けは後で追加します。
ミス 5: localhost での表示だけで満足し、ChatGPT 内で確認しない。
http://localhost:3000/widget が開けたからといって、そこで終わりにしたくなることがあります。しかしこのモジュールの目的は、ChatGPT の中で App を見ることです。通常のブラウザーで問題なくても、ChatGPT が iframe を正しく作成し、トンネル経由でリソースを取得し、CORS/CSP に引っかからない保証にはなりません。本物のスモークテストは、必ず ChatGPT の UI で App を実際に起動するステップを含みます。
ミス 6: トンネルの閉じ忘れ/切断に気づかない。
コードは更新したのに、ChatGPT 側では古いウィジェットが表示されたまま、あるいは何も読み込まれない。よくある原因は、トンネルがタイムアウトで閉じたのに Developer Mode が古い URL を見続けていることです。公開 URL を通常のブラウザーで開いてエラーが出るなら、まずトンネルを復旧してから Apps SDK を疑いましょう。
ミス 7: iframe 内のコンソールを見落とす。
SPA に慣れた開発者は、自分のアプリの DevTools で console.log を確認しますが、ChatGPT 内では iframe になっているため、DevTools で正しいフレームを選択する必要があります。最上位だけ見ていると、ウィジェット内が真っ赤でもエラーが何ひとつ見えないことがあります。「ウィジェットの iframe に対して DevTools を開く」習慣は大いに助けになります。
GO TO FULL VERSION