From ff46b26cc927135475a26db699e678520b76a611 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 6 Oct 2026 13:39:50 +0200 Subject: [PATCH 1/2] feat: remove global event processors --- src/Integration/EnvironmentIntegration.php | 3 +- .../FrameContextifierIntegration.php | 3 +- src/Integration/ModulesIntegration.php | 3 +- src/Integration/RequestIntegration.php | 3 +- src/Integration/TransactionIntegration.php | 3 +- src/State/MergedScope.php | 2 +- src/State/Scope.php | 20 ------ tests/ClientTest.php | 4 +- .../IntegrationEventProcessorTest.php | 69 +++++++++++++++++++ tests/SentrySdkExtension.php | 9 --- tests/SentrySdkTest.php | 18 +++++ tests/State/ScopeTest.php | 34 +++++++-- 12 files changed, 122 insertions(+), 49 deletions(-) create mode 100644 tests/Integration/IntegrationEventProcessorTest.php diff --git a/src/Integration/EnvironmentIntegration.php b/src/Integration/EnvironmentIntegration.php index adf4f6a53..f1c0894d4 100644 --- a/src/Integration/EnvironmentIntegration.php +++ b/src/Integration/EnvironmentIntegration.php @@ -8,7 +8,6 @@ use Sentry\Context\RuntimeContext; use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; use Sentry\Util\PHPVersion; /** @@ -23,7 +22,7 @@ final class EnvironmentIntegration implements IntegrationInterface */ public function setupOnce(): void { - Scope::addGlobalEventProcessor(static function (Event $event): Event { + SentrySdk::getGlobalScope()->addEventProcessor(static function (Event $event): Event { $integration = SentrySdk::getClient()->getIntegration(self::class); if ($integration !== null) { diff --git a/src/Integration/FrameContextifierIntegration.php b/src/Integration/FrameContextifierIntegration.php index b7ea13b40..56022acdd 100644 --- a/src/Integration/FrameContextifierIntegration.php +++ b/src/Integration/FrameContextifierIntegration.php @@ -10,7 +10,6 @@ use Sentry\Frame; use Sentry\SentrySdk; use Sentry\Stacktrace; -use Sentry\State\Scope; /** * This integration reads excerpts of code around the line that originated an @@ -40,7 +39,7 @@ public function __construct(?LoggerInterface $logger = null) */ public function setupOnce(): void { - Scope::addGlobalEventProcessor(static function (Event $event): Event { + SentrySdk::getGlobalScope()->addEventProcessor(static function (Event $event): Event { $client = SentrySdk::getClient(); $maxContextLines = $client->getOptions()->getContextLines(); diff --git a/src/Integration/ModulesIntegration.php b/src/Integration/ModulesIntegration.php index 9e5ee8c16..dc49d1968 100644 --- a/src/Integration/ModulesIntegration.php +++ b/src/Integration/ModulesIntegration.php @@ -7,7 +7,6 @@ use Composer\InstalledVersions; use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; /** * This integration logs with the event details all the versions of the packages @@ -25,7 +24,7 @@ final class ModulesIntegration implements IntegrationInterface */ public function setupOnce(): void { - Scope::addGlobalEventProcessor(static function (Event $event): Event { + SentrySdk::getGlobalScope()->addEventProcessor(static function (Event $event): Event { $integration = SentrySdk::getClient()->getIntegration(self::class); if ($integration !== null) { diff --git a/src/Integration/RequestIntegration.php b/src/Integration/RequestIntegration.php index 6190743a3..eb23d756d 100644 --- a/src/Integration/RequestIntegration.php +++ b/src/Integration/RequestIntegration.php @@ -11,7 +11,6 @@ use Sentry\Options; use Sentry\OptionsResolver; use Sentry\SentrySdk; -use Sentry\State\Scope; use Sentry\UserDataBag; use Sentry\Util\JSON; @@ -103,7 +102,7 @@ public function __construct(?RequestFetcherInterface $requestFetcher = null, arr */ public function setupOnce(): void { - Scope::addGlobalEventProcessor(function (Event $event): Event { + SentrySdk::getGlobalScope()->addEventProcessor(function (Event $event): Event { $client = SentrySdk::getClient(); $integration = $client->getIntegration(self::class); diff --git a/src/Integration/TransactionIntegration.php b/src/Integration/TransactionIntegration.php index 13c7bfe51..fe64a98b2 100644 --- a/src/Integration/TransactionIntegration.php +++ b/src/Integration/TransactionIntegration.php @@ -7,7 +7,6 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\SentrySdk; -use Sentry\State\Scope; /** * This integration sets the `transaction` attribute of the event to the value @@ -23,7 +22,7 @@ final class TransactionIntegration implements IntegrationInterface */ public function setupOnce(): void { - Scope::addGlobalEventProcessor(static function (Event $event, EventHint $hint): Event { + SentrySdk::getGlobalScope()->addEventProcessor(static function (Event $event, EventHint $hint): Event { $integration = SentrySdk::getClient()->getIntegration(self::class); if ($integration === null) { diff --git a/src/State/MergedScope.php b/src/State/MergedScope.php index 439b5c1d0..4f9d5f37c 100644 --- a/src/State/MergedScope.php +++ b/src/State/MergedScope.php @@ -136,7 +136,7 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op } } - foreach (array_merge(parent::$globalEventProcessors, $this->scopeData->getEventProcessors()) as $processor) { + foreach ($this->scopeData->getEventProcessors() as $processor) { try { $event = $processor($event, $hint); } catch (\Throwable $exception) { diff --git a/src/State/Scope.php b/src/State/Scope.php index 124698e9d..7b8c66807 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -6,8 +6,6 @@ use Sentry\Breadcrumb; use Sentry\ClientInterface; -use Sentry\Event; -use Sentry\EventHint; use Sentry\NoOpClient; use Sentry\UserDataBag; @@ -31,13 +29,6 @@ abstract class Scope */ protected $scopeData; - /** - * @var callable[] List of event processors - * - * @phpstan-var array - */ - protected static $globalEventProcessors = []; - /** * @var callable|null */ @@ -75,17 +66,6 @@ public function getBreadcrumbs(): array return $this->scopeData->getBreadcrumbs(); } - /** - * Adds a new event processor that will be called after {@see MergedScope::applyToEvent} - * finished its work. - * - * @param callable $eventProcessor The event processor - */ - public static function addGlobalEventProcessor(callable $eventProcessor): void - { - self::$globalEventProcessors[] = $eventProcessor; - } - public static function registerExternalPropagationContext(callable $callback): void { self::$externalPropagationContextCallback = $callback; diff --git a/tests/ClientTest.php b/tests/ClientTest.php index b3f1b4785..9fd13f21f 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -17,11 +17,11 @@ use Sentry\Frame; use Sentry\Integration\IntegrationInterface; use Sentry\Options; +use Sentry\SentrySdk; use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\Severity; use Sentry\Stacktrace; use Sentry\State\IsolationScope; -use Sentry\State\Scope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; @@ -50,7 +50,7 @@ public function __construct(bool &$integrationCalled) public function setupOnce(): void { - Scope::addGlobalEventProcessor(function (): ?Event { + SentrySdk::getGlobalScope()->addEventProcessor(function (): ?Event { $this->integrationCalled = true; return null; diff --git a/tests/Integration/IntegrationEventProcessorTest.php b/tests/Integration/IntegrationEventProcessorTest.php new file mode 100644 index 000000000..fdbfb0d9b --- /dev/null +++ b/tests/Integration/IntegrationEventProcessorTest.php @@ -0,0 +1,69 @@ +createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options()); + $client->method('getIntegration') + ->willReturnCallback(static function () use (&$getIntegrationCalls): ?IntegrationInterface { + ++$getIntegrationCalls; + + return null; + }); + + SentrySdk::init($client); + + $integration->setupOnce(); + + $event = (new GlobalScope())->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(0, $getIntegrationCalls); + + $event = SentrySdk::getGlobalScope()->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(1, $getIntegrationCalls); + } + + public static function integrationDataProvider(): \Generator + { + yield [new EnvironmentIntegration()]; + + yield [new FrameContextifierIntegration()]; + + yield [new ModulesIntegration()]; + + yield [new RequestIntegration()]; + + yield [new TransactionIntegration()]; + } +} diff --git a/tests/SentrySdkExtension.php b/tests/SentrySdkExtension.php index 3c4503a59..d4e676251 100644 --- a/tests/SentrySdkExtension.php +++ b/tests/SentrySdkExtension.php @@ -42,15 +42,6 @@ public function executeBeforeTest(string $test): void StubTransport::$events = []; - $reflectionProperty = new \ReflectionProperty(Scope::class, 'globalEventProcessors'); - if (\PHP_VERSION_ID < 80100) { - $reflectionProperty->setAccessible(true); - } - $reflectionProperty->setValue(null, []); - if (\PHP_VERSION_ID < 80100) { - $reflectionProperty->setAccessible(false); - } - $reflectionProperty = new \ReflectionProperty(Scope::class, 'externalPropagationContextCallback'); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(true); diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index 3a5b8a301..9bd7fe8a8 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -122,6 +122,24 @@ public function testInitDoesNotResetGlobalScope(): void $this->assertSame(['baseline' => 'yes'], $event->getTags()); } + public function testInitKeepsGlobalScopeEventProcessors(): void + { + $processorCalled = false; + + SentrySdk::getGlobalScope()->addEventProcessor(static function (Event $event) use (&$processorCalled): Event { + $processorCalled = true; + + return $event; + }); + + SentrySdk::init(); + + $event = SentrySdk::getGlobalScope()->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertTrue($processorCalled); + } + public function testStartAndEndContextIsolateScopeData(): void { SentrySdk::init(); diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 8a73af96f..d7f0cc2c1 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -552,6 +552,32 @@ public function testClear(): void $this->assertSame($client, $scope->getClient()); } + public function testClearKeepsEventProcessors(): void + { + $calls = []; + + $globalScope = new GlobalScope(); + $globalScope->addEventProcessor(static function (Event $event) use (&$calls): Event { + $calls[] = 'global'; + + return $event; + }); + $globalScope->clear(); + + $isolationScope = new IsolationScope(); + $isolationScope->addEventProcessor(static function (Event $event) use (&$calls): Event { + $calls[] = 'isolation'; + + return $event; + }); + $isolationScope->clear(); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['global', 'isolation'], $calls); + } + public function testApplyToEvent(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); @@ -950,12 +976,6 @@ public function testMergeScopesKeepsProcessorOrder(): void { $calls = []; - Scope::addGlobalEventProcessor(static function (Event $event) use (&$calls): ?Event { - $calls[] = 'static'; - - return $event; - }); - $globalScope = new GlobalScope(); $globalScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { $calls[] = 'global'; @@ -973,7 +993,7 @@ public function testMergeScopesKeepsProcessorOrder(): void $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); - $this->assertSame(['static', 'global', 'isolation'], $calls); + $this->assertSame(['global', 'isolation'], $calls); } /** From d9aba5f1d4dd518fbaaeaf390a0a3f166104792a Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 6 Oct 2026 14:39:21 +0200 Subject: [PATCH 2/2] don't apply event processors unconditionally --- src/EventType.php | 14 +++++++++++++ src/State/MergedScope.php | 32 ++++++++++++++++------------ tests/State/ScopeTest.php | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/EventType.php b/src/EventType.php index c5502e35e..b247c52a0 100644 --- a/src/EventType.php +++ b/src/EventType.php @@ -86,6 +86,20 @@ public function requiresEventId(): bool } } + /** + * Specifies whether event processors should run for this type of event. + */ + public function requiresEventProcessors(): bool + { + switch ($this) { + case self::event(): + case self::transaction(): + return true; + default: + return false; + } + } + /** * Returns false if rate limiting should not be applied. */ diff --git a/src/State/MergedScope.php b/src/State/MergedScope.php index 4f9d5f37c..9903b5683 100644 --- a/src/State/MergedScope.php +++ b/src/State/MergedScope.php @@ -136,23 +136,29 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op } } - foreach ($this->scopeData->getEventProcessors() as $processor) { - try { - $event = $processor($event, $hint); - } catch (\Throwable $exception) { - if ($options !== null) { - $options->getLoggerOrNullLogger()->error(\sprintf('The event processor failed with exception: "%s".', $exception->getMessage())); + if ($event->getType()->requiresEventProcessors()) { + foreach ($this->scopeData->getEventProcessors() as $processor) { + try { + $event = $processor($event, $hint); + } catch (\Throwable $exception) { + if ($options !== null) { + $options->getLoggerOrNullLogger()->error(\sprintf('The event processor failed with exception: "%s".', $exception->getMessage())); + } + + return null; } - return null; - } + if ($event === null) { + return null; + } - if ($event === null) { - return null; - } + if (!$event instanceof Event) { + if ($options !== null) { + $options->getLoggerOrNullLogger()->debug(\sprintf('The event processor must return null or an instance of the %s class. The event will be discarded.', Event::class)); + } - if (!$event instanceof Event) { - throw new \InvalidArgumentException(\sprintf('The event processor must return null or an instance of the %s class', Event::class)); + return null; + } } } diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index d7f0cc2c1..0b9c32b0c 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -502,6 +502,50 @@ public function testEventProcessorExceptionDropsEventAndIsLogged(): void ]], StubLogger::$logs); } + public function testEventProcessorReturningInvalidValueDropsEventAndIsLogged(): void + { + StubLogger::$logs = []; + $scope = new IsolationScope(); + $scope->addEventProcessor(static function (): string { + return 'foo'; + }); + + $this->assertNull($this->applyScope($scope, Event::createEvent(), null, new Options([ + 'logger' => StubLogger::getInstance(), + ]))); + $this->assertSame([[ + 'level' => 'debug', + 'message' => 'The event processor must return null or an instance of the Sentry\Event class. The event will be discarded.', + 'context' => [], + ]], StubLogger::$logs); + } + + /** + * @dataProvider eventProcessorsRunForEventTypeDataProvider + */ + public function testEventProcessorsOnlyRunForErrorsAndTransactions(Event $event, bool $expectedProcessorCalled): void + { + $processorCalled = false; + $scope = new IsolationScope(); + $scope->addEventProcessor(static function (Event $event) use (&$processorCalled): Event { + $processorCalled = true; + + return $event; + }); + + $this->assertNotNull($this->applyScope($scope, $event)); + $this->assertSame($expectedProcessorCalled, $processorCalled); + } + + public static function eventProcessorsRunForEventTypeDataProvider(): \Generator + { + yield 'event' => [Event::createEvent(), true]; + yield 'transaction' => [Event::createTransaction(), true]; + yield 'check-in' => [Event::createCheckIn(), false]; + yield 'logs' => [Event::createLogs(), false]; + yield 'metrics' => [Event::createMetrics(), false]; + } + public function testEventProcessorReceivesTheEventAndEventHint(): void { $event = Event::createEvent();