1. Qu’est‑ce qu’un smoke‑test pour une ChatGPT App
Dans le monde habituel du développement web, un smoke‑test est la vérification minimale « le système est‑il en vie ? ». La page s’ouvre, les boutons ne plantent pas, rien de critique ne brûle.
Dans le monde des ChatGPT Apps, le smoke‑test est un peu plus intéressant, car la chaîne implique plusieurs maillons à la fois :
- Votre code du widget (React/Next.js).
- Le serveur de développement Next.js.
- Le tunnel (ngrok/Cloudflare).
- ChatGPT, qui crée un iframe et charge votre widget dans la discussion.
Pour nous, un bon smoke‑test, c’est lorsque :
- le widget est rendu à l’intérieur de ChatGPT sans erreurs ;
- l’interactivité de base fonctionne (par exemple, on clique sur un bouton — un lien externe s’ouvre) ;
- ni dans la console du navigateur, ni dans les logs du serveur de développement, il n’y a une avalanche rouge d’erreurs.
Important : à ce stade, nous ne testons pas encore les outils MCP, nous ne faisons pas de test de charge et nous ne comptons pas l’argent des tokens. Notre objectif est modeste et très pratique : prouver que la chaîne « code → Next.js → tunnel → ChatGPT → utilisateur » se ferme bien.
Il est pratique de s’imaginer cela sous la forme d’un petit tableau :
| Ce que l’on vérifie | Comment savoir que tout va bien |
|---|---|
| Rendu du widget | Dans ChatGPT, on voit notre UI, et non « iframe cassé » |
| Connexion ChatGPT ↔ notre serveur | Aucune erreur du type « impossible de charger l’application » |
| Exécution du JS dans la sandbox | Les gestionnaires onClick s’exécutent réellement |
| Possibilité d’ouvrir un lien externe | Le bouton ouvre un nouvel onglet/fenêtre avec l’URL indiquée |
2. Notre App pédagogique : un simple « Hello GiftGenius »
Dans ce cours, nous construisons progressivement l’application GiftGenius — un assistant pour trouver des idées de cadeaux. À cette étape, elle ne choisit encore rien, mais peut au moins saluer poliment et afficher un lien « en savoir plus ».
Nous avons besoin d’un widget minimal mais « honnête » : sans logique complexe, mais avec du code React vivant.
La variante la plus simple du composant du widget peut ressembler à ceci (vous pouvez adapter le nom et les styles, mais reprenons la base du plan du cours) :
// 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 }}>
C’est votre première ChatGPT App. Ensuite, nous lui apprendrons à choisir des cadeaux.
</p>
</main>
);
}
Quelques points importants.
Premièrement, la directive 'use client'; au début du fichier rend le composant « client ». Sans elle, Next.js traite le fichier comme un composant serveur, et vous ne pourrez pas utiliser window, les gestionnaires onClick et, de manière générale, aucune API du navigateur.
Deuxièmement, c’est un composant React ordinaire. Aucune « magie de l’Apps SDK » n’y est visible — et c’est honnête. Toute la magie qui fait qu’il se retrouve dans ChatGPT est cachée dans la configuration du serveur MCP et de l’outil qui renvoie le lien vers l’URL du widget. Nous nous en occuperons plus tard ; pour l’instant, seul l’UI nous intéresse.
3. Intégrer le widget dans le template et démarrer
Dans le template officiel Next.js pour l’Apps SDK, la page du widget existe généralement déjà ; vous la modifiez ou vous en créez une sous la route voulue (par exemple, /widget).
Supposons que vous ayez justement app/widget/page.tsx et que vous remplaciez son contenu par le code ci‑dessus. La chaîne ressemble alors à ceci :
- Vous enregistrez le fichier.
- Le serveur de développement Next.js (déjà lancé via npm run dev) redémarre les modules nécessaires, HMR met la page à jour.
- Via le tunnel, votre URL publique HTTPS au même chemin /widget commence à servir l’UI mis à jour.
Vous pouvez le vérifier de deux façons.
D’abord à l’ancienne — dans le navigateur local. Ouvrez :
http://localhost:3000/widget
et vous voyez le même Hello from GiftGenius. Oui, ce n’est pas encore ChatGPT, vous vous assurez simplement que l’UI de votre application Next.js est vivante.
Ensuite — via le tunnel. Prenez l’URL fournie (quelque chose comme https://witty-cat.ngrok-free.app), ajoutez /widget et ouvrez‑la dans un navigateur classique :
https://witty-cat.ngrok-free.app/widget
Si tout va bien, la page devrait être identique. Cela signifie que la chaîne « Next.js → tunnel → votre navigateur » fonctionne, il ne reste plus qu’à insérer ChatGPT entre les deux.
4. Vérifier le widget dans ChatGPT
En Dev Mode, ChatGPT fait essentiellement trois étapes : crée un iframe, met dans son src votre URL publique, et laisse cet iframe vivre à l’intérieur du message de chat.
De façon simplifiée, l’événement ressemble à ceci :
sequenceDiagram
participant Dev as Vous (Dev)
participant Next as Serveur de développement Next.js
participant Tun as Tunnel (HTTPS)
participant GPT as ChatGPT
participant User as Utilisateur
Dev->>Next: npm run dev (http://localhost:3000)
Dev->>Tun: Lancement du tunnel vers le port 3000
GPT->>Tun: GET https://.../widget
Tun->>Next: Proxy vers http://localhost:3000/widget
Next-->>Tun: HTML + JS du widget
Tun-->>GPT: Réponse avec HTML/JS
GPT->>User: Rendu de l’iframe avec le widget
Pour voir le résultat, vous :
- Ouvrez ChatGPT dans le navigateur, choisissez le modèle voulu (en général GPT‑5.1 ou celui défini par défaut pour le Dev Mode).
- Sélectionnez explicitement votre application (via le menu Apps/Developer) ou « invoquez‑la » par une phrase du genre : « Lance l’application GiftGenius ».
- ChatGPT appelle votre App, le serveur MCP renvoie une réponse incluant le lien vers l’UI (le fameux /widget), et votre widget apparaît dans le message du chat.
Si tout va bien, vous voyez l’en‑tête familier « Hello from GiftGenius » directement dans ChatGPT. À ce stade, le smoke‑test est presque réussi : l’iframe est rendue, la chaîne « Next.js → tunnel → ChatGPT » est vivante. Il reste à vérifier le dernier point de notre tableau — que le widget sait ouvrir un lien externe de façon prévisible. Pour cela, nous allons utiliser openExternal.
Un peu plus tard, quand vous commencerez à modifier le code, le cycle de dev normal ressemblera à ceci :
- Vous modifiez le JSX.
- Vous enregistrez.
- Soit vous actualisez l’onglet ChatGPT, soit (parfois) il suffit de « remuer » le widget — par exemple, envoyer un nouveau message ou relancer l’App (selon la configuration de votre template et le cache).
Si les changements ne sont pas visibles, pensez d’abord à trois suspects : le serveur de développement n’est pas lancé, le tunnel a chuté ou ChatGPT pointe vers une ancienne URL. Dans la section « Où chercher les erreurs si quelque chose ne va pas », nous détaillerons ce scénario.
5. Pourquoi ne pas simplement mettre <a href> et passer à autre chose
Pour réaliser le dernier point de notre smoke‑test — un bouton qui ouvre une page externe — nous devons comprendre openExternal. Question logique : « À quoi sert donc ce openExternal ? Qu’est‑ce qui empêche d’utiliser un lien classique ? »
Le problème, c’est que votre widget ne « vit » pas juste dans un navigateur, mais dans un iframe géré par ChatGPT. Cet iframe fonctionne dans une sandbox assez stricte : des contraintes de Content Security Policy peuvent s’appliquer, des attributs sandbox, des bizarreries avec target="_blank" et le blocage des pop‑ups. En conséquence, le comportement de <ahref="…"> ou de window.open() dans un tel iframe peut s’avérer imprévisible : de l’ignorance totale à des avertissements surgissants hors de votre contrôle.
De plus, du point de vue UX, OpenAI souhaite contrôler quand et comment vous ouvrez des pages externes. C’est pourquoi l’Apps SDK fournit un pont unifié window.openai : votre code n’accède pas directement à la fenêtre parente, il délègue l’action à l’application hôte via une API clairement définie.
6. API window.openai.openExternal : qu’est‑ce que c’est et comment ça fonctionne
Dans la sandbox du widget, l’objet global window.openai est disponible. C’est le « pont » principal entre votre UI et ChatGPT : via lui, vous pouvez appeler des outils, envoyer des follow‑ups, changer le mode d’affichage, gérer l’état du widget et, bien sûr, ouvrir des liens externes.
Dans cette leçon, un seul méthode nous intéresse :
window.openai.openExternal({ href: string }): void;
Quand vous appelez window.openai.openExternal({ href: 'https://example.com' }), ChatGPT :
- Vérifie que l’URL est autorisée par les politiques.
- Peut afficher un avertissement à l’utilisateur (par exemple, que c’est un site externe).
- Ouvre le lien dans un nouvel onglet/fenêtre du navigateur de l’utilisateur.
Deux points importants.
Premièrement, c’est une opération purement côté client. Elle n’appelle pas d’outils MCP, n’accède pas à votre backend et ne consomme pas de tokens OpenAI. C’est simplement un signal à l’application hôte « s’il te plaît, ouvre cette URL ».
Deuxièmement, cette méthode est compatible avec la sandbox. ChatGPT décide lui‑même comment ouvrir le lien, sans permettre à votre iframe d’abuser de window.open().
7. Ajouter un bouton avec openExternal dans notre widget
Apprenons maintenant à ouvrir un lien externe depuis notre « Hello GiftGenius ». Scénario le plus simple : un bouton « Ouvrir le lien de démo » qui mène, par exemple, vers la documentation ou la landing page de votre service.
Pour commencer, écrivons un petit helper pour éviter les plaintes de TypeScript et pour que le widget ne plante pas si vous ouvrez /widget directement dans le navigateur (où window.openai n’existe pas encore) :
// 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 {
// Fallback pour un affichage local sans ChatGPT
window.open(href, '_blank', 'noopener,noreferrer');
}
}
Ici, j’utilise délibérément (window as any), pour ne pas vous encombrer avec la typage de window.openai. Un peu plus loin dans le cours, nous décrirons proprement l’interface de cet objet. Pour l’instant, il suffit que le code compile et fonctionne.
Connectons maintenant le helper dans notre widget et ajoutons un bouton :
// 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 }}>
C’est votre première ChatGPT App. Ensuite, nous lui apprendrons à choisir des cadeaux.
</p>
<button
type="button"
onClick={() => openExternalSafe('https://example.com')}
style={{
padding: '8px 16px',
borderRadius: 8,
border: '1px solid #ccc',
cursor: 'pointer',
}}
>
Ouvrir le lien de démo
</button>
</main>
);
}
Ce qui se passe au clic.
Si le widget est lancé dans ChatGPT, window.openai.openExternal existe, et ChatGPT ouvrira https://example.com conformément à ses règles.
Si vous avez ouvert http://localhost:3000/widget dans un navigateur ordinaire, window.openai n’existe pas, et le fallback s’exécute : un nouvel onglet s’ouvre avec les moyens habituels du navigateur. Ici, window.open n’est utilisé que lors de l’ouverture directe de /widget dans un navigateur classique, c’est‑à‑dire en dehors de la sandbox de ChatGPT. Dans ce contexte, il fonctionne comme d’habitude et ne pose aucun problème.
Nous détaillerons openExternal plus en profondeur dans le module 3 (leçon dédiée au widget et à la sandbox), vous pouvez donc passer sereinement au lancement de l’application.
8. Mini smoke‑test end‑to‑end
Vous pouvez maintenant faire un véritable run « en conditions réelles ». Essayons de parcourir toutes les étapes :
- Assurez‑vous que le serveur de développement est lancé (npm run dev) et que vous voyez Hello from GiftGenius sur http://localhost:3000/widget.
- Assurez‑vous que le tunnel vers le port 3000 est actif et que l’URL publique s’ouvre depuis un navigateur externe.
- Ouvrez ChatGPT, activez le Dev Mode et vérifiez que votre App pointe vers la bonne URL (publique, et non localhost).
- Ouvrez une discussion, sélectionnez l’App (ou demandez au modèle de la lancer).
- Vérifiez que, dans le widget intégré, on voit « Hello from GiftGenius ».
- Cliquez sur le bouton « Ouvrir le lien de démo » et vérifiez que https://example.com (ou votre adresse) s’ouvre dans le navigateur.
Si tout cela fonctionne, alors :
- L’HTML/JS du widget est correctement assemblé et servi par le serveur Next.
- Le tunnel HTTPS proxy correctement les requêtes.
- ChatGPT fait confiance à votre URL et peut charger le widget.
- window.openai fonctionne et transmet la commande d’ouverture du lien externe.
C’est exactement ce que nous attendions de ce premier smoke‑test.
9. Où chercher les erreurs si quelque chose ne va pas
Contrairement au « frontend » classique, vous avez ici trois endroits principaux pour diagnostiquer. Il est important d’identifier rapidement lequel est en panne :
- Regardez d’abord l’UI dans ChatGPT. Si, au lieu du widget, vous voyez un message d’erreur comme « Error loading app » ou « We had trouble talking to your app », le problème vient probablement du tunnel ou de la disponibilité de votre serveur de dev. Essayez d’ouvrir l’URL publique directement dans le navigateur : si elle ne s’ouvre pas ou s’ouvre avec une erreur Next.js, corrigez cela en priorité.
- Ensuite, ouvrez les DevTools du navigateur sur l’onglet où ChatGPT fonctionne. Il y a un iframe dédié à votre widget, et à l’intérieur de celui‑ci — l’onglet Console que vous connaissez. Si, lors d’un clic sur le bouton avec openExternal, il ne se passe rien, regardez s’il n’y a pas des erreurs du type « window.openai is undefined » ou d’autres erreurs JS. Si une telle erreur apparaît, vous essayez sans doute le widget en dehors de ChatGPT (directement à l’URL du tunnel) ou vous avez oublié la directive 'use client';.
- En parallèle, regardez le terminal avec npm run dev. Si des erreurs de build s’y accumulent (TypeScript, ESLint, compilation), ChatGPT verra au mieux une ancienne version du code, au pire — rien du tout. S’il n’y a pas d’erreurs mais que vous ne voyez pas les mises à jour, vérifiez que le tunnel est toujours actif : de nombreux services de tunnel ferment les sessions pour cause d’inactivité.
Il existe un autre cas typique : tout fonctionne en localhost, mais via le tunnel vous obtenez un 404 ou une page étrange. Vérifiez alors soigneusement le chemin de base (/widget vs /), les réglages basePath/assetPrefix (si vous y avez déjà touché) et l’adresse configurée dans le Dev Mode.
10. Un mot sur le « ménage » : arrêter les processus
C’est un détail, mais très utile en pratique. Les débutants oublient souvent que le serveur de dev et le tunnel sont des processus séparés qui continuent de vivre en arrière‑plan.
Si soudainement « le port 3000 est déjà occupé », il se peut qu’un ancien npm run dev soit resté caché au fond d’un terminal. Sous Windows, cela se transforme parfois en « danse autour du gestionnaire des tâches », et sous macOS et Linux, Ctrl + C dans le terminal où le processus tourne suffit.
Même chose pour le tunnel : si vous avez expérimenté avec plusieurs tunnels d’affilée ou avez oublié de fermer l’ancien, on se perd vite sur l’URL à laquelle votre App est actuellement reliée dans le Dev Mode. Mieux vaut prendre l’habitude suivante : lorsque vous terminez une session, coupez le tunnel, arrêtez le serveur de dev, et au prochain lancement repartez d’une base propre.
11. Erreurs typiques lors du premier smoke‑test
Erreur n°1 : utiliser localhost au lieu d’une URL publique HTTPS.
Histoire fréquente : dans le Dev Mode, vous indiquez par inadvertance http://localhost:3000 ou vous oubliez carrément le tunnel. Sur votre machine, tout fonctionne, mais ChatGPT, qui vit dans le cloud, ne peut pas physiquement atteindre localhost. Le remède est simple : vérifiez que, dans les paramètres de l’App, c’est bien l’adresse publique HTTPS du tunnel qui est indiquée, avec le bon chemin (/mcp ou la racine — selon le template).
Erreur n°2 : oublier la directive 'use client'; dans le fichier du widget.
Vous écrivez un beau code React, ajoutez onClick, accédez à window.openai, mais Next.js, silencieusement, fait de la page un composant serveur. Au mieux, vous obtiendrez « window is not defined », au pire — le composant ne se compilera pas. Pour accéder aux API du navigateur, le widget doit être un composant client, ce que signale la première ligne 'use client';.
Erreur n°3 : appel direct à window.open() au lieu de openExternal.
Il peut sembler plus simple de faire window.open('https://example.com'). Dans un navigateur classique, cela peut encore fonctionner, mais dans la sandbox de ChatGPT, le comportement sera imprévisible : de l’ignorance totale au blocage. La voie correcte pour les ChatGPT Apps est window.openai.openExternal({ href }), qui délègue l’ouverture du lien à l’hôte et respecte toutes les politiques de sécurité.
Erreur n°4 : TypeScript se plaint de window.openai, et le développeur « soigne » cela en désactivant les types.
Dans le désespoir, certains écrivent // @ts-nocheck en tête de fichier. Cela élimine les erreurs de compilation, mais coupe en même temps tout TypeScript dans ce fichier. Il est bien plus sûr d’utiliser un as any ciblé autour de window, ou de décrire dans un fichier séparé l’interface minimale de window.openai. Dans ce module, nous avons choisi un petit helper openExternalSafe avec (window as any), et nous ajouterons une typisation propre plus tard.
Erreur n°5 : ne voir le résultat qu’en localhost, mais pas dans ChatGPT.
La tentation est grande de se contenter du fait que http://localhost:3000/widget s’ouvre et de considérer la tâche comme résolue. Mais le but de ce module est justement de voir l’App dans ChatGPT. Le fait que tout aille bien dans un navigateur classique ne garantit pas que ChatGPT créera correctement l’iframe, récupérera les ressources via le tunnel et ne butera pas sur CORS/CSP. Un smoke‑test complet inclut toujours une étape avec un lancement réel de l’App dans l’interface de ChatGPT.
Erreur n°6 : tunnel oublié ou tombé.
Vous avez mis à jour le code, mais ChatGPT affiche une ancienne version du widget ou ne charge rien du tout. Il s’avère souvent que le tunnel s’est fermé pour cause d’inactivité, tandis que le Developer Mode pointe toujours vers l’ancienne URL. Si, en ouvrant l’URL du tunnel dans un navigateur, vous voyez une erreur — restaurez d’abord le tunnel, puis suspectez l’Apps SDK.
Erreur n°7 : ignorer la console dans l’iframe.
Les développeurs habitués aux SPA ont le réflexe de regarder le console.log dans les DevTools de leur application, mais dans ChatGPT, il s’agit d’un iframe et il faut sélectionner le bon frame dans les DevTools. Si vous ne regardez que le niveau supérieur, vous risquez de ne voir aucune erreur alors que, à l’intérieur du widget, tout est déjà rouge. L’habitude « ouvrir les DevTools précisément sur l’iframe du widget » économise beaucoup de nerfs.
GO TO FULL VERSION