From 5980c36ae4a20e9e16c12620e90aafdd6ba9b500 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 6 May 2026 12:42:25 +0200 Subject: [PATCH] refactor: SentryEventScrubber static + config array notation The scrubber is fully stateless. Container-resolution per event was overhead without value, the closure indirection polluted the config layer with executable logic, and stack traces showed an anonymous closure frame instead of the class name. - SentryEventScrubber::scrub() and its private helpers all become static methods. No instance fields, so the change is mechanical. - config/sentry.php before_send switches from a closure that calls app() to PHP array-callable notation [Class, method]. Symfony OptionsResolver accepts array-callables for static methods. - PiiScrubbingTest swaps (new SentryEventScrubber)->scrub(...) for SentryEventScrubber::scrub(...). Semantics unchanged. Tests 1537 unchanged. Larastan and Pint clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Observability/SentryEventScrubber.php | 16 ++++++++-------- api/config/sentry.php | 8 ++++++-- .../Feature/Observability/PiiScrubbingTest.php | 8 ++++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/api/app/Services/Observability/SentryEventScrubber.php b/api/app/Services/Observability/SentryEventScrubber.php index 1671f5b2..4e0f3c6c 100644 --- a/api/app/Services/Observability/SentryEventScrubber.php +++ b/api/app/Services/Observability/SentryEventScrubber.php @@ -44,7 +44,7 @@ final class SentryEventScrubber private const MAX_DEPTH = 10; - public function scrub(Event $event, ?EventHint $hint = null): ?Event + public static function scrub(Event $event, ?EventHint $hint = null): ?Event { if ($hint?->exception instanceof HttpException && $hint->exception->getStatusCode() < 500) { return null; @@ -54,9 +54,9 @@ final class SentryEventScrubber if ($request !== []) { $event->setRequest(array_merge($request, [ - 'data' => $this->scrubBody($request['data'] ?? []), - 'headers' => $this->scrubHeaders($request['headers'] ?? []), - 'query_string' => $this->scrubQueryString($request['query_string'] ?? ''), + 'data' => self::scrubBody($request['data'] ?? []), + 'headers' => self::scrubHeaders($request['headers'] ?? []), + 'query_string' => self::scrubQueryString($request['query_string'] ?? ''), 'cookies' => self::SCRUBBED, ])); } @@ -68,7 +68,7 @@ final class SentryEventScrubber * @param mixed $data * @return mixed */ - private function scrubBody($data, int $depth = 0) + private static function scrubBody($data, int $depth = 0) { if (! is_array($data)) { return $data; @@ -92,7 +92,7 @@ final class SentryEventScrubber } if (is_array($value)) { - $data[$key] = $this->scrubBody($value, $depth + 1); + $data[$key] = self::scrubBody($value, $depth + 1); } } @@ -103,7 +103,7 @@ final class SentryEventScrubber * @param array|string $headers * @return array|string */ - private function scrubHeaders($headers) + private static function scrubHeaders($headers) { if (! is_array($headers)) { return $headers; @@ -118,7 +118,7 @@ final class SentryEventScrubber return $headers; } - private function scrubQueryString(string $queryString): string + private static function scrubQueryString(string $queryString): string { if ($queryString === '') { return ''; diff --git a/api/config/sentry.php b/api/config/sentry.php index 4dbb31be..4e249727 100644 --- a/api/config/sentry.php +++ b/api/config/sentry.php @@ -25,8 +25,12 @@ return [ // When left empty or `null` the Laravel environment will be used (usually discovered from `APP_ENV` in your `.env`) 'environment' => env('SENTRY_ENVIRONMENT', env('APP_ENV')), - // Crewli observability scrubber (RFC-WS-7 §3.7). - 'before_send' => static fn (\Sentry\Event $event, ?\Sentry\EventHint $hint = null): ?\Sentry\Event => app(\App\Services\Observability\SentryEventScrubber::class)->scrub($event, $hint), + // RFC-WS-7 §3.7 — stateless static method + array notation. + // Configuration is declarative (a reference, not executable logic); + // container-resolution per event would be overhead without value for + // stateless scrubbing, and stack traces show the class name instead of + // an anonymous closure frame. + 'before_send' => [\App\Services\Observability\SentryEventScrubber::class, 'scrub'], // Errors-only — RFC §2 amendment B explicitly excludes performance tracing. // Force traces/profiles off regardless of env. diff --git a/api/tests/Feature/Observability/PiiScrubbingTest.php b/api/tests/Feature/Observability/PiiScrubbingTest.php index 8cdc2896..7207518e 100644 --- a/api/tests/Feature/Observability/PiiScrubbingTest.php +++ b/api/tests/Feature/Observability/PiiScrubbingTest.php @@ -25,7 +25,7 @@ final class PiiScrubbingTest extends TestCase $event = Event::createEvent(); $event->setRequest($request); - return (new SentryEventScrubber)->scrub($event, $hint); + return SentryEventScrubber::scrub($event, $hint); } public function test_password_in_request_body_is_scrubbed(): void @@ -171,7 +171,7 @@ final class PiiScrubbingTest extends TestCase $event = Event::createEvent(); $hint = EventHint::fromArray(['exception' => new NotFoundHttpException]); - $this->assertNull((new SentryEventScrubber)->scrub($event, $hint)); + $this->assertNull(SentryEventScrubber::scrub($event, $hint)); } public function test_http_exception_500_is_captured(): void @@ -179,7 +179,7 @@ final class PiiScrubbingTest extends TestCase $event = Event::createEvent(); $hint = EventHint::fromArray(['exception' => new HttpException(500, 'boom')]); - $this->assertNotNull((new SentryEventScrubber)->scrub($event, $hint)); + $this->assertNotNull(SentryEventScrubber::scrub($event, $hint)); } public function test_throwable_from_controller_is_captured(): void @@ -187,7 +187,7 @@ final class PiiScrubbingTest extends TestCase $event = Event::createEvent(); $hint = EventHint::fromArray(['exception' => new RuntimeException('programmer error')]); - $this->assertNotNull((new SentryEventScrubber)->scrub($event, $hint)); + $this->assertNotNull(SentryEventScrubber::scrub($event, $hint)); } public function test_form_values_replacement_blocks_attempts_to_smuggle_pii(): void