| @@ -353,16 +353,17 @@ | ||
| 353 | 353 | try { |
| 354 | 354 | // Get device type from request |
| 355 | 355 | $device_type = $request->get_param('device_type') ?? 'mobile'; |
| 356 | 356 | |
| 357 | - $opportunities = $this->get_performance_manager()->get_performance_opportunities('', $device_type); | |
| 357 | + $manager = $this->get_performance_manager(); | |
| 358 | + $opportunities = $manager->get_performance_opportunities('', $device_type); | |
| 358 | 359 | |
| 359 | - return new WP_REST_Response([ | |
| 360 | - 'success' => true, | |
| 361 | - 'data' => $opportunities, | |
| 362 | - 'device_type' => $device_type, | |
| 363 | - 'message' => __('Performance opportunities retrieved successfully', 'thinkrank') | |
| 364 | - ], 200); | |
| 360 | + return $this->pagespeed_list_response( | |
| 361 | + $manager, | |
| 362 | + $opportunities, | |
| 363 | + $device_type, | |
| 364 | + __('Performance opportunities retrieved successfully', 'thinkrank') | |
| 365 | + ); | |
| 365 | 366 | |
| 366 | 367 | } catch (\Exception $e) { |
| 367 | 368 | return new WP_Error( |
| 368 | 369 | 'opportunities_failed', |
| @@ -384,16 +385,17 @@ | ||
| 384 | 385 | try { |
| 385 | 386 | // Get device type from request |
| 386 | 387 | $device_type = $request->get_param('device_type') ?? 'mobile'; |
| 387 | 388 | |
| 388 | - $diagnostics = $this->get_performance_manager()->get_performance_diagnostics('', $device_type); | |
| 389 | + $manager = $this->get_performance_manager(); | |
| 390 | + $diagnostics = $manager->get_performance_diagnostics('', $device_type); | |
| 389 | 391 | |
| 390 | - return new WP_REST_Response([ | |
| 391 | - 'success' => true, | |
| 392 | - 'data' => $diagnostics, | |
| 393 | - 'device_type' => $device_type, | |
| 394 | - 'message' => __('Performance diagnostics retrieved successfully', 'thinkrank') | |
| 395 | - ], 200); | |
| 392 | + return $this->pagespeed_list_response( | |
| 393 | + $manager, | |
| 394 | + $diagnostics, | |
| 395 | + $device_type, | |
| 396 | + __('Performance diagnostics retrieved successfully', 'thinkrank') | |
| 397 | + ); | |
| 396 | 398 | |
| 397 | 399 | } catch (\Exception $e) { |
| 398 | 400 | return new WP_Error( |
| 399 | 401 | 'diagnostics_failed', |
| @@ -414,14 +416,30 @@ | ||
| 414 | 416 | public function collect_performance_data(WP_REST_Request $request) { |
| 415 | 417 | try { |
| 416 | 418 | $results = $this->get_data_collector()->manual_collect(); |
| 417 | 419 | |
| 418 | - return new WP_REST_Response([ | |
| 419 | - 'success' => $results['success'], | |
| 420 | - 'data' => $results, | |
| 421 | - 'message' => $results['message'] | |
| 422 | - ], $results['success'] ? 200 : 500); | |
| 420 | + if (!empty($results['success'])) { | |
| 421 | + return new WP_REST_Response([ | |
| 422 | + 'success' => true, | |
| 423 | + 'data' => $results, | |
| 424 | + 'message' => $results['message'] | |
| 425 | + ], 200); | |
| 426 | + } | |
| 423 | 427 | |
| 428 | + // A failure here is almost never a server fault: the site is not | |
| 429 | + // connected, Google cannot reach the URL, or the quota is spent. This | |
| 430 | + // used to answer 500 for all of them, with a hardcoded message that | |
| 431 | + // dropped the real reason, so the user could neither tell what was | |
| 432 | + // wrong nor that it was their configuration rather than a bug. | |
| 433 | + // Return a WP_Error like every other failure in this file, so clients | |
| 434 | + // get the normal code/message envelope instead of a 200-shaped body | |
| 435 | + // carrying a 500. | |
| 436 | + return new WP_Error( | |
| 437 | + $this->collection_error_code((string) ($results['error_code'] ?? '')), | |
| 438 | + $results['message'], | |
| 439 | + $this->collection_error_data((string) ($results['error_code'] ?? '')) | |
| 440 | + ); | |
| 441 | + | |
| 424 | 442 | } catch (\Exception $e) { |
| 425 | 443 | return new WP_Error( |
| 426 | 444 | 'data_collection_failed', |
| 427 | 445 | 'Failed to collect performance data: ' . $e->getMessage(), |
| @@ -429,11 +447,65 @@ | ||
| 429 | 447 | ); |
| 430 | 448 | } |
| 431 | 449 | } |
| 432 | 450 | |
| 451 | + /** | |
| 452 | + * REST error code for a collection failure class. | |
| 453 | + * | |
| 454 | + * @since 1.31.0 | |
| 455 | + * @param string $error_code One of Performance_Data_Collector::ERROR_*. | |
| 456 | + * @return string | |
| 457 | + */ | |
| 458 | + private function collection_error_code(string $error_code): string { | |
| 459 | + $codes = [ | |
| 460 | + Performance_Data_Collector::ERROR_NOT_CONFIGURED => 'pagespeed_not_configured', | |
| 461 | + Performance_Data_Collector::ERROR_URL_UNREACHABLE => 'site_not_reachable', | |
| 462 | + Performance_Data_Collector::ERROR_RATE_LIMITED => 'pagespeed_rate_limited', | |
| 463 | + Performance_Data_Collector::ERROR_RECENT_FAILURE => 'pagespeed_recently_failed', | |
| 464 | + Performance_Data_Collector::ERROR_STORAGE_FAILED => 'performance_storage_failed', | |
| 465 | + ]; | |
| 433 | 466 | |
| 467 | + return $codes[$error_code] ?? 'data_collection_failed'; | |
| 468 | + } | |
| 434 | 469 | |
| 435 | 470 | /** |
| 471 | + * HTTP status (and Retry-After, where it applies) for a failure class. | |
| 472 | + * | |
| 473 | + * @since 1.31.0 | |
| 474 | + * @param string $error_code One of Performance_Data_Collector::ERROR_*. | |
| 475 | + * @return array Error data for WP_Error. | |
| 476 | + */ | |
| 477 | + private function collection_error_data(string $error_code): array { | |
| 478 | + switch ($error_code) { | |
| 479 | + case Performance_Data_Collector::ERROR_NOT_CONFIGURED: | |
| 480 | + // Client-side condition: no credential to call PageSpeed with. | |
| 481 | + return ['status' => 400]; | |
| 482 | + | |
| 483 | + case Performance_Data_Collector::ERROR_URL_UNREACHABLE: | |
| 484 | + // The request was well-formed and authorised; the site simply | |
| 485 | + // cannot be fetched by Google. | |
| 486 | + return ['status' => 422]; | |
| 487 | + | |
| 488 | + case Performance_Data_Collector::ERROR_RATE_LIMITED: | |
| 489 | + return ['status' => 429]; | |
| 490 | + | |
| 491 | + case Performance_Data_Collector::ERROR_RECENT_FAILURE: | |
| 492 | + // Nothing was attempted — a recent failure is still remembered. | |
| 493 | + return ['status' => 503, 'retry_after' => 300]; | |
| 494 | + | |
| 495 | + case Performance_Data_Collector::ERROR_STORAGE_FAILED: | |
| 496 | + // Measured fine but the write failed: genuinely our side. | |
| 497 | + return ['status' => 500]; | |
| 498 | + | |
| 499 | + default: | |
| 500 | + // An upstream API error we could not classify. | |
| 501 | + return ['status' => 502]; | |
| 502 | + } | |
| 503 | + } | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + /** | |
| 436 | 508 | * Check read permissions |
| 437 | 509 | * |
| 438 | 510 | * @since 1.0.0 |
| 439 | 511 | * |
| @@ -462,8 +534,46 @@ | ||
| 462 | 534 | * @since 1.0.0 |
| 463 | 535 | * |
| 464 | 536 | * @return array Arguments array |
| 465 | 537 | */ |
| 538 | + /** | |
| 539 | + * Wrap a PageSpeed-backed list, reporting whether it was actually fetched. | |
| 540 | + * | |
| 541 | + * An empty list used to come back as `success: true` / | |
| 542 | + * "retrieved successfully" whether the site was clean, the request had | |
| 543 | + * failed, or nothing had been attempted for want of a credential — so no | |
| 544 | + * API or MCP consumer could tell the three apart, and the admin UI told | |
| 545 | + * everyone to connect Google (#519). The list itself keeps its shape. | |
| 546 | + * | |
| 547 | + * @since 2.1.1 | |
| 548 | + * | |
| 549 | + * @param Performance_Monitoring_Manager $manager Manager that produced the list. | |
| 550 | + * @param array $data The list. | |
| 551 | + * @param string $device_type Device the list is for. | |
| 552 | + * @param string $success_message Message for a completed request. | |
| 553 | + * @return WP_REST_Response | |
| 554 | + */ | |
| 555 | + private function pagespeed_list_response(Performance_Monitoring_Manager $manager, array $data, string $device_type, string $success_message): WP_REST_Response { | |
| 556 | + $error = $manager->get_last_error(); | |
| 557 | + | |
| 558 | + if ('' !== $error['code']) { | |
| 559 | + return new WP_REST_Response([ | |
| 560 | + 'success' => false, | |
| 561 | + 'data' => $data, | |
| 562 | + 'device_type' => $device_type, | |
| 563 | + 'error_code' => $error['code'], | |
| 564 | + 'message' => $error['message'], | |
| 565 | + ], 200); | |
| 566 | + } | |
| 567 | + | |
| 568 | + return new WP_REST_Response([ | |
| 569 | + 'success' => true, | |
| 570 | + 'data' => $data, | |
| 571 | + 'device_type' => $device_type, | |
| 572 | + 'message' => $success_message, | |
| 573 | + ], 200); | |
| 574 | + } | |
| 575 | + | |
| 466 | 576 | private function get_historical_data_args(): array { |
| 467 | 577 | return [ |
| 468 | 578 | 'days' => [ |
| 469 | 579 | 'required' => false, |
| @@ -476,9 +586,9 @@ | ||
| 476 | 586 | 'metric' => [ |
| 477 | 587 | 'required' => false, |
| 478 | 588 | 'type' => 'string', |
| 479 | 589 | 'default' => 'all', |
| 480 | - 'enum' => ['all', 'lcp', 'fid', 'cls', 'inp', 'score'], | |
| 590 | + 'enum' => ['all', 'lcp', 'cls', 'inp', 'score'], | |
| 481 | 591 | 'description' => 'Specific metric to retrieve' |
| 482 | 592 | ] |
| 483 | 593 | ]; |
| 484 | 594 | } |