PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.10
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.10
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-site-health.php

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

1,201 lines 36.5 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 <support@searchatlas.com>
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' );
141
142 $info['metasync'] = [
143 'label' => $plugin_name,
144 'fields' => [
145 'version' => [
146 'label' => __( 'Plugin Version' ),
147 'value' => METASYNC_VERSION,
148 ],
149 'queue_pending' => [
150 'label' => __( 'Queue - Pending Jobs' ),
151 'value' => isset( $queue_stats['metasync_pending'] ) ? (int) $queue_stats['metasync_pending'] : 0,
152 ],
153 'queue_completed' => [
154 'label' => __( 'Queue - Completed Jobs' ),
155 'value' => $completed_count,
156 ],
157 'queue_failed_24h' => [
158 'label' => __( 'Queue - Failed Jobs (last 24h)' ),
159 'value' => isset( $failed_stats['count'] ) ? (int) $failed_stats['count'] : 0,
160 ],
161 ] + $performance_fields + [
162 'last_api_backoff' => [
163 'label' => __( 'Last API Backoff Event' ),
164 'value' => $last_backoff_value,
165 ],
166 'memory_limit' => [
167 'label' => __( 'Memory - PHP Limit' ),
168 'value' => $memory_stats['limit_formatted'],
169 ],
170 'memory_current' => [
171 'label' => __( 'Memory - Current Usage' ),
172 'value' => $memory_stats['current_formatted'],
173 ],
174 'memory_peak' => [
175 'label' => __( 'Memory - Peak Usage' ),
176 'value' => $memory_stats['peak_formatted'],
177 ],
178 'memory_percentage' => [
179 'label' => __( 'Memory - Usage Percentage' ),
180 'value' => $unlimited ? __( 'N/A (unlimited)' ) : $memory_stats['percentage'] . '%',
181 ],
182 'memory_available' => [
183 'label' => __( 'Memory - Available' ),
184 'value' => $memory_stats['available_formatted'],
185 ],
186 ],
187 ];
188
189 return $info;
190 }
191
192 /**
193 * Register Site Health tests with WordPress
194 *
195 * Hooks into the site_status_tests filter to add our custom tests.
196 * Uses 'direct' test type which runs immediately when the Site Health page is loaded.
197 *
198 * @since 1.0.0
199 */
200 public function register_tests()
201 {
202 add_filter('site_status_tests', function ($tests) {
203 // WordPress Cron Queue Health Check
204 $tests['direct']['metasync_queue_size'] = [
205 'label' => __('WordPress Cron Queue Health'),
206 'test' => [$this, 'queue_size_check']
207 ];
208
209 // Memory Usage Health Check
210 $tests['direct']['metasync_memory_usage'] = [
211 'label' => __('PHP Memory Usage'),
212 'test' => [$this, 'memory_usage_check']
213 ];
214
215 // OTTO API Rate Limit Health Check
216 $tests['direct']['metasync_otto_rate_limit'] = [
217 'label' => sprintf(__('%s OTTO API Rate Limit'), Metasync::get_effective_plugin_name()),
218 'test' => [$this, 'otto_rate_limit_check']
219 ];
220
221 // Debug Mode Health Check
222 $tests['direct']['metasync_debug_mode'] = [
223 'label' => __('WordPress Debug Mode'),
224 'test' => [$this, 'debug_mode_check']
225 ];
226
227 // Failed Actions Health Check
228 $tests['direct']['metasync_failed_actions'] = [
229 'label' => __('MetaSync Failed Actions'),
230 'test' => [$this, 'failed_actions_check']
231 ];
232
233 return $tests;
234 });
235 }
236
237 /**
238 * Get the size of the cron option in the database using direct SQL
239 *
240 * This method checks the byte size of the serialized cron data WITHOUT
241 * loading and unserializing it, providing excellent performance even
242 * on sites with bloated cron queues.
243 *
244 * @since 1.0.0
245 * @return int Size in bytes of the cron option_value
246 */
247 private function get_cron_option_size()
248 {
249 global $wpdb;
250
251 $size = $wpdb->get_var(
252 $wpdb->prepare(
253 "SELECT LENGTH(option_value) FROM {$wpdb->options} WHERE option_name = %s",
254 'cron'
255 )
256 );
257
258 return (int) $size;
259 }
260
261 /**
262 * Get comprehensive queue statistics with bifurcated breakdown
263 *
264 * Analyzes the entire WordPress cron system and categorizes jobs:
265 * - One-time events (true pending jobs that are removed after execution)
266 * - Recurring events (permanent schedule entries that never get removed)
267 * - MetaSync-specific contribution (subset of the above)
268 *
269 * Uses performance-safe approach with early size check and count limits.
270 *
271 * @since 1.0.0
272 * @return array Array containing statistics or error information
273 */
274 private function get_queue_stats()
275 {
276 // First check size for safety - don't load huge cron arrays
277 $size = $this->get_cron_option_size();
278
279 // If cron data is excessively large (>3MB), don't try to load it
280 if ($size > self::MAX_SAFE_CRON_SIZE) {
281 return [
282 'error' => 'excessive_size',
283 'size' => $size,
284 'one_time' => -1,
285 'recurring' => -1,
286 'metasync_pending' => -1
287 ];
288 }
289
290 // Safe to load the cron array now
291 $cron = _get_cron_array();
292
293 if (empty($cron)) {
294 return [
295 'one_time' => 0,
296 'recurring' => 0,
297 'metasync_pending' => 0
298 ];
299 }
300
301 $one_time = 0;
302 $recurring = 0;
303 $metasync_pending = 0;
304
305 // Loop through ALL WordPress cron jobs (core, plugins, themes, custom)
306 foreach ($cron as $hooks) {
307 foreach ($hooks as $hook => $events) { // ALL hooks from ALL sources
308 foreach ($events as $event) {
309 // Distinguish by schedule property
310 // schedule === false means one-time event (true pending job)
311 // schedule !== false means recurring event (permanent schedule)
312 if ($event['schedule'] === false) {
313 // One-time event (true pending job)
314 $one_time++;
315
316 // Track MetaSync's contribution separately
317 if (strpos($hook, 'metasync_') === 0) {
318 $metasync_pending++;
319 }
320 } else {
321 // Recurring event (permanent schedule entry)
322 $recurring++;
323 }
324
325 // Early exit for performance - stop counting after threshold
326 if ($one_time > self::MAX_COUNT_THRESHOLD) {
327 return [
328 'one_time' => self::MAX_COUNT_THRESHOLD,
329 'recurring' => $recurring,
330 'metasync_pending' => $metasync_pending,
331 'truncated' => true
332 ];
333 }
334 }
335 }
336 }
337
338 return [
339 'one_time' => $one_time,
340 'recurring' => $recurring,
341 'metasync_pending' => $metasync_pending
342 ];
343 }
344
345 /**
346 * Main Site Health test for WordPress cron queue
347 *
348 * Performs comprehensive health check of the entire WordPress cron system.
349 * Returns formatted result array for WordPress Site Health UI.
350 *
351 * Status levels:
352 * - good: <=1,000 pending jobs
353 * - critical: >1,000 pending jobs or cron data excessively large (>3MB)
354 *
355 * @since 1.0.0
356 * @return array Site Health test result
357 */
358 public function queue_size_check()
359 {
360 $stats = $this->get_queue_stats();
361
362 // Handle excessive size error
363 if (isset($stats['error']) && $stats['error'] === 'excessive_size') {
364 return [
365 'label' => __('WordPress cron queue is excessively large'),
366 'status' => 'critical',
367 'badge' => [
368 'label' => __('MetaSync'),
369 'color' => 'red'
370 ],
371 'description' => sprintf(
372 '<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.') . '</p>' .
373 '<p>' . __('This typically occurs when cron jobs accumulate from deactivated plugins or failed operations.') . '</p>',
374 size_format($stats['size'])
375 ),
376 'actions' => sprintf(
377 '<p><strong>%s</strong></p>' .
378 '<ul>' .
379 '<li>%s</li>' .
380 '<li>%s</li>' .
381 '<li>%s</li>' .
382 '</ul>',
383 __('Recommended Actions:'),
384 __('Use WP-CLI to list and clean up old cron jobs: <code>wp cron event list</code>'),
385 __('Install a cron management plugin to identify and remove orphaned jobs'),
386 __('Consider clearing the cron option and letting WordPress rebuild it from active plugins')
387 ),
388 'test' => 'metasync_queue_size'
389 ];
390 }
391
392 $one_time_count = $stats['one_time'];
393 $recurring_count = $stats['recurring'];
394 $metasync_count = $stats['metasync_pending'];
395 $truncated = isset($stats['truncated']) && $stats['truncated'];
396
397 // Determine status based on TOTAL system-wide ONE-TIME jobs from ALL sources
398 // This includes WordPress core, all plugins, themes, and custom code
399 if ($one_time_count <= self::PENDING_JOBS_THRESHOLD) {
400 $status = 'good';
401 $label = __('WordPress cron queue is healthy');
402 } else {
403 $status = 'critical';
404 $label = __('WordPress cron queue has high pending job count');
405 }
406
407 // Build detailed description showing system-wide statistics
408 $description = sprintf(
409 '<p><strong>%s</strong></p>' .
410 '<ul>' .
411 '<li>%s <strong>%s</strong></li>' .
412 '<li>%s %d</li>' .
413 '<li>%s %d</li>' .
414 '</ul>',
415 __('WordPress Cron Queue Statistics (All Sources):'),
416 __('Total pending jobs (one-time events):'),
417 $truncated ? sprintf(__('%d+'), $one_time_count) : number_format($one_time_count),
418 __('Total scheduled events (recurring):'),
419 $recurring_count,
420 __('MetaSync pending jobs:'),
421 $metasync_count
422 );
423
424 // Add context about what these numbers mean
425 $description .= sprintf(
426 '<p><em>%s</em></p>',
427 __('Pending jobs are one-time tasks awaiting execution. Recurring events are permanent schedules that run automatically.')
428 );
429
430 // If status is not good, provide actionable recommendations
431 if ($status === 'recommended') {
432 $description .= sprintf(
433 '<p><strong>%s</strong></p>' .
434 '<ul>' .
435 '<li>%s</li>' .
436 '<li>%s</li>' .
437 '<li>%s</li>' .
438 '</ul>',
439 __('A high number of pending jobs may indicate:'),
440 __('WordPress cron is not running properly (check if WP-Cron is disabled or blocked)'),
441 __('High volume of scheduled tasks from plugins creating jobs faster than they can be processed'),
442 __('Slow server performance or resource constraints preventing timely job execution')
443 );
444
445 // If MetaSync is a significant contributor, add specific guidance
446 if ($metasync_count > 500) {
447 $description .= sprintf(
448 '<p><strong>%s</strong> %s</p>',
449 __('MetaSync Contribution:'),
450 sprintf(
451 __('MetaSync has %s pending OTTO SEO processing jobs. This may indicate high crawl volume from OTTO or slow processing. Consider reviewing OTTO crawl settings if this number continues to grow.'),
452 number_format($metasync_count)
453 )
454 );
455 }
456
457 // Add helpful actions
458 $description .= sprintf(
459 '<p><strong>%s</strong></p>' .
460 '<ul>' .
461 '<li>%s</li>' .
462 '<li>%s</li>' .
463 '<li>%s</li>' .
464 '</ul>',
465 __('Recommended Actions:'),
466 __('Verify WordPress cron is functioning: Tools → Site Health → Info → WordPress Constants → DISABLE_WP_CRON should be false'),
467 __('Check server logs for cron execution errors or timeouts'),
468 __('Consider using a real system cron instead of WP-Cron for better reliability')
469 );
470
471 $description .= sprintf(
472 '<p>%s %s</p>',
473 __( 'Quick link:' ),
474 sprintf(
475 '<a href="%s">%s</a>',
476 esc_url( admin_url( 'site-health.php?tab=debug' ) ),
477 esc_html( __( 'Site Health Info (check DISABLE_WP_CRON)' ) )
478 )
479 );
480 }
481
482 return [
483 'label' => $label,
484 'status' => $status,
485 'badge' => [
486 'label' => __('MetaSync'),
487 'color' => $status === 'good' ? 'green' : 'red'
488 ],
489 'description' => $description,
490 'test' => 'metasync_queue_size'
491 ];
492 }
493
494 /**
495 * Convert PHP memory notation (e.g., "256M", "1G") to bytes
496 *
497 * @since 1.0.0
498 * @param string $value Memory value from PHP configuration
499 * @return int Memory value in bytes
500 */
501 private function convert_to_bytes($value)
502 {
503 $value = trim($value);
504 $last = strtolower($value[strlen($value) - 1]);
505 $value = (int) $value;
506
507 switch ($last) {
508 case 'g':
509 $value *= 1024;
510 // Fall through
511 case 'm':
512 $value *= 1024;
513 // Fall through
514 case 'k':
515 $value *= 1024;
516 break;
517 default:
518 // Value is already in bytes or has no unit suffix
519 break;
520 }
521
522 return $value;
523 }
524
525 /**
526 * Get comprehensive memory usage statistics
527 *
528 * Retrieves PHP memory configuration and current usage, calculating
529 * percentage and providing formatted human-readable values.
530 *
531 * @since 1.0.0
532 * @return array Array containing memory statistics
533 */
534 private function get_memory_stats()
535 {
536 // Get memory limit from PHP configuration
537 $memory_limit = ini_get('memory_limit');
538
539 // Handle unlimited memory (-1)
540 if ($memory_limit === '-1') {
541 return [
542 'limit' => -1,
543 'limit_bytes' => -1,
544 'limit_formatted' => __('Unlimited'),
545 'current' => memory_get_usage(true),
546 'current_formatted' => size_format(memory_get_usage(true)),
547 'peak' => memory_get_peak_usage(true),
548 'peak_formatted' => size_format(memory_get_peak_usage(true)),
549 'percentage' => 0,
550 'available' => -1,
551 'available_formatted' => __('Unlimited')
552 ];
553 }
554
555 // Convert limit to bytes
556 $limit_bytes = $this->convert_to_bytes($memory_limit);
557
558 // Get current memory usage
559 $current_usage = memory_get_usage(true);
560 $peak_usage = memory_get_peak_usage(true);
561
562 // Calculate percentage
563 $percentage = ($current_usage / $limit_bytes) * 100;
564
565 // Calculate available memory
566 $available = $limit_bytes - $current_usage;
567
568 return [
569 'limit' => $memory_limit,
570 'limit_bytes' => $limit_bytes,
571 'limit_formatted' => size_format($limit_bytes),
572 'current' => $current_usage,
573 'current_formatted' => size_format($current_usage),
574 'peak' => $peak_usage,
575 'peak_formatted' => size_format($peak_usage),
576 'percentage' => round($percentage, 2),
577 'available' => $available,
578 'available_formatted' => size_format($available)
579 ];
580 }
581
582 /**
583 * Main Site Health test for PHP memory usage
584 *
585 * Monitors PHP memory consumption and warns when approaching the limit.
586 * Helps prevent out-of-memory errors and performance issues.
587 *
588 * Status levels:
589 * - good: <=90% memory usage
590 * - critical: >90% memory usage
591 *
592 * @since 1.0.0
593 * @return array Site Health test result
594 */
595 public function memory_usage_check()
596 {
597 $stats = $this->get_memory_stats();
598
599 // Handle unlimited memory
600 if ($stats['limit_bytes'] === -1) {
601 return [
602 'label' => __('PHP memory limit is unlimited'),
603 'status' => 'good',
604 'badge' => [
605 'label' => __('MetaSync'),
606 'color' => 'green'
607 ],
608 'description' => sprintf(
609 '<p>%s</p>' .
610 '<p><strong>%s</strong></p>' .
611 '<ul>' .
612 '<li>%s %s</li>' .
613 '<li>%s %s</li>' .
614 '</ul>',
615 __('PHP memory limit is set to unlimited. While this prevents memory errors, it may allow poorly optimized code to consume excessive server resources.'),
616 __('Current Memory Usage:'),
617 __('Current usage:'),
618 $stats['current_formatted'],
619 __('Peak usage:'),
620 $stats['peak_formatted']
621 ),
622 'test' => 'metasync_memory_usage'
623 ];
624 }
625
626 $percentage = $stats['percentage'];
627
628 // Determine status based on memory usage percentage
629 if ($percentage > self::MEMORY_CRITICAL_THRESHOLD) {
630 $status = 'critical';
631 $label = __('PHP memory usage is critically high');
632 } else {
633 $status = 'good';
634 $label = __('PHP memory usage is healthy');
635 }
636
637 // Build detailed description
638 $description = sprintf(
639 '<p><strong>%s</strong></p>' .
640 '<ul>' .
641 '<li>%s %s (%s)</li>' .
642 '<li>%s %s</li>' .
643 '<li>%s %s</li>' .
644 '<li>%s <strong>%.2f%%</strong></li>' .
645 '<li>%s %s</li>' .
646 '</ul>',
647 __('PHP Memory Statistics:'),
648 __('Memory limit:'),
649 $stats['limit_formatted'],
650 $stats['limit'],
651 __('Current usage:'),
652 $stats['current_formatted'],
653 __('Peak usage:'),
654 $stats['peak_formatted'],
655 __('Usage percentage:'),
656 $percentage,
657 __('Available memory:'),
658 $stats['available_formatted']
659 );
660
661 // Add context about memory usage
662 if ($status === 'good') {
663 $description .= sprintf(
664 '<p><em>%s</em></p>',
665 __('Memory usage is within acceptable limits. The site has sufficient memory available for normal operations.')
666 );
667 } else {
668 // Critical status - provide actionable recommendations
669 $description .= sprintf(
670 '<p><strong>%s</strong></p>' .
671 '<p>%s</p>' .
672 '<ul>' .
673 '<li>%s</li>' .
674 '<li>%s</li>' .
675 '<li>%s</li>' .
676 '<li>%s</li>' .
677 '</ul>',
678 __('Critical Memory Usage Detected:'),
679 __('PHP memory usage is critically high (>90%). This may cause:'),
680 __('Fatal errors: "Allowed memory size exhausted"'),
681 __('Failed page loads and white screens'),
682 __('Incomplete plugin/theme operations'),
683 __('Performance degradation and slow response times')
684 );
685
686 $description .= sprintf(
687 '<p><strong>%s</strong></p>' .
688 '<ul>' .
689 '<li>%s</li>' .
690 '<li>%s</li>' .
691 '<li>%s</li>' .
692 '<li>%s</li>' .
693 '<li>%s</li>' .
694 '</ul>',
695 __('Recommended Actions:'),
696 sprintf(
697 __('Increase PHP memory limit in wp-config.php: <code>define(\'WP_MEMORY_LIMIT\', \'256M\');</code> (current: %s)'),
698 $stats['limit']
699 ),
700 __('Contact your hosting provider to increase server memory limits'),
701 __('Deactivate unnecessary plugins that consume excessive memory'),
702 __('Optimize images and reduce media library size'),
703 __('Consider upgrading to a hosting plan with more resources')
704 );
705
706 // Add warning if peak usage is also high
707 if ($stats['peak'] > ($stats['limit_bytes'] * 0.95)) {
708 $description .= sprintf(
709 '<p><strong>%s</strong> %s</p>',
710 __('Warning:'),
711 sprintf(
712 __('Peak memory usage (%s) is very close to the limit. Memory errors may occur during high-traffic periods or resource-intensive operations.'),
713 $stats['peak_formatted']
714 )
715 );
716 }
717
718 $description .= sprintf(
719 '<p>%s %s</p>',
720 __( 'Quick link:' ),
721 sprintf(
722 '<a href="%s">%s</a>',
723 esc_url( admin_url( 'plugins.php' ) ),
724 esc_html( __( 'Manage Plugins (deactivate memory-heavy plugins)' ) )
725 )
726 );
727 }
728
729 return [
730 'label' => $label,
731 'status' => $status,
732 'badge' => [
733 'label' => __('MetaSync'),
734 'color' => $status === 'critical' ? 'red' : 'green'
735 ],
736 'description' => $description,
737 'test' => 'metasync_memory_usage'
738 ];
739 }
740
741 /**
742 * Get OTTO API rate limit statistics for the last 24 hours
743 *
744 * Reads the rolling timestamp log written by metasync_record_otto_rate_limit_hit()
745 * and counts how many 429 responses were received in the past 24 hours.
746 *
747 * @since 1.0.0
748 * @return array Array with 'hits_24h' (int) and 'last_hit' (int|null)
749 */
750 private function get_rate_limit_stats()
751 {
752 $log = get_option('metasync_otto_rate_limit_log', []);
753 $cutoff = time() - DAY_IN_SECONDS;
754
755 $recent = array_filter($log, function($t) use ($cutoff) { return $t > $cutoff; });
756
757 return [
758 'hits_24h' => count($recent),
759 'last_hit' => !empty($recent) ? max($recent) : null,
760 ];
761 }
762
763 /**
764 * Main Site Health test for SearchAtlas OTTO API rate limits
765 *
766 * Checks how frequently the OTTO_URL_DETAILS endpoint has returned HTTP 429
767 * (Too Many Requests) in the last 24 hours and surfaces a warning when the
768 * rate limit is being hit too often.
769 *
770 * Status levels:
771 * - good: < 5 hits in the last 24 hours
772 * - recommended: 5–19 hits in the last 24 hours
773 * - critical: >= 20 hits in the last 24 hours
774 *
775 * @since 1.0.0
776 * @return array Site Health test result
777 */
778 public function otto_rate_limit_check()
779 {
780 $stats = $this->get_rate_limit_stats();
781 $hits = $stats['hits_24h'];
782 $last_hit = $stats['last_hit'];
783
784 // Format the last hit timestamp if available
785 $last_hit_text = $last_hit
786 ? sprintf(
787 __('Last occurrence: %s'),
788 date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $last_hit)
789 )
790 : __('No rate limit hits recorded.');
791
792 if ($hits >= self::RATE_LIMIT_CRITICAL_THRESHOLD) {
793 $status = 'critical';
794 $label = sprintf(
795 __('%s OTTO API is being rate limited frequently (%d times in 24h)'),
796 Metasync::get_effective_plugin_name(),
797 $hits
798 );
799 } elseif ($hits >= self::RATE_LIMIT_WARNING_THRESHOLD) {
800 $status = 'recommended';
801 $label = sprintf(
802 __('%s OTTO API rate limit hit %d times in the last 24 hours'),
803 Metasync::get_effective_plugin_name(),
804 $hits
805 );
806 } else {
807 $status = 'good';
808 $label = sprintf(__('%s OTTO API rate limits are within normal range'), Metasync::get_effective_plugin_name());
809 }
810
811 // Build description
812 $description = sprintf(
813 '<p><strong>%s</strong></p>' .
814 '<ul>' .
815 '<li>%s <strong>%d</strong></li>' .
816 '<li>%s</li>' .
817 '</ul>',
818 __('OTTO API Rate Limit Statistics (last 24 hours):'),
819 __('HTTP 429 responses received:'),
820 $hits,
821 $last_hit_text
822 );
823
824 if ($status === 'good') {
825 $description .= sprintf(
826 '<p><em>%s</em></p>',
827 __('The OTTO API is responding normally. No action required.')
828 );
829 } else {
830 $description .= sprintf(
831 '<p><strong>%s</strong></p>' .
832 '<p>%s</p>' .
833 '<ul>' .
834 '<li>%s</li>' .
835 '<li>%s</li>' .
836 '<li>%s</li>' .
837 '</ul>',
838 __('Frequent rate limiting can degrade OTTO SEO data delivery on your site.'),
839 __('Recommended Actions:'),
840 __('Review your OTTO crawl frequency settings in the SearchAtlas dashboard to reduce request volume.'),
841 __('Check if multiple server processes are triggering OTTO jobs simultaneously.'),
842 __('Contact SearchAtlas support if rate limiting persists despite low crawl volume.')
843 );
844
845 $description .= sprintf(
846 '<p>%s %s</p>',
847 __( 'Quick link:' ),
848 $this->get_admin_link( '&tab=general', __( 'Review API Settings' ) )
849 );
850 }
851
852 return [
853 'label' => $label,
854 'status' => $status,
855 'badge' => [
856 'label' => __('MetaSync'),
857 'color' => $status === 'critical' ? 'red' : ( $status === 'recommended' ? 'orange' : 'green' ),
858 ],
859 'description' => $description,
860 'test' => 'metasync_otto_rate_limit',
861 ];
862 }
863
864 /**
865 * Get current debug mode status across all relevant constants.
866 *
867 * Reads live PHP constants so the result reflects what PHP is
868 * actually executing, even when wp-config.php was edited manually.
869 *
870 * @since 1.0.0
871 * @return array {
872 * @type bool $wp_debug True if WP_DEBUG is defined and truthy.
873 * @type bool $wp_debug_log True if WP_DEBUG_LOG is defined and truthy.
874 * @type bool $wp_debug_display True if WP_DEBUG_DISPLAY is defined and truthy.
875 * @type bool $metasync_debug True if METASYNC_DEBUG is defined and truthy.
876 * }
877 */
878 private function get_debug_stats()
879 {
880 return [
881 'wp_debug' => defined('WP_DEBUG') && WP_DEBUG,
882 'wp_debug_log' => defined('WP_DEBUG_LOG') && WP_DEBUG_LOG,
883 'wp_debug_display' => defined('WP_DEBUG_DISPLAY') && WP_DEBUG_DISPLAY,
884 'metasync_debug' => defined('METASYNC_DEBUG') && constant('METASYNC_DEBUG'),
885 ];
886 }
887
888 /**
889 * Main Site Health test for WordPress debug mode.
890 *
891 * Checks whether WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY, or
892 * METASYNC_DEBUG are active. Debug mode is normal in development
893 * but should be disabled on production sites to avoid exposing
894 * server internals to visitors and incurring performance overhead.
895 *
896 * Status levels:
897 * - good: No debug constants are active.
898 * - recommended: One or more debug constants are active.
899 *
900 * The 'recommended' (not 'critical') status is intentional: debug mode
901 * is a configuration choice, not a breakage, and should not alarm users
902 * who are legitimately running a staging environment.
903 *
904 * @since 1.0.0
905 * @return array Site Health test result
906 */
907 public function debug_mode_check()
908 {
909 $stats = $this->get_debug_stats();
910 $any_debug_on = $stats['wp_debug'] || $stats['metasync_debug'];
911
912 // Good state: no debug constants active
913 if ( ! $any_debug_on ) {
914 return [
915 'label' => __('Debug mode is disabled'),
916 'status' => 'good',
917 'badge' => [
918 'label' => __('MetaSync'),
919 'color' => 'green',
920 ],
921 'description' => sprintf(
922 '<p>%s</p>',
923 __('WordPress debug mode is not active. This is the recommended configuration for production sites.')
924 ),
925 'test' => 'metasync_debug_mode',
926 ];
927 }
928
929 // Recommended state: one or more debug constants are active
930 if ( $stats['wp_debug_display'] ) {
931 $label = __('Debug mode is enabled with error display active');
932 } else {
933 $label = __('Debug mode is enabled on this site');
934 }
935
936 // Status table showing each flag's current state
937 $description = sprintf(
938 '<p><strong>%s</strong></p>' .
939 '<ul>' .
940 '<li>%s <strong>%s</strong></li>' .
941 '<li>%s <strong>%s</strong></li>' .
942 '<li>%s <strong>%s</strong></li>' .
943 '</ul>',
944 __('Active Debug Configuration:'),
945 __('WP_DEBUG:'),
946 $stats['wp_debug'] ? __('Enabled') : __('Disabled'),
947 __('WP_DEBUG_LOG:'),
948 $stats['wp_debug_log'] ? __('Enabled') : __('Disabled'),
949 __('WP_DEBUG_DISPLAY:'),
950 $stats['wp_debug_display'] ? __('Enabled') : __('Disabled')
951 );
952
953 // Call out METASYNC_DEBUG if it is separately active
954 if ( $stats['metasync_debug'] ) {
955 $description .= sprintf(
956 '<p>%s</p>',
957 __('The <code>METASYNC_DEBUG</code> constant is also active. This enables additional MetaSync diagnostic output.')
958 );
959 }
960
961 // Escalated warning when WP_DEBUG_DISPLAY is on (errors visible to site visitors)
962 if ( $stats['wp_debug_display'] ) {
963 $description .= sprintf(
964 '<p><strong>%s</strong> %s</p>',
965 __('Warning:'),
966 __('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.')
967 );
968 }
969
970 $description .= sprintf(
971 '<p><em>%s</em></p>',
972 __('Debug mode is intended for development environments. On a production site, disabling it prevents information leakage and removes the performance overhead of error collection.')
973 );
974
975 $description .= sprintf(
976 '<p><strong>%s</strong></p>' .
977 '<ul>' .
978 '<li>%s</li>' .
979 '<li>%s</li>' .
980 '</ul>',
981 __('Recommended Actions:'),
982 __('Set <code>define(\'WP_DEBUG\', false);</code> in your wp-config.php if this is a live production site.'),
983 __('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.')
984 );
985
986 $description .= sprintf(
987 '<p>%s %s</p>',
988 __( 'Quick link:' ),
989 $this->get_admin_link( '&tab=advanced', __( 'Advanced Settings' ) )
990 );
991
992 return [
993 'label' => $label,
994 'status' => 'recommended',
995 'badge' => [
996 'label' => __('MetaSync'),
997 'color' => 'orange',
998 ],
999 'description' => $description,
1000 'test' => 'metasync_debug_mode',
1001 ];
1002 }
1003
1004 /**
1005 * Get current performance settings for display in the Site Health Info tab
1006 *
1007 * Reads PHP configuration, WordPress constants, class constants, and scheduled
1008 * event timing to give administrators a single-glance view of the settings that
1009 * most directly affect MetaSync's runtime performance.
1010 *
1011 * @since 1.0.0
1012 * @return array Associative array of performance setting fields ready for the info tab
1013 */
1014 private function get_performance_settings()
1015 {
1016 // WP-Cron mode
1017 $wp_cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
1018 $wp_cron_mode = $wp_cron_disabled
1019 ? __( 'System Cron (DISABLE_WP_CRON = true)' )
1020 : __( 'WP-Cron (default)' );
1021
1022 // PHP max execution time
1023 $max_exec_raw = ini_get( 'max_execution_time' );
1024 $max_exec = $max_exec_raw === '0' || $max_exec_raw === 0
1025 ? __( 'Unlimited (0)' )
1026 : (int) $max_exec_raw . 's';
1027
1028 // Next scheduled transient cleanup
1029 $next_cleanup_ts = wp_next_scheduled( 'metasync_cleanup_transients' );
1030 $next_cleanup_val = $next_cleanup_ts
1031 ? wp_date( 'Y-m-d H:i:s', $next_cleanup_ts ) . ' (in ' . human_time_diff( $next_cleanup_ts ) . ')'
1032 : __( 'Not scheduled' );
1033
1034 // Telemetry flush interval label
1035 $flush_interval = Metasync_Telemetry_Config::QUEUE_FLUSH_INTERVAL;
1036 $schedules = wp_get_schedules();
1037 $flush_interval_label = isset( $schedules[ $flush_interval ]['display'] )
1038 ? $schedules[ $flush_interval ]['display']
1039 : $flush_interval;
1040
1041 // OTTO suggestions cache TTL (stored in seconds, display as minutes)
1042 $otto_cache_ttl_sec = Metasync_Otto_Transient_Cache::SUGGESTIONS_TTL;
1043 $otto_cache_ttl_val = ( $otto_cache_ttl_sec / MINUTE_IN_SECONDS ) . ' ' . __( 'minutes' );
1044
1045 return [
1046 'perf_wp_cron' => [
1047 'label' => __( 'Performance - WP-Cron Mode' ),
1048 'value' => $wp_cron_mode,
1049 ],
1050 'perf_php_max_exec' => [
1051 'label' => __( 'Performance - PHP Max Execution Time' ),
1052 'value' => $max_exec,
1053 ],
1054 'perf_otto_request_timeout' => [
1055 'label' => __( 'Performance - OTTO API Request Timeout' ),
1056 'value' => '30s',
1057 ],
1058 'perf_otto_cache_ttl' => [
1059 'label' => __( 'Performance - OTTO Suggestions Cache TTL' ),
1060 'value' => $otto_cache_ttl_val,
1061 ],
1062 'perf_otto_max_calls' => [
1063 'label' => __( 'Performance - OTTO Max API Calls/Min' ),
1064 'value' => Metasync_Otto_Transient_Cache::MAX_API_CALLS_PER_MINUTE,
1065 ],
1066 'perf_telemetry_queue_max' => [
1067 'label' => __( 'Performance - Telemetry Max Queue Size' ),
1068 'value' => Metasync_Telemetry_Config::MAX_QUEUE_SIZE,
1069 ],
1070 'perf_telemetry_batch' => [
1071 'label' => __( 'Performance - Telemetry Batch Size' ),
1072 'value' => Metasync_Telemetry_Config::QUEUE_BATCH_SIZE,
1073 ],
1074 'perf_telemetry_flush' => [
1075 'label' => __( 'Performance - Telemetry Queue Flush Interval' ),
1076 'value' => $flush_interval_label,
1077 ],
1078 'perf_api_max_retries' => [
1079 'label' => __( 'Performance - API Max Retry Attempts' ),
1080 'value' => Metasync_Telemetry_Config::MAX_RETRY_ATTEMPTS,
1081 ],
1082 'perf_next_cleanup' => [
1083 'label' => __( 'Performance - Next Transient Cleanup' ),
1084 'value' => $next_cleanup_val,
1085 ],
1086 ];
1087 }
1088
1089 /**
1090 * Get failed MetaSync cron action statistics for the last 24 hours
1091 *
1092 * Reads the rolling timestamp log written by metasync_record_failed_action()
1093 * and counts how many failures were recorded in the past 24 hours.
1094 *
1095 * @since 1.0.0
1096 * @return array Array with 'count' (int) and 'last_hit' (int|null)
1097 */
1098 private function get_failed_actions_stats()
1099 {
1100 $log = get_option( 'metasync_failed_actions_log', [] );
1101 $cutoff = time() - DAY_IN_SECONDS;
1102
1103 $recent = array_filter( $log, function( $t ) use ( $cutoff ) { return $t > $cutoff; } );
1104
1105 return [
1106 'count' => count( $recent ),
1107 'last_hit' => ! empty( $recent ) ? max( $recent ) : null,
1108 ];
1109 }
1110
1111 /**
1112 * Main Site Health test for MetaSync failed cron actions
1113 *
1114 * Checks how many MetaSync OTTO SEO processing jobs have failed in the last
1115 * 24 hours and surfaces a warning when failures exceed the threshold.
1116 * Failures are recorded by metasync_record_failed_action() in otto_pixel.php
1117 * when metasync_process_otto_seo_data() encounters an API error or exception.
1118 *
1119 * Status levels:
1120 * - good: <= 100 failures in the last 24 hours
1121 * - recommended: > 100 failures in the last 24 hours
1122 *
1123 * @since 1.0.0
1124 * @return array Site Health test result
1125 */
1126 public function failed_actions_check()
1127 {
1128 $stats = $this->get_failed_actions_stats();
1129 $count = $stats['count'];
1130 $last_hit = $stats['last_hit'];
1131
1132 $last_hit_text = $last_hit
1133 ? sprintf(
1134 __( 'Last failure: %s' ),
1135 date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $last_hit )
1136 )
1137 : __( 'No failed actions recorded in the last 24 hours.' );
1138
1139 if ( $count > self::FAILED_ACTIONS_THRESHOLD ) {
1140 $status = 'recommended';
1141 $label = sprintf(
1142 __( 'MetaSync has %d failed SEO processing actions in the last 24 hours' ),
1143 $count
1144 );
1145 } else {
1146 $status = 'good';
1147 $label = __( 'MetaSync SEO action queue is healthy' );
1148 }
1149
1150 $description = sprintf(
1151 '<p><strong>%s</strong></p>' .
1152 '<ul>' .
1153 '<li>%s <strong>%d</strong></li>' .
1154 '<li>%s</li>' .
1155 '</ul>',
1156 __( 'MetaSync Failed Actions Statistics (last 24 hours):' ),
1157 __( 'Failed OTTO SEO processing jobs:' ),
1158 $count,
1159 $last_hit_text
1160 );
1161
1162 if ( $status === 'good' ) {
1163 $description .= sprintf(
1164 '<p><em>%s</em></p>',
1165 __( 'OTTO SEO processing jobs are completing successfully. No action required.' )
1166 );
1167 } else {
1168 $description .= sprintf(
1169 '<p><strong>%s</strong></p>' .
1170 '<ul>' .
1171 '<li>%s</li>' .
1172 '<li>%s</li>' .
1173 '<li>%s</li>' .
1174 '</ul>',
1175 __( 'Recommended Actions:' ),
1176 __( 'Check your SearchAtlas API connection in MetaSync → Settings.' ),
1177 __( 'Review your PHP error log for OTTO processing exceptions.' ),
1178 __( 'Verify your OTTO UUID is correctly configured and the SearchAtlas API is reachable.' )
1179 );
1180
1181 $description .= sprintf(
1182 '<p>%s %s &middot; %s</p>',
1183 __( 'Quick links:' ),
1184 $this->get_admin_link( '&tab=general', __( 'Review API Settings' ) ),
1185 $this->get_admin_link( '-sync-log', __( 'View Changes Log' ) )
1186 );
1187 }
1188
1189 return [
1190 'label' => $label,
1191 'status' => $status,
1192 'badge' => [
1193 'label' => __( 'MetaSync' ),
1194 'color' => $status === 'recommended' ? 'orange' : 'green',
1195 ],
1196 'description' => $description,
1197 'test' => 'metasync_failed_actions',
1198 ];
1199 }
1200 }
1201