← back to Watches

analytics/cohort-analysis.js

288 lines

/**
 * Cohort Analysis System
 * Track user cohorts based on first visit date and behavior patterns
 */

class CohortAnalyzer {
  constructor(analytics) {
    this.analytics = analytics;
    this.cohortData = this.loadCohortData();
    this.initializeCohort();
  }

  /**
   * Initialize user's cohort
   */
  initializeCohort() {
    if (!this.cohortData.cohortMonth) {
      const now = new Date();
      this.cohortData.cohortMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
      this.cohortData.cohortWeek = this.getWeekId(now);
      this.cohortData.firstVisit = now.toISOString();
      this.cohortData.visits = [];
      this.cohortData.actions = [];
      this.saveCohortData();

      // Track cohort creation
      this.analytics.trackEvent('cohort_created', {
        cohort_month: this.cohortData.cohortMonth,
        cohort_week: this.cohortData.cohortWeek,
        first_visit: this.cohortData.firstVisit
      });
    }

    // Record this visit
    this.recordVisit();
  }

  /**
   * Record a visit
   */
  recordVisit() {
    const visit = {
      timestamp: Date.now(),
      date: new Date().toISOString(),
      daysSinceFirstVisit: this.getDaysSinceFirstVisit(),
      weeksSinceFirstVisit: this.getWeeksSinceFirstVisit(),
      monthsSinceFirstVisit: this.getMonthsSinceFirstVisit()
    };

    this.cohortData.visits.push(visit);
    this.cohortData.lastVisit = visit.date;
    this.cohortData.totalVisits = this.cohortData.visits.length;
    this.saveCohortData();

    // Track retention
    this.trackRetention(visit);
  }

  /**
   * Track user retention
   */
  trackRetention(visit) {
    this.analytics.trackEvent('user_retention', {
      cohort_month: this.cohortData.cohortMonth,
      cohort_week: this.cohortData.cohortWeek,
      days_since_first_visit: visit.daysSinceFirstVisit,
      weeks_since_first_visit: visit.weeksSinceFirstVisit,
      months_since_first_visit: visit.monthsSinceFirstVisit,
      total_visits: this.cohortData.totalVisits,
      is_returning_user: this.cohortData.totalVisits > 1
    });
  }

  /**
   * Record user action for cohort analysis
   */
  recordAction(actionType, actionDetails = {}) {
    const action = {
      type: actionType,
      timestamp: Date.now(),
      daysSinceFirstVisit: this.getDaysSinceFirstVisit(),
      details: actionDetails
    };

    this.cohortData.actions.push(action);
    this.saveCohortData();

    // Track cohort action
    this.analytics.trackEvent('cohort_action', {
      cohort_month: this.cohortData.cohortMonth,
      action_type: actionType,
      days_since_first_visit: action.daysSinceFirstVisit,
      total_actions: this.cohortData.actions.length,
      ...actionDetails
    });
  }

  /**
   * Get days since first visit
   */
  getDaysSinceFirstVisit() {
    if (!this.cohortData.firstVisit) return 0;
    const firstVisit = new Date(this.cohortData.firstVisit);
    const now = new Date();
    return Math.floor((now - firstVisit) / (1000 * 60 * 60 * 24));
  }

  /**
   * Get weeks since first visit
   */
  getWeeksSinceFirstVisit() {
    return Math.floor(this.getDaysSinceFirstVisit() / 7);
  }

  /**
   * Get months since first visit
   */
  getMonthsSinceFirstVisit() {
    if (!this.cohortData.firstVisit) return 0;
    const firstVisit = new Date(this.cohortData.firstVisit);
    const now = new Date();
    return (now.getFullYear() - firstVisit.getFullYear()) * 12 +
           (now.getMonth() - firstVisit.getMonth());
  }

  /**
   * Get week ID for a date
   */
  getWeekId(date) {
    const yearStart = new Date(date.getFullYear(), 0, 1);
    const weekNumber = Math.ceil(((date - yearStart) / 86400000 + yearStart.getDay() + 1) / 7);
    return `${date.getFullYear()}-W${String(weekNumber).padStart(2, '0')}`;
  }

  /**
   * Get user engagement score
   */
  getEngagementScore() {
    const daysSinceFirst = this.getDaysSinceFirstVisit();
    if (daysSinceFirst === 0) return 0;

    const visitFrequency = this.cohortData.totalVisits / (daysSinceFirst + 1);
    const actionFrequency = this.cohortData.actions.length / (daysSinceFirst + 1);
    const recency = this.getDaysSinceLastVisit();

    // Calculate weighted engagement score
    const score = (
      (visitFrequency * 40) +
      (actionFrequency * 40) +
      (Math.max(0, 30 - recency) / 30 * 20)
    );

    return Math.min(100, Math.round(score));
  }

  /**
   * Get days since last visit
   */
  getDaysSinceLastVisit() {
    if (!this.cohortData.lastVisit) return 0;
    const lastVisit = new Date(this.cohortData.lastVisit);
    const now = new Date();
    return Math.floor((now - lastVisit) / (1000 * 60 * 60 * 24));
  }

  /**
   * Get cohort segment
   */
  getCohortSegment() {
    const engagementScore = this.getEngagementScore();
    const daysSinceFirst = this.getDaysSinceFirstVisit();

    if (daysSinceFirst === 0) {
      return 'new_user';
    } else if (daysSinceFirst < 7) {
      return engagementScore > 50 ? 'active_new' : 'passive_new';
    } else if (daysSinceFirst < 30) {
      if (engagementScore > 60) return 'power_user';
      if (engagementScore > 30) return 'regular_user';
      return 'occasional_user';
    } else {
      if (engagementScore > 60) return 'loyal_user';
      if (engagementScore > 30) return 'returning_user';
      if (this.getDaysSinceLastVisit() > 30) return 'at_risk';
      return 'dormant';
    }
  }

  /**
   * Get user lifecycle stage
   */
  getLifecycleStage() {
    const totalVisits = this.cohortData.totalVisits;
    const daysSinceFirst = this.getDaysSinceFirstVisit();
    const daysSinceLast = this.getDaysSinceLastVisit();

    if (totalVisits === 1) return 'acquisition';
    if (daysSinceFirst < 7 && totalVisits < 5) return 'activation';
    if (totalVisits >= 5 && daysSinceLast < 30) return 'active';
    if (daysSinceLast >= 30 && daysSinceLast < 60) return 'at_risk';
    if (daysSinceLast >= 60) return 'churned';

    return 'retention';
  }

  /**
   * Get cohort metrics summary
   */
  getCohortMetrics() {
    return {
      cohortMonth: this.cohortData.cohortMonth,
      cohortWeek: this.cohortData.cohortWeek,
      firstVisit: this.cohortData.firstVisit,
      lastVisit: this.cohortData.lastVisit,
      totalVisits: this.cohortData.totalVisits,
      totalActions: this.cohortData.actions.length,
      daysSinceFirstVisit: this.getDaysSinceFirstVisit(),
      daysSinceLastVisit: this.getDaysSinceLastVisit(),
      engagementScore: this.getEngagementScore(),
      cohortSegment: this.getCohortSegment(),
      lifecycleStage: this.getLifecycleStage()
    };
  }

  /**
   * Load cohort data from storage
   */
  loadCohortData() {
    try {
      const data = localStorage.getItem('cohort_data');
      return data ? JSON.parse(data) : {
        visits: [],
        actions: []
      };
    } catch (error) {
      console.error('Failed to load cohort data:', error);
      return { visits: [], actions: [] };
    }
  }

  /**
   * Save cohort data to storage
   */
  saveCohortData() {
    try {
      localStorage.setItem('cohort_data', JSON.stringify(this.cohortData));
    } catch (error) {
      console.error('Failed to save cohort data:', error);
    }
  }

  /**
   * Export cohort data
   */
  exportCohortData() {
    return {
      ...this.cohortData,
      metrics: this.getCohortMetrics()
    };
  }

  /**
   * Send cohort data to backend
   */
  async syncWithBackend() {
    try {
      await fetch('/api/analytics/cohorts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          userId: this.analytics.userId,
          sessionId: this.analytics.sessionId,
          cohortData: this.exportCohortData()
        })
      });
    } catch (error) {
      console.error('Failed to sync cohort data:', error);
    }
  }
}

// Export
if (typeof module !== 'undefined' && module.exports) {
  module.exports = CohortAnalyzer;
} else {
  window.CohortAnalyzer = CohortAnalyzer;
}