PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.32.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.32.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / includes / api / class-performance-endpoint.php

class-performance-endpoint.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.32.0, at includes/api/class-performance-endpoint.php

556 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Performance API Endpoint
4 *
5 * REST API endpoints for Core Web Vitals monitoring and performance insights.
6 * Connects the frontend Performance tab to the existing Performance Monitoring Manager.
7 *
8 * @package ThinkRank
9 * @subpackage API
10 * @since 1.0.0
11 */
12
13 namespace ThinkRank\API;
14
15 use ThinkRank\SEO\Performance_Monitoring_Manager;
16 use ThinkRank\SEO\Performance_Data_Collector;
17 use ThinkRank\SEO\Analytics_Manager;
18 use ThinkRank\API\Traits\API_Cache;
19 use WP_REST_Controller;
20 use WP_REST_Server;
21 use WP_REST_Request;
22 use WP_REST_Response;
23 use WP_Error;
24
25 // Prevent direct access
26 if (!defined('ABSPATH')) {
27 exit;
28 }
29
30 // Load API Cache trait
31 require_once THINKRANK_PLUGIN_DIR . 'includes/api/traits/trait-api-cache.php';
32
33 /**
34 * Performance API Endpoint Class
35 *
36 * @since 1.0.0
37 */
38 class Performance_Endpoint extends WP_REST_Controller {
39
40 use API_Cache;
41
42 /**
43 * REST API namespace
44 *
45 * @since 1.0.0
46 * @var string
47 */
48 protected $namespace = 'thinkrank/v1';
49
50 /**
51 * REST API base
52 *
53 * @since 1.0.0
54 * @var string
55 */
56 protected $rest_base = 'performance';
57
58 /**
59 * Performance Monitoring Manager instance
60 *
61 * @since 1.0.0
62 * @var Performance_Monitoring_Manager|null
63 */
64 private ?Performance_Monitoring_Manager $performance_manager = null;
65
66 /**
67 * Performance Data Collector instance (lazy)
68 *
69 * @since 1.0.0
70 * @var Performance_Data_Collector|null
71 */
72 private ?Performance_Data_Collector $data_collector = null;
73
74 /**
75 * Constructor
76 *
77 * Endpoint objects are constructed on every REST request (any namespace),
78 * so the manager chain is built lazily — only when one of this endpoint's
79 * routes actually executes.
80 *
81 * @since 1.0.0
82 */
83 public function __construct() {
84 // Configure caching for performance endpoints
85 $this->set_cache_prefix('thinkrank_performance_');
86 $this->set_cache_duration(300); // 5 minutes for performance data
87 }
88
89 /**
90 * Get the Performance Monitoring Manager (lazy)
91 *
92 * @return Performance_Monitoring_Manager
93 */
94 private function get_performance_manager(): Performance_Monitoring_Manager {
95 if ($this->performance_manager === null) {
96 $this->performance_manager = new Performance_Monitoring_Manager();
97 }
98 return $this->performance_manager;
99 }
100
101 /**
102 * Get the Performance Data Collector (lazy)
103 *
104 * @return Performance_Data_Collector
105 */
106 private function get_data_collector(): Performance_Data_Collector {
107 if ($this->data_collector === null) {
108 $this->data_collector = new Performance_Data_Collector();
109 }
110 return $this->data_collector;
111 }
112
113 /**
114 * Register REST API routes
115 *
116 * @since 1.0.0
117 */
118 public function register_routes() {
119 // Monitor endpoint - Get current performance data
120 register_rest_route(
121 $this->namespace,
122 '/' . $this->rest_base . '/monitor',
123 [
124 [
125 'methods' => WP_REST_Server::READABLE,
126 'callback' => [$this, 'get_performance_data'],
127 'permission_callback' => [$this, 'check_read_permissions'],
128 'args' => [
129 'device_type' => [
130 'default' => 'mobile',
131 'type' => 'string',
132 'enum' => ['mobile', 'desktop'],
133 'sanitize_callback' => 'sanitize_key'
134 ]
135 ]
136 ]
137 ]
138 );
139
140
141
142 // Recommendations endpoint
143 register_rest_route(
144 $this->namespace,
145 '/' . $this->rest_base . '/recommendations',
146 [
147 [
148 'methods' => WP_REST_Server::READABLE,
149 'callback' => [$this, 'get_recommendations'],
150 'permission_callback' => [$this, 'check_read_permissions']
151 ]
152 ]
153 );
154
155 // Historical data endpoint
156 register_rest_route(
157 $this->namespace,
158 '/' . $this->rest_base . '/history',
159 [
160 [
161 'methods' => WP_REST_Server::READABLE,
162 'callback' => [$this, 'get_historical_data'],
163 'permission_callback' => [$this, 'check_read_permissions'],
164 'args' => $this->get_historical_data_args()
165 ]
166 ]
167 );
168
169 // Opportunities endpoint
170 register_rest_route(
171 $this->namespace,
172 '/' . $this->rest_base . '/opportunities',
173 [
174 [
175 'methods' => WP_REST_Server::READABLE,
176 'callback' => [$this, 'get_opportunities'],
177 'permission_callback' => [$this, 'check_read_permissions'],
178 'args' => [
179 'device_type' => [
180 'default' => 'mobile',
181 'type' => 'string',
182 'enum' => ['mobile', 'desktop'],
183 'sanitize_callback' => 'sanitize_key'
184 ]
185 ]
186 ]
187 ]
188 );
189
190 // Diagnostics endpoint
191 register_rest_route(
192 $this->namespace,
193 '/' . $this->rest_base . '/diagnostics',
194 [
195 [
196 'methods' => WP_REST_Server::READABLE,
197 'callback' => [$this, 'get_diagnostics'],
198 'permission_callback' => [$this, 'check_read_permissions'],
199 'args' => [
200 'device_type' => [
201 'default' => 'mobile',
202 'type' => 'string',
203 'enum' => ['mobile', 'desktop'],
204 'sanitize_callback' => 'sanitize_key'
205 ]
206 ]
207 ]
208 ]
209 );
210
211 // Data collection endpoint
212 register_rest_route(
213 $this->namespace,
214 '/' . $this->rest_base . '/collect',
215 [
216 [
217 'methods' => WP_REST_Server::CREATABLE,
218 'callback' => [$this, 'collect_performance_data'],
219 'permission_callback' => [$this, 'check_manage_permissions']
220 ]
221 ]
222 );
223
224
225 }
226
227 /**
228 * Get comprehensive performance data
229 *
230 * @since 1.0.0
231 *
232 * @param WP_REST_Request $request Request object
233 * @return WP_REST_Response|WP_Error Response object or error
234 */
235 public function get_performance_data(WP_REST_Request $request) {
236 try {
237 // Get device type from request
238 $device_type = $request->get_param('device_type') ?? 'mobile';
239
240 $cache_params = ['device_type' => $device_type];
241 // Core Web Vitals are site-wide, not user-specific — a per-user
242 // cache key would duplicate the entry (and the cold-path work)
243 // for every admin user.
244 $user_id = null;
245 $cache_enabled = $this->is_caching_enabled();
246
247 // Serve a fresh cached response when available.
248 if ($cache_enabled) {
249 $cached = $this->get_cached_response('performance_data', $cache_params, $user_id);
250 if ($cached !== null) {
251 return new WP_REST_Response(
252 array_merge($cached['data'], [
253 'cached' => true,
254 'cached_at' => $cached['cached_at'],
255 ]),
256 200
257 );
258 }
259 }
260
261 // Serve from the existing cache / collected DB data first; only the
262 // background collector performs a live PageSpeed audit. A cold cache
263 // no longer blocks the request on a 10-40s inline Lighthouse run.
264 $response_data = $this->get_performance_manager()->get_performance_snapshot($device_type);
265
266 // Don't pin a transient "collecting" state in the response cache — the
267 // background collection must be re-checked on the next request.
268 if ($cache_enabled && ($response_data['data']['status'] ?? '') !== 'collecting') {
269 $this->set_cached_response('performance_data', $response_data, $cache_params, null, $user_id);
270 }
271
272 return new WP_REST_Response(array_merge($response_data, ['cached' => false]), 200);
273
274 } catch (\Exception $e) {
275 return new WP_Error(
276 'performance_data_failed',
277 'Failed to retrieve performance data: ' . $e->getMessage(),
278 ['status' => 500]
279 );
280 }
281 }
282
283
284
285
286
287 /**
288 * Get performance recommendations
289 *
290 * @since 1.0.0
291 *
292 * @param WP_REST_Request $request Request object
293 * @return WP_REST_Response|WP_Error Response object or error
294 */
295 public function get_recommendations(WP_REST_Request $request) {
296 try {
297 $recommendations = $this->get_performance_manager()->get_performance_recommendations();
298
299 return new WP_REST_Response([
300 'success' => true,
301 'data' => $recommendations,
302 'message' => __('Performance recommendations retrieved successfully', 'thinkrank')
303 ], 200);
304
305 } catch (\Exception $e) {
306 return new WP_Error(
307 'recommendations_failed',
308 'Failed to retrieve recommendations: ' . $e->getMessage(),
309 ['status' => 500]
310 );
311 }
312 }
313
314 /**
315 * Get historical performance data
316 *
317 * @since 1.0.0
318 *
319 * @param WP_REST_Request $request Request object
320 * @return WP_REST_Response|WP_Error Response object or error
321 */
322 public function get_historical_data(WP_REST_Request $request) {
323 try {
324 $days = $request->get_param('days') ?? 30;
325 $metric = $request->get_param('metric') ?? 'all';
326
327 $historical_data = $this->get_performance_manager()->get_historical_data($days, $metric);
328
329 return new WP_REST_Response([
330 'success' => true,
331 'data' => $historical_data,
332 'message' => __('Historical data retrieved successfully', 'thinkrank')
333 ], 200);
334
335 } catch (\Exception $e) {
336 return new WP_Error(
337 'historical_data_failed',
338 'Failed to retrieve historical data: ' . $e->getMessage(),
339 ['status' => 500]
340 );
341 }
342 }
343
344 /**
345 * Get performance opportunities
346 *
347 * @since 1.0.0
348 *
349 * @param WP_REST_Request $request Request object
350 * @return WP_REST_Response|WP_Error Response object or error
351 */
352 public function get_opportunities(WP_REST_Request $request) {
353 try {
354 // Get device type from request
355 $device_type = $request->get_param('device_type') ?? 'mobile';
356
357 $opportunities = $this->get_performance_manager()->get_performance_opportunities('', $device_type);
358
359 return new WP_REST_Response([
360 'success' => true,
361 'data' => $opportunities,
362 'device_type' => $device_type,
363 'message' => __('Performance opportunities retrieved successfully', 'thinkrank')
364 ], 200);
365
366 } catch (\Exception $e) {
367 return new WP_Error(
368 'opportunities_failed',
369 'Failed to retrieve performance opportunities: ' . $e->getMessage(),
370 ['status' => 500]
371 );
372 }
373 }
374
375 /**
376 * Get performance diagnostics
377 *
378 * @since 1.0.0
379 *
380 * @param WP_REST_Request $request Request object
381 * @return WP_REST_Response|WP_Error Response object or error
382 */
383 public function get_diagnostics(WP_REST_Request $request) {
384 try {
385 // Get device type from request
386 $device_type = $request->get_param('device_type') ?? 'mobile';
387
388 $diagnostics = $this->get_performance_manager()->get_performance_diagnostics('', $device_type);
389
390 return new WP_REST_Response([
391 'success' => true,
392 'data' => $diagnostics,
393 'device_type' => $device_type,
394 'message' => __('Performance diagnostics retrieved successfully', 'thinkrank')
395 ], 200);
396
397 } catch (\Exception $e) {
398 return new WP_Error(
399 'diagnostics_failed',
400 'Failed to retrieve performance diagnostics: ' . $e->getMessage(),
401 ['status' => 500]
402 );
403 }
404 }
405
406 /**
407 * Manually collect performance data
408 *
409 * @since 1.0.0
410 *
411 * @param WP_REST_Request $request Request object
412 * @return WP_REST_Response|WP_Error Response object or error
413 */
414 public function collect_performance_data(WP_REST_Request $request) {
415 try {
416 $results = $this->get_data_collector()->manual_collect();
417
418 if (!empty($results['success'])) {
419 return new WP_REST_Response([
420 'success' => true,
421 'data' => $results,
422 'message' => $results['message']
423 ], 200);
424 }
425
426 // A failure here is almost never a server fault: the site is not
427 // connected, Google cannot reach the URL, or the quota is spent. This
428 // used to answer 500 for all of them, with a hardcoded message that
429 // dropped the real reason, so the user could neither tell what was
430 // wrong nor that it was their configuration rather than a bug.
431 // Return a WP_Error like every other failure in this file, so clients
432 // get the normal code/message envelope instead of a 200-shaped body
433 // carrying a 500.
434 return new WP_Error(
435 $this->collection_error_code((string) ($results['error_code'] ?? '')),
436 $results['message'],
437 $this->collection_error_data((string) ($results['error_code'] ?? ''))
438 );
439
440 } catch (\Exception $e) {
441 return new WP_Error(
442 'data_collection_failed',
443 'Failed to collect performance data: ' . $e->getMessage(),
444 ['status' => 500]
445 );
446 }
447 }
448
449 /**
450 * REST error code for a collection failure class.
451 *
452 * @since 1.31.0
453 * @param string $error_code One of Performance_Data_Collector::ERROR_*.
454 * @return string
455 */
456 private function collection_error_code(string $error_code): string {
457 $codes = [
458 Performance_Data_Collector::ERROR_NOT_CONFIGURED => 'pagespeed_not_configured',
459 Performance_Data_Collector::ERROR_URL_UNREACHABLE => 'site_not_reachable',
460 Performance_Data_Collector::ERROR_RATE_LIMITED => 'pagespeed_rate_limited',
461 Performance_Data_Collector::ERROR_RECENT_FAILURE => 'pagespeed_recently_failed',
462 Performance_Data_Collector::ERROR_STORAGE_FAILED => 'performance_storage_failed',
463 ];
464
465 return $codes[$error_code] ?? 'data_collection_failed';
466 }
467
468 /**
469 * HTTP status (and Retry-After, where it applies) for a failure class.
470 *
471 * @since 1.31.0
472 * @param string $error_code One of Performance_Data_Collector::ERROR_*.
473 * @return array Error data for WP_Error.
474 */
475 private function collection_error_data(string $error_code): array {
476 switch ($error_code) {
477 case Performance_Data_Collector::ERROR_NOT_CONFIGURED:
478 // Client-side condition: no credential to call PageSpeed with.
479 return ['status' => 400];
480
481 case Performance_Data_Collector::ERROR_URL_UNREACHABLE:
482 // The request was well-formed and authorised; the site simply
483 // cannot be fetched by Google.
484 return ['status' => 422];
485
486 case Performance_Data_Collector::ERROR_RATE_LIMITED:
487 return ['status' => 429];
488
489 case Performance_Data_Collector::ERROR_RECENT_FAILURE:
490 // Nothing was attempted — a recent failure is still remembered.
491 return ['status' => 503, 'retry_after' => 300];
492
493 case Performance_Data_Collector::ERROR_STORAGE_FAILED:
494 // Measured fine but the write failed: genuinely our side.
495 return ['status' => 500];
496
497 default:
498 // An upstream API error we could not classify.
499 return ['status' => 502];
500 }
501 }
502
503
504
505 /**
506 * Check read permissions
507 *
508 * @since 1.0.0
509 *
510 * @return bool True if user can read
511 */
512 public function check_read_permissions(): bool {
513 // Performance data + settings are not subscriber-visible — require the
514 // same Performance management capability as the write routes.
515 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_performance');
516 }
517
518 /**
519 * Check manage permissions
520 *
521 * @since 1.0.0
522 *
523 * @return bool True if user can manage options
524 */
525 public function check_manage_permissions(): bool {
526 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_performance');
527 }
528
529 /**
530 * Get arguments for historical data endpoint
531 *
532 * @since 1.0.0
533 *
534 * @return array Arguments array
535 */
536 private function get_historical_data_args(): array {
537 return [
538 'days' => [
539 'required' => false,
540 'type' => 'integer',
541 'default' => 30,
542 'minimum' => 1,
543 'maximum' => 365,
544 'description' => 'Number of days of historical data to retrieve'
545 ],
546 'metric' => [
547 'required' => false,
548 'type' => 'string',
549 'default' => 'all',
550 'enum' => ['all', 'lcp', 'cls', 'inp', 'score'],
551 'description' => 'Specific metric to retrieve'
552 ]
553 ];
554 }
555 }
556