PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.5.23
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.5.23
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 2.5.23, at telemetry/wordpress-error-handler.php

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