Silence was two different bugs
One missing leaderboard entry exposed both silent server rejections and a client race that could erase a finished game without making a request.
A player finished the Daily Challenge and could not submit the score. There was no error, no explanation, and no useful record of what the server had decided. An incognito window worked, which made several causes plausible but proved none of them. The fix had to answer two questions separately: what should a player see when a submission is rejected, and was a rejection even what happened here?
Make every rejection visible#
The first pass mapped every non-success response from the Daily and AI Duel leaderboard endpoints. The previous policy gave one validation failure specific treatment and left the rest silent, on the reasoning that retyping a name could not fix a rate limit, an expired session, or a server error. That reasoning still held, but silence did not follow from it. The replacement has four outcomes: blocked text, rate limited, session expired, or a generic failure. Each gets an honest message, and each rejected request writes a small structured reason to the server log so the next report has evidence behind it.
The implementation also corrected its own map while it was being built. AI Duel had two nearby endpoints, and the first logging pass covered the background end-game call rather than the leaderboard call the client actually awaited. Following the real client path exposed the mismatch. The logging and message work then landed together, with 1,698 unit tests passing and three end-to-end cases driving rate-limited, expired-session, and generic failures through the recap.
The request that never happened#
The original incident still could not be explained from production telemetry, because the telemetry did not exist when it happened. Code-level investigation ruled out the obvious theories and found a different silent path. The Daily page reloads its challenge when the tab becomes visible. If that happened after the game ended but before the completion effect saved the result, this condition treated game-over as permission to start again:
!game.isStarted ||
game.state.phase === 'game-over' ||
game.state.challengeId !== challenge.challengeId
The middle clause was redundant for every intended flow. A new day is already identified by a different challenge, and the same Daily Challenge cannot be replayed. Removing it made the finished in-memory game authoritative, rather than asking a local-storage effect to win a timing race. A small pure decision function now carries that rule, with deterministic tests for the race, the day rollover, the first start, and an older game still in progress.
The two fixes meet at the same promise: a completed game should not disappear into silence. If the server rejects a submission, the player gets a useful answer and the logs retain a reason. If no request was sent because the client replaced the finished game first, that state is preserved instead. The remaining unknown is whether the original report took exactly that race path, but the system no longer needs a guess before it can explain the next failure.