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

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

349 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Google Analytics Tracking Manager Class
4 *
5 * Handles GA4 tracking code injection, verification, and conflict detection.
6 * Follows ThinkRank patterns for frontend integration and security standards.
7 *
8 * @package ThinkRank\Frontend
9 * @since 1.0.0
10 */
11
12 declare(strict_types=1);
13
14 namespace ThinkRank\Frontend;
15
16 use ThinkRank\Core\Settings;
17
18 // Prevent direct access
19 if (!defined('ABSPATH')) {
20 exit;
21 }
22
23 /**
24 * Google Analytics Tracking Manager Class
25 *
26 * Single Responsibility: Manage GA4 tracking code injection and verification
27 * Following ThinkRank frontend patterns from SEO_Manager
28 *
29 * @since 1.0.0
30 */
31 class Google_Analytics_Tracking_Manager {
32
33 /**
34 * Settings instance
35 *
36 * @var Settings
37 */
38 private Settings $settings;
39
40 /**
41 * Constructor
42 *
43 * @since 1.0.0
44 */
45 public function __construct() {
46 $this->settings = new Settings();
47 $this->init();
48 }
49
50 /**
51 * Initialize tracking manager
52 * Following ThinkRank initialization patterns
53 *
54 * @since 1.0.0
55 * @return void
56 */
57 public function init(): void {
58 // Always add the hook, but check conditions during injection
59 add_action('wp_head', [$this, 'inject_ga4_tracking'], 1);
60 }
61
62 /**
63 * Determine if tracking should be injected
64 * Following ThinkRank conditional logic patterns
65 *
66 * @since 1.0.0
67 * @return bool Whether to inject tracking
68 */
69 private function should_inject_tracking(): bool {
70 // Don't inject in admin area
71 if (is_admin()) {
72 return false;
73 }
74
75 $measurement_id = $this->get_setting('ga4_measurement_id');
76 $auto_inject = $this->get_setting('ga4_auto_inject');
77
78 // Basic requirements
79 if (empty($measurement_id) || !$auto_inject) {
80 return false;
81 }
82
83 // Exclude admin users if configured
84 $exclude_admin = (bool) $this->get_setting('ga4_exclude_admin');
85 if ($exclude_admin && current_user_can('manage_options')) {
86 return false;
87 }
88
89 return true;
90 }
91
92 /**
93 * Inject GA4 tracking code
94 * Following WordPress script enqueuing standards
95 *
96 * @since 1.0.0
97 * @return void
98 */
99 public function inject_ga4_tracking(): void {
100 // Check if we should inject tracking at runtime
101 if (!$this->should_inject_tracking()) {
102 return;
103 }
104
105 $measurement_id = $this->get_setting('ga4_measurement_id');
106
107 if (empty($measurement_id)) {
108 return;
109 }
110
111 // Sanitize and validate measurement ID
112 $measurement_id = sanitize_text_field($measurement_id);
113
114 if (!preg_match('/^G-[A-Z0-9]{10}$/', $measurement_id)) {
115 echo "<!-- Invalid GA4 Measurement ID format -->\n";
116 return;
117 }
118
119 $this->enqueue_ga4_scripts($measurement_id);
120 }
121
122 /**
123 * Enqueue GA4 tracking scripts using WordPress standards
124 * Following WordPress script enqueuing patterns
125 *
126 * @since 1.0.0
127 * @param string $measurement_id GA4 Measurement ID
128 * @return void
129 */
130 private function enqueue_ga4_scripts(string $measurement_id): void {
131 $anonymize_ip = (bool) $this->get_setting('ga4_anonymize_ip');
132
133 // Enqueue external Google Analytics script
134 wp_enqueue_script(
135 'google-analytics-gtag',
136 "https://www.googletagmanager.com/gtag/js?id=" . esc_attr($measurement_id),
137 [],
138 null, // Use null for external scripts to avoid version query parameter
139 false // Load in head for proper GA4 initialization
140 );
141
142 // Add async attribute to the external script
143 add_filter('script_loader_tag', [$this, 'add_async_attribute'], 10, 2);
144
145 // Generate inline configuration script
146 $config_options = [];
147 if ($anonymize_ip) {
148 $config_options[] = "'anonymize_ip': true";
149 }
150 $config_string = !empty($config_options) ? ', {' . implode(', ', $config_options) . '}' : '';
151
152 $inline_script = "
153 window.dataLayer = window.dataLayer || [];
154 function gtag(){dataLayer.push(arguments);}
155 gtag('js', new Date());
156 gtag('config', '" . esc_js($measurement_id) . "'" . $config_string . ");
157 ";
158
159 // Add inline script after the external script
160 wp_add_inline_script('google-analytics-gtag', $inline_script);
161
162 // Add HTML comment for identification
163 add_action('wp_head', function() use ($measurement_id) {
164 echo "<!-- ThinkRank SEO: Google Analytics 4 Tracking (Measurement ID: " . esc_attr($measurement_id) . ") -->\n";
165 }, 0);
166 }
167
168 /**
169 * Add async attribute to Google Analytics script
170 * Following WordPress script attribute patterns
171 *
172 * @since 1.0.0
173 * @param string $tag Script tag HTML
174 * @param string $handle Script handle
175 * @return string Modified script tag
176 */
177 public function add_async_attribute(string $tag, string $handle): string {
178 if ('google-analytics-gtag' === $handle) {
179 return str_replace(' src', ' async src', $tag);
180 }
181 return $tag;
182 }
183
184
185
186
187
188
189
190
191
192 /**
193 * Verify tracking is working
194 * Following ThinkRank verification patterns
195 *
196 * @since 1.0.0
197 * @param string $measurement_id GA4 Measurement ID to verify
198 * @return array Verification results
199 */
200 public function verify_tracking(string $measurement_id): array {
201 // Sanitize and validate measurement ID
202 $measurement_id = sanitize_text_field($measurement_id);
203
204 if (!preg_match('/^G-[A-Z0-9]{10}$/', $measurement_id)) {
205 return [
206 'success' => false,
207 'message' => __('Invalid GA4 Measurement ID format. Expected format: G-XXXXXXXXXX', 'thinkrank')
208 ];
209 }
210
211 // Check homepage for tracking code
212 $home_url = home_url();
213 $response = wp_remote_get($home_url, [
214 'timeout' => 15,
215 'user-agent' => 'ThinkRank/' . THINKRANK_VERSION . ' Verification Bot'
216 ]);
217
218 if (is_wp_error($response)) {
219 return [
220 'success' => false,
221 'message' => sprintf(
222 /* translators: %s: Error message from WordPress HTTP request */
223 __('Could not verify tracking: %s', 'thinkrank'),
224 $response->get_error_message()
225 )
226 ];
227 }
228
229 $body = wp_remote_retrieve_body($response);
230 $status_code = wp_remote_retrieve_response_code($response);
231
232 if ($status_code !== 200) {
233 return [
234 'success' => false,
235 'message' => sprintf(
236 /* translators: %d: HTTP status code number (e.g., 404, 500) */
237 __('Could not verify tracking: HTTP %d response', 'thinkrank'),
238 $status_code
239 )
240 ];
241 }
242
243 // Check for GA4 script presence
244 $has_measurement_id = strpos($body, $measurement_id) !== false;
245 $has_gtag = strpos($body, 'gtag') !== false;
246 $has_ga4_script = strpos($body, 'googletagmanager.com/gtag/js') !== false;
247
248 if ($has_measurement_id && $has_gtag && $has_ga4_script) {
249 // Update verification status
250 $this->settings->set('ga4_tracking_verified', true);
251 $this->settings->set('ga4_last_verification', current_time('mysql'));
252
253 return [
254 'success' => true,
255 'message' => __('�
256 GA4 tracking code detected and working correctly!', 'thinkrank')
257 ];
258 }
259
260 // Provide specific feedback
261 if (!$has_ga4_script) {
262 return [
263 'success' => false,
264 'message' => __('⚠️ GA4 script not detected. Please check your configuration or enable auto-inject.', 'thinkrank')
265 ];
266 }
267
268 if (!$has_measurement_id) {
269 return [
270 'success' => false,
271 'message' => __('⚠️ Measurement ID not found in tracking code. Please verify your Measurement ID.', 'thinkrank')
272 ];
273 }
274
275 return [
276 'success' => false,
277 'message' => __('⚠️ GA4 tracking code not properly configured. Please check your setup.', 'thinkrank')
278 ];
279 }
280
281 /**
282 * Detect existing GA4 implementations
283 * Following ThinkRank conflict detection patterns
284 *
285 * @since 1.0.0
286 * @return array Array of detected conflicts
287 */
288 public function detect_existing_tracking(): array {
289 $conflicts = [];
290
291 // Check for common GA4 plugins
292 $ga4_plugins = [
293 'google-analytics-for-wordpress/googleanalytics.php' => 'MonsterInsights',
294 'ga-google-analytics/ga-google-analytics.php' => 'GA Google Analytics',
295 'google-analytics-dashboard-for-wp/gadwp.php' => 'ExactMetrics',
296 'gtag/gtag.php' => 'Gtag Plugin',
297 'google-site-kit/google-site-kit.php' => 'Site Kit by Google'
298 ];
299
300 foreach ($ga4_plugins as $plugin_file => $plugin_name) {
301 if (is_plugin_active($plugin_file)) {
302 $conflicts[] = [
303 'type' => 'plugin',
304 'name' => $plugin_name,
305 'file' => $plugin_file,
306 'recommendation' => 'disable_auto_inject'
307 ];
308 }
309 }
310
311 // Check for manual GA4 in active theme
312 $theme_files = [
313 get_template_directory() . '/header.php',
314 get_template_directory() . '/functions.php'
315 ];
316
317 foreach ($theme_files as $file) {
318 if (file_exists($file) && is_readable($file)) {
319 $content = file_get_contents($file);
320 if ($content && (strpos($content, 'gtag') !== false || strpos($content, 'G-') !== false)) {
321 $conflicts[] = [
322 'type' => 'theme',
323 'name' => get_template(),
324 'file' => basename($file),
325 'recommendation' => 'verify_manual'
326 ];
327 }
328 }
329 }
330
331 return $conflicts;
332 }
333
334
335
336 /**
337 * Get setting value with proper fallback
338 * Following ThinkRank settings access patterns
339 *
340 * @since 1.0.0
341 * @param string $key Setting key
342 * @param mixed $default Default value
343 * @return mixed Setting value
344 */
345 private function get_setting(string $key, $default = '') {
346 return $this->settings->get($key, $default);
347 }
348 }
349