Jake Vanderwerf
2026-01-02 b5abd615697146beeca6dba4acd057d049554a30
inc/managers/ErrorHandler.php
@@ -186,33 +186,61 @@
     *
     * @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 {
         $table = $this->wpdb->prefix . BASE . 'error_log';
         // Validate severity
         if (!array_key_exists($severity, $this->error_levels)) {
            $severity = 'error';
         }
            // 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;
        }
    }
         // 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
@@ -221,126 +249,210 @@
     *
     * @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
   {
      $table = $this->wpdb->prefix . BASE . 'error_log';
        // 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
        ));
      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 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
        ));
      // 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
      ));
        return [
            'frequent' => $frequent_errors,
            'critical' => $critical_errors
        ];
    }
      // 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
    {
        // 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 +538,8 @@
    }
    protected function buildParams(WP_REST_Request $request):array {
        $allowedSeverity = [
            'all',