appwinwiki
Attribution (serveur)

Backend - connecteurs régies

Reporting TikTok/Meta (spend, campagnes, vignettes) et activation Meta CAPI

En une phrase : deux familles de connecteurs dans le module attribution - le reporting (lecture seule : spend, campagnes, ad groups, ads depuis les APIs annonceur TikTok/Meta) et l'activation (écriture : nos events consentis poussés vers Meta CAPI, et le câblage de l'adapter TikTok embarqué).

Code : apps/api/src/modules/attribution/connectors/ (reporting) et activation/ (activation). Décision : ADR-0038.

Reporting : « reads are the whole story »

Promesse produit affichée : on ne fait que LIRE les comptes pub, jamais écrire.

Connexion des comptes

OAuth TikTok (Login for Business, config_id) et Meta (Login for Business, redirect AVEC slash final - TikTok SANS), token collé en fallback. Tokens chiffrés AES-256-GCM (common/crypto.ts, format enc:<iv>:<tag>:<data>). Un compte se rattache à une app APRÈS connexion (l'OAuth arrive avant qu'on sache pour quelle app) : ad_accounts.project_id nullable, select par ligne dans la page Intégrations. Un compte non rattaché reste dormant.

La hiérarchie à trois niveaux

campaigns + campaign_daily (0065), puis ad_entities + ad_entity_daily (0070) - une paire GÉNÉRIQUE pour ad groups ET ads, discriminée par level (vocabulaire neutre : un adset Meta est un ad_group), avec parent_id.

Le sync (ad-accounts.service.ts) est tolérant par étage : le rapport de métriques d'abord (ses noms servent de registre de secours si le scope de lecture manque), le registre ensuite, chaque échec est un warn jamais un throw. Côté TikTok, les listes de métriques dégradent progressivement (app_install/skan_conversion/conversion sont optionnelles selon les comptes) au lieu de faire échouer le sync. Cron nocturne attribution.spend-sync à 8h45 UTC + bouton par compte.

Les vignettes de créas (2026-09-08)

ad_entities.thumbnail_url (0071), rafraîchies à CHAQUE sync car les URLs CDN sont signées et expirent ; COALESCE à l'upsert pour garder la dernière cover connue sur un raté ponctuel.

  • Meta : champ creative{thumbnail_url} sur le listing /ads (même scope ads_read, rien de plus).
  • TikTok : détour obligatoire - les Spark Ads exposent tiktok_item_id sur /ad/get/, mais /identity/video/info/ exige un scope Identity qu'on n'a pas. Solution : l'oEmbed public (https://www.tiktok.com/oembed?url=.../@_/video/<item_id>, le handle placeholder marche, TikTok redirige) donne thumbnail_url sans auth. Résolution concurrente (8 workers, timeout 5 s), un échec = cover absente, jamais un sync raté.

Statuts

active/paused/deleted/unknown. TikTok : operation_status est l'interrupteur, secondary_status porte la suppression. Piège vécu : un token copié AVANT l'approbation d'un scope liste les entités mais sans statut (unknown partout) - re-auth après approbation obligatoire.

Activation Meta CAPI (2026-09-08)

activation/ : la table activation_destinations (0074, une par app x réseau : dataset, token chiffré, curseur, santé), le client meta-capi.client.ts et le cron attribution.capi-forward toutes les 5 min.

Le tick, par destination active :

  1. lit les devices consentis (device_ad_ids, cf. la couche attribution du SDK pour les trois états) - aucun ? rien ne part et le curseur ne bouge pas ;
  2. lit les events ClickHouse depuis le curseur (app_install, start_trial, purchase, subscribe ; backfill initial 72 h ; lots de 500, 20 max par tick) ;
  3. filtre aux devices consentis (un event scanné sans consentement est passé pour toujours : un consentement tardif ne ré-expédie pas le passé) ;
  4. pousse sur graph.facebook.com/v23.0/{dataset}/events : event_id = notre UUID (dédup Meta), madid si ad_id présent, advertiser_tracking_enabled = verdict ATT/LAT, action_source: 'app' ;
  5. avance le curseur, met à jour last_delivery_at/forwarded_count.

Santé : token refusé (code Graph 190) → status='error' visible dans l'UI (« reconnecte avec un token frais ») ; autre échec → last_error, retry au tick suivant. La connexion depuis le dashboard valide le credential immédiatement par un envoi vide.

Pourquoi pas l'OAuth existant ? Trois raisons : scope d'écriture ≠ scope de lecture (consentement minimal par usage), le token OAuth expire (~60 j) là où le token système ne meurt jamais (un pipeline S2S sans surveillance ne doit pas s'éteindre en silence), et lister/écrire les datasets d'un tiers exige l'Advanced Access Meta (review). Les MMP établis demandent la même manip sur ce rail. One-click possible après la review Meta.

Activation TikTok

Pas d'envoi serveur (leur Events API App Install n'est pas GA) : la destination TikTok stocke le TikTok App ID + le token d'app (chiffré), et GET /sdk/v1/attribution/activation-config les sert à l'adapter embarqué (appwin-tiktok-events). Le jour de la GA, l'envoi bascule ici, à côté de Meta.

On this page