Analytics et préjugés : pourquoi les métriques n'intéressent pas que mon chat
Découvrez pourquoi chaque clic vaut son pesant de clic, même si votre chat en doute.
"Les métriques, c'est comme les opinions : tout le monde en a, mais personne ne veut entendre celles des autres."
Pourquoi on s'emmerde avec des chiffres ?
Alors tu vois, on pourrait se contenter d'écrire des articles de qualité et attendre que les Internets nous envoient des chocolats de remerciement. Mais NON. On est des psychopathes du data-driven, alors on a mis en place un système complet pour savoir EXACTEMENT combien de personnes ont eu le malheur de cliquer sur nos liens.
Est-ce qu'on avait besoin de ça ? Non. Est-ce qu'on l'a fait quand même ? Oui. Parce que c'est ça, être un développeur en 2023 2024 2025 (raye la mention inutile, ou toutes, comme ton âme).
Architecture de la chose
Notre système de métriques est composé de trois grandes parties :
- Récupération des données depuis Umami (parce que Google Analytics c'est has-been et ça mange des cookies)
- Traitement et pondération des métriques (parce que tous les clics ne se valent pas, comme les mecs)
- Exploitation pour les Top Articles (parce qu'il faut bien mettre des paillettes dans la vie des gens)
Umami → Notre Système → Résultats Pondérés
(données brutes) (algorithmes) (articles populaires)
Composants principaux : la dream teamp des métriques
Les Commandes (a.k.a "les Ordres qu'on donne au serveur")
On a trois commandes essentielles, comme les trois repas par jour, sauf que là c'est vraiment utile :
1. FetchUmamiStatsCommand : Le Ramasseur de Données
#[AsCommand(
name: 'app:umami:fetch-stats',
description: 'Fetch Umami stats and dispatch messages to process them',
)]
class FetchUmamiStatsCommand extends Command
{
// Le code qui va chercher des données comme si c'était des truffes
}
Ce petit bijou de technologie fait une chose simple : il va chercher les stats sur Umami et les balance dans notre système de messages. C'est comme envoyer un stagiaire chercher du café, sauf que là ça marche.
2. CleanMetricsDuplicatesCommand : Le Nettoyeur
#[AsCommand(
name: 'app:metrics:clean-duplicates',
description: 'Nettoie les entrées en double dans la table page_metrics',
)]
class CleanMetricsDuplicatesCommand extends Command
{
// Le code qui fait le ménage comme ta mère t'a toujours dit de le faire
}
Cette commande, c'est Marie Kondo mais pour les données. Elle regarde tes métriques en double et se demande : "Est-ce que cette métrique me procure de la joie ?" Si non, elle dégage.
3. RecalculateScoresCommand : le juge de Drag Race
#[AsCommand(
name: 'app:metrics:recalculate-scores',
description: 'Recalcule les scores pondérés de tous les articles',
)]
class RecalculateScoresCommand extends Command
{
// Le code qui note tes articles comme un jury de patinage artistique
}
Cette commande recalcule les scores de tous tes articles, parce que ton article sur "Comment faire des poneys avec Symfony" mérite peut-être mieux que sa 47ème place actuelle. Ou pas.
Le Service de Pondération : le génie du système
Le cœur de notre système de génialitude, c'est AdvancedScoreCalculator
. Si Einstein avait codé des systèmes de recommandation d'articles, ça aurait ressemblé à ça (en moins bien).
class AdvancedScoreCalculator
{
// Facteurs influençant le score final
private const float POPULARITY_WEIGHT = 0.4; // Popularité (vues)
private const float RECENCY_WEIGHT = 0.25; // Récence du contenu
private const float TREND_WEIGHT = 0.2; // Tendance récente (augmentation des vues)
private const float DIVERSITY_WEIGHT = 0.15; // Diversité (fréquence d'apparition)
// Configuration
private const int MAX_RECENT_DAYS = 90; // Récence maximale (3 mois)
private const int TREND_DAYS = 7; // Période pour la tendance (7 jours)
// Et plein de méthodes compliquées qui font des maths
}
Cet algorithme est tellement sophistiqué qu'il prend en compte :
- La popularité : Combien de fois les gens ont cliqué dessus (comme ton ex sur Instagram)
- La récence : Si c'est frais comme une baguette du matin ou vieux comme le chewing-gum au fond de ton sac
- La tendance : Si ça monte plus vite que le prix de l'immobilier à Paris
- La diversité : Pour éviter de voir TOUJOURS les mêmes articles, comme toujours tomber sur Despacito en fin de soirée
Workflow complet : comment ça marche bordel ?
-
Umami collecte les vues en temps réel sur ton site (les gens qui cliquent, scrollent, et parfois même lisent)
-
Toutes les 3 heures, la commande
app:uami:fetch-stats
se déclenche et va chercher les données :# Ça lance ce truc symfony console app:umami:fetch-stats
-
Pour chaque URL visitée, un message
ProcessPageMetricsMessage
est envoyé :$message = new ProcessPageMetricsMessage( $metric->getUrl(), $metric->getViews(), );
-
Le handler de messages reçoit et traite ces informations en créant des
PageMetric
:$metric = new PageMetric(); $metric->setUrl($message->getUrl()); $metric->setViews($message->getViews());
-
Toutes les 6 heures, les scores sont recalculés avec notre algorithme avancé :
# Ça lance l'algorithme qui fait des maths comme si ta vie en dépendait symfony console app:metrics:recalculate-scores
-
Une fois par semaine (le dimanche à 4h30 du matin, quand même les fêtards sont couchés), on nettoie les données en double :
# Nettoyage comme si Marie Kondo passait sur tes données symfony console app:metrics:clean-duplicates
-
Le composant TopPosts utilise ces scores pour afficher les articles les plus intéressants :
// Quelque part dans le code qui génère les recommandations $diverseTopArticles = $this->scoreCalculator->getDiverseTopArticles(5);
CRONs : la planification des basses œuvres
Dans .platform.app.yaml
, on a configuré des tâches planifiées qui s'exécutent automatiquement :
crons:
# Métriques - données Umami et pondération
umami-fetch:
# Récupère les statistiques de trafic depuis Umami toutes les 3 heures à :15 (ex: 3h15, 6h15, 9h15...)
# Cette commande collecte les données de pages vues pour les articles
spec: '15 */3 * * *'
cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape symfony console app:metrics:fetch-stats; fi
recalculate-scores:
# Recalcule les scores pondérés pour le classement d'articles toutes les 6 heures à :45 (ex: 5h45, 11h45, 17h45...)
# Utilise l'algorithme AdvancedScoreCalculator pour calculer la popularité, la récence, la tendance et la diversité
spec: '45 */6 * * *'
cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape symfony console app:metrics:recalculate-scores; fi
clean-metrics:
# Nettoie les doublons de métriques une fois par semaine (dimanche à 4h30 du matin)
# Cette opération de maintenance garantit l'intégrité de la base de données des métriques
spec: '30 4 * * 0'
cmd: if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then croncape symfony console app:metrics:clean-duplicates; fi
Ça s'exécute automatiquement, comme ton anxiété quand tu reçois un message "On peut parler ?" de ton chef.
Algorithme de pondération : la sauce secrète
L'algorithme qui calcule les scores est plus complexe que ton dernier rendez-vous Tinder. Il prend en compte quatre facteurs principaux :
1. Score de Popularité (40% du score final)
private function calculatePopularityScore(int $views): float
{
// log10(vues + 1) pour éviter log(0) et normaliser les résultats
// Un article avec 1000 vues aura un score de 0.75, 10000 = 1.0
return min(1.0, log10($views + 1) / 4);
}
On utilise une échelle logarithmique parce que 10 000 vues, c'est pas 10 fois mieux que 1 000 vues. C'est juste que l'auteur a probablement partagé l'article dans plus de groupes Facebook.
2. Score de Récence (25% du score final)
private function calculateRecencyScore(Post $post): float
{
$publishedAt = $post->getPublishedAt();
if (!$publishedAt instanceof \DateTimeImmutable) {
return 0;
}
$now = new DateTime();
$daysSincePublished = $now->diff($publishedAt)->days;
// Score dégressif en fonction de l'ancienneté
if ($daysSincePublished > self::MAX_RECENT_DAYS) {
return 0.1; // Score minimum pour les articles très anciens
}
return 1 - (0.9 * $daysSincePublished / self::MAX_RECENT_DAYS);
}
Parce que ton article sur "Comment installer Windows Vista" n'est peut-être plus aussi pertinent en 2024.
3. Score de Tendance (20% du score final)
private function calculateTrendScore(Post $post): float
{
// Si le post a moins de TREND_DAYS jours, lui donner un bonus de tendance
$publishedAt = $post->getPublishedAt();
if (!$publishedAt instanceof \DateTimeImmutable) {
return 0.5; // Valeur moyenne par défaut
}
$now = new DateTime();
$age = $now->diff($publishedAt)->days;
// Si l'article est très récent, lui donner un bonus
if ($age <= self::TREND_DAYS) {
return 0.8 + (0.2 * (self::TREND_DAYS - $age) / self::TREND_DAYS);
}
return 0.5;
}
Ceci favorise les articles qui sont en train de monter en popularité, comme un nouveau mème sur Twitter avant qu'Elon ne le ruine.
4. Score de Diversité (15% du score final)
private function calculateDiversityScore(Post $post): float
{
// Logique complexe pour éviter de toujours montrer les mêmes articles
// ...
// Si l'article a beaucoup de vues, réduire son score de diversité
if ($views > 10000) {
return 0.4; // Forte pénalité pour les articles très populaires
}
// ...
}
Ceci évite l'effet "DJ qui passe toujours les mêmes chansons". On veut parfois découvrir autre chose que "Despacito".
Sélection finale : comment on choisit les gagnants
La méthode qui sélectionne les articles à afficher est un chef-d'œuvre d'équilibrage entre popularité et diversité :
public function getDiverseTopArticles(int $limit = 5): array
{
// 1. Récupérer plus d'articles que nécessaire (pour avoir de la marge)
$candidates = $this->metricRepository->findTopArticlesWithWeighting($limit * 2);
// 2. Toujours prendre les 2 premiers (les meilleurs scores)
// ...
// 3. Pour les places restantes, faire une sélection pseudo-aléatoire pondérée
// avec un facteur aléatoire qui réduit légèrement l'importance du score
$randomFactor = (random_int(0, 100) / 100) * 0.3; // 30% d'aléatoire
// ...
}
On garde les deux articles vraiment populaires, puis on ajoute un peu de hasard pour les autres places. Comme la playlist Spotify en mode shuffle qui joue quand même toujours du Queen.
Le fin mot de tout ça
Tu viens de découvrir un système de métriques plus sophistiqué que la politique monétaire de la BCE, mais moins mystérieux que l'algorithme de recommandation de TikTok.
En résumé :
- On récupère des données d'Umami
- On les traite avec un algorithme qui prend en compte plusieurs facteurs
- On utilise ces données pour afficher les meilleurs articles
- Tout ça s'exécute automatiquement grâce aux crons
C'est comme ça qu'on fait des recommandations d'articles en 2024, et c'est exactement comme ça que Google fonctionne (à quelques milliards de dollars de budget R&D près).
Liens utiles
- Documentation Symfony Messenger - Pour comprendre comment on envoie des messages comme des pigeons voyageurs modernes
- Documentation Symfony Console - Pour créer des commandes CLI qui font des trucs
- Documentation Umami - L'API qui nous donne les chiffres magiques
- Calculer des scores avec des logarithmes - Pour les matheux qui veulent comprendre pourquoi log10
- Cron Expression Explainer - Pour déchiffrer les expressions cron comme si c'était des hiéroglyphes
"Les métriques, c'est comme les abdos : tout le monde en veut, mais personne ne veut faire l'effort de les obtenir."