PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / trunk
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO vtrunk
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 1.11.0 All 47 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 trunk, at includes/api/class-performance-endpoint.php

596 lines 19.6 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 $manager = $this->get_performance_manager();
358 $opportunities = $manager->get_performance_opportunities('', $device_type);
359
360 return $this->pagespeed_list_response(
361 $manager,
362 $opportunities,
363 $device_type,
364 __('Performance opportunities retrieved successfully', 'thinkrank')
365 );
366
367 } catch (\Exception $e) {
368 return new WP_Error(
369 'opportunities_failed',
370 'Failed to retrieve performance opportunities: ' . $e->getMessage(),
371 ['status' => 500]
372 );
373 }
374 }
375
376 /**
377 * Get performance diagnostics
378 *
379 * @since 1.0.0
380 *
381 * @param WP_REST_Request $request Request object
382 * @return WP_REST_Response|WP_Error Response object or error
383 */
384 public function get_diagnostics(WP_REST_Request $request) {
385 try {
386 // Get device type from request
387 $device_type = $request->get_param('device_type') ?? 'mobile';
388
389 $manager = $this->get_performance_manager();
390 $diagnostics = $manager->get_performance_diagnostics('', $device_type);
391
392 return $this->pagespeed_list_response(
393 $manager,
394 $diagnostics,
395 $device_type,
396 __('Performance diagnostics retrieved successfully', 'thinkrank')
397 );
398
399 } catch (\Exception $e) {
400 return new WP_Error(
401 'diagnostics_failed',
402 'Failed to retrieve performance diagnostics: ' . $e->getMessage(),
403 ['status' => 500]
404 );
405 }
406 }
407
408 /**
409 * Manually collect performance data
410 *
411 * @since 1.0.0
412 *
413 * @param WP_REST_Request $request Request object
414 * @return WP_REST_Response|WP_Error Response object or error
415 */
416 public function collect_performance_data(WP_REST_Request $request) {
417 try {
418 $results = $this->get_data_collector()->manual_collect();
419
420 if (!empty($results['success'])) {
421 return new WP_REST_Response([
422 'success' => true,
423 'data' => $results,
424 'message' => $results['message']
425 ], 200);
426 }
427
428 // A failure here is almost never a server fault: the site is not
429 // connected, Google cannot reach the URL, or the quota is spent. This
430 // used to answer 500 for all of them, with a hardcoded message that
431 // dropped the real reason, so the user could neither tell what was
432 // wrong nor that it was their configuration rather than a bug.
433 // Return a WP_Error like every other failure in this file, so clients
434 // get the normal code/message envelope instead of a 200-shaped body
435 // carrying a 500.
436 return new WP_Error(
437 $this->collection_error_code((string) ($results['error_code'] ?? '')),
438 $results['message'],
439 $this->collection_error_data((string) ($results['error_code'] ?? ''))
440 );
441
442 } catch (\Exception $e) {
443 return new WP_Error(
444 'data_collection_failed',
445 'Failed to collect performance data: ' . $e->getMessage(),
446 ['status' => 500]
447 );
448 }
449 }
450
451 /**
452 * REST error code for a collection failure class.
453 *
454 * @since 1.31.0
455 * @param string $error_code One of Performance_Data_Collector::ERROR_*.
456 * @return string
457 */
458 private function collection_error_code(string $error_code): string {
459 $codes = [
460 Performance_Data_Collector::ERROR_NOT_CONFIGURED => 'pagespeed_not_configured',
461 Performance_Data_Collector::ERROR_URL_UNREACHABLE => 'site_not_reachable',
462 Performance_Data_Collector::ERROR_RATE_LIMITED => 'pagespeed_rate_limited',
463 Performance_Data_Collector::ERROR_RECENT_FAILURE => 'pagespeed_recently_failed',
464 Performance_Data_Collector::ERROR_STORAGE_FAILED => 'performance_storage_failed',
465 ];
466
467 return $codes[$error_code] ?? 'data_collection_failed';
468 }
469
470 /**
471 * HTTP status (and Retry-After, where it applies) for a failure class.
472 *
473 * @since 1.31.0
474 * @param string $error_code One of Performance_Data_Collector::ERROR_*.
475 * @return array Error data for WP_Error.
476 */
477 private function collection_error_data(string $error_code): array {
478 switch ($error_code) {
479 case Performance_Data_Collector::ERROR_NOT_CONFIGURED:
480 // Client-side condition: no credential to call PageSpeed with.
481 return ['status' => 400];
482
483 case Performance_Data_Collector::ERROR_URL_UNREACHABLE:
484 // The request was well-formed and authorised; the site simply
485 // cannot be fetched by Google.
486 return ['status' => 422];
487
488 case Performance_Data_Collector::ERROR_RATE_LIMITED:
489 return ['status' => 429];
490
491 case Performance_Data_Collector::ERROR_RECENT_FAILURE:
492 // Nothing was attempted — a recent failure is still remembered.
493 return ['status' => 503, 'retry_after' => 300];
494
495 case Performance_Data_Collector::ERROR_STORAGE_FAILED:
496 // Measured fine but the write failed: genuinely our side.
497 return ['status' => 500];
498
499 default:
500 // An upstream API error we could not classify.
501 return ['status' => 502];
502 }
503 }
504
505
506
507 /**
508 * Check read permissions
509 *
510 * @since 1.0.0
511 *
512 * @return bool True if user can read
513 */
514 public function check_read_permissions(): bool {
515 // Performance data + settings are not subscriber-visible — require the
516 // same Performance management capability as the write routes.
517 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_performance');
518 }
519
520 /**
521 * Check manage permissions
522 *
523 * @since 1.0.0
524 *
525 * @return bool True if user can manage options
526 */
527 public function check_manage_permissions(): bool {
528 return \ThinkRank\Core\Capability_Manager::current_user_can('thinkrank_performance');
529 }
530
531 /**
532 * Get arguments for historical data endpoint
533 *
534 * @since 1.0.0
535 *
536 * @return array Arguments array
537 */
538 /**
539 * Wrap a PageSpeed-backed list, reporting whether it was actually fetched.
540 *
541 * An empty list used to come back as `success: true` /
542 * "retrieved successfully" whether the site was clean, the request had
543 * failed, or nothing had been attempted for want of a credential — so no
544 * API or MCP consumer could tell the three apart, and the admin UI told
545 * everyone to connect Google (#519). The list itself keeps its shape.
546 *
547 * @since 2.1.1
548 *
549 * @param Performance_Monitoring_Manager $manager Manager that produced the list.
550 * @param array $data The list.
551 * @param string $device_type Device the list is for.
552 * @param string $success_message Message for a completed request.
553 * @return WP_REST_Response
554 */
555 private function pagespeed_list_response(Performance_Monitoring_Manager $manager, array $data, string $device_type, string $success_message): WP_REST_Response {
556 $error = $manager->get_last_error();
557
558 if ('' !== $error['code']) {
559 return new WP_REST_Response([
560 'success' => false,
561 'data' => $data,
562 'device_type' => $device_type,
563 'error_code' => $error['code'],
564 'message' => $error['message'],
565 ], 200);
566 }
567
568 return new WP_REST_Response([
569 'success' => true,
570 'data' => $data,
571 'device_type' => $device_type,
572 'message' => $success_message,
573 ], 200);
574 }
575
576 private function get_historical_data_args(): array {
577 return [
578 'days' => [
579 'required' => false,
580 'type' => 'integer',
581 'default' => 30,
582 'minimum' => 1,
583 'maximum' => 365,
584 'description' => 'Number of days of historical data to retrieve'
585 ],
586 'metric' => [
587 'required' => false,
588 'type' => 'string',
589 'default' => 'all',
590 'enum' => ['all', 'lcp', 'cls', 'inp', 'score'],
591 'description' => 'Specific metric to retrieve'
592 ]
593 ];
594 }
595 }
596