PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 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 All 35 releases
← All changes | includes/class-scan.php +69 -4 1.2.4 → 1.4.1 View file →
@@ -102,8 +102,22 @@
102 102 return (bool) apply_filters( 'xspeed_scan_private_supported', true );
103 103 }
104 104
105 105 /**
106 + * Which Lighthouse runs a scan may spend. `both` is the engine's default:
107 + * the desktop run graded as the headline, the mobile run graded alongside
108 + * it. A single device costs half the engine's quota and becomes the
109 + * headline. Anything else is sent as `both`.
110 + */
111 + const STRATEGIES = array( 'both', 'desktop', 'mobile' );
112 +
113 + /** Coerce a caller's strategy to one the engine knows. */
114 + public static function strategy( string $value ): string {
115 + $value = strtolower( trim( $value ) );
116 + return in_array( $value, self::STRATEGIES, true ) ? $value : 'both';
117 + }
118 +
119 + /**
106 120 * Start a scan. Returns the scan id to poll, or a WP_Error.
107 121 *
108 122 * `$fresh` forces a new run; without it the engine may hand back a
109 123 * recent cached report for the same URL, which is what you want for a
@@ -108,11 +122,16 @@
108 122 * `$fresh` forces a new run; without it the engine may hand back a
109 123 * recent cached report for the same URL, which is what you want for a
110 124 * first look and not what you want after a change.
111 125 *
126 + * `$strategy` picks the Lighthouse runs (see STRATEGIES). The engine
127 + * grades the desktop run as the headline and the mobile run beside it,
128 + * so `both` is right for a report a person reads; a site that only
129 + * wants the run Google ranks on asks for `mobile` and spends half.
130 + *
112 131 * @return array{scan_id:string,report_url:string,cached:bool}|\WP_Error
113 132 */
114 - public static function start( string $url = '', bool $fresh = false ) {
133 + public static function start( string $url = '', bool $fresh = false, string $strategy = 'both' ) {
115 134 $url = '' !== $url ? $url : home_url( '/' );
116 135
117 136 // The engine probes this URL from the outside, so a host it cannot
118 137 // reach produces a confusing failure deep in the scan rather than
@@ -145,10 +164,11 @@
145 164 'timeout' => 20,
146 165 'headers' => array( 'Content-Type' => 'application/json' ),
147 166 'body' => wp_json_encode(
148 167 array(
149 - 'url' => $url,
150 - 'fresh' => $fresh,
168 + 'url' => $url,
169 + 'fresh' => $fresh,
170 + 'strategy' => self::strategy( $strategy ),
151 171 // Sent ahead of engine support: an unknown field is
152 172 // ignored today and becomes meaningful the moment
153 173 // the flag lands, with no plugin release needed.
154 174 'visibility' => 'private' === self::visibility() ? 'unlisted' : 'listed',
@@ -313,11 +333,43 @@
313 333
314 334 /** The last completed report, or null. */
315 335 public static function latest(): ?array {
316 336 $r = get_option( self::RESULT_OPTION );
317 - return is_array( $r ) && isset( $r['score'] ) ? $r : null;
337 + if ( ! is_array( $r ) || ! isset( $r['score'] ) ) {
338 + return null;
339 + }
340 + return self::with_devices( $r );
318 341 }
319 342
343 + /**
344 + * Give a stored record the per-device Lighthouse shape every reader now
345 + * expects, whenever it was written.
346 + *
347 + * A record written BEFORE the engine graded desktop as the headline has
348 + * no `device` and its `lighthouse` is the mobile run — that was the only
349 + * run PSI made by default. A record written after has `device` set and
350 + * carries both devices explicitly. So `device` is what decides how to
351 + * read `lighthouse`, and nothing else has to know the difference.
352 + *
353 + * Deliberately does NOT invent a missing device: a desktop-only scan
354 + * never measured mobile, and showing the desktop number under a mobile
355 + * label is exactly what this replaced.
356 + */
357 + private static function with_devices( array $r ): array {
358 + $m = isset( $r['measured'] ) && is_array( $r['measured'] ) ? $r['measured'] : array();
359 + if ( ! array_key_exists( 'lighthouse_mobile', $m ) ) {
360 + $m['lighthouse_mobile'] = '' === (string) ( $r['device'] ?? '' )
361 + ? ( $m['lighthouse'] ?? null ) // pre-change record: it was mobile
362 + : null; // post-change: mobile simply wasn't run
363 + }
364 + if ( ! array_key_exists( 'lighthouse_desktop', $m ) ) {
365 + $m['lighthouse_desktop'] = null;
366 + }
367 + $r['measured'] = $m;
368 + $r['device'] = (string) ( $r['device'] ?? '' );
369 + return $r;
370 + }
371 +
320 372 public static function report_url( string $scan_id ): string {
321 373 return self::base() . '/scan/r/' . $scan_id;
322 374 }
323 375
@@ -393,12 +445,25 @@
393 445 // A partial scan graded less than the full rubric; saying so is
394 446 // the difference between a low score and an incomplete one.
395 447 'partial' => ! empty( $body['partial'] ),
396 448 'dimensions' => $dims,
449 + // Which device the headline grade is for: `desktop` (the default
450 + // `both` run grades desktop as the headline), `mobile`, or ''
451 + // from an engine that predates the strategy option.
452 + 'device' => isset( $body['device'] ) ? (string) $body['device'] : '',
397 453 'measured' => array(
398 454 // Lighthouse is reported alongside, never AS, the score.
455 + //
456 + // `lighthouse` is the GRADED run's score, and which device
457 + // that is changed when the engine started grading desktop as
458 + // the headline. Read the two per-device fields instead; this
459 + // one stays only so a stored record keeps its shape, and
460 + // `latest()` back-fills it for records written before the
461 + // change. Reading it AS the mobile score is the bug this
462 + // comment exists to prevent (it printed desktop as mobile).
399 463 'lighthouse' => self::num( $measured['lighthouse'] ?? null ),
400 464 'lighthouse_desktop' => self::num( $measured['lighthouseDesktop'] ?? null ),
465 + 'lighthouse_mobile' => self::num( $measured['lighthouseMobile'] ?? null ),
401 466 'ttfb_ms' => self::num( $measured['ttfbMs'] ?? null ),
402 467 'lcp_ms' => self::num( $measured['lcpMs'] ?? null ),
403 468 'cls' => self::num( $measured['cls'] ?? null ),
404 469 'tbt_ms' => self::num( $measured['tbtMs'] ?? null ),