- 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.
18 KiB
18 KiB
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:
public function resolve(ComponentId $componentId, ?ComponentData $state = null): LiveComponentContract
{
if (!$className) {
throw new \InvalidArgumentException("Unknown component: {$componentName}");
}
}
Improved Error Recovery:
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:
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:
#[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:
// 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:
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 registeredCOMP_INSTANTIATION_FAILED- Failed to create component instanceCOMP_INVALID_ID- Invalid component ID format
Action Errors (ACTION_*)
ACTION_NOT_FOUND- Action method does not existACTION_NOT_CALLABLE- Action is not public or is staticACTION_INVALID_PARAMS- Invalid action parametersACTION_EXECUTION_FAILED- Action threw exceptionACTION_INVALID_RETURN- Action did not return ComponentData
State Errors (STATE_*)
STATE_INVALID_FORMAT- State is not valid JSON/arraySTATE_DESERIALIZATION_FAILED- Failed to parse stateSTATE_VALIDATION_FAILED- State validation errorSTATE_VERSION_CONFLICT- Optimistic locking conflict
Rendering Errors (RENDER_*)
RENDER_TEMPLATE_NOT_FOUND- Template file does not existRENDER_TEMPLATE_SYNTAX- Template has syntax errorRENDER_EXCEPTION- Rendering threw exceptionRENDER_CACHE_WRITE_FAILED- Failed to write to cache
Testing Error Recovery
Unit Tests
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
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 typelivecomponent.error.component- Errors by component namelivecomponent.error.action- Errors by action namelivecomponent.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