A missing stat is not a zero

DHSeaDev — Chrome Extensions, Windows Tools, & Idle Games

Dota Companion 1.9.0 is out. The headline is an Insights tab, but the part worth writing down is a one-word default that had been wrong since the first build and that nothing caught for a month.

The line that was wrong

Every match that comes back from OpenDota goes through a normaliser before anything touches it. That normaliser had a line that read, in effect, gold per minute, or zero if absent. Which sounds like defensive programming, and is the opposite of it.

OpenDota omits those fields on aborted games and on some partial rows. Zero is not a missing value. Zero is a legitimate observation that a player earned no gold at all, and the code downstream had no way to tell the two apart. So one omitted field in a twenty-game window dragged the goal baseline down by about five percent, moved the progress bar backwards, and became the floor of the GPM sparkline, flattening the whole trend line against a number that never happened.

The stats module had guarded against exactly this. Its baseline function checks the type of every value before it counts it. The guard was correct and it was being defeated one layer upstream, because by the time the value reached it, the missing field had already been turned into a real number.

Fixing it broke something else, which is the interesting part

Changing the default to null immediately broke the improvement streak, and the reason is a piece of JavaScript trivia that is worth carrying around: isFinite(null) is true. Not an error, not false. JavaScript converts null to zero, decides zero is finite, and agrees. The streak calculation used a bare isFinite check, so the null it now received sailed straight through and was counted as a zero anyway — the same bug, one layer down, wearing a different hat.

Both checks now test the type first. A match with no metric breaks the streak instead of silently scoring zero, which is the honest behaviour: an unknown is not a failure and it is not a success.

What the browser found that the tests could not

The new code shipped with ninety-eight tests. All of them run in Node, none of them open a browser, and that boundary turned out to be exactly where three more defects were sitting. Loading the extension as a real unpacked extension in Chromium and driving the popup found all three in one pass.

The first: the new status line stayed silent for anyone with refresh on open switched off, because that code path never recorded when the cached data was fetched. The one user most likely to be reading hours-old numbers was the one user the freshness warning could not reach.

The second: each hero row displayed one metric’s number beside a verdict computed from all of them, so a hero could read zero percent GPM and sliding on the same line. The row contradicted itself. It now shows whichever metric actually drove the verdict.

The third: adding an enemy pick in the draft helper downgraded every suggestion to thin sample — a label about matchup coverage, sitting next to a ranking that was never about matchups.

None of those are logic errors. Every one of them is a statement the interface was making that the data did not support, and a unit test has no opinion about what a row says to a person.

The draft helper says what it is

The Matchups tab can now take the enemy picks as they happen and rank your own heroes against them. What it will not do is pretend to be more than it is.

A true counter-pick would need your record on each candidate hero against each specific enemy, which is one request per hero in your pool, against an API that this extension does not yet pace itself against. So the ranking is your hero pool ordered by your own record, and the panel says that in plain words rather than letting the layout imply otherwise. The enemy picks drive a second set of lines underneath: how you actually perform when that hero is on the other team, from data already fetched.

The ordering uses a confidence lower bound rather than the raw win rate, because the first version of that list put a three-game hero at ninety-something percent above a forty-game hero at sixty-five. A record that thin is not a win rate, it is a coin that came up heads three times. The number on screen is still the estimate; the thing doing the sorting is the bound underneath it, and the gap between them is the uncertainty.

What it says before it says anything

The last piece is the smallest and probably the most useful. A line at the top of the Overview tab now describes the data before showing anything built on it: whether Dota 2 is hiding your match history, whether your rank is uncalibrated, whether the sample is too thin for a baseline to mean anything, how old the cached copy is, and the fact that a finished game can take up to an hour to reach OpenDota at all.

Every first-run failure this extension had was a silent one. A private profile and a working profile with no recent games rendered identically: dashes, and no explanation. Saying so costs a line of text and removes the most common reason someone concludes the thing is broken and uninstalls it.

Dota Companion is free on the Chrome Web Store — no account, no API key, and the full changelog is on the project page.

Leave a Reply

Your email address will not be published. Required fields are marked *