Files
electric-cherry/src/lib/metrics.ts
T

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;
}