exception->component ?? 'unknown', operation: $context->exception->operation ?? 'unknown', errorCode: self::extractErrorCode($context), errorMessage: self::extractUserMessage($context), severity: self::determineSeverity($context), occurredAt: new \DateTimeImmutable(), context: $context->exception->data, metadata: $context->exception->metadata, requestId: $context->request->requestId, userId: $context->request->userId ?? null, clientIp: $context->request->clientIp, isSecurityEvent: $context->exception->metadata['security_event'] ?? false, stackTrace: self::extractStackTrace($context), userAgent: $context->request->userAgent, ); } /** * Converts to array for storage/transmission */ public function toArray(): array { return [ 'id' => (string) $this->id, 'service' => $this->service, 'component' => $this->component, 'operation' => $this->operation, 'error_code' => $this->errorCode->value, 'error_message' => $this->errorMessage, 'severity' => $this->severity->value, 'occurred_at' => $this->occurredAt->format('c'), 'context' => $this->context, 'metadata' => $this->metadata, 'request_id' => $this->requestId, 'user_id' => $this->userId, 'client_ip' => $this->clientIp, 'is_security_event' => $this->isSecurityEvent, 'stack_trace' => $this->stackTrace, 'user_agent' => $this->userAgent, ]; } /** * Creates from array (for deserialization) */ public static function fromArray(array $data): self { return new self( id: Ulid::fromString($data['id']), service: $data['service'], component: $data['component'], operation: $data['operation'], errorCode: ErrorCode::from($data['error_code']), errorMessage: $data['error_message'], severity: ErrorSeverity::from($data['severity']), occurredAt: new \DateTimeImmutable($data['occurred_at']), context: $data['context'] ?? [], metadata: $data['metadata'] ?? [], requestId: $data['request_id'], userId: $data['user_id'], clientIp: $data['client_ip'], isSecurityEvent: $data['is_security_event'] ?? false, stackTrace: $data['stack_trace'], userAgent: $data['user_agent'], ); } /** * Gets fingerprint for grouping similar errors */ public function getFingerprint(): string { $components = [ $this->service, $this->component, $this->operation, $this->errorCode->value, // Normalize error message to group similar errors $this->normalizeErrorMessage($this->errorMessage), ]; return hash('sha256', implode('|', $components)); } /** * Checks if this error should trigger an alert */ public function shouldTriggerAlert(): bool { // Critical and error severity always trigger alerts if (in_array($this->severity, [ErrorSeverity::CRITICAL, ErrorSeverity::ERROR])) { return true; } // Security events always trigger alerts if ($this->isSecurityEvent) { return true; } // Check metadata for explicit alert requirement return $this->metadata['requires_alert'] ?? false; } /** * Gets alert urgency level */ public function getAlertUrgency(): AlertUrgency { if ($this->severity === ErrorSeverity::CRITICAL || $this->isSecurityEvent) { return AlertUrgency::URGENT; } if ($this->severity === ErrorSeverity::ERROR) { return AlertUrgency::HIGH; } if ($this->severity === ErrorSeverity::WARNING) { return AlertUrgency::MEDIUM; } return AlertUrgency::LOW; } private static function extractServiceName(ErrorHandlerContext $context): string { // Try to extract service from request URI $uri = $context->request->requestUri; if ($uri !== null) { if (str_starts_with($uri, '/api/')) { return 'api'; } if (str_starts_with($uri, '/admin/')) { return 'admin'; } } // Extract from component if available if ($context->exception->component) { return strtolower($context->exception->component); } return 'web'; } private static function extractErrorCode(ErrorHandlerContext $context): ErrorCode { // Try to get ErrorCode from original exception if it's a FrameworkException if (isset($context->exception->data['original_exception'])) { $originalException = $context->exception->data['original_exception']; if ($originalException instanceof \App\Framework\Exception\FrameworkException) { $errorCode = $originalException->getErrorCode(); if ($errorCode !== null) { return $errorCode; } } } // Fallback: Use SystemErrorCode::RESOURCE_EXHAUSTED as generic error return \App\Framework\Exception\Core\SystemErrorCode::RESOURCE_EXHAUSTED; } private static function extractUserMessage(ErrorHandlerContext $context): string { // Try user_message first if (isset($context->exception->data['user_message'])) { return $context->exception->data['user_message']; } // Try exception_message (stored by ErrorHandler) if (isset($context->exception->data['exception_message'])) { return $context->exception->data['exception_message']; } // Try to get from original_exception if it's a FrameworkException if (isset($context->exception->data['original_exception'])) { $originalException = $context->exception->data['original_exception']; if ($originalException instanceof \App\Framework\Exception\FrameworkException) { return $originalException->getMessage(); } if ($originalException instanceof \Throwable) { return $originalException->getMessage(); } } // Fallback to operation and component $operation = $context->exception->operation ?? 'unknown_operation'; $component = $context->exception->component ?? 'unknown_component'; return "Error in {$component} during {$operation}"; } private static function determineSeverity(ErrorHandlerContext $context): ErrorSeverity { // Security events are always critical if ($context->exception->metadata['security_event'] ?? false) { return ErrorSeverity::CRITICAL; } // Check explicit severity in metadata if (isset($context->exception->metadata['severity'])) { return ErrorSeverity::tryFrom($context->exception->metadata['severity']) ?? ErrorSeverity::ERROR; } // Get severity from ErrorCode if available $errorCode = self::extractErrorCode($context); if ($errorCode !== null) { return $errorCode->getSeverity(); } // Determine from HTTP status $httpStatus = $context->metadata['http_status'] ?? 500; return match (true) { $httpStatus >= 500 => ErrorSeverity::ERROR, $httpStatus >= 400 => ErrorSeverity::WARNING, default => ErrorSeverity::INFO, }; } private static function extractStackTrace(ErrorHandlerContext $context): ?string { // Don't include stack traces for security events in production if (($context->exception->metadata['security_event'] ?? false) && ! ($_ENV['APP_DEBUG'] ?? false)) { return null; } // Extract from exception debug data if (isset($context->exception->debug['stack_trace'])) { return $context->exception->debug['stack_trace']; } return null; } private function normalizeErrorMessage(string $message): string { // Remove specific details to group similar errors $normalized = $message; // Remove file paths $normalized = preg_replace('#/[^\s]+#', '/path/to/file', $normalized); // Remove specific IDs/numbers $normalized = preg_replace('#\b\d+\b#', 'N', $normalized); // Remove timestamps $normalized = preg_replace('#\d{4}-\d{2}-\d{2}[\sT]\d{2}:\d{2}:\d{2}#', 'TIMESTAMP', $normalized); // Remove ULIDs/UUIDs $normalized = preg_replace('#[0-9A-HJ-NP-TV-Z]{26}#', 'ULID', $normalized); $normalized = preg_replace('#[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#i', 'UUID', $normalized); return $normalized; } }