Fix keyed telemetry match attribution (Refs #144)
CI / checks (push) Successful in 5m26s

This commit is contained in:
2026-10-12 00:58:05 -04:00
parent 4ea291d305
commit 785cc4616b
24 changed files with 1115 additions and 112 deletions
+49 -24
View File
@@ -14,9 +14,10 @@ only under All squad sizes. The JSON API also defaults to all maps.
Matches only appear in server/global history once they reach round 2.
Leaderboards, summaries, map options, profile statistics and player history include only
completed matches with a received `game_end`. Active and interrupted matches do
not contribute. A player must also be observed at round 2 or later; players who
left before a completed game's end retain their recorded stats.
completed matches with a received `game_end`. Active, interrupted, and
**End report missing** matches do not contribute. A player must also be observed
at round 2 or later; players who left before a completed game's end retain their
recorded stats.
This also excludes existing round 1 records. Early telemetry is retained so
qualifying games keep their start time, roster and cumulative counters.
@@ -76,7 +77,9 @@ map and server, final round, start/end times, duration when the start was observ
player counts, and the full saved roster. Players missing from the final report
retain their earlier counters; players with no stats display dashes. Counters and
score are the highest reported values within the match, not necessarily its final
scoreboard. Active and interrupted matches remain listed without results links.
scoreboard. Active, interrupted, and **End report missing** matches remain listed
without results links. Matches can filter each status and show known gaps in the
reporter's sequence; a delayed report can reduce that missing-report count.
Match pages remain available after the original server is removed.
### Player identity
@@ -126,8 +129,9 @@ rate also requires 100 kills; kills per down requires five downs. The board show
the numerator, denominator and game count beside each ratio. Ratios are computed
from aggregate counters, not an average of individual match percentages.
Completion rate is the share of those ended games whose final report contained
the player; it is not a win rate. Active and interrupted games do not enter any
denominator. Zero-denominator ratios remain unknown and are not ranked.
the player; it is not a win rate. Games without a received `game_end`, including
**End report missing** games, do not enter any denominator. Zero-denominator ratios
remain unknown and are not ranked.
Qualification is applied before ranking, search and pagination. Summary cards
still describe all players in the game/map selection, including those below the
@@ -149,12 +153,29 @@ They persist across app restarts and server deletion, and are not subject to
campaign history retention. The latest scoreboard and leaderboard update in the
same transaction. Each match keeps player counters even after someone leaves.
A `game_start` opens a match; `game_end` completes it. A changed map, a lower round,
or a round report after a completed game can also establish a new match when the
start was missed. Superseded active matches remain in history as interrupted.
Repeated starts before any stats belong to the same match. Identical consecutive
payloads (ignoring receipt time and roster order) do not add records. Repeated
snapshots in a match cannot increase counters or rounds twice.
A `game_start` opens a match; `game_end` completes it. Reporters can send an optional
`match` key and per-game `seq` on every event; see the
[reporter protocol](beta-test-plans.md#event-fields) for field limits and retry rules.
A key identifies one game on one server, including when the start report was
missed. An unseen key opens a new game and labels the previous active keyed game
**End report missing**. This records missing evidence; it does not establish
whether the game finished or was interrupted. A late `game_end` can still complete its own match; old keys never
replace the newer game's live state.
With both fields present, each sequence is counted once, including across app
restarts. Lower sequences fill missing-report gaps without rolling back newer
rounds, counters, or rosters. A delayed `game_start` may fill the recorded starting
roster without resetting progression. The history API exposes the highest
sequence minus the count of unique received sequences as `missingReports`, or
`null` when sequencing is unavailable. A missing report count does not grant
credit for unobserved rounds or complete a game without `game_end`.
Without a match key, the legacy heuristics remain: a changed map, a lower round,
or a round report after a completed game can establish a new match when the
start was missed. Superseded active matches remain interrupted. Repeated starts
before any stats belong to the same match. Identical consecutive payloads
(ignoring receipt time and roster order) do not add records. Repeated snapshots
in a match cannot increase counters or rounds twice.
The first upgrade imports the latest stored full scoreboard from each server
once. That provides the available counters and highest round, but not discarded
@@ -167,14 +188,13 @@ explicitly identified instead of being labeled as a known selection. A match
retains the game/map identity recorded at ingestion even if the server is later
reconfigured or removed.
The current protocol sends events in gameplay order and has no match ID, event
ID, or source sequence. A late report from an older game cannot be reliably
distinguished from a new game. Likewise, identical starts before any stats are
indistinguishable from retries. Send reports once in order. Within a match,
counter decreases use the previous high-water mark; if a reconnect resets a
player's counters, subsequent activity may be undercounted until those counters
pass their earlier maxima. Exact replay handling and counter-reset accounting
would require additional reporter identity/ordering information.
Legacy reports without match identity cannot reliably distinguish a late report
from an older game from a new game. Likewise, identical starts before any stats
are indistinguishable from retries. Send those reports once in order. Within a
match, counter decreases use the previous high-water mark; if a reconnect resets
a player's counters, subsequent activity may be undercounted until those counters
pass their earlier maxima. Match keys and sequence numbers do not identify
player counter resets.
## Read API
@@ -349,10 +369,15 @@ an identity rename can change results without changing those timestamps.
### Related routes and current limits
`GET /api/matches` and `GET /api/servers/{name}/matches` return JSON match
summaries as `{ "matches": [...], "total": number }`. These routes still require
session authentication. Both accept only `limit` (default 25, range 1–10,000); there is
no offset pagination. Results are newest first by first observation, then match
ID, and include active/interrupted matches that reached round 2. Their
summaries as `{ "matches": [...], "total": number, "page": number, "limit": number, "maps": [...] }`.
These routes still require session authentication. Both accept `limit` (default
25, range 1–10,000), `page`, `game`, `map`, `q` (server name), `days` (0, 7, 30, or
90), `status`, `sort` (`started`, `round`, `duration`, or `players`), and `direction`
(`asc` or `desc`). Results default to newest first by first observation, then
match ID, and include unfinished matches that reached round 2. Status values are
`active`, `completed`, `interrupted`, and `end-report-missing`; omit the filter for
all statuses. `reporterMatchKey` is the supplied match key or `null`;
`missingReports` is the known sequence-gap count or `null`. Their
[ServerMatch fields](../../src/lib/match-history.ts) differ from leaderboard entries
and do not include player counters or a roster. Unknown server names return an
empty list. URL-encode server names in paths.