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

509 lines 17.9 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 * 15 minutes — same error won't be re-sent within this window
57 */
58 private $error_memory_duration = 900;
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 fatal-level 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 fatal-level errors — skip warnings, notices, deprecated, strict
126 $fatal_severities = [E_USER_ERROR, E_RECOVERABLE_ERROR];
127 if (!in_array($severity, $fatal_severities, true)) {
128 return $handled;
129 }
130
131 // Only capture errors from our plugin or directly related to our plugin
132 if ($this->should_capture_error($file)) {
133 $this->send_to_sentry('php_error', $message, array(
134 'error_type' => 'php_error',
135 'severity' => $severity,
136 'file' => $file,
137 'line' => $line,
138 'error_level' => $this->get_error_level($severity),
139 'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10)
140 ));
141 }
142
143 // Return the result from the previous handler, or false to continue normal error handling
144 return $handled;
145 }
146
147 /**
148 * Capture uncaught exceptions - Only plugin-specific exceptions
149 */
150 public function capture_exception($exception) {
151 // First, call the previous exception handler if it exists
152 if ($this->previous_exception_handler && is_callable($this->previous_exception_handler)) {
153 call_user_func($this->previous_exception_handler, $exception);
154 }
155
156 // Only capture exceptions from our plugin or directly related to our plugin
157 if ($this->should_capture_error($exception->getFile())) {
158 $this->send_to_sentry('exception', $exception->getMessage(), array(
159 'error_type' => 'uncaught_exception',
160 'exception_class' => get_class($exception),
161 'file' => $exception->getFile(),
162 'line' => $exception->getLine(),
163 'trace' => $exception->getTraceAsString(),
164 'exception_object' => $exception
165 ));
166 }
167 }
168
169 /**
170 * Capture fatal errors
171 */
172 public function capture_fatal_error($error) {
173 if (is_array($error) && $this->should_capture_error($error['file'] ?? '')) {
174 $this->send_to_sentry('fatal_error', $error['message'] ?? 'Fatal error', array(
175 'error_type' => 'fatal_error',
176 'error_details' => $error,
177 'file' => $error['file'] ?? 'unknown',
178 'line' => $error['line'] ?? 0
179 ));
180 }
181 return $error;
182 }
183
184 /**
185 * Capture shutdown errors
186 */
187 public function capture_shutdown_error() {
188 $error = error_get_last();
189 if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
190 if ($this->should_capture_error($error['file'])) {
191 $this->send_to_sentry('shutdown_error', $error['message'], array(
192 'error_type' => 'shutdown_error',
193 'error_details' => $error,
194 'file' => $error['file'],
195 'line' => $error['line']
196 ));
197 }
198 }
199 }
200
201 /**
202 * Capture plugin activation events
203 */
204 public function capture_plugin_activation($plugin, $network_wide) {
205 // Only track our plugin
206 if (strpos($plugin, 'metasync') !== false) {
207 $this->send_to_sentry('plugin_activation', 'Plugin activated successfully', array(
208 'event_type' => 'plugin_lifecycle',
209 'action' => 'activation',
210 'plugin' => $plugin,
211 'network_wide' => $network_wide,
212 'wp_version' => get_bloginfo('version'),
213 'php_version' => PHP_VERSION
214 ));
215 }
216 }
217
218 /**
219 * Capture plugin deactivation events
220 */
221 public function capture_plugin_deactivation($plugin) {
222 // Only track our plugin
223 if (strpos($plugin, 'metasync') !== false) {
224 $this->send_to_sentry('plugin_deactivation', 'Plugin deactivated', array(
225 'event_type' => 'plugin_lifecycle',
226 'action' => 'deactivation',
227 'plugin' => $plugin,
228 'reason' => 'user_action'
229 ));
230 }
231 }
232
233 /**
234 * Determine if we should capture this error - Much more restrictive filtering
235 */
236 private function should_capture_error($file = '') {
237 # Don't capture if no file specified - this prevents capturing system-wide errors
238 if (empty($file)) {
239 return false;
240 }
241
242 # Primary check: error must be directly from our plugin directory
243 if (strpos($file, $this->plugin_dir) !== false) {
244 return true;
245 }
246
247 # Secondary check: error must be in a file that contains our plugin slug
248 if (strpos($file, $this->plugin_slug) !== false) {
249 return true;
250 }
251
252 # REMOVED: Tertiary backtrace check that was too broad
253 # The previous backtrace logic was capturing errors from other plugins
254 # that happened to be called during MetaSync execution. We now only
255 # capture errors that directly originate from MetaSync files.
256
257 # Additional check: Only capture if the error file path contains 'metasync'
258 # This is a more conservative approach to avoid false positives
259 $file_lower = strtolower($file);
260 if (strpos($file_lower, 'metasync') !== false) {
261 return true;
262 }
263
264 return false;
265 }
266
267 /**
268 * Check if the current environment is localhost/development
269 */
270 private function is_localhost() {
271 $host = parse_url(home_url(), PHP_URL_HOST);
272
273 // Check for common localhost patterns
274 $localhost_patterns = [
275 'localhost',
276 '127.0.0.1',
277 '::1',
278 '0.0.0.0',
279 '.local',
280 '.test',
281 '.dev',
282 '.localhost'
283 ];
284
285 foreach ($localhost_patterns as $pattern) {
286 if (strpos($host, $pattern) !== false) {
287 return true;
288 }
289 }
290
291 // Check if host is an IP address in private ranges
292 if (filter_var($host, FILTER_VALIDATE_IP)) {
293 $ip = ip2long($host);
294 if ($ip !== false) {
295 // Private IP ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
296 if (($ip >= ip2long('10.0.0.0') && $ip <= ip2long('10.255.255.255')) ||
297 ($ip >= ip2long('172.16.0.0') && $ip <= ip2long('172.31.255.255')) ||
298 ($ip >= ip2long('192.168.0.0') && $ip <= ip2long('192.168.255.255'))) {
299 return true;
300 }
301 }
302 }
303
304 return false;
305 }
306
307 /**
308 * Send error to Sentry with required metadata
309 */
310 private function send_to_sentry($error_type, $message, $context = array()) {
311 // Skip sending to Sentry if running on localhost/development environment
312 if ($this->is_localhost()) {
313 return;
314 }
315
316 // Generate error fingerprint for deduplication
317 $error_fingerprint = $this->generate_error_fingerprint($error_type, $message, $context);
318
319 // Check if this error has already been sent
320 if ($this->is_error_already_sent($error_fingerprint)) {
321 return; // Skip sending duplicate error
322 }
323
324 // Add required metadata
325 $context = array_merge($context, array(
326 'wp_url' => home_url(),
327 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0',
328 'plugin_name' => 'Search Engine Labs SEO (MetaSync)',
329 'error_timestamp' => date('Y-m-d H:i:s'),
330 'site_info' => array(
331 'site_title' => get_bloginfo('name'),
332 'wp_version' => get_bloginfo('version'),
333 'admin_email' => get_bloginfo('admin_email'),
334 'active_theme' => get_template(),
335 'php_version' => PHP_VERSION,
336 'memory_limit' => ini_get('memory_limit'),
337 'multisite' => is_multisite()
338 ),
339 'request_info' => array(
340 'url' => $_SERVER['REQUEST_URI'] ?? 'unknown',
341 'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
342 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
343 'referer' => $_SERVER['HTTP_REFERER'] ?? 'none'
344 )
345 ));
346
347 // Determine severity level
348 $level = $this->get_sentry_level($error_type);
349
350 // Only send fatal/error-level events — drop info, warning, debug
351 if (!in_array($level, ['error', 'fatal'], true)) {
352 return;
353 }
354
355 // Send directly to Sentry (wordpress-error-handler is the primary/only error capture point)
356 $sentry_success = false;
357
358 if (function_exists('metasync_sentry_capture_exception') && isset($context['exception_object'])) {
359 $sentry_success = metasync_sentry_capture_exception($context['exception_object'], $context);
360 } elseif (function_exists('metasync_sentry_capture_message')) {
361 $sentry_success = metasync_sentry_capture_message($message, $level, $context);
362 }
363
364 // Only mark as sent if Sentry call was successful
365 if ($sentry_success) {
366 $this->mark_error_as_sent($error_fingerprint);
367 }
368 }
369
370 /**
371 * Get error level name
372 */
373 private function get_error_level($severity) {
374 $levels = array(
375 E_ERROR => 'E_ERROR',
376 E_WARNING => 'E_WARNING',
377 E_PARSE => 'E_PARSE',
378 E_NOTICE => 'E_NOTICE',
379 E_CORE_ERROR => 'E_CORE_ERROR',
380 E_CORE_WARNING => 'E_CORE_WARNING',
381 E_COMPILE_ERROR => 'E_COMPILE_ERROR',
382 E_COMPILE_WARNING => 'E_COMPILE_WARNING',
383 E_USER_ERROR => 'E_USER_ERROR',
384 E_USER_WARNING => 'E_USER_WARNING',
385 E_USER_NOTICE => 'E_USER_NOTICE',
386 E_STRICT => 'E_STRICT',
387 E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
388 E_DEPRECATED => 'E_DEPRECATED',
389 E_USER_DEPRECATED => 'E_USER_DEPRECATED'
390 );
391
392 return $levels[$severity] ?? 'UNKNOWN';
393 }
394
395 /**
396 * Get Sentry level from error type
397 */
398 private function get_sentry_level($error_type) {
399 $levels = array(
400 'php_error' => 'error',
401 'exception' => 'error',
402 'uncaught_exception' => 'error',
403 'fatal_error' => 'fatal',
404 'shutdown_error' => 'fatal',
405 'wp_die' => 'error',
406 'plugin_activation' => 'info',
407 'plugin_deactivation' => 'info'
408 );
409
410 return $levels[$error_type] ?? 'error';
411 }
412
413 /**
414 * Generate a unique fingerprint for an error to detect duplicates
415 *
416 * @param string $error_type Type of error
417 * @param string $message Error message
418 * @param array $context Error context
419 * @return string Unique fingerprint
420 */
421 private function generate_error_fingerprint($error_type, $message, $context = array()) {
422 // Create a fingerprint based on key error characteristics
423 $fingerprint_data = array(
424 'error_type' => $error_type,
425 'message' => $message,
426 'file' => $context['file'] ?? '',
427 'line' => $context['line'] ?? 0,
428 'exception_class' => $context['exception_class'] ?? '',
429 'severity' => $context['severity'] ?? 0
430 );
431
432 // Create a hash of the fingerprint data
433 return md5(serialize($fingerprint_data));
434 }
435
436 /**
437 * Check if an error has already been sent
438 *
439 * @param string $error_fingerprint Error fingerprint
440 * @return bool True if already sent
441 */
442 private function is_error_already_sent($error_fingerprint) {
443 // Check if error exists and hasn't expired
444 if (isset($this->sent_errors[$error_fingerprint])) {
445 $sent_time = $this->sent_errors[$error_fingerprint];
446 $time_elapsed = time() - $sent_time;
447
448 // If error was sent recently (within memory duration), skip it
449 if ($time_elapsed < $this->error_memory_duration) {
450 return true;
451 } else {
452 // Error has expired, remove it from cache
453 unset($this->sent_errors[$error_fingerprint]);
454 $this->save_sent_errors_to_cache();
455 }
456 }
457
458 return false;
459 }
460
461 /**
462 * Mark an error as sent to prevent duplicates
463 *
464 * @param string $error_fingerprint Error fingerprint
465 */
466 private function mark_error_as_sent($error_fingerprint) {
467 // Add to sent errors array
468 $this->sent_errors[$error_fingerprint] = time();
469
470 // Clean up old entries to prevent memory bloat
471 $this->cleanup_old_errors();
472
473 // Persist to cache
474 $this->save_sent_errors_to_cache();
475 }
476
477 /**
478 * Clean up old error entries to prevent memory bloat
479 */
480 private function cleanup_old_errors() {
481 $current_time = time();
482
483 // Remove expired errors
484 foreach ($this->sent_errors as $fingerprint => $timestamp) {
485 if ($current_time - $timestamp > $this->error_memory_duration) {
486 unset($this->sent_errors[$fingerprint]);
487 }
488 }
489
490 // If we still have too many errors tracked, remove the oldest ones
491 if (count($this->sent_errors) > $this->max_tracked_errors) {
492 // Sort by timestamp (oldest first)
493 asort($this->sent_errors);
494
495 // Remove oldest entries, keeping only the most recent ones
496 $errors_to_remove = count($this->sent_errors) - $this->max_tracked_errors;
497 $this->sent_errors = array_slice($this->sent_errors, $errors_to_remove, null, true);
498 }
499 }
500 }
501
502 // Initialize the error handler immediately when this file is loaded
503 // This ensures we catch errors that happen during plugin initialization
504 // Must be initialized early to catch parse errors and fatal errors
505 if (class_exists('MetaSync_WordPress_Error_Handler')) {
506 new MetaSync_WordPress_Error_Handler();
507 }
508 ?>
509