feat(api): champion scaling endpoint (win rate by game duration)#544
Conversation
GET /champions/{id}/scaling?position=&patch= returns win rate bucketed by game
duration (<20/20-25/25-30/30-35/35m+) plus a scaling index (long-game win rate
minus short-game; positive = scales late). Computed live from match participants
via a CASE bucket in GROUP BY — no timeline or aggregation table. Same queue/
patch/tracked-account population and sample floor as the sibling champion reads,
cached 60s.
Part of #537.
There was a problem hiding this comment.
Review — feat(api): champion scaling endpoint
Ce que fait ce PR
Nouvel endpoint GET /champions/{id}/scaling?position=&patch= : win rate bucketed par durée de partie (5 tranches) + scaling index (WR long - WR court). Un seul round-trip SQL via GROUP BY conditionnel, cache 60 s, zéro nouvelle table.
Points vérifiés
- Logique du scaling index :
buckets[^1].WinRate - buckets[0].WinRatesur la liste triée par bucket → toujours « plus long qualifié - plus court qualifié ». Correct. - Filtre thin-bucket en SQL (
.Where(x => x.Games >= minGames)) : correct, évite une collection côté client. - Traduction EF Core : le CASE conditionnel dans le GroupBy est supporté par le provider Npgsql et les tests passent sur vrai Postgres. OK.
- Sécurité :
championIdest unintpath-param,positionetpatchsont validés avant usage, le LIKE est paramétré par EF Core. Pas d'injection. - Population cohérente avec les siblings (tracked accounts, queue, patch optionnel).
- Tests : 3 tests d'intégration sur vrai Postgres couvrant le chemin nominal, le drop de thin-bucket + null index, et le 400 sur position invalide.
Suggestions (non bloquantes)
[FromQuery] string? positiondevrait être[FromQuery, Required] string positionpour que le schéma OpenAPI reflète le caractère obligatoire du paramètre (commentaire inline).- Deux cas de test à envisager : filtre patch (vérifier que
EF.Functions.Likeexclut bien les autres patchs) et jeu de données vide →Buckets = [],ScalingIndex = null(commentaire inline).
Aucun point bloquant identifié. ✅
There was a problem hiding this comment.
Bon ajout, l'implémentation est propre et les tests couvrent bien les cas importants (buckets, floor, patch filter, réponse vide, 400).
Aucun point bloquant.
Suggestions (non bloquantes) :
-
Double normalisation du patch (
ChampionScalingQueryService.csl.35-37) —ChampionQueryParameterNormalizer.NormalizePatchétant identique au re-parse interne (PatchVersion.TryParse + ToMajorMinor), le contrôleur passe déjà soitnullsoit une chaîne validée. La ligne dans le service peut être remplacée parvar normalizedPatch = patch;. Détail inline. -
Bucketentier dans le read-model (ChampionScalingResponse.csl.23) — exposer l'index 0-4 dans l'API publique en plus duLabelcrée un couplage fragile ; si les tranches bougent, les clients qui s'appuient sur la valeur numérique cassent silencieusement. Détail inline.
What
The cheapest item of the timeline-analytics epic (#539): a champion scaling stat — and it needs no timeline at all.
GET /champions/{championId}/scaling?position=&patch=returns win rate bucketed by game duration (<20,20-25,25-30,30-35,35m+) plus a single scaling index = long-game win rate minus short-game win rate (positive = the champion scales into the late game).How
ChampionScalingQueryService: filters this champion at this position (tracked accounts, queue, optional patch), joinsmatchesforGameDurationSeconds, buckets via a CASE in GROUP BY, counts games + wins per bucket, drops buckets below the sample floor — one SQL round-trip. Index computed in-memory from the qualifying buckets.gameDuration+Win, both already in base — no new ingestion or table.Testing
dotnet build --configuration Releaseclean (0 warnings).ChampionScalingApiIntegrationTests(3 tests) pass on real Postgres: deterministic per-bucket win rate + scaling index, thin-bucket drop + null index, invalid-position 400.Remaining for #537
Part of #537