<?php
|
namespace JVBase\managers;
|
|
use JVBase\JVB;
|
use WP_Error;
|
use Exception;
|
use WP_REST_Request;
|
|
if (!defined('ABSPATH')) {
|
exit; // Exit if accessed directly
|
}
|
|
class ErrorHandler
|
{
|
protected object $wpdb;
|
protected string $tableName;
|
protected int $notification_threshold = 5; // Critical errors within 1 hour
|
|
protected array $error_levels = [
|
'debug' => 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(
|
'<p><strong>Component:</strong> ' . esc_html($component) . '</p>' .
|
'<p><strong>Message:</strong></p>' .
|
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('<p>Error summary for <strong>%s</strong></p>', $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 .= '<strong>' . esc_html($error->error_type) . '</strong>';
|
$cardContent .= '<p style="margin:10px 0 5px 0;font-size:13px;">' . esc_html(wp_trim_words($error->message, 15)) . '</p>';
|
$cardContent .= '<p style="margin:0;font-size:12px;color:' . JVB()->email()->colours['dark-200'] . ';">
|
Source: ' . esc_html($error->source) . ' |
|
Logged in: ' . $error->logged_in_count . ' |
|
Logged out: ' . $error->logged_out_count . '
|
</p>';
|
|
$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 = '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>';
|
$cardContent .= '<p><strong>Message:</strong></p>';
|
$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 .= '<p><strong>Context:</strong></p>';
|
$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 = '<div style="font-family: sans-serif; max-width: 800px; margin: 0 auto;">';
|
$html .= '<h1 style="color:#FF0080;border-bottom:2px solid #1B1B1B;padding-bottom:10px;">Error Summary</h1>';
|
|
// Frequent errors table
|
if (!empty($summary['frequent'])) {
|
$html .= '<h2>Most Frequent Errors</h2>';
|
$html .= '<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">';
|
$html .= '<tr style="background-color: #1B1B1B; color: #EFEFEF;">';
|
$html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Component</th>';
|
$html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Error Type</th>';
|
$html .= '<th style="padding: 8px; text-align: left; border: 1px solid #ddd;">Message</th>';
|
$html .= '<th style="padding: 8px; text-align: center; border: 1px solid #ddd;">Count</th>';
|
$html .= '</tr>';
|
|
foreach ($summary['frequent'] as $error) {
|
$html .= '<tr>';
|
$html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html($error->component) . '</td>';
|
$html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html($error->error_type) . '</td>';
|
$html .= '<td style="padding:8px;border:1px solid #ddd;">' . esc_html(wp_trim_words($error->message, 10, '...')) . '</td>';
|
$html .= '<td style="padding:8px;border:1px solid #ddd;text-align:center;"><strong>' . esc_html($error->count) . '</strong></td>';
|
$html .= '</tr>';
|
}
|
|
$html .= '</table>';
|
}
|
|
// Critical errors section
|
if (!empty($summary['critical'])) {
|
$html .= '<h2>Recent Critical Errors</h2>';
|
|
foreach ($summary['critical'] as $error) {
|
$html .= '<div style="margin-bottom: 20px; padding: 15px; border-left: 4px solid #FF0080; background-color: #fff9fb;">';
|
$html .= '<h3 style="margin-top: 0; color: #1B1B1B;">' . esc_html($error->component) . ': ' . esc_html($error->error_type) . '</h3>';
|
$html .= '<p><strong>Time:</strong> ' . esc_html($error->created_at) . '</p>';
|
$html .= '<p><strong>Message:</strong> ' . esc_html($error->message) . '</p>';
|
|
// Context display
|
if (!empty($error->context)) {
|
$context = json_decode($error->context, true);
|
if (is_array($context)) {
|
$html .= '<div style="margin-top: 10px;">';
|
$html .= '<strong>Context:</strong>';
|
$html .= '<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; overflow: auto; max-height: 200px;">';
|
$html .= esc_html(json_encode($context, JSON_PRETTY_PRINT));
|
$html .= '</pre>';
|
$html .= '</div>';
|
}
|
}
|
|
$html .= '</div>';
|
}
|
}
|
|
// Dashboard link
|
$admin_url = admin_url('admin.php?page=jvb-error-logs');
|
$html .= '<p style="margin-top: 30px; padding-top: 15px; border-top: 1px solid #ddd;">';
|
$html .= '<a href="' . esc_url($admin_url) . '" style="display: inline-block; background-color: #FF0080; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px;">View All Errors in Dashboard</a>';
|
$html .= '</p>';
|
|
$html .= '</div>';
|
|
return $html;
|
}
|
|
public function renderAdminPage()
|
{
|
?>
|
<h1>Error Logs</h1>
|
<?php
|
}
|
|
public function getErrors(WP_REST_Request $request) {
|
$params = $this->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',
|
];
|
}
|
}
|