← back to Watches
analytics/report-generator.js
596 lines
/**
* Automated Analytics Report Generator
* Generates daily, weekly, and monthly analytics reports
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class ReportGenerator {
constructor(analyticsStore) {
this.analyticsStore = analyticsStore;
this.reportsDir = path.join(__dirname, 'reports');
if (!fs.existsSync(this.reportsDir)) {
fs.mkdirSync(this.reportsDir, { recursive: true });
}
}
/**
* Generate daily report
*/
async generateDailyReport() {
const date = new Date();
const yesterday = new Date(date.getTime() - 24 * 60 * 60 * 1000);
const report = {
reportType: 'daily',
reportDate: date.toISOString().split('T')[0],
period: {
start: yesterday.toISOString(),
end: date.toISOString()
},
summary: this.generateSummary(yesterday, date),
topEvents: this.getTopEvents(yesterday, date, 20),
userMetrics: this.getUserMetrics(yesterday, date),
funnelPerformance: await this.getFunnelPerformance(yesterday, date),
cohortInsights: this.getCohortInsights(yesterday, date),
abTestResults: await this.getABTestResults(),
recommendations: this.generateRecommendations()
};
this.saveReport(report, 'daily');
return report;
}
/**
* Generate weekly report
*/
async generateWeeklyReport() {
const date = new Date();
const lastWeek = new Date(date.getTime() - 7 * 24 * 60 * 60 * 1000);
const report = {
reportType: 'weekly',
reportDate: date.toISOString().split('T')[0],
period: {
start: lastWeek.toISOString(),
end: date.toISOString()
},
summary: this.generateSummary(lastWeek, date),
trends: this.analyzeTrends(lastWeek, date),
topPerformingContent: this.getTopPerformingContent(lastWeek, date),
userEngagement: this.getUserEngagement(lastWeek, date),
conversionFunnels: await this.getConversionFunnels(lastWeek, date),
cohortRetention: this.getCohortRetention(lastWeek, date),
experimentResults: await this.getExperimentResults(),
actionItems: this.generateActionItems()
};
this.saveReport(report, 'weekly');
this.generateHTMLReport(report, 'weekly');
return report;
}
/**
* Generate monthly report
*/
async generateMonthlyReport() {
const date = new Date();
const lastMonth = new Date(date.getTime() - 30 * 24 * 60 * 60 * 1000);
const report = {
reportType: 'monthly',
reportDate: date.toISOString().split('T')[0],
period: {
start: lastMonth.toISOString(),
end: date.toISOString()
},
executiveSummary: this.generateExecutiveSummary(lastMonth, date),
keyMetrics: this.getKeyMetrics(lastMonth, date),
growthAnalysis: this.analyzeGrowth(lastMonth, date),
userBehavior: this.analyzeUserBehavior(lastMonth, date),
funnelOptimization: await this.getFunnelOptimization(lastMonth, date),
cohortAnalysis: this.getDetailedCohortAnalysis(lastMonth, date),
experimentImpact: await this.getExperimentImpact(lastMonth, date),
strategicRecommendations: this.generateStrategicRecommendations()
};
this.saveReport(report, 'monthly');
this.generateHTMLReport(report, 'monthly');
this.generatePDFReport(report, 'monthly');
return report;
}
/**
* Generate summary metrics
*/
generateSummary(startDate, endDate) {
const events = this.getEventsInRange(startDate, endDate);
const uniqueUsers = new Set(events.map(e => e.userId)).size;
const uniqueSessions = new Set(events.map(e => e.params?.session_id)).size;
return {
totalEvents: events.length,
uniqueUsers,
uniqueSessions,
eventsPerUser: uniqueUsers > 0 ? Math.round(events.length / uniqueUsers) : 0,
eventsPerSession: uniqueSessions > 0 ? Math.round(events.length / uniqueSessions) : 0,
avgEngagementTime: this.calculateAvgEngagement(events),
topEventTypes: this.getTopEventTypes(events, 10)
};
}
/**
* Get events in date range
*/
getEventsInRange(startDate, endDate) {
return this.analyticsStore.events.filter(event => {
const eventDate = new Date(event.params?.timestamp || event.receivedAt);
return eventDate >= startDate && eventDate <= endDate;
});
}
/**
* Get top events
*/
getTopEvents(startDate, endDate, limit) {
const events = this.getEventsInRange(startDate, endDate);
const eventCounts = {};
events.forEach(event => {
const name = event.event_name;
eventCounts[name] = (eventCounts[name] || 0) + 1;
});
return Object.entries(eventCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, limit)
.map(([event, count]) => ({ event, count }));
}
/**
* Get top event types
*/
getTopEventTypes(events, limit) {
const eventCounts = {};
events.forEach(event => {
eventCounts[event.event_name] = (eventCounts[event.event_name] || 0) + 1;
});
return Object.entries(eventCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, limit)
.map(([type, count]) => ({ type, count, percentage: (count / events.length * 100).toFixed(2) }));
}
/**
* Calculate average engagement time
*/
calculateAvgEngagement(events) {
const engagementEvents = events.filter(e => e.event_name === 'user_engagement');
if (engagementEvents.length === 0) return 0;
const totalTime = engagementEvents.reduce((sum, e) => sum + (e.params?.engagement_time_msec || 0), 0);
return Math.round(totalTime / engagementEvents.length / 1000); // Convert to seconds
}
/**
* Get user metrics
*/
getUserMetrics(startDate, endDate) {
const events = this.getEventsInRange(startDate, endDate);
const users = new Set(events.map(e => e.userId));
const newUsers = Array.from(users).filter(userId => {
const userEvents = events.filter(e => e.userId === userId);
const firstEvent = userEvents.sort((a, b) =>
new Date(a.params?.timestamp) - new Date(b.params?.timestamp)
)[0];
const firstEventDate = new Date(firstEvent.params?.timestamp);
return firstEventDate >= startDate && firstEventDate <= endDate;
});
return {
totalUsers: users.size,
newUsers: newUsers.length,
returningUsers: users.size - newUsers.length,
newUserPercentage: users.size > 0 ? (newUsers.length / users.size * 100).toFixed(2) : 0
};
}
/**
* Get funnel performance
*/
async getFunnelPerformance(startDate, endDate) {
const funnels = this.analyticsStore.funnels.filter(f => {
const funnelDate = new Date(f.timestamp);
return funnelDate >= startDate && funnelDate <= endDate;
});
const funnelStats = {};
funnels.forEach(funnel => {
if (!funnelStats[funnel.funnelId]) {
funnelStats[funnel.funnelId] = {
totalUsers: new Set(),
steps: {}
};
}
funnelStats[funnel.funnelId].totalUsers.add(funnel.userId);
funnelStats[funnel.funnelId].steps[funnel.stepId] =
(funnelStats[funnel.funnelId].steps[funnel.stepId] || 0) + 1;
});
return Object.entries(funnelStats).map(([funnelId, stats]) => ({
funnelId,
totalUsers: stats.totalUsers.size,
steps: stats.steps
}));
}
/**
* Get cohort insights
*/
getCohortInsights(startDate, endDate) {
const cohortArray = Array.from(this.analyticsStore.cohorts.entries());
const activeCohorts = cohortArray.filter(([, data]) => {
const lastUpdate = new Date(data.lastUpdated);
return lastUpdate >= startDate && lastUpdate <= endDate;
});
const cohortsByMonth = {};
activeCohorts.forEach(([userId, data]) => {
const month = data.cohortMonth;
if (!cohortsByMonth[month]) {
cohortsByMonth[month] = {
users: 0,
totalEngagement: 0
};
}
cohortsByMonth[month].users++;
cohortsByMonth[month].totalEngagement += data.metrics?.engagementScore || 0;
});
return Object.entries(cohortsByMonth).map(([month, data]) => ({
month,
users: data.users,
avgEngagement: Math.round(data.totalEngagement / data.users)
}));
}
/**
* Get A/B test results
*/
async getABTestResults() {
const experiments = ['price_chart_layout', 'watch_card_design', 'cta_button_color', 'search_algorithm'];
const results = [];
for (const experimentId of experiments) {
const assignments = this.analyticsStore.abTests.assignments.filter(a => a.experimentId === experimentId);
if (assignments.length > 0) {
const variantCounts = {};
assignments.forEach(a => {
variantCounts[a.variantId] = (variantCounts[a.variantId] || 0) + 1;
});
results.push({
experimentId,
totalUsers: assignments.length,
variants: Object.entries(variantCounts).map(([variant, count]) => ({
variant,
users: count,
percentage: (count / assignments.length * 100).toFixed(2)
}))
});
}
}
return results;
}
/**
* Analyze trends
*/
analyzeTrends(startDate, endDate) {
const events = this.getEventsInRange(startDate, endDate);
const dayGroups = {};
events.forEach(event => {
const day = new Date(event.params?.timestamp || event.receivedAt).toISOString().split('T')[0];
dayGroups[day] = (dayGroups[day] || 0) + 1;
});
const days = Object.keys(dayGroups).sort();
const values = days.map(day => dayGroups[day]);
// Calculate trend
const avg = values.reduce((a, b) => a + b, 0) / values.length;
const recentAvg = values.slice(-3).reduce((a, b) => a + b, 0) / 3;
const trend = recentAvg > avg ? 'increasing' : 'decreasing';
const trendPercentage = ((recentAvg - avg) / avg * 100).toFixed(2);
return {
trend,
trendPercentage,
dailyAverage: Math.round(avg),
recentAverage: Math.round(recentAvg),
dailyData: days.map((day, i) => ({ day, events: values[i] }))
};
}
/**
* Generate recommendations
*/
generateRecommendations() {
const recommendations = [];
// Analyze recent data
const recentEvents = this.analyticsStore.events.slice(-1000);
const eventCounts = {};
recentEvents.forEach(e => {
eventCounts[e.event_name] = (eventCounts[e.event_name] || 0) + 1;
});
// Low engagement recommendation
const engagementEvents = eventCounts['user_engagement'] || 0;
if (engagementEvents < recentEvents.length * 0.1) {
recommendations.push({
priority: 'high',
category: 'engagement',
title: 'Low User Engagement Detected',
description: 'Less than 10% of events are user engagement events',
action: 'Consider adding more interactive elements or improving content relevance'
});
}
// High bounce rate
const pageViews = eventCounts['page_view'] || 0;
const watchViews = eventCounts['watch_view'] || 0;
if (watchViews < pageViews * 0.3) {
recommendations.push({
priority: 'medium',
category: 'conversion',
title: 'Low Watch View Rate',
description: 'Users are viewing pages but not engaging with watch details',
action: 'Improve watch card design and call-to-action visibility'
});
}
// Search optimization
const searches = eventCounts['search'] || 0;
if (searches > pageViews * 0.5) {
recommendations.push({
priority: 'medium',
category: 'ux',
title: 'High Search Usage',
description: 'Users are heavily relying on search functionality',
action: 'Consider improving navigation and categorization'
});
}
return recommendations;
}
/**
* Generate action items
*/
generateActionItems() {
return this.generateRecommendations().map((rec, index) => ({
id: index + 1,
...rec,
status: 'pending',
assignee: 'TBD'
}));
}
/**
* Generate strategic recommendations
*/
generateStrategicRecommendations() {
const recommendations = this.generateRecommendations();
return {
immediate: recommendations.filter(r => r.priority === 'high'),
shortTerm: recommendations.filter(r => r.priority === 'medium'),
longTerm: recommendations.filter(r => r.priority === 'low')
};
}
/**
* Save report to file
*/
saveReport(report, type) {
const filename = `${type}-report-${report.reportDate}.json`;
const filepath = path.join(this.reportsDir, filename);
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
console.log(`Report saved: ${filepath}`);
}
/**
* Generate HTML report
*/
generateHTMLReport(report, type) {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${type.toUpperCase()} Analytics Report - ${report.reportDate}</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 20px;
}
.section {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.metric {
display: inline-block;
padding: 15px;
margin: 10px;
background: #f0f0f0;
border-radius: 6px;
min-width: 200px;
}
.metric-value {
font-size: 32px;
font-weight: bold;
color: #667eea;
}
.metric-label {
color: #666;
font-size: 14px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f5f5f5;
font-weight: bold;
}
.recommendation {
padding: 15px;
margin: 10px 0;
border-left: 4px solid #667eea;
background: #f9f9f9;
}
.priority-high { border-left-color: #f56565; }
.priority-medium { border-left-color: #ed8936; }
.priority-low { border-left-color: #48bb78; }
</style>
</head>
<body>
<div class="header">
<h1>${type.toUpperCase()} Analytics Report</h1>
<p>Period: ${report.period.start} to ${report.period.end}</p>
<p>Generated: ${new Date().toLocaleString()}</p>
</div>
<div class="section">
<h2>Summary Metrics</h2>
<div class="metric">
<div class="metric-value">${report.summary.totalEvents.toLocaleString()}</div>
<div class="metric-label">Total Events</div>
</div>
<div class="metric">
<div class="metric-value">${report.summary.uniqueUsers.toLocaleString()}</div>
<div class="metric-label">Unique Users</div>
</div>
<div class="metric">
<div class="metric-value">${report.summary.uniqueSessions.toLocaleString()}</div>
<div class="metric-label">Active Sessions</div>
</div>
<div class="metric">
<div class="metric-value">${report.summary.eventsPerUser}</div>
<div class="metric-label">Events per User</div>
</div>
</div>
<div class="section">
<h2>Top Events</h2>
<table>
<thead>
<tr>
<th>Event Type</th>
<th>Count</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
${report.summary.topEventTypes.map(evt => `
<tr>
<td>${evt.type}</td>
<td>${evt.count}</td>
<td>${evt.percentage}%</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
${report.recommendations ? `
<div class="section">
<h2>Recommendations</h2>
${report.recommendations.map(rec => `
<div class="recommendation priority-${rec.priority}">
<h3>${rec.title}</h3>
<p><strong>Category:</strong> ${rec.category} | <strong>Priority:</strong> ${rec.priority}</p>
<p>${rec.description}</p>
<p><strong>Action:</strong> ${rec.action}</p>
</div>
`).join('')}
</div>
` : ''}
</body>
</html>
`;
const filename = `${type}-report-${report.reportDate}.html`;
const filepath = path.join(this.reportsDir, filename);
fs.writeFileSync(filepath, html);
console.log(`HTML report saved: ${filepath}`);
}
/**
* Generate PDF report (stub - would use puppeteer in production)
*/
generatePDFReport(report, type) {
console.log(`PDF report generation requested for ${type} report`);
// In production, use puppeteer or similar to convert HTML to PDF
}
/**
* Placeholder methods for additional metrics
*/
getTopPerformingContent() { return []; }
getUserEngagement() { return {}; }
getConversionFunnels() { return []; }
getCohortRetention() { return []; }
getExperimentResults() { return []; }
generateExecutiveSummary() { return {}; }
getKeyMetrics() { return {}; }
analyzeGrowth() { return {}; }
analyzeUserBehavior() { return {}; }
getFunnelOptimization() { return []; }
getDetailedCohortAnalysis() { return {}; }
getExperimentImpact() { return {}; }
}
export default ReportGenerator;