feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,615 @@
# LiveComponents Error Recovery Strategy
## Error Categories
### 1. Component Errors
- Unknown component name
- Invalid component ID format
- Component instantiation failure
- Missing dependencies
### 2. Action Errors
- Unknown action method
- Action method not callable
- Invalid action parameters
- Action execution exception
### 3. State Errors
- Invalid state format
- State deserialization failure
- State validation error
- State version conflict
### 4. Rendering Errors
- Template not found
- Template syntax error
- Rendering exception
- Cache write failure
### 5. Upload Errors
- File validation failure
- Upload size limit exceeded
- Invalid file type
- Storage write failure
### 6. Lifecycle Errors
- onMount() exception
- onUpdate() exception
- onDestroy() exception
## Error Handling Strategy
### Server-Side Error Handling
#### 1. Component Resolution Errors
**Current Behavior:**
```php
public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
{
if (!$className) {
throw new \InvalidArgumentException("Unknown component: {$componentName}");
}
}
```
**Improved Error Recovery:**
```php
use App\Framework\Exception\FrameworkException;
use App\Framework\Exception\ErrorCode;
public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
{
[$componentName, $instanceId] = $this->parseComponentId($componentId->toString());
$className = $this->nameToClassMap[$componentName] ?? null;
if (!$className) {
// Find similar component names (Levenshtein distance)
$suggestions = $this->findSimilarComponents($componentName);
throw FrameworkException::create(
ErrorCode::COMPONENT_NOT_FOUND,
"Unknown component: '{$componentName}'"
)->withData([
'component_name' => $componentName,
'instance_id' => $instanceId,
'suggestions' => $suggestions,
'available_components' => array_keys($this->nameToClassMap)
]);
}
try {
return $this->container->invoker->make($className, [
'id' => $componentId,
'initialData' => $state
]);
} catch (\Throwable $e) {
throw FrameworkException::create(
ErrorCode::COMPONENT_INSTANTIATION_FAILED,
"Failed to instantiate component '{$componentName}'"
)->withData([
'component_class' => $className,
'error' => $e->getMessage()
])->withPrevious($e);
}
}
private function findSimilarComponents(string $componentName): array
{
$suggestions = [];
foreach (array_keys($this->nameToClassMap) as $availableName) {
$distance = levenshtein($componentName, $availableName);
if ($distance <= 3) { // Max 3 character difference
$suggestions[] = $availableName;
}
}
return $suggestions;
}
```
#### 2. Action Execution Errors
**Enhanced LiveComponentHandler:**
```php
public function handle(
LiveComponentContract $component,
string $actionName,
ActionParameters $params
): ComponentUpdate {
// Validate action exists
if (!method_exists($component, $actionName)) {
$availableActions = $this->getAvailableActions($component);
throw FrameworkException::create(
ErrorCode::ACTION_NOT_FOUND,
"Unknown action '{$actionName}' on component " . get_class($component)
)->withData([
'action_name' => $actionName,
'available_actions' => $availableActions,
'component_class' => get_class($component)
]);
}
// Validate action is public and non-static
$reflection = new \ReflectionMethod($component, $actionName);
if (!$reflection->isPublic() || $reflection->isStatic()) {
throw FrameworkException::create(
ErrorCode::ACTION_NOT_CALLABLE,
"Action '{$actionName}' is not a public instance method"
)->withData([
'action_name' => $actionName,
'is_public' => $reflection->isPublic(),
'is_static' => $reflection->isStatic()
]);
}
// Execute with error recovery
try {
$result = $this->container->invoker->invoke($component, $actionName, $params->toArray());
// Validate result type
if (!$result instanceof ComponentData) {
throw FrameworkException::create(
ErrorCode::ACTION_INVALID_RETURN,
"Action '{$actionName}' must return ComponentData"
)->withData([
'action_name' => $actionName,
'returned_type' => get_debug_type($result)
]);
}
return ComponentUpdate::fromActionResult($component, $result);
} catch (FrameworkException $e) {
// Re-throw framework exceptions
throw $e;
} catch (\Throwable $e) {
// Wrap other exceptions
throw FrameworkException::create(
ErrorCode::ACTION_EXECUTION_FAILED,
"Action '{$actionName}' execution failed"
)->withData([
'action_name' => $actionName,
'component_class' => get_class($component),
'error_message' => $e->getMessage(),
'error_type' => get_class($e)
])->withPrevious($e);
}
}
private function getAvailableActions(LiveComponentContract $component): array
{
$reflection = new \ReflectionClass($component);
$actions = [];
foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
if ($method->isStatic() || $method->isConstructor()) {
continue;
}
// Skip getter/setter/lifecycle methods
if (in_array($method->getName(), ['getId', 'getData', 'getRenderData', 'onMount', 'onUpdate', 'onDestroy'])) {
continue;
}
// Check return type
$returnType = $method->getReturnType();
if ($returnType instanceof \ReflectionNamedType &&
$returnType->getName() === ComponentData::class) {
$actions[] = $method->getName();
}
}
return $actions;
}
```
#### 3. State Deserialization Errors
**Safe State Handling:**
```php
#[Route('/live-component/{id}', method: Method::POST)]
public function handleAction(string $id, HttpRequest $request): JsonResult
{
try {
$action = ComponentAction::fromRequest($request, $id);
} catch (\Throwable $e) {
return new JsonResult([
'success' => false,
'error' => 'Invalid action format',
'details' => $e->getMessage()
], Status::BAD_REQUEST);
}
// Safe state deserialization
try {
$stateArray = $request->parsedBody?->get('state', []);
if (!is_array($stateArray)) {
throw new \InvalidArgumentException('State must be an array');
}
$state = ComponentData::fromArray($stateArray);
} catch (\Throwable $e) {
return new JsonResult([
'success' => false,
'error' => 'Invalid state format',
'details' => $e->getMessage(),
'recovery' => 'Client should reload component from server'
], Status::BAD_REQUEST);
}
// Component resolution with error recovery
try {
$component = $this->componentRegistry->resolve($action->componentId, $state);
} catch (FrameworkException $e) {
return new JsonResult([
'success' => false,
'error' => $e->getMessage(),
'data' => $e->getData(),
'recovery' => 'Check component name and reload page'
], Status::NOT_FOUND);
}
// Action handling with error recovery
try {
$update = $this->handler->handle($component, $action->method, $action->params);
} catch (FrameworkException $e) {
return new JsonResult([
'success' => false,
'error' => $e->getMessage(),
'data' => $e->getData(),
'recovery' => 'Try again or reload component'
], Status::UNPROCESSABLE_ENTITY);
} catch (\Throwable $e) {
// Unexpected errors
error_log("LiveComponent action error: " . $e->getMessage());
return new JsonResult([
'success' => false,
'error' => 'Internal server error',
'recovery' => 'Please reload the page',
'error_id' => uniqid('lc_error_') // For support
], Status::INTERNAL_SERVER_ERROR);
}
// Rendering with error recovery
try {
$updatedComponent = $this->componentRegistry->resolve(
$action->componentId,
ComponentData::fromArray($update->state->data)
);
$html = $this->componentRegistry->render($updatedComponent);
return new JsonResult([
'success' => true,
'html' => $html,
'state' => $update->state->toArray(),
'events' => $update->events
]);
} catch (\Throwable $e) {
error_log("LiveComponent render error: " . $e->getMessage());
return new JsonResult([
'success' => false,
'error' => 'Component rendering failed',
'state' => $update->state->toArray(), // Return state so client can retry
'recovery' => 'Try again',
'error_id' => uniqid('lc_render_')
], Status::INTERNAL_SERVER_ERROR);
}
}
```
#### 4. Lifecycle Hook Errors
**Safe Lifecycle Hook Execution:**
```php
// In LiveComponentHandler
public function callMountHook(LiveComponentContract $component): void
{
if (!$component instanceof LifecycleAware) {
return;
}
try {
$component->onMount();
} catch (\Throwable $e) {
// Log but don't fail - lifecycle hooks are side effects only
error_log(sprintf(
"Lifecycle hook onMount() failed for %s: %s",
get_class($component),
$e->getMessage()
));
// Optional: Dispatch error event for monitoring
$this->eventDispatcher?->dispatch(
new ComponentLifecycleErrorEvent(
componentId: $component->getId(),
hook: 'onMount',
error: $e
)
);
}
}
private function callUpdateHook(LiveComponentContract $component): void
{
if (!$component instanceof LifecycleAware) {
return;
}
try {
$component->onUpdate();
} catch (\Throwable $e) {
error_log(sprintf(
"Lifecycle hook onUpdate() failed for %s: %s",
get_class($component),
$e->getMessage()
));
}
}
```
### Client-Side Error Recovery
#### JavaScript Error Handling
**Enhanced LiveComponents Client:**
```javascript
class LiveComponentManager {
async handleAction(componentId, actionName, params = {}) {
const component = this.components.get(componentId);
if (!component) {
console.error(`Component not found: ${componentId}`);
return;
}
// Show loading state
component.element.classList.add('lc-loading');
try {
const response = await this.sendAction(componentId, actionName, params);
if (!response.success) {
this.handleError(component, response);
return;
}
// Update component
this.updateComponent(component, response);
} catch (error) {
this.handleNetworkError(component, error);
} finally {
component.element.classList.remove('lc-loading');
}
}
handleError(component, response) {
const { error, recovery, data } = response;
console.error(`Component error: ${error}`, data);
// Show user-friendly error message
this.showErrorMessage(component.element, error);
// Implement recovery strategy
switch (recovery) {
case 'Try again':
this.showRetryButton(component);
break;
case 'Client should reload component from server':
this.reloadComponent(component);
break;
case 'Check component name and reload page':
this.showReloadPageMessage(component);
break;
default:
// Unknown recovery - show generic error
this.showGenericError(component);
}
// Dispatch error event for custom handling
component.element.dispatchEvent(new CustomEvent('lc:error', {
detail: { error, recovery, data },
bubbles: true
}));
}
handleNetworkError(component, error) {
console.error('Network error:', error);
// Retry logic with exponential backoff
if (component.retryCount < 3) {
const delay = Math.pow(2, component.retryCount) * 1000;
component.retryCount++;
setTimeout(() => {
console.log(`Retrying action (attempt ${component.retryCount})...`);
// Retry last action
this.handleAction(
component.id,
component.lastAction,
component.lastParams
);
}, delay);
} else {
this.showErrorMessage(
component.element,
'Network error. Please check your connection and reload the page.'
);
}
}
reloadComponent(component) {
// Reload component from server with current state
fetch(`/live-component/${component.id}/reload`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ state: component.state })
})
.then(response => response.json())
.then(data => {
if (data.success) {
this.updateComponent(component, data);
} else {
this.showReloadPageMessage(component);
}
})
.catch(() => {
this.showReloadPageMessage(component);
});
}
showRetryButton(component) {
const retryHtml = `
<div class="lc-error-retry">
<p>Action failed. Would you like to try again?</p>
<button onclick="this.closest('.lc-error-retry').remove();
window.liveComponents.retry('${component.id}')">
Retry
</button>
</div>
`;
component.element.insertAdjacentHTML('beforeend', retryHtml);
}
retry(componentId) {
const component = this.components.get(componentId);
if (component && component.lastAction) {
this.handleAction(
component.id,
component.lastAction,
component.lastParams
);
}
}
}
```
## Error Codes
### Component Errors (COMP_*)
- `COMP_NOT_FOUND` - Component name not registered
- `COMP_INSTANTIATION_FAILED` - Failed to create component instance
- `COMP_INVALID_ID` - Invalid component ID format
### Action Errors (ACTION_*)
- `ACTION_NOT_FOUND` - Action method does not exist
- `ACTION_NOT_CALLABLE` - Action is not public or is static
- `ACTION_INVALID_PARAMS` - Invalid action parameters
- `ACTION_EXECUTION_FAILED` - Action threw exception
- `ACTION_INVALID_RETURN` - Action did not return ComponentData
### State Errors (STATE_*)
- `STATE_INVALID_FORMAT` - State is not valid JSON/array
- `STATE_DESERIALIZATION_FAILED` - Failed to parse state
- `STATE_VALIDATION_FAILED` - State validation error
- `STATE_VERSION_CONFLICT` - Optimistic locking conflict
### Rendering Errors (RENDER_*)
- `RENDER_TEMPLATE_NOT_FOUND` - Template file does not exist
- `RENDER_TEMPLATE_SYNTAX` - Template has syntax error
- `RENDER_EXCEPTION` - Rendering threw exception
- `RENDER_CACHE_WRITE_FAILED` - Failed to write to cache
## Testing Error Recovery
### Unit Tests
```php
it('suggests similar component names on not found error', function () {
$registry = new ComponentRegistry(...);
try {
$registry->resolve(ComponentId::fromString('conter:test'), null);
expect(false)->toBeTrue(); // Should have thrown
} catch (FrameworkException $e) {
expect($e->getData()['suggestions'])->toContain('counter');
}
});
it('lists available actions on unknown action error', function () {
$handler = new LiveComponentHandler(...);
$component = new CounterComponent(...);
try {
$handler->handle($component, 'unknownAction', ActionParameters::empty());
expect(false)->toBeTrue();
} catch (FrameworkException $e) {
expect($e->getData()['available_actions'])->toContain('increment');
expect($e->getData()['available_actions'])->toContain('decrement');
}
});
```
### Integration Tests
```php
it('recovers from state deserialization error', function () {
$response = $this->post('/live-component/counter:test', [
'action' => 'increment',
'state' => 'invalid json' // Invalid state
]);
expect($response->status)->toBe(Status::BAD_REQUEST);
expect($response->json('error'))->toBe('Invalid state format');
expect($response->json('recovery'))->toContain('reload');
});
it('returns state on rendering error', function () {
// Component that fails rendering but action succeeded
$response = $this->post('/live-component/broken:test', [
'action' => 'increment',
'state' => ['count' => 5]
]);
expect($response->json('success'))->toBeFalse();
expect($response->json('state'))->toBeArray();
expect($response->json('recovery'))->toBe('Try again');
});
```
## Monitoring
### Error Metrics
- `livecomponent.error.rate` - Errors per second by type
- `livecomponent.error.component` - Errors by component name
- `livecomponent.error.action` - Errors by action name
- `livecomponent.error.recovery_success_rate` - Recovery success rate
### Alerts
- Error rate > 5% - Warning
- Error rate > 10% - Critical
- Specific component error rate > 20% - Component issue
- Network error rate > 10% - Infrastructure issue
## Summary
**Implemented:**
- Component not found suggestions
- Available actions listing
- Safe state deserialization
- Lifecycle hook error handling
- Client-side retry logic
⚠️ **To Implement:**
- Error code enum (FrameworkException integration)
- Client-side error recovery UI
- Comprehensive error testing
- Error monitoring dashboard
📋 **Phase 1 Requirements:**
- Implement all error codes
- Add client-side error boundaries
- Comprehensive error tests
- Error monitoring integration