← back to Watches

analytics/ab-testing.js

342 lines

/**
 * A/B Testing Framework
 * Run controlled experiments to optimize user experience
 */

class ABTestingFramework {
  constructor(analytics) {
    this.analytics = analytics;
    this.experiments = new Map();
    this.userAssignments = this.loadAssignments();
    this.setupExperiments();
  }

  /**
   * Setup active experiments
   */
  setupExperiments() {
    // Example experiments - can be configured via backend
    this.createExperiment('price_chart_layout', {
      name: 'Price Chart Layout Test',
      variants: [
        { id: 'control', name: 'Original Layout', weight: 50 },
        { id: 'variant_a', name: 'Side by Side Layout', weight: 50 }
      ],
      startDate: '2025-01-01',
      endDate: '2025-12-31',
      metrics: ['chart_interaction_rate', 'time_on_page', 'comparison_rate']
    });

    this.createExperiment('watch_card_design', {
      name: 'Watch Card Design Test',
      variants: [
        { id: 'control', name: 'Original Design', weight: 50 },
        { id: 'variant_a', name: 'Enhanced Details', weight: 50 }
      ],
      startDate: '2025-01-01',
      endDate: '2025-12-31',
      metrics: ['watch_view_rate', 'comparison_rate', 'engagement_time']
    });

    this.createExperiment('cta_button_color', {
      name: 'CTA Button Color Test',
      variants: [
        { id: 'control', name: 'Blue Button', weight: 33 },
        { id: 'variant_a', name: 'Green Button', weight: 33 },
        { id: 'variant_b', name: 'Orange Button', weight: 34 }
      ],
      startDate: '2025-01-01',
      endDate: '2025-12-31',
      metrics: ['cta_click_rate', 'conversion_rate']
    });

    this.createExperiment('search_algorithm', {
      name: 'Search Algorithm Test',
      variants: [
        { id: 'control', name: 'Basic Search', weight: 50 },
        { id: 'variant_a', name: 'Fuzzy Search', weight: 50 }
      ],
      startDate: '2025-01-01',
      endDate: '2025-12-31',
      metrics: ['search_success_rate', 'search_refinement_rate', 'results_clicked']
    });
  }

  /**
   * Create a new experiment
   */
  createExperiment(experimentId, config) {
    this.experiments.set(experimentId, {
      id: experimentId,
      ...config,
      active: this.isExperimentActive(config.startDate, config.endDate)
    });
  }

  /**
   * Check if experiment is active
   */
  isExperimentActive(startDate, endDate) {
    const now = new Date();
    const start = new Date(startDate);
    const end = new Date(endDate);
    return now >= start && now <= end;
  }

  /**
   * Get variant for user
   */
  getVariant(experimentId) {
    const experiment = this.experiments.get(experimentId);
    if (!experiment || !experiment.active) {
      return 'control';
    }

    // Check if user already assigned
    if (this.userAssignments[experimentId]) {
      return this.userAssignments[experimentId];
    }

    // Assign variant based on weights
    const variant = this.assignVariant(experiment.variants);
    this.userAssignments[experimentId] = variant;
    this.saveAssignments();

    // Track assignment
    this.trackAssignment(experimentId, variant);

    return variant;
  }

  /**
   * Assign variant based on weights
   */
  assignVariant(variants) {
    const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0);
    let random = Math.random() * totalWeight;

    for (const variant of variants) {
      random -= variant.weight;
      if (random <= 0) {
        return variant.id;
      }
    }

    return variants[0].id; // Fallback to first variant
  }

  /**
   * Track variant assignment
   */
  trackAssignment(experimentId, variantId) {
    const experiment = this.experiments.get(experimentId);

    this.analytics.trackEvent('ab_test_assignment', {
      experiment_id: experimentId,
      experiment_name: experiment.name,
      variant_id: variantId,
      variant_name: experiment.variants.find(v => v.id === variantId)?.name
    });

    this.sendAssignmentToBackend(experimentId, variantId);
  }

  /**
   * Track experiment goal
   */
  trackGoal(experimentId, goalName, goalValue = 1) {
    const variantId = this.userAssignments[experimentId];
    if (!variantId) return;

    const experiment = this.experiments.get(experimentId);

    this.analytics.trackEvent('ab_test_goal', {
      experiment_id: experimentId,
      experiment_name: experiment.name,
      variant_id: variantId,
      goal_name: goalName,
      goal_value: goalValue
    });

    this.sendGoalToBackend(experimentId, variantId, goalName, goalValue);
  }

  /**
   * Track metric for experiment
   */
  trackMetric(experimentId, metricName, metricValue) {
    const variantId = this.userAssignments[experimentId];
    if (!variantId) return;

    const experiment = this.experiments.get(experimentId);

    this.analytics.trackEvent('ab_test_metric', {
      experiment_id: experimentId,
      experiment_name: experiment.name,
      variant_id: variantId,
      metric_name: metricName,
      metric_value: metricValue
    });

    this.sendMetricToBackend(experimentId, variantId, metricName, metricValue);
  }

  /**
   * Get active experiments for user
   */
  getActiveExperiments() {
    const activeExperiments = [];

    for (const [experimentId, experiment] of this.experiments) {
      if (experiment.active) {
        activeExperiments.push({
          id: experimentId,
          name: experiment.name,
          variant: this.getVariant(experimentId),
          metrics: experiment.metrics
        });
      }
    }

    return activeExperiments;
  }

  /**
   * Check if variant is active
   */
  isVariant(experimentId, variantId) {
    return this.getVariant(experimentId) === variantId;
  }

  /**
   * Force variant (for testing)
   */
  forceVariant(experimentId, variantId) {
    this.userAssignments[experimentId] = variantId;
    this.saveAssignments();
    this.trackAssignment(experimentId, variantId);
  }

  /**
   * Load assignments from storage
   */
  loadAssignments() {
    try {
      const assignments = localStorage.getItem('ab_assignments');
      return assignments ? JSON.parse(assignments) : {};
    } catch (error) {
      console.error('Failed to load AB assignments:', error);
      return {};
    }
  }

  /**
   * Save assignments to storage
   */
  saveAssignments() {
    try {
      localStorage.setItem('ab_assignments', JSON.stringify(this.userAssignments));
    } catch (error) {
      console.error('Failed to save AB assignments:', error);
    }
  }

  /**
   * Send assignment to backend
   */
  async sendAssignmentToBackend(experimentId, variantId) {
    try {
      await fetch('/api/analytics/ab-tests/assignments', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          userId: this.analytics.userId,
          sessionId: this.analytics.sessionId,
          experimentId,
          variantId,
          timestamp: Date.now()
        })
      });
    } catch (error) {
      console.error('Failed to send AB assignment:', error);
    }
  }

  /**
   * Send goal to backend
   */
  async sendGoalToBackend(experimentId, variantId, goalName, goalValue) {
    try {
      await fetch('/api/analytics/ab-tests/goals', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          userId: this.analytics.userId,
          sessionId: this.analytics.sessionId,
          experimentId,
          variantId,
          goalName,
          goalValue,
          timestamp: Date.now()
        })
      });
    } catch (error) {
      console.error('Failed to send AB goal:', error);
    }
  }

  /**
   * Send metric to backend
   */
  async sendMetricToBackend(experimentId, variantId, metricName, metricValue) {
    try {
      await fetch('/api/analytics/ab-tests/metrics', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          userId: this.analytics.userId,
          sessionId: this.analytics.sessionId,
          experimentId,
          variantId,
          metricName,
          metricValue,
          timestamp: Date.now()
        })
      });
    } catch (error) {
      console.error('Failed to send AB metric:', error);
    }
  }

  /**
   * Get experiment results (from backend)
   */
  async getExperimentResults(experimentId) {
    try {
      const response = await fetch(`/api/analytics/ab-tests/${experimentId}/results`);
      return await response.json();
    } catch (error) {
      console.error('Failed to get experiment results:', error);
      return null;
    }
  }

  /**
   * Export experiment data
   */
  exportExperimentData() {
    return {
      assignments: this.userAssignments,
      activeExperiments: this.getActiveExperiments(),
      timestamp: Date.now()
    };
  }
}

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