Simplify telemetry to three game events
This commit is contained in:
@@ -1,108 +1,83 @@
|
||||
import type { BetaPlan, RequirementProgress } from '../../beta-plan.js';
|
||||
import type { GameEvent } from '../../telemetry.js';
|
||||
|
||||
export interface SessionEvidence {
|
||||
started: number;
|
||||
sequence: number;
|
||||
export interface MatchEvidence {
|
||||
map: string;
|
||||
started?: number;
|
||||
lastEvent: number;
|
||||
players: number;
|
||||
ended: boolean;
|
||||
rounds: Record<string, number>;
|
||||
peakPlayers: number;
|
||||
}
|
||||
export interface TestMetrics {
|
||||
values: Record<string, number>;
|
||||
sessions: Record<string, SessionEvidence>;
|
||||
match?: MatchEvidence;
|
||||
manual: Record<string, string>;
|
||||
bootId?: string;
|
||||
boots: string[];
|
||||
restarts: number;
|
||||
lastSample?: number;
|
||||
wasHealthy?: boolean;
|
||||
lastPlayers?: number;
|
||||
outages: number;
|
||||
logErrors: number;
|
||||
logAvailable?: boolean;
|
||||
streaks: Record<string, number>;
|
||||
}
|
||||
export const emptyMetrics = (): TestMetrics => ({ values: {}, sessions: {}, manual: {}, boots: [], restarts: 0, outages: 0, logErrors: 0, streaks: {} });
|
||||
export const emptyMetrics = (): TestMetrics => ({ values: {}, manual: {}, outages: 0, streaks: {} });
|
||||
|
||||
export interface TestSignal {
|
||||
event: 'boot' | 'session_start' | 'round_change' | 'match_end' | 'feature';
|
||||
bootId: string;
|
||||
sessionId: string;
|
||||
sequence: number;
|
||||
round: number;
|
||||
players: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** Events are accepted once, in session order; a match must start in this run. */
|
||||
export function applySignal(plan: BetaPlan, metrics: TestMetrics, signal: TestSignal, now: number): boolean {
|
||||
if (signal.event === 'boot') {
|
||||
if (metrics.boots.includes(signal.bootId)) return false;
|
||||
if (metrics.bootId) metrics.restarts++;
|
||||
metrics.boots.push(signal.bootId);
|
||||
metrics.bootId = signal.bootId;
|
||||
return true;
|
||||
/** Gameplay comes only from the script. No reporter IDs or beta handshake. */
|
||||
export function applyGameEvent(plan: BetaPlan, metrics: TestMetrics, event: GameEvent, now: number): void {
|
||||
const players = event.players.length;
|
||||
if (event.event === 'game_start' || !metrics.match || metrics.match.map !== event.map) {
|
||||
metrics.match = { map: event.map, started: event.event === 'game_start' ? now : undefined,
|
||||
lastEvent: now, players, ended: false, rounds: {} };
|
||||
metrics.streaks = {};
|
||||
}
|
||||
if (metrics.bootId !== signal.bootId) return false;
|
||||
const key = `${signal.bootId}:${signal.sessionId}`;
|
||||
if (signal.event === 'session_start') {
|
||||
if (metrics.sessions[key]) return false;
|
||||
metrics.sessions[key] = { started: now, sequence: signal.sequence, ended: false, rounds: {}, peakPlayers: signal.players };
|
||||
return true;
|
||||
}
|
||||
const session = metrics.sessions[key];
|
||||
if (!session || session.ended || signal.sequence <= session.sequence) return false;
|
||||
session.sequence = signal.sequence;
|
||||
session.peakPlayers = Math.max(session.peakPlayers, signal.players);
|
||||
const match = metrics.match;
|
||||
if (match.ended) return;
|
||||
const minutes = Math.max(0, now - match.lastEvent) / 60_000;
|
||||
for (const r of plan.requirements) {
|
||||
const current = metrics.values[r.id] ?? 0;
|
||||
if (signal.event === 'round_change' && signal.players >= r.minPlayers) {
|
||||
session.rounds[r.id] = Math.max(session.rounds[r.id] ?? 0, signal.round);
|
||||
if (r.kind === 'round') metrics.values[r.id] = Math.max(current, signal.round);
|
||||
const populated = players >= Math.max(1, r.minPlayers) && match.players >= Math.max(1, r.minPlayers);
|
||||
if (r.kind === 'population') metrics.values[r.id] = Math.max(current, players);
|
||||
if (r.kind === 'populated-minutes' && populated) metrics.values[r.id] = current + minutes;
|
||||
if (r.kind === 'player-hours' && populated) metrics.values[r.id] = current + minutes * Math.min(players, match.players) / 60;
|
||||
if (r.kind === 'sustained-population') {
|
||||
metrics.streaks[r.id] = populated ? (metrics.streaks[r.id] ?? 0) + minutes : 0;
|
||||
metrics.values[r.id] = Math.max(current, metrics.streaks[r.id]);
|
||||
}
|
||||
if (event.event !== 'game_start' && players >= Math.max(1, r.minPlayers)) {
|
||||
match.rounds[r.id] = Math.max(match.rounds[r.id] ?? 0, event.round);
|
||||
if (r.kind === 'round') metrics.values[r.id] = Math.max(current, event.round);
|
||||
}
|
||||
if (event.event === 'game_end' && r.kind === 'matches'
|
||||
&& (match.rounds[r.id] ?? -1) >= r.minRound
|
||||
&& (r.minMinutes === 0 || (match.started !== undefined && now - match.started >= r.minMinutes * 60_000))) {
|
||||
metrics.values[r.id] = current + 1;
|
||||
}
|
||||
if (signal.event === 'match_end' && r.kind === 'matches' && (r.minRound > 0 || r.minPlayers > 1)
|
||||
&& (session.rounds[r.id] ?? 0) >= r.minRound && session.peakPlayers >= r.minPlayers
|
||||
&& now - session.started >= r.minMinutes * 60_000) metrics.values[r.id] = current + 1;
|
||||
if (signal.event === 'feature' && r.kind === 'feature' && signal.name === r.event
|
||||
&& signal.players >= r.minPlayers && signal.round >= r.minRound) metrics.values[r.id] = current + 1;
|
||||
}
|
||||
if (signal.event === 'match_end') session.ended = true;
|
||||
return true;
|
||||
match.lastEvent = now;
|
||||
match.players = players;
|
||||
match.ended = event.event === 'game_end';
|
||||
}
|
||||
|
||||
/** Sample both ends of an interval. Monitoring gaps never earn playtime. */
|
||||
export function applySample(plan: BetaPlan, m: TestMetrics, at: number, healthy: boolean, players: number): void {
|
||||
/** Infrastructure health is separate from script-reported gameplay. */
|
||||
export function applySample(plan: BetaPlan, m: TestMetrics, at: number, healthy: boolean): void {
|
||||
if (m.lastSample !== undefined && at <= m.lastSample) return;
|
||||
const gap = m.lastSample === undefined ? Infinity : at - m.lastSample;
|
||||
const contiguous = gap <= 60_000;
|
||||
const minutes = healthy && m.wasHealthy && contiguous ? gap / 60_000 : 0;
|
||||
if (!healthy && m.wasHealthy === true) m.outages++;
|
||||
for (const r of plan.requirements) {
|
||||
const value = m.values[r.id] ?? 0;
|
||||
const populated = players >= Math.max(1, r.minPlayers) && (m.lastPlayers ?? 0) >= Math.max(1, r.minPlayers);
|
||||
if (r.kind === 'populated-minutes' && populated) m.values[r.id] = value + minutes;
|
||||
if (r.kind === 'player-hours' && populated) m.values[r.id] = value + minutes * Math.min(players, m.lastPlayers ?? 0) / 60;
|
||||
if (r.kind === 'population' && healthy) m.values[r.id] = Math.max(value, players);
|
||||
if (r.kind === 'healthy-minutes') m.values[r.id] = healthy && contiguous && m.wasHealthy ? value + minutes : 0;
|
||||
if (r.kind === 'sustained-population') {
|
||||
m.streaks[r.id] = healthy && populated && contiguous ? (m.streaks[r.id] ?? 0) + minutes : 0;
|
||||
m.values[r.id] = Math.max(value, m.streaks[r.id]);
|
||||
}
|
||||
if (r.kind === 'healthy-minutes') m.values[r.id] = healthy && contiguous && m.wasHealthy ? (m.values[r.id] ?? 0) + minutes : 0;
|
||||
}
|
||||
m.lastSample = at; m.wasHealthy = healthy; m.lastPlayers = players;
|
||||
m.lastSample = at; m.wasHealthy = healthy;
|
||||
}
|
||||
|
||||
export function evaluateRequirements(plan: BetaPlan, metrics: TestMetrics, phase: number): RequirementProgress[] {
|
||||
return plan.requirements.map(r => {
|
||||
const budget = ['failure-budget', 'restart-budget', 'clean-logs'].includes(r.kind);
|
||||
const current = r.kind === 'failure-budget' ? metrics.outages : r.kind === 'clean-logs' ? metrics.logErrors
|
||||
: r.kind === 'restart-budget' ? metrics.restarts
|
||||
: r.kind === 'manual' ? Number(!!metrics.manual[r.id]) : r.kind === 'recovery' ? phase : metrics.values[r.id] ?? 0;
|
||||
const available = (r.kind !== 'clean-logs' || metrics.logAvailable === true) && (r.kind !== 'restart-budget' || !!metrics.bootId);
|
||||
return { id: r.id, label: r.label, current, target: r.target, passed: available && (budget ? current <= r.target : current >= r.target),
|
||||
const budget = r.kind === 'failure-budget';
|
||||
const current = budget ? metrics.outages : r.kind === 'manual' ? Number(!!metrics.manual[r.id])
|
||||
: r.kind === 'recovery' ? phase : metrics.values[r.id] ?? 0;
|
||||
return { id: r.id, label: r.label, current, target: r.target, passed: budget ? current <= r.target : current >= r.target,
|
||||
failed: budget && current > r.target,
|
||||
detail: r.kind === 'manual' ? metrics.manual[r.id] ?? 'Awaiting tester sign-off'
|
||||
: !available ? r.kind === 'restart-budget' ? 'Waiting for boot telemetry' : 'Waiting for a readable console log' : budget ? `${current} / ${r.target} allowed; monitored until promotion`
|
||||
: budget ? `${current} / ${r.target} allowed; monitored until promotion`
|
||||
: `${Math.round(current * 100) / 100} / ${r.target}` };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user