Files
ssessesse-ki-battle/tests/http-analytics.test.ts
T

76 lines
6.0 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';
import Fastify from 'fastify';
import { createStatsStore } from '../src/server/stats-store.js';
import { createGameSessionStore } from '../src/server/game-session-store.js';
import { createAnalyticsStore } from '../src/server/analytics-store.js';
import { Analytics } from '../src/server/analytics.js';
import { registerGameApi, registerAnalyticsApi } from '../src/server/game-api.js';
import { ManualClock } from './helpers.js';
import { getAnalyticsReport } from '../src/server/analytics-report.js';
test('HTTP analytics batches deduplicate, throttle activity writes and keep client game summaries separate', { skip: !process.env.TEST_MONGODB_URI }, async () => {
const stats = await createStatsStore({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName: `dragon_ball_test_http_${randomUUID().replaceAll('-', '')}` });
const app = Fastify(), clock = new ManualClock();
const store = await createAnalyticsStore(stats.db);
let touches = 0; const originalTouch = store.touch;
store.touch = async (...args) => { touches++; return originalTouch(...args); };
const analytics = new Analytics(store, clock, 'test');
try {
const sessions = await createGameSessionStore(stats.db);
registerGameApi(app, sessions, stats); registerAnalyticsApi(app, sessions, analytics);
const session = (await app.inject({ method: 'POST', url: '/api/game-sessions', payload: { createRequestId: 'b'.repeat(43) } })).json().data;
const headers = { authorization: `Bearer ${session.token}`, 'x-game-session': session.gameSessionId };
const post = (url: string, payload: any) => app.inject({ method: 'POST', url, headers, payload });
const events = [{ eventId: 'page', eventType: 'page_view', screen: 'home', metadata: { device: 'mobile' } }, ...Array.from({ length: 19 }, (_, i) => ({ eventId: `screen${i}`, eventType: 'screen_view', screen: 'match' }))];
assert.equal((await post('/api/analytics/events', { events })).statusCode, 200);
assert.equal((await post('/api/analytics/events', { events })).statusCode, 200);
assert.equal(await stats.db.collection('analytics_events').countDocuments(), 20);
assert.equal(touches, 1);
for (let i = 0; i < 10; i++) await post('/api/analytics/activity', { visible: true });
assert.equal(touches, 1);
assert.equal(analytics.live().active, 1);
clock.time += 60000;
await post('/api/analytics/activity', { visible: true }); assert.equal(touches, 2);
for (let i = 0; i < 10; i++) await post('/api/analytics/activity', { visible: false });
assert.equal(touches, 3); assert.equal(analytics.live().active, 0);
const summary = await post('/api/analytics/events', { events: [{ eventId: 'start', eventType: 'game_started', matchId: 'm', mode: 'friend' }] });
assert.equal(summary.statusCode, 200);
const event = await stats.db.collection('analytics_events').findOne({ eventType: 'game_started' });
assert.equal(event?.source, 'client'); assert.equal(event?.gameSessionId, session.gameSessionId);
assert.equal(await stats.db.collection('matches').countDocuments(), 0);
assert.equal((await post('/api/analytics/events', { events: Array(21).fill(events[0]) })).statusCode, 400);
assert.equal((await post('/api/analytics/events', { events: [{ eventId: 'fake', eventType: 'match_started' }] })).statusCode, 400);
assert.equal((await post('/api/analytics/events', { events: [
{ eventId: 'ranked-screen', eventType: 'screen_view', screen: 'ranked-queue' },
{ eventId: 'disconnect-summary', eventType: 'game_finished', matchId: 'disconnected', mode: 'friend', outcome: 'aborted', reason: 'disconnect' },
{ eventId: 'double-disconnect-summary', eventType: 'game_finished', matchId: 'both-disconnected', mode: 'friend', outcome: 'aborted', reason: 'double_disconnect' },
] })).statusCode, 200);
assert.equal((await app.inject({ method: 'POST', url: '/api/analytics/activity', payload: { visible: true } })).statusCode, 401);
} finally { await app.close(); await analytics.close(); try { await stats.db.dropDatabase(); } finally { await stats.close(); } }
});
test('current client summaries remain available without legacy server metrics', { skip: !process.env.TEST_MONGODB_URI }, async () => {
const stats = await createStatsStore({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName: `dragon_ball_test_source_${randomUUID().replaceAll('-', '')}` });
const clock = new ManualClock(), store = await createAnalyticsStore(stats.db);
const analytics = new Analytics(store, clock);
try {
const guest = await stats.createGuest(), p = { playerId: guest.playerId, gameSessionId: 'game-session' };
await analytics.clientEvent(p, { eventId: 'page', eventType: 'page_view', screen: 'home' });
await analytics.clientEvent(p, { eventId: 'start', eventType: 'game_started', matchId: 'match', mode: 'friend' });
await analytics.clientEvent(p, { eventId: 'finish', eventType: 'game_finished', matchId: 'match', mode: 'friend', outcome: 'win', reason: 'hit' });
await analytics.clientEvent(p, { eventId: 'finish-after-refresh', eventType: 'game_finished', matchId: 'match', mode: 'friend', outcome: 'win', reason: 'hit' });
clock.time++;
const report = await getAnalyticsReport(stats.db, { start: new Date(clock.now() - 86400000), end: new Date(clock.now()) }, new Date(clock.now()));
assert.equal(report.clientGameplay.source, 'client');
assert.equal(report.clientGameplay.players, 1); assert.equal(report.clientGameplay.startedMatches, 1);
assert.equal(report.clientGameplay.resultReports, 1); assert.equal(report.clientGameplay.actions, null);
assert.equal(report.clientGameplay.turns, null); assert.equal(report.clientGameplay.serverVerifiedCompletion, null);
assert.ok(!('gameplay' in report));
assert.ok(!('sessionConversion' in report.overview));
assert.ok(!('visitOnly' in report.overview));
assert.ok(!('conversion' in report.acquisition.source[0]));
} finally { await analytics.close(); try { await stats.db.dropDatabase(); } finally { await stats.close(); } }
});