Backend - la chaîne SKAN
Réception des postbacks Apple, signature, restitution Reported vs Observed
En une phrase : Apple envoie une copie développeur des postbacks SKAdNetwork/AdAttributionKit gagnants sur notre endpoint ; on vérifie la signature, on résout l'org, on stocke, et le dashboard affiche « SKAN observé (appwin) » face au « Reported » des régies.
Décision d'origine : ADR-0038. Code : apps/api/src/modules/attribution/skan/.
L'entrée : des routes hors du monde authentifié
SkanPostbackController écoute sur @Controller('.well-known') :
POST .well-known/skadnetwork/report- SKAN classique, parsing strict ;POST .well-known/{skadnetwork,appattribution}/report-attribution- AAK, parsing tolérant (payload loggé brut, mapping à construire sur du trafic réel), dédup par hash du body.
Ces chemins sont exclus du préfixe API ET du middleware tenant (même traitement
que /health) : Apple ne porte aucun contexte. Piège vécu : oublier l'exclusion
CLS donne un « No CLS context available » silencieux.
eTLD+1, la découverte de terrain : l'endpoint déclaré dans l'app
(NSAdvertisingAttributionReportEndpoint) est normalisé par iOS à l'apex.
skan.appwin.io déclaré = postbacks reçus sur appwin.io. L'ingress route donc
les chemins .well-known des hosts apex vers l'api d'ingestion ; les hosts
skan.* ne servent qu'aux tests d'URL manuelle. Conséquence assumée : pas de
séparation staging/prod possible sur ce flux.
Le traitement
SkanIngestionService, derrière une queue BullMQ (ingestion-skan, pattern
porte 1) :
- Signature : vérification ECDSA P-256 avec la clé publique Apple
(constante dans
skan-signature.ts), chaîne canonique v3/v4, séparateur. Une signature invalide n'est PAS jetée : la ligne est stockée avecsignature_valid=false(à revalider au premier vrai postback). - Résolution d'org : l'
app idADAM du postback est cherché dansintegrations.appstore.appId. Orphelin = stocké avecorg_id NULL, invisible des tenants, rattachable plus tard. - Stockage :
attribution.skan_postbacks(migration 0068), payloads non parsés loggés bruts.
La restitution
GET /v1/attribution/skan agrège par jour d'ARRIVÉE x canal. Le mapping
ad-network-id → canal vit dans skan-networks.ts (ids TikTok/Meta publics, à
confirmer au premier postback réel). Le dashboard affiche la tuile « SKAN
observé (appwin) » et un compteur par onglet réseau sur la page Campagnes, avec
le hint d'honnêteté sur les délais Apple (24-144 h).
Règle produit transverse : « Reported by network » (revendiqué par la régie via leurs APIs) et « Observed by appwin » (vu par notre plomberie) ne se mélangent JAMAIS silencieusement - deux colonnes, deux vérités, le MMM au-dessus.
Les conversion values, l'autre moitié de la boucle
Le serveur définit le schéma (par app, attribution.skan_cv_schemas, défaut
start_trial=32/medium + purchase=63/high dans skan-cv.service.ts), le SDK
le télécharge et pilote la valeur (cf.
la couche attribution du SDK). La valeur revient... dans
les postbacks ci-dessus, décodée avec ce même schéma. Rappel : chaque régie
décode SES postbacks avec le mapping déclaré chez elle - le studio doit
répliquer notre schéma dans chaque Events Manager.
Reste ouvert
- Test device réel inachevé (les postbacks de dev iOS exigent le câble USB pour
idevicesyslog). - Mapping AAK à construire au premier payload réel.
- Mapping source-identifier → campagne quand il y aura du volume.