81 lines
6.3 KiB
TypeScript
81 lines
6.3 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { MongoClient } from 'mongodb';
|
|
import { createAnalyticsStore } from '../src/server/analytics-store.js';
|
|
import { getAnalyticsReport, kstDay, ratio, retention } from '../src/server/analytics-report.js';
|
|
import { createStatsStore } from '../src/server/stats-store.js';
|
|
import { RETENTION_MS, VISIT_SESSION_MS } from '../constants/analytics.js';
|
|
|
|
test('KST boundaries, zero denominators, calendar cohorts', () => {
|
|
assert.equal(kstDay(new Date('2026-10-11T15:00:00Z')), '2026-10-12');
|
|
assert.equal(kstDay(new Date('2026-10-11T14:59:59Z')), '2026-10-11');
|
|
assert.deepEqual(ratio(0, 0), { numerator: 0, denominator: 0, percent: null });
|
|
const visits = [{ playerId: 'a', day: '2026-10-01' }, { playerId: 'a', day: '2026-10-02' }, { playerId: 'b', day: '2026-10-02' }];
|
|
const cohorts = retention([{ playerId: 'a', day: '2026-10-01' }, { playerId: 'b', day: '2026-10-02' }], visits, new Date('2026-10-03T00:00:00Z'));
|
|
assert.deepEqual(cohorts[0].d1, { numerator: 1, denominator: 1, percent: 100 });
|
|
assert.equal(cohorts[0].d7, null); assert.equal(cohorts[1].d1, null);
|
|
assert.deepEqual(retention([{ playerId: 'a', day: '2026-09-30' }], [{ playerId: 'a', day: '2026-10-07' }], new Date('2026-10-08T00:00:00Z'))[0].d7, { numerator: 1, denominator: 1, percent: 100 });
|
|
|
|
});
|
|
|
|
test('presence retention shortens legacy expiry and reports count raw observations alongside aggregates', { skip: !process.env.TEST_MONGODB_URI }, async () => {
|
|
const stats = await createStatsStore({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName: `dragon_ball_test_samples_${randomUUID().replaceAll('-', '')}` });
|
|
try {
|
|
const now = new Date(), at = new Date(+now - 60000), old = new Date(+now - 8 * 86400000);
|
|
const samples = stats.db.collection('analytics_presence_samples');
|
|
await samples.insertMany([
|
|
{ occurredAt: at, active: 2, environment: 'production', source: 'activity_reports', expiresAt: new Date(+at + RETENTION_MS) },
|
|
{ occurredAt: old, active: 99, environment: 'production', source: 'activity_reports', expiresAt: new Date(+old + RETENTION_MS) },
|
|
]);
|
|
const store = await createAnalyticsStore(stats.db);
|
|
assert.equal(+(await samples.findOne({ occurredAt: at }))!.expiresAt, +at + 604800000);
|
|
await store.sample(at, { active: 4, connected: null, waitingRooms: null, playingPeople: null, matches: null, source: 'activity_reports' }, 'production',
|
|
{ restartedAt: new Date(+at - 300000), failures: 2, dropped: 1, rateLimited: 3 }, 30);
|
|
const aggregate = await samples.findOne({ sampleCount: 30 });
|
|
assert.equal(+aggregate!.expiresAt, +at + 604800000);
|
|
const guest = await stats.createGuest();
|
|
await store.insert({ _id: 'old-visit', playerId: guest.playerId, occurredAt: old, eventType: 'page_view', environment: 'production', schemaVersion: 1, appVersion: '0.1.0' });
|
|
const report = await getAnalyticsReport(stats.db, { start: new Date(+now - 30 * 86400000), end: now }, now);
|
|
assert.deepEqual(report.sampledPeak, { count: 31, active: 4 });
|
|
assert.deepEqual(report.historicalCollection, { failures: 2, dropped: 1, rateLimited: 3 });
|
|
assert.equal(report.overview.mau.visitors, 1);
|
|
assert.equal(+(await stats.db.collection('analytics_events').findOne({ _id: 'old-visit' as any }))!.expiresAt, +old + RETENTION_MS);
|
|
await createAnalyticsStore(stats.db);
|
|
assert.equal(+(await samples.findOne({ sampleCount: 30 }))!.expiresAt, +at + 604800000);
|
|
} finally { await stats.db.dropDatabase(); await stats.close(); }
|
|
});
|
|
|
|
test('atomic visit session rotation, duplicate collection and safe metadata on actual MongoDB', { skip: !process.env.TEST_MONGODB_URI }, async () => {
|
|
const databaseName = `dragon_ball_test_analytics_${randomUUID().replaceAll('-', '')}`;
|
|
const stats = await createStatsStore({ mongoUri: process.env.TEST_MONGODB_URI!, databaseName });
|
|
try {
|
|
const store = await createAnalyticsStore(stats.db);
|
|
const guest = await stats.createGuest();
|
|
const t = new Date('2026-10-01T00:00:00Z');
|
|
const metadata = { device: 'mobile', browser: 'Chrome 141', source: 'direct_or_unknown', invitation: false };
|
|
const first = await store.touch(guest.playerId, t, true, metadata);
|
|
const second = await store.touch(guest.playerId, new Date(+t + 30000), true, metadata);
|
|
assert.equal(second.sessionId, first.sessionId); assert.equal(second.activeMs, 30000);
|
|
await store.touch(guest.playerId, new Date(+t + 40000), false, metadata);
|
|
const visible = await store.touch(guest.playerId, new Date(+t + 60000), true, metadata);
|
|
assert.equal(visible.activeMs, 30000);
|
|
const [a, b] = await Promise.all([store.touch(guest.playerId, new Date(+t + 60000 + VISIT_SESSION_MS), true, metadata), store.touch(guest.playerId, new Date(+t + 60000 + VISIT_SESSION_MS), true, metadata)]);
|
|
assert.equal(a.sessionId, b.sessionId); assert.notEqual(a.sessionId, first.sessionId);
|
|
await store.touch(guest.playerId, new Date('2026-10-01T15:00:00Z'), true, metadata);
|
|
const acrossDay = await store.touch(guest.playerId, new Date('2026-10-01T15:10:00Z'), true, metadata);
|
|
assert.deepEqual(acrossDay.activeDays, ['2026-10-02']);
|
|
assert.equal(await stats.db.collection('analytics_sessions').countDocuments(), 3);
|
|
const event = { _id: 'event-one', playerId: guest.playerId, sessionId: first.sessionId, occurredAt: t, eventType: 'page_view', environment: 'production', schemaVersion: 1, appVersion: '0.1.0', metadata };
|
|
await Promise.all([store.insert(event), store.insert(event)]);
|
|
assert.equal(await stats.db.collection('analytics_events').countDocuments(), 1);
|
|
assert.equal(+(await stats.db.collection('analytics_events').findOne())!.expiresAt, +t + RETENTION_MS);
|
|
for (const name of ['analytics_events', 'analytics_sessions', 'analytics_presence_samples']) {
|
|
assert.ok((await stats.db.collection(name).indexes()).some(i => i.key.expiresAt === 1 && i.expireAfterSeconds === 0));
|
|
}
|
|
await store.cleanup(new Date(+t + RETENTION_MS + VISIT_SESSION_MS + 86400000));
|
|
const retained = await stats.db.collection('players').findOne({ _id: guest.playerId as any });
|
|
assert.ok(retained); assert.equal(retained?.visit, undefined); assert.ok(retained?.guestTokenHash);
|
|
} finally { await stats.db.dropDatabase(); await stats.close(); }
|
|
});
|