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
Trois choix de conception expliquent presque tout le reste :
- 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.
- Un seul consommateur. Tout passe par un acteur (Swift) / une coroutine unique (Kotlin) : enqueue, flush, consentement, sessions. Aucun verrou ailleurs, aucune course possible.
- 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.
granted: capture + envoi.unknown: capture et persiste, n'envoie rien - le mode queue-before-consent des apps à CMP. Poserunknownavantconfigurefonctionne (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 serveur | Réaction du SDK |
|---|---|
| 2xx | lot supprimé (dédup serveur par eventId si double envoi) |
2xx + quotaExceeded | lot supprimé SANS retry, pause 60 min - le serveur a jeté par design |
| 401 | réouverture de session une fois, puis retry |
| 408 / 429 / 5xx / réseau | lot conservé, backoff |
| autre 4xx | lot 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 pasDispatchers.IOoù vit OkHttp - les tests réseau tournent en temps réel avecMockWebServer.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.