PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.14.2
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.14.2
2.14.2 2.14.1 2.14.0 2.13.0 2.12.0 2.11.0 2.10.0 2.9.0 2.8.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 All 57 releases
thinkrank / includes / integrations / class-google-analytics-client.php

class-google-analytics-client.php in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 2.14.2, at includes/integrations/class-google-analytics-client.php

341 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Google Analytics Client Class
5 *
6 * Handles communication with Google Analytics API for website analytics data
7 * retrieval and connection testing. Extends the base Google API client with
8 * Analytics-specific functionality and rate limiting.
9 *
10 * @package ThinkRank\Integrations
11 * @since 1.0.0
12 */
13
14 declare(strict_types=1);
15
16 namespace ThinkRank\Integrations;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Google Analytics Client Class
25 *
26 * Single Responsibility: Handle Google Analytics API communication
27 * Following ThinkRank HTTP client patterns from Claude_Client and OpenAI_Client
28 *
29 * @since 1.0.0
30 */
31 class Google_Analytics_Client extends Google_API_Base_Client {
32
33 /**
34 * Google Analytics Data API base URL (GA4)
35 */
36 private const API_BASE_URL = 'https://analyticsdata.googleapis.com/v1beta';
37
38 /**
39 * Rate limit transient key prefix
40 * Following ThinkRank option naming patterns
41 */
42 private const RATE_LIMIT_KEY = 'thinkrank_analytics_rate_limit';
43
44 /**
45 * Maximum requests per day (Google standard quota)
46 */
47 private const MAX_REQUESTS_PER_DAY = 50000;
48
49 /**
50 * Google Analytics Property ID (GA4)
51 *
52 * @var string
53 */
54 private string $property_id;
55
56 /**
57 * Constructor
58 *
59 * @param string $api_key Google Analytics API key
60 * @param string $property_id GA4 Property ID (properties/XXXXXXXXX)
61 * @param int $timeout Request timeout in seconds
62 * @param string|null $access_token OAuth Access Token (optional)
63 */
64 public function __construct(string $api_key, string $property_id, int $timeout = 30, ?string $access_token = null) {
65 parent::__construct($api_key, $timeout, $access_token);
66 $this->property_id = $property_id;
67 }
68
69 /**
70 * Test API connection
71 * Following ThinkRank test_connection patterns from AI clients
72 *
73 * @return array Connection test results
74 */
75 public function test_connection(): array {
76 try {
77 // Test with a simple metadata request
78 $result = $this->get_metadata();
79
80 return [
81 'success' => true,
82 'message' => 'Google Analytics Data API connection successful',
83 'property_id' => $this->property_id,
84 'dimensions_count' => count($result['dimensions'] ?? []),
85 'metrics_count' => count($result['metrics'] ?? [])
86 ];
87 } catch (\Exception $e) {
88 // `message` as well as `error`: the success branch above reports
89 // its outcome as `message`, and every consumer reads that key, so
90 // a failure that only set `error` was reported to the admin as an
91 // error with no reason at all (#852). `error` is kept for any
92 // caller that already reads it.
93 return [
94 'success' => false,
95 'message' => $e->getMessage(),
96 'error' => $e->getMessage()
97 ];
98 }
99 }
100
101 /**
102 * Get metadata for available dimensions and metrics
103 *
104 * @return array Metadata information
105 * @throws \Exception If API request fails
106 */
107 public function get_metadata(): array {
108 $endpoint = "/{$this->property_id}/metadata";
109 $params = [];
110
111 $full_url = self::API_BASE_URL . $endpoint;
112
113 // Only append API key if no access token is present
114 if (!empty($this->api_key) && empty($this->access_token)) {
115 $params['key'] = $this->api_key;
116 }
117
118 return $this->make_request($full_url, $params, 'GET');
119 }
120
121 /**
122 * Run analytics report for specified metrics and date range
123 *
124 * The window is exactly `$date_range` days long and ends yesterday (UTC):
125 * "30d" is the 30 complete days before today. GA4's startDate and endDate
126 * are both inclusive, so the start is `days - 1` days before the end.
127 * Today is left out because it is a partial day, which is also what
128 * GA4's own "Last 30 days" does. Search Console ends two days earlier
129 * (D-2) because its data lags; GA4's does not, so it is not held back
130 * to match (#923).
131 *
132 * @param string $date_range Date range ('7d', '30d', '90d')
133 * @param array $metrics Metrics to retrieve (GA4 metric names)
134 * @param array $dimensions Dimensions to group by
135 * @return array Analytics report data
136 * @throws \Exception If API request fails
137 */
138 public function run_report(string $date_range = '30d', array $metrics = ['sessions'], array $dimensions = []): array {
139 $endpoint = "/{$this->property_id}:runReport";
140
141 // Convert date range to start/end dates, from one anchor so a request
142 // straddling midnight cannot mix two days.
143 $days = max(1, (int) str_replace('d', '', $date_range));
144 $end_ts = strtotime('-1 day', time());
145 $end_date = gmdate('Y-m-d', $end_ts);
146 $start_date = gmdate('Y-m-d', strtotime('-' . ($days - 1) . ' days', $end_ts));
147
148 $request_body = [
149 'dateRanges' => [
150 [
151 'startDate' => $start_date,
152 'endDate' => $end_date
153 ]
154 ],
155 'metrics' => array_map(function ($metric) {
156 return ['name' => $metric];
157 }, $metrics)
158 ];
159
160 // Add dimensions if provided
161 if (!empty($dimensions)) {
162 $request_body['dimensions'] = array_map(function ($dimension) {
163 return ['name' => $dimension];
164 }, $dimensions);
165 }
166
167 $full_url = self::API_BASE_URL . $endpoint;
168
169 // The API key is sent by make_request() in the x-goog-api-key header — never
170 // in the query string, which is captured by proxy/access logs.
171 return $this->make_request($full_url, $request_body, 'POST');
172 }
173
174 /**
175 * Get website traffic data using GA4 metrics
176 *
177 * @param string $date_range Date range for data
178 * @return array Traffic data
179 * @throws \Exception If API request fails
180 */
181 public function get_traffic_data(string $date_range = '30d'): array {
182 $metrics = [
183 'sessions',
184 'screenPageViews',
185 'bounceRate',
186 'averageSessionDuration',
187 'activeUsers'
188 ];
189
190 $result = $this->run_report($date_range, $metrics);
191
192 // Parse the response and extract metric values
193 $rows = $result['rows'] ?? [];
194 $metric_values = [];
195
196 if (!empty($rows)) {
197 $metric_values = $rows[0]['metricValues'] ?? [];
198 }
199
200 return [
201 'sessions' => (int) ($metric_values[0]['value'] ?? 0),
202 'pageviews' => (int) ($metric_values[1]['value'] ?? 0),
203 'bounce_rate' => (float) ($metric_values[2]['value'] ?? 0),
204 'avg_session_duration' => (float) ($metric_values[3]['value'] ?? 0),
205 'active_users' => (int) ($metric_values[4]['value'] ?? 0),
206 'date_range' => $date_range,
207 'property_id' => $this->property_id
208 ];
209 }
210
211 /**
212 * Get top pages data using GA4 dimensions and metrics
213 *
214 * @param int $limit Number of pages to retrieve
215 * @param string $date_range Date range for data
216 * @return array Top pages data
217 * @throws \Exception If API request fails
218 */
219 public function get_top_pages(int $limit = 10, string $date_range = '30d'): array {
220 $metrics = ['screenPageViews', 'sessions'];
221 $dimensions = ['pagePath', 'pageTitle'];
222
223 $result = $this->run_report($date_range, $metrics, $dimensions);
224
225 $pages = [];
226 $rows = $result['rows'] ?? [];
227
228 foreach (array_slice($rows, 0, $limit) as $row) {
229 $dimension_values = $row['dimensionValues'] ?? [];
230 $metric_values = $row['metricValues'] ?? [];
231
232 $pages[] = [
233 'path' => $dimension_values[0]['value'] ?? '',
234 'title' => $dimension_values[1]['value'] ?? '',
235 'pageviews' => (int) ($metric_values[0]['value'] ?? 0),
236 'sessions' => (int) ($metric_values[1]['value'] ?? 0)
237 ];
238 }
239
240 return [
241 'pages' => $pages,
242 'limit' => $limit,
243 'date_range' => $date_range,
244 'total_pages' => count($rows)
245 ];
246 }
247
248 /**
249 * Get organic search traffic data for SEO analytics
250 *
251 * @param string $date_range Date range for data
252 * @return array Organic traffic data
253 * @throws \Exception If API request fails
254 */
255 public function get_organic_traffic(string $date_range = '30d'): array {
256 $metrics = ['sessions', 'screenPageViews', 'activeUsers'];
257 $dimensions = ['sessionDefaultChannelGrouping'];
258
259 $result = $this->run_report($date_range, $metrics, $dimensions);
260
261 $organic_data = [
262 'sessions' => 0,
263 'pageviews' => 0,
264 'users' => 0
265 ];
266
267 $rows = $result['rows'] ?? [];
268
269 foreach ($rows as $row) {
270 $dimension_values = $row['dimensionValues'] ?? [];
271 $metric_values = $row['metricValues'] ?? [];
272
273 $channel = $dimension_values[0]['value'] ?? '';
274
275 // Filter for organic search traffic
276 if (strtolower($channel) === 'organic search') {
277 $organic_data['sessions'] = (int) ($metric_values[0]['value'] ?? 0);
278 $organic_data['pageviews'] = (int) ($metric_values[1]['value'] ?? 0);
279 $organic_data['users'] = (int) ($metric_values[2]['value'] ?? 0);
280 break;
281 }
282 }
283
284 return [
285 'organic_traffic' => $organic_data,
286 'date_range' => $date_range,
287 'property_id' => $this->property_id
288 ];
289 }
290
291 /**
292 * Get rate limit configuration
293 * Following ThinkRank rate limiting patterns
294 *
295 * @return array Rate limit configuration
296 */
297 protected function get_rate_limits(): array {
298 return [
299 'max_requests_per_day' => self::MAX_REQUESTS_PER_DAY,
300 'reset_time' => get_transient(self::RATE_LIMIT_KEY . '_reset') ?: strtotime('tomorrow')
301 ];
302 }
303
304 /**
305 * Get rate limit transient key
306 * Following ThinkRank option naming patterns
307 *
308 * @return string Rate limit key
309 */
310 protected function get_rate_limit_key(): string {
311 return self::RATE_LIMIT_KEY;
312 }
313
314 /**
315 * Get rate limit error message
316 *
317 * @return string Error message
318 */
319 protected function get_rate_limit_error_message(): string {
320 return 'Google Analytics API rate limit exceeded. Try again tomorrow.';
321 }
322
323 /**
324 * Get measurement ID
325 *
326 * @return string Measurement ID
327 */
328 public function get_measurement_id(): string {
329 return $this->property_id;
330 }
331
332 /**
333 * Get property ID
334 *
335 * @return string Property ID
336 */
337 public function get_property_id(): string {
338 return $this->property_id;
339 }
340 }
341