Add unique and new player metrics (Refs #142)
CI / checks (push) Successful in 4m53s

This commit is contained in:
2026-10-10 18:47:56 -04:00
parent 7fcf1cded2
commit e3e740cea3
16 changed files with 1212 additions and 3 deletions
+71 -1
View File
@@ -7,6 +7,7 @@ import MetricsAutoscaling from '../../src/lib/ui/components/metrics/MetricsAutos
import MetricsTables from '../../src/lib/ui/components/metrics/MetricsTables.svelte';
import MetricsExplore from '../../src/lib/ui/components/metrics/MetricsExplore.svelte';
import MetricsChart from '../../src/lib/ui/components/metrics/MetricsChart.svelte';
import MetricsPlayers from '../../src/lib/ui/components/metrics/MetricsPlayers.svelte';
import { metricPage } from '../../src/lib/ui/metrics.js';
import { page } from '$app/state';
@@ -66,6 +67,7 @@ function report(): MetricsData {
scopeFirstSampleAt: now - 14 * 86400000,
scopeLatestSampleAt: now,
matches: matches(),
players: { uniquePlayers: 104, newPlayers: 70, historyStartAt: now - 14 * 86400000 },
autoscaling: {
added: 73,
removed: 48,
@@ -86,6 +88,9 @@ function report(): MetricsData {
startAt: now - 86400000,
endAt: now,
partial: true,
uniquePlayers: 28,
newPlayers: 5,
playerHistoryPartial: false,
capturedHours: 1
}
],
@@ -500,6 +505,68 @@ it('shows a global pause once while keeping Kino’s shortage ahead of quiet map
expect(body).toContain('Consider raising the policy maximum');
});
it('renders player totals separately from periods with accessible selection, gaps and partial history', () => {
const data = report();
const original = data.points[0];
data.points = [
{
...original,
at: original.at - 86400000,
startAt: original.startAt - 86400000,
endAt: original.startAt,
partial: false,
uniquePlayers: null,
newPlayers: null
},
{ ...original, partial: false, playerHistoryPartial: true }
];
data.players.historyStartAt = original.startAt + 3600000;
const body = render(MetricsPlayers, {
props: {
points: data.points,
players: data.players,
days: data.days,
since: data.since,
timeZone: data.scope.timeZone
}
}).body;
expect(body).toContain('Players over time');
expect(body).toContain('>104</dd>');
expect(body).toContain('>70</dd>');
expect(body).toContain('28 unique players · 5 new');
expect(body).toContain('Retained player history starts');
expect(body.replace(/\s+/g, ' ')).toContain('Earlier periods are gaps');
expect(body).toContain('Range totals count each player once');
expect(body).toContain('aria-label="Selected player period summary"');
expect(body).toContain('aria-label="Player time data" tabindex="0"');
expect(body).toContain('role="status"');
expect(body).toContain('View exact time data');
expect(body).toContain('No retained history');
const periods = body.match(/<button[^>]*class="period\b[\s\S]*?<\/button>/g) ?? [];
expect(periods).toHaveLength(2);
expect(periods[0]).not.toContain('class="dot');
expect(periods[1]).toContain('partial');
expect(periods[1]).toContain('tabindex="0"');
expect(body).not.toMatch(/<details[^>]*open/);
});
it('shows player trends without occupancy and shows unavailable counts when no match history remains', () => {
const data = report();
data.summary = { observed: 0, active: 0, idle: 0, unknown: 0, playerHours: 0 };
let body = render(MetricsPage, {
props: { data: { ...data, user: null, setupRequired: false } }
}).body;
expect(body).toContain('Players over time');
expect(body).toContain('>104</dd>');
data.players = { historyStartAt: null, uniquePlayers: null, newPlayers: null };
data.points = data.points.map((point) => ({ ...point, uniquePlayers: null, newPlayers: null }));
body = render(MetricsPage, {
props: { data: { ...data, user: null, setupRequired: false } }
}).body;
expect(body).toContain('No retained player history yet');
expect(body).toContain('>—</dd>');
});
it('keeps accessible chart selection, partial periods and gaps while collapsing inspection controls', () => {
const data = report();
data.points.push({
@@ -512,7 +579,10 @@ it('keeps accessible chart selection, partial periods and gaps while collapsing
idle: 0,
unknown: 0,
playerHours: 0,
capturedHours: 0
capturedHours: 0,
uniquePlayers: null,
newPlayers: null,
playerHistoryPartial: false
});
const body = render(MetricsChart, {
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }