Aucun outil tiers. Trois sources :
app ──POST /events──► events (bruts, 30 jours) ──┐
jeu ───────────────► answers, matches, messages… ─┼─► rollup (10 min) ─► daily_stats, question_stats (sans limite)
└─► requêtes directes de la console (/api/admin/*)
app/lib/api/analytics.dart)| Règle | Valeur |
|---|---|
| File | en mémoire + persistée sur le téléphone (_persist) |
| Envoi | toutes les 30 s, à la mise en arrière-plan, au démarrage ; tout de suite si la file atteint 200 |
| Paquet | 200 événements au plus |
| Nouvelle session | retour au premier plan après ≥ 5 min en arrière-plan, ou lancement à froid |
| Bornage | textes coupés à 200 caractères (query : 40) |
back/app/routers/events.py, post_events)| Étape | Règle |
|---|---|
Schéma (schemas.py, EventsIn) |
1 à 200 événements ; type 1-40 car., ts datetime obligatoire, session_id ≤ 36, platform ≤ 16, app_version ≤ 20, props objet |
| Authentification | CurrentUser (jeton obligatoire) |
| Filtre de type | hors EVENT_TYPES (26 types) : ignoré sans erreur |
platform |
ios, android, sinon other (vide si vide) |
props (clean_props) |
20 clés au plus, clés coupées à 40 ; chaînes ≤ 200 (query ≤ 40) ; listes ≤ 20 scalaires ; objets imbriqués supprimés |
| Ajouts serveur | id, user_id, country_code du compte (FR par défaut), ts converti en UTC |
| Écriture | après la réponse 202 (BackgroundTasks, store) : insertion + last_seen_at, platform, app_version du compte (d'après le dernier événement du paquet) |
Types : session_start, session_end, screen_view, signup, login, onboarding_done, feed_view, feed_skip,
theme_change, theme_follow, like, save, share, comment, follow, search, create_question, level_up,
answer_error, app_error, report, feed_card_view, feed_card_tap, feed_card_reward, paywall_view,
purchase. Propriétés de chacun : back/ANALYTICS.md. Les réponses ne sont pas des événements (table answers).
back/app/services/analytics.py, rollup)| Point | Valeur |
|---|---|
| Lancement | rollup_loop dans chaque worker : au démarrage puis toutes les 600 s (ROLLUP_EVERY_S) |
| Verrou | SET analytics:rollup:lock NX EX 540 ; pris par un autre → on saute ce passage ; supprimé à la fin |
| Fenêtre | les 2 derniers jours locaux (aujourd'hui compris) : DELETE puis recalcul de daily_stats |
| Journées | ANALYTICS_TZ (défaut Europe/Paris) ; bornes UTC par localtime.day_bounds |
| Ordre | rollup_days → rollup_questions → celebrate_milestones → purge_events → requeue_low_quality → commit → close_finished_seasons |
| Manuel | docker compose exec api python -m app.cli rollup --days N (1 à 366, force : ignore le verrou) |
Purge : DELETE FROM events WHERE ts < maintenant − 30 jours (RETENTION_DAYS), à chaque passage.
daily_stats (clé : date, platform ∈ all, ios, android)| Colonne | Calcul (compute_daily) |
|---|---|
signups |
users.created_at dans la journée |
active_users |
COUNT(DISTINCT user_id) sur l'union {événements du jour} ∪ |
sessions |
événements session_start |
session_minutes |
Σ par session de MAX(props.duration_ms) des session_end (jamais la somme), ÷ 60 000 |
answers, correct |
lignes answers du jour, SUM(correct) |
feed_views, feed_skips, shares, searches |
événements du type |
searches_empty |
search avec props.results = 0 |
comments, follows |
lignes comments, follows du jour |
questions_created |
questions du jour hors équipe (team_condition : @slidz ou is_admin) |
Filtre de plateforme : sur events.platform pour les événements ; sur users.platform actuel pour les tables sans
plateforme (answers, comments, follows, questions, users).
question_stats (une ligne par question, tout est effacé puis recalculé)| Colonne | Source |
|---|---|
plays, correct, avg_elapsed_ms |
answers, depuis toujours |
views, skips, shares |
événements feed_view, feed_skip, share par props.question_id : 30 jours |
likes, comments |
compteurs de questions |
difficulty |
skill.smoothed_difficulty(plays, correct, indice du filtre de modération) |
difficulty_confident |
plays ≥ CONFIDENT_PLAYS |
Paliers : celebrate_milestones annonce 100, 1 000, 10 000 parties à l'auteur (hors @slidz), une fois par palier
(questions.milestone).
back/app/routers/admin.py)Fonctions communes (services/analytics.py) :
| Fonction | Définition |
|---|---|
pct(a, b) |
round(100 × a / b, 1), 0 si b = 0 |
kpi(v, p) |
{value: v, previous: p} |
active_users(début, fin, plateforme) |
joueurs distincts avec un événement ou une réponse dans [début, fin[ |
session_totals |
une ligne par session_id : MAX(duration_ms), MAX(answers), dernier ts |
Période (Period) : from, to inclus, défaut 28 jours finissant aujourd'hui, 366 jours max ; période précédente =
même nombre de jours juste avant.
overview)| Indicateur | Formule |
|---|---|
| sommes (inscriptions, sessions, réponses…) | Σ daily_stats de la période |
| actifs par jour (moyenne) | Σ daily_stats.active_users ÷ nombre de jours |
| actifs 7 j | active_users(fin − 7 j, fin) ; précédent : (fin − 14 j, fin − 7 j) |
| actifs 30 j | active_users(fin − 30 j, fin) ; précédent : (fin − 60 j, fin − 30 j) |
| collage | pct(actifs/jour moyen, actifs 30 j) |
| durée moyenne | session_minutes ÷ sessions |
| réussite | pct(correct, answers) |
| parties | lignes matches créées dans la période |
retention)| Bloc | Formule |
|---|---|
| Cohortes | 12 semaines (lundi) finissant la semaine de to. Pour J+n (1, 7, 30) : éligibles = inscrits avec inscription + n ≤ aujourd'hui ; retenus = actifs exactement le jour inscription + n (événement ou réponse) ; pct(retenus, éligibles), null sans éligible |
| Entonnoir | inscrits de la période → onboarding_done (toutes dates) → au moins une réponse → actif un jour ≥ J+1 → actif un jour ≥ J+7 |
| Méthodes | COUNT(DISTINCT user_id) des login + signup par props.method |
| Plateformes | comptes vus ou créés depuis le début de la période, par users.platform |
engagement)Questions par session (session_totals.answers : 1-5, 6-10, 11-20, 21-50, 50+) ; durée (<1, 1-3, 3-5, 5-10, 10+ min) ;
réussite par format et par thème ; abandon : pour les positions 1 à 30, vues − sauts ; réussite par palier de
difficulté (EASY_MAX, HARD_MIN, défaut PRIOR_DIFFICULTY) ; force : meilleure skill du joueur par thème ; heures et
jours des session_start (heure locale).
social)| Indicateur | Formule |
|---|---|
| taux de participation | pct(match_players avec finished_at, match_players) des parties créées dans la période |
| part dans les groupes | pct(parties dans une conversation de groupe, parties lancées) |
| joueurs par partie | match_players terminés ÷ parties finies |
| recherches vides | pct(searches_empty, searches) ; top 20 des props.query |
economy)Niveaux : état actuel. XP par source : Σ xp_events.amount dans la période. Saison en cours : scores all > 0 hors
exclus (nombre, max, médiane). Défis : pct(user_challenges.rewarded, user_challenges). Cartes du fil :
pct(feed_card_reward, feed_card_view) par kind. Badges : pct(détenteurs, comptes).
content)Lit question_stats : réussite 100 × correct ÷ plays, sauts pct(skips, views), difficulté (défaut
PRIOR_DIFFICULTY).
Abonnements : voir Slidz+.
CLAUDE.md dit « agrégés chaque heure » : le rollup tourne toutes les 10 minutes. Et comme le verrou est rendu àAPI_WORKERS passages par 10 min.rollup --days N avec N > 30 dégrade l'historique : les événements de plus de 30 jours sont purgés, donc lesactive_users au-delà de 30 jours ne voient plus que lesonboarding_done est cherché dans les événements bruts ; pour des inscrits de plus de 30 jours,answers, comments, follows, signups : c'est la plateforme actuelle du joueur ;ts vient du téléphone et n'est pas borné : un paquet resté hors ligne plus de 2 jours n'entre dans daily_statsts futur est accepté.question_stats : views, skips, shares couvrent 30 jours, plays depuis toujours ; les deux ne sont pasCode : back/app/routers/events.py (post_events, clean_props, store), back/app/services/analytics.py
(rollup, rollup_loop, compute_daily, rollup_questions, active_users, session_totals, kpi, pct),
back/app/routers/admin.py (Period, overview, retention, engagement, social, economy, content),
back/app/services/localtime.py, back/app/cli.py, app/lib/api/analytics.dart