PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.26.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.26.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-email-report-data-provider.php

class-email-report-data-provider.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.26.0, at includes/seo/class-email-report-data-provider.php

215 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email Report Data Provider
4 *
5 * Pulls dashboard data for the current period and the immediately
6 * preceding period of equal length, then hands both to sections so they
7 * can diff and rank. Sections never call Analytics_Manager directly —
8 * one fetch per period, shared across the report.
9 *
10 * If Analytics_Manager isn't available (no Google connection, etc.) the
11 * provider returns an `available => false` result; sections then render
12 * their fallback HTML per PRD's graceful-degradation requirement.
13 *
14 * @package ThinkRank
15 * @subpackage SEO
16 * @since 1.9.0
17 */
18
19 declare(strict_types=1);
20
21 namespace ThinkRank\SEO;
22
23 use Throwable;
24
25 if (!defined('ABSPATH')) {
26 exit;
27 }
28
29 /**
30 * Email_Report_Data_Provider
31 *
32 * @since 1.9.0
33 */
34 final class Email_Report_Data_Provider {
35
36 /**
37 * Pull current + prior period dashboard data.
38 *
39 * @param int $frequency_days Reporting frequency in days.
40 * @return array{
41 * available:bool,
42 * current:array,
43 * prior:array,
44 * period_start:string,
45 * period_end:string,
46 * period_label:string,
47 * error?:string
48 * }
49 */
50 public function fetch(int $frequency_days): array {
51 $frequency_days = max(1, $frequency_days);
52 $today = current_time('Y-m-d');
53 $period_end = $today;
54 $period_start = wp_date('Y-m-d', strtotime("-{$frequency_days} days", strtotime($today)));
55 $period_label = $this->format_period_label($period_start, $period_end);
56
57 $manager = $this->get_analytics_manager();
58 if ($manager === null) {
59 return [
60 'available' => false,
61 'current' => [],
62 'prior' => [],
63 'period_start' => $period_start,
64 'period_end' => $period_end,
65 'period_label' => $period_label,
66 'error' => __('Analytics integration not available.', 'thinkrank'),
67 ];
68 }
69
70 try {
71 $range = $frequency_days . 'd';
72 $current = $manager->get_dashboard_data($range);
73
74 // Real period-over-period comparison: pull query- and page-level
75 // metrics for the current window AND the immediately preceding
76 // window of equal length straight from Search Console, then key
77 // them so winning/losing sections can compute true deltas.
78 $comparison = $this->build_comparison($manager, $frequency_days);
79
80 return [
81 'available' => true,
82 'current' => is_array($current) ? $current : [],
83 'comparison' => $comparison,
84 'period_start' => $period_start,
85 'period_end' => $period_end,
86 'period_label' => $period_label,
87 ];
88 } catch (Throwable $e) {
89 return [
90 'available' => false,
91 'current' => [],
92 'comparison' => ['available' => false, 'queries' => [], 'pages' => []],
93 'period_start' => $period_start,
94 'period_end' => $period_end,
95 'period_label' => $period_label,
96 'error' => $e->getMessage(),
97 ];
98 }
99 }
100
101 /**
102 * Build the current-vs-previous comparison from Search Console.
103 *
104 * Uses the Search Console client's arbitrary date-range API
105 * (`get_search_performance_by_dates`) — the same one the Rank Tracker
106 * and the Pro winning/losing endpoint use — to fetch query- and
107 * page-level rows for two equal, adjacent windows. GSC lags ~2 days, so
108 * the current window ends yesterday and the previous window is the N
109 * days before it.
110 *
111 * @param object $manager Analytics_Manager instance.
112 * @param int $frequency_days Window length in days.
113 * @return array{available:bool,queries:array,pages:array}
114 */
115 private function build_comparison($manager, int $frequency_days): array {
116 $empty = ['available' => false, 'queries' => [], 'pages' => []];
117
118 if (!method_exists($manager, 'get_search_console_client')) {
119 return $empty;
120 }
121 $sc = $manager->get_search_console_client();
122 if (!$sc || !method_exists($sc, 'get_search_performance_by_dates')) {
123 return $empty;
124 }
125 $site_url = method_exists($manager, 'get_property_url') ? (string) $manager->get_property_url() : '';
126 if ($site_url === '') {
127 return $empty;
128 }
129
130 $cur_end = gmdate('Y-m-d', strtotime('-1 day'));
131 $cur_start = gmdate('Y-m-d', strtotime('-' . $frequency_days . ' days'));
132 $prev_end = gmdate('Y-m-d', strtotime('-' . ($frequency_days + 1) . ' days'));
133 $prev_start = gmdate('Y-m-d', strtotime('-' . ($frequency_days * 2) . ' days'));
134
135 $cur_q = $sc->get_search_performance_by_dates($site_url, $cur_start, $cur_end, 1000, ['query']);
136 $prev_q = $sc->get_search_performance_by_dates($site_url, $prev_start, $prev_end, 1000, ['query']);
137 $cur_p = $sc->get_search_performance_by_dates($site_url, $cur_start, $cur_end, 1000, ['page']);
138 $prev_p = $sc->get_search_performance_by_dates($site_url, $prev_start, $prev_end, 1000, ['page']);
139
140 return [
141 'available' => true,
142 'queries' => $this->merge_periods($cur_q, $prev_q, true),
143 'pages' => $this->merge_periods($cur_p, $prev_p, false),
144 ];
145 }
146
147 /**
148 * Merge current + previous GSC rows into one keyed map carrying both
149 * periods' clicks and (for queries) average position.
150 *
151 * @param array $current Current-window rows.
152 * @param array $previous Previous-window rows.
153 * @param bool $is_query True for query rows, false for page rows.
154 * @return array<string,array>
155 */
156 private function merge_periods(array $current, array $previous, bool $is_query): array {
157 $prev_map = [];
158 foreach ($previous as $row) {
159 $key = (string) ($row['keys'][0] ?? '');
160 if ($key === '') {
161 continue;
162 }
163 $prev_map[$this->normalize_key($key, $is_query)] = $row;
164 }
165
166 $merged = [];
167 foreach ($current as $row) {
168 $raw = (string) ($row['keys'][0] ?? '');
169 if ($raw === '') {
170 continue;
171 }
172 $key = $this->normalize_key($raw, $is_query);
173 $prev = $prev_map[$key] ?? null;
174
175 $entry = [
176 'cur_clicks' => (int) ($row['clicks'] ?? 0),
177 'prev_clicks' => $prev ? (int) ($prev['clicks'] ?? 0) : 0,
178 ];
179 if ($is_query) {
180 $entry['query'] = $raw;
181 $entry['cur_pos'] = round((float) ($row['position'] ?? 0), 1);
182 $entry['prev_pos'] = $prev ? round((float) ($prev['position'] ?? 0), 1) : null;
183 } else {
184 $entry['url'] = $raw;
185 }
186 $merged[$key] = $entry;
187 }
188
189 return $merged;
190 }
191
192 private function normalize_key(string $key, bool $is_query): string {
193 return $is_query ? trim(strtolower($key)) : $key;
194 }
195
196 private function get_analytics_manager() {
197 $cls = '\\ThinkRank\\SEO\\Analytics_Manager';
198 if (!class_exists($cls)) {
199 return null;
200 }
201 try {
202 return new $cls();
203 } catch (Throwable $e) {
204 return null;
205 }
206 }
207
208 private function format_period_label(string $start, string $end): string {
209 $fmt = (string) get_option('date_format', 'M j, Y');
210 $a = wp_date($fmt, strtotime($start) ?: time());
211 $b = wp_date($fmt, strtotime($end) ?: time());
212 return sprintf('%s – %s', $a, $b);
213 }
214 }
215