buildReport(); if ((bool) $this->option('json')) { $this->line(json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ?: '{}'); return self::SUCCESS; } $this->line('Academy Analytics Health Check'); $this->line('=============================='); $this->newLine(); $this->line(sprintf('Events last 24h: %d', $report['events_last_24h'])); $this->line(sprintf('Events last 7d: %d', $report['events_last_7d'])); $this->line(sprintf('Latest event: %s', $report['latest_event_at'] ?? 'none')); $this->line(sprintf('Latest rollup date: %s', $report['latest_rollup_date'] ?? 'none')); $this->line(sprintf('Search logs: %d', $report['search_logs'])); $this->line(sprintf('Search clicks: %d', $report['search_clicks'])); $this->line(sprintf('Likes: %d', $report['likes'])); $this->line(sprintf('Saves: %d', $report['saves'])); $this->line(sprintf('Progress records: %d', $report['progress_records'])); $this->line(sprintf('Prompt copies: %d', $report['prompt_copies'])); $this->line(sprintf('Upgrade clicks: %d', $report['upgrade_clicks'])); $this->line(sprintf('Human events: %d', $report['human_events'])); $this->line(sprintf('Bot/admin events: %d', $report['bot_admin_events'])); $this->line(sprintf('Recent daily metric rows: %d', $report['recent_daily_metric_rows'])); $this->line(sprintf('Raw IP storage detected: %s', $report['raw_ip_storage_detected'] ? 'yes' : 'no')); $this->line(sprintf('Events older than retention: %d', $report['events_older_than_retention'])); $this->newLine(); foreach ($report['warnings'] as $warning) { $this->warn(sprintf('WARNING: %s', $warning)); } $this->info(sprintf('Status: %s', $report['status'])); return self::SUCCESS; } /** * @return array */ private function buildReport(): array { $now = now(); $last24Hours = $now->copy()->subDay(); $last7Days = $now->copy()->subDays(7); $retentionCutoff = $now->copy()->subDays(self::RETENTION_DAYS); $warnings = []; $rawIpStorageDetected = $this->rawIpStorageDetected(); $eventsTableExists = Schema::hasTable('academy_events'); $metricsTableExists = Schema::hasTable('academy_content_metrics_daily'); $searchLogsTableExists = Schema::hasTable('academy_search_logs'); $likesTableExists = Schema::hasTable('academy_likes'); $savesTableExists = Schema::hasTable('academy_saves'); $progressTableExists = Schema::hasTable('academy_user_progress'); $latestEvent = $eventsTableExists ? AcademyEvent::query()->latest('occurred_at')->value('occurred_at') : null; $latestRollup = $metricsTableExists ? AcademyContentMetricDaily::query()->latest('date')->value('date') : null; $searchLogCount = $searchLogsTableExists ? AcademySearchLog::query()->count() : 0; $searchClickCount = $searchLogsTableExists ? AcademySearchLog::query()->whereNotNull('clicked_content_id')->count() : 0; $eventsOlderThanRetention = $eventsTableExists ? AcademyEvent::query()->where('occurred_at', '<', $retentionCutoff)->count() : 0; $recentDailyMetricRows = $metricsTableExists ? AcademyContentMetricDaily::query()->whereBetween('date', [$now->copy()->subDays(6)->toDateString(), $now->toDateString()])->count() : 0; $report = [ 'events_last_24h' => $eventsTableExists ? AcademyEvent::query()->where('occurred_at', '>=', $last24Hours)->count() : 0, 'events_last_7d' => $eventsTableExists ? AcademyEvent::query()->where('occurred_at', '>=', $last7Days)->count() : 0, 'latest_event_at' => $latestEvent ? Carbon::parse((string) $latestEvent)->toDateTimeString() : null, 'latest_rollup_date' => $latestRollup ? Carbon::parse((string) $latestRollup)->toDateString() : null, 'search_logs' => $searchLogCount, 'search_clicks' => $searchClickCount, 'likes' => $likesTableExists ? AcademyLike::query()->count() : 0, 'saves' => $savesTableExists ? AcademySave::query()->count() : 0, 'progress_records' => $progressTableExists ? AcademyUserProgress::query()->count() : 0, 'prompt_copies' => $eventsTableExists ? AcademyEvent::query()->where('event_type', AcademyAnalyticsEventType::PROMPT_COPY)->count() : 0, 'upgrade_clicks' => $eventsTableExists ? AcademyEvent::query()->where('event_type', AcademyAnalyticsEventType::UPGRADE_CLICK)->count() : 0, 'human_events' => $eventsTableExists ? AcademyEvent::query()->where('is_bot', false)->where('is_admin', false)->where('is_suspicious', false)->count() : 0, 'bot_admin_events' => $eventsTableExists ? AcademyEvent::query()->where(function ($query): void { $query->where('is_bot', true)->orWhere('is_admin', true)->orWhere('is_suspicious', true); })->count() : 0, 'raw_ip_storage_detected' => $rawIpStorageDetected, 'events_older_than_retention' => $eventsOlderThanRetention, 'recent_daily_metric_rows' => $recentDailyMetricRows, 'retention_days' => self::RETENTION_DAYS, 'tables_present' => [ 'academy_events' => $eventsTableExists, 'academy_content_metrics_daily' => $metricsTableExists, 'academy_search_logs' => $searchLogsTableExists, 'academy_likes' => $likesTableExists, 'academy_saves' => $savesTableExists, 'academy_user_progress' => $progressTableExists, ], 'warnings' => [], 'status' => 'OK', ]; foreach ($report['tables_present'] as $table => $present) { if (! $present) { $warnings[] = sprintf('Analytics table %s is missing.', $table); } } if ($report['events_last_24h'] === 0) { $warnings[] = 'No events received in last 24 hours.'; } if ($report['events_last_7d'] === 0) { $warnings[] = 'No events received in last 7 days.'; } if ($report['latest_rollup_date'] === null) { $warnings[] = 'No rollup rows exist yet.'; } elseif ($report['latest_rollup_date'] !== $now->toDateString()) { $warnings[] = 'Rollup has not run for today.'; } if ($searchLogCount > 0 && $searchClickCount === 0) { $warnings[] = 'Search clicks are zero although search logs exist.'; } if ($eventsOlderThanRetention > 0) { $warnings[] = 'Raw events older than configured retention period exist.'; } if ($recentDailyMetricRows === 0) { $warnings[] = 'No daily metrics exist for recent days.'; } if ($rawIpStorageDetected) { $warnings[] = 'Raw IP storage indicators were found in Academy analytics tables.'; } $report['warnings'] = $warnings; $report['status'] = $warnings === [] ? 'OK' : 'WARNING'; return $report; } private function rawIpStorageDetected(): bool { foreach (['academy_events', 'academy_search_logs', 'academy_content_metrics_daily', 'academy_likes', 'academy_saves', 'academy_user_progress'] as $table) { if (! Schema::hasTable($table)) { continue; } foreach (['ip', 'ip_address', 'visitor_ip', 'raw_ip', 'remote_addr'] as $column) { if (Schema::hasColumn($table, $column)) { return true; } } } return false; } }