<?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
|
];
|
|
public function __construct()
|
{
|
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 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'):bool
|
{
|
try {
|
// Normal queue-based logging
|
JVB()->queue()->queueOperation(
|
'error_log',
|
get_current_user_id(),
|
[
|
'component' => $component,
|
'message' => $message,
|
'context' => $context,
|
'severity' => $severity
|
],
|
['priority' => 'high']
|
);
|
|
|
// Immediate notification for critical errors
|
if ($severity === 'critical') {
|
$this->notifyAdmin($component, $message, $context);
|
}
|
return true;
|
} catch (Exception $e) {
|
error_log("[edmonton.ink Error] Failed to log error: " . $e->getMessage());
|
return false;
|
}
|
}
|
|
/**
|
* @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 = "[edmonton.ink Critical Error] {$component}";
|
$body = "Error: {$message}\n\nContext: " . print_r($context, true);
|
|
return jvbMail($admin_email, $subject, $body);
|
}
|
|
/**
|
* Gather summary of the most important errors
|
* @return array
|
*/
|
protected function gatherErrorSummary():array
|
{
|
$yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
|
|
// Get most frequent errors
|
$frequent_errors = $this->wpdb->get_results($this->wpdb->prepare(
|
"SELECT error_type, component, message, COUNT(*) as count
|
FROM {$this->tableName}
|
WHERE created_at > %s
|
GROUP BY error_type, component, message
|
ORDER BY count DESC
|
LIMIT 20",
|
$yesterday
|
));
|
|
// Get most recent critical errors
|
$critical_errors = $this->wpdb->get_results($this->wpdb->prepare(
|
"SELECT * FROM {$this->tableName}
|
WHERE severity = 'critical' AND created_at > %s
|
ORDER BY created_at DESC
|
LIMIT 5",
|
$yesterday
|
));
|
|
return [
|
'frequent' => $frequent_errors,
|
'critical' => $critical_errors
|
];
|
}
|
|
/**
|
* Send daily error summary email to administrator
|
* @return bool Whether email is sent
|
*/
|
public function sendErrorSummary():bool
|
{
|
// Get summary data
|
$summary = $this->gatherErrorSummary();
|
|
// Only send if there are errors
|
if (empty($summary['frequent']) && empty($summary['critical'])) {
|
return false;
|
}
|
|
$admin_email = get_option('admin_email');
|
$site_name = get_bloginfo('name');
|
$today = date('Y-m-d');
|
$yesterday = date('Y-m-d', strtotime('-1 day'));
|
|
$subject = "[{$site_name}] Daily Error Summary - {$today}";
|
|
// Build email body
|
$body = "= Error Summary for {$yesterday} =\n\n";
|
|
// Add frequent errors section
|
if (!empty($summary['frequent'])) {
|
$body .= "== Most Frequent Errors ==\n\n";
|
|
foreach ($summary['frequent'] as $index => $error) {
|
$body .= ($index + 1) . ". [{$error->component}] {$error->error_type}\n";
|
$body .= " Message: " . wp_trim_words($error->message, 20, '...') . "\n";
|
$body .= " Count: {$error->count}\n\n";
|
}
|
}
|
|
// Add critical errors section
|
if (!empty($summary['critical'])) {
|
$body .= "== Recent Critical Errors ==\n\n";
|
|
foreach ($summary['critical'] as $index => $error) {
|
$body .= ($index + 1) . ". [{$error->component}] {$error->error_type}\n";
|
$body .= " Time: {$error->created_at}\n";
|
$body .= " Message: " . $error->message . "\n\n";
|
|
// Include context for critical errors if available
|
if (!empty($error->context)) {
|
$context = json_decode($error->context, true);
|
if (is_array($context)) {
|
$body .= " Context:\n";
|
foreach ($context as $key => $value) {
|
if (is_array($value) || is_object($value)) {
|
$value = json_encode($value);
|
}
|
$body .= " - {$key}: {$value}\n";
|
}
|
}
|
$body .= "\n";
|
}
|
}
|
}
|
|
// Add dashboard link if available
|
$admin_url = admin_url('admin.php?page=jvb-error-logs');
|
$body .= "View detailed error logs in the dashboard: {$admin_url}\n\n";
|
|
// Send the email
|
$sent = jvbMail($admin_email, $subject, $body, 'ERROR SUMMARY');
|
|
// Log that summary was sent
|
if ($sent) {
|
error_log("[ErrorHandler] Daily error summary sent to {$admin_email}");
|
} else {
|
error_log("[ErrorHandler] Daily error summary was not sent.");
|
}
|
|
return $sent;
|
}
|
|
/**
|
* 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',
|
];
|
}
|
}
|