PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-ga4.php

class-metasync-ga4.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at includes/class-metasync-ga4.php

313 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GA4 Analytics Integration for MetaSync Plugin
4 *
5 * Client-side (gtag.js) for admin page views and 1-click activation.
6 * Server-side (Measurement Protocol) for Content Genius and OTTO events.
7 *
8 * @package Search Atlas SEO
9 * @copyright Copyright (C) 2021-2026, Search Atlas Group - [email protected]
10 * @link https://searchatlas.com/
11 * @since 2.6.0
12 */
13
14 // If this file is called directly, abort.
15 if (!defined('WPINC')) {
16 die;
17 }
18
19 class Metasync_GA4 {
20
21 private const OPT_IN_OPTION = 'metasync_analytics_opt_in';
22
23 private static $instance = null;
24
25 public static function get_instance() {
26 if (self::$instance === null) {
27 self::$instance = new self();
28 }
29 return self::$instance;
30 }
31
32 private function __construct() {
33 if (!$this->is_configured()) {
34 return;
35 }
36
37 add_action('admin_enqueue_scripts', array($this, 'enqueue_ga4_script'));
38 add_action('admin_head', array($this, 'track_admin_page_view'));
39 }
40
41 private function get_measurement_id() {
42 return defined('METASYNC_GA4_MEASUREMENT_ID') && !empty(METASYNC_GA4_MEASUREMENT_ID)
43 ? METASYNC_GA4_MEASUREMENT_ID
44 : null;
45 }
46
47 private function is_configured() {
48 return $this->get_measurement_id() !== null;
49 }
50
51 public function is_opted_in() {
52 return get_option(self::OPT_IN_OPTION, 'no') === 'yes';
53 }
54
55 private function get_anonymized_user_id() {
56 $current_user = wp_get_current_user();
57 if (!$current_user || !$current_user->ID) {
58 return 'guest';
59 }
60
61 $site_identifier = get_option('siteurl');
62 return 'user_' . wp_hash($current_user->ID . '|' . $site_identifier);
63 }
64
65 private function get_plugin_version() {
66 return defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown';
67 }
68
69 private function is_plugin_admin_page() {
70 if (!isset($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets
71 return false;
72 }
73
74 $page = sanitize_text_field(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets
75
76 $plugin_slug = 'searchatlas';
77 if (class_exists('Metasync_Admin') && isset(Metasync_Admin::$page_slug)) {
78 $plugin_slug = Metasync_Admin::$page_slug;
79 }
80
81 return $page === $plugin_slug || strpos($page, $plugin_slug . '-') === 0;
82 }
83
84 private function get_admin_screen_name() {
85 if (!isset($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets
86 return 'Unknown';
87 }
88
89 $page = sanitize_text_field(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets
90
91 $plugin_slug = 'searchatlas';
92 if (class_exists('Metasync_Admin') && isset(Metasync_Admin::$page_slug)) {
93 $plugin_slug = Metasync_Admin::$page_slug;
94 }
95
96 $screen_map = array(
97 $plugin_slug => 'Settings',
98 $plugin_slug . '-dashboard' => 'Dashboard',
99 $plugin_slug . '-compatibility' => 'Compatibility',
100 $plugin_slug . '-sync-log' => 'Changes Log',
101 $plugin_slug . '-seo-controls' => 'Indexation Control',
102 $plugin_slug . '-redirections' => 'Redirections',
103 $plugin_slug . '-xml-sitemap' => 'XML Sitemap',
104 $plugin_slug . '-robots-txt' => 'Robots.txt',
105 $plugin_slug . '-404-monitor' => '404 Monitor',
106 'ottodebug' => 'OTTO Debug',
107 );
108
109 if ($page === $plugin_slug && isset($_GET['tab'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets
110 $tab = sanitize_text_field(wp_unslash($_GET['tab'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets
111 return 'Settings - ' . ucfirst(str_replace('_', ' ', $tab));
112 }
113
114 return $screen_map[$page] ?? 'Unknown Page';
115 }
116
117 public function enqueue_ga4_script() {
118 if (!$this->is_configured() || !$this->is_plugin_admin_page() || !$this->is_opted_in()) {
119 return;
120 }
121
122 $measurement_id = esc_js($this->get_measurement_id());
123 $user_id = esc_js($this->get_anonymized_user_id());
124 $plugin_version = esc_js($this->get_plugin_version());
125 $wp_version = esc_js(get_bloginfo('version'));
126 $php_version = esc_js(PHP_VERSION);
127
128 $inline_script = "
129 var gtagScript = document.createElement('script');
130 gtagScript.async = true;
131 gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id={$measurement_id}';
132 document.head.appendChild(gtagScript);
133
134 window.dataLayer = window.dataLayer || [];
135 function gtag(){dataLayer.push(arguments);}
136 gtag('js', new Date());
137 gtag('config', '{$measurement_id}', {
138 'user_id': '{$user_id}',
139 'send_page_view': false
140 });
141 gtag('set', {
142 'plugin_version': '{$plugin_version}',
143 'wp_version': '{$wp_version}',
144 'php_version': '{$php_version}'
145 });
146
147 window.metasyncGA4Track = function(eventName, params) {
148 if (typeof gtag !== 'undefined') {
149 gtag('event', eventName, params || {});
150 }
151 };";
152
153 wp_register_script('metasync-ga4-init', '', array(), $this->get_plugin_version(), false);
154 wp_enqueue_script('metasync-ga4-init');
155 wp_add_inline_script('metasync-ga4-init', $inline_script, 'before');
156 }
157
158 public function track_admin_page_view() {
159 if (!$this->is_configured() || !$this->is_plugin_admin_page() || !$this->is_opted_in()) {
160 return;
161 }
162
163 $screen_name = $this->get_admin_screen_name();
164 $params = wp_json_encode(
165 array('screen_name' => $screen_name),
166 JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
167 );
168
169 $inline_tracking = "<script type='text/javascript'>\n if (typeof gtag !== 'undefined') {\n gtag('event', 'plugin_admin_accessed', " . $params . ");\n }\n </script>";
170 echo $inline_tracking; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $params is wp_json_encode() output with JSON_HEX_TAG/AMP/APOS/QUOT, safe inside an inline <script>
171 }
172
173 /**
174 * Track Content Genius article creation/update (server-side)
175 * @param int $post_id
176 * @param string $action 'created' or 'updated'
177 */
178 public function track_content_genius_event($post_id, $action) {
179 if (!$this->is_configured() || !$this->is_opted_in()) {
180 return;
181 }
182
183 $post = get_post($post_id);
184 if (!$post) {
185 return;
186 }
187
188 // Bulk syncs fire this once per article; roll up to at most one event
189 // per action per hour with accumulated counts so a sync of N articles
190 // stays a single send (same shape as the PostHog rollup).
191 $action = strtolower($action);
192 $key = 'metasync_ga4_cg_' . $action;
193 $state = get_option($key, array());
194 if (!is_array($state)) {
195 $state = array();
196 }
197 $counts = isset($state['counts']) && is_array($state['counts']) ? $state['counts'] : array();
198 $sent_at = (int) ($state['sent_at'] ?? 0);
199 $type = (string) $post->post_type;
200 $counts[$type] = ($counts[$type] ?? 0) + 1;
201
202 if (time() - $sent_at >= HOUR_IN_SECONDS) {
203 $this->send_mp_event('content_genius_article_' . $action, array(
204 'content_type' => $type,
205 'action' => ucfirst($action),
206 'count' => array_sum($counts),
207 ));
208 update_option($key, array('counts' => array(), 'sent_at' => time()), false);
209 } else {
210 update_option($key, array('counts' => $counts, 'sent_at' => $sent_at), false);
211 }
212 }
213
214 /**
215 * Track OTTO page optimization (server-side)
216 * @param array $data Notification data from OTTO
217 */
218 public function track_otto_optimization($data) {
219 if (!$this->is_configured() || !$this->is_opted_in()) {
220 return;
221 }
222
223 $urls = isset($data['urls']) ? $data['urls'] : array();
224
225 foreach ($urls as $url) {
226 $post_id = url_to_postid((isset($data['domain']) ? $data['domain'] : '') . $url);
227 $content_type = 'unknown';
228
229 if ($post_id > 0) {
230 $post = get_post($post_id);
231 if ($post) {
232 $content_type = $post->post_type;
233 }
234 }
235
236 $this->send_mp_event('otto_page_optimized', array(
237 'content_type' => $content_type,
238 'url' => $url,
239 ));
240 }
241 }
242
243 /**
244 * Track 1-Click Activation (server-side)
245 * @param string $auth_method
246 * @param bool $is_reconnection
247 */
248 public function track_one_click_activation($auth_method = 'searchatlas_connect', $is_reconnection = false) {
249 if (!$this->is_configured() || !$this->is_opted_in()) {
250 return;
251 }
252
253 $this->send_mp_event('one_click_activation', array(
254 'auth_method' => $auth_method,
255 'is_reconnection' => $is_reconnection,
256 ));
257 }
258
259 /**
260 * Get GA4 Measurement Protocol API secret
261 * @return string|null
262 */
263 private function get_api_secret() {
264 return defined('METASYNC_GA4_API_SECRET') && !empty(METASYNC_GA4_API_SECRET)
265 ? METASYNC_GA4_API_SECRET
266 : null;
267 }
268
269 /**
270 * Send event via GA4 Measurement Protocol (server-side)
271 * Requires METASYNC_GA4_API_SECRET to be set; silently skips if not configured.
272 * @param string $event_name
273 * @param array $params
274 */
275 private function send_mp_event($event_name, $params = array()) {
276 $measurement_id = $this->get_measurement_id();
277 $api_secret = $this->get_api_secret();
278
279 if (!$measurement_id || !$api_secret) {
280 return;
281 }
282
283 $params['plugin_version'] = $this->get_plugin_version();
284 $params['wp_version'] = get_bloginfo('version');
285 $params['php_version'] = PHP_VERSION;
286
287 $body = wp_json_encode(array(
288 'client_id' => $this->get_anonymized_user_id(),
289 'events' => array(
290 array(
291 'name' => $event_name,
292 'params' => $params,
293 ),
294 ),
295 ));
296
297 $url = add_query_arg(
298 array(
299 'measurement_id' => $measurement_id,
300 'api_secret' => $api_secret,
301 ),
302 'https://www.google-analytics.com/mp/collect'
303 );
304
305 wp_remote_post($url, array(
306 'body' => $body,
307 'headers' => array('Content-Type' => 'application/json'),
308 'timeout' => 5,
309 'blocking' => false,
310 ));
311 }
312 }
313