appwinwiki
Attribution (serveur)

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) :

  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 avec signature_valid=false (à revalider au premier vrai postback).
  2. Résolution d'org : l'app id ADAM du postback est cherché dans integrations.appstore.appId. Orphelin = stocké avec org_id NULL, invisible des tenants, rattachable plus tard.
  3. 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.

On this page