Jake Vanderwerf
2026-09-18 a6413da67a4579866f0fd0323ddb23a7b6b9654a
inc/managers/Errors.php
@@ -13,504 +13,30 @@
class Errors
{
   use Instance;
    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;
    private 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
   /**
    * @param string $message
    * @param array $context
    * @return void
    */
   public static function log(string $message, array $context = []):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 = AdminPages::getInstance();
        $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 {
            // 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->table->insert(
                [
                    'error_type' => $error_type,
                    'component' => $component,
                    'message' => $message,
                    'context' => $context_json,
                    'severity' => $severity,
                    'user_id' => get_current_user_id(),
                    'created_at' => current_time('mysql')
                ]
            );
            if (!$result) {
                // If insert fails, log to PHP error log as fallback
                error_log("[ErrorHandler] Database insert failed: " . $this->table->getLastError());
                return [
               'success'   => false,
               'message'   => "[ErrorHandler] Database insert failed: " . $this->table->getLastError()
            ];
            }
            // 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->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 ($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 {
         // 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->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()];
      $msg = '{'.BASE.'}';
      if (array_key_exists('class', $context)) {
         $msg .= '['.$context['class'].']';
         unset($context['class']);
      }
   }
    /**
     * @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 = Emails::getInstance()->alert(
         'A critical error has occurred and requires immediate attention',
         'error'
      );
      $body .= Emails::getInstance()->h2('Error Details');
      $body .= Emails::getInstance()->card(
         '<p><strong>Component:</strong> ' . esc_html($component) . '</p>' .
         '<p><strong>Message:</strong></p>' .
         Emails::getInstance()->codeBlock($message),
         'Error Information'
      );
      if (array_key_exists('method', $context)) {
         $msg .= '::'.$context['method'];
         unset($context['method']);
      }
      if (array_key_exists('function', $context)) {
         $msg .= $context['function'];
         unset($context['function']);
      }
      error_log($msg.' '.$message);
      if (!empty($context)) {
         $body .= Emails::getInstance()->h3('Additional Context');
         $body .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT));
         error_log('Context: '.print_r($context, true));
      }
      return Emails::getInstance()->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
   {
      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->table->getMany([
         'where' => [
            'created_at'   => ['BETWEEN', "{$start_date} AND {$end_date}"]
         ]
      ]);
      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
   {
      $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 = Emails::getInstance()->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 = [
            Emails::getInstance()->stat($summary['stats']->total_errors, 'Total Errors'),
            Emails::getInstance()->stat($summary['stats']->critical_count, 'Critical', 'Requires attention'),
            Emails::getInstance()->stat($summary['stats']->error_count, 'Errors'),
            Emails::getInstance()->stat($summary['stats']->warning_count, 'Warnings')
         ];
         $body .= Emails::getInstance()->grid($stats, 4);
      }
      // Alert if critical errors exist
      if (!empty($summary['critical'])) {
         $body .= Emails::getInstance()->alert(
            sprintf('Found %d critical errors that need immediate attention', count($summary['critical'])),
            'error'
         );
      }
      $body .= Emails::getInstance()->spacer(20);
      // Frequent errors section
      if (!empty($summary['frequent'])) {
         $body .= Emails::getInstance()->h2('Most Frequent Errors');
         foreach ($summary['frequent'] as $error) {
            $cardContent = Emails::getInstance()->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:' . Emails::getInstance()->colours['dark-200'] . ';">
                Source: ' . esc_html($error->source) . ' |
                Logged in: ' . $error->logged_in_count . ' |
                Logged out: ' . $error->logged_out_count . '
            </p>';
            $body .= Emails::getInstance()->card($cardContent, $error->component);
         }
      }
      // Critical errors section
      if (!empty($summary['critical'])) {
         $body .= Emails::getInstance()->spacer(30);
         $body .= Emails::getInstance()->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 .= Emails::getInstance()->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 .= Emails::getInstance()->codeBlock(json_encode($context, JSON_PRETTY_PRINT));
               }
            }
            $body .= Emails::getInstance()->card($cardContent, $error->component . ': ' . $error->error_type);
         }
      }
      // Dashboard link
      $admin_url = admin_url('admin.php?page=jvb-error-logs');
      $body .= Emails::getInstance()->spacer(30);
      $body .= Emails::getInstance()->button($admin_url, 'View Detailed Logs');
      return Emails::getInstance()->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',
        ];
    }
}