PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.0.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.0.4
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 1.2.0 All 28 releases
xspeed / includes / class-pro-audit.php

class-pro-audit.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.0.4, at includes/class-pro-audit.php

237 lines 8.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Pro_Audit — scans the current Free configuration + cache stats and
4 * surfaces Pro features that would specifically help THIS site.
5 *
6 * Powers the dashboard's "Run Pro audit" button. The point isn't to
7 * list every Pro feature; it's to make each suggestion personal
8 * ("Cache hit ratio is 38% → Pro Recommendations would tell you why")
9 * so the user converts because Pro solves a problem they actually
10 * see, not because we shouted "BUY NOW."
11 *
12 * Pure read-only. Returns an ordered list of suggestions:
13 *
14 * [ id, severity ('high'|'med'|'low'), reason, fact ]
15 *
16 * - id → matches a key in PRO_FEATURES (the React catalog),
17 * so the panel renders title/body without duplicating
18 * copy here.
19 * - severity controls sort order + visual tone.
20 * - reason → one-sentence explanation specific to this site's
21 * state. Already-baked numbers/percentages so the
22 * React side just prints it.
23 * - fact → optional shorter inline stat (e.g. "38%") for the
24 * result card's chip.
25 *
26 * Adding a rule: drop another `if (…) $out[] = …` block in run().
27 * Rules are independent — keep them small + concrete + factual.
28 *
29 * @package XSpeed
30 */
31
32 declare(strict_types=1);
33
34 namespace XSpeed;
35
36 defined( 'ABSPATH' ) || exit;
37
38 final class Pro_Audit {
39
40 public const SEVERITY_HIGH = 'high';
41 public const SEVERITY_MED = 'med';
42 public const SEVERITY_LOW = 'low';
43
44 /**
45 * Snapshot of state every rule needs. Computed once per audit run
46 * so we don't read the same option 8 times.
47 *
48 * @param array|null $totals_override Test injection — Brain Monkey
49 * can't mock static class methods,
50 * so tests synthesize the 24h
51 * counter shape here directly.
52 * Production callers leave null.
53 *
54 * @return array<string,mixed>
55 */
56 private static function snapshot( ?array $totals_override = null ): array {
57 $opts = static function ( string $slug ): array {
58 return (array) get_option( 'xspeed_module_' . $slug, array() );
59 };
60 if ( null !== $totals_override ) {
61 $totals = $totals_override;
62 } elseif ( class_exists( '\\XSpeed\\Hit_Counter' ) ) {
63 $totals = Hit_Counter::totals_24h();
64 } else {
65 $totals = array( 'hits' => 0, 'misses' => 0, 'ratio' => 0.0 );
66 }
67 return array(
68 'cache' => $opts( 'cache' ),
69 'minify' => $opts( 'minify' ),
70 'lazy' => $opts( 'lazy' ),
71 'gzip' => $opts( 'gzip' ),
72 'browser_cache' => $opts( 'browser-cache' ),
73 'cloudflare' => $opts( 'cloudflare' ),
74 'cdn' => $opts( 'cdn' ),
75 'database' => $opts( 'database' ),
76 'preloader' => $opts( 'preloader' ),
77 'heartbeat' => $opts( 'heartbeat' ),
78 'cache_enabled' => class_exists( '\\XSpeed\\Settings' )
79 ? ! empty( Settings::get()['cache_enabled'] )
80 : false,
81 'totals_24h' => array(
82 'hits' => (int) ( $totals['hits'] ?? 0 ),
83 'misses' => (int) ( $totals['misses'] ?? 0 ),
84 'total' => (int) ( $totals['hits'] ?? 0 ) + (int) ( $totals['misses'] ?? 0 ),
85 'ratio' => (float) ( $totals['ratio'] ?? 0.0 ),
86 ),
87 );
88 }
89
90 /**
91 * @param array|null $totals_override See snapshot(). Production
92 * callers pass nothing.
93 *
94 * @return array<int,array{id:string,severity:string,reason:string,fact?:string}>
95 */
96 public static function run( ?array $totals_override = null ): array {
97 $s = self::snapshot( $totals_override );
98 $out = array();
99
100 // Rule 1 — Cloudflare connected but APO not in use.
101 // High signal: user already pays the Cloudflare overhead, APO
102 // is the highest-leverage Pro feature they can flip on next.
103 if ( ! empty( $s['cloudflare']['enabled'] ) ) {
104 $out[] = array(
105 'id' => 'cloudflare-apo',
106 'severity' => self::SEVERITY_HIGH,
107 'reason' => 'Cloudflare is already connected. Pro adds Automatic Platform Optimization, which edge-caches your HTML — typically cuts TTFB in half.',
108 'fact' => 'Cloudflare on',
109 );
110 }
111
112 // Rule 2 — Low cache hit ratio with meaningful traffic.
113 // "Meaningful" = > 50 hits over 24h; below that the ratio is
114 // statistical noise and we'd suggest based on bad data.
115 if ( $s['totals_24h']['total'] >= 50 && $s['totals_24h']['ratio'] < 0.5 ) {
116 $pct = (int) round( $s['totals_24h']['ratio'] * 100 );
117 $out[] = array(
118 'id' => 'recommendations',
119 'severity' => self::SEVERITY_HIGH,
120 'reason' => sprintf(
121 'Cache hit ratio is %d%% over the last 24h. Pro Recommendations identifies which URLs miss the cache and why, with one-click fixes.',
122 $pct
123 ),
124 'fact' => $pct . '% hit',
125 );
126 }
127
128 // Rule 3 — Lazy-load enabled but no auto WebP/AVIF.
129 // User cares about images (lazy on) → next gain is format.
130 if ( ! empty( $s['lazy']['lazy_images'] ) ) {
131 $out[] = array(
132 'id' => 'webp-avif',
133 'severity' => self::SEVERITY_MED,
134 '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.',
135 );
136 }
137
138 // Rule 4 — High traffic without RUM data.
139 // Real-user metrics matter more than synthetic Lighthouse when
140 // the site has actual visitors.
141 if ( $s['totals_24h']['total'] >= 100 ) {
142 $views = number_format( $s['totals_24h']['total'] );
143 $out[] = array(
144 'id' => 'rum',
145 'severity' => self::SEVERITY_MED,
146 'reason' => sprintf(
147 'You served %s requests in 24h. Pro RUM samples actual LCP, CLS and INP from those visitors — Lighthouse only simulates one device, one connection.',
148 $views
149 ),
150 'fact' => $views . ' / 24h',
151 );
152 }
153
154 // Rule 5 — HTML minify on but JS minify off (theme-safe stance).
155 // Suggest Critical CSS as the next gain that doesn't touch JS.
156 if ( ! empty( $s['minify']['minify_html'] ) && empty( $s['minify']['minify_js'] ) ) {
157 $out[] = array(
158 'id' => 'critical-css',
159 'severity' => self::SEVERITY_MED,
160 'reason' => 'JS minify is off (good — high theme-conflict risk). Pro Critical CSS delivers similar first-paint gains without touching JavaScript.',
161 );
162 }
163
164 // Rule 6 — Database cleanup on manual schedule.
165 // Only fire when the user has actually configured the Database
166 // module (has saved options). Empty option = user hasn't
167 // touched it; don't suggest scheduling something they might
168 // never use.
169 if ( ! empty( $s['database'] ) && 'manual' === ( $s['database']['schedule'] ?? 'manual' ) ) {
170 $out[] = array(
171 'id' => 'recommendations',
172 'severity' => self::SEVERITY_LOW,
173 'reason' => 'Database cleanup is set to manual. Pro Recommendations engine auto-schedules cleanups based on smart triggers (after publish, before backup).',
174 );
175 }
176
177 // Rule 7 — Agency / professional usage signal.
178 // >= 5 enabled modules suggests serious use → white-label is
179 // what they'd actually want next.
180 $enabled = 0;
181 foreach ( array( 'minify', 'gzip', 'lazy', 'browser_cache', 'cloudflare', 'cdn', 'preloader' ) as $k ) {
182 if ( ! empty( $s[ $k ]['enabled'] ) ) {
183 $enabled++;
184 }
185 }
186 if ( $s['cache_enabled'] ) {
187 $enabled++;
188 }
189 if ( $enabled >= 5 ) {
190 $out[] = array(
191 'id' => 'white-label',
192 'severity' => self::SEVERITY_LOW,
193 'reason' => sprintf(
194 'You\'ve configured %d modules — looks like agency work. Pro White-Label rebrands the dashboard chrome for client handoff.',
195 $enabled
196 ),
197 'fact' => $enabled . ' modules',
198 );
199 }
200
201 // Fallback — never return an empty audit. Analytics is the
202 // safe always-relevant suggestion (every site has cache
203 // activity to chart).
204 if ( empty( $out ) ) {
205 $out[] = array(
206 'id' => 'analytics',
207 'severity' => self::SEVERITY_LOW,
208 'reason' => 'See which pages benefit most from caching, where your slow URLs are, and your hit-ratio over time.',
209 );
210 }
211
212 // Dedupe by id, keeping the highest-severity rule per feature.
213 // Rules independently suggest the same feature for different
214 // reasons; pick the strongest reason to show.
215 $by_id = array();
216 $order = array( self::SEVERITY_HIGH => 0, self::SEVERITY_MED => 1, self::SEVERITY_LOW => 2 );
217 foreach ( $out as $row ) {
218 $id = $row['id'];
219 if ( ! isset( $by_id[ $id ] ) ) {
220 $by_id[ $id ] = $row;
221 continue;
222 }
223 $existing_rank = $order[ $by_id[ $id ]['severity'] ] ?? 9;
224 $new_rank = $order[ $row['severity'] ] ?? 9;
225 if ( $new_rank < $existing_rank ) {
226 $by_id[ $id ] = $row;
227 }
228 }
229
230 $out = array_values( $by_id );
231 usort( $out, static function ( $a, $b ) use ( $order ) {
232 return ( $order[ $a['severity'] ] ?? 9 ) <=> ( $order[ $b['severity'] ] ?? 9 );
233 } );
234 return $out;
235 }
236 }
237