110 lines
3.4 KiB
TypeScript
110 lines
3.4 KiB
TypeScript
import { DEFAULT_TIME_ZONE, validTimeZone } from './time-zone.js';
|
|
|
|
export type MetricsDays = 1 | 7 | 30;
|
|
|
|
export function parseMetricsDays(value: string | null | undefined): MetricsDays {
|
|
const days = Number(value ?? 7);
|
|
return days === 1 || days === 7 || days === 30 ? days : 7;
|
|
}
|
|
|
|
export interface MetricsScope {
|
|
game: string | null;
|
|
map: string | null;
|
|
timeZone: string;
|
|
}
|
|
|
|
export interface MetricsQuery extends MetricsScope {
|
|
days: MetricsDays;
|
|
}
|
|
|
|
/** A map is an exact recorded label within a game, never a cross-game match. */
|
|
export function parseMetricsQuery(params: URLSearchParams): MetricsQuery {
|
|
const requestedGame = params.get('game')?.trim() ?? '';
|
|
const game = /^[a-z0-9_-]{1,32}$/.test(requestedGame) ? requestedGame : null;
|
|
const requestedMap = params.get('map');
|
|
const map = game && requestedMap && requestedMap.trim() && requestedMap.length <= 200 && !/[\u0000-\u001f\u007f]/.test(requestedMap)
|
|
? requestedMap : null;
|
|
const requestedZone = params.get('timeZone');
|
|
return { days: parseMetricsDays(params.get('days')), game, map,
|
|
timeZone: validTimeZone(requestedZone) ? requestedZone : DEFAULT_TIME_ZONE };
|
|
}
|
|
|
|
/** All durations are hours. Observed is known server time, not collection coverage. */
|
|
export interface Utilization {
|
|
observed: number;
|
|
active: number;
|
|
idle: number;
|
|
unknown: number;
|
|
playerHours: number;
|
|
}
|
|
|
|
export interface ServerMetrics extends Utilization {
|
|
server: string;
|
|
/** A current registration with this name exists; historical identity may differ. */
|
|
currentRegistered: boolean;
|
|
}
|
|
|
|
export interface MapMetrics extends Utilization {
|
|
game: string;
|
|
map: string;
|
|
/** Map wall time, not summed server time. */
|
|
full: number;
|
|
capacityObserved: number;
|
|
}
|
|
|
|
export interface MetricPoint extends Utilization {
|
|
/** UTC bucket boundary, including when the selected interval starts later. */
|
|
at: number;
|
|
startAt: number;
|
|
endAt: number;
|
|
partial: boolean;
|
|
}
|
|
|
|
export interface PreviousMetrics {
|
|
since: number;
|
|
until: number;
|
|
summary: Utilization;
|
|
firstSampleAt: number | null;
|
|
lastSampleAt: number | null;
|
|
/** Retained scope evidence starts late or is absent, not a completeness guarantee. */
|
|
partialHistory: boolean;
|
|
}
|
|
|
|
export interface HeatmapCell extends Utilization {
|
|
/** Monday = 0, Sunday = 6. */
|
|
weekday: number;
|
|
hour: number;
|
|
/** Distinct local dates with any captured time, including unknown status. */
|
|
distinctDates: number;
|
|
}
|
|
|
|
export interface PooledPressure {
|
|
/** Share of known map-hours with every tracked lobby full, from 0 to 100. */
|
|
value: number | null;
|
|
fullMapHours: number;
|
|
knownMapHours: number;
|
|
coveredMaps: number;
|
|
totalMaps: number;
|
|
}
|
|
|
|
export interface MetricsData {
|
|
days: MetricsDays;
|
|
since: number;
|
|
now: number;
|
|
/** Retained sample timestamps describe collection, not gameplay freshness. */
|
|
firstRetainedSampleAt: number | null;
|
|
latestSampleAt: number | null;
|
|
registeredServerCount: number;
|
|
scope: MetricsScope;
|
|
scopeOptions: { games: string[]; maps: { game: string; map: string }[] };
|
|
scopeFirstSampleAt: number | null;
|
|
scopeLatestSampleAt: number | null;
|
|
summary: Utilization;
|
|
servers: ServerMetrics[];
|
|
maps: MapMetrics[];
|
|
points: MetricPoint[];
|
|
previous: PreviousMetrics;
|
|
heatmap: { timeZone: string; cells: HeatmapCell[] };
|
|
pooledPressure: PooledPressure;
|
|
}
|