CognxSafeTrack commited on
Commit ·
b8d93e2
1
Parent(s): 87dcd87
docs: add post-mortems for Meta API deprecation, SSE 401 and file import bugs
Browse files- docs/post-mortems.md +191 -0
docs/post-mortems.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Post-Mortems — XAMLÉ Platform
|
| 2 |
+
|
| 3 |
+
Ce document recense les bugs significatifs rencontrés en production, leurs causes racines, et les règles à appliquer pour ne pas les reproduire. À lire avant tout changement touchant l'infrastructure, les APIs tierces ou l'authentification.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Architecture de référence
|
| 8 |
+
|
| 9 |
+
```
|
| 10 |
+
Netlify (Admin Frontend — React/Vite)
|
| 11 |
+
│
|
| 12 |
+
│ VITE_API_URL = https://safetrack-edtech.hf.space
|
| 13 |
+
▼
|
| 14 |
+
HuggingFace Space (safetrack/edtech)
|
| 15 |
+
└── API Fastify [port 7860] ← répond à toutes les requêtes REST du frontend
|
| 16 |
+
│
|
| 17 |
+
│ BullMQ (Redis partagé)
|
| 18 |
+
▼
|
| 19 |
+
Railway "whatsapp-worker"
|
| 20 |
+
├── API Fastify [port 7860] ← PM2, même image Docker
|
| 21 |
+
└── Worker Bridge [port 8082] ← consommateur BullMQ, envoie les messages WhatsApp
|
| 22 |
+
seul à pouvoir appeler graph.facebook.com
|
| 23 |
+
(HuggingFace est bloqué réseau vers Meta)
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
> **Règle fondamentale :** HuggingFace ne peut **pas** appeler `graph.facebook.com` directement. Tout appel Meta doit passer par le Worker Railway via un job BullMQ.
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## Post-Mortem #1 — Daily Limit affiche « — » (mai 2026)
|
| 31 |
+
|
| 32 |
+
### Symptôme
|
| 33 |
+
La carte "Daily Limit" (Limite Quotidienne WhatsApp) sur `/clients` affichait `—` au lieu du tier (`TIER_1K`, `TIER_10K`…).
|
| 34 |
+
|
| 35 |
+
### Cause racine
|
| 36 |
+
Le service `fetchMetaStatus` appelait l'API Meta avec la version **v19.0** :
|
| 37 |
+
```typescript
|
| 38 |
+
`https://graph.facebook.com/v19.0/${phoneNumberId}?fields=messaging_limit_tier,quality_rating`
|
| 39 |
+
```
|
| 40 |
+
`v19.0` a été **dépréciée en février 2026** (2 ans après sa sortie en février 2024). Meta renvoie une erreur de dépréciation, mais le `catch { /* non-fatal */ }` du code la masquait silencieusement. `messagingLimitTier` restait `undefined` → affichage `—`.
|
| 41 |
+
|
| 42 |
+
Le même problème touchait **7 fichiers** avec des versions hardcodées incohérentes (`v18.0` et `v19.0` mélangées).
|
| 43 |
+
|
| 44 |
+
### Correction appliquée
|
| 45 |
+
- Toutes les URLs Meta remplacées par `process.env.META_GRAPH_API_VERSION || 'v22.0'`
|
| 46 |
+
- Le `catch` silencieux remplacé par un `logger.error` visible
|
| 47 |
+
- Log de démarrage ajouté dans l'API et le Worker : `✅ [META] Graph API version: v22.0`
|
| 48 |
+
|
| 49 |
+
### Règles à retenir
|
| 50 |
+
|
| 51 |
+
> ⚠️ **Ne jamais hardcoder une version d'API Meta dans le code.**
|
| 52 |
+
> Toujours utiliser la variable d'environnement `META_GRAPH_API_VERSION`.
|
| 53 |
+
|
| 54 |
+
> ⚠️ **Meta déprécie ses versions tous les ~2 ans.**
|
| 55 |
+
> Vérifier la version active sur [developers.facebook.com/docs/graph-api/changelog](https://developers.facebook.com/docs/graph-api/changelog) lors de chaque mise à jour majeure du projet.
|
| 56 |
+
|
| 57 |
+
> ⚠️ **Un `catch { /* non-fatal */ }` masque les bugs en production.**
|
| 58 |
+
> Tout catch doit au minimum faire un `logger.error(err)`.
|
| 59 |
+
|
| 60 |
+
### Procédure de mise à jour future
|
| 61 |
+
1. Changer `META_GRAPH_API_VERSION=vXX.0` dans Railway (variables d'env)
|
| 62 |
+
2. Redémarrer le service → vérifier le log `✅ [META] Graph API version: vXX.0`
|
| 63 |
+
3. Vider le cache Redis : `redis-cli --scan --pattern "meta:status:*" | xargs redis-cli del`
|
| 64 |
+
4. **Aucun changement de code nécessaire**
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## Post-Mortem #2 — SSE Stream : 401 Unauthorized (mai 2026)
|
| 69 |
+
|
| 70 |
+
### Symptôme
|
| 71 |
+
```
|
| 72 |
+
safetrack-edtech.hf.space/v1/organizations/.../stream → 401 Unauthorized
|
| 73 |
+
```
|
| 74 |
+
La connexion SSE (Server-Sent Events) pour les notifications en temps réel échouait systématiquement. Le badge de messages non lus ne se mettait jamais à jour.
|
| 75 |
+
|
| 76 |
+
### Cause racine : deux problèmes distincts
|
| 77 |
+
|
| 78 |
+
**2A — `VITE_API_URL` pointait vers le mauvais service**
|
| 79 |
+
À un moment, la variable pointait vers `https://whatsapp-worker-production-0bc0.up.railway.app` (le Worker Railway) au lieu de `https://safetrack-edtech.hf.space` (l'API HuggingFace). Le Worker n'expose que 2 routes (`/v1/internal/whatsapp/inbound` et `/health`). Tous les autres appels retournaient 401/404.
|
| 80 |
+
|
| 81 |
+
**2B — L'API `EventSource` du navigateur ne peut pas envoyer de headers**
|
| 82 |
+
```typescript
|
| 83 |
+
// ❌ AVANT — le JWT n'arrive jamais au serveur
|
| 84 |
+
const es = new EventSource(`${apiBase}/v1/organizations/${orgId}/stream`);
|
| 85 |
+
|
| 86 |
+
// ✅ APRÈS — le JWT passe en query param
|
| 87 |
+
const es = new EventSource(`${apiBase}/v1/organizations/${orgId}/stream?token=${encodeURIComponent(token)}`);
|
| 88 |
+
```
|
| 89 |
+
L'API web `EventSource` est une limitation navigateur : elle ne supporte pas les headers personnalisés (`Authorization: Bearer ...`). Le serveur ne recevait donc jamais le JWT → 401.
|
| 90 |
+
|
| 91 |
+
### Correction appliquée
|
| 92 |
+
- **Frontend** (`MainLayout.tsx`, `CrmConversationalDashboard.tsx`) : ajout de `?token=` dans l'URL EventSource
|
| 93 |
+
- **Backend** (`verifyJwt.ts`) : injection du query param dans le header `Authorization` avant `jwtVerify()`
|
| 94 |
+
|
| 95 |
+
```typescript
|
| 96 |
+
// verifyJwt.ts — fallback query param pour SSE
|
| 97 |
+
const queryToken = (request.query as Record<string, string>)?.token;
|
| 98 |
+
if (queryToken && !request.headers.authorization) {
|
| 99 |
+
request.headers.authorization = `Bearer ${queryToken}`;
|
| 100 |
+
}
|
| 101 |
+
await request.jwtVerify();
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
### Règles à retenir
|
| 105 |
+
|
| 106 |
+
> ⚠️ **`EventSource` ne supporte pas les headers custom.**
|
| 107 |
+
> Toute connexion SSE nécessite le token en query param (`?token=...`). C'est le standard pour SSE — ne pas essayer de passer le JWT autrement.
|
| 108 |
+
|
| 109 |
+
> ⚠️ **`VITE_API_URL` doit toujours pointer vers l'API HuggingFace**, jamais vers le Worker Railway.
|
| 110 |
+
> Valeur correcte sur Netlify : `https://safetrack-edtech.hf.space`
|
| 111 |
+
|
| 112 |
+
> ℹ️ Le service Railway s'appelle "whatsapp-worker" mais fait tourner les deux processus (API + Worker) via PM2 sur des ports distincts (7860 et 8082). Le nom du service Railway est trompeur.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## Post-Mortem #3 — Import Excel : "File chooser dialog can only be shown with a user activation" (mai 2026)
|
| 117 |
+
|
| 118 |
+
### Symptôme
|
| 119 |
+
Cliquer sur "Parcourir mes fichiers" dans le CRM ouvrait un sélecteur de fichier, mais après sélection du fichier, une erreur console bloquait le traitement :
|
| 120 |
+
```
|
| 121 |
+
File chooser dialog can only be shown with a user activation.
|
| 122 |
+
onFileUpload @ index-BcnTfhzI.js:598
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### Cause racine
|
| 126 |
+
Deux `<input type="file">` en cascade — architecture incorrecte entre `CrmAIAssistant` et `FileImporter` :
|
| 127 |
+
|
| 128 |
+
```
|
| 129 |
+
Utilisateur clique "Parcourir mes fichiers"
|
| 130 |
+
└── CrmAIAssistant : <label> ouvre son propre <input type="file"> ← Input 1
|
| 131 |
+
│
|
| 132 |
+
└── onChange={() => onFileUpload()} ← déclenché après sélection
|
| 133 |
+
│
|
| 134 |
+
└── document.getElementById('crm-file-upload')?.click()
|
| 135 |
+
│ ← Input 2 (FileImporter)
|
| 136 |
+
└── 🚫 BLOQUÉ — .click() depuis un onChange()
|
| 137 |
+
n'est pas une "user activation" directe
|
| 138 |
+
(Chrome 126+, Safari 17+)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Le fichier sélectionné dans l'Input 1 était **ignoré**. L'Input 2 tentait de s'ouvrir depuis un `onChange` — ce que les navigateurs modernes refusent.
|
| 142 |
+
|
| 143 |
+
### Correction appliquée
|
| 144 |
+
Suppression de l'input embarqué dans `CrmAIAssistant`. Le `<label>` pointe maintenant directement sur l'input de `FileImporter` via `htmlFor` :
|
| 145 |
+
|
| 146 |
+
```tsx
|
| 147 |
+
// ❌ AVANT — double input en cascade
|
| 148 |
+
<label>
|
| 149 |
+
Parcourir mes fichiers
|
| 150 |
+
<input type="file" onChange={() => onFileUpload()} /> {/* déclenche un 2ème input */}
|
| 151 |
+
</label>
|
| 152 |
+
|
| 153 |
+
// ✅ APRÈS — un seul input, liaison directe
|
| 154 |
+
<label htmlFor="crm-file-upload">
|
| 155 |
+
Parcourir mes fichiers
|
| 156 |
+
</label>
|
| 157 |
+
{/* FileImporter rend : <input id="crm-file-upload" type="file" onChange={handleFile} /> */}
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
### Règles à retenir
|
| 161 |
+
|
| 162 |
+
> ⚠️ **Ne jamais appeler `.click()` sur un `<input type="file">` depuis un handler `onChange`, `setTimeout`, ou toute fonction async.**
|
| 163 |
+
> Les navigateurs modernes exigent une activation utilisateur *directe* (click, keydown) pour ouvrir un sélecteur de fichier.
|
| 164 |
+
|
| 165 |
+
> ✅ **Pattern correct :** utiliser `<label htmlFor="id-de-l-input">` pour déclencher un input caché. C'est natif, fiable dans tous les navigateurs, aucun JavaScript nécessaire.
|
| 166 |
+
|
| 167 |
+
> ⚠️ **Ne jamais avoir deux `<input type="file">` pour le même flux d'upload.**
|
| 168 |
+
> Un seul input, un seul handler. Si plusieurs composants doivent déclencher le même upload, ils partagent tous le même `id` via `htmlFor`.
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## Checklist avant tout déploiement
|
| 173 |
+
|
| 174 |
+
- [ ] `META_GRAPH_API_VERSION` est défini dans les variables d'env Railway ET est ≥ v21.0
|
| 175 |
+
- [ ] `VITE_API_URL` sur Netlify = `https://safetrack-edtech.hf.space` (HuggingFace, pas Railway)
|
| 176 |
+
- [ ] Toute connexion SSE passe le token en `?token=` dans l'URL
|
| 177 |
+
- [ ] Tout `catch` loggue au minimum l'erreur
|
| 178 |
+
- [ ] Aucun appel direct à `graph.facebook.com` depuis le code HuggingFace — passer par BullMQ
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## Variables d'environnement critiques
|
| 183 |
+
|
| 184 |
+
| Variable | Service | Valeur correcte | Risque si absent/faux |
|
| 185 |
+
|---|---|---|---|
|
| 186 |
+
| `META_GRAPH_API_VERSION` | Railway API + Worker | `v22.0` (ou version récente) | Daily Limit affiche `—`, erreurs Meta silencieuses |
|
| 187 |
+
| `VITE_API_URL` | Netlify | `https://safetrack-edtech.hf.space` | Tous les appels API retournent 401/404 |
|
| 188 |
+
| `JWT_SECRET` | Railway | chaîne aléatoire ≥ 64 chars | Auth compromise |
|
| 189 |
+
| `ADMIN_API_KEY` | Railway | chaîne aléatoire ≥ 32 chars | Bridge Worker non sécurisé |
|
| 190 |
+
| `REDIS_URL` | Railway + HF | URL Redis partagée | BullMQ ne fonctionne plus, pas de jobs |
|
| 191 |
+
| `DATABASE_URL` | Railway + HF | Neon PostgreSQL | Toute l'app en erreur |
|