PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.10.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.10.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 / seo / class-analytics-manager.php

class-analytics-manager.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.10.0, at includes/seo/class-analytics-manager.php

1,233 lines 44.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Analytics Manager Class
5 *
6 * Coordinates Google API integrations for SEO analytics data collection,
7 * processing, and AI-powered insights generation. Manages Google Analytics,
8 * Search Console, and PageSpeed data with intelligent caching and rate limiting.
9 *
10 * @package ThinkRank
11 * @subpackage SEO
12 * @since 1.0.0
13 */
14
15 declare(strict_types=1);
16
17 namespace ThinkRank\SEO;
18
19 use ThinkRank\Core\Settings_Manager;
20 use ThinkRank\Integrations\Google_Analytics_Client;
21 use ThinkRank\Integrations\Google_Search_Console_Client;
22 use ThinkRank\Integrations\Google_PageSpeed_Client;
23 use ThinkRank\Integrations\Google_Search_Analytics_Client;
24
25 // Prevent direct access
26 if (!defined('ABSPATH')) {
27 exit;
28 }
29
30 /**
31 * Analytics Manager Class
32 *
33 * Single Responsibility: Coordinate Google API data collection and processing
34 * Following ThinkRank manager patterns from AI_Manager and Performance_Monitoring_Manager
35 *
36 * @since 1.0.0
37 */
38 class Analytics_Manager {
39
40 /**
41 * Google Client ID
42 * @var string
43 */
44 private const THINKRANK_GOOGLE_CLIENT_ID = '435184728932-8urjh1ah43490lu32se135o4mj123s84.apps.googleusercontent.com';
45
46 /**
47 * Settings Manager instance
48 *
49 * @var Settings_Manager
50 */
51 private Settings_Manager $settings_manager;
52
53 /**
54 * Google Analytics client
55 *
56 * @var Google_Analytics_Client|null
57 */
58 private ?Google_Analytics_Client $analytics_client = null;
59
60 /**
61 * Google Search Console client
62 *
63 * @var Google_Search_Console_Client|null
64 */
65 private ?Google_Search_Console_Client $search_console_client = null;
66
67 /**
68 * Google Search Analytics client
69 *
70 * @var Google_Search_Analytics_Client|null
71 */
72 private ?Google_Search_Analytics_Client $search_analytics_client = null;
73
74 /**
75 * Google PageSpeed client
76 *
77 * @var Google_PageSpeed_Client|null
78 */
79 private ?Google_PageSpeed_Client $pagespeed_client = null;
80
81 /**
82 * Cache duration in seconds
83 *
84 * @var int
85 */
86 private int $cache_duration;
87
88 /**
89 * Static flag to prevent multiple token refreshes in the same request
90 *
91 * @var bool
92 */
93 private static bool $token_refreshed_this_request = false;
94
95 /**
96 * Constructor
97 *
98 * @param Settings_Manager|null $settings_manager Settings manager instance
99 */
100 public function __construct(?Settings_Manager $settings_manager = null) {
101 $this->settings_manager = $settings_manager ?? new Settings_Manager();
102 // Pro: daily refresh (86400s), Free: 3-day refresh (259200s)
103 $this->cache_duration = defined('THINKRANK_PRO_VERSION') ? 86400 : 259200;
104
105 // Clear any existing cached insights to ensure new logic takes effect
106 $this->clear_insights_cache();
107 }
108
109 /**
110 * Initialize Analytics Manager
111 * Following ThinkRank init patterns
112 *
113 * @return void
114 */
115 public function init(): void {
116 // Register custom cron interval (45 minutes)
117 add_filter('cron_schedules', [$this, 'add_cron_intervals']);
118
119 // Initialize Google API clients
120 add_action('init', [$this, 'initialize_clients']);
121
122 // Initialize token refresh scheduling
123 add_action('init', [$this, 'init_token_refresh']);
124
125 // Cron hook for token refresh
126 add_action('thinkrank_google_token_refresh', [$this, 'refresh_access_token_cron']);
127
128 // Schedule cache cleanup
129 add_action('thinkrank_daily_cleanup', [$this, 'cleanup_cache']);
130
131 // Cleanup cron on plugin deactivation
132 register_deactivation_hook(THINKRANK_PLUGIN_FILE, [__CLASS__, 'deactivation_cleanup']);
133 }
134
135 /**
136 * Add custom cron intervals
137 *
138 * @param array $schedules Existing cron schedules
139 * @return array Modified cron schedules
140 */
141 public function add_cron_intervals(array $schedules): array {
142 $schedules['thinkrank_45min'] = [
143 'interval' => 2700, // 45 minutes in seconds
144 'display' => __('Every 45 Minutes', 'thinkrank')
145 ];
146 return $schedules;
147 }
148
149 /**
150 * Clean up cron events on plugin deactivation
151 *
152 * @return void
153 */
154 public static function deactivation_cleanup(): void {
155 $timestamp = wp_next_scheduled('thinkrank_google_token_refresh');
156 if ($timestamp) {
157 wp_unschedule_event($timestamp, 'thinkrank_google_token_refresh');
158 }
159 }
160
161 /**
162 * Get the initialized Search Console client
163 *
164 * @return Google_Search_Console_Client|null
165 */
166 public function get_search_console_client(): ?Google_Search_Console_Client {
167 if (!$this->search_console_client) {
168 $this->initialize_clients();
169 }
170 return $this->search_console_client;
171 }
172
173 /**
174 * Get the configured Search Console property URL
175 *
176 * @return string
177 */
178 public function get_property_url(): string {
179 return $this->get_setting('search_console_property', get_site_url());
180 }
181
182 /**
183 * Initialize Google API clients
184 * Following AI_Manager client initialization pattern
185 *
186 * @return void
187 */
188 public function initialize_clients(): void {
189 try {
190 // Refresh token if needed (non-forced, checks expiration)
191 $this->refresh_access_token();
192
193 // Initialize Search Console client
194 $gsc_api_key = $this->get_setting('google_search_console_api_key');
195 $access_token = $this->get_setting('google_access_token');
196
197 $timeout = (int) $this->get_setting('api_timeout', 30);
198 $this->search_console_client = new Google_Search_Console_Client(
199 $gsc_api_key ?: '',
200 $timeout,
201 !empty($access_token) ? $access_token : null
202 );
203
204 // Initialize Search Analytics client
205 $this->search_analytics_client = new Google_Search_Analytics_Client(
206 $gsc_api_key ?: '',
207 $timeout,
208 !empty($access_token) ? $access_token : null
209 );
210
211 // Initialize PageSpeed client with OAuth token (same token as Search Console)
212 if (!empty($access_token)) {
213 $timeout = (int) $this->get_setting('api_timeout', 30);
214 $this->pagespeed_client = new Google_PageSpeed_Client('', $timeout, $access_token);
215 }
216 } catch (\Exception $e) {
217 error_log('ThinkRank Analytics Init Error: ' . $e->getMessage());
218 }
219 }
220
221 /**
222 * Initialize token refresh scheduling
223 * Also migrates old absolute-timestamp expires_in values to relative seconds
224 *
225 * @return void
226 */
227 public function init_token_refresh(): void {
228 $access_token = $this->get_setting('google_access_token');
229 $refresh_token = $this->get_setting('google_refresh_token');
230
231 if (empty($access_token) || empty($refresh_token)) {
232 return;
233 }
234
235 // Migrate old expires_in values stored as absolute timestamps
236 $this->maybe_migrate_expires_in();
237
238 // Schedule recurring hourly cron for token refresh
239 $this->schedule_token_refresh();
240 }
241
242 /**
243 * Migrate old expires_in values from absolute timestamps to relative seconds
244 *
245 * Old callback.php stored expires_in as time() + token->expires_in (e.g., 1771330205).
246 * New behavior stores raw seconds from Google (e.g., 3599).
247 *
248 * @return void
249 */
250 private function maybe_migrate_expires_in(): void {
251 $expires_in = (int) $this->get_setting('google_token_expires_in');
252 $created = (int) $this->get_setting('google_token_created');
253
254 // Google tokens expire in 3600 seconds max. If stored value is > 86400,
255 // it's almost certainly the old absolute timestamp format.
256 if ($expires_in > 86400 && $created > 0) {
257 $relative = $expires_in - $created;
258 if ($relative > 0 && $relative <= 7200) {
259 // Valid relative value, save the corrected value
260 $this->settings_manager->update_settings([
261 'google_token_expires_in' => $relative
262 ], 'integrations');
263 } else {
264 // Can't reliably compute, default to standard 3600
265 $this->settings_manager->update_settings([
266 'google_token_expires_in' => 3600
267 ], 'integrations');
268 }
269 }
270 }
271
272 /**
273 * Schedule recurring cron for token refresh (every 45 minutes)
274 *
275 * Uses WP recurring cron instead of single events for reliability.
276 * The cron callback checks expiration and only refreshes when needed.
277 * Using 45-minute interval ensures the cron always fires before
278 * Google's ~60-minute token expiry window.
279 *
280 * @return void
281 */
282 public function schedule_token_refresh(): void {
283 $next = wp_next_scheduled('thinkrank_google_token_refresh');
284
285 // If already scheduled with the old 'hourly' interval, reschedule with 45min
286 if ($next) {
287 // Check if it's using the old interval by looking at the schedule
288 $crons = _get_cron_array();
289 foreach ($crons as $timestamp => $cron_hooks) {
290 if (isset($cron_hooks['thinkrank_google_token_refresh'])) {
291 foreach ($cron_hooks['thinkrank_google_token_refresh'] as $hash => $args) {
292 if (($args['schedule'] ?? '') === 'hourly') {
293 // Remove old hourly schedule and re-add with 45min
294 wp_unschedule_event($timestamp, 'thinkrank_google_token_refresh');
295 $next = false; // Will be rescheduled below
296 }
297 }
298 break;
299 }
300 }
301 }
302
303 if (!$next) {
304 wp_schedule_event(time(), 'thinkrank_45min', 'thinkrank_google_token_refresh');
305 }
306 }
307
308 /**
309 * Cron callback for token refresh
310 * Called every 45 minutes; only refreshes if token is expired or expiring soon.
311 *
312 * @return void
313 */
314 public function refresh_access_token_cron(): void {
315 $this->refresh_access_token();
316 }
317
318 /**
319 * Ensure the Google access token is fresh before making API calls.
320 *
321 * This is a static convenience method that can be called from any endpoint
322 * (including the Pro plugin) before making Google API requests.
323 * Uses a per-request flag to avoid redundant refreshes when multiple
324 * endpoints are called in the same HTTP request.
325 *
326 * @since 1.6.0
327 * @return void
328 */
329 public static function ensure_fresh_token(): void {
330 // Only refresh once per HTTP request to avoid parallel race conditions
331 if (self::$token_refreshed_this_request) {
332 return;
333 }
334
335 $manager = new self();
336 $manager->refresh_access_token();
337 self::$token_refreshed_this_request = true;
338 }
339
340 /**
341 * Refresh OAuth access token if expired or expiring soon
342 *
343 * @since 1.5.0
344 * @param bool $force Force refresh even if not expired
345 * @return void
346 */
347 public function refresh_access_token(bool $force = false): void {
348 $refresh_token = $this->get_setting('google_refresh_token');
349
350 // If no refresh token, we can't refresh
351 if (empty($refresh_token)) {
352 return;
353 }
354
355 $expires_in = (int) $this->get_setting('google_token_expires_in');
356 $created = (int) $this->get_setting('google_token_created');
357 $current_time = time();
358
359 // Calculate absolute expiration time (created + relative seconds)
360 $expiration_time = $created + $expires_in;
361
362 // Refresh if forced, expired, or expiring within 5 minutes (300 seconds)
363 if ($force || $current_time >= ($expiration_time - 300)) {
364
365 $api_url = 'https://api.thinkrank.ai/v1/callback.php';
366
367 $response = wp_remote_post($api_url, [
368 'body' => [
369 'type' => 'google_analytics',
370 'refresh_token' => $refresh_token,
371 'client_id' => self::THINKRANK_GOOGLE_CLIENT_ID
372 ],
373 'timeout' => 30
374 ]);
375
376 if (is_wp_error($response)) {
377 return;
378 }
379
380 $body = wp_remote_retrieve_body($response);
381 $data = json_decode($body, true);
382
383 if (empty($data['access_token'])) {
384 return;
385 }
386
387 // Update settings with new token data
388 $this->settings_manager->update_settings([
389 'google_access_token' => $data['access_token'],
390 'google_token_created' => $current_time,
391 'google_token_expires_in' => (int) ($data['expires_in'] ?? 3600)
392 ], 'integrations');
393
394 // Also update refresh token if a new one was returned
395 if (!empty($data['refresh_token'])) {
396 $this->settings_manager->update_settings([
397 'google_refresh_token' => $data['refresh_token']
398 ], 'integrations');
399 }
400 }
401 }
402
403 /**
404 * Test all Google API connections
405 * Following ThinkRank test_connection patterns
406 *
407 * @return array Connection test results
408 */
409 public function test_connections(): array {
410 $results = [
411 'google_analytics' => ['status' => 'not_configured'],
412 'search_console' => ['status' => 'not_configured'],
413 'pagespeed' => ['status' => 'not_configured']
414 ];
415
416 // Test Google Analytics connection
417 if ($this->analytics_client) {
418 try {
419 $test_result = $this->analytics_client->test_connection();
420 $results['google_analytics'] = [
421 'status' => $test_result['success'] ? 'connected' : 'error',
422 'message' => $test_result['message'],
423 'details' => $test_result
424 ];
425 } catch (\Exception $e) {
426 $results['google_analytics'] = [
427 'status' => 'error',
428 'message' => $e->getMessage()
429 ];
430 }
431 }
432
433 // Test Search Console connection
434 if ($this->search_console_client) {
435 try {
436 $test_result = $this->search_console_client->test_connection();
437 $results['search_console'] = [
438 'status' => $test_result['success'] ? 'connected' : 'error',
439 'message' => $test_result['message'],
440 'details' => $test_result
441 ];
442 } catch (\Exception $e) {
443 $results['search_console'] = [
444 'status' => 'error',
445 'message' => $e->getMessage()
446 ];
447 }
448 }
449
450 // Test PageSpeed connection
451 if ($this->pagespeed_client) {
452 try {
453 $test_result = $this->pagespeed_client->test_connection();
454 $results['pagespeed'] = [
455 'status' => $test_result['success'] ? 'connected' : 'error',
456 'message' => $test_result['message'],
457 'details' => $test_result
458 ];
459 } catch (\Exception $e) {
460 $results['pagespeed'] = [
461 'status' => 'error',
462 'message' => $e->getMessage()
463 ];
464 }
465 }
466
467 return $results;
468 }
469
470 /**
471 * Get analytics dashboard data
472 * Combines data from all Google APIs with caching
473 *
474 * @param string $date_range Date range for data
475 * @return array Dashboard data
476 */
477 public function get_dashboard_data(string $date_range = '30d'): array {
478 $cache_key = "analytics_dashboard_v4_{$date_range}";
479 $cached_data = get_transient($cache_key);
480
481 if ($cached_data !== false) {
482 return $cached_data;
483 }
484
485 $dashboard_data = [
486 'traffic' => [],
487 'search_performance' => [],
488 'core_web_vitals' => [],
489 'last_updated' => current_time('mysql'),
490 'date_range' => $date_range
491 ];
492
493 $retry_count = 0;
494 $max_retries = 1;
495
496 while ($retry_count <= $max_retries) {
497 try {
498 // Get Google Analytics traffic data
499 if ($this->analytics_client) {
500 $dashboard_data['traffic'] = $this->analytics_client->get_traffic_data($date_range);
501 $dashboard_data['organic_traffic'] = $this->analytics_client->get_organic_traffic($date_range);
502 $dashboard_data['top_pages'] = $this->analytics_client->get_top_pages(10, $date_range);
503 }
504
505 // Ensure clients are initialized (lazy load)
506 if (!$this->search_console_client || !$this->search_analytics_client) {
507 $this->initialize_clients();
508 }
509
510 // Get Search Console data
511 if ($this->search_console_client) {
512 $site_url = $this->get_setting('search_console_property', get_site_url());
513 // Get totals
514 $totals = $this->search_console_client->get_search_totals($site_url, $date_range);
515
516 // Get performance data (keywords) using new client
517 if ($this->search_analytics_client) {
518 // GSC data has a 2-day delay; use D-2 as end_date to match the GSC dashboard.
519 $days = (int) str_replace('d', '', $date_range);
520 $end_date = gmdate('Y-m-d', strtotime('-2 days'));
521 $start_date = gmdate('Y-m-d', strtotime('-' . ($days - 1) . ' days', strtotime($end_date)));
522
523 $search_performance = $this->search_analytics_client->get_search_analytics_data(
524 $site_url,
525 $start_date,
526 $end_date,
527 ['query'],
528 1000
529 );
530 } else {
531 // Fallback to old client if new one fails init (shouldn't happen if they use same creds)
532 $search_performance = $this->search_console_client->get_search_performance($site_url, $date_range, ['query'], 1000);
533 }
534
535 // Calculate position distribution
536 $position_distribution = [
537 'top_3' => 0,
538 '4_10' => 0,
539 '10_50' => 0,
540 '51_100' => 0
541 ];
542
543 foreach ($search_performance['rows'] ?? [] as $row) {
544 $position = $row['position'] ?? 0;
545 if ($position <= 3) {
546 $position_distribution['top_3']++;
547 } elseif ($position <= 10) {
548 $position_distribution['4_10']++;
549 } elseif ($position <= 50) {
550 $position_distribution['10_50']++;
551 } elseif ($position <= 100) {
552 $position_distribution['51_100']++;
553 }
554 }
555
556 $dashboard_data['search_performance'] = array_merge($search_performance, [
557 'totals' => $totals,
558 'position_distribution' => $position_distribution
559 ]);
560
561 $dashboard_data['page_performance'] = $this->search_console_client->get_page_performance($site_url, $date_range, 10);
562 } // Closing Search Console block
563
564 // Get Core Web Vitals data (isolated try-catch so PageSpeed failures
565 // don't abort the entire dashboard data collection)
566 if ($this->pagespeed_client) {
567 try {
568 $site_url = get_site_url();
569 $dashboard_data['core_web_vitals'] = $this->pagespeed_client->get_core_web_vitals($site_url);
570 } catch (\Exception $psi_error) {
571 // Log the PageSpeed error but don't fail the whole dashboard
572 $dashboard_data['core_web_vitals'] = [
573 'error' => $psi_error->getMessage(),
574 'note' => 'PageSpeed data unavailable. This is expected on localhost or non-public URLs.'
575 ];
576 }
577 }
578
579 // If successful, break loop
580 break;
581 } catch (\Exception $e) {
582 // Check for 401 error
583 if ($e->getCode() === 401 && $retry_count < $max_retries) {
584 $this->refresh_access_token(true); // Force refresh
585
586 // Re-initialize clients with new token
587 $this->initialize_clients();
588
589 $retry_count++;
590 continue;
591 }
592 $dashboard_data['error'] = $e->getMessage();
593 break;
594 }
595 }
596
597 // Add last updated timestamp
598 $dashboard_data['last_updated'] = current_time('mysql');
599
600 // Cache the results
601 set_transient($cache_key, $dashboard_data, $this->cache_duration);
602
603 return $dashboard_data;
604 }
605
606 /**
607 * Get SEO opportunities using Search Console data
608 *
609 * @param string $date_range Date range for analysis
610 * @return array SEO opportunities
611 */
612 public function get_seo_opportunities(string $date_range = '30d'): array {
613 $cache_key = "seo_opportunities_{$date_range}";
614 $cached_data = get_transient($cache_key);
615
616 if ($cached_data !== false) {
617 return $cached_data;
618 }
619
620 $opportunities = [
621 'keyword_opportunities' => [],
622 'page_opportunities' => [],
623 'device_insights' => [],
624 'last_updated' => current_time('mysql')
625 ];
626
627 $retry_count = 0;
628 $max_retries = 1;
629
630 while ($retry_count <= $max_retries) {
631 try {
632 if ($this->search_console_client) {
633 $site_url = $this->get_setting('search_console_property', get_site_url());
634
635 // Get keyword opportunities
636 $opportunities['keyword_opportunities'] = $this->search_console_client->get_keyword_opportunities($site_url, $date_range);
637
638 // Get device performance insights
639 $opportunities['device_insights'] = $this->search_console_client->get_device_performance($site_url, $date_range);
640
641 // Get search appearance data
642 $opportunities['search_appearance'] = $this->search_console_client->get_search_appearance($site_url, $date_range);
643 }
644
645 // If successful, break loop
646 break;
647 } catch (\Exception $e) {
648 // Check for 401 error
649 if ($e->getCode() === 401 && $retry_count < $max_retries) {
650 $this->refresh_access_token(true); // Force refresh
651
652 // Re-initialize clients with new token
653 $this->initialize_clients();
654
655 $retry_count++;
656 continue;
657 }
658
659 $opportunities['error'] = $e->getMessage();
660 break;
661 }
662 }
663
664 // Cache the results
665 set_transient($cache_key, $opportunities, $this->cache_duration);
666
667 return $opportunities;
668 }
669
670 private function get_setting(string $key, $default = '') {
671 $integrations_settings = $this->settings_manager->get_settings('integrations');
672 $analytics_settings = $this->settings_manager->get_settings('seo_analytics');
673
674 // Merge settings to allow access to both categories
675 $all_settings = array_merge($integrations_settings, $analytics_settings);
676
677 return $all_settings[$key] ?? $default;
678 }
679
680 /**
681 * One-click setup for Google Search Console verification
682 * Following ThinkRank setup patterns
683 *
684 * @param string $site_url Site URL to verify
685 * @return array Setup results
686 */
687 public function setup_search_console_verification(string $site_url): array {
688 try {
689 if (!$this->search_console_client) {
690 return [
691 'success' => false,
692 'message' => 'Search Console API key not configured'
693 ];
694 }
695
696 $verification_result = $this->search_console_client->verify_site($site_url);
697
698 if ($verification_result['success']) {
699 // Update settings with verified site URL
700 $this->settings_manager->update_settings(['search_console_property' => $site_url], 'seo_analytics');
701 }
702
703 return $verification_result;
704 } catch (\Exception $e) {
705 return [
706 'success' => false,
707 'message' => $e->getMessage()
708 ];
709 }
710 }
711
712 /**
713 * Get site indexing status
714 *
715 * @return array Indexing status data
716 */
717 public function get_indexing_status(): array {
718 $cache_key = 'indexing_status';
719 $cached_data = get_transient($cache_key);
720
721 if ($cached_data !== false) {
722 return $cached_data;
723 }
724
725 $indexing_data = [
726 'status' => 'unknown',
727 'last_updated' => current_time('mysql')
728 ];
729
730 try {
731 if ($this->search_console_client) {
732 $site_url = $this->get_setting('search_console_property', get_site_url());
733 $indexing_data = $this->search_console_client->get_indexing_status($site_url);
734 }
735 } catch (\Exception $e) {
736 $indexing_data['error'] = $e->getMessage();
737 }
738
739 // Cache for 1 hour
740 set_transient($cache_key, $indexing_data, 3600);
741
742 return $indexing_data;
743 }
744
745 /**
746 * Force refresh of all cached data
747 *
748 * @return array Refresh results
749 */
750 public function refresh_data(): array {
751 // Clear all analytics-related transients
752 $cache_keys = [
753 'analytics_dashboard_v4_7d',
754 'analytics_dashboard_v4_30d',
755 'analytics_dashboard_v4_90d',
756 'seo_opportunities_7d',
757 'seo_opportunities_30d',
758 'seo_opportunities_90d',
759 'seo_insights_7d',
760 'seo_insights_30d',
761 'seo_insights_90d',
762 'indexing_status'
763 ];
764
765 $cleared = 0;
766 foreach ($cache_keys as $key) {
767 if (delete_transient($key)) {
768 $cleared++;
769 }
770 }
771
772 return [
773 'success' => true,
774 'message' => "Cleared {$cleared} cached data entries",
775 'cleared_count' => $cleared,
776 'timestamp' => current_time('mysql')
777 ];
778 }
779
780 /**
781 * Clear insights cache specifically
782 *
783 * @return void
784 */
785 private function clear_insights_cache(): void {
786 $insight_cache_keys = [
787 'seo_insights_7d',
788 'seo_insights_30d',
789 'seo_insights_90d'
790 ];
791
792 foreach ($insight_cache_keys as $key) {
793 delete_transient($key);
794 }
795 }
796
797 /**
798 * Get client status for debugging
799 *
800 * @return array Client status information
801 */
802 public function get_client_status(): array {
803 return [
804 'google_analytics' => [
805 'initialized' => !is_null($this->analytics_client),
806 'api_key_configured' => !empty($this->get_setting('google_analytics_api_key')),
807 'property_id_configured' => !empty($this->get_setting('google_analytics_property_id'))
808 ],
809 'search_console' => [
810 'initialized' => !is_null($this->search_console_client),
811 'api_key_configured' => !empty($this->get_setting('google_search_console_api_key')),
812 'site_url_configured' => !empty($this->get_setting('search_console_property'))
813 ],
814 'pagespeed' => [
815 'initialized' => !is_null($this->pagespeed_client),
816 'api_key_configured' => !empty($this->get_setting('google_pagespeed_api_key'))
817 ],
818 'cache_duration' => $this->cache_duration,
819 'last_checked' => current_time('mysql')
820 ];
821 }
822
823 /**
824 * Cleanup expired cache data
825 * Following ThinkRank cache cleanup patterns
826 *
827 * @return void
828 */
829 public function cleanup_cache(): void {
830 // WordPress handles transient cleanup automatically
831 // This method is for future custom cache cleanup if needed
832 }
833
834 // ========================================
835 // SEO Intelligence Enhancement Methods
836 // ========================================
837
838 /**
839 * Get intelligent dashboard data with trends and insights
840 *
841 * @param string $date_range Date range for analysis
842 * @return array Enhanced dashboard data with intelligence
843 */
844 public function get_intelligent_dashboard_data(string $date_range = '30d'): array {
845 // Get base dashboard data
846 $dashboard_data = $this->get_dashboard_data($date_range);
847
848 // Check if there's an error in the data
849 if (isset($dashboard_data['error'])) {
850 return [
851 'success' => false,
852 'data' => null,
853 'message' => 'Failed to retrieve dashboard data: ' . $dashboard_data['error'],
854 'timestamp' => current_time('mysql')
855 ];
856 }
857
858 // Check if we have real data available
859 if (!$this->has_real_data($dashboard_data)) {
860 return [
861 'success' => false,
862 'data' => null,
863 'message' => 'No analytics data available yet. Please ensure your Google Analytics and Search Console are properly configured and have collected data.',
864 'timestamp' => current_time('mysql')
865 ];
866 }
867
868 // Initialize intelligence classes — only available in Pro
869 if (
870 !class_exists('ThinkRank\SEO\SEO_Trend_Analyzer') ||
871 !class_exists('ThinkRank\SEO\SEO_Scoring_Engine') ||
872 !class_exists('ThinkRank\SEO\SEO_Insight_Generator')
873 ) {
874 return [
875 'success' => false,
876 'data' => null,
877 'message' => 'Intelligent dashboard requires ThinkRank Pro.',
878 'timestamp' => current_time('mysql')
879 ];
880 }
881
882 $trend_analyzer = new SEO_Trend_Analyzer();
883 $scoring_engine = new SEO_Scoring_Engine();
884 $insight_generator = new SEO_Insight_Generator();
885
886 $data = $dashboard_data;
887
888 // Generate trend analysis
889 $current_data = $data;
890 $historical_data = $this->get_historical_data($date_range);
891
892 $trends = [
893 'traffic_trends' => $trend_analyzer->analyze_traffic_trends($current_data, $historical_data),
894 'keyword_trends' => $trend_analyzer->analyze_keyword_trends($data['search_performance'] ?? [], $date_range),
895 'content_trends' => $trend_analyzer->analyze_content_trends($data, $data['search_performance'] ?? [])
896 ];
897
898 // Calculate SEO health score
899 $seo_health = $scoring_engine->calculate_seo_health_score($data, $data['search_performance'] ?? []);
900
901 // Generate insights
902 $insights = [
903 'traffic_insights' => $insight_generator->generate_traffic_insights($trends['traffic_trends']),
904 'keyword_insights' => $insight_generator->generate_keyword_insights($trends['keyword_trends']),
905 'content_insights' => $insight_generator->generate_content_insights($trends['content_trends'])
906 ];
907
908 // Combine all intelligence data
909 $enhanced_data = array_merge($data, [
910 'intelligence' => [
911 'trends' => $trends,
912 'seo_health_score' => $seo_health,
913 'insights' => $insights,
914 'last_analyzed' => current_time('mysql')
915 ]
916 ]);
917
918 return [
919 'success' => true,
920 'data' => $enhanced_data,
921 'message' => 'Intelligent dashboard data retrieved successfully'
922 ];
923 }
924
925 /**
926 * Get intelligent SEO opportunities with prioritization
927 *
928 * @param string $date_range Date range for analysis
929 * @return array Enhanced opportunities with intelligence
930 */
931 public function get_intelligent_seo_opportunities(string $date_range = '30d'): array {
932 // Get base opportunities data
933 $opportunities_data = $this->get_seo_opportunities($date_range);
934
935 // Check if there's an error in the data
936 if (isset($opportunities_data['error'])) {
937 return [
938 'success' => false,
939 'data' => null,
940 'message' => 'Failed to retrieve opportunities data: ' . $opportunities_data['error'],
941 'timestamp' => current_time('mysql')
942 ];
943 }
944
945 // Check if we have real search console data for opportunities
946 $search_performance = $opportunities_data['search_performance'] ?? [];
947 $has_search_data = !empty($search_performance['rows']) ||
948 ($search_performance['total_clicks'] ?? 0) > 0 ||
949 ($search_performance['total_impressions'] ?? 0) > 0;
950
951 if (!$has_search_data) {
952 return [
953 'success' => false,
954 'data' => null,
955 'message' => 'No Search Console data available yet. Please ensure your Search Console is properly configured and has collected data.',
956 'timestamp' => current_time('mysql')
957 ];
958 }
959
960 // Initialize intelligence classes — only available in Pro
961 if (
962 !class_exists('ThinkRank\SEO\SEO_Opportunity_Detector') ||
963 !class_exists('ThinkRank\SEO\SEO_Scoring_Engine')
964 ) {
965 return [
966 'success' => false,
967 'data' => null,
968 'message' => 'Intelligent opportunities require ThinkRank Pro.',
969 'timestamp' => current_time('mysql')
970 ];
971 }
972
973 $opportunity_detector = new SEO_Opportunity_Detector();
974 $scoring_engine = new SEO_Scoring_Engine();
975
976 $data = $opportunities_data;
977
978 // Detect intelligent opportunities
979 $search_console_data = $data['search_performance'] ?? [];
980 $analytics_data = $data;
981
982 $intelligent_opportunities = [
983 'quick_wins' => $opportunity_detector->detect_quick_wins($search_console_data, $analytics_data),
984 'content_opportunities' => $opportunity_detector->identify_content_opportunities($search_console_data, $analytics_data),
985 'keyword_opportunities' => $scoring_engine->score_keyword_opportunities($search_console_data)
986 ];
987
988 // Prioritize all opportunities
989 $all_opportunities = array_merge(
990 $intelligent_opportunities['quick_wins']['opportunities'] ?? [],
991 $intelligent_opportunities['content_opportunities']['opportunities'] ?? [],
992 $intelligent_opportunities['keyword_opportunities']['opportunities'] ?? []
993 );
994
995 $prioritized = $opportunity_detector->prioritize_opportunities($all_opportunities);
996 $impact_matrix = $opportunity_detector->calculate_impact_effort_matrix($all_opportunities);
997
998 // Enhance original data with intelligence
999 $enhanced_data = array_merge($data, [
1000 'intelligent_opportunities' => $intelligent_opportunities,
1001 'prioritized_opportunities' => $prioritized,
1002 'impact_effort_matrix' => $impact_matrix,
1003 'opportunity_summary' => $this->generate_opportunity_summary($intelligent_opportunities),
1004 'last_analyzed' => current_time('mysql')
1005 ]);
1006
1007 return [
1008 'success' => true,
1009 'data' => $enhanced_data,
1010 'message' => 'Intelligent SEO opportunities retrieved successfully'
1011 ];
1012 }
1013
1014 /**
1015 * Get SEO performance insights
1016 *
1017 * @param string $date_range Date range for analysis
1018 * @return array SEO insights data
1019 */
1020 public function get_seo_insights(string $date_range = '30d'): array {
1021 $cache_key = "seo_insights_{$date_range}";
1022 $cached_data = get_transient($cache_key);
1023
1024 if ($cached_data !== false) {
1025 return [
1026 'success' => true,
1027 'data' => $cached_data,
1028 'cached' => true,
1029 'message' => 'SEO insights retrieved from cache'
1030 ];
1031 }
1032
1033 try {
1034 // Get dashboard data for analysis
1035 $dashboard_result = $this->get_intelligent_dashboard_data($date_range);
1036
1037 if (!$dashboard_result['success']) {
1038 return $dashboard_result;
1039 }
1040
1041 $dashboard_data = $dashboard_result['data'];
1042 $intelligence = $dashboard_data['intelligence'] ?? [];
1043
1044 // Initialize insight generator — only available in Pro
1045 if (!class_exists('ThinkRank\SEO\SEO_Insight_Generator')) {
1046 return [
1047 'success' => false,
1048 'data' => null,
1049 'message' => 'SEO insights require ThinkRank Pro.',
1050 'timestamp' => current_time('mysql')
1051 ];
1052 }
1053
1054 $insight_generator = new SEO_Insight_Generator();
1055
1056 // Collect all insights
1057 $all_insights = [];
1058
1059 if (!empty($intelligence['insights']['traffic_insights']['insights'])) {
1060 $all_insights = array_merge($all_insights, $intelligence['insights']['traffic_insights']['insights']);
1061 }
1062
1063 if (!empty($intelligence['insights']['keyword_insights']['insights'])) {
1064 $all_insights = array_merge($all_insights, $intelligence['insights']['keyword_insights']['insights']);
1065 }
1066
1067 if (!empty($intelligence['insights']['content_insights']['insights'])) {
1068 $all_insights = array_merge($all_insights, $intelligence['insights']['content_insights']['insights']);
1069 }
1070
1071 // Format and prioritize insights
1072 $formatted_insights = $insight_generator->format_insights_for_display($all_insights);
1073 $prioritized_insights = $insight_generator->prioritize_insights_by_impact($formatted_insights);
1074
1075 $insights_data = [
1076 'insights' => $prioritized_insights['prioritized_insights'],
1077 'summary' => [
1078 'total_insights' => count($formatted_insights),
1079 'high_impact_count' => $prioritized_insights['high_impact_count'],
1080 'action_required_count' => $prioritized_insights['action_required_count']
1081 ],
1082 'seo_health_score' => $intelligence['seo_health_score'] ?? null,
1083 'generated_at' => current_time('mysql')
1084 ];
1085
1086 // Cache the results
1087 set_transient($cache_key, $insights_data, $this->cache_duration);
1088
1089 return [
1090 'success' => true,
1091 'data' => $insights_data,
1092 'cached' => false,
1093 'message' => 'SEO insights generated successfully'
1094 ];
1095 } catch (\Exception $e) {
1096 return [
1097 'success' => false,
1098 'error' => 'Failed to generate SEO insights: ' . $e->getMessage(),
1099 'data' => null
1100 ];
1101 }
1102 }
1103
1104 /**
1105 * Check if real analytics data is available
1106 *
1107 * @param array $dashboard_data Dashboard data to check
1108 * @return bool True if real data is available
1109 */
1110 private function has_real_data(array $dashboard_data): bool {
1111 // Check if we have meaningful traffic data
1112 $traffic = $dashboard_data['traffic'] ?? [];
1113 $search_performance = $dashboard_data['search_performance'] ?? [];
1114
1115 $has_traffic = !empty($traffic) && (
1116 ($traffic['sessions'] ?? 0) > 0 ||
1117 ($traffic['pageviews'] ?? 0) > 0 ||
1118 ($traffic['active_users'] ?? 0) > 0
1119 );
1120
1121 $has_search_data = !empty($search_performance) && (
1122 !empty($search_performance['rows']) ||
1123 ($search_performance['total_clicks'] ?? 0) > 0 ||
1124 ($search_performance['total_impressions'] ?? 0) > 0
1125 );
1126
1127 return $has_traffic || $has_search_data;
1128 }
1129
1130 /**
1131 * Get historical data for trend comparison
1132 *
1133 * @param string $current_range Current date range
1134 * @return array Historical data
1135 */
1136 private function get_historical_data(string $current_range): array {
1137 // Calculate previous period based on current range
1138 $previous_range = $this->calculate_previous_period($current_range);
1139
1140 // Try to get actual historical data from previous period
1141 $historical_data = $this->get_dashboard_data($previous_range);
1142
1143 // Return the actual historical data (may be empty if no real data available)
1144 return [
1145 'sessions' => $historical_data['traffic']['sessions'] ?? 0,
1146 'pageviews' => $historical_data['traffic']['pageviews'] ?? 0,
1147 'organic_traffic' => $historical_data['organic_traffic'] ?? ['organic_traffic' => ['sessions' => 0]],
1148 'bounce_rate' => $historical_data['traffic']['bounce_rate'] ?? 0,
1149 'avg_session_duration' => $historical_data['traffic']['avg_session_duration'] ?? 0
1150 ];
1151 }
1152
1153 /**
1154 * Calculate previous period for comparison
1155 *
1156 * @param string $current_range Current range
1157 * @return string Previous period range
1158 */
1159 private function calculate_previous_period(string $current_range): string {
1160 // Simple mapping for now - could be enhanced with actual date calculations
1161 $period_mapping = [
1162 '7d' => '14d',
1163 '30d' => '60d',
1164 '90d' => '180d'
1165 ];
1166
1167 return $period_mapping[$current_range] ?? '60d';
1168 }
1169
1170 /**
1171 * Generate opportunity summary
1172 *
1173 * @param array $opportunities All opportunities
1174 * @return array Opportunity summary
1175 */
1176 private function generate_opportunity_summary(array $opportunities): array {
1177 $quick_wins_count = count($opportunities['quick_wins']['opportunities'] ?? []);
1178 $content_opportunities_count = count($opportunities['content_opportunities']['opportunities'] ?? []);
1179 $keyword_opportunities_count = count($opportunities['keyword_opportunities']['opportunities'] ?? []);
1180
1181 $total_opportunities = $quick_wins_count + $content_opportunities_count + $keyword_opportunities_count;
1182
1183 $potential_clicks = 0;
1184 if (!empty($opportunities['quick_wins']['potential_additional_clicks'])) {
1185 $potential_clicks = $opportunities['quick_wins']['potential_additional_clicks'];
1186 }
1187
1188 return [
1189 'total_opportunities' => $total_opportunities,
1190 'quick_wins_count' => $quick_wins_count,
1191 'content_opportunities_count' => $content_opportunities_count,
1192 'keyword_opportunities_count' => $keyword_opportunities_count,
1193 'potential_additional_clicks' => $potential_clicks,
1194 'priority_recommendation' => $quick_wins_count > 0 ?
1195 'Focus on quick wins first for immediate impact' :
1196 'Focus on content optimization for long-term growth'
1197 ];
1198 }
1199
1200 /**
1201 * Clear intelligence cache
1202 *
1203 * @return array Clear result
1204 */
1205 public function clear_intelligence_cache(): array {
1206 $intelligence_cache_keys = [
1207 'seo_insights_7d',
1208 'seo_insights_30d',
1209 'seo_insights_90d',
1210 'intelligent_dashboard_7d',
1211 'intelligent_dashboard_30d',
1212 'intelligent_dashboard_90d',
1213 'intelligent_opportunities_7d',
1214 'intelligent_opportunities_30d',
1215 'intelligent_opportunities_90d'
1216 ];
1217
1218 $cleared = 0;
1219 foreach ($intelligence_cache_keys as $key) {
1220 if (delete_transient($key)) {
1221 $cleared++;
1222 }
1223 }
1224
1225 return [
1226 'success' => true,
1227 'message' => "Cleared {$cleared} intelligence cache entries",
1228 'cleared_count' => $cleared,
1229 'timestamp' => current_time('mysql')
1230 ];
1231 }
1232 }
1233