Intelligence / Spy : runtime checklist
Ce document répond à trois symptômes opérateur récurrents : La DB ne s'alimente jamais vraiment. ; Les données d'une boutique de référence ne remontent pas comme dans l'outil de référence. ; Une boutique supprimée du…
Ce document répond à trois symptômes opérateur récurrents :
- La DB ne s'alimente jamais vraiment.
- Les données d'une boutique de référence ne remontent pas comme dans l'outil de référence.
- Une boutique supprimée du Spy revient toujours.
Les causes code sont corrigées (voir PR de l'audit Spy). Ce qui reste ci-dessous est runtime / config prod : impossible à exécuter depuis l'éditeur, à faire dans le dashboard Vercel + sur le prod déployé.
1. La DB ne s'alimente pas → QSTASH_TOKEN manquant
Cause racine (code-prouvée). Un deep-scan est un heavy job : sur Vercel
il DOIT passer par QStash (le fallback inline est refusé, un scan de 60-300 s
ferait exploser le budget de durée de la route appelante). Donc si
QSTASH_TOKEN n'est pas configuré, chaque enqueue("intelligence-scan-store")
lève une exception : le firehose de découverte moissonne des milliers de
domaines et n'en indexe zéro. C'est silencieux aujourd'hui (un log
enqueue_failed par domaine).
Note : l'intégration Vercel « Upstash » injecte
KV_REST_API_*(Redis). QStash est un produit Upstash séparé, ses clés ne sont PAS injectées automatiquement. C'est le piège le plus probable.
À faire
-
Dans Vercel → Project → Settings → Environment Variables, ajouter :
QSTASH_TOKENQSTASH_CURRENT_SIGNING_KEYQSTASH_NEXT_SIGNING_KEY
(depuis le dashboard Upstash → QStash.)
-
Redéployer.
-
Vérifier : ouvrir
/admin/platform/cronsou la santé pipeline (getPipelineHealth→ sondeprobeQStash). Elle doit passer deQSTASH_TOKEN missingàconfigured: true. -
Amorcer sans attendre le cron : déclencher
discovery-harvest-tick(toutes les 4 h en auto), il moissonne crt.sh + Common Crawl (gratuits, sans clé), vérifie chaque domaine, et enqueue les storefronts vivants.
Depuis cette PR — la DB se remplit MÊME sans QStash, en dégradé. Si la queue n'est pas prête,
discovery-harvest-tickne renvoie plus unenqueued: 0muet : il scanne inline un petit lot borné (INLINE_FALLBACK_MAX= 3 stores/tick) et renvoie{ queueUnready: true, inlineScanned, reason }
- un log d'erreur explicite. Donc la DB trickle ~3 stores toutes les 4 h même sans config. QStash reste indispensable pour le vrai débit (des milliers/jour au lieu de 3/tick).
Volume (optionnel)
Pour remplir vite au-delà du filet crt.sh, configurer la source Apify :
APIFY_TOKEN + APIFY_SHOPIFY_DATASET_ID. Sans elles,
discovery-apify-sync no-op proprement (apify-sync.ts retourne early).
Comment lire son rapport. Le cron parcourt le dataset par fenêtres,
en reprenant à un curseur persisté dans KV
(discovery:apify:cursor:<datasetId>), et rebouclant à 0 en fin de dataset.
Il ne promet donc plus un débit : il le rapporte.
| Champ | Ce qu'il dit |
|---|---|
startOffset / nextOffset | où la marche a repris, où elle reprendra |
enqueued / failed | publications QStash réussies / refusées |
deferred + outOfTime | la fenêtre n'a pas tenu dans le budget ; le curseur n'a pas bougé, la prochaine exécution la rejoue et le filtre de fraîcheur saute ce qui a déjà atterri |
aborted | plus rien ne passait (quota QStash épuisé en cours de route) : arrêt après 25 échecs consécutifs |
cursorPersisted: false | la fenêtre a été traitée mais KV n'a pas pu enregistrer la position ; la même fenêtre sera rejouée |
harvestFailed | le pull Apify a échoué (timeout, 5xx). Ce n'est PAS une fin de dataset : le curseur reste exactement où il était |
cursorReset: "past-end" | le curseur avait survécu à un dataset qui a rétréci ; il est remis à 0 |
reachedEnd + cycles | la marche a fait un tour complet du catalogue |
elapsedMs | c'est ce chiffre, sur une vraie exécution, qui autorise à monter MAX_ENQUEUE_PER_TICK — pas un calcul |
Avant ce correctif (intelligence/0239) aucun de ces champs n'existait, et
pour cause : la fonction publiait jusqu'à 20 000 messages QStash en série
dans une route déclarée maxDuration = 60, donc Vercel la tuait en pleine
boucle chaque dimanche, sans log de fin. Elle ne persistait par ailleurs
aucun offset, donc elle relisait les mêmes premiers items d'un dataset de
~500 000 à chaque fois. L'en-tête du cron promettait « 80k/mois ».
Deux fixes persist adjacents (même symptôme « ne s'alimente pas »)
- Store connecté rétrogradé. Un re-scan anonyme (recherche hub, crons
refresh WARM/COLD : tous
storeId:null) d'un domaine connecté en OAuth mettait sonstoreIdànull, l'évinçant du tier HOT → ses données gelaient. L'upsertn'écrase plus lestoreIdsur update ; il ne l'écrit que si le scan en porte un. - Échec d'écriture masqué. Le
catchde persist du scan avalait toute erreur → un vrai échec DB (dérive de schéma) ressortait enpersisted:false→ lu comme « DB vide ». La dérive est désormais healed + retry ×1 (miroir du reactive-heal de/api/me).
2. valisedemo ≠ l'outil de référence → 2ᵉ scan (le trafic, lui, ne coûte rien)
Recalibré le 2026-08-21. Cette section disait « Trafic :
SIMILARWEB_API_KEY(sinon pas demonthly_visits) ». Le code dit le contraire, et c'est une dérive qui coûte de l'argent : elle envoie souscrire un contrat pour un champ déjà obtenu gratuitement.traffic-provider.tsscrape la page overview publique de SimilarWeb et en tire{monthly_visits, top_country, top_source, paid_share}: son en-tête l'énonce mot pour mot, « without paying for a SimilarWeb / SEMrush contract ». Un scan live de valisedemo a remonté 724 visites, FR 100 % sans aucune clé. Un provider sous licence reste une montée en gamme (série longue, quotas), pas un prérequis.Ce qui bloquait réellement le trafic était un mur anti-bot renvoyant une page « Human Verification » que le détecteur ne reconnaissait pas, donc lu comme « ce store n'est pas dans l'index ». Corrigé (détection élargie
- routage résidentiel
proxy: "auto"à la demande).
valisedemo n'expose pas son inventory_quantity et n'a pas de pub Meta
active, les deux signaux de demande directs sont donc absents. Le pipeline
utilise alors :
- Trafic : scrape public, sans clé.
SIMILARWEB_API_KEY/DATAFORSEO_API_KEYn'ajoutent que l'historique multi-mois (la courbe 14 mois de l'outil de référence) : aucune page publique ne le donne. - Best-seller velocity : dérivée du churn du classement best-selling. Elle a besoin de deux snapshots ≥ 6 h d'écart : un premier scan pose la baseline (aucune vélocité), le second calcule le mouvement réel. C'est volontaire : on n'invente pas de ventes sur une seule observation.
- Availability velocity (nouveau) :
variant_inventory_sniffer(tierfull) lit désormais aussi le booléenavailablede/products/{handle}.js, exposé même quand le thème masqueinventory_quantity. Un fliptrue→false(rupture) compte comme ~1 vente, via le même pipelineinventory_velocity. Donc unmode:"full"×2 à ≥6 h capte les ventes d'un store à quantité masquée même si son classement best-seller ne bouge pas. Confiance moindre (rupture = 1 à N unités).
Levier config le plus sûr pour la parité de l'outil de référence sur valisedemo : un proxy résidentiel (
BRIGHT_DATA_*, chaîne de fallbackprobeFetch). Une IP résidentielle obtient souvent leinventory_quantityprécis qu'un fetch datacenter reçoit masqué, c'est exactement ce que l'outil de référence exploite. Sans lui, on reste sur les signauxavailable+ best-seller.
À faire
- Scanner valisedemo une première fois (recherche hub) → pose la baseline best-seller, et peuple le trafic (sans clé).
- Attendre ≥ 6 h, re-scanner → la ligne de revenu apparaît (best-seller velocity × AOV).
SIMILARWEB_API_KEY/DATAFORSEO_API_KEYseulement si la courbe de trafic multi-mois est voulue. Ne pas la souscrire pour obtenirmonthly_visits: il arrive déjà.
Ce que la parité de l'outil de référence a demandé côté code (août 2026)
Le symptôme « tout est vide chez nous, tout est plein chez eux » n'était pas un manque de source. Quatre fois de suite, la donnée était en main et le code refusait de la transmettre :
| Symptôme opérateur | Cause réelle |
|---|---|
| 1 pixel listé, eux 5 | webPixelsConfigList (déclaration Shopify) parsé puis jeté ; seuls les hits regex étaient émis |
| « Aucun compte social détecté » | handles trouvés mais carte liée aux seuls compteurs ; et un seul endpoint interrogé par réseau, celui que chacun mure le plus |
| Avis à 2/11 | le chiffre était dans le DOM rendu (data-number-of-reviews), et l'API vendeur était interrogée avec le domaine personnalisé au lieu du myshopify |
| « 724 » sans direction | visits_change_pct émis dans le record, absent de la projection et de l'écran |
Corollaire opérateur : avant de conclure « il manque une clé », vérifier
data du probe dans le rapport de scan. Un champ présent dans le payload
mais absent de l'interface est un bug de transmission, pas un budget.
Si un scan revient vide alors que le store est en ligne, c'est un blocage anti-bot au scrape (IP datacenter). L'outil de référence utilise des proxies résidentiels ; côté BoostEcom c'est le proxy de scrape qui doit être configuré. valisedemo étant un petit store peu protégé, un scan direct devrait passer.
3. Gymshark revient → corrigé (persist-gate tombstone au coordinateur)
Corrigé en code. Le tombstone de suppression (IntelligenceSuppression)
était consulté par seulement 2 des nombreux points d'entrée de scan. Tout
appel direct à runShopifyDeepScan le contournait et re-persistait la
ligne :
- les outils agent
getCompetitorCatalog/getCompetitorAds(intelligence-tools.ts→scanOnDemand) — le vecteur le plus probable : Gymshark est un exemple de concurrent standard, donc un audit agent le ramenait ; - le cron
discovery-bootstrap-ticket le cold-start (instrumentation-node.ts), dont le skip ne testait que la liste env, jamais le tombstone.
Désormais le blocage vit au coordinateur (shopify-deep-scan.ts), le seul
point par lequel tout scan passe : un domaine tombstoné scanne encore
(le caller voit des données live in-session) mais ne persiste plus → la
ligne supprimée ne peut plus revenir, quelle que soit la voie. Le propriétaire
scannant son propre store connecté (storeId dans l'actor) est exempté. Un
restore explicite (recherche hub, admin persist-scan) efface le tombstone
avant le scan (unsuppressDomain), donc le restore reste possible à la
demande.
À faire (vérification)
- Supprimer Gymshark depuis l'admin Spy (écrit le tombstone).
- Déclencher le vrai vecteur de résurrection : un audit concurrent via
l'agent (« audite gymshark.com ») ou un
discovery-bootstrap-tick/ bulk-index. → la ligne ne revient plus dans/admin/.../registry. - Le faire revenir volontairement : le chercher dans le hub (ou
POST /api/admin/intelligence/scanavecpersist:true), le restore explicite efface le tombstone et ré-indexe (restoredFromTombstone:truedans la réponse admin).
Vérification express des 3 objectifs (~15 min, sur le prod déployé)
Tout passe par le diagnostic admin intégré
POST /api/admin/intelligence/scan (admin-gated), qui renvoie en un coup :
persisted, restoredFromTombstone, diagnosis (verdict par catégorie + la
var d'env exacte à poser), config (présence des clés provider), et
highlights (les chiffres comparables à l'outil de référence). Lire diagnosis d'abord.
A. « La DB s'alimente ? » — persist réel
POST /api/admin/intelligence/scan { "domain": "valisedemo.com", "mode": "full", "persist": true }
→ attendu : persisted: true. Si false, lire diagnosis : il nomme la
cause (QStash absent, clé manquante, ou, désormais, une dérive de schéma qui
s'auto-répare et re-persiste au retry). config liste les clés présentes sur
CE runtime.
B. « valisedemo ≡ l'outil de référence ? » — parité des chiffres
- Poser
SIMILARWEB_API_KEY(+APIFY_*pour le volume), redéployer. - Scan #1 (commande A) → pose la baseline best-seller + trafic.
- ≥ 6 h plus tard, scan #2 → comparer
highlights(monthly_visits, monthly_revenue, best_seller_velocity…) à l'outil de référence. La ligne de revenu n'apparaît qu'au scan #2 (la best-seller velocity a besoin de 2 snapshots).
C. « Gymshark reste supprimé ? » — non-résurrection
- Supprimer gymshark.com depuis l'admin Spy.
- Déclencher le vrai vecteur : demander à l'agent d'auditer gymshark.com
(
getCompetitorCatalog/getCompetitorAds), sans passer par le persist-scan admin (qui, lui, restaure volontairement). → vérifier que la ligne n'est pas re-créée dans le registry. - NE PAS utiliser la commande A pour ce test :
persist:truesur l'endpoint admin dé-supprime exprès (restoredFromTombstone:true).
D. « Store connecté préservé ? » — après un re-scan anonyme (hub) d'un
domaine que tu as connecté en OAuth, vérifier dans le registry que son
storeId est toujours renseigné (tier HOT), pas repassé à « externe ».
Récapitulatif des variables d'environnement
| Variable | Débloque | Sans elle |
|---|---|---|
QSTASH_TOKEN (+ signing keys) | Toute la découverte / le remplissage DB | Aucun scan enqueué → DB vide |
SIMILARWEB_API_KEY | Trafic (monthly_visits) | Pas de trafic |
APIFY_TOKEN + APIFY_SHOPIFY_DATASET_ID | Découverte haut volume | Filet crt.sh uniquement |
La source de vérité des variables reste src/env/server.ts.