← 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;