← back to Watches
analytics/analytics-server.js
511 lines
/**
* Analytics Backend Server
* 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';
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());
// 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 });
}
/**
* 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 =>
now - new Date(e.params.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'
};
}
/**
* POST /api/analytics/events
* Receive 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);
// Save to disk every 100 events
if (analyticsStore.events.length % 100 === 0) {
saveAnalyticsData();
}
res.json({ success: true, eventId: analyticsStore.events.length });
});
/**
* POST /api/analytics/funnels
* Receive funnel tracking data
*/
app.post('/api/analytics/funnels', (req, res) => {
const funnelData = {
...req.body,
receivedAt: new Date().toISOString()
};
analyticsStore.funnels.push(funnelData);
res.json({ success: true });
});
/**
* GET /api/analytics/funnels/:funnelId/conversions
* Get funnel conversion rates
*/
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
);
// Group by step
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);
});
// Calculate conversion rates
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
* Receive cohort data
*/
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
* Get cohort summary analytics
*/
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;
});
// Calculate averages
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
* Track A/B test 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
* Track A/B test 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
* Track A/B test 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
* Get A/B test results
*/
app.get('/api/analytics/ab-tests/:experimentId/results', (req, res) => {
const { experimentId } = req.params;
// Get all assignments for this experiment
const assignments = analyticsStore.abTests.assignments.filter(
a => a.experimentId === experimentId
);
// Get all goals for this experiment
const goals = analyticsStore.abTests.goals.filter(
g => g.experimentId === experimentId
);
// Get all metrics for this experiment
const metrics = analyticsStore.abTests.metrics.filter(
m => m.experimentId === experimentId
);
// Group by variant
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);
});
// Add goals
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);
}
});
// Add metrics
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);
}
});
// Calculate statistics
const results = Object.values(variantData).map(variant => {
const userCount = variant.users.size;
// Process goals
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
};
});
// Process metrics
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
* Get analytics dashboard data
*/
app.get('/api/analytics/dashboard', (req, res) => {
const hours = parseInt(req.query.hours) || 24;
const metrics = calculateMetrics(analyticsStore.events, hours * 60 * 60 * 1000);
// Top events
const topEvents = Object.entries(metrics.eventCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([event, count]) => ({ event, count }));
// Recent events
const recentEvents = analyticsStore.events
.slice(-50)
.reverse()
.map(e => ({
eventName: e.event_name,
timestamp: e.params.timestamp,
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
* Export all analytics data
*/
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);
});
/**
* GET /api/analytics/health
* Health check
*/
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
}
});
});
// 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();
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}/api/analytics/dashboard
🔌 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
READY TO TRACK!
`);
});