| @@ -195,9 +195,9 @@ | ||
| 195 | 195 | return array( |
| 196 | 196 | 'attached' => $state['attached'], |
| 197 | 197 | 'account_email' => $state['account_email'], |
| 198 | 198 | 'attached_at' => $state['attached_at'], |
| 199 | - 'site_url' => home_url( '/' ), | |
| 199 | + 'site_url' => Mcp_Pairing::absolute( home_url( '/' ) ), | |
| 200 | 200 | // Method 1 paste-in credential — the existing per-site token. |
| 201 | 201 | // Empty until generate_token() (or a per-site Connect) mints one. |
| 202 | 202 | 'site_token' => Mcp_Pairing::site_token(), |
| 203 | 203 | 'hub_url' => self::hub_url(), |
| @@ -624,12 +624,48 @@ | ||
| 624 | 624 | * |
| 625 | 625 | * @return array<string,mixed>|\WP_Error |
| 626 | 626 | */ |
| 627 | 627 | public static function gtmetrix_test() { |
| 628 | - return self::gtmetrix_request( 'POST', '/api/site/gtmetrix/test' ); | |
| 628 | + return self::hub_request( 'POST', '/api/site/gtmetrix/test' ); | |
| 629 | 629 | } |
| 630 | 630 | |
| 631 | 631 | /** |
| 632 | + * Ask the Hub to run a PageSpeed Insights audit for this site. | |
| 633 | + * | |
| 634 | + * The PSI twin of gtmetrix_test(): the Hub holds a real Google API key, so | |
| 635 | + * routing the audit through it is what makes a keyless site's test work — | |
| 636 | + * an unkeyed call straight to Google shares one anonymous per-IP pool with | |
| 637 | + * every other unkeyed caller and refuses with "Quota exceeded" under any | |
| 638 | + * real load (issue #426). | |
| 639 | + * | |
| 640 | + * The Hub answers 202 with a run row and audits in the background; the | |
| 641 | + * result arrives via psi_runs(). | |
| 642 | + * | |
| 643 | + * @param string $strategy 'mobile', 'desktop' or 'both'. | |
| 644 | + * @return array<string,mixed>|\WP_Error | |
| 645 | + */ | |
| 646 | + public static function psi_test( string $strategy = 'mobile' ) { | |
| 647 | + $strategy = in_array( $strategy, array( 'mobile', 'desktop', 'both' ), true ) ? $strategy : 'mobile'; | |
| 648 | + return self::hub_request( 'POST', '/api/site/psi/test', array( 'strategy' => $strategy ) ); | |
| 649 | + } | |
| 650 | + | |
| 651 | + /** | |
| 652 | + * PSI runs for this site, finished ones copied into the local history. | |
| 653 | + * | |
| 654 | + * The polling half of psi_test() — that route answers before the audit | |
| 655 | + * runs, so without this the plugin would never learn the score. | |
| 656 | + * | |
| 657 | + * @return array<string,mixed>|\WP_Error | |
| 658 | + */ | |
| 659 | + public static function psi_runs() { | |
| 660 | + $result = self::hub_request( 'GET', '/api/site/psi/runs' ); | |
| 661 | + if ( ! is_wp_error( $result ) ) { | |
| 662 | + self::store_hub_results( $result ); | |
| 663 | + } | |
| 664 | + return $result; | |
| 665 | + } | |
| 666 | + | |
| 667 | + /** | |
| 632 | 668 | * Recent Hub-run tests for this site, plus the remaining allowance. |
| 633 | 669 | * |
| 634 | 670 | * Polled while a run is in flight, and read once on load so the button can |
| 635 | 671 | * show the count before anyone presses anything. |
| @@ -636,9 +672,9 @@ | ||
| 636 | 672 | * |
| 637 | 673 | * @return array<string,mixed>|\WP_Error |
| 638 | 674 | */ |
| 639 | 675 | public static function gtmetrix_runs() { |
| 640 | - $result = self::gtmetrix_request( 'GET', '/api/site/gtmetrix/runs' ); | |
| 676 | + $result = self::hub_request( 'GET', '/api/site/gtmetrix/runs' ); | |
| 641 | 677 | if ( ! is_wp_error( $result ) ) { |
| 642 | 678 | self::store_hub_results( $result ); |
| 643 | 679 | } |
| 644 | 680 | return $result; |
| @@ -682,15 +718,19 @@ | ||
| 682 | 718 | if ( $ts <= 0 || '' === $remote_id || Score_Store::exists_remote( $remote_id ) ) { |
| 683 | 719 | continue; |
| 684 | 720 | } |
| 685 | 721 | |
| 722 | + // The runs table is shared between providers on the Hub too — a | |
| 723 | + // PSI run must not be recorded as a GTmetrix row. | |
| 724 | + $provider = 'psi' === ( $run['provider'] ?? '' ) ? 'psi' : 'gtmetrix'; | |
| 725 | + | |
| 686 | 726 | Score_Store::insert( |
| 687 | 727 | array( |
| 688 | 728 | 'ok' => true, |
| 689 | - 'provider' => 'gtmetrix', | |
| 729 | + 'provider' => $provider, | |
| 690 | 730 | 'ts' => $ts, |
| 691 | 731 | 'url' => (string) ( $r['url'] ?? '' ), |
| 692 | - 'strategy' => (string) ( $r['strategy'] ?? 'desktop' ), | |
| 732 | + 'strategy' => (string) ( $r['strategy'] ?? ( 'psi' === $provider ? 'mobile' : 'desktop' ) ), | |
| 693 | 733 | 'score' => $r['score'] ?? null, |
| 694 | 734 | 'metrics' => array( |
| 695 | 735 | 'lcp' => $r['lcp'] ?? null, |
| 696 | 736 | 'fcp' => $r['fcp'] ?? null, |
| @@ -717,25 +757,26 @@ | ||
| 717 | 757 | * failure into a stable error code — must behave identically for both. A |
| 718 | 758 | * divergence there would show up as the UI handling a quota error on one |
| 719 | 759 | * path and not the other. |
| 720 | 760 | * |
| 721 | - * @param string $method HTTP method. | |
| 722 | - * @param string $path Path under the hub base URL. | |
| 761 | + * @param string $method HTTP method. | |
| 762 | + * @param string $path Path under the hub base URL. | |
| 763 | + * @param array<string,mixed> $body Extra POST body fields beside site_url. | |
| 723 | 764 | * @return array<string,mixed>|\WP_Error |
| 724 | 765 | */ |
| 725 | - private static function gtmetrix_request( string $method, string $path ) { | |
| 766 | + private static function hub_request( string $method, string $path, array $body = array() ) { | |
| 726 | 767 | $token = Mcp_Pairing::site_token(); |
| 727 | 768 | if ( '' === $token ) { |
| 728 | 769 | return new \WP_Error( |
| 729 | 770 | 'not_connected', |
| 730 | - __( 'Connect this site to xSpeed Hub to run a free GTmetrix test.', 'xspeed' ) | |
| 771 | + __( 'Connect this site to xSpeed Hub to run a free speed test.', 'xspeed' ) | |
| 731 | 772 | ); |
| 732 | 773 | } |
| 733 | 774 | |
| 734 | 775 | $site_url = self::site_url_canonical(); |
| 735 | 776 | $args = array( |
| 736 | - // A GTmetrix test takes a minute, but the Hub answers as soon as it | |
| 737 | - // has ACCEPTED the job — this waits for that handshake only. | |
| 777 | + // A test takes a minute, but the Hub answers as soon as it has | |
| 778 | + // ACCEPTED the job — this waits for that handshake only. | |
| 738 | 779 | 'timeout' => 15, |
| 739 | 780 | 'headers' => array( 'X-XSpeed-Site-Token' => $token ), |
| 740 | 781 | ); |
| 741 | 782 | |
| @@ -740,9 +781,9 @@ | ||
| 740 | 781 | ); |
| 741 | 782 | |
| 742 | 783 | if ( 'POST' === $method ) { |
| 743 | 784 | $args['headers']['Content-Type'] = 'application/json'; |
| 744 | - $args['body'] = wp_json_encode( array( 'site_url' => $site_url ) ); | |
| 785 | + $args['body'] = wp_json_encode( array_merge( array( 'site_url' => $site_url ), $body ) ); | |
| 745 | 786 | $resp = wp_remote_post( self::hub_url() . $path, $args ); |
| 746 | 787 | } else { |
| 747 | 788 | $resp = wp_remote_get( |
| 748 | 789 | add_query_arg( array( 'site_url' => rawurlencode( $site_url ) ), self::hub_url() . $path ), |