0, 'info' => 1, 'warning' => 2, 'error' => 3, 'critical' => 4 ]; protected CustomTable $table; public function __construct() { $this->defineTables(); // global $wpdb; // $this->wpdb = $wpdb; // $this->tableName = $wpdb->prefix . BASE . 'error_log'; add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); // Hook our send function to the cron event add_action('jvb_daily_error_summary', [$this, 'sendErrorSummary']); jvb_register_do_once('error_admin_action_registered', [$this, 'registerAdminAction']); // add_filter(BASE.'admin_action_filter', [$this, 'adminActionFilter'], 10, 3); } public function defineTables():void { $table = CustomTable::for('error_log'); $table->setColumns([ 'id' => 'bigint(20) unsigned NOT NULL AUTO_INCREMENT', 'error_type' => 'varchar(50) NOT NULL', 'component' => 'varchar(100) NOT NULL', 'method' => 'varchar(100) DEFAULT NULL', 'page_url' => 'varchar(255) DEFAULT NULL', 'message' => 'text NOT NULL', 'context' => 'JSON', 'severity' => 'ENUM(\'high\',\'normal\',\'low\') DEFAULT \'normal\'', 'user_id' => $table->getUserIDType().' DEFAULT NULL', 'user_was_logged_in' => 'tinyint(1) NOT NULL', 'source' => 'ENUM(\'frontend\', \'backend\') NOT NULL', 'created_at' => 'timestamp DEFAULT CURRENT_TIMESTAMP', ]); $table->setKeys([ ['key' => 'PRIMARY', 'value' => 'id'], '`created_at` (`created_at`)', '`component_severity_date` (`component`, `severity`, `created_at`)', '`error_type_date` (`error_type`, `created_at`)', '`severity_date` (`severity`, `created_at`)' ]); $table->defineTable(); $this->table = $table; } public function registerAdminAction():void { $admin = JVB()->admin(); $admin->registerSubpage( 'Error Log', 'Errors', 'manage_options', 'jvb-error-log', [$this, 'renderAdminPage'], ); } /** * Process error log operations queued through OperationQueue * * @param WP_Error|array $result The result we are filtering * @param object $operation The operation information * @param array $data The current chunk of data to process * * @return WP_Error|array Either the unfiltered WP_Error, or the result of the operation */ public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array { if ($operation->type !== 'error_log') { return $result; } try { $table = $this->tableName; // Extract error data $component = sanitize_text_field($data['component'] ?? ''); $message = sanitize_textarea_field($data['message'] ?? ''); $context = $data['context'] ?? []; $severity = sanitize_text_field($data['severity'] ?? 'error'); // Validate severity level if (!array_key_exists($severity, $this->error_levels)) { $severity = 'error'; // Default to error if invalid severity provided } // Determine error type from context or use component as fallback $error_type = sanitize_text_field($context['error_type'] ?? $component); // Store context as JSON $context_json = json_encode($context); if (json_last_error() !== JSON_ERROR_NONE) { // If JSON encoding fails, store only basic info $context_json = json_encode([ 'error' => 'Failed to encode original context', 'json_error' => json_last_error_msg() ]); } // Insert into database $result = $this->wpdb->insert( $table, [ 'error_type' => $error_type, 'component' => $component, 'message' => $message, 'context' => $context_json, 'severity' => $severity, 'user_id' => get_current_user_id(), 'created_at' => current_time('mysql') ], [ '%s', // error_type '%s', // component '%s', // message '%s', // context (JSON) '%s', // severity '%d', // user_id '%s' // created_at ] ); if ($result === false) { // If insert fails, log to PHP error log as fallback error_log("[ErrorHandler] Database insert failed: " . $this->wpdb->last_error); return [ 'success' => false, 'message' => "[ErrorHandler] Database insert failed: " . $this->wpdb->last_error ]; } // Potentially track error metrics or trigger additional actions for critical errors if ($severity === 'critical') { $this->checkErrorThreshold($error_type, $component); } return [ 'success' => true, 'result' => $result ]; } catch (Exception $e) { // Fallback to PHP error log if our error handler itself fails error_log("[ErrorHandler Exception] " . $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Check if error threshold has been reached for a specific error type/component * and take additional actions if needed * * @param string $error_type The type of error * @param string $component The component where the error occurred */ protected function checkErrorThreshold(string $error_type, string $component) { // Get count of similar critical errors in the last hour $count = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->tableName} WHERE error_type = %s AND component = %s AND severity = 'critical' AND created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)", $error_type, $component )); // If threshold reached, take additional actions (e.g., notify developers) if ((int)$count >= $this->notification_threshold) { // You could send an urgent notification, Slack message, etc. $admin_email = get_option('admin_email'); $subject = "[URGENT] Error Threshold Exceeded for {$component}"; $message = "The error '{$error_type}' in component '{$component}' has occurred {$count} times in the last hour."; wp_mail($admin_email, $subject, $message); } } /** * @param string $component Where the error is being logged from * @param string $message The error message, usually extracted from $e->getMessage()) * @param array $context Additional context (i.e: data that was attempted to be acted on) * @param string $severity * * @return bool Whether it gets logged successfully */ public function log(string $component, string $message, array $context = [], string $severity = 'error'): array { try { $table = $this->wpdb->prefix . BASE . 'error_log'; // Validate severity if (!array_key_exists($severity, $this->error_levels)) { $severity = 'error'; } // Extract info $error_type = sanitize_text_field($context['error_type'] ?? $component); $method = isset($context['method']) ? sanitize_text_field($context['method']) : null; $page_url = isset($context['url']) ? esc_url_raw($context['url']) : null; $user_id = get_current_user_id(); $user_was_logged_in = $user_id > 0 || (!empty($context['isLoggedIn'])); // Determine source from context $source = isset($context['source']) ? $context['source'] : (isset($context['url']) ? 'frontend' : 'backend'); $result = $this->wpdb->insert( $table, [ 'error_type' => $error_type, 'component' => $component, 'method' => $method, 'page_url' => $page_url, 'message' => sanitize_textarea_field($message), 'context' => json_encode($context), 'severity' => $severity, 'user_id' => $user_id ?: null, 'user_was_logged_in' => $user_was_logged_in ? 1 : 0, 'source' => $source, 'created_at' => current_time('mysql') ], ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s'] ); if ($result === false) { error_log("[ErrorHandler] Database insert failed: " . $this->wpdb->last_error); return ['success' => false, 'message' => $this->wpdb->last_error]; } if ($severity === 'critical') { $this->checkErrorThreshold($error_type, $component); } return ['success' => true, 'id' => $this->wpdb->insert_id]; } catch (Exception $e) { error_log("[ErrorHandler Exception] " . $e->getMessage()); return ['success' => false, 'message' => $e->getMessage()]; } } /** * @param string $component What class or function logs the error * @param string $message Details on the error * @param array $context additional context of the data that was being processed * * @return bool Whether the notification is sent successfully */ protected function notifyAdmin(string $component, string $message, array $context):bool { $admin_email = get_option('admin_email'); $subject = "[" . get_bloginfo('name') . " Critical Error] {$component}"; $body = JVB()->email()->alert( 'A critical error has occurred and requires immediate attention', 'error' ); $body .= JVB()->email()->h2('Error Details'); $body .= JVB()->email()->card( '

Component: ' . esc_html($component) . '

' . '

Message:

' . JVB()->email()->codeBlock($message), 'Error Information' ); if (!empty($context)) { $body .= JVB()->email()->h3('Additional Context'); $body .= JVB()->email()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); } return JVB()->email()->sendEmail($admin_email, $subject, $body, 'CRITICAL ERROR'); } /** * Gather summary of the most important errors * @param ?string $start_date Defaults to today * @param ?string $end_date Defaults to today * @return array */ public function gatherErrorSummary(?string $start_date = null, ?string $end_date = null): array { $table = $this->wpdb->prefix . BASE . 'error_log'; if (!$start_date) { $start_date = gmdate('Y-m-d 00:00:00', strtotime('-1 day')); } if (!$end_date) { $end_date = gmdate('Y-m-d 23:59:59'); } // Most frequent error patterns (deduplicated by component/method/message) $frequent = $this->wpdb->get_results($this->wpdb->prepare( "SELECT component, method, error_type, message, severity, source, COUNT(*) as count, SUM(CASE WHEN user_was_logged_in = 1 THEN 1 ELSE 0 END) as logged_in_count, SUM(CASE WHEN user_was_logged_in = 0 THEN 1 ELSE 0 END) as logged_out_count, MIN(created_at) as first_seen, MAX(created_at) as last_seen FROM {$table} WHERE created_at BETWEEN %s AND %s GROUP BY component, method, error_type, message, severity, source ORDER BY count DESC, severity DESC LIMIT 10", $start_date, $end_date )); // Critical errors $critical = $this->wpdb->get_results($this->wpdb->prepare( "SELECT component, method, error_type, message, source, COUNT(*) as count, SUM(CASE WHEN user_was_logged_in = 1 THEN 1 ELSE 0 END) as logged_in_count, SUM(CASE WHEN user_was_logged_in = 0 THEN 1 ELSE 0 END) as logged_out_count, MIN(created_at) as first_seen, MAX(created_at) as last_seen FROM {$table} WHERE created_at BETWEEN %s AND %s AND severity = 'critical' GROUP BY component, method, error_type, message, source ORDER BY count DESC LIMIT 5", $start_date, $end_date )); // Overall stats $stats = $this->wpdb->get_row($this->wpdb->prepare( "SELECT COUNT(*) as total_errors, COUNT(DISTINCT CONCAT(component, '-', COALESCE(method, ''), '-', error_type)) as unique_error_types, SUM(CASE WHEN user_was_logged_in = 1 THEN 1 ELSE 0 END) as logged_in_errors, SUM(CASE WHEN user_was_logged_in = 0 THEN 1 ELSE 0 END) as logged_out_errors, SUM(CASE WHEN source = 'frontend' THEN 1 ELSE 0 END) as frontend_errors, SUM(CASE WHEN source = 'backend' THEN 1 ELSE 0 END) as backend_errors, SUM(CASE WHEN severity = 'critical' THEN 1 ELSE 0 END) as critical_count, SUM(CASE WHEN severity = 'error' THEN 1 ELSE 0 END) as error_count, SUM(CASE WHEN severity = 'warning' THEN 1 ELSE 0 END) as warning_count FROM {$table} WHERE created_at BETWEEN %s AND %s", $start_date, $end_date )); return [ 'frequent' => $frequent, 'critical' => $critical, 'stats' => $stats, 'date_range' => ['start' => $start_date, 'end' => $end_date] ]; } /** * Send daily error summary email to administrator * @return bool Whether email is sent */ public function sendErrorSummary():bool { $summary = $this->gatherErrorSummary(); if (empty($summary['frequent']) && empty($summary['critical'])) { return false; } $admin_email = get_option('admin_email'); $site_name = get_bloginfo('name'); $yesterday = date('Y-m-d', strtotime('-1 day')); $subject = "[{$site_name}] Daily Error Summary - " . date('Y-m-d'); // Header with alert $body = JVB()->email()->h1('Daily Error Summary'); $body .= sprintf('

Error summary for %s

', $yesterday); // Summary stats in a grid if (!empty($summary['stats'])) { $stats = [ JVB()->email()->stat($summary['stats']->total_errors, 'Total Errors'), JVB()->email()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'), JVB()->email()->stat($summary['stats']->error_count, 'Errors'), JVB()->email()->stat($summary['stats']->warning_count, 'Warnings') ]; $body .= JVB()->email()->grid($stats, 4); } // Alert if critical errors exist if (!empty($summary['critical'])) { $body .= JVB()->email()->alert( sprintf('Found %d critical errors that need immediate attention', count($summary['critical'])), 'error' ); } $body .= JVB()->email()->spacer(20); // Frequent errors section if (!empty($summary['frequent'])) { $body .= JVB()->email()->h2('Most Frequent Errors'); foreach ($summary['frequent'] as $error) { $cardContent = JVB()->email()->badge($error->count . 'x', 'warning') . ' '; $cardContent .= '' . esc_html($error->error_type) . ''; $cardContent .= '

' . esc_html(wp_trim_words($error->message, 15)) . '

'; $cardContent .= '

Source: ' . esc_html($error->source) . ' | Logged in: ' . $error->logged_in_count . ' | Logged out: ' . $error->logged_out_count . '

'; $body .= JVB()->email()->card($cardContent, $error->component); } } // Critical errors section if (!empty($summary['critical'])) { $body .= JVB()->email()->spacer(30); $body .= JVB()->email()->h2('Recent Critical Errors'); foreach ($summary['critical'] as $error) { $cardContent = '

Time: ' . esc_html($error->created_at) . '

'; $cardContent .= '

Message:

'; $cardContent .= JVB()->email()->codeBlock($error->message); // Include context if available if (!empty($error->context)) { $context = json_decode($error->context, true); if (is_array($context)) { $cardContent .= '

Context:

'; $cardContent .= JVB()->email()->codeBlock(json_encode($context, JSON_PRETTY_PRINT)); } } $body .= JVB()->email()->card($cardContent, $error->component . ': ' . $error->error_type); } } // Dashboard link $admin_url = admin_url('admin.php?page=jvb-error-logs'); $body .= JVB()->email()->spacer(30); $body .= JVB()->email()->button($admin_url, 'View Detailed Logs'); return JVB()->email()->sendEmail($admin_email, $subject, $body, 'ERROR SUMMARY'); } /** * Get HTML version of the error summary for nicer emails * * @param array $summary The error summary data * @return string HTML formatted error summary */ protected function getHtmlErrorSummary(array $summary):string { $html = '
'; $html .= '

Error Summary

'; // Frequent errors table if (!empty($summary['frequent'])) { $html .= '

Most Frequent Errors

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; foreach ($summary['frequent'] as $error) { $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= '
ComponentError TypeMessageCount
' . esc_html($error->component) . '' . esc_html($error->error_type) . '' . esc_html(wp_trim_words($error->message, 10, '...')) . '' . esc_html($error->count) . '
'; } // Critical errors section if (!empty($summary['critical'])) { $html .= '

Recent Critical Errors

'; foreach ($summary['critical'] as $error) { $html .= '
'; $html .= '

' . esc_html($error->component) . ': ' . esc_html($error->error_type) . '

'; $html .= '

Time: ' . esc_html($error->created_at) . '

'; $html .= '

Message: ' . esc_html($error->message) . '

'; // Context display if (!empty($error->context)) { $context = json_decode($error->context, true); if (is_array($context)) { $html .= '
'; $html .= 'Context:'; $html .= '
';
                        $html .= esc_html(json_encode($context, JSON_PRETTY_PRINT));
                        $html .= '
'; $html .= '
'; } } $html .= '
'; } } // Dashboard link $admin_url = admin_url('admin.php?page=jvb-error-logs'); $html .= '

'; $html .= 'View All Errors in Dashboard'; $html .= '

'; $html .= '
'; return $html; } public function renderAdminPage() { ?>

Error Logs

buildParams($request); } protected function buildParams(WP_REST_Request $request):array { $allowedSeverity = [ 'all', 'notice', 'warning', 'error', 'critical', ]; $allowedComponents = [ 'all', //Get all components ]; $severity = $request->get_param('severity')??'all'; $component = $request->get_param('component')??'all'; return [ 'page' => (int)$request->get_param('page'), 'severity' => (in_array($severity, $allowedSeverity)) ? $severity : 'all', 'component' => (in_array($component, $allowedComponents)) ? $component : 'all', ]; } }