232 lines
9.5 KiB
PHP
232 lines
9.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\ErrorReporting;
|
|
|
|
use App\Framework\Config\Environment;
|
|
use App\Framework\Config\EnvironmentType;
|
|
use App\Framework\Config\EnvKey;
|
|
|
|
/**
|
|
* Error Reporting Configuration that adapts to environment
|
|
*
|
|
* Provides type-safe configuration for error reporting system
|
|
* with environment-specific defaults (dev/staging/production).
|
|
*
|
|
* Features:
|
|
* - Environment-based factory methods
|
|
* - Type-safe configuration properties
|
|
* - Framework-compliant readonly Value Object
|
|
* - Sensible defaults per environment
|
|
*/
|
|
final readonly class ErrorReportingConfig
|
|
{
|
|
/**
|
|
* @param bool $enabled Enable/disable error reporting globally
|
|
* @param bool $asyncProcessing Process error reports asynchronously via queue
|
|
* @param string[] $filterLevels Only report these log levels (empty = all levels)
|
|
* @param string[] $excludedExceptionTypes Exception types to exclude from reporting
|
|
* @param bool $captureRequestContext Capture HTTP request context
|
|
* @param bool $captureUserContext Capture user/session context
|
|
* @param bool $captureStackTraces Include full stack traces in reports
|
|
* @param int $maxStackTraceDepth Maximum stack trace depth
|
|
* @param bool $sanitizeSensitiveData Sanitize passwords, tokens, etc.
|
|
* @param int $samplingRate Sampling rate for high-volume errors (1-100, 100 = all)
|
|
* @param int $maxReportsPerMinute Rate limit for error reports
|
|
* @param bool $enableAnalytics Enable error analytics and anomaly detection
|
|
* @param int $analyticsRetentionDays Days to retain error reports for analytics
|
|
*/
|
|
public function __construct(
|
|
public bool $enabled = true,
|
|
public bool $asyncProcessing = true,
|
|
public array $filterLevels = [],
|
|
public array $excludedExceptionTypes = [],
|
|
public bool $captureRequestContext = true,
|
|
public bool $captureUserContext = true,
|
|
public bool $captureStackTraces = true,
|
|
public int $maxStackTraceDepth = 20,
|
|
public bool $sanitizeSensitiveData = true,
|
|
public int $samplingRate = 100,
|
|
public int $maxReportsPerMinute = 60,
|
|
public bool $enableAnalytics = true,
|
|
public int $analyticsRetentionDays = 30
|
|
) {
|
|
// Validation
|
|
if ($samplingRate < 1 || $samplingRate > 100) {
|
|
throw new \InvalidArgumentException('Sampling rate must be between 1 and 100');
|
|
}
|
|
|
|
if ($maxStackTraceDepth < 1) {
|
|
throw new \InvalidArgumentException('Max stack trace depth must be at least 1');
|
|
}
|
|
|
|
if ($analyticsRetentionDays < 1) {
|
|
throw new \InvalidArgumentException('Analytics retention days must be at least 1');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create configuration from environment
|
|
*/
|
|
public static function fromEnvironment(Environment $env): self
|
|
{
|
|
$appEnv = $env->getString(EnvKey::APP_ENV, 'production');
|
|
|
|
$environmentType = match (strtolower($appEnv)) {
|
|
'production', 'prod' => EnvironmentType::PROD,
|
|
'staging', 'stage' => EnvironmentType::STAGING,
|
|
'development', 'dev', 'local' => EnvironmentType::DEV,
|
|
default => EnvironmentType::PROD
|
|
};
|
|
|
|
return self::forEnvironment($environmentType, $env);
|
|
}
|
|
|
|
/**
|
|
* Create environment-specific configuration
|
|
*/
|
|
public static function forEnvironment(EnvironmentType $environment, Environment $env): self
|
|
{
|
|
return match ($environment) {
|
|
EnvironmentType::PROD => self::production($env),
|
|
EnvironmentType::STAGING => self::staging($env),
|
|
EnvironmentType::DEV => self::development($env),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Production configuration - strict, sanitized, sampled
|
|
*/
|
|
private static function production(Environment $env): self
|
|
{
|
|
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
|
|
$excludedTypes = $env->getString('ERROR_REPORTING_EXCLUDED_TYPES', '');
|
|
|
|
// Production default: only error, critical, alert, emergency
|
|
$defaultFilterLevels = ['error', 'critical', 'alert', 'emergency'];
|
|
|
|
return new self(
|
|
enabled: $env->getBool('ERROR_REPORTING_ENABLED', true),
|
|
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
|
|
filterLevels: $filterLevels !== ''
|
|
? explode(',', $filterLevels)
|
|
: $defaultFilterLevels,
|
|
excludedExceptionTypes: $excludedTypes !== ''
|
|
? explode(',', $excludedTypes)
|
|
: [],
|
|
captureRequestContext: $env->getBool('ERROR_REPORTING_CAPTURE_REQUEST', true),
|
|
captureUserContext: $env->getBool('ERROR_REPORTING_CAPTURE_USER', true),
|
|
captureStackTraces: $env->getBool('ERROR_REPORTING_CAPTURE_STACK_TRACES', true),
|
|
maxStackTraceDepth: $env->getInt('ERROR_REPORTING_MAX_STACK_DEPTH', 15),
|
|
sanitizeSensitiveData: $env->getBool('ERROR_REPORTING_SANITIZE', true),
|
|
samplingRate: $env->getInt('ERROR_REPORTING_SAMPLING_RATE', 100),
|
|
maxReportsPerMinute: $env->getInt('ERROR_REPORTING_MAX_PER_MINUTE', 30),
|
|
enableAnalytics: $env->getBool('ERROR_REPORTING_ANALYTICS', true),
|
|
analyticsRetentionDays: $env->getInt('ERROR_REPORTING_RETENTION_DAYS', 30)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Staging configuration - more verbose, less restrictive
|
|
*/
|
|
private static function staging(Environment $env): self
|
|
{
|
|
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
|
|
$excludedTypes = $env->getString('ERROR_REPORTING_EXCLUDED_TYPES', '');
|
|
|
|
// Staging default: warning and above
|
|
$defaultFilterLevels = ['warning', 'error', 'critical', 'alert', 'emergency'];
|
|
|
|
return new self(
|
|
enabled: $env->getBool('ERROR_REPORTING_ENABLED', true),
|
|
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
|
|
filterLevels: $filterLevels !== ''
|
|
? explode(',', $filterLevels)
|
|
: $defaultFilterLevels,
|
|
excludedExceptionTypes: $excludedTypes !== ''
|
|
? explode(',', $excludedTypes)
|
|
: [],
|
|
captureRequestContext: $env->getBool('ERROR_REPORTING_CAPTURE_REQUEST', true),
|
|
captureUserContext: $env->getBool('ERROR_REPORTING_CAPTURE_USER', true),
|
|
captureStackTraces: $env->getBool('ERROR_REPORTING_CAPTURE_STACK_TRACES', true),
|
|
maxStackTraceDepth: $env->getInt('ERROR_REPORTING_MAX_STACK_DEPTH', 20),
|
|
sanitizeSensitiveData: $env->getBool('ERROR_REPORTING_SANITIZE', true),
|
|
samplingRate: $env->getInt('ERROR_REPORTING_SAMPLING_RATE', 100),
|
|
maxReportsPerMinute: $env->getInt('ERROR_REPORTING_MAX_PER_MINUTE', 60),
|
|
enableAnalytics: $env->getBool('ERROR_REPORTING_ANALYTICS', true),
|
|
analyticsRetentionDays: $env->getInt('ERROR_REPORTING_RETENTION_DAYS', 14)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Development configuration - verbose, everything enabled
|
|
*/
|
|
private static function development(Environment $env): self
|
|
{
|
|
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
|
|
$excludedTypes = $env->getString('ERROR_REPORTING_EXCLUDED_TYPES', '');
|
|
|
|
return new self(
|
|
enabled: $env->getBool('ERROR_REPORTING_ENABLED', true),
|
|
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', false), // Sync in dev for debugging
|
|
filterLevels: $filterLevels !== '' ? explode(',', $filterLevels) : [], // All levels
|
|
excludedExceptionTypes: $excludedTypes !== '' ? explode(',', $excludedTypes) : [],
|
|
captureRequestContext: $env->getBool('ERROR_REPORTING_CAPTURE_REQUEST', true),
|
|
captureUserContext: $env->getBool('ERROR_REPORTING_CAPTURE_USER', true),
|
|
captureStackTraces: $env->getBool('ERROR_REPORTING_CAPTURE_STACK_TRACES', true),
|
|
maxStackTraceDepth: $env->getInt('ERROR_REPORTING_MAX_STACK_DEPTH', 30), // Deep traces
|
|
sanitizeSensitiveData: $env->getBool('ERROR_REPORTING_SANITIZE', false), // See real data
|
|
samplingRate: $env->getInt('ERROR_REPORTING_SAMPLING_RATE', 100), // All errors
|
|
maxReportsPerMinute: $env->getInt('ERROR_REPORTING_MAX_PER_MINUTE', 1000), // No limit
|
|
enableAnalytics: $env->getBool('ERROR_REPORTING_ANALYTICS', true),
|
|
analyticsRetentionDays: $env->getInt('ERROR_REPORTING_RETENTION_DAYS', 7)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if error level should be reported
|
|
*/
|
|
public function shouldReportLevel(string $level): bool
|
|
{
|
|
// Empty filter = all levels
|
|
if (empty($this->filterLevels)) {
|
|
return true;
|
|
}
|
|
|
|
return in_array(strtolower($level), array_map('strtolower', $this->filterLevels), true);
|
|
}
|
|
|
|
/**
|
|
* Check if exception type should be reported
|
|
*/
|
|
public function shouldReportException(\Throwable $exception): bool
|
|
{
|
|
if (empty($this->excludedExceptionTypes)) {
|
|
return true;
|
|
}
|
|
|
|
$exceptionClass = get_class($exception);
|
|
|
|
foreach ($this->excludedExceptionTypes as $excludedType) {
|
|
if ($exceptionClass === $excludedType || is_subclass_of($exceptionClass, $excludedType)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if error should be sampled (for high-volume scenarios)
|
|
*/
|
|
public function shouldSample(): bool
|
|
{
|
|
if ($this->samplingRate === 100) {
|
|
return true;
|
|
}
|
|
|
|
return random_int(1, 100) <= $this->samplingRate;
|
|
}
|
|
}
|