Skip to content
Open
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
82 changes: 82 additions & 0 deletions docs/analytics-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Game Analytics System

## Overview

This document describes the analytics system implemented in `ArenaX`.
It tracks player behaviour, game performance, and business metrics,
providing insights for game balancing and business intelligence.

---

## API Endpoints

| Method | Path | Description |
|--------|------|-------------|
| POST | /api/v1/analytics/events | Track custom events |
| GET | /api/v1/analytics/dashboard | Get analytics dashboard |
| GET | /api/v1/analytics/players/:id | Get player analytics |
| GET | /api/v1/analytics/games/metrics | Get game performance |
| GET | /api/v1/analytics/reports/:type | Generate reports |
| GET | /api/v1/analytics/realtime | Get real-time stats |

---

## Event Tracking

### `POST /api/v1/analytics/events`

```json
{
"eventType": "GAME_COMPLETED",
"data": { "result": "win", "duration": 300 },
"sessionId": "session-123"
}
```

Supported event types: `GAME_COMPLETED`, `SESSION_START`,
`PAYMENT_COMPLETED`, `PAGE_VIEW`, and any custom type.

---

## Report Types

| Type | Description | Required Parameters |
|------|-------------|---------------------|
| player | Player metrics | userId, startDate, endDate |
| game | Game performance | startDate, endDate |
| revenue | Revenue metrics | startDate, endDate |

---

## Implementation

**Files:**
- `server/src/services/analytics.service.ts` — core analytics logic
- `server/src/controllers/analytics.controller.ts` — REST endpoints
- `server/src/routes/analytics.routes.ts` — route definitions
- `server/prisma/schema.prisma` — AnalyticsEvent, PlayerMetrics, GameMetrics models

---

## Performance

- Events processed with under 1 second latency
- Real-time metrics update every 30 seconds
- Queries optimized with database indexes on userId, eventType, createdAt

---

## Security

- IP address and user agent captured for fraud detection
- GDPR: userId is optional — anonymous events supported
- Rate limiting applied via existing middleware

---

## Test Coverage

**File:** `server/test/analytics.test.js`

10 tests covering event tracking, player metrics, game metrics,
report generation, real-time stats, and input validation.
1 change: 1 addition & 0 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -739,3 +739,47 @@ model GameSessionEvent {
@@index([eventType])
@@index([timestamp])
}

model AnalyticsEvent {
id String @id @default(cuid())
userId String?
eventType String
data Json
sessionId String?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())

@@index([userId])
@@index([eventType])
@@index([createdAt])
@@map("analytics_events")
}

model PlayerMetrics {
id String @id @default(cuid())
userId String @unique
totalGames Int @default(0)
wins Int @default(0)
losses Int @default(0)
totalPlaytime Int @default(0)
avgSessionTime Float @default(0)
lastActive DateTime @default(now())
updatedAt DateTime @updatedAt

@@index([userId])
@@map("player_metrics")
}

model GameMetrics {
id String @id @default(cuid())
period String
totalGames Int @default(0)
activePlayers Int @default(0)
avgGameDuration Float @default(0)
revenue Float @default(0)
createdAt DateTime @default(now())

@@index([period])
@@map("game_metrics")
}
137 changes: 137 additions & 0 deletions server/src/controllers/analytics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Request, Response } from 'express';
import {
trackEvent,
calculatePlayerMetrics,
getGamePerformanceMetrics,
generateReport,
getRealTimeStats,
getDashboard,
} from '../services/analytics.service';
import { logger } from '../services/logger.service';

/**
* @notice POST /api/v1/analytics/events
* @dev Track a custom analytics event
*/
export async function trackEventHandler(req: Request, res: Response) {
try {
const { eventType, data, sessionId } = req.body;

if (!eventType) {
return res.status(400).json({ error: 'eventType is required' });
}

if (!data || typeof data !== 'object') {
return res.status(400).json({ error: 'data must be an object' });
}

const event = await trackEvent({
userId: (req as any).user?.id,
eventType,
data,
sessionId,
ipAddress: req.ip,
userAgent: req.headers['user-agent'],
});

return res.status(201).json({ success: true, eventId: event.id });
} catch (error) {
logger.error('trackEventHandler error', { error });
return res.status(500).json({ error: 'Failed to track event' });
}
}

/**
* @notice GET /api/v1/analytics/dashboard
* @dev Get analytics dashboard data
*/
export async function getDashboardHandler(req: Request, res: Response) {
try {
const dashboard = await getDashboard();
return res.json(dashboard);
} catch (error) {
logger.error('getDashboardHandler error', { error });
return res.status(500).json({ error: 'Failed to get dashboard' });
}
}

/**
* @notice GET /api/v1/analytics/players/:id
* @dev Get player analytics for a specific user
*/
export async function getPlayerAnalyticsHandler(req: Request, res: Response) {
try {
const { id } = req.params;
const { startDate, endDate } = req.query;

const period = {
startDate: startDate
? new Date(startDate as string)
: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
endDate: endDate ? new Date(endDate as string) : new Date(),
};

const metrics = await calculatePlayerMetrics(id, period);
return res.json(metrics);
} catch (error) {
logger.error('getPlayerAnalyticsHandler error', { error });
return res.status(500).json({ error: 'Failed to get player analytics' });
}
}

/**
* @notice GET /api/v1/analytics/games/metrics
* @dev Get game performance metrics
*/
export async function getGameMetricsHandler(req: Request, res: Response) {
try {
const { startDate, endDate } = req.query;

const period = {
startDate: startDate
? new Date(startDate as string)
: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
endDate: endDate ? new Date(endDate as string) : new Date(),
};

const metrics = await getGamePerformanceMetrics(period);
return res.json(metrics);
} catch (error) {
logger.error('getGameMetricsHandler error', { error });
return res.status(500).json({ error: 'Failed to get game metrics' });
}
}

/**
* @notice GET /api/v1/analytics/reports/:type
* @dev Generate a custom report
*/
export async function generateReportHandler(req: Request, res: Response) {
try {
const { type } = req.params;
const parameters = req.query as Record<string, unknown>;

const report = await generateReport(type, parameters);
return res.json(report);
} catch (error: any) {
logger.error('generateReportHandler error', { error });
if (error.message?.includes('Unknown report type')) {
return res.status(400).json({ error: error.message });
}
return res.status(500).json({ error: 'Failed to generate report' });
}
}

/**
* @notice GET /api/v1/analytics/realtime
* @dev Get real-time statistics
*/
export async function getRealTimeStatsHandler(req: Request, res: Response) {
try {
const stats = await getRealTimeStats();
return res.json(stats);
} catch (error) {
logger.error('getRealTimeStatsHandler error', { error });
return res.status(500).json({ error: 'Failed to get real-time stats' });
}
}
49 changes: 49 additions & 0 deletions server/src/routes/analytics.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Router } from 'express';
import {
trackEventHandler,
getDashboardHandler,
getPlayerAnalyticsHandler,
getGameMetricsHandler,
generateReportHandler,
getRealTimeStatsHandler,
} from '../controllers/analytics.controller';

const router = Router();

/**
* @notice POST /api/v1/analytics/events
* Track a custom analytics event
*/
router.post('/events', trackEventHandler);

/**
* @notice GET /api/v1/analytics/dashboard
* Get analytics dashboard data
*/
router.get('/dashboard', getDashboardHandler);

/**
* @notice GET /api/v1/analytics/players/:id
* Get player analytics
*/
router.get('/players/:id', getPlayerAnalyticsHandler);

/**
* @notice GET /api/v1/analytics/games/metrics
* Get game performance metrics
*/
router.get('/games/metrics', getGameMetricsHandler);

/**
* @notice GET /api/v1/analytics/reports/:type
* Generate a custom report
*/
router.get('/reports/:type', generateReportHandler);

/**
* @notice GET /api/v1/analytics/realtime
* Get real-time statistics
*/
router.get('/realtime', getRealTimeStatsHandler);

export default router;
3 changes: 3 additions & 0 deletions server/src/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import walletRoutes from './wallet.routes';
import matchRoutes from './match.routes';
import achievementRoutes from './achievement.routes';
import tournamentRoutes from './tournament.routes';
import analyticsRoutes from './analytics.routes';


import { publicRateLimiter } from '../middleware/rate-limit.middleware';
import { auditMiddleware } from '../middleware/audit.middleware';
Expand All @@ -26,5 +28,6 @@ router.use('/wallets', walletRoutes);
router.use('/wallet', walletRoutes);
router.use('/v1/achievements', achievementRoutes);
router.use('/v1/tournaments', tournamentRoutes);
router.use('/v1/analytics', analyticsRoutes);

export default router;
Loading
Loading