Pourquoi Rust est-il idéal pour les tests A/B ?
\nQuand on parle de tests A/B, on pense généralement à Python, R ou JavaScript. Mais Rust est le cheval sombre qui, dans des conditions réelles de combat, se montre plus rapide, plus sûr et moins cher. Si votre backend est écrit en Rust, vous pouvez intégrer des expériences directement dans le cœur du système sans le surcoût des microservices et de Redis.
\nImaginez : vous modifiez l'algorithme de recommandations, la couleur du bouton "Acheter" ou la logique d'analyse. Une erreur dans le code d'un test A/B en Python peut faire planter le serveur. En Rust, vous ne compilerez tout simplement pas si vous oubliez une branche de condition. Le compilateur est votre meilleur QA.
\nDans cet article, nous allons détailler les tests A/B en Rust de l'architecture à la production. Vous apprendrez à réaliser des tests fractionnés sans douleur, à calculer des statistiques et à ne pas perdre d'utilisateurs.
\n\nArchitecture d'un test A/B en Rust : les trois piliers
\nToute expérience dans une application Rust repose sur trois composants :
\n- \n
- Randomiseur — qui va dans le groupe A, qui va dans le groupe B ? \n
- Stockage des affectations — pour que l'utilisateur ne saute pas entre les groupes à chaque requête. \n
- Journalisation et statistiques — on calcule la conversion et la p-value. \n
Écrivons un module simple mais fonctionnel. Pas de frameworks, seulement Rust et sa bibliothèque standard.
\n\nÉtape 1. Randomisation basée sur le hachage
\nLa randomisation déterministe est la clé de la stabilité. Si vous utilisez rand::random(), à chaque redémarrage du serveur, l'utilisateur peut basculer dans un autre groupe. Utilisons un hachage pondéré à partir de l'ID utilisateur et du nom de l'expérience.
use std::collections::hash_map::DefaultHasher;\nuse std::hash::{Hash, Hasher};\n\n#[derive(Debug, Clone, Copy, PartialEq)]\npub enum Group {\n Control,\n Treatment,\n}\n\npub fn assign_group(user_id: &str, experiment: &str, treatment_weight: f64) -> Group {\n let mut hasher = DefaultHasher::new();\n user_id.hash(&mut hasher);\n experiment.hash(&mut hasher);\n let hash = hasher.finish();\n\n let normalized = (hash as f64) / (u64::MAX as f64);\n if normalized < treatment_weight {\n Group::Treatment\n } else {\n Group::Control\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_deterministic() {\n let g1 = assign_group("user_42", "button_color_v2", 0.5);\n let g2 = assign_group("user_42", "button_color_v2", 0.5);\n assert_eq!(g1, g2);\n }\n}\n\n\nCe code garantit que le même utilisateur sera toujours dans le même groupe pour une expérience donnée. Le poids treatment_weight est la proportion du trafic qui va dans le groupe test (par exemple, 0.1 = 10 %).
Étape 2. Stockage : cache ou base de données ?
\nPour les projets à forte charge, stocker les affectations en mémoire est risqué. Si le serveur tombe, l'utilisateur perd son groupe. Il est préférable d'utiliser Redis ou SQLite. Mais pour simplifier, nous montrons un cache en mémoire avec TTL.
\n\nuse std::collections::HashMap;\nuse std::time::{Duration, Instant};\n\nstruct AssignmentCache {\n store: HashMap<String, (Group, Instant)>,\n ttl: Duration,\n}\n\nimpl AssignmentCache {\n fn new(ttl_secs: u64) -> Self {\n Self {\n store: HashMap::new(),\n ttl: Duration::from_secs(ttl_secs),\n }\n }\n\n fn get(&mut self, key: &str) -> Option<Group> {\n if let Some((group, time)) = self.store.get(key) {\n if time.elapsed() < self.ttl {\n return Some(*group);\n }\n }\n None\n }\n\n fn set(&mut self, key: String, group: Group) {\n self.store.insert(key, (group, Instant::now()));\n }\n}\n\n\nDans un projet réel, remplacez HashMap par dashmap ou Redis — cela apportera la sécurité des threads et la persistance.