PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 2.12.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v2.12.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 1.27.0 1.26.0 1.25.0 trunk All 53 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.12.0, at includes/integrations/class-google-analytics-client.php

331 lines 10.2 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 * @param string $date_range Date range ('7d', '30d', '90d')
125 * @param array $metrics Metrics to retrieve (GA4 metric names)
126 * @param array $dimensions Dimensions to group by
127 * @return array Analytics report data
128 * @throws \Exception If API request fails
129 */
130 public function run_report(string $date_range = '30d', array $metrics = ['sessions'], array $dimensions = []): array {
131 $endpoint = "/{$this->property_id}:runReport";
132
133 // Convert date range to start/end dates
134 $end_date = gmdate('Y-m-d');
135 $days = (int) str_replace('d', '', $date_range);
136 $start_date = gmdate('Y-m-d', strtotime("-{$days} days"));
137
138 $request_body = [
139 'dateRanges' => [
140 [
141 'startDate' => $start_date,
142 'endDate' => $end_date
143 ]
144 ],
145 'metrics' => array_map(function ($metric) {
146 return ['name' => $metric];
147 }, $metrics)
148 ];
149
150 // Add dimensions if provided
151 if (!empty($dimensions)) {
152 $request_body['dimensions'] = array_map(function ($dimension) {
153 return ['name' => $dimension];
154 }, $dimensions);
155 }
156
157 $full_url = self::API_BASE_URL . $endpoint;
158
159 // The API key is sent by make_request() in the x-goog-api-key header — never
160 // in the query string, which is captured by proxy/access logs.
161 return $this->make_request($full_url, $request_body, 'POST');
162 }
163
164 /**
165 * Get website traffic data using GA4 metrics
166 *
167 * @param string $date_range Date range for data
168 * @return array Traffic data
169 * @throws \Exception If API request fails
170 */
171 public function get_traffic_data(string $date_range = '30d'): array {
172 $metrics = [
173 'sessions',
174 'screenPageViews',
175 'bounceRate',
176 'averageSessionDuration',
177 'activeUsers'
178 ];
179
180 $result = $this->run_report($date_range, $metrics);
181
182 // Parse the response and extract metric values
183 $rows = $result['rows'] ?? [];
184 $metric_values = [];
185
186 if (!empty($rows)) {
187 $metric_values = $rows[0]['metricValues'] ?? [];
188 }
189
190 return [
191 'sessions' => (int) ($metric_values[0]['value'] ?? 0),
192 'pageviews' => (int) ($metric_values[1]['value'] ?? 0),
193 'bounce_rate' => (float) ($metric_values[2]['value'] ?? 0),
194 'avg_session_duration' => (float) ($metric_values[3]['value'] ?? 0),
195 'active_users' => (int) ($metric_values[4]['value'] ?? 0),
196 'date_range' => $date_range,
197 'property_id' => $this->property_id
198 ];
199 }
200
201 /**
202 * Get top pages data using GA4 dimensions and metrics
203 *
204 * @param int $limit Number of pages to retrieve
205 * @param string $date_range Date range for data
206 * @return array Top pages data
207 * @throws \Exception If API request fails
208 */
209 public function get_top_pages(int $limit = 10, string $date_range = '30d'): array {
210 $metrics = ['screenPageViews', 'sessions'];
211 $dimensions = ['pagePath', 'pageTitle'];
212
213 $result = $this->run_report($date_range, $metrics, $dimensions);
214
215 $pages = [];
216 $rows = $result['rows'] ?? [];
217
218 foreach (array_slice($rows, 0, $limit) as $row) {
219 $dimension_values = $row['dimensionValues'] ?? [];
220 $metric_values = $row['metricValues'] ?? [];
221
222 $pages[] = [
223 'path' => $dimension_values[0]['value'] ?? '',
224 'title' => $dimension_values[1]['value'] ?? '',
225 'pageviews' => (int) ($metric_values[0]['value'] ?? 0),
226 'sessions' => (int) ($metric_values[1]['value'] ?? 0)
227 ];
228 }
229
230 return [
231 'pages' => $pages,
232 'limit' => $limit,
233 'date_range' => $date_range,
234 'total_pages' => count($rows)
235 ];
236 }
237
238 /**
239 * Get organic search traffic data for SEO analytics
240 *
241 * @param string $date_range Date range for data
242 * @return array Organic traffic data
243 * @throws \Exception If API request fails
244 */
245 public function get_organic_traffic(string $date_range = '30d'): array {
246 $metrics = ['sessions', 'screenPageViews', 'activeUsers'];
247 $dimensions = ['sessionDefaultChannelGrouping'];
248
249 $result = $this->run_report($date_range, $metrics, $dimensions);
250
251 $organic_data = [
252 'sessions' => 0,
253 'pageviews' => 0,
254 'users' => 0
255 ];
256
257 $rows = $result['rows'] ?? [];
258
259 foreach ($rows as $row) {
260 $dimension_values = $row['dimensionValues'] ?? [];
261 $metric_values = $row['metricValues'] ?? [];
262
263 $channel = $dimension_values[0]['value'] ?? '';
264
265 // Filter for organic search traffic
266 if (strtolower($channel) === 'organic search') {
267 $organic_data['sessions'] = (int) ($metric_values[0]['value'] ?? 0);
268 $organic_data['pageviews'] = (int) ($metric_values[1]['value'] ?? 0);
269 $organic_data['users'] = (int) ($metric_values[2]['value'] ?? 0);
270 break;
271 }
272 }
273
274 return [
275 'organic_traffic' => $organic_data,
276 'date_range' => $date_range,
277 'property_id' => $this->property_id
278 ];
279 }
280
281 /**
282 * Get rate limit configuration
283 * Following ThinkRank rate limiting patterns
284 *
285 * @return array Rate limit configuration
286 */
287 protected function get_rate_limits(): array {
288 return [
289 'max_requests_per_day' => self::MAX_REQUESTS_PER_DAY,
290 'reset_time' => get_transient(self::RATE_LIMIT_KEY . '_reset') ?: strtotime('tomorrow')
291 ];
292 }
293
294 /**
295 * Get rate limit transient key
296 * Following ThinkRank option naming patterns
297 *
298 * @return string Rate limit key
299 */
300 protected function get_rate_limit_key(): string {
301 return self::RATE_LIMIT_KEY;
302 }
303
304 /**
305 * Get rate limit error message
306 *
307 * @return string Error message
308 */
309 protected function get_rate_limit_error_message(): string {
310 return 'Google Analytics API rate limit exceeded. Try again tomorrow.';
311 }
312
313 /**
314 * Get measurement ID
315 *
316 * @return string Measurement ID
317 */
318 public function get_measurement_id(): string {
319 return $this->property_id;
320 }
321
322 /**
323 * Get property ID
324 *
325 * @return string Property ID
326 */
327 public function get_property_id(): string {
328 return $this->property_id;
329 }
330 }
331