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
← All changes | telemetry/sentry-wordpress-integration.php +200 -38 2.6.14 → 2.7.2 View file →
@@ -3,8 +3,10 @@
3 3 if (!defined('ABSPATH')) {
4 4 exit;
5 5 }
6 6
7 +require_once __DIR__ . '/privacy.php';
8 +
7 9 /**
8 10 * WordPress-Native Sentry Integration
9 11 *
10 12 * Uses WordPress HTTP API to send data directly to Sentry
@@ -41,14 +43,14 @@
41 43 $this->host = null; // Proxy handles this
42 44 $this->scheme = 'proxy';
43 45 } else {
44 46 // Legacy DSN format for backward compatibility
45 - $parsed = parse_url($this->dsn);
46 - $this->public_key = isset($parsed['user']) ? $parsed['user'] : null;
47 - $this->secret_key = isset($parsed['pass']) ? $parsed['pass'] : null;
48 - $this->project_id = trim($parsed['path'], '/');
49 - $this->host = isset($parsed['host']) ? $parsed['host'] : null;
50 - $this->scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https';
47 + $parsed = wp_parse_url($this->dsn);
48 + $this->public_key = is_array($parsed) && isset($parsed['user']) ? $parsed['user'] : null;
49 + $this->secret_key = is_array($parsed) && isset($parsed['pass']) ? $parsed['pass'] : null;
50 + $this->project_id = is_array($parsed) && isset($parsed['path']) ? trim($parsed['path'], '/') : null;
51 + $this->host = is_array($parsed) && isset($parsed['host']) ? $parsed['host'] : null;
52 + $this->scheme = is_array($parsed) && isset($parsed['scheme']) ? $parsed['scheme'] : 'https';
51 53 }
52 54 }
53 55
54 56 /**
@@ -67,8 +69,97 @@
67 69 $data = $this->formatMessage($message, $level, $extra);
68 70 $result = $this->sendToSentry($data, 'event', $attachment);
69 71 return is_array($result) ? $result['success'] : $result;
70 72 }
73 +
74 + /**
75 + * Capture a message without waiting for Sentry to answer.
76 + *
77 + * captureMessage() blocks: sendToSentry() uses cURL with a 5s connect and 5s
78 + * read timeout, so a slow or unreachable collector can add up to ten seconds
79 + * to whatever request called it. That is acceptable for admin-side and cron
80 + * work, but not on a visitor's page render — least of all when the reason we
81 + * are reporting is that the request is already degraded.
82 + *
83 + * This variant hands the envelope to WordPress's HTTP API with
84 + * 'blocking' => false, so the request is dispatched and the caller continues
85 + * immediately. The trade-off is that delivery is unconfirmed and never
86 + * retried: a dropped report is silently lost. For counting how often
87 + * something happens that is a fair exchange for not touching page-load time.
88 + *
89 + * The token is read from cache only. metasync_get_jwt_token() falls through to
90 + * a fresh fetch on a cache miss — a blocking POST with a 15 second timeout —
91 + * which would defeat the whole point of this method and land that cost on an
92 + * already-degraded request. With no cached token the report is skipped; the
93 + * next admin or cron request repopulates the cache.
94 + *
95 + * @param string $message Message to record.
96 + * @param string $level Sentry level (info|warning|error|fatal).
97 + * @param array $extra Additional context.
98 + * @return bool True if a request was dispatched, false if it could not be.
99 + */
100 + public function captureMessageNonBlocking($message, $level = 'info', $extra = []) {
101 + if (metasync_telemetry_is_disabled()) {
102 + return false;
103 + }
104 +
105 + # Mirrors sendToSentry()'s preconditions.
106 + if ($this->isLocalhost()) {
107 + return false;
108 + }
109 +
110 + try {
111 + # method_exists() is redundant to static analysis — this MR adds the
112 + # method, so PHPStan proves the call always true. Kept for the upgrade
113 + # window: during a plugin update an opcache can still hold the previous
114 + # Metasync_Connect_Manager, where class_exists() passes but the
115 + # cache-only accessor is absent. Falling through to the else branch is
116 + # the safe outcome there; an unguarded call would fatal.
117 + # @phpstan-ignore-next-line function.alreadyNarrowedType
118 + if (class_exists('Metasync_Connect_Manager') && method_exists('Metasync_Connect_Manager', 'get_cached_jwt_token')) {
119 + $jwt_token = Metasync_Connect_Manager::get_cached_jwt_token();
120 + } else {
121 + # No cache-only accessor available — skip rather than risk the
122 + # blocking fetch path.
123 + return false;
124 + }
125 + } catch (Exception $e) {
126 + return false;
127 + } catch (Error $e) {
128 + return false;
129 + }
130 +
131 + if (empty($jwt_token)) {
132 + return false;
133 + }
134 +
135 + $data = $this->formatMessage($message, $level, $extra);
136 + $envelope = $this->createSentryEnvelope($data, 'event', null);
137 +
138 + if (empty($envelope)) {
139 + return false;
140 + }
141 +
142 + $plugin_version = defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0';
143 +
144 + # Same tunnel endpoint sendToSentry() posts to.
145 + $url = 'https://wordpress.telemetry.infra.searchatlas.com/api/4509950439849985/envelope/';
146 +
147 + wp_remote_post($url, [
148 + 'blocking' => false,
149 + 'timeout' => 0.01,
150 + 'sslverify' => true,
151 + 'headers' => [
152 + 'Authorization' => 'Bearer ' . $jwt_token,
153 + 'Content-Type' => 'application/x-sentry-envelope',
154 + 'X-Plugin-Version' => $plugin_version,
155 + 'User-Agent' => 'WordPress MetaSync Plugin/' . $plugin_version,
156 + ],
157 + 'body' => $envelope,
158 + ]);
159 +
160 + return true;
161 + }
71 162
72 163 /**
73 164 * Capture user feedback and send to Sentry
74 165 *
@@ -152,8 +243,12 @@
152 243 } else {
153 244 $event_data['tags'] = ['category' => 'user-feedback'];
154 245 }
155 246
247 + // Attach white-label metadata to the issue-report event (not only the
248 + // user-feedback item) so support can identify the reporting site.
249 + $event_data = self::apply_white_label_context($event_data, $feedback);
250 +
156 251 // Send the event first with attachment
157 252 $event_result = $this->sendToSentry($event_data, 'event', $attachment);
158 253
159 254 // Extract event_id from response
@@ -178,10 +273,47 @@
178 273 $feedback_result = $this->sendToSentry($data, 'user_report');
179 274
180 275 return is_array($feedback_result) ? $feedback_result['success'] : $feedback_result;
181 276 }
182 -
277 +
183 278 /**
279 + * Attach white-label metadata to an issue-report event payload.
280 + *
281 + * Support needs to tell from the Sentry event alone whether the reporting
282 + * site is white-labeled, so the values go on the event itself (tags plus a
283 + * white_label context), not only on the associated user-feedback item.
284 + *
285 + * Static and public so the unit suite can exercise the mapping directly;
286 + * captureFeedback() runs the result through a private curl sender that has
287 + * no test seam.
288 + *
289 + * @param array $event_data Event payload built by formatMessage().
290 + * @param array $feedback Feedback data supplied by the report handler.
291 + * @return array The event payload with white-label tags and context added.
292 + */
293 + public static function apply_white_label_context(array $event_data, array $feedback)
294 + {
295 + $is_whitelabel = !empty($feedback['is_whitelabel']);
296 + $brand_name = isset($feedback['whitelabel_brand_name'])
297 + ? sanitize_text_field($feedback['whitelabel_brand_name']) : '';
298 + $support_email = isset($feedback['whitelabel_support_email'])
299 + ? sanitize_email($feedback['whitelabel_support_email']) : '';
300 + $site_url = isset($feedback['site_url']) ? esc_url_raw($feedback['site_url']) : '';
301 + $event_data['tags']['is_whitelabel'] = $is_whitelabel ? 'true' : 'false';
302 + $event_data['tags']['whitelabel_brand_name'] = $brand_name;
303 + $event_data['tags']['whitelabel_support_email'] = $support_email;
304 + $event_data['tags']['site_url'] = $site_url;
305 + $event_data['contexts']['white_label'] = array(
306 + 'is_whitelabel' => $is_whitelabel,
307 + 'whitelabel_brand_name' => $brand_name,
308 + 'whitelabel_support_email' => $support_email,
309 + 'site_url' => $site_url,
310 + );
311 +
312 + return $event_data;
313 + }
314 +
315 + /**
184 316 * Format exception data for Sentry API
185 317 */
186 318 private function formatException($exception, $extra = []) {
187 319 $trace = [];
@@ -302,9 +434,9 @@
302 434 /**
303 435 * Check if the current environment is localhost/development
304 436 */
305 437 private function isLocalhost() {
306 - $host = parse_url(home_url(), PHP_URL_HOST);
438 + $host = wp_parse_url(home_url(), PHP_URL_HOST);
307 439
308 440 // Check for common localhost patterns
309 441 $localhost_patterns = [
310 442 'localhost',
@@ -317,9 +449,9 @@
317 449 '.localhost'
318 450 ];
319 451
320 452 foreach ($localhost_patterns as $pattern) {
321 - if (strpos($host, $pattern) !== false) {
453 + if ($host !== false && $host !== null && strpos($host, $pattern) !== false) {
322 454 return true;
323 455 }
324 456 }
325 457
@@ -347,8 +479,12 @@
347 479 * @param array|null $attachment Optional attachment data
348 480 * @return bool|array Success status, or array with success and event_id
349 481 */
350 482 private function sendToSentry($data, $item_type = 'event', $attachment = null) {
483 + if (metasync_telemetry_is_disabled()) {
484 + return false;
485 + }
486 +
351 487 // Skip sending to Sentry if running on localhost/development environment
352 488 if ($this->isLocalhost()) {
353 489 return false;
354 490 }
@@ -384,32 +520,23 @@
384 520 'X-Plugin-Version' => $plugin_version,
385 521 'User-Agent' => 'WordPress MetaSync Plugin/' . $plugin_version
386 522 ];
387 523
388 - // Use cURL directly to ensure proper envelope format
389 - $ch = curl_init();
390 - curl_setopt($ch, CURLOPT_URL, $url);
391 - curl_setopt($ch, CURLOPT_POST, true);
392 - curl_setopt($ch, CURLOPT_POSTFIELDS, $envelope);
393 - curl_setopt($ch, CURLOPT_HTTPHEADER, array_map(function($key, $value) {
394 - return $key . ': ' . $value;
395 - }, array_keys($headers), $headers));
396 - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
397 - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
398 - curl_setopt($ch, CURLOPT_TIMEOUT, 5); // 5 second timeout as requested
399 - curl_setopt($ch, CURLOPT_USERAGENT, 'WordPress MetaSync Plugin/' . $plugin_version);
400 - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 second connection timeout
401 -
402 - $response = curl_exec($ch);
403 - $response_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
404 - $error = curl_error($ch);
524 + $response_data = wp_remote_post($url, [
525 + 'body' => $envelope,
526 + 'headers' => $headers,
527 + 'timeout' => 5, // 5 second timeout as requested
528 + 'sslverify' => true,
529 + 'user-agent' => 'WordPress MetaSync Plugin/' . $plugin_version,
530 + ]);
531 + $error = is_wp_error($response_data) ? $response_data->get_error_message() : '';
532 + $response_code = is_wp_error($response_data) ? 0 : (int) wp_remote_retrieve_response_code($response_data);
533 + $response = is_wp_error($response_data) ? '' : wp_remote_retrieve_body($response_data);
405 534
406 - #curl_close($ch);
407 -
408 535 // Log errors in debug mode for troubleshooting
409 536 if (defined('WP_DEBUG') && WP_DEBUG && WP_DEBUG_LOG) {
410 537 if ($error) {
411 - error_log(sprintf(
538 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
412 539 'MetaSync Sentry Error (%s): %s | Response Code: %s | Item Type: %s',
413 540 $item_type,
414 541 $error,
415 542 $response_code,
@@ -415,13 +542,12 @@
415 542 $response_code,
416 543 $item_type
417 544 ));
418 545 } elseif ($response_code < 200 || $response_code >= 300) {
419 - error_log(sprintf(
420 - 'MetaSync Sentry HTTP Error (%s): Response Code: %s | Response: %s | Item Type: %s',
546 + error_log(sprintf( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
547 + 'MetaSync Sentry HTTP Error (%s): Response Code: %s | Item Type: %s',
421 548 $item_type,
422 549 $response_code,
423 - substr($response, 0, 200),
424 550 $item_type
425 551 ));
426 552 }
427 553 }
@@ -625,9 +751,9 @@
625 751 if ($cached_context !== false) {
626 752 // Add dynamic data that changes per request
627 753 $cached_context['memory_usage'] = memory_get_usage(true);
628 754 $cached_context['memory_peak'] = memory_get_peak_usage(true);
629 - $cached_context['request_uri'] = $_SERVER['REQUEST_URI'] ?? 'unknown';
755 + $cached_context['request_uri'] = metasync_telemetry_request_path();
630 756 return $cached_context;
631 757 }
632 758
633 759 // Collect static system context (expensive operations)
@@ -644,14 +770,14 @@
644 770 'max_execution_time' => ini_get('max_execution_time'),
645 771 'active_plugins' => count(get_option('active_plugins', [])),
646 772 'active_theme' => get_template(),
647 773 'multisite' => is_multisite(),
648 - 'mysql_version' => method_exists($wpdb, 'get_var') ? $wpdb->get_var('SELECT VERSION()') : 'unknown',
774 + 'mysql_version' => method_exists($wpdb, 'get_var') ? $wpdb->get_var('SELECT VERSION()') : 'unknown', // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- diagnostic metadata — SELECT VERSION() has no cached WordPress API
649 775 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown',
650 776 // Dynamic data added per request
651 777 'memory_usage' => memory_get_usage(true),
652 778 'memory_peak' => memory_get_peak_usage(true),
653 - 'request_uri' => $_SERVER['REQUEST_URI'] ?? 'unknown'
779 + 'request_uri' => metasync_telemetry_request_path()
654 780 ];
655 781
656 782 // Cache static context for 1 hour
657 783 wp_cache_set($cache_key, $context, 'metasync', HOUR_IN_SECONDS);
@@ -727,10 +853,10 @@
727 853 if ($this->isLocalhost()) {
728 854 return 'development';
729 855 }
730 856
731 - $host = parse_url(home_url(), PHP_URL_HOST);
732 - if (strpos($host, 'staging') !== false || strpos($host, 'dev') !== false) {
857 + $host = wp_parse_url(home_url(), PHP_URL_HOST);
858 + if ($host !== false && $host !== null && (strpos($host, 'staging') !== false || strpos($host, 'dev') !== false)) {
733 859 return 'staging';
734 860 }
735 861
736 862 return 'production';
@@ -789,8 +915,12 @@
789 915 * Initialize Sentry with DSN configuration
790 916 */
791 917 function init_metasync_sentry_wordpress() {
792 918 global $metasync_sentry_wordpress;
919 +
920 + if (metasync_telemetry_is_disabled()) {
921 + return null;
922 + }
793 923
794 924 $dsn = '';
795 925
796 926 // Use constants defined in metasync.php for configuration
@@ -820,8 +950,11 @@
820 950 * Helper function to capture exceptions
821 951 */
822 952 function metasync_sentry_capture_exception($exception, $extra = []) {
823 953 global $metasync_sentry_wordpress;
954 + if (metasync_telemetry_is_disabled()) {
955 + return false;
956 + }
824 957 if (!$metasync_sentry_wordpress) {
825 958 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
826 959 }
827 960
@@ -835,12 +968,15 @@
835 968 * Helper function to capture messages
836 969 */
837 970 function metasync_sentry_capture_message($message, $level = 'info', $extra = [], $attachment = null) {
838 971 global $metasync_sentry_wordpress;
972 + if (metasync_telemetry_is_disabled()) {
973 + return false;
974 + }
839 975 if (!$metasync_sentry_wordpress) {
840 976 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
841 977 }
842 -
978 +
843 979 if ($metasync_sentry_wordpress) {
844 980 return $metasync_sentry_wordpress->captureMessage($message, $level, $extra, $attachment);
845 981 }
846 982 return false;
@@ -846,8 +982,31 @@
846 982 return false;
847 983 }
848 984
849 985 /**
986 + * Helper function to capture messages without blocking the current request.
987 + *
988 + * Use this instead of metasync_sentry_capture_message() from anything that runs
989 + * on a visitor's page render — the blocking variant can hold the request for up
990 + * to ten seconds if the collector is slow. See
991 + * MetaSync_Sentry_WordPress::captureMessageNonBlocking() for the trade-off.
992 + */
993 +function metasync_sentry_capture_message_nonblocking($message, $level = 'info', $extra = []) {
994 + global $metasync_sentry_wordpress;
995 + if (metasync_telemetry_is_disabled()) {
996 + return false;
997 + }
998 + if (!$metasync_sentry_wordpress) {
999 + $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
1000 + }
1001 +
1002 + if ($metasync_sentry_wordpress) {
1003 + return $metasync_sentry_wordpress->captureMessageNonBlocking($message, $level, $extra);
1004 + }
1005 + return false;
1006 +}
1007 +
1008 +/**
850 1009 * Helper function to capture user feedback
851 1010 *
852 1011 * @param array $feedback Feedback data with keys: name (optional), email (optional), message (required), event_id (optional)
853 1012 * @return bool Success status
@@ -853,8 +1012,11 @@
853 1012 * @return bool Success status
854 1013 */
855 1014 function metasync_sentry_capture_feedback($feedback, $attachment = null) {
856 1015 global $metasync_sentry_wordpress;
1016 + if (metasync_telemetry_is_disabled()) {
1017 + return false;
1018 + }
857 1019 if (!$metasync_sentry_wordpress) {
858 1020 $metasync_sentry_wordpress = init_metasync_sentry_wordpress();
859 1021 }
860 1022