database = new Almaweb_Database(); } /** * 1. JAUGE DE PRESSION AI (0-100%) */ public function get_ai_pressure($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $total_visits = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE visited_at >= %s", $date_limit)) ?? 0; $ai_visits = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE is_ai_bot = 1 AND visited_at >= %s", $date_limit)) ?? 0; if ($total_visits == 0) { return ['percentage' => 0, 'status' => 'unknown', 'message' => __('Insufficient data', 'almaweb-ai-visitor-analytics')]; } $percentage = round(($ai_visits / $total_visits) * 100, 1); if ($percentage < 10) { $status = 'healthy'; // translators: %s is the AI traffic percentage $message = sprintf(__('Healthy (%s%%)', 'almaweb-ai-visitor-analytics'), $percentage); } elseif ($percentage < 25) { $status = 'moderate'; // translators: %s is the AI traffic percentage $message = sprintf(__('Moderate (%s%%)', 'almaweb-ai-visitor-analytics'), $percentage); } elseif ($percentage < 50) { $status = 'high'; // translators: %s is the AI traffic percentage $message = sprintf(__('High (%s%%)', 'almaweb-ai-visitor-analytics'), $percentage); } else { $status = 'critical'; // translators: %s is the AI traffic percentage $message = sprintf(__('Critical (%s%%)', 'almaweb-ai-visitor-analytics'), $percentage); } return ['percentage' => $percentage, 'status' => $status, 'message' => $message]; } /** * Helper: Get intention counts for quality score calculation */ private function get_intention_counts($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $results = $wpdb->get_results($wpdb->prepare( "SELECT bot_intention, COUNT(*) as count FROM {$table_name} WHERE is_ai_bot = 1 AND bot_type = 'ai_bot' AND visited_at >= %s GROUP BY bot_intention", $date_limit ), ARRAY_A); $counts = [ 'inference' => 0, 'search' => 0, 'training' => 0, 'scraping' => 0 ]; foreach ($results as $row) { if (isset($counts[$row['bot_intention']])) { $counts[$row['bot_intention']] = (int) $row['count']; } } return $counts; } /** * 2. SCORE DE POPULARITÉ AI (AI Authority Score) */ public function get_ai_authority_score($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $stats = $wpdb->get_row($wpdb->prepare( "SELECT COUNT(DISTINCT bot_family) as unique_families, COUNT(DISTINCT bot_name) as unique_bots, COUNT(*) as total_visits, COUNT(DISTINCT request_url) as unique_pages FROM {$table_name} WHERE is_ai_bot = 1 AND bot_family IS NOT NULL AND bot_family != 'Unknown' AND visited_at >= %s", $date_limit ), ARRAY_A); if (empty($stats) || $stats['total_visits'] == 0) { return ['score' => 0, 'grade' => 'N/A', 'status' => 'waiting', 'message' => __('Waiting for sufficient data...', 'almaweb-ai-visitor-analytics')]; } // Get intention counts for quality calculation $intention_counts = $this->get_intention_counts($days); // 1. DIVERSITY SCORE (max 25 points) - More demanding // 1 family = 5pts, 3 = 15pts, 5+ = 25pts $diversity_score = min((($stats['unique_families'] ?? 0) * 5), 25); // 2. VOLUME SCORE (max 25 points) - More demanding // 100 visits = 14pts, 1000 = 21pts, 10000+ = 25pts $volume_score = min(log10(max($stats['total_visits'], 1)) * 7, 25); // 3. COVERAGE SCORE (max 10 points) - Reduced importance // 10 pages = 3.3pts, 100 = 6.6pts, 1000+ = 10pts $coverage_score = min(log10(max($stats['unique_pages'], 1)) * 3.33, 10); // 4. QUALITY SCORE (max 40 points) - NEW & MOST IMPORTANT // Weighted by intention value: inference (100%), search (70%), training (30%), scraping (0%) $inference = $intention_counts['inference'] ?? 0; $search = $intention_counts['search'] ?? 0; $training = $intention_counts['training'] ?? 0; $scraping = $intention_counts['scraping'] ?? 0; $total_ai_visits = $inference + $search + $training + $scraping; if ($total_ai_visits > 0) { $weighted_quality = ( ($inference * 1.0) + // 100% value - AI citing you in responses ($search * 0.7) + // 70% value - AI indexing you as trusted source ($training * 0.3) + // 30% value - AI collecting your data ($scraping * 0.0) // 0% value - Basic crawling ); $quality_ratio = $weighted_quality / $total_ai_visits; $quality_score = $quality_ratio * 40; // Scale to 0-40 points } else { $quality_score = 0; } $total_score = round($diversity_score + $volume_score + $coverage_score + $quality_score); if ($total_score >= 80) { $grade = 'A+'; $status = 'elite'; $message = __('Elite - Your site is a major reference for AIs', 'almaweb-ai-visitor-analytics'); } elseif ($total_score >= 60) { $grade = 'A'; $status = 'excellent'; $message = __('Excellent - Reliable source for AI models', 'almaweb-ai-visitor-analytics'); } elseif ($total_score >= 40) { $grade = 'B'; $status = 'good'; $message = __('Good - Significant presence in the AI ecosystem', 'almaweb-ai-visitor-analytics'); } elseif ($total_score >= 20) { $grade = 'C'; $status = 'emerging'; $message = __('Emerging - Starting to be detected by AIs', 'almaweb-ai-visitor-analytics'); } else { $grade = 'D'; $status = 'minimal'; $message = __('Minimal - Low AI visibility currently', 'almaweb-ai-visitor-analytics'); } return ['score' => $total_score, 'grade' => $grade, 'status' => $status, 'message' => $message]; } /** * 3. RATIO TRAINING VS INFERENCE (ROBUST) */ public function get_training_vs_inference($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $intention_case_sql = "CASE WHEN bot_intention IS NOT NULL AND bot_intention != '' THEN bot_intention WHEN bot_type = 'search_engine' THEN 'search' WHEN bot_type = 'scraper' THEN 'scraping' ELSE 'training' END"; $results = $wpdb->get_results($wpdb->prepare( "SELECT {$intention_case_sql} as intention_category, COUNT(*) as count FROM {$table_name} WHERE is_ai_bot = 1 AND visited_at >= %s GROUP BY intention_category", $date_limit ), ARRAY_A); $data = ['training' => 0, 'inference' => 0, 'search' => 0, 'scraping' => 0, 'unknown' => 0]; foreach ($results as $row) { if (isset($data[$row['intention_category']])) { $data[$row['intention_category']] = (int) $row['count']; } else { $data['unknown'] += (int) $row['count']; } } $total = array_sum($data); $percentages = []; foreach ($data as $key => $value) { $percentages[$key] = $total > 0 ? round(($value / $total) * 100, 1) : 0; } return [ 'counts' => $data, 'percentages' => $percentages, 'total' => $total, 'labels' => [ 'training' => __('Model training', 'almaweb-ai-visitor-analytics'), 'inference' => __('User responses', 'almaweb-ai-visitor-analytics'), 'search' => __('Indexing & search', 'almaweb-ai-visitor-analytics'), 'scraping' => __('Data collection', 'almaweb-ai-visitor-analytics'), ] ]; } /** * 4. POTENTIEL DE CITATION (ROBUST) */ public function get_citation_potential($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $intention_case_sql = "CASE WHEN bot_intention IS NOT NULL AND bot_intention != '' THEN bot_intention WHEN bot_type = 'search_engine' THEN 'search' ELSE 'other' END"; $citations = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE is_ai_bot = 1 AND visited_at >= %s AND ({$intention_case_sql}) = 'search'", $date_limit )) ?? 0; // translators: %s is the number of citation opportunities return ['total_opportunities' => $citations, 'message' => sprintf(__('%s citation opportunities', 'almaweb-ai-visitor-analytics'), number_format($citations))]; } /** * 5. TOP SUJETS (ROBUST) */ public function get_top_topics($limit = 10, $days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); // Static SQL CASE statement - safe from injection $intention_case_sql = "CASE WHEN bot_intention IS NOT NULL AND bot_intention != '' THEN bot_intention WHEN bot_type = 'search_engine' THEN 'search' WHEN bot_type = 'scraper' THEN 'scraping' ELSE 'training' END"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $intention_case_sql is a safe static CASE statement, $table_name from $wpdb->prefix $results = $wpdb->get_results($wpdb->prepare( "SELECT request_url, COALESCE(NULLIF(page_visited, ''), LEFT(request_url, 200)) as page_title, COUNT(*) as visit_count, SUM(CASE WHEN {$intention_case_sql} = 'inference' THEN 1 ELSE 0 END) as inference_visits, SUM(CASE WHEN {$intention_case_sql} = 'search' THEN 1 ELSE 0 END) as search_visits, SUM(CASE WHEN {$intention_case_sql} = 'training' THEN 1 ELSE 0 END) as training_visits, SUM(CASE WHEN {$intention_case_sql} = 'scraping' THEN 1 ELSE 0 END) as scraping_visits FROM {$table_name} WHERE is_ai_bot = 1 AND visited_at >= %s GROUP BY page_title ORDER BY visit_count DESC LIMIT %d", $date_limit, $limit ), ARRAY_A); // Enrichir avec les vrais titres WordPress foreach ($results as &$result) { $result['display_title'] = $this->get_page_title_from_url($result['request_url']); } return $results; } /** * Extrait le titre de page WordPress depuis une URL */ private function get_page_title_from_url($url) { // Parser l'URL pour extraire le path $parsed = wp_parse_url($url); $path = isset($parsed['path']) ? trim($parsed['path'], '/') : ''; // Page d'accueil if (empty($path)) { return get_bloginfo('name') . ' - ' . __('Home', 'almaweb-ai-visitor-analytics'); } // Essayer de trouver la page/post par URL $page_id = url_to_postid($url); if ($page_id > 0) { $title = get_the_title($page_id); if (!empty($title)) { return $title; } } // Fallback: nettoyer l'URL pour affichage $clean_path = str_replace(['/', '-', '_'], ' ', $path); return ucwords($clean_path); } /** * 6. COÛT ESTIMÉ DU SCRAPING */ public function get_scraping_cost($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $ai_requests = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE is_ai_bot = 1 AND visited_at >= %s", $date_limit )) ?? 0; $avg_page_size_kb = 150; $avg_response_time_ms = 200; $total_bandwidth_mb = round(($ai_requests * $avg_page_size_kb) / 1024, 2); $total_cpu_time_seconds = round(($ai_requests * $avg_response_time_ms) / 1000, 2); return [ 'bandwidth_readable' => $total_bandwidth_mb > 1024 ? number_format($total_bandwidth_mb / 1024, 2) . ' GB' : number_format($total_bandwidth_mb) . ' MB', 'cpu_time_readable' => $total_cpu_time_seconds > 3600 ? number_format($total_cpu_time_seconds / 3600, 1) . 'h' : number_format($total_cpu_time_seconds / 60, 1) . 'm', ]; } /** * 7. VISITES "LIVE" */ public function get_live_visits($days = 30) { $data = $this->get_training_vs_inference($days); // translators: %s is the percentage of AI visits from real users return ['count' => $data['counts']['inference'], 'message' => sprintf(__('%s%% of AI visits come from real users', 'almaweb-ai-visitor-analytics'), $data['percentages']['inference'])]; } /** * 8. PAGE LA PLUS CONSULTÉE */ public function get_hottest_page($days = 7) { $top_topics = $this->get_top_topics(1, $days); if (empty($top_topics)) { return ['page' => __('None', 'almaweb-ai-visitor-analytics'), 'visit_count' => 0, 'url' => '#']; } return [ 'page' => $top_topics[0]['display_title'], 'visit_count' => $top_topics[0]['visit_count'], 'url' => $top_topics[0]['request_url'] ]; } /** * 9. TENDANCE DE PÉNÉTRATION */ public function get_penetration_trend($days = 30) { $current = $this->get_ai_pressure($days)['percentage'] ?? 0; $previous_data = $this->get_ai_pressure_for_period(strtotime("-{$days} days"), strtotime("-".($days * 2)." days")); $previous = $previous_data['percentage'] ?? 0; $trend = round($current - $previous, 1); // translators: %s is the trend percentage with + or - sign return ['percentage' => $trend, 'message' => sprintf(__('%s vs previous period', 'almaweb-ai-visitor-analytics'), ($trend > 0 ? "+{$trend}" : $trend) . '%')]; } private function get_ai_pressure_for_period($end_date, $start_date) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $total_visits = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE visited_at BETWEEN %s AND %s", gmdate('Y-m-d H:i:s', $start_date), gmdate('Y-m-d H:i:s', $end_date))) ?? 0; $ai_visits = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE is_ai_bot = 1 AND visited_at BETWEEN %s AND %s", gmdate('Y-m-d H:i:s', $start_date), gmdate('Y-m-d H:i:s', $end_date))) ?? 0; return ['percentage' => $total_visits > 0 ? round(($ai_visits / $total_visits) * 100, 1) : 0]; } /** * 10. DASHBOARD COMPLET (tous les KPIs en une fois) */ /** * Get AI Referrer Insights * * @param int $days Number of days to look back * @return array AI referrer insights */ public function get_ai_referrer_insights($days = 30) { $cache_key = 'almaweb_referrer_insights_' . $days; $cached = get_transient($cache_key); if (false !== $cached) { return $cached; } $diversity_score = min($this->database->get_referrer_diversity($days), 10); // Déterminer la note et le message de diversité if ($diversity_score >= 8) { $grade = 'A'; $message = __('Excellent diversification of your AI traffic sources.', 'almaweb-ai-visitor-analytics'); } elseif ($diversity_score >= 5) { $grade = 'B'; $message = __('Good diversification. You do not depend on a single major source.', 'almaweb-ai-visitor-analytics'); } elseif ($diversity_score >= 2) { $grade = 'C'; $message = __('Low diversification. Your AI traffic comes from few different sources.', 'almaweb-ai-visitor-analytics'); } else { $grade = 'D'; $message = __('Very low diversification. You are heavily dependent on a single source.', 'almaweb-ai-visitor-analytics'); } $insights = [ 'top_sources' => $this->database->get_top_referrer_sources($days, 10), 'total_visits' => $this->database->get_ai_referrer_total($days), 'landing_pages' => $this->database->get_ai_landing_pages($days, 10), 'timeline' => $this->database->get_referrer_timeline($days), 'diversity_score' => $diversity_score, 'diversity_grade' => $grade, 'diversity_message' => $message, ]; set_transient($cache_key, $insights, 5 * MINUTE_IN_SECONDS); return $insights; } public function get_blocked_stats($days = 30) { global $wpdb; $table_name = $wpdb->prefix . 'ai_monitor_logs'; $date_limit = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $total = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table_name} WHERE is_blocked = 1 AND visited_at >= %s", $date_limit)) ?? 0; $top_family = $wpdb->get_var($wpdb->prepare("SELECT bot_family FROM {$table_name} WHERE is_blocked = 1 AND visited_at >= %s GROUP BY bot_family ORDER BY COUNT(*) DESC LIMIT 1", $date_limit)); return ['total' => (int) $total, 'top_family' => $top_family ?: '-']; } public function get_all_kpis($days = 30) { $training_vs_inference = $this->get_training_vs_inference($days); return [ 'ai_pressure' => $this->get_ai_pressure($days), 'ai_authority_score' => $this->get_ai_authority_score($days), 'training_vs_inference' => $training_vs_inference, 'intention_breakdown' => $this->format_for_chart($training_vs_inference), 'citation_potential' => $this->get_citation_potential($days), 'top_topics' => $this->get_top_topics(10, $days), 'scraping_cost' => $this->get_scraping_cost($days), 'penetration_trend' => $this->get_penetration_trend($days), 'live_visits' => $this->get_live_visits($days), 'hottest_page' => $this->get_hottest_page(7), 'ai_referrer_insights' => $this->get_ai_referrer_insights($days), 'blocked_stats' => $this->get_blocked_stats($days), ]; } private function format_for_chart($data) { $colors = ['inference' => '#10b981', 'search' => '#3b82f6', 'training' => '#f59e0b', 'scraping' => '#ef4444', 'unknown' => '#94a3b8']; $chart_data = ['labels' => [], 'datasets' => [['data' => [], 'backgroundColor' => []]]]; foreach ($data['counts'] as $intention => $count) { if ($count > 0) { $chart_data['labels'][] = $data['labels'][$intention] ?? ucfirst($intention); $chart_data['datasets'][0]['data'][] = $count; $chart_data['datasets'][0]['backgroundColor'][] = $colors[$intention] ?? '#94a3b8'; } } return $chart_data; } }