fix(stats): vlr.gg page revamp broke /stats — header-keyed data-col parsing, new region vocabulary, session priming#163
Open
aLaaah wants to merge 23 commits into
Open
Conversation
# Conflicts: # requirements.txt
# Conflicts: # requirements.txt
- Use the GHCR_PAT secret for container registry login - Keep branch-gated pushes for latest and dev image tags - Avoid GITHUB_TOKEN package permission issues on existing GHCR packages
- Add the official setup-uv action to both Docker workflows. - Enable uv caching keyed by requirements.txt. - Replace pip install with uv pip install against Python 3.11.
- Move dependency installation into a builder stage so the final image only ships runtime artifacts.\n- Switch the base image to the latest Python slim line and replace the curl healthcheck with a Python probe.\n- Expand .dockerignore to keep local workspace, docs, and test files out of the build context.
…b, session priming
vlr.gg revamped its /stats page, breaking the positional scraper in four ways.
All four are fixed here without touching the shared /rankings region dict, so the
patch is self-contained and upstream-contributable.
D1 — Column indices shifted (21 -> 23 columns: `maps` and `fkfd` inserted). Build a
{data-col -> index} map from <thead> once per parse and read every cell by semantic
key. REQUIRED_KEYS = {rnd, rating2, acs, kd, adr}: if the header is keyed but a
required key is missing, raise (fail closed) rather than emit a keyed None that would
flow out as rating=0. Only a header with zero data-col attributes falls back to the
legacy positional indices, with a logged warning. Keying off data-col ends the
recurring positional fragility (upstream axsddlr#4, axsddlr#14).
D2 — Org selector renamed .stats-player-country -> .st-pl-country. Try the new class
first, fall back to the old, keep the N/A fail-closed sentinel.
D3 — /stats region vocabulary changed to all|americas|emea|pacific|china|intl. Add
STATS_REGIONS + validate_stats_region (stats-only); normalize deprecated aliases
(na/br->americas, eu->emea, ap/kr/jp/oce->pacific, cn->china) to canonical BEFORE the
cache key is formed so na and americas share one entry. The shared /rankings region
dict is untouched — /rankings?region=americas still 400s.
D4 — Cold-session requests ignore region= (vlr.gg binds filter state to PHPSESSID).
Prime the singleton client once with a throwaway GET (asyncio lock + module flag),
raising on prime failure so a cold response is never cached. Response-level
validation is the correctness signal: parse the selected <option> of
select[name=region]; on mismatch re-prime + refetch once, then raise. Nothing wrong
is ever cached.
timespan and MIN_ROUNDS are left exactly as-is. Output field set is unchanged.
Tests: header-keyed / legacy-fallback / missing-key / org-fallback / priming-order /
prime-failure / region-mismatch-retry / alias-cache-key / empty-region / region
vocabulary and the /rankings-still-400 guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The /stats routes' OpenAPI descriptions still advertised the retired na|eu|ap|la|... shortnames, several of which now 400 by design. Document the canonical vocabulary (all|americas|emea|pacific|china|intl) and the deprecated aliases. /rankings descriptions untouched (old keys still apply). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
vlr.gg revamped its
/statspage and it broke the scraper in four independent ways. Each defect below was reproduced against live vlr.gg and, where relevant, the Wayback archive of the old layout. The fix keys the parser off the semanticdata-colattributes vlr.gg now emits, so the whole class of "vlr.gg added a column" breakage becomes impossible going forward.The four defects (all reproduced)
(a) The table went 21 → 23 columns — every positional index after
agentsreads the wrong stat.vlr.gg inserted a
mapscolumn attd[2]and afk:fdcolumn attd[11]._parse_stats_rowreads hardcoded positional cells, so fields 2–10 shift by one and 11+ shift by two. Concretely,rounds_playedends up reading a Maps count (e.g."14") andratingreads an ACS (e.g."280"). Shifting today's payload back restores sanity (rounds≈280, rating≈1.40, acs≈210).Archive proof of the old 21-column layout: https://web.archive.org/web/20250915134359id_/https://www.vlr.gg/stats/ — there
td[2]=Rnd,td[3]=R2.0,td[4]=ACS, and.stats-player-countryis present.(b) The org selector no longer exists.
.stats-player-countryreturns zero matches on the current markup — it is now.st-pl-country. Theif not org: org = "N/A"default therefore fires for 100% of rows.(c) The
/statsregion vocabulary changed.<select name="region">now offersall | americas | emea | pacific | china | intl. The oldna | eu | ap | …keys are no longer recognized and silently fall back toallrather than erroring — so a caller passingregion=naunknowingly gets the global list. (Note:/rankingsis untouched and still uses the oldna|eu|ap|…keys — this taxonomy change is scoped to the/statspage only.)(d) The finding I haven't seen reported anywhere:
/statsbinds its filter to aPHPSESSIDsession.The first request on a cold client returns the unfiltered global list regardless of
region=— the filter only takes effect once vlr.gg has set a session cookie. Reproduced deterministically with a fresh cookie jar:This is quietly dangerous in a long-running service: with a singleton
httpx.AsyncClient(cookies persist for the process lifetime) plusCACHE_TTL_STATS = 1800, the first region requested after any restart receives the global list, and that wrong response is cached for 30 minutes under that region's key. One priming GET tohttps://www.vlr.gg/stats/before the first fetch fixes it deterministically, and the page conveniently echoes the applied region as theselectedoption ofselect[name=region], which gives a reliable response-level correctness signal.The fix
{data-col → index}map from<thead>once per parse and read cells by key, never by literal index. Theplayercell is unlabelled and stays positionaltd[0]; the previously mis-indexedclutch_attemptsis folded into the map under keycl.REQUIRED_KEYS = {rnd, rating2, acs, kd, adr}. If<thead>has ≥1data-colbut any required key is missing, the parser raises instead of shipping shifted numbers. A future layout change surfaces loudly rather than silently.<thead>yields zerodata-colattributes (old markup), and logs a warning when it does — so this stays backward-compatible with the pre-revamp page..st-pl-country, fall back to the old.stats-player-country, keep the"N/A"sentinel. Works against either markup./stats-specific setall, americas, emea, pacific, china, intl, validated byvalidate_stats_region. The legacy keys are accepted as deprecated aliases and normalized (na/br→americas,eu→emea,ap/kr/jp/oce→pacific,cn→china) before the cache key is formed, so an alias and its canonical don't hold duplicate cache entries. The shared/rankingsregion dict is left completely untouched — a test asserts/rankings?region=americasstill 400s.PHPSESSID, guarded by anasyncioonce-lock (the singleton client is shared across concurrent requests). Prime failure raises rather than caching a cold response. Correctness is validated at the response level: parse the selected<option>ofselect[name=region]; on mismatch, re-prime + refetch once, then raise on a second mismatch.timespanandMIN_ROUNDSare deliberately left alone.Why key off
data-colThis is the third recurrence of the same positional-index fragility — issue #4 (2022) and issue #14 (2024) were both "vlr.gg added a column", and both were fixed by re-hardcoding the new indices. Keying off the
data-colattributes vlr.gg now emits, with fail-closedREQUIRED_KEYS, retires the class of bug: a future column insertion can no longer shift stats into the wrong fields, and if a required column ever disappears the parser raises instead of returning quietly wrong numbers.Tests
+35 tests (suite 92 → 127, all passing on this branch):
rounds_played ≈ 228/rating ≈ 1.37(not the shifted11/228);N/A);/rankings?region=americasstill-400 guard proving the shared region dict is untouched.Related observations (reproducible; fixes intentionally out of scope here)
These look like the same revamp bleeding into other endpoints. I've kept this PR focused on
/stats, but I'm happy to open separate issues:/match/details→maps[].playersreturns empty scoreboards on the current markup (verified on matches spanning May 31 – Jul 9 2026). Almost certainly the same page revamp changed the scoreboard selectors./team?q=transactionsrows are field-shifted — the date column carries the player name and the role column carries the announcement URL. Same positional-parsing disease as (a) above.Let me know if you'd like these as their own issues, or folded into a follow-up.
🤖 Generated with Claude Code