Skip to content

refactor(server): extract the match telemetry recorder out of GameServer - #5145

Merged
evanpelle merged 1 commit into
mainfrom
gameserver-phase3-telemetry
Aug 27, 2026
Merged

refactor(server): extract the match telemetry recorder out of GameServer#5145
evanpelle merged 1 commit into
mainfrom
gameserver-phase3-telemetry

Conversation

@evanpelle

Copy link
Copy Markdown
Collaborator

Summary

Phase 3 of docs/GameServerRefactor.md, sixth and last module extraction (previous: #5114, #5116, #5117, #5122, #5124, #5126, #5142, #5143). A pure move with no behaviour change — the golden wire snapshot is untouched and MatchTelemetryIntegration.test.ts passes unmodified.

  • src/server/MatchTelemetryRecorder.ts (new): one match's view of the telemetry stream. emit builds the envelope (schemaVersion, matchId, per-match sequence, observedAt, serverTick); a throwing emitter counts as a drop and still consumes a sequence number, so gaps mark drops. intentObserved / takeTickCounts keep the per-tick counters turn_committed reports; noteArchiveAttempted and matchFinished (once, however many times end() runs) cover the end of the match. identityFor, which keeps persistentID out of telemetry, is an exported function.
  • GameServer: four fields and four private methods gone; every emit site passes turns.length explicitly where the old emitTelemetry default applied. Net −68 lines.
  • tests/server/MatchTelemetryRecorder.test.ts (new, 7 tests): envelope/sequence/tick, the sequence gap a drop leaves, per-tick counters split by verdict, zero counts and clearing on take, finished-once with the archive flag, identityFor never carrying the persistentID.

With this, Phase 3 is complete: six modules in six PRs, golden snapshot unchanged throughout. GameServer.ts is 1,944 lines (from 2,365) and test reach-ins are 36 (from ~150), none of them spies on private methods; what remains is lifecycle and ingress state, which Phases 5–6 own.

Test plan

  • npx vitest tests/server --run: 52 files / 522 tests green (was 51 / 516).
  • Golden snapshot (GameServerWire.test.ts.snap): unchanged.
  • Mutation check: removing the finished-once guard in matchFinished fails 1 test; restored.
  • Full npm test: 31 failing files / 379 tests — identical to the pristine origin/main baseline (the pre-existing localStorage client failures); +6 passing.
  • tsc --noEmit, prettier, oxlint, eslint clean.

🤖 Generated with Claude Code

Phase 3 of docs/GameServerRefactor.md, sixth and last module. One match's
view of the telemetry stream moves into src/server/MatchTelemetryRecorder.ts:
the event envelope and per-match sequence (emit, with a throwing emitter
counted as a drop), the per-tick intent counters the turn_committed event
reports (intentObserved, takeTickCounts), the archive-attempted flag, and
match_finished going out once however many times end() runs. identityFor,
which keeps persistentID out of telemetry, is an exported function.

GameServer passes turns.length explicitly where the old emitTelemetry
default applied. No behaviour change: the golden wire snapshot is untouched
and MatchTelemetryIntegration.test.ts passes unmodified.

Tests: MatchTelemetryRecorder.test.ts covers the envelope, the sequence gap a
drop leaves, the counters, and finished-once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5838ce5b-e1dc-49bf-88f9-36a6bd192ca5

📥 Commits

Reviewing files that changed from the base of the PR and between 6f82f99 and a31311a.

📒 Files selected for processing (4)
  • docs/GameServerRefactor.md
  • src/server/GameServer.ts
  • src/server/MatchTelemetryRecorder.ts
  • tests/server/MatchTelemetryRecorder.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


Walkthrough

GameServer telemetry handling now uses a dedicated MatchTelemetryRecorder. The recorder owns event envelopes, sequencing, intent counters, identity mapping, archive tracking, and one-time completion. Unit tests cover the new behavior, and the refactor documentation marks Phase 3 complete.

Changes

Telemetry recorder extraction

Layer / File(s) Summary
Recorder contracts and state
src/server/MatchTelemetryRecorder.ts, tests/server/MatchTelemetryRecorder.test.ts
Adds typed telemetry identity and tick-count handling. The recorder stamps events, tracks dropped emissions, counts intents, records archive attempts, and emits match_finished once.
GameServer telemetry integration
src/server/GameServer.ts, docs/GameServerRefactor.md
Initializes the recorder and delegates all existing telemetry events and lifecycle updates to it. The refactor checklist records Phase 3 completion.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to a3131

This refactor moves telemetry state into a dedicated recorder while preserving sequencing, drop handling, identity minimization, and completion behavior; no actionable merge-blocking risk remains at the current head after normal checks and review.

Suggested reviewers: celant

Poem

Events find a focused home,
Ticks count softly as they roam,
Dropped signals leave a trace,
Finished matches close with grace,
Clean TypeScript keeps the pace.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: extracting the match telemetry recorder from GameServer.
Description check ✅ Passed The description directly explains the MatchTelemetryRecorder extraction, the GameServer changes, added tests, and validation results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Verdict: No issues found — clean, faithful mechanical refactor. 0 findings (0 critical, 0 major, 0 minor).

This PR extracts GameServer's match-telemetry bookkeeping (event envelope/sequence stamping, per-tick intent counters, the finished-once guard, identityFor) into a new src/server/MatchTelemetryRecorder.ts class. I traced every call site against the pre-PR code on main:

  • All emit sites that previously relied on emitTelemetry's default serverTick: number = this.turns.length parameter (match_opened, player_joined, match_started, match_finished) now pass this.turns.length explicitly at the same point in execution — values are unchanged.
  • turn_committed and intent_observed sites already passed serverTick explicitly and are unchanged.
  • matchFinished(totalTurns) reuses one argument for both payload.totalTurns and the envelope's serverTick; in the old code these were two separate reads of this.turns.length at the same call, so the emitted values are identical.
  • The finished-once guard, the throw→"dropped" fallback (which still consumes a sequence number, preserving gap-as-drop semantics), the archive-attempted flag, and the per-tick counter get-or-zero/delete pair all carry over verbatim.
  • identityFor still excludes persistentID from telemetry — no new leak.
  • No dangling references to the removed GameServer fields/methods or the removed type imports; the new test file's helpers (makeClient, cid) and mock (RecordingEmitter) match the real APIs.

No CLAUDE.md violations: no src/core changes (test-required rule N/A), no user-visible strings (i18n rule N/A), and the new server unit test follows the same tests/server/ convention (GameServerHarness + hand-written mocks) used throughout that directory.

Four independent review passes (2× CLAUDE.md compliance, 2× bug-focused) each returned zero findings.

@evanpelle evanpelle added this to the v34 milestone Aug 27, 2026
@evanpelle
evanpelle merged commit 21e9574 into main Aug 27, 2026
17 of 18 checks passed
@evanpelle
evanpelle deleted the gameserver-phase3-telemetry branch August 27, 2026 19:14
@github-project-automation github-project-automation Bot moved this from Triage to Complete in OpenFront Release Management Aug 27, 2026
evanpelle added a commit that referenced this pull request Aug 27, 2026
## Summary

Phase 4 of `docs/GameServerRefactor.md` (the plan checked in by #5114;
Phases 0–3 are merged, #5145 was the last Phase 3 module).

- **`src/server/Roster.ts`** (new): the seven parallel client
collections in `GameServer` — `activeClients`, `allClients`,
`websockets`, `persistentIdToClientId`, `admittedPersistentIds`,
`kickedPersistentIds`, `clientsDisconnectedStatus` — become one object.
Bookkeeping: `add`, `reconnect` (also swaps in the new socket and closes
the old one, which `rejoinClient` did by hand), `markLeft`,
`forgetReconnect` (the lobby-phase seat release), `kick`, `pruneStale`,
`closeAll`. Reads: `active()` (readonly, join order kept —
`gameStartInfo.players` depends on it), `players()`, `all()`, `get()`,
`byPersistentId()`, `isKicked`, `wasAdmitted`,
`isDisconnected`/`setDisconnected`, `votingUniqueIPs` (moved with its
spectator-electorate comment).
- **`GameServer.ts`** keeps every policy decision — allowlist, IP cap,
duplicate sessions, host-left, the kicked checks on join and reconnect —
and delegates the bookkeeping through `this.clients` (`roster()` is
already a public method, hence the field name). `byPersistentId()` is
deliberately raw so `lobbyCreatorID` resolves exactly as before; the
kicked exclusion stays in `getClientIdForPersistentId`. 1,944 → 1,888
lines.
- `activeClients` is no longer public; `GameManager.activeClients()`
sums `numClients()`.
- **Tests:** `Roster.test.ts` (9 tests) covers the bookkeeping on its
own. The seven roster reach-ins in `GameServerRejoin.test.ts` and
`AdminBotIntent.test.ts` are replaced by real joins and observable
outcomes (`wasAdmitted(...)` false, a re-join returning `"kicked"`).
`(game as any)` reach-ins 36 → 29; what remains is lifecycle/ingress
state for Phases 5–6.

Pure move, no behaviour change: the golden wire transcript
(`GameServerWire.test.ts.snap`) is byte-identical.

## Test plan

- `npx vitest tests/server --run`: 53 files / 531 tests pass (was 52 /
522); snapshot unchanged
- `npx tsc --noEmit`, `npm run lint` (oxlint + eslint), `prettier
--check`: clean
- Mutation spot-check: dropping the kicked exclusion from
`Roster.wasAdmitted` fails 4 tests across 4 files (`Roster`,
`AdminBotIntent`, `GameServerJoin`, `GameServerRejoin`), restored
- Full `npm test`: 31 failed files / 379 failed tests — identical to
pristine `origin/main` (pre-existing client `localStorage` failures)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant