Un jeu mobile vendu sur les stores parle à au moins deux services extérieurs. Chez moi, ce sont RevenueCat, qui gère l'achat unique, et Sentry, qui remonte les plantages. Les deux ont besoin d'une clé. Et les deux posent la même question : que doit faire l'application quand elle tourne sur ma machine, dans un test, ou entre les mains d'un testeur, plutôt que chez un joueur qui l'a téléchargée ?
J'ai donné trois réponses différentes à cette question, une par jeu. En relisant les dépôts d'AbcDonjon, de Cendrelune et de Murudenu Tavern dans l'ordre, on voit la réponse se préciser — et on voit aussi une erreur que j'ai commise deux fois.
L'outil commun : --dart-define
Flutter permet de passer des valeurs au moment de la compilation : flutter run --dart-define=CLE=valeur, ou --dart-define-from-file=fichier.json pour en passer plusieurs d'un coup. Dans le code, on les lit avec String.fromEnvironment('CLE'), qui renvoie une chaîne vide quand rien n'a été passé. Ce sont des constantes : elles sont figées dans le binaire au moment du build, pas lues au lancement.
Les trois jeux s'en servent. Ce qui change, c'est ce qu'ils y mettent, et ce qu'ils font quand la valeur est absente.
AbcDonjon : les clés dans le code
Dans AbcDonjon, les deux clés RevenueCat — une pour l'App Store, une pour Google Play — sont écrites en clair dans le fichier qui gère l'achat. Le commentaire au-dessus le justifie : ce sont des clés publiques, faites pour être embarquées dans l'application. C'est exact, RevenueCat les conçoit ainsi.
Le DSN Sentry suit la même logique, avec une nuance. Il est lui aussi écrit dans le code, mais comme valeur par défaut d'un String.fromEnvironment('SENTRY_DSN') : un build peut le remplacer sans toucher au source. Le commentaire rappelle qu'un DSN ne sert qu'à écrire des événements, et qu'il finit de toute façon dans le binaire.
Le garde-fou est ailleurs : Sentry n'est branché que si le build n'est pas en mode débogage. Un clone frais du dépôt et le développement quotidien ne touchent jamais le réseau ; seuls les builds livrés remontent leurs erreurs. Et quand la boutique n'est pas disponible — sur ordinateur, en intégration continue, dans les tests — le gestionnaire d'achat se rabat sur l'état enregistré localement au lieu de planter.
Cendrelune : les clés hors du dépôt
Dans Cendrelune, plus aucune clé n'est écrite dans le code. Les clés RevenueCat et le DSN Sentry arrivent tous par --dart-define-from-file=.dart_defines.json, et ce fichier est exclu du dépôt par le .gitignore. Sans lui, l'achat intégré est simplement désactivé et le jeu tourne en démo.
La condition d'activation de Sentry, elle, a pris un deuxième étage. Il faut un DSN et un build de release. Le mode profile, que Flutter utilise pour mesurer les performances, est exclu lui aussi : c'est encore la machine de développement. Un dernier interrupteur, SENTRY_IN_DEBUG, permet de forcer l'envoi depuis un build de débogage, uniquement pour vérifier le câblage.
Ce deuxième étage a une histoire, et le code la raconte. Les assertions internes de Flutter ne s'exécutent qu'en débogage : elles n'atteindront jamais un joueur, mais Sentry les étiquette comme des plantages fatals. Une session de travail remontait donc comme une série d'incidents. Le commentaire donne le chiffre : quatre cents événements de rechargement à chaud, qui noyaient le seul défaut touchant vraiment des joueurs.
« Un tableau de bord d'erreurs rempli par sa propre machine ne dit plus rien sur les joueurs. »
Autre réglage visible dans ce fichier : l'échantillonnage des performances est à zéro. Cendrelune ne remonte que les erreurs et les plantages, pas de traces de performance. Sur ce jeu, Sentry sert à voir les erreurs, pas à mesurer des temps de réponse — et même là, il a ses propres réflexes : son nettoyage par défaut a déjà effacé de lui-même la seule information que je cherchais à observer.
Murudenu Tavern : trois environnements au lieu de deux
Murudenu reprend le principe de Cendrelune — les clés dans un fichier cles.json ignoré par git, passé au build — et y ajoute un troisième état entre « chez moi » et « chez un joueur » : le playtest.
Le code le dit en une ligne. Chaque événement Sentry porte un environnement parmi trois : developpement, playtest ou production. Ils ne se lisent pas ensemble, et le commentaire explique pourquoi : un build de playtest a le paywall court-circuité et des soirées ouvertes d'avance, et un plantage de la machine de développement n'est pas un incident.
La boutique simulée
Le playtest repose sur un interrupteur : --dart-define=ACHATS=simules. Avec lui, le jeu remplace la vraie boutique RevenueCat par une boutique de mise au point, sans réseau, où l'achat aboutit toujours. Le commentaire lui donne trois usages : traverser le paywall pendant les playtests, faire les captures du store, et, avant cela, distribuer une version TestFlight à une époque où le produit n'existait pas encore dans App Store Connect — sans dépendre d'un compte de bac à sable.
Deux précautions l'entourent. La première : la boutique simulée n'est retenue que si la valeur vaut exactement simules, et le build de release ne passe pas ce drapeau. Comme c'est une constante de compilation, le chemin qu'elle garde n'est pas activable dans le binaire livré. La seconde : un build qui porte ce drapeau le dit à l'écran. Un bandeau apparaît sur le paywall pour prévenir que l'achat est simulé et que rien n'est débité. Le commentaire du code donne les deux raisons : un testeur croirait avoir payé, et une capture de cet écran serait indiscernable de la version vendue.
Sans clé, ne rien appeler
Murudenu ajoute aussi une règle que je n'avais pas eu besoin d'écrire avant. Le SDK RevenueCat, appelé avant d'avoir été configuré, ne lève pas une exception qu'on pourrait rattraper : il arrête le processus natif. Sans clé — en développement, dans les tests — chaque méthode de la boutique doit donc court-circuiter avant de toucher au SDK. Le fichier le note en gras, en tête de la variable qui garde cet état.
Rien de personnel
Côté Sentry, le jeu passe explicitement sendDefaultPii à false. Il n'y aurait de toute façon rien à expurger : pas de compte, pas de profil, pas d'adresse. Mais le commentaire tient à l'écrire noir sur blanc, parce qu'une valeur par défaut peut changer à la prochaine version majeure d'un paquet. Dans la même veine, la configuration de l'outil qui envoie les symboles de débogage à Sentry précise upload_sources: false : les piles d'appels sont lisibles, le code source ne quitte pas ma machine.
L'erreur que j'ai faite deux fois
C'est la partie la moins glorieuse, et la plus utile. Sur Murudenu, le garde-fou « pas en débogage » avait sauté. La raison, consignée dans le code : à l'époque, le simulateur iOS ne se construisait qu'en débogage, et le chemin Sentry devenait impossible à essayer. L'argument a cessé de tenir — flutter build ios --simulator produit un build de release, et c'est ainsi que je vérifie la salle au quotidien.
Le provisoire a donné exactement le même résultat que sur Cendrelune. Le commentaire le résume sèchement : quatre issues triées, quatre venues de ma machine, zéro d'un joueur. Le second interrupteur est revenu. Aujourd'hui, une exception de débogage ne quitte plus l'appareil ; le chemin Sentry s'essaie en release sur simulateur, où il tombe dans l'environnement playtest, et production reste réservé aux vraies livraisons.
Je retiens de cet épisode que la leçon de Cendrelune ne s'était pas transmise d'elle-même. Elle était dans un commentaire, dans un autre dépôt. C'est d'ailleurs une bonne raison de laisser ce genre d'explication dans le code plutôt que dans sa tête : le jour où on défait le garde-fou, le commentaire est sous les yeux.
Ce qui a convergé
Mis côte à côte, les trois dépôts dessinent une progression assez nette :
- Un build sans configuration doit démarrer. C'est vrai dans les trois jeux : pas de clé, pas de plantage, simplement moins de fonctionnalités. C'est ce qui permet à
flutter testet à l'intégration continue de tourner sans secret. - Le débogage ne parle pas à la production. Présent dès AbcDonjon, renforcé dans Cendrelune, défait puis rétabli dans Murudenu.
- Les clés quittent le dépôt. Écrites dans le code pour AbcDonjon, dans un fichier ignoré pour les deux suivants. Les clés concernées sont publiques ; le commentaire de Murudenu donne une autre raison de les sortir quand même : elles diffèrent par plateforme et par environnement.
- Le test a sa propre case. Murudenu est le seul à distinguer le playtest de la production, et le seul à pouvoir faire traverser son paywall à un testeur sans compte de bac à sable.
Rien de tout ça n'est spectaculaire. Mais pour un développeur seul, ces interrupteurs font la différence entre un tableau de bord d'erreurs qu'on lit et un tableau de bord qu'on finit par ignorer — et entre un testeur qui sait ce qu'il teste et un testeur qui croit avoir payé. Si vous préparez votre première soumission sur les stores, c'est un bon moment pour décider ce que votre build de développement a le droit de faire.