← back to Watches

analytics/funnel-tracker.js

302 lines

/**
 * User Funnel Tracking System
 * Track user journeys through predefined conversion funnels
 */

class FunnelTracker {
  constructor(analytics) {
    this.analytics = analytics;
    this.funnels = new Map();
    this.userFunnelState = this.loadFunnelState();
    this.setupFunnels();
  }

  /**
   * Define conversion funnels
   */
  setupFunnels() {
    // Watch Discovery Funnel
    this.registerFunnel('watch_discovery', [
      { step: 'landing', name: 'Land on Site' },
      { step: 'browse', name: 'Browse Watches' },
      { step: 'view_details', name: 'View Watch Details' },
      { step: 'compare', name: 'Compare Watches' },
      { step: 'export', name: 'Export Data' }
    ]);

    // Price Research Funnel
    this.registerFunnel('price_research', [
      { step: 'search', name: 'Search Watch' },
      { step: 'view_price_history', name: 'View Price History' },
      { step: 'view_predictions', name: 'View Price Predictions' },
      { step: 'export_report', name: 'Export Report' }
    ]);

    // Collection Analysis Funnel
    this.registerFunnel('collection_analysis', [
      { step: 'filter_series', name: 'Filter by Series' },
      { step: 'view_collection_stats', name: 'View Collection Stats' },
      { step: 'compare_models', name: 'Compare Models' },
      { step: 'deep_analysis', name: 'Deep Analysis' }
    ]);

    // Investment Research Funnel
    this.registerFunnel('investment_research', [
      { step: 'view_trending', name: 'View Trending Watches' },
      { step: 'check_appreciation', name: 'Check Appreciation' },
      { step: 'view_predictions', name: 'View Predictions' },
      { step: 'export_investment_data', name: 'Export Investment Data' }
    ]);
  }

  /**
   * Register a new funnel
   */
  registerFunnel(funnelId, steps) {
    this.funnels.set(funnelId, {
      id: funnelId,
      steps: steps,
      conversions: new Map()
    });
  }

  /**
   * Track funnel step
   */
  trackStep(funnelId, stepId, metadata = {}) {
    const funnel = this.funnels.get(funnelId);
    if (!funnel) {
      console.warn(`Funnel ${funnelId} not found`);
      return;
    }

    const step = funnel.steps.find(s => s.step === stepId);
    if (!step) {
      console.warn(`Step ${stepId} not found in funnel ${funnelId}`);
      return;
    }

    // Update user's funnel state
    if (!this.userFunnelState[funnelId]) {
      this.userFunnelState[funnelId] = {
        steps: [],
        startTime: Date.now(),
        metadata: {}
      };
    }

    const funnelState = this.userFunnelState[funnelId];
    const stepIndex = funnel.steps.findIndex(s => s.step === stepId);

    // Check if this is a new step or revisit
    const existingStepIndex = funnelState.steps.findIndex(s => s.step === stepId);

    if (existingStepIndex === -1) {
      funnelState.steps.push({
        step: stepId,
        timestamp: Date.now(),
        stepIndex: stepIndex,
        metadata: metadata
      });
    } else {
      // Update existing step
      funnelState.steps[existingStepIndex].timestamp = Date.now();
      funnelState.steps[existingStepIndex].metadata = {
        ...funnelState.steps[existingStepIndex].metadata,
        ...metadata
      };
    }

    // Update global metadata
    funnelState.metadata = {
      ...funnelState.metadata,
      ...metadata
    };

    this.saveFunnelState();

    // Track in analytics
    this.analytics.trackEvent('funnel_step', {
      funnel_id: funnelId,
      funnel_name: this.getFunnelName(funnelId),
      step_id: stepId,
      step_name: step.name,
      step_index: stepIndex,
      total_steps: funnel.steps.length,
      time_in_funnel: Date.now() - funnelState.startTime,
      completion_percentage: ((stepIndex + 1) / funnel.steps.length) * 100,
      ...metadata
    });

    // Check if funnel is completed
    if (stepIndex === funnel.steps.length - 1) {
      this.trackFunnelCompletion(funnelId);
    }

    // Send to backend
    this.sendFunnelDataToBackend(funnelId, stepId, metadata);
  }

  /**
   * Track funnel completion
   */
  trackFunnelCompletion(funnelId) {
    const funnelState = this.userFunnelState[funnelId];
    if (!funnelState) return;

    const completionTime = Date.now() - funnelState.startTime;
    const funnel = this.funnels.get(funnelId);

    this.analytics.trackEvent('funnel_completion', {
      funnel_id: funnelId,
      funnel_name: this.getFunnelName(funnelId),
      completion_time: completionTime,
      steps_completed: funnelState.steps.length,
      total_steps: funnel.steps.length,
      metadata: funnelState.metadata
    });

    // Clear funnel state after completion
    delete this.userFunnelState[funnelId];
    this.saveFunnelState();
  }

  /**
   * Track funnel abandonment
   */
  trackAbandonment(funnelId, reason = '') {
    const funnelState = this.userFunnelState[funnelId];
    if (!funnelState) return;

    const funnel = this.funnels.get(funnelId);
    const lastStep = funnelState.steps[funnelState.steps.length - 1];

    this.analytics.trackEvent('funnel_abandonment', {
      funnel_id: funnelId,
      funnel_name: this.getFunnelName(funnelId),
      abandoned_at_step: lastStep?.step,
      abandoned_at_step_index: lastStep?.stepIndex,
      steps_completed: funnelState.steps.length,
      total_steps: funnel.steps.length,
      time_before_abandonment: Date.now() - funnelState.startTime,
      abandonment_reason: reason
    });
  }

  /**
   * Get funnel analytics
   */
  getFunnelAnalytics(funnelId) {
    const funnelState = this.userFunnelState[funnelId];
    if (!funnelState) return null;

    const funnel = this.funnels.get(funnelId);
    const currentStepIndex = Math.max(...funnelState.steps.map(s => s.stepIndex));

    return {
      funnelId,
      currentStep: currentStepIndex,
      totalSteps: funnel.steps.length,
      completionPercentage: ((currentStepIndex + 1) / funnel.steps.length) * 100,
      timeInFunnel: Date.now() - funnelState.startTime,
      steps: funnelState.steps,
      metadata: funnelState.metadata
    };
  }

  /**
   * Get all active funnels for user
   */
  getActiveFunnels() {
    return Object.keys(this.userFunnelState).map(funnelId =>
      this.getFunnelAnalytics(funnelId)
    ).filter(Boolean);
  }

  /**
   * Load funnel state from storage
   */
  loadFunnelState() {
    try {
      const state = localStorage.getItem('funnel_state');
      return state ? JSON.parse(state) : {};
    } catch (error) {
      console.error('Failed to load funnel state:', error);
      return {};
    }
  }

  /**
   * Save funnel state to storage
   */
  saveFunnelState() {
    try {
      localStorage.setItem('funnel_state', JSON.stringify(this.userFunnelState));
    } catch (error) {
      console.error('Failed to save funnel state:', error);
    }
  }

  /**
   * Get funnel name
   */
  getFunnelName(funnelId) {
    return funnelId.split('_').map(word =>
      word.charAt(0).toUpperCase() + word.slice(1)
    ).join(' ');
  }

  /**
   * Send funnel data to backend
   */
  async sendFunnelDataToBackend(funnelId, stepId, metadata) {
    try {
      await fetch('/api/analytics/funnels', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          funnelId,
          stepId,
          metadata,
          timestamp: Date.now(),
          userId: this.analytics.userId,
          sessionId: this.analytics.sessionId
        })
      });
    } catch (error) {
      console.error('Failed to send funnel data:', error);
    }
  }

  /**
   * Get funnel conversion rates (from backend)
   */
  async getFunnelConversionRates(funnelId, dateRange = 30) {
    try {
      const response = await fetch(`/api/analytics/funnels/${funnelId}/conversions?days=${dateRange}`);
      return await response.json();
    } catch (error) {
      console.error('Failed to get funnel conversion rates:', error);
      return null;
    }
  }

  /**
   * Export funnel data
   */
  exportFunnelData() {
    return {
      activeFunnels: this.getActiveFunnels(),
      funnelState: this.userFunnelState,
      timestamp: Date.now()
    };
  }
}

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