Jake Vanderwerf
3 days ago ba1e1ccf869b818f7a7a897264dfea05563a7796
inc/managers/ErrorHandler.php
@@ -12,8 +12,6 @@
class ErrorHandler
{
    protected object $wpdb;
   protected string $tableName;
    protected int $notification_threshold = 5; // Critical errors within 1 hour
    protected array $error_levels = [
@@ -24,11 +22,14 @@
        'critical' => 4
    ];
   protected CustomTable $table;
    public function __construct()
    {
        global $wpdb;
        $this->wpdb = $wpdb;
      $this->tableName = $wpdb->prefix . BASE . 'error_log';
      $this->defineTables();
//        global $wpdb;
//        $this->wpdb = $wpdb;
//    $this->tableName = $wpdb->prefix . BASE . 'error_log';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
@@ -39,6 +40,35 @@
//        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();
@@ -68,7 +98,7 @@
        }
        try {
            $table = $this->tableName;
            // Extract error data
            $component = sanitize_text_field($data['component'] ?? '');
            $message = sanitize_textarea_field($data['message'] ?? '');
@@ -94,8 +124,7 @@
            }
            // Insert into database
            $result = $this->wpdb->insert(
                $table,
            $result = $this->table->insert(
                [
                    'error_type' => $error_type,
                    'component' => $component,
@@ -104,24 +133,15 @@
                    '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 (!$result) {
                // If insert fails, log to PHP error log as fallback
                error_log("[ErrorHandler] Database insert failed: " . $this->wpdb->last_error);
                error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError());
                return [
               'success'   => false,
               'message'   => "[ErrorHandler] Database insert failed: " . $this->wpdb->last_error
               'message'   => "[ErrorHandler] Database insert failed: " . $this->table->getLastError()
            ];
            }
@@ -155,19 +175,15 @@
    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
        ));
      $count = $this->table->count([
         'error_type' => $error_type,
         'component' => $component,
         'severity' => $this->error_levels[$error_type],
         'created_at' => ['>','DATE_SUB(NOW(), INTERVAL 1 HOUR)']
      ]);
        // If threshold reached, take additional actions (e.g., notify developers)
        if ((int)$count >= $this->notification_threshold) {
        if ($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}";
@@ -186,33 +202,56 @@
     *
     * @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']
            );
   public function log(string $component, string $message, array $context = [], string $severity = 'error'): array
   {
      try {
         // 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']));
            // 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;
        }
    }
         // Determine source from context
         $source = isset($context['source']) ? $context['source'] :
            (isset($context['url']) ? 'frontend' : 'backend');
         $result = $this->table->insert(
            [
               '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')
            ]);
         if (!$result) {
            error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError());
            return ['success' => false, 'message' => $this->table->getLastError()];
         }
         if ($severity === 'critical') {
            $this->checkErrorThreshold($error_type, $component);
         }
         return ['success' => true, 'id' => $result];
      } 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
@@ -221,126 +260,150 @@
     *
     * @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);
   protected function notifyAdmin(string $component, string $message, array $context):bool
   {
      $admin_email = get_option('admin_email');
      $subject = "[" . get_bloginfo('name') . " Critical Error] {$component}";
        return jvbMail($admin_email, $subject, $body);
    }
      $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
     */
    protected function gatherErrorSummary():array
    {
        $yesterday = date('Y-m-d H:i:s', strtotime('-24 hours'));
   public function gatherErrorSummary(?string $start_date = null, ?string $end_date = null): array
   {
      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');
      }
        // 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
        ));
      // Most frequent error patterns (deduplicated by component/method/message)
      $frequent = $this->table->getMany([
         'where' => [
            'created_at'   => ['BETWEEN', "{$start_date} AND {$end_date}"]
         ]
      ]);
        // 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
        ];
    }
      return [
         'errors' => $frequent,
         '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
    {
        // Get summary data
        $summary = $this->gatherErrorSummary();
   public function sendErrorSummary():bool
   {
      $summary = $this->gatherErrorSummary();
        // Only send if there are errors
        if (empty($summary['frequent']) && empty($summary['critical'])) {
            return false;
        }
      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'));
      $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');
        $subject = "[{$site_name}] Daily Error Summary - {$today}";
      // Header with alert
      $body = JVB()->email()->h1('Daily Error Summary');
      $body .= sprintf('<p>Error summary for <strong>%s</strong></p>', $yesterday);
        // Build email body
        $body = "= Error Summary for {$yesterday} =\n\n";
      // 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);
      }
        // Add frequent errors section
        if (!empty($summary['frequent'])) {
            $body .= "== Most Frequent Errors ==\n\n";
      // 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'
         );
      }
            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";
            }
        }
      $body .= JVB()->email()->spacer(20);
        // Add critical errors section
        if (!empty($summary['critical'])) {
            $body .= "== Recent Critical Errors ==\n\n";
      // Frequent errors section
      if (!empty($summary['frequent'])) {
         $body .= JVB()->email()->h2('Most Frequent Errors');
            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";
         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>';
                // 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";
                }
            }
        }
            $body .= JVB()->email()->card($cardContent, $error->component);
         }
      }
        // 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";
      // Critical errors section
      if (!empty($summary['critical'])) {
         $body .= JVB()->email()->spacer(30);
         $body .= JVB()->email()->h2('Recent Critical Errors');
        // Send the email
        $sent = jvbMail($admin_email, $subject, $body, 'ERROR SUMMARY');
         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);
        // 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.");
        }
            // 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));
               }
            }
        return $sent;
    }
            $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
@@ -426,6 +489,8 @@
    }
    protected function buildParams(WP_REST_Request $request):array {
        $allowedSeverity = [
            'all',