Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 98 |
|
0.00% |
0 / 2 |
CRAP | |
0.00% |
0 / 1 |
| InvestigateArticleCommand | |
0.00% |
0 / 98 |
|
0.00% |
0 / 2 |
156 | |
0.00% |
0 / 1 |
| buildOptionParser | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 91 |
|
0.00% |
0 / 1 |
132 | |||
| 1 | <?php |
| 2 | declare(strict_types=1); |
| 3 | |
| 4 | namespace App\Command; |
| 5 | |
| 6 | use Cake\Command\Command; |
| 7 | use Cake\Console\Arguments; |
| 8 | use Cake\Console\ConsoleIo; |
| 9 | use Cake\Console\ConsoleOptionParser; |
| 10 | use Cake\ORM\TableRegistry; |
| 11 | use Exception; |
| 12 | |
| 13 | /** |
| 14 | * InvestigateArticle command to debug translation and SEO issues. |
| 15 | * |
| 16 | * This command provides comprehensive debugging information for articles that may have |
| 17 | * translation or SEO generation issues. It checks multiple data sources to help identify |
| 18 | * why AI jobs might have failed or not been triggered. |
| 19 | * |
| 20 | * ## Usage Examples: |
| 21 | * |
| 22 | * ```bash |
| 23 | * # Basic usage - investigate a specific article |
| 24 | * bin/cake investigate_article my-article-slug |
| 25 | * |
| 26 | * # Using Docker (recommended for development) |
| 27 | * docker compose exec willowcms bin/cake investigate_article my-article-slug |
| 28 | * ``` |
| 29 | * |
| 30 | * ## What This Command Checks: |
| 31 | * |
| 32 | * 1. **Article Basic Info**: Verifies the article exists and shows core metadata |
| 33 | * 2. **Translation Status**: Checks if translations exist in articles_translations table |
| 34 | * 3. **Translation Logs**: Searches system_logs for translation job activities and errors |
| 35 | * 4. **SEO Logs**: Searches system_logs for SEO generation job activities and errors |
| 36 | * 5. **Queue Jobs**: Checks for pending or failed queue jobs related to the article |
| 37 | * |
| 38 | * ## Common Issues This Helps Diagnose: |
| 39 | * |
| 40 | * - Articles not appearing in other languages (translation missing) |
| 41 | * - Empty SEO fields (meta_title, meta_description, etc.) |
| 42 | * - AI jobs that were queued but never completed |
| 43 | * - Failed API calls to Anthropic or Google Translate |
| 44 | * - Queue worker not running when article was published |
| 45 | * |
| 46 | * ## Prerequisites: |
| 47 | * |
| 48 | * - Queue worker should be running: `bin/cake queue worker --verbose` |
| 49 | * - AI settings must be enabled in admin area |
| 50 | * - Valid API keys for Anthropic and/or Google Translate |
| 51 | * |
| 52 | * @since 1.0.0 |
| 53 | */ |
| 54 | class InvestigateArticleCommand extends Command |
| 55 | { |
| 56 | /** |
| 57 | * Hook method for defining this command's option parser. |
| 58 | * |
| 59 | * Configures the command to accept a required 'slug' argument which identifies |
| 60 | * the article to investigate. The slug is the URL-friendly identifier used |
| 61 | * in article URLs (e.g., 'my-article-title' from '/en/articles/my-article-title'). |
| 62 | * |
| 63 | * @param \Cake\Console\ConsoleOptionParser $parser The parser to be defined |
| 64 | * @return \Cake\Console\ConsoleOptionParser The built parser. |
| 65 | */ |
| 66 | public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser |
| 67 | { |
| 68 | $parser |
| 69 | ->setDescription('Investigate article translation and SEO generation issues') |
| 70 | ->addArgument('slug', [ |
| 71 | 'help' => 'The slug of the article to investigate', |
| 72 | 'required' => true, |
| 73 | ]); |
| 74 | |
| 75 | return $parser; |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * Execute the investigation command. |
| 80 | * |
| 81 | * Performs a comprehensive analysis of an article's AI processing status by: |
| 82 | * 1. Locating the article by slug |
| 83 | * 2. Checking for existing translations in all configured locales |
| 84 | * 3. Reviewing system logs for translation job activity and errors |
| 85 | * 4. Reviewing system logs for SEO generation job activity and errors |
| 86 | * 5. Checking queue_jobs table for pending/failed jobs |
| 87 | * |
| 88 | * ## Output Sections: |
| 89 | * - **ARTICLE FOUND**: Basic article metadata and verification |
| 90 | * - **EXISTING TRANSLATIONS**: Shows translations in articles_translations table |
| 91 | * - **SYSTEM LOGS (Translation related)**: Recent logs containing translation keywords |
| 92 | * - **SYSTEM LOGS (SEO related)**: Recent logs containing SEO generation keywords |
| 93 | * - **PENDING QUEUE JOBS**: Active/failed jobs in the queue system |
| 94 | * |
| 95 | * ## Return Codes: |
| 96 | * - `Command::CODE_SUCCESS` (0): Investigation completed successfully |
| 97 | * - `Command::CODE_ERROR` (1): Article not found or exception occurred |
| 98 | * |
| 99 | * @param \Cake\Console\Arguments $args The command arguments containing the article slug |
| 100 | * @param \Cake\Console\ConsoleIo $io Console I/O for output formatting and display |
| 101 | * @return int The exit code indicating success or failure |
| 102 | */ |
| 103 | public function execute(Arguments $args, ConsoleIo $io): ?int |
| 104 | { |
| 105 | $slug = $args->getArgument('slug'); |
| 106 | |
| 107 | $io->out("Investigating article with slug: $slug"); |
| 108 | $io->hr(); |
| 109 | |
| 110 | try { |
| 111 | // 1. Find the article by slug |
| 112 | $articles = TableRegistry::getTableLocator()->get('Articles'); |
| 113 | $article = $articles->find()->where(['slug' => $slug])->first(); |
| 114 | |
| 115 | if (!$article) { |
| 116 | $io->error("Article with slug '$slug' not found"); |
| 117 | |
| 118 | return static::CODE_ERROR; |
| 119 | } |
| 120 | |
| 121 | $io->out('=== ARTICLE FOUND ==='); |
| 122 | $io->out('ID: ' . $article->id); |
| 123 | $io->out('Title: ' . $article->title); |
| 124 | $io->out('Slug: ' . $article->slug); |
| 125 | $io->out('Locale: ' . $article->locale); |
| 126 | $io->out('Created: ' . $article->created); |
| 127 | $io->out('Modified: ' . $article->modified); |
| 128 | $io->out(''); |
| 129 | |
| 130 | // 2. Check for translations in articles_translations table |
| 131 | // Note: Uses TranslateBehavior structure where translations are stored separately |
| 132 | $translations = TableRegistry::getTableLocator()->get('ArticlesTranslations'); |
| 133 | $existingTranslations = $translations->find() |
| 134 | ->where(['id' => $article->id]) |
| 135 | ->toArray(); |
| 136 | |
| 137 | $io->out('=== EXISTING TRANSLATIONS ==='); |
| 138 | if (empty($existingTranslations)) { |
| 139 | $io->out('No translations found'); |
| 140 | } else { |
| 141 | foreach ($existingTranslations as $translation) { |
| 142 | $io->out('Locale: ' . $translation->locale); |
| 143 | $io->out('Field: ' . $translation->field); |
| 144 | $io->out('Content: ' . substr($translation->content, 0, 100) . '...'); |
| 145 | $io->out('---'); |
| 146 | } |
| 147 | } |
| 148 | $io->out(''); |
| 149 | |
| 150 | // 3. Search system logs for translation-related activities and errors |
| 151 | // This includes job queuing, processing, completion, and failure logs |
| 152 | $systemLogs = TableRegistry::getTableLocator()->get('SystemLogs'); |
| 153 | $translationErrors = $systemLogs->find() |
| 154 | ->where(function ($exp) use ($article) { |
| 155 | return $exp->or([ |
| 156 | $exp->like('message', '%TranslateArticleJob%'), |
| 157 | $exp->like('message', '%translation%'), |
| 158 | $exp->like('message', '%' . $article->id . '%'), |
| 159 | ]); |
| 160 | }) |
| 161 | ->orderByDesc('created') |
| 162 | ->limit(10) |
| 163 | ->toArray(); |
| 164 | |
| 165 | $io->out('=== SYSTEM LOGS (Translation related) ==='); |
| 166 | if (empty($translationErrors)) { |
| 167 | $io->out('No translation-related log entries found'); |
| 168 | } else { |
| 169 | foreach ($translationErrors as $log) { |
| 170 | $io->out('Time: ' . $log->created); |
| 171 | $io->out('Level: ' . $log->level); |
| 172 | $io->out('Message: ' . $log->message); |
| 173 | $io->out('---'); |
| 174 | } |
| 175 | } |
| 176 | $io->out(''); |
| 177 | |
| 178 | // 4. Search system logs for SEO generation activities and errors |
| 179 | // Includes ArticleSeoUpdateJob processing and AI-powered SEO content generation |
| 180 | $seoErrors = $systemLogs->find() |
| 181 | ->where(function ($exp) { |
| 182 | return $exp->or([ |
| 183 | $exp->like('message', '%ArticleSeoUpdateJob%'), |
| 184 | $exp->like('message', '%SEO%'), |
| 185 | $exp->like('message', '%seo%'), |
| 186 | ]); |
| 187 | }) |
| 188 | ->orderByDesc('created') |
| 189 | ->limit(10) |
| 190 | ->toArray(); |
| 191 | |
| 192 | $io->out('=== SYSTEM LOGS (SEO related) ==='); |
| 193 | if (empty($seoErrors)) { |
| 194 | $io->out('No SEO-related log entries found'); |
| 195 | } else { |
| 196 | foreach ($seoErrors as $log) { |
| 197 | $io->out('Time: ' . $log->created); |
| 198 | $io->out('Level: ' . $log->level); |
| 199 | $io->out('Message: ' . $log->message); |
| 200 | $io->out('---'); |
| 201 | } |
| 202 | } |
| 203 | $io->out(''); |
| 204 | |
| 205 | // 5. Check queue_jobs table for pending, processing, or failed jobs |
| 206 | // This helps identify if jobs are stuck in the queue or failed to process |
| 207 | $connection = $articles->getConnection(); |
| 208 | $queueJobs = $connection->execute( |
| 209 | 'SELECT * FROM queue_jobs WHERE payload LIKE ? OR payload LIKE ? ORDER BY created DESC LIMIT 10', |
| 210 | ['%' . $article->id . '%', '%TranslateArticleJob%'], |
| 211 | )->fetchAll(); |
| 212 | |
| 213 | $io->out('=== PENDING QUEUE JOBS ==='); |
| 214 | if (empty($queueJobs)) { |
| 215 | $io->out('No pending queue jobs found for this article'); |
| 216 | } else { |
| 217 | foreach ($queueJobs as $job) { |
| 218 | $io->out('ID: ' . $job['id']); |
| 219 | $io->out('Status: ' . $job['status']); |
| 220 | $io->out('Queue: ' . $job['queue']); |
| 221 | $io->out('Job Type: ' . $job['job_type']); |
| 222 | $io->out('Created: ' . $job['created']); |
| 223 | $io->out('Payload excerpt: ' . substr($job['payload'], 0, 200) . '...'); |
| 224 | $io->out('---'); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | return static::CODE_SUCCESS; |
| 229 | } catch (Exception $e) { |
| 230 | $io->error('Error: ' . $e->getMessage()); |
| 231 | $io->error('Trace: ' . $e->getTraceAsString()); |
| 232 | |
| 233 | return static::CODE_ERROR; |
| 234 | } |
| 235 | } |
| 236 | } |