Combine analytics and game types (#839)

## Description:

Combine analytics and game types. Simplify and remove redundant player
information.

- Remove ip address.
- Add playerID.
- Combine redundant player tables.
- Move game metadata in to GameEndInfo type, an extension of
GameStartInfo

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

---------

Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
Scott Anderson
2025-05-22 15:06:30 -04:00
committed by GitHub
parent 373de1a752
commit 9302af868d
14 changed files with 75 additions and 104 deletions
+18 -34
View File
@@ -1,6 +1,6 @@
import { S3 } from "@aws-sdk/client-s3";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameID, GameRecord } from "../core/Schemas";
import { AnalyticsRecord, GameID, GameRecord } from "../core/Schemas";
import { logger } from "./Logger";
const config = getServerConfigFromServer();
@@ -30,12 +30,12 @@ export async function archive(gameRecord: GameRecord) {
// Archive full game if there are turns
if (gameRecord.turns.length > 0) {
log.info(
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to R2`,
`${gameRecord.info.gameID}: game has more than zero turns, attempting to write to full game to R2`,
);
await archiveFullGameToR2(gameRecord);
}
} catch (error) {
log.error(`${gameRecord.id}: Final archive error: ${error}`, {
log.error(`${gameRecord.info.gameID}: Final archive error: ${error}`, {
message: error?.message || error,
stack: error?.stack,
name: error?.name,
@@ -46,29 +46,16 @@ export async function archive(gameRecord: GameRecord) {
async function archiveAnalyticsToR2(gameRecord: GameRecord) {
// Create analytics data object
const analyticsData = {
id: gameRecord.id,
env: config.env(),
start_time: new Date(gameRecord.startTimestampMS).toISOString(),
end_time: new Date(gameRecord.endTimestampMS).toISOString(),
duration_seconds: gameRecord.durationSeconds,
number_turns: gameRecord.num_turns,
game_mode: gameRecord.gameStartInfo.config.gameType,
winner: gameRecord.winner,
difficulty: gameRecord.gameStartInfo.config.difficulty,
mapType: gameRecord.gameStartInfo.config.gameMap,
players: gameRecord.players.map((p) => ({
username: p.username,
ip: p.ip,
persistentID: p.persistentID,
clientID: p.clientID,
stats: p.stats,
})),
const { info, version, gitCommit } = gameRecord;
const analyticsData: AnalyticsRecord = {
info,
version,
gitCommit,
};
try {
// Store analytics data using just the game ID as the key
const analyticsKey = `${gameRecord.id}.json`;
const analyticsKey = `${info.gameID}.json`;
await r2.putObject({
Bucket: bucket,
@@ -77,17 +64,14 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) {
ContentType: "application/json",
});
log.info(`${gameRecord.id}: successfully wrote game analytics to R2`);
log.info(`${info.gameID}: successfully wrote game analytics to R2`);
} catch (error) {
log.error(
`${gameRecord.id}: Error writing game analytics to R2: ${error}`,
{
message: error?.message || error,
stack: error?.stack,
name: error?.name,
...(error && typeof error === "object" ? error : {}),
},
);
log.error(`${info.gameID}: Error writing game analytics to R2: ${error}`, {
message: error?.message || error,
stack: error?.stack,
name: error?.name,
...(error && typeof error === "object" ? error : {}),
});
throw error;
}
}
@@ -110,11 +94,11 @@ async function archiveFullGameToR2(gameRecord: GameRecord) {
ContentType: "application/json",
});
} catch (error) {
log.error(`error saving game ${gameRecord.id}`);
log.error(`error saving game ${gameRecord.info.gameID}`);
throw error;
}
log.info(`${gameRecord.id}: game record successfully written to R2`);
log.info(`${gameRecord.info.gameID}: game record successfully written to R2`);
}
export async function readGameRecord(
+2 -2
View File
@@ -393,7 +393,7 @@ export class GameServer {
);
}
return {
ip: ipAnonymize(client.ip),
playerID: client.playerID,
clientID: client.clientID,
username: client.username,
persistentID: client.persistentID,
@@ -403,7 +403,7 @@ export class GameServer {
archive(
createGameRecord(
this.id,
this.gameStartInfo,
this.gameStartInfo.config,
playerRecords,
this.turns,
this._startTime ?? 0,
-2
View File
@@ -242,14 +242,12 @@ export function startWorker() {
"/api/archive_singleplayer_game",
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
const gameRecord: GameRecord = req.body;
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
if (!gameRecord) {
log.info("game record not found in request");
res.status(404).json({ error: "Game record not found" });
return;
}
gameRecord.players.forEach((p) => (p.ip = clientIP));
archive(gameRecord);
res.json({
success: true,