=> [], 'metadata' => [], 'timestamp' => time(), 'error' => 'Unexpected API response format', 'tracking' => [ 'status' => 'error', 'quantity' => 0, 'duration' => $duration, ], ] ); // Track Mixpanel event immediately. $this->track_event( 'Rocket Insights Recommendation', [ 'status' => 'error', 'quantity' => 0, 'duration' => $duration, ] ); return false; } /** * Prepare average metrics for API parameters. * * @return array */ private function prepare_metrics_to_api(): array { $params = []; // Get average metrics from global score data. $average_metrics = $this->get_average_metrics( false ); if ( ! is_array( $average_metrics ) || empty( $average_metrics ) ) { return []; } if ( isset( $average_metrics['global_score'] ) ) { $params['global_score'] = $average_metrics['global_score']; unset( $average_metrics['global_score'] ); } foreach ( $average_metrics as $metric_key => $metric_value ) { if ( ! isset( self::METRICS_MAPPING[ $metric_key ] ) ) { continue; } $param_key = self::METRICS_MAPPING[ $metric_key ]; $params[ $param_key ] = in_array( $param_key, [ 'cls', 'tbt' ], true ) ? $metric_value : $metric_value / 1000; } return $params; } /** * Clear cached recommendations. * * @return void */ public function clear_recommendations(): void { delete_transient( self::TRANSIENT_NAME ); $this->logger::debug( 'Recommendations: Cache cleared' ); } /** * Get list of WP Rocket option keys that affect recommendations. * * @return array Array of option keys. */ public static function get_tracked_option_keys(): array { return array_keys( self::TRACKED_OPTIONS ); } /** * Get current recommendation status. * * @return string Status: 'expired', 'pending', 'loading', 'completed', 'failed'. */ public function get_status(): string { $data = $this->get_recommendations(); if ( false === $data ) { return 'expired'; } return $data['status']; } /** * Check if required metrics are available for recommendations. * * @return bool True if metrics exist, false otherwise. */ public function has_required_metrics(): bool { $average_metrics = $this->get_average_metrics(); if ( null === $average_metrics ) { return false; } // Verify core metrics exist. $required = Calculator::METRIC_KEYS; foreach ( $required as $metric ) { if ( ! isset( $average_metrics[ $metric ] ) || 'N/A' === $average_metrics[ $metric ] ) { return false; } } return true; } /** * Determine if recommendations should be fetched. * * Compares current metrics hash with cached hash. * * @return bool True if should fetch, false if cache is valid. */ public function should_fetch_recommendations(): bool { $recommendations = $this->get_recommendations(); // No cache = should fetch. if ( false === $recommendations ) { return true; } // Calculate current hash. $current_hash = $this->calculate_metrics_hash(); $cached_hash = $recommendations['metrics_hash'] ?? ''; // Fetch if hash changed. return $current_hash !== $cached_hash; } /** * Extend transient expiration without fetching. * * Used when data hasn't changed but transient is expiring. * * @return void */ public function extend_transient(): void { $data = $this->get_recommendations(); if ( false === $data ) { return; } set_transient( self::TRANSIENT_NAME, $data, self::CACHE_EXPIRATION ); $this->logger::debug( 'Recommendations: Transient extended (no changes detected)' ); } /** * Set loading status in transient. * * @return void */ private function set_loading_status(): void { $data = [ 'status' => 'loading', 'recommendations' => [], 'metadata' => [], 'timestamp' => time(), ]; set_transient( self::TRANSIENT_NAME, $data, self::CACHE_EXPIRATION ); $this->logger::debug( 'Recommendations: Status set to loading' ); } /** * Save recommendations to transient. * * @param array $data Recommendations data. * @return void */ private function save_recommendations( array $data ): void { set_transient( self::TRANSIENT_NAME, $data, self::CACHE_EXPIRATION ); $this->logger::debug( 'Recommendations: Saved to cache', [ 'status' => $data['status'] ] ); } /** * Calculate hash of current metrics and settings. * * @return string MD5 hash. */ private function calculate_metrics_hash(): string { $global_score_data = $this->global_score->get_global_score_data(); $enabled_options = $this->get_enabled_options(); $data = [ 'score' => $global_score_data['score'] ?? 0, 'average_metrics' => $global_score_data['average_metrics'] ?? [], 'enabled_options' => $enabled_options, ]; return md5( (string) wp_json_encode( $data ) ); } /** * Get average metrics from global score data. * * @param bool $formatted Whether to return formatted metrics (default: true). * @return array|null Average metrics or null if not available. */ private function get_average_metrics( bool $formatted = true ): ?array { $global_score_data = $this->global_score->get_global_score_data(); if ( empty( $global_score_data['average_metrics'] ) ) { $this->logger::debug( 'Recommendations: No average metrics available' ); return null; } foreach ( $global_score_data['average_metrics'] as $metric_key => $metric ) { if ( empty( $metric ) || ! isset( $metric['value'] ) ) { continue; } $global_score_data['average_metrics'][ $metric_key ] = $formatted ? $this->metric_formatter->format_metric( $metric_key, $metric['value'] ) : $metric['value']; } $global_score_data['average_metrics']['global_score'] = $global_score_data['score']; return $global_score_data['average_metrics']; } /** * Get enabled WP Rocket options. * * Returns array of option slugs that are currently enabled. * * @param array $options Optional array of new settings to check instead of current options. * @return array Enabled option slugs. */ private function get_enabled_options( array $options = [] ): array { $enabled = []; foreach ( self::TRACKED_OPTIONS as $option_key => $option_tab ) { $value = $this->get_option_value( $option_key, $options ); // Check if option is enabled. if ( $this->is_option_enabled( $option_key, $value ) ) { $enabled[] = $option_key; } } $this->logger::debug( 'Recommendations: Enabled options', [ 'enabled' => $enabled ] ); return $enabled; } /** * Check if a specific option is enabled. * * @param string $option_key Option key. * @param mixed $value Option value. * @return bool True if enabled, false otherwise. */ private function is_option_enabled( string $option_key, $value ): bool { // Boolean options. return ! empty( $value ) && 1 === (int) $value; } /** * Get option value from either provided options array or current options instance. * * @param string $option_key Option key to retrieve. * @param array|string[] $options Optional array of new settings to check instead of current options. If empty, will use current options instance. * * @return mixed */ private function get_option_value( string $option_key, $options = [] ) { if ( ! empty( $options ) ) { return $options[ $option_key ] ?? false; } return $this->options->get( $option_key, false ); } /** * Get current language code. * * @return string ISO language code (e.g., 'en', 'fr'). */ private function get_language(): string { // Get WordPress locale (e.g., 'en_US', 'fr_FR'). $locale = get_locale(); // Extract language code (first 2 characters). $language = substr( $locale, 0, 2 ); return $language; } /** * Maybe fetch recommendations with validation. * * Checks: * 1. Average metrics are available * 2. Hash has changed (data is different) * 3. Not already loading * * @return void */ public function maybe_fetch_recommendations(): void { // Bail if already loading. if ( 'loading' === $this->get_status() ) { return; } // Get global score data to check for URLs. $global_score_data = $this->global_score->get_global_score_data(); $status = $global_score_data['status']; // If no URLs in Performance Monitoring, save failed state. // There's nothing to analyze yet, so show failed state. if ( 'no-url' === $status ) { $this->save_empty_recommendations(); return; } // Save empty state if metrics not ready (e.g., all tests failed). if ( ! $this->has_required_metrics() ) { return; } // Bail if data hasn't changed. if ( ! $this->should_fetch_recommendations() ) { $this->extend_transient(); // Extend for another 24h. return; } // Fetch new recommendations. $this->fetch_recommendations(); } /** * Returns the section anchor corresponding to a given option slug. * * This method maps a provided option slug to its associated section anchor, * which is used for navigation within the admin interface. If the option slug * does not exist in the mapping, it defaults to 'dashboard'. * * @param string $option_slug The slug of the option to map. * @return string */ public function get_section_from_option_slug( string $option_slug ): string { return self::TRACKED_OPTIONS[ $option_slug ] ?? 'dashboard'; } /** * Save empty recommendations state when metrics are not available. * * This displays the failed state when: * - No URLs exist in Performance Monitoring yet * - All tests have failed * - Metrics are incomplete * * @return void */ private function save_empty_recommendations(): void { $this->save_recommendations( [ 'status' => 'failed', 'recommendations' => [], 'metadata' => [], 'timestamp' => time(), 'error' => 'Recommendations unavailable', ] ); $this->logger::debug( 'Recommendations: Saved failed state (no recommendations available)' ); } /** * Set recommendations to failed status with error message and tracking info. * * @param string $error_message Error message to store (default: 'Failed to fetch recommendations'). * @param float $duration Duration of the failed fetch attempt in milliseconds (default: 0). * @return void */ public function set_recommendations_failed( string $error_message = 'Failed to fetch recommendations', float $duration = 0 ): void { $this->save_recommendations( [ 'status' => 'failed', 'recommendations' => [], 'metadata' => [], 'timestamp' => time(), 'error' => $error_message, 'tracking' => [ 'status' => 'error', 'quantity' => 0, 'duration' => $duration, ], ] ); // Track Mixpanel event immediately. $this->track_event( 'Rocket Insights Recommendation', [ 'status' => 'error', 'quantity' => 0, 'duration' => $duration, ] ); } /** * Forces the recalculation of global metrics. * * @return void */ public function force_global_metrics_recalculation(): void { $this->global_score->reset(); } }