PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / trunk
Search Atlas SEO – OTTO AI SEO Automation for WordPress vtrunk
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 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / telemetry / wordpress-error-handler.php

wordpress-error-handler.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress trunk, at telemetry/wordpress-error-handler.php

513 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WordPress Error Handler for Sentry Integration
4 *
5 * Automatically captures WordPress errors and sends them to Sentry
6 * Includes WP URL and Plugin Version as required
7 */
8
9 // If this file is called directly, abort.
10 if (!defined('WPINC')) {
11 die;
12 }
13
14 require_once __DIR__ . '/privacy.php';
15
16 /**
17 * WordPress Error Handler Class
18 */
19 class MetaSync_WordPress_Error_Handler {
20
21 /**
22 * Plugin directory for filtering errors
23 */
24 private $plugin_dir;
25
26 /**
27 * Plugin slug for filtering
28 */
29 private $plugin_slug = 'metasync';
30
31 /**
32 * Previous error handler to restore
33 */
34 private $previous_error_handler;
35
36 /**
37 * Previous exception handler to restore
38 */
39 private $previous_exception_handler;
40
41 /**
42 * Track sent errors to prevent duplicates (in-memory cache)
43 */
44 private $sent_errors = array();
45
46 /**
47 * Maximum number of errors to track in memory
48 */
49 private $max_tracked_errors = 100;
50
51 /**
52 * Transient key for persistent error tracking
53 */
54 private $transient_key = 'metasync_sent_errors';
55
56 /**
57 * How long to remember sent errors (in seconds)
58 * 15 minutes — same error won't be re-sent within this window
59 */
60 private $error_memory_duration = 900;
61
62 /**
63 * Constructor
64 */
65 public function __construct() {
66 $this->plugin_dir = dirname(__DIR__); // Parent directory of telemetry folder
67 $this->load_sent_errors_from_cache();
68 $this->setup_error_handlers();
69 }
70
71 /**
72 * Load previously sent errors from persistent cache
73 */
74 private function load_sent_errors_from_cache() {
75 $cached_errors = get_transient($this->transient_key);
76 if (is_array($cached_errors)) {
77 $this->sent_errors = $cached_errors;
78 }
79 }
80
81 /**
82 * Save sent errors to persistent cache
83 */
84 private function save_sent_errors_to_cache() {
85 set_transient($this->transient_key, $this->sent_errors, $this->error_memory_duration);
86 }
87
88 /**
89 * Setup WordPress error handlers - Only for plugin-specific errors
90 */
91 private function setup_error_handlers() {
92 // Store previous handlers to chain them properly
93 $this->previous_error_handler = set_error_handler(array($this, 'capture_php_error'), E_ALL);
94 $this->previous_exception_handler = set_exception_handler(array($this, 'capture_exception'));
95
96 // Hook into WordPress fatal error handler (only for plugin errors)
97 add_filter('wp_fatal_error_handler', array($this, 'capture_fatal_error'));
98
99 // Hook into plugin activation/deactivation errors (only for our plugin)
100 add_action('activated_plugin', array($this, 'capture_plugin_activation'), 10, 2);
101 add_action('deactivated_plugin', array($this, 'capture_plugin_deactivation'), 10, 2);
102
103 // Hook into WordPress shutdown to catch fatal errors (only plugin-related)
104 register_shutdown_function(array($this, 'capture_shutdown_error'));
105
106 // Remove wp_die handler as it captures too many system errors
107 // add_action('wp_die_handler', array($this, 'capture_wp_die'), 10, 1);
108 }
109
110 /**
111 * Capture WordPress die events - REMOVED
112 * This method was too broad and captured system-wide wp_die events
113 * We now only capture plugin-specific errors through other handlers
114 */
115 // public function capture_wp_die($message) - REMOVED TO PREVENT SYSTEM-WIDE ERROR CAPTURE
116
117 /**
118 * Capture PHP errors - Only fatal-level plugin-specific errors
119 */
120 public function capture_php_error($severity, $message, $file, $line) {
121 // First, call the previous error handler if it exists
122 $handled = false;
123 if ($this->previous_error_handler && is_callable($this->previous_error_handler)) {
124 $handled = call_user_func($this->previous_error_handler, $severity, $message, $file, $line);
125 }
126
127 // Only capture fatal-level errors — skip warnings, notices, deprecated, strict
128 $fatal_severities = [E_USER_ERROR, E_RECOVERABLE_ERROR];
129 if (!in_array($severity, $fatal_severities, true)) {
130 return $handled;
131 }
132
133 // Only capture errors from our plugin or directly related to our plugin
134 if ($this->should_capture_error($file)) {
135 $this->send_to_sentry('php_error', $message, array(
136 'error_type' => 'php_error',
137 'severity' => $severity,
138 'file' => $file,
139 'line' => $line,
140 'error_level' => $this->get_error_level($severity),
141 'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10)
142 ));
143 }
144
145 // Return the result from the previous handler, or false to continue normal error handling
146 return $handled;
147 }
148
149 /**
150 * Capture uncaught exceptions - Only plugin-specific exceptions
151 */
152 public function capture_exception($exception) {
153 // First, call the previous exception handler if it exists
154 if ($this->previous_exception_handler && is_callable($this->previous_exception_handler)) {
155 call_user_func($this->previous_exception_handler, $exception);
156 }
157
158 // Only capture exceptions from our plugin or directly related to our plugin
159 if ($this->should_capture_error($exception->getFile())) {
160 $this->send_to_sentry('exception', $exception->getMessage(), array(
161 'error_type' => 'uncaught_exception',
162 'exception_class' => get_class($exception),
163 'file' => $exception->getFile(),
164 'line' => $exception->getLine(),
165 'trace' => $exception->getTraceAsString(),
166 'exception_object' => $exception
167 ));
168 }
169 }
170
171 /**
172 * Capture fatal errors
173 */
174 public function capture_fatal_error($error) {
175 if (is_array($error) && $this->should_capture_error($error['file'] ?? '')) {
176 $this->send_to_sentry('fatal_error', $error['message'] ?? 'Fatal error', array(
177 'error_type' => 'fatal_error',
178 'error_details' => $error,
179 'file' => $error['file'] ?? 'unknown',
180 'line' => $error['line'] ?? 0
181 ));
182 }
183 return $error;
184 }
185
186 /**
187 * Capture shutdown errors
188 */
189 public function capture_shutdown_error() {
190 $error = error_get_last();
191 if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
192 if ($this->should_capture_error($error['file'])) {
193 $this->send_to_sentry('shutdown_error', $error['message'], array(
194 'error_type' => 'shutdown_error',
195 'error_details' => $error,
196 'file' => $error['file'],
197 'line' => $error['line']
198 ));
199 }
200 }
201 }
202
203 /**
204 * Capture plugin activation events
205 */
206 public function capture_plugin_activation($plugin, $network_wide) {
207 // Only track our plugin
208 if (strpos($plugin, 'metasync') !== false) {
209 $this->send_to_sentry('plugin_activation', 'Plugin activated successfully', array(
210 'event_type' => 'plugin_lifecycle',
211 'action' => 'activation',
212 'plugin' => $plugin,
213 'network_wide' => $network_wide,
214 'wp_version' => get_bloginfo('version'),
215 'php_version' => PHP_VERSION
216 ));
217 }
218 }
219
220 /**
221 * Capture plugin deactivation events
222 */
223 public function capture_plugin_deactivation($plugin) {
224 // Only track our plugin
225 if (strpos($plugin, 'metasync') !== false) {
226 $this->send_to_sentry('plugin_deactivation', 'Plugin deactivated', array(
227 'event_type' => 'plugin_lifecycle',
228 'action' => 'deactivation',
229 'plugin' => $plugin,
230 'reason' => 'user_action'
231 ));
232 }
233 }
234
235 /**
236 * Determine if we should capture this error - Much more restrictive filtering
237 */
238 private function should_capture_error($file = '') {
239 # Don't capture if no file specified - this prevents capturing system-wide errors
240 if (empty($file)) {
241 return false;
242 }
243
244 # Primary check: error must be directly from our plugin directory
245 if (strpos($file, $this->plugin_dir) !== false) {
246 return true;
247 }
248
249 # Secondary check: error must be in a file that contains our plugin slug
250 if (strpos($file, $this->plugin_slug) !== false) {
251 return true;
252 }
253
254 # REMOVED: Tertiary backtrace check that was too broad
255 # The previous backtrace logic was capturing errors from other plugins
256 # that happened to be called during MetaSync execution. We now only
257 # capture errors that directly originate from MetaSync files.
258
259 # Additional check: Only capture if the error file path contains 'metasync'
260 # This is a more conservative approach to avoid false positives
261 $file_lower = strtolower($file);
262 if (strpos($file_lower, 'metasync') !== false) {
263 return true;
264 }
265
266 return false;
267 }
268
269 /**
270 * Check if the current environment is localhost/development
271 */
272 private function is_localhost() {
273 $host = parse_url(home_url(), PHP_URL_HOST);
274
275 // Check for common localhost patterns
276 $localhost_patterns = [
277 'localhost',
278 '127.0.0.1',
279 '::1',
280 '0.0.0.0',
281 '.local',
282 '.test',
283 '.dev',
284 '.localhost'
285 ];
286
287 foreach ($localhost_patterns as $pattern) {
288 if (strpos($host, $pattern) !== false) {
289 return true;
290 }
291 }
292
293 // Check if host is an IP address in private ranges
294 if (filter_var($host, FILTER_VALIDATE_IP)) {
295 $ip = ip2long($host);
296 if ($ip !== false) {
297 // Private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
298 if (($ip >= ip2long('10.0.0.0') && $ip <= ip2long('10.255.255.255')) ||
299 ($ip >= ip2long('172.16.0.0') && $ip <= ip2long('172.31.255.255')) ||
300 ($ip >= ip2long('192.168.0.0') && $ip <= ip2long('192.168.255.255'))) {
301 return true;
302 }
303 }
304 }
305
306 return false;
307 }
308
309 /**
310 * Send error to Sentry with required metadata
311 */
312 private function send_to_sentry($error_type, $message, $context = array()) {
313 // Skip sending to Sentry if running on localhost/development environment
314 if ($this->is_localhost()) {
315 return;
316 }
317
318 // Generate error fingerprint for deduplication
319 $error_fingerprint = $this->generate_error_fingerprint($error_type, $message, $context);
320
321 // Check if this error has already been sent
322 if ($this->is_error_already_sent($error_fingerprint)) {
323 return; // Skip sending duplicate error
324 }
325
326 // Add required metadata
327 $context = array_merge($context, array(
328 'wp_url' => home_url(),
329 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0',
330 'plugin_name' => 'Search Engine Labs SEO (MetaSync)',
331 'error_timestamp' => date('Y-m-d H:i:s'),
332 'site_info' => array(
333 'site_title' => get_bloginfo('name'),
334 'wp_version' => get_bloginfo('version'),
335 'admin_email' => get_bloginfo('admin_email'),
336 'active_theme' => get_template(),
337 'php_version' => PHP_VERSION,
338 'memory_limit' => ini_get('memory_limit'),
339 'multisite' => is_multisite()
340 ),
341 'request_info' => array(
342 'url' => metasync_telemetry_request_path(),
343 'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
344 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
345 'referer' => isset($_SERVER['HTTP_REFERER'])
346 ? metasync_telemetry_request_path($_SERVER['HTTP_REFERER'])
347 : 'none'
348 )
349 ));
350
351 // Determine severity level
352 $level = $this->get_sentry_level($error_type);
353
354 // Only send fatal/error-level events — drop info, warning, debug
355 if (!in_array($level, ['error', 'fatal'], true)) {
356 return;
357 }
358
359 // Send directly to Sentry (wordpress-error-handler is the primary/only error capture point)
360 $sentry_success = false;
361
362 if (function_exists('metasync_sentry_capture_exception') && isset($context['exception_object'])) {
363 $sentry_success = metasync_sentry_capture_exception($context['exception_object'], $context);
364 } elseif (function_exists('metasync_sentry_capture_message')) {
365 $sentry_success = metasync_sentry_capture_message($message, $level, $context);
366 }
367
368 // Only mark as sent if Sentry call was successful
369 if ($sentry_success) {
370 $this->mark_error_as_sent($error_fingerprint);
371 }
372 }
373
374 /**
375 * Get error level name
376 */
377 private function get_error_level($severity) {
378 $levels = array(
379 E_ERROR => 'E_ERROR',
380 E_WARNING => 'E_WARNING',
381 E_PARSE => 'E_PARSE',
382 E_NOTICE => 'E_NOTICE',
383 E_CORE_ERROR => 'E_CORE_ERROR',
384 E_CORE_WARNING => 'E_CORE_WARNING',
385 E_COMPILE_ERROR => 'E_COMPILE_ERROR',
386 E_COMPILE_WARNING => 'E_COMPILE_WARNING',
387 E_USER_ERROR => 'E_USER_ERROR',
388 E_USER_WARNING => 'E_USER_WARNING',
389 E_USER_NOTICE => 'E_USER_NOTICE',
390 E_STRICT => 'E_STRICT',
391 E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
392 E_DEPRECATED => 'E_DEPRECATED',
393 E_USER_DEPRECATED => 'E_USER_DEPRECATED'
394 );
395
396 return $levels[$severity] ?? 'UNKNOWN';
397 }
398
399 /**
400 * Get Sentry level from error type
401 */
402 private function get_sentry_level($error_type) {
403 $levels = array(
404 'php_error' => 'error',
405 'exception' => 'error',
406 'uncaught_exception' => 'error',
407 'fatal_error' => 'fatal',
408 'shutdown_error' => 'fatal',
409 'wp_die' => 'error',
410 'plugin_activation' => 'info',
411 'plugin_deactivation' => 'info'
412 );
413
414 return $levels[$error_type] ?? 'error';
415 }
416
417 /**
418 * Generate a unique fingerprint for an error to detect duplicates
419 *
420 * @param string $error_type Type of error
421 * @param string $message Error message
422 * @param array $context Error context
423 * @return string Unique fingerprint
424 */
425 private function generate_error_fingerprint($error_type, $message, $context = array()) {
426 // Create a fingerprint based on key error characteristics
427 $fingerprint_data = array(
428 'error_type' => $error_type,
429 'message' => $message,
430 'file' => $context['file'] ?? '',
431 'line' => $context['line'] ?? 0,
432 'exception_class' => $context['exception_class'] ?? '',
433 'severity' => $context['severity'] ?? 0
434 );
435
436 // Create a hash of the fingerprint data
437 return md5(serialize($fingerprint_data));
438 }
439
440 /**
441 * Check if an error has already been sent
442 *
443 * @param string $error_fingerprint Error fingerprint
444 * @return bool True if already sent
445 */
446 private function is_error_already_sent($error_fingerprint) {
447 // Check if error exists and hasn't expired
448 if (isset($this->sent_errors[$error_fingerprint])) {
449 $sent_time = $this->sent_errors[$error_fingerprint];
450 $time_elapsed = time() - $sent_time;
451
452 // If error was sent recently (within memory duration), skip it
453 if ($time_elapsed < $this->error_memory_duration) {
454 return true;
455 } else {
456 // Error has expired, remove it from cache
457 unset($this->sent_errors[$error_fingerprint]);
458 $this->save_sent_errors_to_cache();
459 }
460 }
461
462 return false;
463 }
464
465 /**
466 * Mark an error as sent to prevent duplicates
467 *
468 * @param string $error_fingerprint Error fingerprint
469 */
470 private function mark_error_as_sent($error_fingerprint) {
471 // Add to sent errors array
472 $this->sent_errors[$error_fingerprint] = time();
473
474 // Clean up old entries to prevent memory bloat
475 $this->cleanup_old_errors();
476
477 // Persist to cache
478 $this->save_sent_errors_to_cache();
479 }
480
481 /**
482 * Clean up old error entries to prevent memory bloat
483 */
484 private function cleanup_old_errors() {
485 $current_time = time();
486
487 // Remove expired errors
488 foreach ($this->sent_errors as $fingerprint => $timestamp) {
489 if ($current_time - $timestamp > $this->error_memory_duration) {
490 unset($this->sent_errors[$fingerprint]);
491 }
492 }
493
494 // If we still have too many errors tracked, remove the oldest ones
495 if (count($this->sent_errors) > $this->max_tracked_errors) {
496 // Sort by timestamp (oldest first)
497 asort($this->sent_errors);
498
499 // Remove oldest entries, keeping only the most recent ones
500 $errors_to_remove = count($this->sent_errors) - $this->max_tracked_errors;
501 $this->sent_errors = array_slice($this->sent_errors, $errors_to_remove, null, true);
502 }
503 }
504 }
505
506 // Initialize the error handler immediately when this file is loaded
507 // This ensures we catch errors that happen during plugin initialization
508 // Must be initialized early to catch parse errors and fatal errors
509 if (!metasync_telemetry_is_disabled() && class_exists('MetaSync_WordPress_Error_Handler')) {
510 new MetaSync_WordPress_Error_Handler();
511 }
512 ?>
513