| 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 · %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 |
|