From 2b971a4620e4f78bf33486b325297adce17125d1 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:17:25 +0100 Subject: [PATCH 1/3] add game feedback exports, add player.id to player game stats --- src/entities/data-export.ts | 3 +- src/services/data-export.service.ts | 43 +++++++++++++++++-- tests/services/data-export/generation.test.ts | 27 ++++++++++++ .../data-export/included-data.test.ts | 35 +++++++++++++++ 4 files changed, 104 insertions(+), 4 deletions(-) diff --git a/src/entities/data-export.ts b/src/entities/data-export.ts index d7cc7092..a9b9cfa9 100644 --- a/src/entities/data-export.ts +++ b/src/entities/data-export.ts @@ -16,7 +16,8 @@ export enum DataExportAvailableEntities { LEADERBOARD_ENTRIES = 'leaderboardEntries', GAME_STATS = 'gameStats', PLAYER_GAME_STATS = 'playerGameStats', - GAME_ACTIVITIES = 'gameActivities' + GAME_ACTIVITIES = 'gameActivities', + GAME_FEEDBACK = 'gameFeedback' } @Entity() diff --git a/src/services/data-export.service.ts b/src/services/data-export.service.ts index 95c8dcbe..15120f09 100644 --- a/src/services/data-export.service.ts +++ b/src/services/data-export.service.ts @@ -28,6 +28,7 @@ import { Job, Queue } from 'bullmq' import createEmailQueue from '../lib/queues/createEmailQueue' import { EmailConfig } from '../lib/messaging/sendEmail' import createClickhouseClient from '../lib/clickhouse/createClient' +import GameFeedback from '../entities/game-feedback' type PropCollection = Collection @@ -45,7 +46,7 @@ type DataExportJob = { includeDevData: boolean } -type ExportableEntity = Event | Player | PlayerAlias | LeaderboardEntry | GameStat | PlayerGameStat | GameActivity +type ExportableEntity = Event | Player | PlayerAlias | LeaderboardEntry | GameStat | PlayerGameStat | GameActivity | GameFeedback type ExportableEntityWithProps = ExportableEntity & EntityWithProps @Routes([ @@ -221,7 +222,7 @@ export default class DataExportService extends Service { where.player = devDataPlayerFilter(em) } - return await em.getRepository(PlayerGameStat).find(where) + return await em.getRepository(PlayerGameStat).find(where, { populate: ['player'] }) } private async getGameActivities(dataExport: DataExport, em: EntityManager): Promise { @@ -232,6 +233,26 @@ export default class DataExportService extends Service { }) } + private async getGameFeedback(dataExport: DataExport, em: EntityManager, includeDevData: boolean): Promise { + const where: FilterQuery = { + playerAlias: { + player: { + game: dataExport.game + } + } + } + + if (!includeDevData) { + where.playerAlias = Object.assign(where.playerAlias, { + player: devDataPlayerFilter(em) + }) + } + + return await em.getRepository(GameFeedback).find(where, { + populate: ['playerAlias.player'] + }) + } + private async createZip(dataExport: DataExport, em: EntityManager, includeDevData: boolean): Promise { const zip = new AdmZip() @@ -270,6 +291,11 @@ export default class DataExportService extends Service { zip.addFile(`${DataExportAvailableEntities.GAME_ACTIVITIES}.csv`, this.buildCSV(DataExportAvailableEntities.GAME_ACTIVITIES, items)) } + if (dataExport.entities.includes(DataExportAvailableEntities.GAME_FEEDBACK)) { + const items = await this.getGameFeedback(dataExport, em, includeDevData) + zip.addFile(`${DataExportAvailableEntities.GAME_FEEDBACK}.csv`, this.buildCSV(DataExportAvailableEntities.GAME_FEEDBACK, items)) + } + return zip } @@ -286,9 +312,11 @@ export default class DataExportService extends Service { case DataExportAvailableEntities.GAME_STATS: return ['id', 'internalName', 'name', 'defaultValue', 'minValue', 'maxValue', 'global', 'globalValue', 'createdAt', 'updatedAt'] case DataExportAvailableEntities.PLAYER_GAME_STATS: - return ['id', 'value', 'stat.id', 'stat.internalName', 'createdAt', 'updatedAt'] + return ['id', 'player.id', 'value', 'stat.id', 'stat.internalName', 'createdAt', 'updatedAt'] case DataExportAvailableEntities.GAME_ACTIVITIES: return ['id', 'user.username', 'gameActivityType', 'gameActivityExtra', 'createdAt'] + case DataExportAvailableEntities.GAME_FEEDBACK: + return ['id', 'category.internalName', 'comment', 'playerAlias.id', 'playerAlias.service', 'playerAlias.identifier', 'playerAlias.player.id', 'createdAt'] } } @@ -319,6 +347,15 @@ export default class DataExportService extends Service { return `"${JSON.stringify(value).replace(/"/g, '\'')}"` case 'globalValue': return get(object, 'hydratedGlobalValue') ?? 'N/A' + case 'playerAlias.id': + case 'playerAlias.service': + case 'playerAlias.identifier': + case 'playerAlias.player.id': + if (object instanceof GameFeedback && object.anonymised) { + return 'Anonymous' + } else { + return String(value) + } default: return String(value) } diff --git a/tests/services/data-export/generation.test.ts b/tests/services/data-export/generation.test.ts index f82f1a75..42c4f47a 100644 --- a/tests/services/data-export/generation.test.ts +++ b/tests/services/data-export/generation.test.ts @@ -16,6 +16,7 @@ import PricingPlanFactory from '../../fixtures/PricingPlanFactory' import PlayerProp from '../../../src/entities/player-prop' import createOrganisationAndGame from '../../utils/createOrganisationAndGame' import createUserAndToken from '../../utils/createUserAndToken' +import GameFeedbackFactory from '../../fixtures/GameFeedbackFactory' describe('Data export service - generation', () => { it('should transform basic columns', async () => { @@ -201,4 +202,30 @@ describe('Data export service - generation', () => { const planActions = await (global.em).getRepository(OrganisationPricingPlanAction).find({ organisationPricingPlan: orgPlan }) expect(planActions).toHaveLength(0) }) + + it('should transform anonymised feedback columns', async () => { + const [, game] = await createOrganisationAndGame() + + const service = new DataExportService() + const proto = Object.getPrototypeOf(service) + + const feedback = await new GameFeedbackFactory(game).state(() => ({ anonymised: true })).one() + + for (const key of ['playerAlias.id', 'playerAlias.service', 'playerAlias.identifier', 'playerAlias.player.id']) { + expect(proto.transformColumn(key, feedback)).toBe('Anonymous') + } + }) + + it('should not transform non-anonymised feedback columns', async () => { + const [, game] = await createOrganisationAndGame() + + const service = new DataExportService() + const proto = Object.getPrototypeOf(service) + + const feedback = await new GameFeedbackFactory(game).state(() => ({ anonymised: false })).one() + + for (const key of ['playerAlias.id', 'playerAlias.service', 'playerAlias.identifier', 'playerAlias.player.id']) { + expect(proto.transformColumn(key, feedback)).not.toBe('Anonymous') + } + }) }) diff --git a/tests/services/data-export/included-data.test.ts b/tests/services/data-export/included-data.test.ts index 9f07cd33..bdb78fcb 100644 --- a/tests/services/data-export/included-data.test.ts +++ b/tests/services/data-export/included-data.test.ts @@ -9,6 +9,7 @@ import GameStatFactory from '../../fixtures/GameStatFactory' import PlayerGameStatFactory from '../../fixtures/PlayerGameStatFactory' import createOrganisationAndGame from '../../utils/createOrganisationAndGame' import { NodeClickHouseClient } from '@clickhouse/client/dist/client' +import GameFeedbackFactory from '../../fixtures/GameFeedbackFactory' describe('Data export service - included data', () => { it('should not include events from dev build players without the dev data header', async () => { @@ -202,4 +203,38 @@ describe('Data export service - included data', () => { const items = await proto.getPlayerGameStats(dataExport, global.em, true) expect(items).toHaveLength(1) }) + + it('should not include feedback from dev build players without the dev data header', async () => { + const [, game] = await createOrganisationAndGame() + + const service = new DataExportService() + const proto = Object.getPrototypeOf(service) + + const player = await new PlayerFactory([game]).devBuild().one() + const feedback = await new GameFeedbackFactory(game).state(() => ({ + playerAlias: player.aliases[0] + })).one() + const dataExport = await new DataExportFactory(game).one() + await (global.em).persistAndFlush([feedback, dataExport]) + + const items = await proto.getGameFeedback(dataExport, global.em, false) + expect(items).toHaveLength(0) + }) + + it('should include player stats from dev build players with the dev data header', async () => { + const [, game] = await createOrganisationAndGame() + + const service = new DataExportService() + const proto = Object.getPrototypeOf(service) + + const player = await new PlayerFactory([game]).devBuild().one() + const feedback = await new GameFeedbackFactory(game).state(() => ({ + playerAlias: player.aliases[0] + })).one() + const dataExport = await new DataExportFactory(game).one() + await (global.em).persistAndFlush([feedback, dataExport]) + + const items = await proto.getGameFeedback(dataExport, global.em, true) + expect(items).toHaveLength(1) + }) }) From c2b4c6fda82a580176227d9dc05baf678595c6d4 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:38:07 +0100 Subject: [PATCH 2/3] fix test --- tests/services/data-export/entities.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/services/data-export/entities.test.ts b/tests/services/data-export/entities.test.ts index 3f3f0115..40d0add4 100644 --- a/tests/services/data-export/entities.test.ts +++ b/tests/services/data-export/entities.test.ts @@ -13,6 +13,6 @@ describe('Data export service - available entities', () => { .auth(token, { type: 'bearer' }) .expect(200) - expect(res.body.entities).toStrictEqual([ 'events', 'players', 'playerAliases', 'leaderboardEntries', 'gameStats', 'playerGameStats', 'gameActivities' ]) + expect(res.body.entities).toStrictEqual([ 'events', 'players', 'playerAliases', 'leaderboardEntries', 'gameStats', 'playerGameStats', 'gameActivities', 'gameFeedback' ]) }) }) From c5729b6a8e13d7ef0491a1852ead54fd4ac335c7 Mon Sep 17 00:00:00 2001 From: tudor <7089284+tudddorrr@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:04:52 +0100 Subject: [PATCH 3/3] fix tests --- .../integrations/steamworksSyncStats.test.ts | 25 +++++++++---------- tests/services/data-export/post.test.ts | 13 ++++++++++ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/lib/integrations/steamworksSyncStats.test.ts b/tests/lib/integrations/steamworksSyncStats.test.ts index 225d7218..910eacfb 100644 --- a/tests/lib/integrations/steamworksSyncStats.test.ts +++ b/tests/lib/integrations/steamworksSyncStats.test.ts @@ -31,7 +31,7 @@ describe('Steamworks integration - sync stats', () => { it('should pull in stats from steamworks', async () => { const [, game] = await createOrganisationAndGame(em) - const statDisplayName = casual.word + const statDisplayName = casual.words(3) const config = await new IntegrationConfigFactory().one() const integration = await new IntegrationFactory().construct(IntegrationType.STEAMWORKS, game, config).one() @@ -44,7 +44,7 @@ describe('Steamworks integration - sync stats', () => { availableGameStats: { stats: [ { - name: 'stat_' + casual.word, + name: 'stat_' + casual.array_of_words(3).join('-'), defaultvalue: 500, displayName: statDisplayName } @@ -81,8 +81,8 @@ describe('Steamworks integration - sync stats', () => { it('should update existing stats with the name and default value from steamworks', async () => { const [, game] = await createOrganisationAndGame(em) - const statName = 'stat_' + casual.word - const statDisplayName = casual.word + const statName = 'stat_' + casual.array_of_words(3).join('-') + const statDisplayName = casual.words(3) const stat = await new GameStatFactory([game]).state(() => ({ internalName: statName })).one() @@ -121,7 +121,7 @@ describe('Steamworks integration - sync stats', () => { it('should pull in player stats from steamworks', async () => { const [, game] = await createOrganisationAndGame(em) - const statName = 'stat_' + casual.word + const statName = 'stat_' + casual.array_of_words(3).join('-') const player = await new PlayerFactory([game]).withSteamAlias().one() @@ -139,7 +139,7 @@ describe('Steamworks integration - sync stats', () => { { name: statName, defaultvalue: 500, - displayName: casual.word + displayName: casual.words(3) } ], achievements: [] @@ -187,9 +187,9 @@ describe('Steamworks integration - sync stats', () => { availableGameStats: { stats: [ { - name: 'stat_' + casual.word, + name: 'stat_' + casual.array_of_words(3).join('-'), defaultvalue: 500, - displayName: casual.word + displayName: casual.words(3) } ], achievements: [] @@ -213,7 +213,7 @@ describe('Steamworks integration - sync stats', () => { it('should update player stats with the ones from steamworks', async () => { const [, game] = await createOrganisationAndGame(em) - const statName = 'stat_' + casual.word + const statName = 'stat_' + casual.array_of_words(3).join('-') const player = await new PlayerFactory([game]).withSteamAlias().one() const stat = await new GameStatFactory([game]).state(() => ({ internalName: statName })).one() @@ -233,7 +233,7 @@ describe('Steamworks integration - sync stats', () => { { name: statName, defaultvalue: 500, - displayName: casual.word + displayName: casual.words(3) } ], achievements: [] @@ -260,14 +260,13 @@ describe('Steamworks integration - sync stats', () => { expect(getSchemaMock).toHaveBeenCalledTimes(1) expect(getUserStatsMock).toHaveBeenCalledTimes(1) - await em.refresh(playerStat) expect(playerStat.value).toBe(301) }) it('should push through player stats that only exist in talo', async () => { const [, game] = await createOrganisationAndGame(em) - const statName = 'stat_' + casual.word + const statName = 'stat_' + casual.array_of_words(3).join('-') const player = await new PlayerFactory([game]).withSteamAlias().one() const stat = await new GameStatFactory([game]).state(() => ({ internalName: statName })).one() @@ -287,7 +286,7 @@ describe('Steamworks integration - sync stats', () => { { name: statName, defaultvalue: 500, - displayName: casual.word + displayName: casual.words(3) } ], achievements: [] diff --git a/tests/services/data-export/post.test.ts b/tests/services/data-export/post.test.ts index cd8f7f09..d2f6a52f 100644 --- a/tests/services/data-export/post.test.ts +++ b/tests/services/data-export/post.test.ts @@ -142,6 +142,19 @@ describe('Data export service - post', () => { expect(res.body.dataExport.entities).toStrictEqual([DataExportAvailableEntities.GAME_ACTIVITIES]) }) + it('should create a data export for game feedback', async () => { + const [organisation, game] = await createOrganisationAndGame() + const [token] = await createUserAndToken({ type: UserType.ADMIN, emailConfirmed: true }, organisation) + + const res = await request(global.app) + .post(`/games/${game.id}/data-exports`) + .send({ entities: [DataExportAvailableEntities.GAME_FEEDBACK] }) + .auth(token, { type: 'bearer' }) + .expect(200) + + expect(res.body.dataExport.entities).toStrictEqual([DataExportAvailableEntities.GAME_FEEDBACK]) + }) + it('should not create a data export for users with unconfirmed emails', async () => { const [organisation, game] = await createOrganisationAndGame() const [token] = await createUserAndToken({ type: UserType.ADMIN }, organisation)