# xspeed/1.1.7/includes/class-pro-audit.php

xSpeed Cache: AI-Powered Performance Hub with MCP, Caching &amp; CDN, version 1.1.7. 252 lines.

- Page: https://pluginprobe.com/plugins/xspeed/1.1.7/code/includes/class-pro-audit.php
- Raw: https://pluginprobe.com/plugins/xspeed/1.1.7/raw/includes/class-pro-audit.php
- Modified: 2026-08-09T20:11:50+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/xspeed/1.1.7/code/includes/class-pro-audit.php#L10-L20`.

```php
<?php
/**
 * Pro_Audit — scans the current Free configuration + cache stats and
 * surfaces Pro features that would specifically help THIS site.
 *
 * Powers the dashboard's "Run Pro audit" button. The point isn't to
 * list every Pro feature; it's to make each suggestion personal
 * ("Cache hit ratio is 38% → Pro Recommendations would tell you why")
 * so the user converts because Pro solves a problem they actually
 * see, not because we shouted "BUY NOW."
 *
 * Pure read-only. Returns an ordered list of suggestions:
 *
 *   [ id, severity ('high'|'med'|'low'), reason, fact ]
 *
 *   - id      → matches a key in PRO_FEATURES (the React catalog),
 *               so the panel renders title/body without duplicating
 *               copy here.
 *   - severity controls sort order + visual tone.
 *   - reason  → one-sentence explanation specific to this site's
 *               state. Already-baked numbers/percentages so the
 *               React side just prints it.
 *   - fact    → optional shorter inline stat (e.g. "38%") for the
 *               result card's chip.
 *
 * Adding a rule: drop another `if (…) $out[] = …` block in run().
 * Rules are independent — keep them small + concrete + factual.
 *
 * @package XSpeed
 */

declare(strict_types=1);

namespace XSpeed;

defined( 'ABSPATH' ) || exit;

final class Pro_Audit {

	public const SEVERITY_HIGH = 'high';
	public const SEVERITY_MED  = 'med';
	public const SEVERITY_LOW  = 'low';

	/**
	 * Snapshot of state every rule needs. Computed once per audit run
	 * so we don't read the same option 8 times.
	 *
	 * @param array|null $totals_override Test injection — Brain Monkey
	 *                                    can't mock static class methods,
	 *                                    so tests synthesize the 24h
	 *                                    counter shape here directly.
	 *                                    Production callers leave null.
	 *
	 * @return array<string,mixed>
	 */
	private static function snapshot( ?array $totals_override = null ): array {
		$opts = static function ( string $slug ): array {
			return (array) get_option( 'xspeed_module_' . $slug, array() );
		};
		if ( null !== $totals_override ) {
			$totals = $totals_override;
		} elseif ( class_exists( '\\XSpeed\\Hit_Counter' ) ) {
			$totals = Hit_Counter::totals_24h();
		} else {
			$totals = array( 'hits' => 0, 'misses' => 0, 'excluded' => 0, 'ratio' => 0.0 );
		}
		$cloudflare = $opts( 'cloudflare' );
		return array(
			'cache'         => $opts( 'cache' ),
			'minify'        => $opts( 'minify' ),
			'lazy'          => $opts( 'lazy' ),
			'gzip'          => $opts( 'gzip' ),
			'browser_cache' => $opts( 'browser-cache' ),
			'cloudflare'    => $cloudflare,
			'cdn'           => $opts( 'cdn' ),
			'database'      => $opts( 'database' ),
			'preloader'     => $opts( 'preloader' ),
			'heartbeat'     => $opts( 'heartbeat' ),
			'cache_enabled' => class_exists( '\\XSpeed\\Settings' )
				? ! empty( Settings::get()['cache_enabled'] )
				: false,
			// An edge cache (Cloudflare) in front means the origin hit ratio is
			// only the origin layer — hits served at the edge never reach PHP —
			// so a low number is an attribution artefact, not a cache problem.
			// Rule 2 must not fire an upsell off it. (#118)
			'edge_cache'    => ! empty( $cloudflare['enabled'] ),
			'totals_24h'    => array(
				'hits'     => (int) ( $totals['hits'] ?? 0 ),
				'misses'   => (int) ( $totals['misses'] ?? 0 ),
				// 404s + bots, kept out of the ratio denominator. (#118)
				'excluded' => (int) ( $totals['excluded'] ?? 0 ),
				'total'    => (int) ( $totals['hits'] ?? 0 ) + (int) ( $totals['misses'] ?? 0 ),
				'ratio'    => (float) ( $totals['ratio'] ?? 0.0 ),
			),
		);
	}

	/**
	 * @param array|null $totals_override See snapshot(). Production
	 *                                    callers pass nothing.
	 *
	 * @return array<int,array{id:string,severity:string,reason:string,fact?:string}>
	 */
	public static function run( ?array $totals_override = null ): array {
		$s   = self::snapshot( $totals_override );
		$out = array();

		// Rule 1 — Cloudflare connected but APO not in use.
		// High signal: user already pays the Cloudflare overhead, APO
		// is the highest-leverage Pro feature they can flip on next.
		if ( ! empty( $s['cloudflare']['enabled'] ) ) {
			$out[] = array(
				'id'       => 'cloudflare-apo',
				'severity' => self::SEVERITY_HIGH,
				'reason'   => 'Cloudflare is already connected. Pro adds Automatic Platform Optimization, which edge-caches your HTML — typically cuts TTFB in half.',
				'fact'     => 'Cloudflare on',
			);
		}

		// Rule 2 — Low cache hit ratio with meaningful traffic.
		// "Meaningful" = > 50 hits over 24h; below that the ratio is
		// statistical noise and we'd suggest based on bad data. The ratio is
		// now computed over real traffic only (404s + bots excluded, #118), and
		// we skip it entirely when an edge cache fronts the origin — behind
		// Cloudflare a low origin ratio means hits are served at the edge, not
		// that the cache is failing, so firing a "your cache is bad" upsell off
		// it is selling against a measurement artefact.
		if ( empty( $s['edge_cache'] )
			&& $s['totals_24h']['total'] >= 50
			&& $s['totals_24h']['ratio'] < 0.5 ) {
			$pct   = (int) round( $s['totals_24h']['ratio'] * 100 );
			$out[] = array(
				'id'       => 'recommendations',
				'severity' => self::SEVERITY_HIGH,
				'reason'   => sprintf(
					'Cache hit ratio is %d%% over the last 24h. Pro Recommendations identifies which URLs miss the cache and why, with one-click fixes.',
					$pct
				),
				'fact'     => $pct . '% hit',
			);
		}

		// Rule 3 — Lazy-load enabled but no auto WebP/AVIF.
		// User cares about images (lazy on) → next gain is format.
		if ( ! empty( $s['lazy']['lazy_images'] ) ) {
			$out[] = array(
				'id'       => 'webp-avif',
				'severity' => self::SEVERITY_MED,
				'reason'   => 'Images are lazy-loaded. Pro auto-converts new JPEG/PNG uploads to WebP and AVIF — typically 25-35% smaller at the same visual quality.',
			);
		}

		// Rule 4 — High traffic without RUM data.
		// Real-user metrics matter more than synthetic Lighthouse when
		// the site has actual visitors.
		if ( $s['totals_24h']['total'] >= 100 ) {
			$views = number_format( $s['totals_24h']['total'] );
			$out[] = array(
				'id'       => 'rum',
				'severity' => self::SEVERITY_MED,
				'reason'   => sprintf(
					'You served %s requests in 24h. Pro RUM samples actual LCP, CLS and INP from those visitors — Lighthouse only simulates one device, one connection.',
					$views
				),
				'fact'     => $views . ' / 24h',
			);
		}

		// Rule 5 — HTML minify on but JS minify off (theme-safe stance).
		// Suggest Critical CSS as the next gain that doesn't touch JS.
		if ( ! empty( $s['minify']['minify_html'] ) && empty( $s['minify']['minify_js'] ) ) {
			$out[] = array(
				'id'       => 'critical-css',
				'severity' => self::SEVERITY_MED,
				'reason'   => 'JS minify is off (good — high theme-conflict risk). Pro Critical CSS delivers similar first-paint gains without touching JavaScript.',
			);
		}

		// Rule 6 — Database cleanup on manual schedule.
		// Only fire when the user has actually configured the Database
		// module (has saved options). Empty option = user hasn't
		// touched it; don't suggest scheduling something they might
		// never use.
		if ( ! empty( $s['database'] ) && 'manual' === ( $s['database']['schedule'] ?? 'manual' ) ) {
			$out[] = array(
				'id'       => 'recommendations',
				'severity' => self::SEVERITY_LOW,
				'reason'   => 'Database cleanup is set to manual. Pro Recommendations engine auto-schedules cleanups based on smart triggers (after publish, before backup).',
			);
		}

		// Rule 7 — Agency / professional usage signal.
		// >= 5 enabled modules suggests serious use → white-label is
		// what they'd actually want next.
		$enabled = 0;
		foreach ( array( 'minify', 'gzip', 'lazy', 'browser_cache', 'cloudflare', 'cdn', 'preloader' ) as $k ) {
			if ( ! empty( $s[ $k ]['enabled'] ) ) {
				$enabled++;
			}
		}
		if ( $s['cache_enabled'] ) {
			$enabled++;
		}
		if ( $enabled >= 5 ) {
			$out[] = array(
				'id'       => 'white-label',
				'severity' => self::SEVERITY_LOW,
				'reason'   => sprintf(
					'You\'ve configured %d modules — looks like agency work. Pro White-Label rebrands the dashboard chrome for client handoff.',
					$enabled
				),
				'fact'     => $enabled . ' modules',
			);
		}

		// Fallback — never return an empty audit. Analytics is the
		// safe always-relevant suggestion (every site has cache
		// activity to chart).
		if ( empty( $out ) ) {
			$out[] = array(
				'id'       => 'analytics',
				'severity' => self::SEVERITY_LOW,
				'reason'   => 'See which pages benefit most from caching, where your slow URLs are, and your hit-ratio over time.',
			);
		}

		// Dedupe by id, keeping the highest-severity rule per feature.
		// Rules independently suggest the same feature for different
		// reasons; pick the strongest reason to show.
		$by_id = array();
		$order = array( self::SEVERITY_HIGH => 0, self::SEVERITY_MED => 1, self::SEVERITY_LOW => 2 );
		foreach ( $out as $row ) {
			$id = $row['id'];
			if ( ! isset( $by_id[ $id ] ) ) {
				$by_id[ $id ] = $row;
				continue;
			}
			$existing_rank = $order[ $by_id[ $id ]['severity'] ] ?? 9;
			$new_rank      = $order[ $row['severity'] ] ?? 9;
			if ( $new_rank < $existing_rank ) {
				$by_id[ $id ] = $row;
			}
		}

		$out = array_values( $by_id );
		usort( $out, static function ( $a, $b ) use ( $order ) {
			return ( $order[ $a['severity'] ] ?? 9 ) <=> ( $order[ $b['severity'] ] ?? 9 );
		} );
		return $out;
	}
}

```
