diff --git a/src/Sentry/Laravel/Integration.php b/src/Sentry/Laravel/Integration.php index e74ad703..f556d738 100644 --- a/src/Sentry/Laravel/Integration.php +++ b/src/Sentry/Laravel/Integration.php @@ -2,16 +2,13 @@ namespace Sentry\Laravel; -use Illuminate\Database\Eloquent\MissingAttributeException; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\LazyLoadingViolationException; use Illuminate\Routing\Route; use Sentry\EventHint; use Sentry\EventId; use Sentry\ExceptionMechanism; -use Sentry\Laravel\Features\Concerns\ResolvesEventOrigin; +use Sentry\Laravel\Integration\ModelViolations\LazyLoadingModelViolationReporter; +use Sentry\Laravel\Integration\ModelViolations\MissingAttributeModelViolationReporter; use Sentry\SentrySdk; -use Sentry\Severity; use Sentry\Tracing\TransactionSource; use Throwable; use Sentry\Breadcrumb; @@ -223,46 +220,9 @@ public static function captureUnhandledException(Throwable $throwable): ?EventId * * @return callable */ - public static function missingAttributeViolationReporter(?callable $callback = null): callable + public static function missingAttributeViolationReporter(?callable $callback = null, bool $supressDuplicateReports = true): callable { - return new class($callback) { - use ResolvesEventOrigin; - - /** @var callable|null $callback */ - private $callback; - - public function __construct(?callable $callback) - { - $this->callback = $callback; - } - - public function __invoke(Model $model, string $attribute): void - { - SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($model, $attribute) { - $scope->setContext('violation', [ - 'model' => get_class($model), - 'attribute' => $attribute, - 'origin' => $this->resolveEventOrigin(), - 'kind' => 'missing_attribute', - ]); - - SentrySdk::getCurrentHub()->captureEvent( - tap(Event::createEvent(), static function (Event $event) { - $event->setLevel(Severity::warning()); - }), - EventHint::fromArray([ - 'exception' => new MissingAttributeException($model, $attribute), - 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true), - ]) - ); - }); - - // Forward the violation to the next handler if there is one - if ($this->callback !== null) { - call_user_func($this->callback, $model, $attribute); - } - } - }; + return new MissingAttributeModelViolationReporter($callback, $supressDuplicateReports); } /** @@ -272,52 +232,9 @@ public function __invoke(Model $model, string $attribute): void * * @return callable */ - public static function lazyLoadingViolationReporter(?callable $callback = null): callable + public static function lazyLoadingViolationReporter(?callable $callback = null, bool $supressDuplicateReports = true): callable { - return new class($callback) { - use ResolvesEventOrigin; - - /** @var callable|null $callback */ - private $callback; - - public function __construct(?callable $callback) - { - $this->callback = $callback; - } - - public function __invoke(Model $model, string $relation): void - { - // Laravel uses these checks itself to not throw an exception if the model doesn't exist or was just created - // See: https://github.com/laravel/framework/blob/438d02d3a891ab4d73ffea2c223b5d37947b5e93/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L559-L561 - if (!$model->exists || $model->wasRecentlyCreated) { - return; - } - - SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($model, $relation) { - $scope->setContext('violation', [ - 'model' => get_class($model), - 'relation' => $relation, - 'origin' => $this->resolveEventOrigin(), - 'kind' => 'lazy_loading', - ]); - - SentrySdk::getCurrentHub()->captureEvent( - tap(Event::createEvent(), static function (Event $event) { - $event->setLevel(Severity::warning()); - }), - EventHint::fromArray([ - 'exception' => new LazyLoadingViolationException($model, $relation), - 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true), - ]) - ); - }); - - // Forward the violation to the next handler if there is one - if ($this->callback !== null) { - call_user_func($this->callback, $model, $relation); - } - } - }; + return new LazyLoadingModelViolationReporter($callback, $supressDuplicateReports); } /** diff --git a/src/Sentry/Laravel/Integration/ModelViolations/LazyLoadingModelViolationReporter.php b/src/Sentry/Laravel/Integration/ModelViolations/LazyLoadingModelViolationReporter.php new file mode 100644 index 00000000..1ab38e18 --- /dev/null +++ b/src/Sentry/Laravel/Integration/ModelViolations/LazyLoadingModelViolationReporter.php @@ -0,0 +1,23 @@ + $property, + 'kind' => 'lazy_loading', + ]; + } + + protected function getViolationException(Model $model, string $property): Exception + { + return new LazyLoadingViolationException($model, $property); + } +} diff --git a/src/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.php b/src/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.php new file mode 100644 index 00000000..8bdeb208 --- /dev/null +++ b/src/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.php @@ -0,0 +1,23 @@ + $property, + 'kind' => 'missing_attribute', + ]; + } + + protected function getViolationException(Model $model, string $property): Exception + { + return new MissingAttributeException($model, $property); + } +} diff --git a/src/Sentry/Laravel/Integration/ModelViolations/ModelViolationReporter.php b/src/Sentry/Laravel/Integration/ModelViolations/ModelViolationReporter.php new file mode 100644 index 00000000..88066014 --- /dev/null +++ b/src/Sentry/Laravel/Integration/ModelViolations/ModelViolationReporter.php @@ -0,0 +1,86 @@ + $reportedViolations */ + private $reportedViolations = []; + + public function __construct(?callable $callback, bool $supressDuplicateReports) + { + $this->callback = $callback; + $this->supressDuplicateReports = $supressDuplicateReports; + } + + public function __invoke(Model $model, string $property): void + { + if ($this->hasAlreadyBeenReported($model, $property)) { + return; + } + + $this->markAsReported($model, $property); + + SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($model, $property) { + $scope->setContext('violation', array_merge([ + 'model' => get_class($model), + 'origin' => $this->resolveEventOrigin(), + ], $this->getViolationContext($model, $property))); + + SentrySdk::getCurrentHub()->captureEvent( + tap(Event::createEvent(), static function (Event $event) { + $event->setLevel(Severity::warning()); + }), + EventHint::fromArray([ + 'exception' => $this->getViolationException($model, $property), + 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true), + ]) + ); + }); + + // Forward the violation to the next handler if there is one + if ($this->callback !== null) { + call_user_func($this->callback, $model, $property); + } + } + + abstract protected function getViolationContext(Model $model, string $property): array; + + abstract protected function getViolationException(Model $model, string $property): Exception; + + private function hasAlreadyBeenReported(Model $model, string $property): bool + { + if (!$this->supressDuplicateReports) { + return false; + } + + return array_key_exists(get_class($model) . $property, $this->reportedViolations); + } + + private function markAsReported(Model $model, string $property): void + { + if (!$this->supressDuplicateReports) { + return; + } + + $this->reportedViolations[get_class($model) . $property] = true; + } +}