PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-site-health.php

class-metasync-site-health.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.1, at includes/class-metasync-site-health.php

1,393 lines 47.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // If this file is called directly, abort.
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * WordPress Site Health Integration for MetaSync
9 *
10 * Provides comprehensive WordPress cron queue health monitoring across all plugins,
11 * themes, and core functionality. Helps administrators identify queue overflow issues
12 * from any source while showing MetaSync's contribution.
13 *
14 * @link https://searchatlas.com
15 * @since 1.0.0
16 *
17 * @package Metasync
18 * @subpackage Metasync/includes
19 */
20
21 /**
22 * Site Health integration class.
23 *
24 * Monitors the entire WordPress cron system and provides detailed statistics
25 * about pending jobs, recurring events, and plugin-specific contributions.
26 *
27 * @since 1.0.0
28 * @package Metasync
29 * @subpackage Metasync/includes
30 * @author Engineering Team <[email protected]>
31 */
32 class Metasync_Site_Health
33 {
34 /**
35 * Maximum safe cron option size (3MB)
36 * Above this size, we won't attempt to load the cron array
37 */
38 const MAX_SAFE_CRON_SIZE = 3145728; // 3MB
39
40 /**
41 * Maximum jobs to count before early exit
42 * Prevents excessive processing on sites with huge queues
43 */
44 const MAX_COUNT_THRESHOLD = 5000;
45
46 /**
47 * Threshold for recommended status warning
48 * One-time pending jobs above this trigger a warning
49 */
50 const PENDING_JOBS_THRESHOLD = 1000;
51
52 /**
53 * Memory usage threshold for critical status (90%)
54 * Memory usage above this percentage triggers critical warning
55 */
56 const MEMORY_CRITICAL_THRESHOLD = 90;
57
58 /**
59 * OTTO API rate limit hits in 24h that trigger a recommended warning
60 */
61 const RATE_LIMIT_WARNING_THRESHOLD = 5;
62
63 /**
64 * OTTO API rate limit hits in 24h that trigger a critical warning
65 */
66 const RATE_LIMIT_CRITICAL_THRESHOLD = 20;
67
68 /**
69 * Failed MetaSync cron actions in 24h that trigger a recommended warning
70 */
71 const FAILED_ACTIONS_THRESHOLD = 100;
72
73 /**
74 * Initialize the Site Health integration
75 *
76 * @since 1.0.0
77 */
78 public function __construct()
79 {
80 // Register debug information for Site Health Info tab
81 $this->register_debug_information();
82 }
83
84 /**
85 * Register MetaSync debug information for the Site Health Info tab
86 *
87 * @since 1.0.0
88 */
89 private function register_debug_information()
90 {
91 add_filter( 'debug_information', [ $this, 'add_debug_info' ] );
92 }
93
94 /**
95 * Build an HTML anchor tag pointing to a MetaSync admin page.
96 *
97 * Uses the whitelabel-aware static page slug from Metasync_Admin so that
98 * links remain correct on white-labelled installs.
99 *
100 * @since 1.0.0
101 * @param string $path URL fragment appended after the page slug (e.g. '&tab=general' or '-sync-log').
102 * Use an empty string to link to the plugin root page.
103 * @param string $label Visible link text shown to the administrator.
104 * @return string HTML anchor tag, ready to embed in a description string.
105 */
106 private function get_admin_link( string $path, string $label ): string
107 {
108 $url = admin_url( 'admin.php?page=' . Metasync_Admin::$page_slug . $path );
109 return sprintf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( $label ) );
110 }
111
112 /**
113 * Add MetaSync debug information section to the Site Health Info tab
114 *
115 * Registers a debug information group with the whitelabeled plugin name.
116 *
117 * @since 1.0.0
118 * @param array $info Existing debug information
119 * @return array Updated debug information with MetaSync fields
120 */
121 public function add_debug_info( $info )
122 {
123 $plugin_name = Metasync::get_effective_plugin_name();
124
125 $queue_stats = $this->get_queue_stats();
126 $failed_stats = $this->get_failed_actions_stats();
127 $performance_fields = $this->get_performance_settings();
128
129 $sync_db = new Metasync_Sync_History_Database();
130 $sync_stats = $sync_db->get_statistics();
131 $completed_count = isset( $sync_stats->published_count ) ? (int) $sync_stats->published_count : 0;
132
133 $memory_stats = $this->get_memory_stats();
134 $unlimited = $memory_stats['limit_bytes'] === -1;
135
136 $rate_limit_log = get_option( 'metasync_otto_rate_limit_log', [] );
137 $last_backoff_time = ! empty( $rate_limit_log ) ? max( $rate_limit_log ) : null;
138 $last_backoff_value = $last_backoff_time
139 ? wp_date( 'Y-m-d H:i:s', $last_backoff_time ) . ' (' . human_time_diff( $last_backoff_time ) . ' ago)'
140 : __( 'Never', 'metasync' );
141
142 $otto_job_counts = class_exists( 'Metasync_Otto_Job_Status' ) ? Metasync_Otto_Job_Status::counts() : [];
143 $otto_job_fields = [];
144 if ( ! empty( $otto_job_counts ) ) {
145 $otto_job_fields = [
146 'otto_jobs_accepted' => [
147 'label' => __( 'Crawl Jobs - Accepted', 'metasync' ),
148 'value' => (int) $otto_job_counts['accepted'],
149 ],
150 'otto_jobs_retrying' => [
151 'label' => __( 'Crawl Jobs - Retrying', 'metasync' ),
152 'value' => (int) $otto_job_counts['retrying'],
153 ],
154 'otto_jobs_completed' => [
155 'label' => __( 'Crawl Jobs - Completed', 'metasync' ),
156 'value' => (int) $otto_job_counts['completed'],
157 ],
158 'otto_jobs_failed' => [
159 'label' => __( 'Crawl Jobs - Failed', 'metasync' ),
160 'value' => (int) $otto_job_counts['failed'],
161 ],
162 'otto_jobs_pending' => [
163 'label' => __( 'Crawl Jobs - Queued Overflow', 'metasync' ),
164 'value' => (int) $otto_job_counts['pending'],
165 ],
166 ];
167 }
168
169 $info['metasync'] = [
170 'label' => $plugin_name,
171 'fields' => [
172 'version' => [
173 'label' => __( 'Plugin Version', 'metasync' ),
174 'value' => METASYNC_VERSION,
175 ],
176 'queue_pending' => [
177 'label' => __( 'Queue - Pending Jobs', 'metasync' ),
178 'value' => isset( $queue_stats['metasync_pending'] ) ? (int) $queue_stats['metasync_pending'] : 0,
179 ],
180 'queue_completed' => [
181 'label' => __( 'Queue - Completed Jobs', 'metasync' ),
182 'value' => $completed_count,
183 ],
184 'queue_failed_24h' => [
185 'label' => __( 'Queue - Failed Jobs (last 24h)', 'metasync' ),
186 'value' => isset( $failed_stats['count'] ) ? (int) $failed_stats['count'] : 0,
187 ],
188 ] + $performance_fields + $otto_job_fields + [
189 'last_api_backoff' => [
190 'label' => __( 'Last API Backoff Event', 'metasync' ),
191 'value' => $last_backoff_value,
192 ],
193 'memory_limit' => [
194 'label' => __( 'Memory - PHP Limit', 'metasync' ),
195 'value' => $memory_stats['limit_formatted'],
196 ],
197 'memory_current' => [
198 'label' => __( 'Memory - Current Usage', 'metasync' ),
199 'value' => $memory_stats['current_formatted'],
200 ],
201 'memory_peak' => [
202 'label' => __( 'Memory - Peak Usage', 'metasync' ),
203 'value' => $memory_stats['peak_formatted'],
204 ],
205 'memory_percentage' => [
206 'label' => __( 'Memory - Usage Percentage', 'metasync' ),
207 'value' => $unlimited ? __( 'N/A (unlimited)', 'metasync' ) : $memory_stats['percentage'] . '%',
208 ],
209 'memory_available' => [
210 'label' => __( 'Memory - Available', 'metasync' ),
211 'value' => $memory_stats['available_formatted'],
212 ],
213 ],
214 ];
215
216 return $info;
217 }
218
219 /**
220 * Register Site Health tests with WordPress
221 *
222 * Hooks into the site_status_tests filter to add our custom tests.
223 * Uses 'direct' test type which runs immediately when the Site Health page is loaded.
224 *
225 * @since 1.0.0
226 */
227 public function register_tests()
228 {
229 add_filter('site_status_tests', function ($tests) {
230 // WordPress Cron Queue Health Check
231 $tests['direct']['metasync_queue_size'] = [
232 'label' => __('WordPress Cron Queue Health', 'metasync'),
233 'test' => [$this, 'queue_size_check']
234 ];
235
236 // Memory Usage Health Check
237 $tests['direct']['metasync_memory_usage'] = [
238 'label' => __('PHP Memory Usage', 'metasync'),
239 'test' => [$this, 'memory_usage_check']
240 ];
241
242 // OTTO API Rate Limit Health Check
243 $tests['direct']['metasync_otto_rate_limit'] = [
244 /* translators: %s: effective plugin name (whitelabel-aware). */
245 'label' => sprintf(__('%s OTTO API Rate Limit', 'metasync'), Metasync::get_effective_plugin_name()),
246 'test' => [$this, 'otto_rate_limit_check']
247 ];
248
249 // Debug Mode Health Check
250 $tests['direct']['metasync_debug_mode'] = [
251 'label' => __('WordPress Debug Mode', 'metasync'),
252 'test' => [$this, 'debug_mode_check']
253 ];
254
255 // Failed Actions Health Check
256 $tests['direct']['metasync_failed_actions'] = [
257 /* translators: %s: effective plugin name (whitelabel-aware). */
258 'label' => sprintf(__('%s Failed Actions', 'metasync'), Metasync::get_effective_plugin_name()),
259 'test' => [$this, 'failed_actions_check']
260 ];
261
262 // OTTO Crawl Job Reliability Health Check
263 $tests['direct']['metasync_otto_job_reliability'] = [
264 /* translators: %s: OTTO name (whitelabel). */
265 'label' => sprintf(__('%s Crawl Job Reliability', 'metasync'), Metasync::get_whitelabel_otto_name()),
266 'test' => [$this, 'otto_job_reliability_check']
267 ];
268
269 return $tests;
270 });
271 }
272
273 /**
274 * Get the size of the cron option in the database using direct SQL
275 *
276 * This method checks the byte size of the serialized cron data WITHOUT
277 * loading and unserializing it, providing excellent performance even
278 * on sites with bloated cron queues.
279 *
280 * @since 1.0.0
281 * @return int Size in bytes of the cron option_value
282 */
283 private function get_cron_option_size()
284 {
285 global $wpdb;
286
287 $size = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Site Health diagnostic — live on-demand size probe in wp-admin; caching would serve stale sizes
288 $wpdb->prepare(
289 "SELECT LENGTH(option_value) FROM {$wpdb->options} WHERE option_name = %s",
290 'cron'
291 )
292 );
293
294 return (int) $size;
295 }
296
297 /**
298 * Get comprehensive queue statistics with bifurcated breakdown
299 *
300 * Analyzes the entire WordPress cron system and categorizes jobs:
301 * - One-time events (true pending jobs that are removed after execution)
302 * - Recurring events (permanent schedule entries that never get removed)
303 * - MetaSync-specific contribution (subset of the above)
304 *
305 * Uses performance-safe approach with early size check and count limits.
306 *
307 * @since 1.0.0
308 * @return array Array containing statistics or error information
309 */
310 private function get_queue_stats()
311 {
312 // First check size for safety - don't load huge cron arrays
313 $size = $this->get_cron_option_size();
314
315 // If cron data is excessively large (>3MB), don't try to load it
316 if ($size > self::MAX_SAFE_CRON_SIZE) {
317 return [
318 'error' => 'excessive_size',
319 'size' => $size,
320 'one_time' => -1,
321 'recurring' => -1,
322 'metasync_pending' => -1
323 ];
324 }
325
326 // Safe to load the cron array now
327 $cron = _get_cron_array();
328
329 if (empty($cron)) {
330 return [
331 'one_time' => 0,
332 'recurring' => 0,
333 'metasync_pending' => 0
334 ];
335 }
336
337 $one_time = 0;
338 $recurring = 0;
339 $metasync_pending = 0;
340
341 // Loop through ALL WordPress cron jobs (core, plugins, themes, custom)
342 foreach ($cron as $hooks) {
343 foreach ($hooks as $hook => $events) { // ALL hooks from ALL sources
344 foreach ($events as $event) {
345 // Distinguish by schedule property
346 // schedule === false means one-time event (true pending job)
347 // schedule !== false means recurring event (permanent schedule)
348 if ($event['schedule'] === false) {
349 // One-time event (true pending job)
350 $one_time++;
351
352 // Track MetaSync's contribution separately
353 if (strpos($hook, 'metasync_') === 0) {
354 $metasync_pending++;
355 }
356 } else {
357 // Recurring event (permanent schedule entry)
358 $recurring++;
359 }
360
361 // Early exit for performance - stop counting after threshold
362 if ($one_time > self::MAX_COUNT_THRESHOLD) {
363 return [
364 'one_time' => self::MAX_COUNT_THRESHOLD,
365 'recurring' => $recurring,
366 'metasync_pending' => $metasync_pending,
367 'truncated' => true
368 ];
369 }
370 }
371 }
372 }
373
374 return [
375 'one_time' => $one_time,
376 'recurring' => $recurring,
377 'metasync_pending' => $metasync_pending
378 ];
379 }
380
381 /**
382 * Main Site Health test for WordPress cron queue
383 *
384 * Performs comprehensive health check of the entire WordPress cron system.
385 * Returns formatted result array for WordPress Site Health UI.
386 *
387 * Status levels:
388 * - good: <=1,000 pending jobs
389 * - critical: >1,000 pending jobs or cron data excessively large (>3MB)
390 *
391 * @since 1.0.0
392 * @return array Site Health test result
393 */
394 public function queue_size_check()
395 {
396 $stats = $this->get_queue_stats();
397
398 // Handle excessive size error
399 if (isset($stats['error']) && $stats['error'] === 'excessive_size') {
400 return [
401 'label' => __('WordPress cron queue is excessively large', 'metasync'),
402 'status' => 'critical',
403 'badge' => [
404 'label' => Metasync::get_effective_plugin_name(),
405 'color' => 'red'
406 ],
407 'description' => sprintf(
408 /* translators: %s: formatted size of the cron data. */
409 '<p>' . __('The WordPress cron data is excessively large (%s). This can cause severe performance issues including slow page loads, high memory usage, and potential timeouts.', 'metasync') . '</p>' .
410 '<p>' . __('This typically occurs when cron jobs accumulate from deactivated plugins or failed operations.', 'metasync') . '</p>',
411 size_format($stats['size'])
412 ),
413 'actions' => sprintf(
414 '<p><strong>%s</strong></p>' .
415 '<ul>' .
416 '<li>%s</li>' .
417 '<li>%s</li>' .
418 '<li>%s</li>' .
419 '</ul>',
420 __('Recommended Actions:', 'metasync'),
421 __('Use WP-CLI to list and clean up old cron jobs: <code>wp cron event list</code>', 'metasync'),
422 __('Install a cron management plugin to identify and remove orphaned jobs', 'metasync'),
423 __('Consider clearing the cron option and letting WordPress rebuild it from active plugins', 'metasync')
424 ),
425 'test' => 'metasync_queue_size'
426 ];
427 }
428
429 $one_time_count = $stats['one_time'];
430 $recurring_count = $stats['recurring'];
431 $metasync_count = $stats['metasync_pending'];
432 $truncated = isset($stats['truncated']) && $stats['truncated'];
433
434 // Determine status based on TOTAL system-wide ONE-TIME jobs from ALL sources
435 // This includes WordPress core, all plugins, themes, and custom code
436 if ($one_time_count <= self::PENDING_JOBS_THRESHOLD) {
437 $status = 'good';
438 $label = __('WordPress cron queue is healthy', 'metasync');
439 } else {
440 $status = 'critical';
441 $label = __('WordPress cron queue has high pending job count', 'metasync');
442 }
443
444 // Build detailed description showing system-wide statistics
445 $description = sprintf(
446 '<p><strong>%s</strong></p>' .
447 '<ul>' .
448 '<li>%s <strong>%s</strong></li>' .
449 '<li>%s %d</li>' .
450 '<li>%s %d</li>' .
451 '</ul>',
452 __('WordPress Cron Queue Statistics (All Sources):', 'metasync'),
453 __('Total pending jobs (one-time events):', 'metasync'),
454 /* translators: %d: number of pending one-time cron events. */
455 $truncated ? sprintf(__('%d+', 'metasync'), $one_time_count) : number_format($one_time_count),
456 __('Total scheduled events (recurring):', 'metasync'),
457 $recurring_count,
458 /* translators: %s: effective plugin name (whitelabel-aware). */
459 sprintf(__('%s pending jobs:', 'metasync'), Metasync::get_effective_plugin_name()),
460 $metasync_count
461 );
462
463 // Add context about what these numbers mean
464 $description .= sprintf(
465 '<p><em>%s</em></p>',
466 __('Pending jobs are one-time tasks awaiting execution. Recurring events are permanent schedules that run automatically.', 'metasync')
467 );
468
469 // If status is not good, provide actionable recommendations. The
470 // status is only ever 'good' or 'critical' here, so the previous
471 // '=== recommended' comparison made this entire guidance block dead
472 // code — exactly when the queue went critical, operators got no
473 // diagnostics at all.
474 if ($status !== 'good') {
475 $description .= sprintf(
476 '<p><strong>%s</strong></p>' .
477 '<ul>' .
478 '<li>%s</li>' .
479 '<li>%s</li>' .
480 '<li>%s</li>' .
481 '</ul>',
482 __('A high number of pending jobs may indicate:', 'metasync'),
483 __('WordPress cron is not running properly (check if WP-Cron is disabled or blocked)', 'metasync'),
484 __('High volume of scheduled tasks from plugins creating jobs faster than they can be processed', 'metasync'),
485 __('Slow server performance or resource constraints preventing timely job execution', 'metasync')
486 );
487
488 // If MetaSync is a significant contributor, add specific guidance
489 if ($metasync_count > 500) {
490 $description .= sprintf(
491 '<p><strong>%s</strong> %s</p>',
492 /* translators: %s: effective plugin name (whitelabel-aware). */
493 sprintf(__('%s Contribution:', 'metasync'), Metasync::get_effective_plugin_name()),
494 sprintf(
495 /* translators: 1: effective plugin name (whitelabel-aware), 2: formatted job count, 3: OTTO name (whitelabel). */
496 __('%1$s has %2$s pending %3$s SEO processing jobs. This may indicate high crawl volume from %3$s or slow processing. Consider reviewing %3$s crawl settings if this number continues to grow.', 'metasync'),
497 Metasync::get_effective_plugin_name(),
498 number_format($metasync_count),
499 Metasync::get_whitelabel_otto_name()
500 )
501 );
502 }
503
504 // Add helpful actions
505 $description .= sprintf(
506 '<p><strong>%s</strong></p>' .
507 '<ul>' .
508 '<li>%s</li>' .
509 '<li>%s</li>' .
510 '<li>%s</li>' .
511 '</ul>',
512 __('Recommended Actions:', 'metasync'),
513 __('Verify WordPress cron is functioning: Tools → Site Health → Info → WordPress Constants → DISABLE_WP_CRON should be false', 'metasync'),
514 __('Check server logs for cron execution errors or timeouts', 'metasync'),
515 __('Consider using a real system cron instead of WP-Cron for better reliability', 'metasync')
516 );
517
518 $description .= sprintf(
519 '<p>%s %s</p>',
520 __( 'Quick link:', 'metasync' ),
521 sprintf(
522 '<a href="%s">%s</a>',
523 esc_url( admin_url( 'site-health.php?tab=debug' ) ),
524 esc_html( __( 'Site Health Info (check DISABLE_WP_CRON)', 'metasync' ) )
525 )
526 );
527 }
528
529 return [
530 'label' => $label,
531 'status' => $status,
532 'badge' => [
533 'label' => Metasync::get_effective_plugin_name(),
534 'color' => $status === 'good' ? 'green' : 'red'
535 ],
536 'description' => $description,
537 'test' => 'metasync_queue_size'
538 ];
539 }
540
541 /**
542 * Convert PHP memory notation (e.g., "256M", "1G") to bytes
543 *
544 * @since 1.0.0
545 * @param string $value Memory value from PHP configuration
546 * @return int Memory value in bytes
547 */
548 private function convert_to_bytes($value)
549 {
550 $value = trim($value);
551 $last = strtolower($value[strlen($value) - 1]);
552 $value = (int) $value;
553
554 switch ($last) {
555 case 'g':
556 $value *= 1024;
557 // Fall through
558 case 'm':
559 $value *= 1024;
560 // Fall through
561 case 'k':
562 $value *= 1024;
563 break;
564 default:
565 // Value is already in bytes or has no unit suffix
566 break;
567 }
568
569 return $value;
570 }
571
572 /**
573 * Get comprehensive memory usage statistics
574 *
575 * Retrieves PHP memory configuration and current usage, calculating
576 * percentage and providing formatted human-readable values.
577 *
578 * @since 1.0.0
579 * @return array Array containing memory statistics
580 */
581 private function get_memory_stats()
582 {
583 // Get memory limit from PHP configuration
584 $memory_limit = ini_get('memory_limit');
585
586 // Handle unlimited memory (-1)
587 if ($memory_limit === '-1') {
588 return [
589 'limit' => -1,
590 'limit_bytes' => -1,
591 'limit_formatted' => __('Unlimited', 'metasync'),
592 'current' => memory_get_usage(true),
593 'current_formatted' => size_format(memory_get_usage(true)),
594 'peak' => memory_get_peak_usage(true),
595 'peak_formatted' => size_format(memory_get_peak_usage(true)),
596 'percentage' => 0,
597 'available' => -1,
598 'available_formatted' => __('Unlimited', 'metasync')
599 ];
600 }
601
602 // Convert limit to bytes
603 $limit_bytes = $this->convert_to_bytes($memory_limit);
604
605 // Get current memory usage
606 $current_usage = memory_get_usage(true);
607 $peak_usage = memory_get_peak_usage(true);
608
609 // Calculate percentage
610 $percentage = ($current_usage / $limit_bytes) * 100;
611
612 // Calculate available memory
613 $available = $limit_bytes - $current_usage;
614
615 return [
616 'limit' => $memory_limit,
617 'limit_bytes' => $limit_bytes,
618 'limit_formatted' => size_format($limit_bytes),
619 'current' => $current_usage,
620 'current_formatted' => size_format($current_usage),
621 'peak' => $peak_usage,
622 'peak_formatted' => size_format($peak_usage),
623 'percentage' => round($percentage, 2),
624 'available' => $available,
625 'available_formatted' => size_format($available)
626 ];
627 }
628
629 /**
630 * Main Site Health test for PHP memory usage
631 *
632 * Monitors PHP memory consumption and warns when approaching the limit.
633 * Helps prevent out-of-memory errors and performance issues.
634 *
635 * Status levels:
636 * - good: <=90% memory usage
637 * - critical: >90% memory usage
638 *
639 * @since 1.0.0
640 * @return array Site Health test result
641 */
642 public function memory_usage_check()
643 {
644 $stats = $this->get_memory_stats();
645
646 // Handle unlimited memory
647 if ($stats['limit_bytes'] === -1) {
648 return [
649 'label' => __('PHP memory limit is unlimited', 'metasync'),
650 'status' => 'good',
651 'badge' => [
652 'label' => Metasync::get_effective_plugin_name(),
653 'color' => 'green'
654 ],
655 'description' => sprintf(
656 '<p>%s</p>' .
657 '<p><strong>%s</strong></p>' .
658 '<ul>' .
659 '<li>%s %s</li>' .
660 '<li>%s %s</li>' .
661 '</ul>',
662 __('PHP memory limit is set to unlimited. While this prevents memory errors, it may allow poorly optimized code to consume excessive server resources.', 'metasync'),
663 __('Current Memory Usage:', 'metasync'),
664 __('Current usage:', 'metasync'),
665 $stats['current_formatted'],
666 __('Peak usage:', 'metasync'),
667 $stats['peak_formatted']
668 ),
669 'test' => 'metasync_memory_usage'
670 ];
671 }
672
673 $percentage = $stats['percentage'];
674
675 // Determine status based on memory usage percentage
676 if ($percentage > self::MEMORY_CRITICAL_THRESHOLD) {
677 $status = 'critical';
678 $label = __('PHP memory usage is critically high', 'metasync');
679 } else {
680 $status = 'good';
681 $label = __('PHP memory usage is healthy', 'metasync');
682 }
683
684 // Build detailed description
685 $description = sprintf(
686 '<p><strong>%s</strong></p>' .
687 '<ul>' .
688 '<li>%s %s (%s)</li>' .
689 '<li>%s %s</li>' .
690 '<li>%s %s</li>' .
691 '<li>%s <strong>%.2f%%</strong></li>' .
692 '<li>%s %s</li>' .
693 '</ul>',
694 __('PHP Memory Statistics:', 'metasync'),
695 __('Memory limit:', 'metasync'),
696 $stats['limit_formatted'],
697 $stats['limit'],
698 __('Current usage:', 'metasync'),
699 $stats['current_formatted'],
700 __('Peak usage:', 'metasync'),
701 $stats['peak_formatted'],
702 __('Usage percentage:', 'metasync'),
703 $percentage,
704 __('Available memory:', 'metasync'),
705 $stats['available_formatted']
706 );
707
708 // Add context about memory usage
709 if ($status === 'good') {
710 $description .= sprintf(
711 '<p><em>%s</em></p>',
712 __('Memory usage is within acceptable limits. The site has sufficient memory available for normal operations.', 'metasync')
713 );
714 } else {
715 // Critical status - provide actionable recommendations
716 $description .= sprintf(
717 '<p><strong>%s</strong></p>' .
718 '<p>%s</p>' .
719 '<ul>' .
720 '<li>%s</li>' .
721 '<li>%s</li>' .
722 '<li>%s</li>' .
723 '<li>%s</li>' .
724 '</ul>',
725 __('Critical Memory Usage Detected:', 'metasync'),
726 __('PHP memory usage is critically high (>90%). This may cause:', 'metasync'),
727 __('Fatal errors: "Allowed memory size exhausted"', 'metasync'),
728 __('Failed page loads and white screens', 'metasync'),
729 __('Incomplete plugin/theme operations', 'metasync'),
730 __('Performance degradation and slow response times', 'metasync')
731 );
732
733 $description .= sprintf(
734 '<p><strong>%s</strong></p>' .
735 '<ul>' .
736 '<li>%s</li>' .
737 '<li>%s</li>' .
738 '<li>%s</li>' .
739 '<li>%s</li>' .
740 '<li>%s</li>' .
741 '</ul>',
742 __('Recommended Actions:', 'metasync'),
743 sprintf(
744 /* translators: %s: current PHP memory limit. */
745 __('Increase PHP memory limit in wp-config.php: <code>define(\'WP_MEMORY_LIMIT\', \'256M\');</code> (current: %s)', 'metasync'),
746 $stats['limit']
747 ),
748 __('Contact your hosting provider to increase server memory limits', 'metasync'),
749 __('Deactivate unnecessary plugins that consume excessive memory', 'metasync'),
750 __('Optimize images and reduce media library size', 'metasync'),
751 __('Consider upgrading to a hosting plan with more resources', 'metasync')
752 );
753
754 // Add warning if peak usage is also high
755 if ($stats['peak'] > ($stats['limit_bytes'] * 0.95)) {
756 $description .= sprintf(
757 '<p><strong>%s</strong> %s</p>',
758 __('Warning:', 'metasync'),
759 sprintf(
760 /* translators: %s: formatted peak memory usage. */
761 __('Peak memory usage (%s) is very close to the limit. Memory errors may occur during high-traffic periods or resource-intensive operations.', 'metasync'),
762 $stats['peak_formatted']
763 )
764 );
765 }
766
767 $description .= sprintf(
768 '<p>%s %s</p>',
769 __( 'Quick link:', 'metasync' ),
770 sprintf(
771 '<a href="%s">%s</a>',
772 esc_url( admin_url( 'plugins.php' ) ),
773 esc_html( __( 'Manage Plugins (deactivate memory-heavy plugins)', 'metasync' ) )
774 )
775 );
776 }
777
778 return [
779 'label' => $label,
780 'status' => $status,
781 'badge' => [
782 'label' => Metasync::get_effective_plugin_name(),
783 'color' => $status === 'critical' ? 'red' : 'green'
784 ],
785 'description' => $description,
786 'test' => 'metasync_memory_usage'
787 ];
788 }
789
790 /**
791 * Get OTTO API rate limit statistics for the last 24 hours
792 *
793 * Reads the rolling timestamp log written by metasync_record_otto_rate_limit_hit()
794 * and counts how many 429 responses were received in the past 24 hours.
795 *
796 * @since 1.0.0
797 * @return array Array with 'hits_24h' (int) and 'last_hit' (int|null)
798 */
799 private function get_rate_limit_stats()
800 {
801 $log = get_option('metasync_otto_rate_limit_log', []);
802 $cutoff = time() - DAY_IN_SECONDS;
803
804 $recent = array_filter($log, function($t) use ($cutoff) { return $t > $cutoff; });
805
806 return [
807 'hits_24h' => count($recent),
808 'last_hit' => !empty($recent) ? max($recent) : null,
809 ];
810 }
811
812 /**
813 * Main Site Health test for SearchAtlas OTTO API rate limits
814 *
815 * Checks how frequently the OTTO_URL_DETAILS endpoint has returned HTTP 429
816 * (Too Many Requests) in the last 24 hours and surfaces a warning when the
817 * rate limit is being hit too often.
818 *
819 * Status levels:
820 * - good: < 5 hits in the last 24 hours
821 * - recommended: 5–19 hits in the last 24 hours
822 * - critical: >= 20 hits in the last 24 hours
823 *
824 * @since 1.0.0
825 * @return array Site Health test result
826 */
827 public function otto_rate_limit_check()
828 {
829 $stats = $this->get_rate_limit_stats();
830 $hits = $stats['hits_24h'];
831 $last_hit = $stats['last_hit'];
832
833 // Format the last hit timestamp if available
834 $last_hit_text = $last_hit
835 ? sprintf(
836 /* translators: %s: date and time of the last occurrence. */
837 __('Last occurrence: %s', 'metasync'),
838 date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_hit)
839 )
840 : __('No rate limit hits recorded.', 'metasync');
841
842 if ($hits >= self::RATE_LIMIT_CRITICAL_THRESHOLD) {
843 $status = 'critical';
844 $label = sprintf(
845 /* translators: 1: effective plugin name (whitelabel-aware), 2: number of rate limit hits. */
846 __('%1$s OTTO API is being rate limited frequently (%2$d times in 24h)', 'metasync'),
847 Metasync::get_effective_plugin_name(),
848 $hits
849 );
850 } elseif ($hits >= self::RATE_LIMIT_WARNING_THRESHOLD) {
851 $status = 'recommended';
852 $label = sprintf(
853 /* translators: 1: effective plugin name (whitelabel-aware), 2: number of rate limit hits. */
854 __('%1$s OTTO API rate limit hit %2$d times in the last 24 hours', 'metasync'),
855 Metasync::get_effective_plugin_name(),
856 $hits
857 );
858 } else {
859 $status = 'good';
860 /* translators: %s: effective plugin name (whitelabel-aware). */
861 $label = sprintf(__('%s OTTO API rate limits are within normal range', 'metasync'), Metasync::get_effective_plugin_name());
862 }
863
864 // Build description
865 $description = sprintf(
866 '<p><strong>%s</strong></p>' .
867 '<ul>' .
868 '<li>%s <strong>%d</strong></li>' .
869 '<li>%s</li>' .
870 '</ul>',
871 /* translators: %s: OTTO name (whitelabel). */
872 sprintf(__('%s API Rate Limit Statistics (last 24 hours):', 'metasync'), Metasync::get_whitelabel_otto_name()),
873 __('HTTP 429 responses received:', 'metasync'),
874 $hits,
875 $last_hit_text
876 );
877
878 if ($status === 'good') {
879 $description .= sprintf(
880 '<p><em>%s</em></p>',
881 /* translators: %s: OTTO name (whitelabel). */
882 sprintf(__('The %s API is responding normally. No action required.', 'metasync'), Metasync::get_whitelabel_otto_name())
883 );
884 } else {
885 $description .= sprintf(
886 '<p><strong>%s</strong></p>' .
887 '<p>%s</p>' .
888 '<ul>' .
889 '<li>%s</li>' .
890 '<li>%s</li>' .
891 '<li>%s</li>' .
892 '</ul>',
893 /* translators: %s: OTTO name (whitelabel). */
894 sprintf(__('Frequent rate limiting can degrade %s SEO data delivery on your site.', 'metasync'), Metasync::get_whitelabel_otto_name()),
895 __('Recommended Actions:', 'metasync'),
896 /* translators: 1: OTTO name (whitelabel), 2: effective plugin name (whitelabel-aware). */
897 sprintf(__('Review your %1$s crawl frequency settings in the %2$s dashboard to reduce request volume.', 'metasync'), Metasync::get_whitelabel_otto_name(), Metasync::get_effective_plugin_name()),
898 /* translators: %s: OTTO name (whitelabel). */
899 sprintf(__('Check if multiple server processes are triggering %s jobs simultaneously.', 'metasync'), Metasync::get_whitelabel_otto_name()),
900 /* translators: %s: effective plugin name (whitelabel-aware). */
901 sprintf(__('Contact %s support if rate limiting persists despite low crawl volume.', 'metasync'), Metasync::get_effective_plugin_name())
902 );
903
904 $description .= sprintf(
905 '<p>%s %s</p>',
906 __( 'Quick link:', 'metasync' ),
907 $this->get_admin_link( '&tab=general', __( 'Review API Settings', 'metasync' ) )
908 );
909 }
910
911 return [
912 'label' => $label,
913 'status' => $status,
914 'badge' => [
915 'label' => Metasync::get_effective_plugin_name(),
916 'color' => $status === 'critical' ? 'red' : ( $status === 'recommended' ? 'orange' : 'green' ),
917 ],
918 'description' => $description,
919 'test' => 'metasync_otto_rate_limit',
920 ];
921 }
922
923 /**
924 * Get current debug mode status across all relevant constants.
925 *
926 * Reads live PHP constants so the result reflects what PHP is
927 * actually executing, even when wp-config.php was edited manually.
928 *
929 * @since 1.0.0
930 * @return array {
931 * @type bool $wp_debug True if WP_DEBUG is defined and truthy.
932 * @type bool $wp_debug_log True if WP_DEBUG_LOG is defined and truthy.
933 * @type bool $wp_debug_display True if WP_DEBUG_DISPLAY is defined and truthy.
934 * @type bool $metasync_debug True if METASYNC_DEBUG is defined and truthy.
935 * }
936 */
937 private function get_debug_stats()
938 {
939 return [
940 'wp_debug' => defined('WP_DEBUG') && WP_DEBUG,
941 'wp_debug_log' => defined('WP_DEBUG_LOG') && WP_DEBUG_LOG,
942 'wp_debug_display' => defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY,
943 'metasync_debug' => defined('METASYNC_DEBUG') && constant('METASYNC_DEBUG'),
944 ];
945 }
946
947 /**
948 * Main Site Health test for WordPress debug mode.
949 *
950 * Checks whether WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY, or
951 * METASYNC_DEBUG are active. Debug mode is normal in development
952 * but should be disabled on production sites to avoid exposing
953 * server internals to visitors and incurring performance overhead.
954 *
955 * Status levels:
956 * - good: No debug constants are active.
957 * - recommended: One or more debug constants are active.
958 *
959 * The 'recommended' (not 'critical') status is intentional: debug mode
960 * is a configuration choice, not a breakage, and should not alarm users
961 * who are legitimately running a staging environment.
962 *
963 * @since 1.0.0
964 * @return array Site Health test result
965 */
966 public function debug_mode_check()
967 {
968 $stats = $this->get_debug_stats();
969 $any_debug_on = $stats['wp_debug'] || $stats['metasync_debug'];
970
971 // Good state: no debug constants active
972 if ( ! $any_debug_on ) {
973 return [
974 'label' => __('Debug mode is disabled', 'metasync'),
975 'status' => 'good',
976 'badge' => [
977 'label' => Metasync::get_effective_plugin_name(),
978 'color' => 'green',
979 ],
980 'description' => sprintf(
981 '<p>%s</p>',
982 __('WordPress debug mode is not active. This is the recommended configuration for production sites.', 'metasync')
983 ),
984 'test' => 'metasync_debug_mode',
985 ];
986 }
987
988 // Recommended state: one or more debug constants are active
989 if ( $stats['wp_debug_display'] ) {
990 $label = __('Debug mode is enabled with error display active', 'metasync');
991 } else {
992 $label = __('Debug mode is enabled on this site', 'metasync');
993 }
994
995 // Status table showing each flag's current state
996 $description = sprintf(
997 '<p><strong>%s</strong></p>' .
998 '<ul>' .
999 '<li>%s <strong>%s</strong></li>' .
1000 '<li>%s <strong>%s</strong></li>' .
1001 '<li>%s <strong>%s</strong></li>' .
1002 '</ul>',
1003 __('Active Debug Configuration:', 'metasync'),
1004 __('WP_DEBUG:', 'metasync'),
1005 $stats['wp_debug'] ? __('Enabled', 'metasync') : __('Disabled', 'metasync'),
1006 __('WP_DEBUG_LOG:', 'metasync'),
1007 $stats['wp_debug_log'] ? __('Enabled', 'metasync') : __('Disabled', 'metasync'),
1008 __('WP_DEBUG_DISPLAY:', 'metasync'),
1009 $stats['wp_debug_display'] ? __('Enabled', 'metasync') : __('Disabled', 'metasync')
1010 );
1011
1012 // Call out METASYNC_DEBUG if it is separately active
1013 if ( $stats['metasync_debug'] ) {
1014 $description .= sprintf(
1015 '<p>%s</p>',
1016 __('The <code>METASYNC_DEBUG</code> constant is also active. This enables additional MetaSync diagnostic output.', 'metasync')
1017 );
1018 }
1019
1020 // Escalated warning when WP_DEBUG_DISPLAY is on (errors visible to site visitors)
1021 if ( $stats['wp_debug_display'] ) {
1022 $description .= sprintf(
1023 '<p><strong>%s</strong> %s</p>',
1024 __('Warning:', 'metasync'),
1025 __('WP_DEBUG_DISPLAY is active. PHP errors and notices may be visible to your site visitors, potentially exposing file paths, database details, and plugin internals. This should be disabled on any publicly accessible site.', 'metasync')
1026 );
1027 }
1028
1029 $description .= sprintf(
1030 '<p><em>%s</em></p>',
1031 __('Debug mode is intended for development environments. On a production site, disabling it prevents information leakage and removes the performance overhead of error collection.', 'metasync')
1032 );
1033
1034 $description .= sprintf(
1035 '<p><strong>%s</strong></p>' .
1036 '<ul>' .
1037 '<li>%s</li>' .
1038 '<li>%s</li>' .
1039 '</ul>',
1040 __('Recommended Actions:', 'metasync'),
1041 __('Set <code>define(\'WP_DEBUG\', false);</code> in your wp-config.php if this is a live production site.', 'metasync'),
1042 __('If error logging is needed in production, avoid WP_DEBUG_LOG — it writes to <code>wp-content/debug.log</code> which is publicly accessible via browser and can expose file paths, database details, and API keys. Use a dedicated logging solution that writes outside the webroot instead.', 'metasync')
1043 );
1044
1045 $description .= sprintf(
1046 '<p>%s %s</p>',
1047 __( 'Quick link:', 'metasync' ),
1048 $this->get_admin_link( '&tab=advanced', __( 'Advanced Settings', 'metasync' ) )
1049 );
1050
1051 return [
1052 'label' => $label,
1053 'status' => 'recommended',
1054 'badge' => [
1055 'label' => Metasync::get_effective_plugin_name(),
1056 'color' => 'orange',
1057 ],
1058 'description' => $description,
1059 'test' => 'metasync_debug_mode',
1060 ];
1061 }
1062
1063 /**
1064 * Get current performance settings for display in the Site Health Info tab
1065 *
1066 * Reads PHP configuration, WordPress constants, class constants, and scheduled
1067 * event timing to give administrators a single-glance view of the settings that
1068 * most directly affect MetaSync's runtime performance.
1069 *
1070 * @since 1.0.0
1071 * @return array Associative array of performance setting fields ready for the info tab
1072 */
1073 private function get_performance_settings()
1074 {
1075 // WP-Cron mode
1076 $wp_cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
1077 $wp_cron_mode = $wp_cron_disabled
1078 ? __( 'System Cron (DISABLE_WP_CRON = true)', 'metasync' )
1079 : __( 'WP-Cron (default)', 'metasync' );
1080
1081 // PHP max execution time
1082 $max_exec_raw = ini_get( 'max_execution_time' );
1083 $max_exec = $max_exec_raw === '0' || $max_exec_raw === 0
1084 ? __( 'Unlimited (0)', 'metasync' )
1085 : (int) $max_exec_raw . 's';
1086
1087 // Next scheduled transient cleanup
1088 $next_cleanup_ts = wp_next_scheduled( 'metasync_cleanup_transients' );
1089 $next_cleanup_val = $next_cleanup_ts
1090 ? wp_date( 'Y-m-d H:i:s', $next_cleanup_ts ) . ' (in ' . human_time_diff( $next_cleanup_ts ) . ')'
1091 : __( 'Not scheduled', 'metasync' );
1092
1093 // Telemetry flush interval label
1094 $flush_interval = Metasync_Telemetry_Config::QUEUE_FLUSH_INTERVAL;
1095 $schedules = wp_get_schedules();
1096 $flush_interval_label = isset( $schedules[ $flush_interval ]['display'] )
1097 ? $schedules[ $flush_interval ]['display']
1098 : $flush_interval;
1099
1100 // OTTO suggestions cache TTL (stored in seconds, display as minutes)
1101 $otto_cache_ttl_sec = Metasync_Otto_Config::get_otto_cache_ttl_seconds();
1102 $otto_cache_ttl_val = ( $otto_cache_ttl_sec / MINUTE_IN_SECONDS ) . ' ' . __( 'minutes', 'metasync' );
1103
1104 return [
1105 'perf_wp_cron' => [
1106 'label' => __( 'Performance - WP-Cron Mode', 'metasync' ),
1107 'value' => $wp_cron_mode,
1108 ],
1109 'perf_php_max_exec' => [
1110 'label' => __( 'Performance - PHP Max Execution Time', 'metasync' ),
1111 'value' => $max_exec,
1112 ],
1113 'perf_otto_request_timeout' => [
1114 /* translators: %s: OTTO name (whitelabel). */
1115 'label' => sprintf(__('Performance - %s API Request Timeout', 'metasync'), Metasync::get_whitelabel_otto_name()),
1116 'value' => '30s',
1117 ],
1118 'perf_otto_cache_ttl' => [
1119 /* translators: %s: OTTO name (whitelabel). */
1120 'label' => sprintf(__('Performance - %s Suggestions Cache TTL', 'metasync'), Metasync::get_whitelabel_otto_name()),
1121 'value' => $otto_cache_ttl_val,
1122 ],
1123 'perf_otto_max_calls' => [
1124 /* translators: %s: OTTO name (whitelabel). */
1125 'label' => sprintf(__('Performance - %s Max API Calls/Min', 'metasync'), Metasync::get_whitelabel_otto_name()),
1126 'value' => Metasync_Otto_Transient_Cache::MAX_API_CALLS_PER_MINUTE,
1127 ],
1128 'perf_telemetry_queue_max' => [
1129 'label' => __( 'Performance - Telemetry Max Queue Size', 'metasync' ),
1130 'value' => Metasync_Telemetry_Config::MAX_QUEUE_SIZE,
1131 ],
1132 'perf_telemetry_batch' => [
1133 'label' => __( 'Performance - Telemetry Batch Size', 'metasync' ),
1134 'value' => Metasync_Telemetry_Config::QUEUE_BATCH_SIZE,
1135 ],
1136 'perf_telemetry_flush' => [
1137 'label' => __( 'Performance - Telemetry Queue Flush Interval', 'metasync' ),
1138 'value' => $flush_interval_label,
1139 ],
1140 'perf_api_max_retries' => [
1141 'label' => __( 'Performance - API Max Retry Attempts', 'metasync' ),
1142 'value' => Metasync_Telemetry_Config::MAX_RETRY_ATTEMPTS,
1143 ],
1144 'perf_next_cleanup' => [
1145 'label' => __( 'Performance - Next Transient Cleanup', 'metasync' ),
1146 'value' => $next_cleanup_val,
1147 ],
1148 ];
1149 }
1150
1151 /**
1152 * Get failed MetaSync cron action statistics for the last 24 hours
1153 *
1154 * Reads the rolling timestamp log written by metasync_record_failed_action()
1155 * and counts how many failures were recorded in the past 24 hours.
1156 *
1157 * @since 1.0.0
1158 * @return array Array with 'count' (int) and 'last_hit' (int|null)
1159 */
1160 private function get_failed_actions_stats()
1161 {
1162 $log = get_option( 'metasync_failed_actions_log', [] );
1163 $cutoff = time() - DAY_IN_SECONDS;
1164
1165 $recent = array_filter( $log, function( $t ) use ( $cutoff ) { return $t > $cutoff; } );
1166
1167 return [
1168 'count' => count( $recent ),
1169 'last_hit' => ! empty( $recent ) ? max( $recent ) : null,
1170 ];
1171 }
1172
1173 /**
1174 * Main Site Health test for MetaSync failed cron actions
1175 *
1176 * Checks how many MetaSync OTTO SEO processing jobs have failed in the last
1177 * 24 hours and surfaces a warning when failures exceed the threshold.
1178 * Failures are recorded by metasync_record_failed_action() in otto_pixel.php
1179 * when metasync_process_otto_seo_data() encounters an API error or exception.
1180 *
1181 * Status levels:
1182 * - good: <= 100 failures in the last 24 hours
1183 * - recommended: > 100 failures in the last 24 hours
1184 *
1185 * @since 1.0.0
1186 * @return array Site Health test result
1187 */
1188 public function failed_actions_check()
1189 {
1190 $stats = $this->get_failed_actions_stats();
1191 $count = $stats['count'];
1192 $last_hit = $stats['last_hit'];
1193
1194 $last_hit_text = $last_hit
1195 ? sprintf(
1196 /* translators: %s: date and time of the last failure. */
1197 __( 'Last failure: %s', 'metasync' ),
1198 date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_hit )
1199 )
1200 : __( 'No failed actions recorded in the last 24 hours.', 'metasync' );
1201
1202 if ( $count > self::FAILED_ACTIONS_THRESHOLD ) {
1203 $status = 'recommended';
1204 $label = sprintf(
1205 /* translators: 1: effective plugin name (whitelabel-aware), 2: number of failed actions. */
1206 __( '%1$s has %2$d failed SEO processing actions in the last 24 hours', 'metasync' ),
1207 Metasync::get_effective_plugin_name(),
1208 $count
1209 );
1210 } else {
1211 $status = 'good';
1212 /* translators: %s: effective plugin name (whitelabel-aware). */
1213 $label = sprintf(__('%s SEO action queue is healthy', 'metasync'), Metasync::get_effective_plugin_name());
1214 }
1215
1216 $description = sprintf(
1217 '<p><strong>%s</strong></p>' .
1218 '<ul>' .
1219 '<li>%s <strong>%d</strong></li>' .
1220 '<li>%s</li>' .
1221 '</ul>',
1222 /* translators: %s: effective plugin name (whitelabel-aware). */
1223 sprintf(__('%s Failed Actions Statistics (last 24 hours):', 'metasync'), Metasync::get_effective_plugin_name()),
1224 /* translators: %s: OTTO name (whitelabel). */
1225 sprintf(__('Failed %s SEO processing jobs:', 'metasync'), Metasync::get_whitelabel_otto_name()),
1226 $count,
1227 $last_hit_text
1228 );
1229
1230 if ( $status === 'good' ) {
1231 $description .= sprintf(
1232 '<p><em>%s</em></p>',
1233 /* translators: %s: OTTO name (whitelabel). */
1234 sprintf(__('%s SEO processing jobs are completing successfully. No action required.', 'metasync'), Metasync::get_whitelabel_otto_name())
1235 );
1236 } else {
1237 $description .= sprintf(
1238 '<p><strong>%s</strong></p>' .
1239 '<ul>' .
1240 '<li>%s</li>' .
1241 '<li>%s</li>' .
1242 '<li>%s</li>' .
1243 '</ul>',
1244 __( 'Recommended Actions:', 'metasync' ),
1245 /* translators: 1: effective plugin name (whitelabel-aware). */
1246 sprintf(__('Check your %1$s API connection in %1$s → Settings.', 'metasync'), Metasync::get_effective_plugin_name()),
1247 /* translators: %s: OTTO name (whitelabel). */
1248 sprintf(__('Review your PHP error log for %s processing exceptions.', 'metasync'), Metasync::get_whitelabel_otto_name()),
1249 /* translators: 1: OTTO name (whitelabel), 2: effective plugin name (whitelabel-aware). */
1250 sprintf(__('Verify your %1$s UUID is correctly configured and the %2$s API is reachable.', 'metasync'), Metasync::get_whitelabel_otto_name(), Metasync::get_effective_plugin_name())
1251 );
1252
1253 $description .= sprintf(
1254 '<p>%s %s &middot; %s</p>',
1255 __( 'Quick links:', 'metasync' ),
1256 $this->get_admin_link( '&tab=general', __( 'Review API Settings', 'metasync' ) ),
1257 $this->get_admin_link( '-sync-log', __( 'View Changes Log', 'metasync' ) )
1258 );
1259 }
1260
1261 return [
1262 'label' => $label,
1263 'status' => $status,
1264 'badge' => [
1265 'label' => Metasync::get_effective_plugin_name(),
1266 'color' => $status === 'recommended' ? 'orange' : 'green',
1267 ],
1268 'description' => $description,
1269 'test' => 'metasync_failed_actions',
1270 ];
1271 }
1272
1273 /**
1274 * Site Health test for OTTO crawl-notify background job reliability
1275 *
1276 * Reads the bounded per-URL status store (Metasync_Otto_Job_Status) that
1277 * the crawl-notify pipeline writes as its background jobs run, so an
1278 * administrator can see what actually happened to notified URLs — not just
1279 * that the webhook was acknowledged.
1280 *
1281 * Status levels:
1282 * - good: no failed URLs and fewer than the threshold stuck retrying
1283 * - recommended: failed URLs exist or retrying count exceeds the threshold
1284 *
1285 * When WP-Cron is disabled (DISABLE_WP_CRON) without a visible system
1286 * crontab entry the pipeline cannot run at all, so an advisory is always
1287 * included in that case.
1288 *
1289 * @since 1.0.0
1290 * @return array Site Health test result
1291 */
1292 public function otto_job_reliability_check()
1293 {
1294 $otto_name = Metasync::get_whitelabel_otto_name();
1295
1296 if ( ! class_exists( 'Metasync_Otto_Job_Status' ) ) {
1297 return [
1298 /* translators: %s: OTTO name (whitelabel). */
1299 'label' => sprintf( __( '%s crawl job tracking is unavailable', 'metasync' ), $otto_name ),
1300 'status' => 'good',
1301 'badge' => [
1302 'label' => Metasync::get_effective_plugin_name(),
1303 'color' => 'green',
1304 ],
1305 'description' => '<p>' . esc_html__( 'The status store class is not loaded; nothing to report.', 'metasync' ) . '</p>',
1306 'test' => 'metasync_otto_job_reliability',
1307 ];
1308 }
1309
1310 $counts = Metasync_Otto_Job_Status::counts();
1311
1312 $retrying_threshold = 25;
1313 if ( $counts['failed'] > 0 || $counts['retrying'] > $retrying_threshold ) {
1314 $status = 'recommended';
1315 $label = sprintf(
1316 /* translators: 1: OTTO name (whitelabel), 2: number of failed jobs, 3: number of retrying jobs. */
1317 __( '%1$s crawl jobs need attention (%2$d failed, %3$d retrying)', 'metasync' ),
1318 $otto_name,
1319 $counts['failed'],
1320 $counts['retrying']
1321 );
1322 } else {
1323 $status = 'good';
1324 /* translators: %s: OTTO name (whitelabel). */
1325 $label = sprintf( __( '%s crawl jobs are completing normally', 'metasync' ), $otto_name );
1326 }
1327
1328 $description = sprintf(
1329 '<p><strong>%s</strong></p><ul><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li><li>%s <strong>%d</strong></li></ul>',
1330 /* translators: %s: OTTO name (whitelabel). */
1331 sprintf( __( '%s crawl job status (last 48 hours):', 'metasync' ), $otto_name ),
1332 __( 'Accepted, awaiting run:', 'metasync' ),
1333 $counts['accepted'],
1334 __( 'Retrying after transient failures:', 'metasync' ),
1335 $counts['retrying'],
1336 __( 'Completed:', 'metasync' ),
1337 $counts['completed'],
1338 __( 'Failed (retries exhausted):', 'metasync' ),
1339 $counts['failed'],
1340 __( 'Queued past the per-batch cap:', 'metasync' ),
1341 $counts['pending']
1342 );
1343
1344 if ( $this->constant_is_truthy( 'DISABLE_WP_CRON' ) ) {
1345 $description .= sprintf(
1346 '<p><strong>%s</strong> %s</p>',
1347 __( 'WP-Cron is disabled on this site.', 'metasync' ),
1348 sprintf(
1349 /* translators: %s: OTTO product name */
1350 __( 'Background jobs only run if a system crontab (or equivalent) calls wp-cron.php. Without it, %s crawl notifications are queued but never processed.', 'metasync' ),
1351 $otto_name
1352 )
1353 );
1354 }
1355
1356 if ( $status !== 'good' ) {
1357 $description .= sprintf(
1358 '<p>%s %s &middot; %s</p>',
1359 __( 'Quick links:', 'metasync' ),
1360 $this->get_admin_link( '&tab=general', __( 'Review API Settings', 'metasync' ) ),
1361 $this->get_admin_link( '-sync-log', __( 'View Changes Log', 'metasync' ) )
1362 );
1363 }
1364
1365 return [
1366 'label' => $label,
1367 'status' => $status,
1368 'badge' => [
1369 'label' => Metasync::get_effective_plugin_name(),
1370 'color' => $status === 'recommended' ? 'orange' : 'green',
1371 ],
1372 'description' => $description,
1373 'test' => 'metasync_otto_job_reliability',
1374 ];
1375 }
1376
1377 /**
1378 * Whether a named constant is defined and evaluates truthy.
1379 *
1380 * The constant name arrives as data so the value is resolved through
1381 * constant() at runtime; some constants are also loaded from WP stubs
1382 * with fixed values, which makes a direct `defined() && CONST` check
1383 * statically redundant even though the live value varies per site.
1384 *
1385 * @since 1.0.0
1386 * @param string $name Constant name, without the leading backslash.
1387 * @return bool
1388 */
1389 private function constant_is_truthy( $name ) {
1390 return defined( $name ) && filter_var( constant( $name ), FILTER_VALIDATE_BOOLEAN );
1391 }
1392 }
1393