diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ec9023..462f9b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,85 @@ # Changelog -## Unreleased +## 4.6.0 + +The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.6.0. +This release contains a colorful bouquet of new features. + +### Features + +- Report exceptions to Sentry as unhandled by default [(#674)](https://github.com/getsentry/sentry-symfony/pull/674) + + All unhandled exceptions will now be marked as `handled: false`. You can query for such events on the issues list page, + by searching for `error.handled:false`. + +- Exceptions from messages which will be retried are sent to Sentry as handled [(#674)](https://github.com/getsentry/sentry-symfony/pull/674) + + All unhandled exceptions happening on the message bus will now be unpacked and reported individually. + The `WorkerMessageFailedEvent::willRetry` property is used to determine the `handled` value of the event sent to Sentry. + +- Add `register_error_handler` config option [(#687)](https://github.com/getsentry/sentry-symfony/pull/687) + + With this option, you can disable the global error and exception handlers of the base PHP SDK. + If disabled, only events logged by Monolog will be sent to Sentry. + + ```yaml + sentry: + dsn: '%env(SENTRY_DSN)%' + register_error_listener: false + register_error_handler: false + + monolog: + handlers: + sentry: + type: sentry + level: !php/const Monolog\Logger::ERROR + hub_id: Sentry\State\HubInterface + ``` + +- Add `before_send_transaction` [(#691)](https://github.com/getsentry/sentry-symfony/pull/691) + + Similar to `before_send`, you can now apply additional logic for `transaction` events. + You can mutate the `transaction` event before it is sent to Sentry. If your callback returns `null`, + the event is dropped. + + ```yaml + sentry: + options: + before_send_transaction: 'sentry.callback.before_send_transaction' + + services: + sentry.callback.before_send_transaction: + class: 'App\Service\Sentry' + factory: [ '@App\Service\Sentry', 'getBeforeSendTransaction' ] + ``` + + ```php + >`. + + You may need to update your starred transactions as well as your dashboards due to this change. + +### Bug Fixes + +- Sanatize HTTP client spans [(#690)](https://github.com/getsentry/sentry-symfony/pull/690) ## 4.5.0 (2022-11-28) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2898c4b0..b55d1a0b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,6 @@ If you feel that you can fix or implement it yourself, please read on to learn h - Add tests for your changes to `tests/`. - Run tests and make sure all of them pass. - Submit a pull request, referencing any issues it addresses. -- Make sure to update the `CHANGELOG.md` file below the `Unreleased` heading. We will review your pull request as soon as possible. Thank you for contributing! diff --git a/composer.json b/composer.json index a6cc505c..087e9c77 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "php": "^7.2||^8.0", "jean85/pretty-package-versions": "^1.5 || ^2.0", "sentry/sdk": "^3.3", + "sentry/sentry": "^3.12", "symfony/cache-contracts": "^1.1||^2.4||^3.0", "symfony/config": "^4.4.20||^5.0.11||^6.0", "symfony/console": "^4.4.20||^5.0.11||^6.0", @@ -97,7 +98,7 @@ }, "extra": { "branch-alias": { - "dev-master": "4.4.x-dev", + "dev-master": "4.6.x-dev", "releases/3.2.x": "3.2.x-dev", "releases/2.x": "2.x-dev", "releases/1.x": "1.x-dev" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4dcdcbf9..a463d910 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -20,6 +20,11 @@ parameters: count: 3 path: src/DependencyInjection/SentryExtension.php + - + message: "#^Cannot access offset 'before_send…' on mixed\\.$#" + count: 3 + path: src/DependencyInjection/SentryExtension.php + - message: "#^Cannot access offset 'class_serializers' on mixed\\.$#" count: 3 @@ -52,7 +57,7 @@ parameters: - message: "#^Cannot access offset 'integrations' on mixed\\.$#" - count: 3 + count: 2 path: src/DependencyInjection/SentryExtension.php - @@ -77,7 +82,7 @@ parameters: - message: "#^Parameter \\#1 \\$id of class Symfony\\\\Component\\\\DependencyInjection\\\\Reference constructor expects string, mixed given\\.$#" - count: 5 + count: 6 path: src/DependencyInjection/SentryExtension.php - @@ -280,11 +285,6 @@ parameters: count: 1 path: tests/DependencyInjection/SentryExtensionTest.php - - - message: "#^Cannot access offset 'integrations' on mixed\\.$#" - count: 1 - path: tests/DependencyInjection/SentryExtensionTest.php - - message: "#^Class Symfony\\\\Component\\\\Debug\\\\Exception\\\\FatalErrorException not found\\.$#" count: 1 diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b1c1bdf3..7dab828c 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -37,6 +37,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('If this value is not provided, the SDK will try to read it from the SENTRY_DSN environment variable. If that variable also does not exist, the SDK will just not send any events.') ->end() ->booleanNode('register_error_listener')->defaultTrue()->end() + ->booleanNode('register_error_handler')->defaultTrue()->end() ->scalarNode('logger') ->info('The service ID of the PSR-3 logger used to log messages coming from the SDK client. Be aware that setting the same logger of the application may create a circular loop when an event fails to be sent.') ->defaultNull() @@ -91,6 +92,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->scalarNode('server_name')->end() ->scalarNode('before_send')->end() + ->scalarNode('before_send_transaction')->end() ->arrayNode('tags') ->useAttributeAsKey('name') ->normalizeKeys(false) diff --git a/src/DependencyInjection/SentryExtension.php b/src/DependencyInjection/SentryExtension.php index f87458bc..5e559a16 100644 --- a/src/DependencyInjection/SentryExtension.php +++ b/src/DependencyInjection/SentryExtension.php @@ -19,6 +19,7 @@ use Sentry\SentryBundle\EventListener\TracingConsoleListener; use Sentry\SentryBundle\EventListener\TracingRequestListener; use Sentry\SentryBundle\EventListener\TracingSubRequestListener; +use Sentry\SentryBundle\Integration\IntegrationConfigurator; use Sentry\SentryBundle\SentryBundle; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware; @@ -101,6 +102,10 @@ private function registerConfiguration(ContainerBuilder $container, array $confi $options['before_send'] = new Reference($options['before_send']); } + if (isset($options['before_send_transaction'])) { + $options['before_send_transaction'] = new Reference($options['before_send_transaction']); + } + if (isset($options['before_breadcrumb'])) { $options['before_breadcrumb'] = new Reference($options['before_breadcrumb']); } @@ -111,9 +116,10 @@ private function registerConfiguration(ContainerBuilder $container, array $confi }, $options['class_serializers']); } - if (isset($options['integrations'])) { - $options['integrations'] = $this->configureIntegrationsOption($options['integrations'], $config); - } + $container->getDefinition(IntegrationConfigurator::class) + ->setArgument(0, $this->configureIntegrationsOption($options['integrations'], $config)) + ->setArgument(1, $config['register_error_handler']); + $options['integrations'] = new Reference(IntegrationConfigurator::class); $container ->register('sentry.client.options', Options::class) diff --git a/src/EventListener/ConsoleListener.php b/src/EventListener/ConsoleListener.php index 46c57e1a..2d39ec89 100644 --- a/src/EventListener/ConsoleListener.php +++ b/src/EventListener/ConsoleListener.php @@ -4,6 +4,9 @@ namespace Sentry\SentryBundle\EventListener; +use Sentry\Event; +use Sentry\EventHint; +use Sentry\ExceptionMechanism; use Sentry\State\HubInterface; use Sentry\State\Scope; use Symfony\Component\Console\Event\ConsoleCommandEvent; @@ -82,7 +85,12 @@ public function handleConsoleErrorEvent(ConsoleErrorEvent $event): void $scope->setTag('console.command.exit_code', (string) $event->getExitCode()); if ($this->captureErrors) { - $this->hub->captureException($event->getError()); + $hint = EventHint::fromArray([ + 'exception' => $event->getError(), + 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false), + ]); + + $this->hub->captureEvent(Event::createEvent(), $hint); } }); } diff --git a/src/EventListener/ErrorListener.php b/src/EventListener/ErrorListener.php index 5ebf8fdd..e4180e37 100644 --- a/src/EventListener/ErrorListener.php +++ b/src/EventListener/ErrorListener.php @@ -4,6 +4,9 @@ namespace Sentry\SentryBundle\EventListener; +use Sentry\Event; +use Sentry\EventHint; +use Sentry\ExceptionMechanism; use Sentry\State\HubInterface; use Symfony\Component\HttpKernel\Event\ExceptionEvent; @@ -34,6 +37,11 @@ public function __construct(HubInterface $hub) */ public function handleExceptionEvent(ExceptionEvent $event): void { - $this->hub->captureException($event->getThrowable()); + $hint = EventHint::fromArray([ + 'exception' => $event->getThrowable(), + 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false), + ]); + + $this->hub->captureEvent(Event::createEvent(), $hint); } } diff --git a/src/EventListener/MessengerListener.php b/src/EventListener/MessengerListener.php index e2af8fc2..f90d5574 100644 --- a/src/EventListener/MessengerListener.php +++ b/src/EventListener/MessengerListener.php @@ -5,6 +5,8 @@ namespace Sentry\SentryBundle\EventListener; use Sentry\Event; +use Sentry\EventHint; +use Sentry\ExceptionMechanism; use Sentry\State\HubInterface; use Sentry\State\Scope; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; @@ -62,13 +64,7 @@ public function handleWorkerMessageFailedEvent(WorkerMessageFailedEvent $event): $scope->setTag('messenger.message_bus', $messageBusStamp->getBusName()); } - if ($exception instanceof HandlerFailedException) { - foreach ($exception->getNestedExceptions() as $nestedException) { - $this->hub->captureException($nestedException); - } - } else { - $this->hub->captureException($exception); - } + $this->captureException($exception, $event->willRetry()); }); $this->flushClient(); @@ -86,6 +82,33 @@ public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event $this->flushClient(); } + /** + * Creates Sentry events from the given exception. + * + * Unpacks multiple exceptions wrapped in a HandlerFailedException and notifies + * Sentry of each individual exception. + * + * If the message will be retried the exceptions will be marked as handled + * in Sentry. + */ + private function captureException(\Throwable $exception, bool $willRetry): void + { + if ($exception instanceof HandlerFailedException) { + foreach ($exception->getNestedExceptions() as $nestedException) { + $this->captureException($nestedException, $willRetry); + } + + return; + } + + $hint = EventHint::fromArray([ + 'exception' => $exception, + 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, $willRetry), + ]); + + $this->hub->captureEvent(Event::createEvent(), $hint); + } + private function flushClient(): void { $client = $this->hub->getClient(); diff --git a/src/Integration/IntegrationConfigurator.php b/src/Integration/IntegrationConfigurator.php new file mode 100644 index 00000000..643b395f --- /dev/null +++ b/src/Integration/IntegrationConfigurator.php @@ -0,0 +1,80 @@ + true, + ExceptionListenerIntegration::class => true, + FatalErrorListenerIntegration::class => true, + ]; + + /** + * @var IntegrationInterface[] + */ + private $userIntegrations; + + /** + * @var bool + */ + private $registerErrorHandler; + + /** + * @param IntegrationInterface[] $userIntegrations + */ + public function __construct(array $userIntegrations, bool $registerErrorHandler) + { + $this->userIntegrations = $userIntegrations; + $this->registerErrorHandler = $registerErrorHandler; + } + + /** + * @see IntegrationRegistry::getIntegrationsToSetup() + * + * @param IntegrationInterface[] $defaultIntegrations + * + * @return IntegrationInterface[] + */ + public function __invoke(array $defaultIntegrations): array + { + $integrations = []; + + $userIntegrationsClasses = array_map('get_class', $this->userIntegrations); + $pickedIntegrationsClasses = []; + + foreach ($defaultIntegrations as $defaultIntegration) { + $integrationClassName = \get_class($defaultIntegration); + + if (!$this->registerErrorHandler && isset(self::ERROR_HANDLER_INTEGRATIONS[$integrationClassName])) { + continue; + } + + if (!\in_array($integrationClassName, $userIntegrationsClasses, true) && !isset($pickedIntegrationsClasses[$integrationClassName])) { + $integrations[] = $defaultIntegration; + $pickedIntegrationsClasses[$integrationClassName] = true; + } + } + + foreach ($this->userIntegrations as $userIntegration) { + $integrationClassName = \get_class($userIntegration); + + if (!isset($pickedIntegrationsClasses[$integrationClassName])) { + $integrations[] = $userIntegration; + $pickedIntegrationsClasses[$integrationClassName] = true; + } + } + + return $integrations; + } +} diff --git a/src/Resources/config/schema/sentry-1.0.xsd b/src/Resources/config/schema/sentry-1.0.xsd index a140d46c..4a803ed2 100644 --- a/src/Resources/config/schema/sentry-1.0.xsd +++ b/src/Resources/config/schema/sentry-1.0.xsd @@ -15,6 +15,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 41fe3633..328a05a3 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -121,6 +121,11 @@ + + + + + diff --git a/src/Tracing/HttpClient/AbstractTraceableHttpClient.php b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php index f4a2a35e..11f2ffbf 100644 --- a/src/Tracing/HttpClient/AbstractTraceableHttpClient.php +++ b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php @@ -52,6 +52,12 @@ public function request(string $method, string $url, array $options = []): Respo $headers['sentry-trace'] = $parent->toTraceparent(); $uri = new Uri($url); + $partialUri = Uri::fromParts([ + 'scheme' => $uri->getScheme(), + 'host' => $uri->getHost(), + 'port' => $uri->getPort(), + 'path' => $uri->getPath(), + ]); // Check if the request destination is allow listed in the trace_propagation_targets option. $client = $this->hub->getClient(); @@ -65,14 +71,16 @@ public function request(string $method, string $url, array $options = []): Respo $options['headers'] = $headers; - $formattedUri = $this->formatUri($uri); - $context = new SpanContext(); $context->setOp('http.client'); - $context->setDescription($method . ' ' . $formattedUri); + $context->setDescription($method . ' ' . (string) $partialUri); $context->setTags([ 'http.method' => $method, - 'http.url' => $formattedUri, + 'http.url' => (string) $partialUri, + ]); + $context->setData([ + 'http.query' => $uri->getQuery(), + 'http.fragment' => $uri->getFragment(), ]); $span = $parent->startChild($context); @@ -111,10 +119,4 @@ public function setLogger(LoggerInterface $logger): void $this->client->setLogger($logger); } } - - private function formatUri(Uri $uri): string - { - // Instead of relying on Uri::__toString, we only use a sub set of the URI - return Uri::composeComponents($uri->getScheme(), $uri->getHost(), $uri->getPath(), null, null); - } } diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 76aaeaf8..06727242 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -21,6 +21,7 @@ public function testProcessConfigurationWithDefaultConfiguration(): void { $expectedBundleDefaultConfig = [ 'register_error_listener' => true, + 'register_error_handler' => true, 'logger' => null, 'transport_factory' => 'Sentry\\Transport\\TransportFactoryInterface', 'options' => [ diff --git a/tests/DependencyInjection/Fixtures/php/error_handler_disabled.php b/tests/DependencyInjection/Fixtures/php/error_handler_disabled.php new file mode 100644 index 00000000..0f433a47 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/php/error_handler_disabled.php @@ -0,0 +1,10 @@ +loadFromExtension('sentry', [ + 'register_error_handler' => false, +]); diff --git a/tests/DependencyInjection/Fixtures/php/full.php b/tests/DependencyInjection/Fixtures/php/full.php index cf52ebf9..52a5bc60 100644 --- a/tests/DependencyInjection/Fixtures/php/full.php +++ b/tests/DependencyInjection/Fixtures/php/full.php @@ -26,6 +26,7 @@ 'release' => '4.0.x-dev', 'server_name' => 'localhost', 'before_send' => 'App\\Sentry\\BeforeSendCallback', + 'before_send_transaction' => 'App\\Sentry\\BeforeSendTransactionCallback', 'tags' => [ 'context' => 'development', ], diff --git a/tests/DependencyInjection/Fixtures/xml/error_handler_disabled.xml b/tests/DependencyInjection/Fixtures/xml/error_handler_disabled.xml new file mode 100644 index 00000000..3050ee1f --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/error_handler_disabled.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/tests/DependencyInjection/Fixtures/xml/full.xml b/tests/DependencyInjection/Fixtures/xml/full.xml index bab488bb..fe2b8bc1 100644 --- a/tests/DependencyInjection/Fixtures/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/xml/full.xml @@ -24,6 +24,7 @@ release="4.0.x-dev" server-name="localhost" before-send="App\Sentry\BeforeSendCallback" + before-send-transaction="App\Sentry\BeforeSendTransactionCallback" error-types="E_ALL" max-breadcrumbs="1" before-breadcrumb="App\Sentry\BeforeBreadcrumbCallback" diff --git a/tests/DependencyInjection/Fixtures/yml/error_handler_disabled.yml b/tests/DependencyInjection/Fixtures/yml/error_handler_disabled.yml new file mode 100644 index 00000000..4bcbfd85 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/error_handler_disabled.yml @@ -0,0 +1,2 @@ +sentry: + register_error_handler: false diff --git a/tests/DependencyInjection/Fixtures/yml/full.yml b/tests/DependencyInjection/Fixtures/yml/full.yml index d0799679..f7fd7b05 100644 --- a/tests/DependencyInjection/Fixtures/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/yml/full.yml @@ -22,6 +22,7 @@ sentry: release: 4.0.x-dev server_name: localhost before_send: App\Sentry\BeforeSendCallback + before_send_transaction: App\Sentry\BeforeSendTransactionCallback tags: context: development error_types: !php/const E_ALL diff --git a/tests/DependencyInjection/SentryExtensionTest.php b/tests/DependencyInjection/SentryExtensionTest.php index d2055e70..fb59da6a 100644 --- a/tests/DependencyInjection/SentryExtensionTest.php +++ b/tests/DependencyInjection/SentryExtensionTest.php @@ -19,6 +19,7 @@ use Sentry\SentryBundle\EventListener\TracingConsoleListener; use Sentry\SentryBundle\EventListener\TracingRequestListener; use Sentry\SentryBundle\EventListener\TracingSubRequestListener; +use Sentry\SentryBundle\Integration\IntegrationConfigurator; use Sentry\SentryBundle\SentryBundle; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\ConnectionConfigurator; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware; @@ -64,6 +65,18 @@ public function testErrorListenerIsRemovedWhenDisabled(): void $this->assertFalse($container->hasDefinition(ErrorListener::class)); } + /** + * @testWith ["full", true] + * ["error_handler_disabled", false] + */ + public function testIntegrationConfiguratorRegisterErrorHandlerArgument(string $fixtureFile, bool $expected): void + { + $container = $this->createContainerFromFixture($fixtureFile); + $definition = $container->getDefinition(IntegrationConfigurator::class); + + $this->assertSame($expected, $definition->getArgument(1)); + } + public function testConsoleCommandListener(): void { $container = $this->createContainerFromFixture('full'); @@ -185,17 +198,7 @@ public function testClientIsCreatedFromOptions(): void $container = $this->createContainerFromFixture('full'); $optionsDefinition = $container->getDefinition('sentry.client.options'); $expectedOptions = [ - 'integrations' => [ - new Definition(IgnoreErrorsIntegration::class, [ - [ - 'ignore_exceptions' => [ - FatalError::class, - FatalErrorException::class, - ], - ], - ]), - new Reference('App\\Sentry\\Integration\\FooIntegration'), - ], + 'integrations' => new Reference(IntegrationConfigurator::class), 'default_integrations' => false, 'send_attempts' => 1, 'prefixes' => [$container->getParameter('kernel.project_dir')], @@ -211,6 +214,7 @@ public function testClientIsCreatedFromOptions(): void 'release' => '4.0.x-dev', 'server_name' => 'localhost', 'before_send' => new Reference('App\\Sentry\\BeforeSendCallback'), + 'before_send_transaction' => new Reference('App\\Sentry\\BeforeSendTransactionCallback'), 'tags' => ['context' => 'development'], 'error_types' => \E_ALL, 'max_breadcrumbs' => 1, @@ -233,6 +237,22 @@ public function testClientIsCreatedFromOptions(): void $this->assertSame(Options::class, $optionsDefinition->getClass()); $this->assertEquals($expectedOptions, $optionsDefinition->getArgument(0)); + $integrationConfiguratorDefinition = $container->getDefinition(IntegrationConfigurator::class); + $expectedIntegrations = [ + new Definition(IgnoreErrorsIntegration::class, [ + [ + 'ignore_exceptions' => [ + FatalError::class, + FatalErrorException::class, + ], + ], + ]), + new Reference('App\\Sentry\\Integration\\FooIntegration'), + ]; + + $this->assertSame(IntegrationConfigurator::class, $integrationConfiguratorDefinition->getClass()); + $this->assertEquals($expectedIntegrations, $integrationConfiguratorDefinition->getArgument(0)); + $clientDefinition = $container->findDefinition(ClientInterface::class); $factory = $clientDefinition->getFactory(); @@ -281,7 +301,7 @@ public function testErrorTypesOptionIsParsedFromStringToIntegerValue(): void public function testIgnoreErrorsIntegrationIsNotAddedTwiceIfAlreadyConfigured(): void { $container = $this->createContainerFromFixture('ignore_errors_integration_overridden'); - $integrations = $container->getDefinition('sentry.client.options')->getArgument(0)['integrations']; + $integrations = $container->getDefinition(IntegrationConfigurator::class)->getArgument(0); $ignoreErrorsIntegrationsCount = 0; foreach ($integrations as $integration) { diff --git a/tests/EventListener/AbstractConsoleListenerTest.php b/tests/EventListener/AbstractConsoleListenerTest.php index 9e124523..89fb94c5 100644 --- a/tests/EventListener/AbstractConsoleListenerTest.php +++ b/tests/EventListener/AbstractConsoleListenerTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Sentry\Event; +use Sentry\EventHint; use Sentry\SentryBundle\EventListener\ConsoleListener; use Sentry\State\HubInterface; use Sentry\State\Scope; @@ -113,8 +114,20 @@ public function testHandleConsoleErrorEvent(bool $captureErrors): void }); $this->hub->expects($captureErrors ? $this->once() : $this->never()) - ->method('captureException') - ->with($consoleEvent->getError()); + ->method('captureEvent') + ->with( + $this->anything(), + $this->logicalAnd( + $this->isInstanceOf(EventHint::class), + $this->callback(function (EventHint $subject) use ($consoleEvent) { + self::assertSame($consoleEvent->getError(), $subject->exception); + self::assertNotNull($subject->mechanism); + self::assertFalse($subject->mechanism->isHandled()); + + return true; + }) + ) + ); $listener->handleConsoleErrorEvent($consoleEvent); diff --git a/tests/EventListener/ErrorListenerTest.php b/tests/EventListener/ErrorListenerTest.php index f97ff438..602f1d80 100644 --- a/tests/EventListener/ErrorListenerTest.php +++ b/tests/EventListener/ErrorListenerTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Sentry\EventHint; use Sentry\SentryBundle\EventListener\ErrorListener; use Sentry\State\HubInterface; use Symfony\Component\HttpFoundation\Request; @@ -35,9 +36,23 @@ protected function setUp(): void */ public function testHandleExceptionEvent(ExceptionEvent $event): void { + $expectedException = $event->getThrowable(); + $this->hub->expects($this->once()) - ->method('captureException') - ->with($event->getThrowable()); + ->method('captureEvent') + ->with( + $this->anything(), + $this->logicalAnd( + $this->isInstanceOf(EventHint::class), + $this->callback(function (EventHint $subject) use ($expectedException) { + self::assertSame($expectedException, $subject->exception); + self::assertNotNull($subject->mechanism); + self::assertFalse($subject->mechanism->isHandled()); + + return true; + }) + ) + ); $this->listener->handleExceptionEvent($event); } diff --git a/tests/EventListener/MessengerListenerTest.php b/tests/EventListener/MessengerListenerTest.php index af7b8462..0404edad 100644 --- a/tests/EventListener/MessengerListenerTest.php +++ b/tests/EventListener/MessengerListenerTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Sentry\ClientInterface; use Sentry\Event; +use Sentry\EventHint; use Sentry\SentryBundle\EventListener\MessengerListener; use Sentry\State\HubInterface; use Sentry\State\Scope; @@ -42,7 +43,7 @@ protected function setUp(): void * @param \Throwable[] $exceptions * @param array $expectedTags */ - public function testHandleWorkerMessageFailedEvent(array $exceptions, WorkerMessageFailedEvent $event, array $expectedTags): void + public function testHandleWorkerMessageFailedEvent(array $exceptions, WorkerMessageFailedEvent $event, array $expectedTags, bool $expectedIsHandled): void { if (!$this->supportsMessenger()) { $this->markTestSkipped('Messenger not supported in this environment.'); @@ -57,9 +58,21 @@ public function testHandleWorkerMessageFailedEvent(array $exceptions, WorkerMess }); $this->hub->expects($this->exactly(\count($exceptions))) - ->method('captureException') - ->withConsecutive(...array_map(static function (\Throwable $exception): array { - return [$exception]; + ->method('captureEvent') + ->withConsecutive(...array_map(function (\Throwable $expectedException) use ($expectedIsHandled): array { + return [ + $this->anything(), + $this->logicalAnd( + $this->isInstanceOf(EventHint::class), + $this->callback(function (EventHint $subject) use ($expectedException, $expectedIsHandled) { + self::assertSame($expectedException, $subject->exception); + self::assertNotNull($subject->mechanism); + self::assertSame($expectedIsHandled, $subject->mechanism->isHandled()); + + return true; + }) + ), + ]; }, $exceptions)); $this->hub->expects($this->once()) @@ -97,6 +110,17 @@ public function handleWorkerMessageFailedEventDataProvider(): \Generator 'messenger.receiver_name' => 'receiver', 'messenger.message_class' => \get_class($envelope->getMessage()), ], + false, + ]; + + yield 'envelope.throwable INSTANCEOF HandlerFailedException - RETRYING' => [ + $exceptions, + $this->getMessageFailedEvent($envelope, 'receiver', new HandlerFailedException($envelope, $exceptions), true), + [ + 'messenger.receiver_name' => 'receiver', + 'messenger.message_class' => \get_class($envelope->getMessage()), + ], + true, ]; yield 'envelope.throwable INSTANCEOF Exception' => [ @@ -106,6 +130,17 @@ public function handleWorkerMessageFailedEventDataProvider(): \Generator 'messenger.receiver_name' => 'receiver', 'messenger.message_class' => \get_class($envelope->getMessage()), ], + false, + ]; + + yield 'envelope.throwable INSTANCEOF Exception - RETRYING' => [ + [$exceptions[0]], + $this->getMessageFailedEvent($envelope, 'receiver', $exceptions[0], true), + [ + 'messenger.receiver_name' => 'receiver', + 'messenger.message_class' => \get_class($envelope->getMessage()), + ], + true, ]; $envelope = new Envelope((object) [], [new BusNameStamp('bus.foo')]); @@ -118,6 +153,7 @@ public function handleWorkerMessageFailedEventDataProvider(): \Generator 'messenger.message_class' => \get_class($envelope->getMessage()), 'messenger.message_bus' => 'bus.foo', ], + false, ]; } diff --git a/tests/Integration/IntegrationConfiguratorTest.php b/tests/Integration/IntegrationConfiguratorTest.php new file mode 100644 index 00000000..44206b5c --- /dev/null +++ b/tests/Integration/IntegrationConfiguratorTest.php @@ -0,0 +1,176 @@ +assertSame($expectedIntegrations, $integrationConfigurator($defaultIntegrations)); + } + + /** + * @return iterable + */ + public function integrationsDataProvider(): iterable + { + $exceptionListenerIntegration = new ExceptionListenerIntegration(); + $errorListenerIntegration = new ErrorListenerIntegration(); + $fatalErrorListenerIntegration = new FatalErrorListenerIntegration(); + $requestIntegration = new RequestIntegration(); + $transactionIntegration = new TransactionIntegration(); + $frameContextifierIntegration = new FrameContextifierIntegration(); + $environmentIntegration = new EnvironmentIntegration(); + $modulesIntegration = new ModulesIntegration(); + + $userIntegration1 = new class() implements IntegrationInterface { + public function setupOnce(): void + { + } + }; + $userRequestIntegration = new RequestIntegration(); + + yield 'Default integrations, register error handler true and no user integrations' => [ + [], + true, + [ + $exceptionListenerIntegration, + $errorListenerIntegration, + $fatalErrorListenerIntegration, + $requestIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + ], + [ + $exceptionListenerIntegration, + $errorListenerIntegration, + $fatalErrorListenerIntegration, + $requestIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + ], + ]; + + yield 'Default integrations, register error handler false and no user integrations' => [ + [], + false, + [ + $exceptionListenerIntegration, + $errorListenerIntegration, + $fatalErrorListenerIntegration, + $requestIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + ], + [ + $requestIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + ], + ]; + + yield 'Default integrations, register error handler true and some user integrations, one of which is also a default integration' => [ + [ + $userIntegration1, + $userRequestIntegration, + ], + true, + [ + $exceptionListenerIntegration, + $errorListenerIntegration, + $fatalErrorListenerIntegration, + $requestIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + ], + [ + $exceptionListenerIntegration, + $errorListenerIntegration, + $fatalErrorListenerIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + $userIntegration1, + $userRequestIntegration, + ], + ]; + + yield 'Default integrations, register error handler false and some user integrations, one of which is also a default integration' => [ + [ + $userIntegration1, + $userRequestIntegration, + ], + false, + [ + $exceptionListenerIntegration, + $errorListenerIntegration, + $fatalErrorListenerIntegration, + $requestIntegration, + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + ], + [ + $transactionIntegration, + $frameContextifierIntegration, + $environmentIntegration, + $modulesIntegration, + $userIntegration1, + $userRequestIntegration, + ], + ]; + + yield 'No default integrations and some user integrations are repeated twice' => [ + [ + $userIntegration1, + $userRequestIntegration, + $userIntegration1, + ], + true, + [], + [ + $userIntegration1, + $userRequestIntegration, + ], + ]; + } +} diff --git a/tests/Tracing/HttpClient/TraceableHttpClientTest.php b/tests/Tracing/HttpClient/TraceableHttpClientTest.php index 06636d26..0a814ded 100644 --- a/tests/Tracing/HttpClient/TraceableHttpClientTest.php +++ b/tests/Tracing/HttpClient/TraceableHttpClientTest.php @@ -78,12 +78,17 @@ public function testRequest(): void 'http.method' => 'GET', 'http.url' => 'https://www.example.com/test-page', ]; + $expectedData = [ + 'http.query' => 'foo=bar', + 'http.fragment' => 'baz', + ]; $this->assertCount(2, $spans); $this->assertNull($spans[1]->getEndTimestamp()); $this->assertSame('http.client', $spans[1]->getOp()); $this->assertSame('GET https://www.example.com/test-page', $spans[1]->getDescription()); $this->assertSame($expectedTags, $spans[1]->getTags()); + $this->assertSame($expectedData, $spans[1]->getData()); } public function testRequestDoesNotContainBaggageHeader(): void