← back to Watches
analytics/analytics-server-complete.js
519 lines
/**
* Complete Analytics Backend Server with Scheduler
* Processes and stores analytics data from frontend
* Port: 7650 (new unique port for analytics service)
*/
import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import ReportGenerator from './report-generator.js';
import AnalyticsScheduler from './scheduler.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = 7650;
// Middleware
app.use(cors());
app.use(express.json());
// Serve dashboard HTML
app.use(express.static(__dirname));
// In-memory storage (would use PostgreSQL/TimescaleDB in production)
const analyticsStore = {
events: [],
funnels: [],
cohorts: new Map(),
abTests: {
assignments: [],
goals: [],
metrics: []
}
};
// Analytics data directory
const ANALYTICS_DIR = path.join(__dirname, 'data');
if (!fs.existsSync(ANALYTICS_DIR)) {
fs.mkdirSync(ANALYTICS_DIR, { recursive: true });
}
// Initialize report generator and scheduler
const reportGenerator = new ReportGenerator(analyticsStore);
const scheduler = new AnalyticsScheduler(analyticsStore);
/**
* Helper: Save analytics data to disk
*/
function saveAnalyticsData() {
const timestamp = new Date().toISOString().split('T')[0];
const filepath = path.join(ANALYTICS_DIR, `analytics-${timestamp}.json`);
const data = {
timestamp: new Date().toISOString(),
events: analyticsStore.events.slice(-10000), // Keep last 10k events
funnels: analyticsStore.funnels.slice(-5000),
cohorts: Array.from(analyticsStore.cohorts.entries()),
abTests: analyticsStore.abTests
};
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
}
/**
* Helper: Calculate metrics
*/
function calculateMetrics(events, timeRange = 24 * 60 * 60 * 1000) {
const now = Date.now();
const recentEvents = events.filter(e => {
const timestamp = e.params?.timestamp || e.receivedAt;
return now - new Date(timestamp).getTime() < timeRange;
});
const uniqueUsers = new Set(recentEvents.map(e => e.userId)).size;
const uniqueSessions = new Set(recentEvents.map(e => e.params?.session_id)).size;
const eventCounts = {};
recentEvents.forEach(event => {
eventCounts[event.event_name] = (eventCounts[event.event_name] || 0) + 1;
});
return {
totalEvents: recentEvents.length,
uniqueUsers,
uniqueSessions,
eventCounts,
timeRange: timeRange / (60 * 60 * 1000) + ' hours'
};
}
// ============================================================================
// API ROUTES
// ============================================================================
/**
* POST /api/analytics/events
*/
app.post('/api/analytics/events', (req, res) => {
const event = {
...req.body,
receivedAt: new Date().toISOString(),
userId: req.headers['x-user-id'] || 'anonymous'
};
analyticsStore.events.push(event);
if (analyticsStore.events.length % 100 === 0) {
saveAnalyticsData();
}
res.json({ success: true, eventId: analyticsStore.events.length });
});
/**
* POST /api/analytics/funnels
*/
app.post('/api/analytics/funnels', (req, res) => {
analyticsStore.funnels.push({
...req.body,
receivedAt: new Date().toISOString()
});
res.json({ success: true });
});
/**
* GET /api/analytics/funnels/:funnelId/conversions
*/
app.get('/api/analytics/funnels/:funnelId/conversions', (req, res) => {
const { funnelId } = req.params;
const days = parseInt(req.query.days) || 30;
const cutoffTime = Date.now() - (days * 24 * 60 * 60 * 1000);
const funnelEvents = analyticsStore.funnels.filter(f =>
f.funnelId === funnelId && f.timestamp > cutoffTime
);
const stepCounts = {};
const usersByStep = {};
funnelEvents.forEach(event => {
const step = event.stepId;
stepCounts[step] = (stepCounts[step] || 0) + 1;
if (!usersByStep[step]) usersByStep[step] = new Set();
usersByStep[step].add(event.userId);
});
const steps = Object.keys(stepCounts).sort();
const conversions = [];
let previousUsers = null;
steps.forEach((step, index) => {
const users = usersByStep[step].size;
const conversionRate = previousUsers ? (users / previousUsers) * 100 : 100;
conversions.push({
step,
stepIndex: index,
users,
events: stepCounts[step],
conversionRate: Math.round(conversionRate * 100) / 100,
dropoffRate: Math.round((100 - conversionRate) * 100) / 100
});
previousUsers = users;
});
res.json({
funnelId,
dateRange: days,
totalUsers: funnelEvents.length > 0 ? usersByStep[steps[0]]?.size || 0 : 0,
conversions
});
});
/**
* POST /api/analytics/cohorts
*/
app.post('/api/analytics/cohorts', (req, res) => {
const { userId, cohortData } = req.body;
analyticsStore.cohorts.set(userId, {
...cohortData,
lastUpdated: new Date().toISOString()
});
res.json({ success: true });
});
/**
* GET /api/analytics/cohorts/summary
*/
app.get('/api/analytics/cohorts/summary', (req, res) => {
const cohortSummary = {};
analyticsStore.cohorts.forEach((data, userId) => {
const cohortMonth = data.cohortMonth;
if (!cohortSummary[cohortMonth]) {
cohortSummary[cohortMonth] = {
users: 0,
totalVisits: 0,
totalActions: 0,
engagementScores: [],
lifecycleStages: {}
};
}
const summary = cohortSummary[cohortMonth];
summary.users++;
summary.totalVisits += data.totalVisits || 0;
summary.totalActions += data.actions?.length || 0;
summary.engagementScores.push(data.metrics?.engagementScore || 0);
const stage = data.metrics?.lifecycleStage || 'unknown';
summary.lifecycleStages[stage] = (summary.lifecycleStages[stage] || 0) + 1;
});
Object.keys(cohortSummary).forEach(cohortMonth => {
const summary = cohortSummary[cohortMonth];
summary.avgVisitsPerUser = Math.round(summary.totalVisits / summary.users);
summary.avgActionsPerUser = Math.round(summary.totalActions / summary.users);
summary.avgEngagementScore = Math.round(
summary.engagementScores.reduce((a, b) => a + b, 0) / summary.engagementScores.length
);
delete summary.engagementScores;
});
res.json({
cohorts: Object.entries(cohortSummary)
.sort(([a], [b]) => b.localeCompare(a))
.map(([month, data]) => ({ month, ...data }))
});
});
/**
* POST /api/analytics/ab-tests/assignments
*/
app.post('/api/analytics/ab-tests/assignments', (req, res) => {
analyticsStore.abTests.assignments.push({
...req.body,
receivedAt: new Date().toISOString()
});
res.json({ success: true });
});
/**
* POST /api/analytics/ab-tests/goals
*/
app.post('/api/analytics/ab-tests/goals', (req, res) => {
analyticsStore.abTests.goals.push({
...req.body,
receivedAt: new Date().toISOString()
});
res.json({ success: true });
});
/**
* POST /api/analytics/ab-tests/metrics
*/
app.post('/api/analytics/ab-tests/metrics', (req, res) => {
analyticsStore.abTests.metrics.push({
...req.body,
receivedAt: new Date().toISOString()
});
res.json({ success: true });
});
/**
* GET /api/analytics/ab-tests/:experimentId/results
*/
app.get('/api/analytics/ab-tests/:experimentId/results', (req, res) => {
const { experimentId } = req.params;
const assignments = analyticsStore.abTests.assignments.filter(
a => a.experimentId === experimentId
);
const goals = analyticsStore.abTests.goals.filter(
g => g.experimentId === experimentId
);
const metrics = analyticsStore.abTests.metrics.filter(
m => m.experimentId === experimentId
);
const variantData = {};
assignments.forEach(assignment => {
const variantId = assignment.variantId;
if (!variantData[variantId]) {
variantData[variantId] = {
variantId,
users: new Set(),
goals: {},
metrics: {}
};
}
variantData[variantId].users.add(assignment.userId);
});
goals.forEach(goal => {
const variantId = goal.variantId;
if (variantData[variantId]) {
const goalName = goal.goalName;
if (!variantData[variantId].goals[goalName]) {
variantData[variantId].goals[goalName] = {
count: 0,
totalValue: 0,
users: new Set()
};
}
variantData[variantId].goals[goalName].count++;
variantData[variantId].goals[goalName].totalValue += goal.goalValue;
variantData[variantId].goals[goalName].users.add(goal.userId);
}
});
metrics.forEach(metric => {
const variantId = metric.variantId;
if (variantData[variantId]) {
const metricName = metric.metricName;
if (!variantData[variantId].metrics[metricName]) {
variantData[variantId].metrics[metricName] = [];
}
variantData[variantId].metrics[metricName].push(metric.metricValue);
}
});
const results = Object.values(variantData).map(variant => {
const userCount = variant.users.size;
const goalStats = {};
Object.entries(variant.goals).forEach(([goalName, goalData]) => {
goalStats[goalName] = {
conversions: goalData.users.size,
conversionRate: userCount > 0 ? (goalData.users.size / userCount) * 100 : 0,
totalValue: goalData.totalValue,
avgValue: goalData.count > 0 ? goalData.totalValue / goalData.count : 0
};
});
const metricStats = {};
Object.entries(variant.metrics).forEach(([metricName, values]) => {
const sum = values.reduce((a, b) => a + b, 0);
const avg = values.length > 0 ? sum / values.length : 0;
const sorted = [...values].sort((a, b) => a - b);
const median = sorted.length > 0 ? sorted[Math.floor(sorted.length / 2)] : 0;
metricStats[metricName] = {
count: values.length,
sum,
avg: Math.round(avg * 100) / 100,
median: Math.round(median * 100) / 100,
min: Math.min(...values),
max: Math.max(...values)
};
});
return {
variantId: variant.variantId,
users: userCount,
goals: goalStats,
metrics: metricStats
};
});
res.json({
experimentId,
totalAssignments: assignments.length,
variants: results
});
});
/**
* GET /api/analytics/dashboard
*/
app.get('/api/analytics/dashboard', (req, res) => {
const hours = parseInt(req.query.hours) || 24;
const metrics = calculateMetrics(analyticsStore.events, hours * 60 * 60 * 1000);
const topEvents = Object.entries(metrics.eventCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([event, count]) => ({ event, count }));
const recentEvents = analyticsStore.events
.slice(-50)
.reverse()
.map(e => ({
eventName: e.event_name,
timestamp: e.params?.timestamp || e.receivedAt,
userId: e.userId
}));
res.json({
summary: metrics,
topEvents,
recentEvents,
storage: {
totalEvents: analyticsStore.events.length,
totalFunnels: analyticsStore.funnels.length,
totalCohorts: analyticsStore.cohorts.size,
abTests: {
assignments: analyticsStore.abTests.assignments.length,
goals: analyticsStore.abTests.goals.length,
metrics: analyticsStore.abTests.metrics.length
}
}
});
});
/**
* GET /api/analytics/export
*/
app.get('/api/analytics/export', (req, res) => {
const data = {
exportedAt: new Date().toISOString(),
events: analyticsStore.events,
funnels: analyticsStore.funnels,
cohorts: Array.from(analyticsStore.cohorts.entries()),
abTests: analyticsStore.abTests
};
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Disposition', 'attachment; filename=analytics-export.json');
res.json(data);
});
/**
* POST /api/analytics/reports/generate
*/
app.post('/api/analytics/reports/generate', async (req, res) => {
const { type } = req.body;
try {
const success = await scheduler.triggerReport(type);
if (success) {
res.json({ success: true, message: `${type} report generated successfully` });
} else {
res.status(500).json({ success: false, error: 'Failed to generate report' });
}
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
/**
* GET /api/analytics/scheduler/status
*/
app.get('/api/analytics/scheduler/status', (req, res) => {
res.json(scheduler.getStatus());
});
/**
* GET /api/analytics/health
*/
app.get('/api/analytics/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
storage: {
events: analyticsStore.events.length,
funnels: analyticsStore.funnels.length,
cohorts: analyticsStore.cohorts.size
},
scheduler: scheduler.getStatus()
});
});
// Save analytics data every 5 minutes
setInterval(() => {
saveAnalyticsData();
console.log(`Analytics saved: ${analyticsStore.events.length} events`);
}, 5 * 60 * 1000);
// Save on shutdown
process.on('SIGINT', () => {
console.log('Saving analytics data before shutdown...');
saveAnalyticsData();
scheduler.stop();
process.exit(0);
});
// Start server
app.listen(PORT, '0.0.0.0', () => {
console.log(`
╔════════════════════════════════════════════════════════════════╗
║ ANALYTICS SERVER - WATCHES PROJECT ║
╚════════════════════════════════════════════════════════════════╝
🚀 Server running on port ${PORT}
📊 Dashboard: http://45.61.58.125:${PORT}/dashboard.html
🔌 API: http://45.61.58.125:${PORT}/api/analytics
💾 Data Directory: ${ANALYTICS_DIR}
ENDPOINTS:
✓ POST /api/analytics/events - Track events
✓ POST /api/analytics/funnels - Track funnels
✓ POST /api/analytics/cohorts - Track cohorts
✓ POST /api/analytics/ab-tests/* - Track A/B tests
✓ GET /api/analytics/dashboard - View dashboard
✓ GET /api/analytics/export - Export data
✓ POST /api/analytics/reports/generate - Generate report
✓ GET /api/analytics/scheduler/status - Scheduler status
SCHEDULER:
✓ Daily reports at 2 AM
✓ Weekly reports at 3 AM (Mondays)
✓ Monthly reports at 4 AM (1st of month)
READY TO TRACK!
`);
// Start scheduler
scheduler.start();
});