appwinwiki
SDK

Analytics

Le pipeline d'événements : de track() à ClickHouse, en survivant au offline et aux kills

Le produit Analytics capture les événements de l'app (track, screen, sessions) et les achemine vers nos serveurs. Sa promesse technique : aucun événement perdu, aucun blocage de l'app, même hors ligne, même si le process est tué, même si l'utilisateur refuse le consentement plus tard.

Où lire le code : sdk/appwin-ios/Sources/AppwinCore/Analytics/ et sdk/appwin-android/.../io/appwin/core/analytics/ (miroir strict entre les deux), façade publique dans le module AppwinAnalytics.

Le voyage d'un événement

track(#quot;purchase#quot;, props) Validation + sanitisationnom, 20 props max, scalaires EventPipelineun seul acteur, zéro lock File sur disquecurrent.jsonl → ready-*.jsonl Flush par lots20 events / 30 s / background / retour réseau POST /api/sdk/v1/events ClickHouseanalytics.events

Trois choix de conception expliquent presque tout le reste :

  1. La file est sur disque, au format d'envoi. La ligne JSON écrite à l'enqueue est exactement celle que le serveur recevra : ce qui est stocké EST ce qui est envoyé, aucune retransformation au flush, donc aucun risque de divergence entre les deux.
  2. Un seul consommateur. Tout passe par un acteur (Swift) / une coroutine unique (Kotlin) : enqueue, flush, consentement, sessions. Aucun verrou ailleurs, aucune course possible.
  3. L'identité vient du serveur. Le payload ne porte ni org, ni app, ni device : tout est déduit de la session bearer côté serveur. Un client ne peut pas se faire passer pour un autre.

Les événements réservés

Le SDK émet lui-même session_start, session_end, screen_view, app_install, app_update et install_referrer - un événement custom ne peut pas prendre ces noms (liste RESERVED_EVENT_NAMES dans @app-win/contracts, vérifiée aussi côté client).

Les sessions : id UUIDv7, coupure après 30 min d'inactivité ou 24 h d'âge. L'état est persisté pour fermer une session rétroactivement après un kill : au lancement suivant, la session périmée se termine à sa dernière activité réelle, pas à l'instant du relancement.

Le consentement analytics (opt-out)

Modèle mesure d'audience (comme PostHog) : trois états, granted par défaut.

défaut setConsent(denied) setConsent(granted) app à écran CMP l'utilisateur accepte l'utilisateur refuse granted denied unknown
  • granted : capture + envoi.
  • unknown : capture et persiste, n'envoie rien - le mode queue-before-consent des apps à CMP. Poser unknown avant configure fonctionne (l'appel est mis en attente) : les tout premiers événements ne partent pas sous le défaut.
  • denied : purge la file, coupe la capture, oublie la session.

À ne pas confondre avec le consentement advertising (opt-in), qui gate l'envoi vers les régies publicitaires - cf. Attribution.

La politique d'envoi

Constantes partagées iOS/Android : flush à 20 événements ou 30 s, file plafonnée à 10 000, backoff exponentiel 2 s → 300 s avec jitter.

Réponse du serveurRéaction du SDK
2xxlot supprimé (dédup serveur par eventId si double envoi)
2xx + quotaExceededlot supprimé SANS retry, pause 60 min - le serveur a jeté par design
401réouverture de session une fois, puis retry
408 / 429 / 5xx / réseaulot conservé, backoff
autre 4xxlot supprimé et loggé fort : c'est un bug, le rejouer bloquerait la file

L'event_id partagé

L'UUID d'un événement est généré au moment du track(), pas à l'écriture : il est transmis tel quel au pipeline ET aux destinations publicitaires (adapter TikTok embarqué, envoi serveur vers Meta). Le même événement arrivant chez une régie par deux chemins se déduplique sur cet id.

Pièges connus

  • Les fichiers de file tournent en ready-<ms>-<seq>, pas en uuid7 : deux rotations dans la même milliseconde rendaient l'ordre d'envoi aléatoire.
  • Tests Android : advanceUntilIdle (coroutines-test) ne pilote pas Dispatchers.IO où vit OkHttp - les tests réseau tournent en temps réel avec MockWebServer.takeRequest(timeout).
  • iOS : le flush de passage en arrière-plan s'enveloppe dans un beginBackgroundTask, sinon la suspension coupe la requête en vol.
  • gzip : accepté par le serveur, pas encore envoyé par le SDK.

On this page