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