PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
← All changes | includes/class-score.php +56 -3 1.1.31.3.3 View file →
@@ -193,8 +193,17 @@
193 193 /* translators: %d: HTTP status code. */
194 194 __( 'PageSpeed Insights returned HTTP %d.', 'xspeed' ),
195 195 $code
196 196 );
197 +
198 + // A keyless refusal is Google's shared anonymous pool running dry,
199 + // not a fault on this site — and Google's own sentence (project
200 + // numbers, quota metric names) reads like a broken plugin. Name
201 + // the two remedies instead. (#426)
202 + if ( '' === trim( $api_key ) && ( 429 === $code || preg_match( '/quota|rate limit/i', $message ) ) ) {
203 + $message = __( 'Google\'s shared anonymous PageSpeed quota is exhausted right now — this is not a problem with your site. Add a free PageSpeed API key in the Speed Test settings, or connect this site to xSpeed Hub to run tests through it.', 'xspeed' );
204 + }
205 +
197 206 return self::failure( 'psi', $url, $strategy, $message );
198 207 }
199 208
200 209 $row = self::parse_psi( $url, $strategy, $json );
@@ -428,9 +437,15 @@
428 437 *
429 438 * @param array<string,mixed> $row A parsed run.
430 439 */
431 440 public static function record( array $row ): void {
432 - $history = self::history();
441 + // The table is the store. It is created on activation and on
442 + // admin_init, but record() can run from CLI on a site that has done
443 + // neither yet, so make sure it exists before writing.
444 + Score_Store::maybe_install();
445 + Score_Store::insert( $row, isset( $row['source'] ) ? (string) $row['source'] : 'local' );
446 +
447 + $history = self::history_option();
433 448 array_unshift( $history, $row );
434 449 if ( count( $history ) > self::MAX_HISTORY ) {
435 450 $history = array_slice( $history, 0, self::MAX_HISTORY );
436 451 }
@@ -447,8 +462,27 @@
447 462 *
448 463 * @return array<int,array<string,mixed>>
449 464 */
450 465 public static function history(): array {
466 + Score_Store::maybe_install();
467 + $rows = Score_Store::history( self::MAX_HISTORY );
468 + if ( ! empty( $rows ) ) {
469 + return $rows;
470 + }
471 + // Empty table on a site whose migration has not run yet — fall back
472 + // so the panel never looks like it lost the user's history.
473 + return self::history_option();
474 + }
475 +
476 + /**
477 + * The legacy option-based history.
478 + *
479 + * Retained for the one-time migration in Score_Store and as a fallback,
480 + * NOT as a second source of truth. Nothing else should call it.
481 + *
482 + * @return array<int,array<string,mixed>>
483 + */
484 + private static function history_option(): array {
451 485 $raw = get_option( self::HISTORY_OPTION, array() );
452 486 if ( ! is_array( $raw ) ) {
453 487 return array();
454 488 }
@@ -460,11 +494,30 @@
460 494 }
461 495 return $out;
462 496 }
463 497
464 - /** Most recent successful run, or null. */
498 + /**
499 + * Most recent successful run, or null.
500 + *
501 + * Asks the store for the newest `ok` row rather than scanning the capped
502 + * history window. Failed runs are recorded too, so a site whose audits
503 + * keep failing — the unauthenticated PSI quota refuses often, and the key
504 + * is optional — would push its last real score out of the window after
505 + * MAX_HISTORY failures and then report no score at all (#306 review,
506 + * issue 1). Reproduced: one genuine audit of 91, then 30 failures, and
507 + * latest() returned null.
508 + *
509 + * The option fallback still scans, because that path has no query to make
510 + * and is only reached when the table is unavailable.
511 + */
465 512 public static function latest(): ?array {
466 - foreach ( self::history() as $row ) {
513 + Score_Store::maybe_install();
514 + $row = Score_Store::latest_ok();
515 + if ( is_array( $row ) ) {
516 + return $row;
517 + }
518 +
519 + foreach ( self::history_option() as $row ) {
467 520 if ( ! empty( $row['ok'] ) ) {
468 521 return $row;
469 522 }
470 523 }