Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion hasura/metadata/actions.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1185,15 +1185,16 @@ type TelemetryFleetTotals {
servers: Int!
dedicatedServers: Int!
publicServers: Int!
serverCapacity: Int!
matches: Int!
matchesWeek: Int!
matchesMonth: Int!
matchesYear: Int!
matchesImported: Int!
matchesImportedMonth: Int!
mapsPlayed: Int!
playersKnown: Int!
playersRegistered: Int!
playersPlayed: Int!
playersActive30d: Int!
teams: Int!
}
Expand Down
49 changes: 33 additions & 16 deletions src/telemetry/telemetry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ export class TelemetryService {
return (name: string) => (alias ? `${alias}.${name}` : name);
}

// populate_game_servers materializes a `servers` row for every port pair in
// a node's range the moment the node is enabled, so a node with a 216 port
// range adds 108 rows nobody provisioned. Counting raw rows reports a fleet
// of thousands of servers that do not exist.
private static readonly RealServer = `(game_server_node_id IS NULL OR is_dedicated)`;

private static activeLineups(interval: string) {
const recent = `${TelemetryService.nativeMatch("m")}
AND m.effective_at >= now() - interval '${interval}'`;
Expand Down Expand Up @@ -187,9 +193,7 @@ export class TelemetryService {
total: counts.servers_total,
enabled: counts.servers_enabled,
dedicated: counts.servers_dedicated,
on_demand: counts.servers_on_demand,
public: counts.servers_public,
capacity: counts.servers_capacity,
},
matches: {
total: counts.matches_ran,
Expand All @@ -211,7 +215,9 @@ export class TelemetryService {
},
},
players: {
known: counts.players_known,
registered: counts.players_registered,
played: counts.players_played,
active_7d: counts.players_active_7d,
active_30d: counts.players_active_30d,
teams: counts.teams_total,
Expand Down Expand Up @@ -324,15 +330,16 @@ export class TelemetryService {
coalesce(sum((payload->'servers'->>'total')::numeric), 0) AS servers,
coalesce(sum((payload->'servers'->>'dedicated')::numeric), 0) AS "dedicatedServers",
coalesce(sum((payload->'servers'->>'public')::numeric), 0) AS "publicServers",
coalesce(sum((payload->'servers'->>'capacity')::numeric), 0) AS "serverCapacity",
coalesce(sum((payload->'matches'->>'total')::numeric), 0) AS matches,
coalesce(sum((payload->'matches'->>'week')::numeric), 0) AS "matchesWeek",
coalesce(sum((payload->'matches'->>'month')::numeric), 0) AS "matchesMonth",
coalesce(sum((payload->'matches'->>'year')::numeric), 0) AS "matchesYear",
coalesce(sum((payload->'matches'->'external'->>'total')::numeric), 0) AS "matchesImported",
coalesce(sum((payload->'matches'->'external'->>'month')::numeric), 0) AS "matchesImportedMonth",
coalesce(sum((payload->'matches'->>'maps_played')::numeric), 0) AS "mapsPlayed",
coalesce(sum((payload->'players'->>'known')::numeric), 0) AS "playersKnown",
coalesce(sum((payload->'players'->>'registered')::numeric), 0) AS "playersRegistered",
coalesce(sum((payload->'players'->>'played')::numeric), 0) AS "playersPlayed",
coalesce(sum((payload->'players'->>'active_30d')::numeric), 0) AS "playersActive30d",
coalesce(sum((payload->'players'->>'teams')::numeric), 0) AS teams
FROM public.telemetry_installs
Expand All @@ -346,15 +353,16 @@ export class TelemetryService {
"servers",
"dedicatedServers",
"publicServers",
"serverCapacity",
"matches",
"matchesWeek",
"matchesMonth",
"matchesYear",
"matchesImported",
"matchesImportedMonth",
"mapsPlayed",
"playersKnown",
"playersRegistered",
"playersPlayed",
"playersActive30d",
"teams",
]);
Expand Down Expand Up @@ -543,9 +551,7 @@ export class TelemetryService {
total: int(servers.total),
enabled: int(servers.enabled),
dedicated: int(servers.dedicated),
on_demand: int(servers.on_demand),
public: int(servers.public),
capacity: int(servers.capacity),
},
matches: {
total: int(matches.total),
Expand All @@ -567,7 +573,9 @@ export class TelemetryService {
},
},
players: {
known: int(players.known),
registered: int(players.registered),
played: int(players.played),
active_7d: int(players.active_7d),
active_30d: int(players.active_30d),
teams: int(players.teams),
Expand Down Expand Up @@ -780,9 +788,9 @@ export class TelemetryService {
}

private async getSettings(): Promise<Map<string, string>> {
const rows = await this.collectQuery<Array<{ name: string; value: string }>>(
`SELECT name, value FROM public.settings`,
);
const rows = await this.collectQuery<
Array<{ name: string; value: string }>
>(`SELECT name, value FROM public.settings`);

return new Map(rows.map(({ name, value }) => [name, value]));
}
Expand All @@ -797,7 +805,9 @@ export class TelemetryService {
}

private async getMatchesByType(): Promise<Record<string, number>> {
const rows = await this.collectQuery<Array<{ type: string; count: string }>>(
const rows = await this.collectQuery<
Array<{ type: string; count: string }>
>(
`SELECT o.type, count(*) AS count
FROM public.matches m
JOIN public.match_options o ON o.id = m.match_options_id
Expand Down Expand Up @@ -860,13 +870,13 @@ export class TelemetryService {
(SELECT count(*) FROM public.game_server_nodes
WHERE gpu AND gpu_streaming_enabled) AS gpu_stream_nodes,

(SELECT count(*) FROM public.servers) AS servers_total,
(SELECT count(*) FROM public.servers WHERE enabled) AS servers_enabled,
(SELECT count(*) FROM public.servers
WHERE ${TelemetryService.RealServer}) AS servers_total,
(SELECT count(*) FROM public.servers
WHERE ${TelemetryService.RealServer} AND enabled) AS servers_enabled,
(SELECT count(*) FROM public.servers WHERE is_dedicated) AS servers_dedicated,
(SELECT count(*) FROM public.servers
WHERE game_server_node_id IS NOT NULL) AS servers_on_demand,
(SELECT count(*) FROM public.servers WHERE type <> 'Ranked') AS servers_public,
(SELECT coalesce(sum(max_players), 0) FROM public.servers) AS servers_capacity,
WHERE ${TelemetryService.RealServer} AND type <> 'Ranked') AS servers_public,

(SELECT count(*) FROM public.matches) AS matches_created,
(SELECT count(*) FROM public.matches WHERE ${TelemetryService.nativeMatch()}) AS matches_ran,
Expand Down Expand Up @@ -906,7 +916,14 @@ export class TelemetryService {
(SELECT count(DISTINCT r.match_id) FROM public.team_scrim_requests r
WHERE r.match_id IS NOT NULL) AS matches_scrim,

(SELECT count(*) FROM public.players) AS players_registered,
-- Connect events, lineup syncs, demo imports and sanctions all create a
-- players row, so the table is mostly steam ids that never signed in.
-- last_sign_in_at is only ever written by the Steam login callback.
(SELECT count(*) FROM public.players) AS players_known,
(SELECT count(*) FROM public.players WHERE last_sign_in_at IS NOT NULL) AS players_registered,
-- Stats land per map from live round events and from parsed demos, so
-- this is everyone who played rather than everyone who was rostered.
(SELECT count(DISTINCT steam_id) FROM public.player_match_map_stats) AS players_played,
(SELECT count(*) FROM public.teams) AS teams_total,
-- An OR across both lineup columns cannot use an index and forces a
-- join over every lineup row. Feeding the two sides in separately lets
Expand Down
14 changes: 11 additions & 3 deletions src/telemetry/types/TelemetryPayload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const TELEMETRY_SCHEMA_VERSION = 1;
export const TELEMETRY_SCHEMA_VERSION = 2;

export type TelemetryFeature = {
enabled: boolean | null;
Expand All @@ -18,13 +18,15 @@ export type TelemetryPayload = {
regions: number;
gpu: number;
};
// A `servers` row is not always a server. Enabling a game server node
// pre-provisions one row per port pair in its range, so a single node adds
// ~100 rows that are slots waiting on a match rather than machines. None of
// these count them.
servers: {
total: number;
enabled: number;
dedicated: number;
on_demand: number;
public: number;
capacity: number;
};
// Everything outside `external` counts only matches this panel actually ran.
// An imported demo is stamped with a started_at, so without the split it
Expand All @@ -48,8 +50,14 @@ export type TelemetryPayload = {
year: number;
};
};
// `known` is every steam id the panel holds a row for, most of which never
// belonged to a person who signed in — connect events, lineup syncs, demo
// imports and sanctions all create players. `registered` is the subset that
// has signed in at least once.
players: {
known: number;
registered: number;
played: number;
active_7d: number;
active_30d: number;
teams: number;
Expand Down
77 changes: 65 additions & 12 deletions test/telemetry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ describe("telemetry (SQL-driven)", () => {
beforeAll(async () => {
await fx.region("TelemetryRegion");

// Enabling a node pre-provisions a `servers` row per port pair in its
// range, so this seeds five rows that are slots, not servers.
await postgres.query(
`INSERT INTO game_server_nodes
(id, public_ip, start_port_range, end_port_range, region, status, enabled, label)
VALUES ('telemetry-node', '203.0.113.1', 27015, 27025, 'TelemetryRegion', 'Online', true, 'telemetry-node')`,
);

const ran = await fx.bareMatch(new Date().toISOString());
await postgres.query(
"UPDATE matches SET started_at = now() WHERE id = $1",
Expand All @@ -82,6 +90,23 @@ describe("telemetry (SQL-driven)", () => {
[reimported.matchId, "5stack-1"],
);

// Only the first of these ever signed in; the other two are the rows a
// panel creates for a steam id it saw in a lineup or a demo.
const signedIn = await fx.player("signed-in");
const ghost = await fx.player("ghost");
await fx.player("never-seen-again");

await postgres.query(
"UPDATE players SET last_sign_in_at = now() WHERE steam_id = $1",
[signedIn],
);

await postgres.query(
`INSERT INTO player_match_map_stats (steam_id, match_map_id, match_id, kills)
VALUES ($1, $3, $4, 10), ($2, $3, $4, 4)`,
[signedIn, ghost, ran.mapId, ran.matchId],
);

payload = await service.collect();
}, 600_000);

Expand Down Expand Up @@ -110,9 +135,25 @@ describe("telemetry (SQL-driven)", () => {
expect(payload.matches.by_source.faceit).toBe(1);
});

it("reports the servers seeded by the region fixture", () => {
expect(payload.servers.total).toBeGreaterThanOrEqual(1);
expect(payload.servers.dedicated).toBeGreaterThanOrEqual(1);
it("keeps a node's pre-provisioned port slots out of the server count", async () => {
const [rows] = await postgres.query<Array<{ count: string }>>(
"SELECT count(*) FROM servers",
);

// Six rows: the region fixture's dedicated server plus the node's five
// port slots. Only the dedicated one is a server anybody runs.
expect(Number(rows.count)).toBe(6);
expect(payload.servers.total).toBe(1);
expect(payload.servers.dedicated).toBe(1);
});

it("counts only players who have signed in as registered", () => {
expect(payload.players.known).toBe(3);
expect(payload.players.registered).toBe(1);
});

it("counts players with stats on at least one map as having played", () => {
expect(payload.players.played).toBe(2);
});

it("reports every feature with an enabled flag or a usage count", () => {
Expand Down Expand Up @@ -164,7 +205,7 @@ describe("telemetry (SQL-driven)", () => {
const install = "11111111-2222-3333-4444-555555555555";

const report = (over: Record<string, any> = {}) => ({
schema: 1,
schema: 2,
install_id: install,
installed_at: "2024-01-01T00:00:00.000Z",
panel_version: "deadbeef",
Expand All @@ -174,9 +215,7 @@ describe("telemetry (SQL-driven)", () => {
total: 10,
enabled: 9,
dedicated: 3,
on_demand: 7,
public: 4,
capacity: 120,
},
matches: {
total: 500,
Expand All @@ -192,7 +231,14 @@ describe("telemetry (SQL-driven)", () => {
scrim: 25,
external: { total: 30, week: 1, month: 3, year: 12 },
},
players: { registered: 300, active_7d: 40, active_30d: 90, teams: 22 },
players: {
known: 900,
registered: 300,
played: 210,
active_7d: 40,
active_30d: 90,
teams: 22,
},
features: {
events: { enabled: true, count: 4 },
news: { enabled: false, count: 0 },
Expand Down Expand Up @@ -304,7 +350,7 @@ describe("telemetry (SQL-driven)", () => {
const installB = "bbbbbbbb-0000-4000-8000-000000000002";

const report = (installId: string, matches: number, servers: number) => ({
schema: 1,
schema: 2,
install_id: installId,
installed_at: "2024-01-01T00:00:00.000Z",
panel_version: "cafebabe",
Expand All @@ -314,9 +360,7 @@ describe("telemetry (SQL-driven)", () => {
total: servers,
enabled: servers,
dedicated: 1,
on_demand: servers - 1,
public: 2,
capacity: servers * 10,
},
matches: {
total: matches,
Expand All @@ -332,7 +376,14 @@ describe("telemetry (SQL-driven)", () => {
scrim: 2,
external: { total: matches / 10, week: 0, month: 2, year: 5 },
},
players: { registered: 50, active_7d: 5, active_30d: 12, teams: 3 },
players: {
known: 200,
registered: 50,
played: 35,
active_7d: 5,
active_30d: 12,
teams: 3,
},
features: {
events: { enabled: installId === installA, count: 3 },
highlights: { enabled: null as boolean | null, count: 10 },
Expand All @@ -351,8 +402,10 @@ describe("telemetry (SQL-driven)", () => {
expect(stats.installs.active24h).toBe(2);
expect(stats.totals.matches).toBe(350);
expect(stats.totals.servers).toBe(10);
expect(stats.totals.serverCapacity).toBe(100);
expect(stats.totals.mapsPlayed).toBe(700);
expect(stats.totals.playersKnown).toBe(400);
expect(stats.totals.playersRegistered).toBe(100);
expect(stats.totals.playersPlayed).toBe(70);
// Imported matches are summed apart from the ones the panels hosted.
expect(stats.totals.matchesImported).toBe(35);
expect(stats.totals.matchesImportedMonth).toBe(4);
Expand Down
Loading