diff --git a/.craft.yml b/.craft.yml index df763ddc..eacd7244 100644 --- a/.craft.yml +++ b/.craft.yml @@ -2,7 +2,6 @@ minVersion: 0.23.1 changelogPolicy: simple artifactProvider: name: none -preReleaseCommand: '' targets: - name: github - name: registry diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 744739fe..0cbd7f5c 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -24,7 +24,6 @@ jobs: - '8.0' - '8.1' symfony-version: - - 3.4.* - 4.4.* - 5.* - 6.* @@ -37,14 +36,7 @@ jobs: symfony-version: 6.* - php: '7.4' symfony-version: 6.* - - php: '8.0' - symfony-version: 3.4.* - - php: '8.1' - symfony-version: 3.4.* include: - - php: '7.2' - symfony-version: 3.4.* - dependencies: lowest - php: '7.2' symfony-version: 4.4.* dependencies: lowest @@ -71,9 +63,9 @@ jobs: - name: Setup Problem Matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Remove Symfony Messenger - run: composer remove --dev symfony/messenger --no-update - if: matrix.symfony-version == '3.4.*' + - name: Update PHPUnit + run: composer require --dev phpunit/phpunit ^9.3.9 --no-update + if: matrix.php == '8.0' && matrix.dependencies == 'lowest' - name: Install dependencies uses: ramsey/composer-install@v1 @@ -100,7 +92,7 @@ jobs: include: - php: '7.2' dependencies: lowest - symfony-version: 3.4.* + symfony-version: 4.4.* - php: '7.4' dependencies: highest - php: '8.0' @@ -124,7 +116,7 @@ jobs: run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Remove optional packages - run: composer remove doctrine/dbal doctrine/doctrine-bundle symfony/messenger symfony/twig-bundle symfony/cache --dev --no-update + run: composer remove doctrine/dbal doctrine/doctrine-bundle symfony/messenger symfony/twig-bundle symfony/cache symfony/http-client --dev --no-update - name: Install dependencies uses: ramsey/composer-install@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 09cf02ab..31ec9023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ ## Unreleased +## 4.5.0 (2022-11-28) + +- Symfony version 3.4 is no longer supported + - Drop Symfony support below 4.4 (#643) +- feat: Add support for tracing of Symfony HTTP client requests (#606) + - feat: Add support for HTTP client baggage propagation (#663) + - ref: Add proper HTTP client span descriptions (#680) +- feat: Support logging the impersonator user, if any (#647) +- ref: Use a constant for the SDK version (#662) + ## 4.4.0 (2022-10-20) - feat: Add support for Dynamic Sampling (#665) @@ -12,7 +22,7 @@ ## 4.3.0 (2022-05-30) -- Fix compatibility issue with Symfony >= 6.1.0 (#635) +- Fix compatibility issue with Symfony `>= 6.1.0` (#635) - Add `TracingDriverConnectionInterface::getNativeConnection()` method to get the original driver connection (#597) - Add `options.http_timeout` and `options.http_connect_timeout` configuration options (#593) diff --git a/composer.json b/composer.json index 293fb12f..fd3fac4a 100644 --- a/composer.json +++ b/composer.json @@ -28,14 +28,14 @@ "php-http/discovery": "^1.11", "sentry/sdk": "^3.3", "symfony/cache-contracts": "^1.1||^2.4||^3.0", - "symfony/config": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/console": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/dependency-injection": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/event-dispatcher": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/http-kernel": "^3.4.44||^4.4.20||^5.0.11||^6.0", + "symfony/config": "^4.4.20||^5.0.11||^6.0", + "symfony/console": "^4.4.20||^5.0.11||^6.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0", + "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0", "symfony/polyfill-php80": "^1.22", "symfony/psr-http-message-bridge": "^1.2||^2.0", - "symfony/security-core": "^3.4.44||^4.4.20||^5.0.11||^6.0" + "symfony/security-core": "^4.4.20||^5.0.11||^6.0" }, "require-dev": { "doctrine/dbal": "^2.13||^3.0", @@ -50,16 +50,17 @@ "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-symfony": "^1.0", "phpunit/phpunit": "^8.5.14||^9.3.9", - "symfony/browser-kit": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/cache": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/dom-crawler": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/framework-bundle": "^3.4.44||^4.4.20||^5.0.11||^6.0", + "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0", + "symfony/cache": "^4.4.20||^5.0.11||^6.0", + "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0", + "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0", + "symfony/http-client": "^4.4.20||^5.0.11||^6.0", "symfony/messenger": "^4.4.20||^5.0.11||^6.0", "symfony/monolog-bundle": "^3.4", "symfony/phpunit-bridge": "^5.2.6||^6.0", - "symfony/process": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/twig-bundle": "^3.4.44||^4.4.20||^5.0.11||^6.0", - "symfony/yaml": "^3.4.44||^4.4.20||^5.0.11||^6.0", + "symfony/process": "^4.4.20||^5.0.11||^6.0", + "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0", + "symfony/yaml": "^4.4.20||^5.0.11||^6.0", "vimeo/psalm": "^4.3" }, "suggest": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fcf117af..4dcdcbf9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -105,6 +105,11 @@ parameters: count: 1 path: src/DependencyInjection/SentryExtension.php + - + message: "#^Parameter \\#2 \\$config of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:registerHttpClientTracingConfiguration\\(\\) expects array\\, mixed given\\.$#" + count: 1 + path: src/DependencyInjection/SentryExtension.php + - message: "#^Parameter \\#2 \\$config of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:registerMessengerListenerConfiguration\\(\\) expects array\\, mixed given\\.$#" count: 1 @@ -122,7 +127,7 @@ parameters: - message: "#^Parameter \\#2 \\$config of method Symfony\\\\Component\\\\DependencyInjection\\\\Extension\\\\Extension\\:\\:isConfigEnabled\\(\\) expects array, mixed given\\.$#" - count: 3 + count: 4 path: src/DependencyInjection/SentryExtension.php - @@ -150,26 +155,11 @@ parameters: count: 1 path: src/EventListener/ConsoleCommandListener.php - - - message: "#^Else branch is unreachable because previous condition is always true\\.$#" - count: 1 - path: src/EventListener/ErrorListener.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 path: src/EventListener/RequestListener.php - - - message: "#^Cannot call method getUser\\(\\) on Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface\\|null\\.$#" - count: 1 - path: src/EventListener/RequestListener.php - - - - message: "#^Parameter \\#1 \\$user of method Sentry\\\\SentryBundle\\\\EventListener\\\\RequestListener\\:\\:getUsername\\(\\) expects object\\|string, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#" - count: 1 - path: src/EventListener/RequestListener.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 @@ -240,6 +230,31 @@ parameters: count: 1 path: src/Tracing/Doctrine/DBAL/TracingStatementForV3.php + - + message: "#^Parameter \\#4 \\$length of method Doctrine\\\\DBAL\\\\Driver\\\\Statement\\:\\:bindParam\\(\\) expects int\\|null, mixed given\\.$#" + count: 1 + path: src/Tracing/Doctrine/DBAL/TracingStatementForV3.php + + - + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableHttpClient\\:\\:request\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Tracing/HttpClient/AbstractTraceableHttpClient.php + + - + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Tracing/HttpClient/AbstractTraceableResponse.php + + - + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\TraceableHttpClientForV6\\:\\:withOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Tracing/HttpClient/TraceableHttpClientForV6.php + + - + message: "#^Call to an undefined method Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\:\\:toStream\\(\\)\\.$#" + count: 1 + path: src/Tracing/HttpClient/TraceableResponseForV6.php + - message: "#^Cannot access offset 'sample_rate' on mixed\\.$#" count: 1 @@ -280,73 +295,28 @@ parameters: count: 1 path: tests/End2End/TracingEnd2EndTest.php - - - message: "#^Call to method getException\\(\\) on an unknown class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseForExceptionEvent\\.$#" - count: 1 - path: tests/EventListener/ErrorListenerTest.php - - - - message: "#^Instantiated class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseForExceptionEvent not found\\.$#" - count: 1 - path: tests/EventListener/ErrorListenerTest.php - - - - message: "#^Parameter \\#1 \\$event of method Sentry\\\\SentryBundle\\\\EventListener\\\\ErrorListener\\:\\:handleExceptionEvent\\(\\) expects Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\ExceptionEvent, Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\ExceptionEvent\\|Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseForExceptionEvent given\\.$#" - count: 1 - path: tests/EventListener/ErrorListenerTest.php - - - - message: "#^Parameter \\$event of method Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\ErrorListenerTest\\:\\:testHandleExceptionEvent\\(\\) has invalid type Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseForExceptionEvent\\.$#" - count: 1 - path: tests/EventListener/ErrorListenerTest.php - - - - message: "#^Class Symfony\\\\Component\\\\Messenger\\\\Event\\\\WorkerMessageFailedEvent constructor invoked with 4 parameters, 3 required\\.$#" - count: 1 - path: tests/EventListener/MessengerListenerTest.php - - - - message: "#^If condition is always false\\.$#" - count: 1 - path: tests/EventListener/MessengerListenerTest.php - - message: "#^Call to function method_exists\\(\\) with \\$this\\(Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\AuthenticatedTokenStub\\) and 'setAuthenticated' will always evaluate to false\\.$#" count: 1 path: tests/EventListener/RequestListenerTest.php - - message: "#^Instantiated class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterControllerEvent not found\\.$#" - count: 3 - path: tests/EventListener/RequestListenerTest.php - - - - message: "#^Instantiated class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent not found\\.$#" - count: 9 - path: tests/EventListener/RequestListenerTest.php - - - - message: "#^Parameter \\#1 \\$event of method Sentry\\\\SentryBundle\\\\EventListener\\\\RequestListener\\:\\:handleKernelControllerEvent\\(\\) expects Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\ControllerEvent, Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\ControllerEvent\\|Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterControllerEvent given\\.$#" - count: 1 - path: tests/EventListener/RequestListenerTest.php - - - - message: "#^Parameter \\#1 \\$event of method Sentry\\\\SentryBundle\\\\EventListener\\\\RequestListener\\:\\:handleKernelRequestEvent\\(\\) expects Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\RequestEvent, Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent\\|Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\RequestEvent given\\.$#" + message: "#^Parameter \\#1 \\$user of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\AbstractToken\\:\\:setUser\\(\\) expects Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface, string\\|Stringable\\|Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#" count: 1 path: tests/EventListener/RequestListenerTest.php - - message: "#^Parameter \\#1 \\$user of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\AbstractToken\\:\\:setUser\\(\\) expects Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface, string\\|Stringable\\|Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#" + message: "#^Parameter \\#3 \\$roles of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects array\\, string given\\.$#" count: 1 path: tests/EventListener/RequestListenerTest.php - - message: "#^Parameter \\$controllerEvent of method Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\RequestListenerTest\\:\\:testHandleKernelControllerEvent\\(\\) has invalid type Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterControllerEvent\\.$#" + message: "#^Parameter \\#4 \\$originalToken of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface, array\\ given\\.$#" count: 1 path: tests/EventListener/RequestListenerTest.php - - message: "#^Parameter \\$requestEvent of method Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\RequestListenerTest\\:\\:testHandleKernelRequestEvent\\(\\) has invalid type Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent\\.$#" + message: "#^Parameter \\#5 \\$originatedFromUri of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects string\\|null, Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\AuthenticatedTokenStub given\\.$#" count: 1 path: tests/EventListener/RequestListenerTest.php @@ -355,26 +325,6 @@ parameters: count: 1 path: tests/EventListener/SubRequestListenerTest.php - - - message: "#^Instantiated class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent not found\\.$#" - count: 2 - path: tests/EventListener/SubRequestListenerTest.php - - - - message: "#^Parameter \\#1 \\$event of method Sentry\\\\SentryBundle\\\\EventListener\\\\SubRequestListener\\:\\:handleKernelRequestEvent\\(\\) expects Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\RequestEvent, Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent\\|Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\RequestEvent given\\.$#" - count: 1 - path: tests/EventListener/SubRequestListenerTest.php - - - - message: "#^Parameter \\#1 \\$event of method Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\SubRequestListenerTest\\:\\:isMainRequest\\(\\) expects Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent, Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent\\|Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\RequestEvent given\\.$#" - count: 1 - path: tests/EventListener/SubRequestListenerTest.php - - - - message: "#^Parameter \\$event of method Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\SubRequestListenerTest\\:\\:testHandleKernelRequestEvent\\(\\) has invalid type Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\GetResponseEvent\\.$#" - count: 1 - path: tests/EventListener/SubRequestListenerTest.php - - message: "#^Call to an undefined method TCacheAdapter of Symfony\\\\Component\\\\Cache\\\\Adapter\\\\AdapterInterface\\:\\:delete\\(\\)\\.$#" count: 2 @@ -495,3 +445,12 @@ parameters: count: 1 path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + - + message: "#^Parameter \\#1 \\$responses of method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableHttpClient\\:\\:stream\\(\\) expects iterable\\<\\(int\\|string\\), Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\>\\|Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface, stdClass given\\.$#" + count: 1 + path: tests/Tracing/HttpClient/TraceableHttpClientTest.php + + - + message: "#^Parameter \\#2 \\$responses of static method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:stream\\(\\) expects iterable\\, array\\ given\\.$#" + count: 1 + path: tests/Tracing/HttpClient/TraceableResponseTest.php diff --git a/phpstan.neon b/phpstan.neon index ce46ce35..684f3989 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -14,6 +14,10 @@ parameters: - src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php - src/Tracing/Doctrine/DBAL/TracingStatementForV2.php - src/Tracing/Doctrine/DBAL/TracingDriverForV2.php + - src/Tracing/HttpClient/TraceableHttpClientForV4.php + - src/Tracing/HttpClient/TraceableHttpClientForV5.php + - src/Tracing/HttpClient/TraceableResponseForV4.php + - src/Tracing/HttpClient/TraceableResponseForV5.php - tests/End2End/App - tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php - tests/EventListener/Fixtures/UserWithoutIdentifierStub.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5ef7a1e3..32016223 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -50,6 +50,16 @@ $params + + + toStream + + + + + toStream + + TracingDriverForV2 @@ -63,4 +73,23 @@ PostResponseEvent + + + $container->getParameter('sentry.tracing.cache.enabled') + + + + + $container->getParameter('sentry.tracing.enabled') + $container->getParameter('sentry.tracing.http_client.enabled') + + + + + $container->getParameter('sentry.tracing.enabled') + $container->getParameter('sentry.tracing.dbal.enabled') + $container->getParameter('sentry.tracing.dbal.connections') + $container->getParameter('doctrine.connections') + + diff --git a/psalm.xml b/psalm.xml index 39fe8712..89240539 100644 --- a/psalm.xml +++ b/psalm.xml @@ -11,6 +11,8 @@ + + diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 00000000..edd3a204 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -eux + +# if [ "$(uname -s)" != "Linux" ]; then +# echo "Please use the GitHub Action." +# exit 1 +# fi + +SCRIPT_DIR="$( dirname "$0" )" +cd $SCRIPT_DIR/.. + +OLD_VERSION="${1}" +NEW_VERSION="${2}" + +echo "Current version: $OLD_VERSION" +echo "Bumping version: $NEW_VERSION" + +function replace() { + ! grep "$2" $3 + perl -i -pe "s/$1/$2/g" $3 + grep "$2" $3 # verify that replacement was successful +} + +replace "SDK_VERSION = '[0-9.]+'" "SDK_VERSION = '$NEW_VERSION'" ./src/SentryBundle.php diff --git a/src/DependencyInjection/Compiler/HttpClientTracingPass.php b/src/DependencyInjection/Compiler/HttpClientTracingPass.php new file mode 100644 index 00000000..2464532b --- /dev/null +++ b/src/DependencyInjection/Compiler/HttpClientTracingPass.php @@ -0,0 +1,32 @@ +getParameter('sentry.tracing.enabled') || !$container->getParameter('sentry.tracing.http_client.enabled')) { + return; + } + + foreach ($container->findTaggedServiceIds('http_client.client') as $id => $tags) { + $container->register('.sentry.traceable.' . $id, TraceableHttpClient::class) + ->setDecoratedService($id) + ->setArgument(0, new Reference('.sentry.traceable.' . $id . '.inner')) + ->setArgument(1, new Reference(HubInterface::class)) + ->addTag('kernel.reset', ['method' => 'reset']); + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 6e5a5902..b1c1bdf3 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Messenger\MessageBusInterface; final class Configuration implements ConfigurationInterface @@ -47,6 +48,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('options') ->addDefaultsIfNotSet() ->fixXmlConfig('integration') + ->fixXmlConfig('trace_propagation_target') ->fixXmlConfig('tag') ->fixXmlConfig('class_serializer') ->fixXmlConfig('prefix', 'prefixes') @@ -71,6 +73,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('The sampling factor to apply to transactions. A value of 0 will deny sending any transaction, and a value of 1 will send all transactions.') ->end() ->scalarNode('traces_sampler')->end() + ->arrayNode('trace_propagation_targets') + ->scalarPrototype()->end() + ->beforeNormalization()->castToArray()->end() + ->end() ->booleanNode('attach_stacktrace')->end() ->integerNode('context_lines')->min(0)->end() ->booleanNode('enable_compression')->end() @@ -184,6 +190,9 @@ private function addDistributedTracingSection(ArrayNodeDefinition $rootNode): vo ->arrayNode('cache') ->{class_exists(CacheItem::class) ? 'canBeDisabled' : 'canBeEnabled'}() ->end() + ->arrayNode('http_client') + ->{class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->end() ->arrayNode('console') ->addDefaultsIfNotSet() ->fixXmlConfig('excluded_command') diff --git a/src/DependencyInjection/SentryExtension.php b/src/DependencyInjection/SentryExtension.php index 1b2bc394..f87458bc 100644 --- a/src/DependencyInjection/SentryExtension.php +++ b/src/DependencyInjection/SentryExtension.php @@ -5,8 +5,6 @@ namespace Sentry\SentryBundle\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Jean85\PrettyVersions; -use LogicException; use Psr\Log\NullLogger; use Sentry\Client; use Sentry\ClientBuilder; @@ -37,6 +35,7 @@ use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; final class SentryExtension extends ConfigurableExtension @@ -74,6 +73,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $this->registerDbalTracingConfiguration($container, $mergedConfig['tracing']); $this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']); $this->registerCacheTracingConfiguration($container, $mergedConfig['tracing']); + $this->registerHttpClientTracingConfiguration($container, $mergedConfig['tracing']); } /** @@ -138,7 +138,7 @@ private function registerConfiguration(ContainerBuilder $container, array $confi $clientBuilderDefinition = (new Definition(ClientBuilder::class)) ->setArgument(0, new Reference('sentry.client.options')) ->addMethodCall('setSdkIdentifier', [SentryBundle::SDK_IDENTIFIER]) - ->addMethodCall('setSdkVersion', [PrettyVersions::getVersion('sentry/sentry-symfony')->getPrettyVersion()]) + ->addMethodCall('setSdkVersion', [SentryBundle::SDK_VERSION]) ->addMethodCall('setTransportFactory', [new Reference($config['transport_factory'])]) ->addMethodCall('setSerializer', [$serializer]) ->addMethodCall('setRepresentationSerializer', [$representationSerializerDefinition]) @@ -203,7 +203,7 @@ private function registerDbalTracingConfiguration(ContainerBuilder $container, a && $this->isConfigEnabled($container, $config['dbal']); if ($isConfigEnabled && !class_exists(DoctrineBundle::class)) { - throw new LogicException('DBAL tracing support cannot be enabled because the doctrine/doctrine-bundle Composer package is not installed.'); + throw new \LogicException('DBAL tracing support cannot be enabled because the doctrine/doctrine-bundle Composer package is not installed.'); } $container->setParameter('sentry.tracing.dbal.enabled', $isConfigEnabled); @@ -224,7 +224,7 @@ private function registerTwigTracingConfiguration(ContainerBuilder $container, a && $this->isConfigEnabled($container, $config['twig']); if ($isConfigEnabled && !class_exists(TwigBundle::class)) { - throw new LogicException('Twig tracing support cannot be enabled because the symfony/twig-bundle Composer package is not installed.'); + throw new \LogicException('Twig tracing support cannot be enabled because the symfony/twig-bundle Composer package is not installed.'); } if (!$isConfigEnabled) { @@ -241,12 +241,27 @@ private function registerCacheTracingConfiguration(ContainerBuilder $container, && $this->isConfigEnabled($container, $config['cache']); if ($isConfigEnabled && !class_exists(CacheItem::class)) { - throw new LogicException('Cache tracing support cannot be enabled because the symfony/cache Composer package is not installed.'); + throw new \LogicException('Cache tracing support cannot be enabled because the symfony/cache Composer package is not installed.'); } $container->setParameter('sentry.tracing.cache.enabled', $isConfigEnabled); } + /** + * @param array $config + */ + private function registerHttpClientTracingConfiguration(ContainerBuilder $container, array $config): void + { + $isConfigEnabled = $this->isConfigEnabled($container, $config) + && $this->isConfigEnabled($container, $config['http_client']); + + if ($isConfigEnabled && !class_exists(HttpClient::class)) { + throw new \LogicException('Http client tracing support cannot be enabled because the symfony/http-client Composer package is not installed.'); + } + + $container->setParameter('sentry.tracing.http_client.enabled', $isConfigEnabled); + } + /** * @param string[] $integrations * @param array $config diff --git a/src/EventListener/AbstractTracingRequestListener.php b/src/EventListener/AbstractTracingRequestListener.php index 6bab5194..022f2d1b 100644 --- a/src/EventListener/AbstractTracingRequestListener.php +++ b/src/EventListener/AbstractTracingRequestListener.php @@ -7,6 +7,7 @@ use Sentry\State\HubInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\Routing\Route; abstract class AbstractTracingRequestListener @@ -34,11 +35,10 @@ public function __construct(HubInterface $hub) * gathering information like the HTTP status code and attaching them as * tags of the span/transaction. * - * @param RequestListenerResponseEvent $event The event + * @param ResponseEvent $event The event */ - public function handleKernelResponseEvent(RequestListenerResponseEvent $event): void + public function handleKernelResponseEvent(ResponseEvent $event): void { - /** @var Response $response */ $response = $event->getResponse(); $span = $this->hub->getSpan(); diff --git a/src/EventListener/ErrorListener.php b/src/EventListener/ErrorListener.php index dc3e3776..5ebf8fdd 100644 --- a/src/EventListener/ErrorListener.php +++ b/src/EventListener/ErrorListener.php @@ -30,14 +30,10 @@ public function __construct(HubInterface $hub) /** * Handles an exception that happened while running the application. * - * @param ErrorListenerExceptionEvent $event The event + * @param ExceptionEvent $event The event */ - public function handleExceptionEvent(ErrorListenerExceptionEvent $event): void + public function handleExceptionEvent(ExceptionEvent $event): void { - if ($event instanceof ExceptionEvent) { - $this->hub->captureException($event->getThrowable()); - } else { - $this->hub->captureException($event->getException()); - } + $this->hub->captureException($event->getThrowable()); } } diff --git a/src/EventListener/RequestListener.php b/src/EventListener/RequestListener.php index 7c094ca7..b566d615 100644 --- a/src/EventListener/RequestListener.php +++ b/src/EventListener/RequestListener.php @@ -7,7 +7,10 @@ use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\UserDataBag; +use Symfony\Component\HttpKernel\Event\ControllerEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -46,9 +49,9 @@ public function __construct(HubInterface $hub, ?TokenStorageInterface $tokenStor * This method is called for each request handled by the framework and * fills the Sentry scope with information about the current user. * - * @param RequestListenerRequestEvent $event The event + * @param RequestEvent $event The event */ - public function handleKernelRequestEvent(RequestListenerRequestEvent $event): void + public function handleKernelRequestEvent(RequestEvent $event): void { if (!$this->isMainRequest($event)) { return; @@ -60,16 +63,11 @@ public function handleKernelRequestEvent(RequestListenerRequestEvent $event): vo return; } - $token = null; $userData = new UserDataBag(); $userData->setIpAddress($event->getRequest()->getClientIp()); if (null !== $this->tokenStorage) { - $token = $this->tokenStorage->getToken(); - } - - if ($this->isTokenAuthenticated($token)) { - $userData->setUsername($this->getUsername($token->getUser())); + $this->setUserData($userData, $this->tokenStorage->getToken()); } $this->hub->configureScope(static function (Scope $scope) use ($userData): void { @@ -81,9 +79,9 @@ public function handleKernelRequestEvent(RequestListenerRequestEvent $event): vo * This method is called for each request handled by the framework and * sets the route on the current Sentry scope. * - * @param RequestListenerControllerEvent $event The event + * @param ControllerEvent $event The event */ - public function handleKernelControllerEvent(RequestListenerControllerEvent $event): void + public function handleKernelControllerEvent(ControllerEvent $event): void { if (!$this->isMainRequest($event)) { return; @@ -101,7 +99,7 @@ public function handleKernelControllerEvent(RequestListenerControllerEvent $even } /** - * @param UserInterface|object|string $user + * @param UserInterface|object|string|null $user */ private function getUsername($user): ?string { @@ -126,12 +124,32 @@ private function getUsername($user): ?string return null; } - private function isTokenAuthenticated(?TokenInterface $token): bool + private function getImpersonatorUser(TokenInterface $token): ?string { - if (null === $token) { - return false; + if (!$token instanceof SwitchUserToken) { + return null; } + return $this->getUsername($token->getOriginalToken()->getUser()); + } + + private function setUserData(UserDataBag $userData, ?TokenInterface $token): void + { + if (null === $token || !$this->isTokenAuthenticated($token)) { + return; + } + + $userData->setUsername($this->getUsername($token->getUser())); + + $impersonatorUser = $this->getImpersonatorUser($token); + + if (null !== $impersonatorUser) { + $userData->setMetadata('impersonator_username', $impersonatorUser); + } + } + + private function isTokenAuthenticated(TokenInterface $token): bool + { if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated(false)) { return false; } diff --git a/src/EventListener/SubRequestListener.php b/src/EventListener/SubRequestListener.php index c835cc31..68c26d63 100644 --- a/src/EventListener/SubRequestListener.php +++ b/src/EventListener/SubRequestListener.php @@ -6,6 +6,7 @@ use Sentry\State\HubInterface; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; /** * This listener ensures that a new {@see \Sentry\State\Scope} is created for @@ -34,9 +35,9 @@ public function __construct(HubInterface $hub) * This method is called for each subrequest handled by the framework and * pushes a new {@see \Sentry\State\Scope} onto the stack. * - * @param SubRequestListenerRequestEvent $event The event + * @param RequestEvent $event The event */ - public function handleKernelRequestEvent(SubRequestListenerRequestEvent $event): void + public function handleKernelRequestEvent(RequestEvent $event): void { if ($this->isMainRequest($event)) { return; diff --git a/src/EventListener/TracingRequestListener.php b/src/EventListener/TracingRequestListener.php index c9ac2f0a..4ed7264d 100644 --- a/src/EventListener/TracingRequestListener.php +++ b/src/EventListener/TracingRequestListener.php @@ -8,6 +8,8 @@ use Sentry\Tracing\TransactionContext; use Sentry\Tracing\TransactionSource; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\TerminateEvent; /** * This event listener acts on the master requests and starts a transaction @@ -21,9 +23,9 @@ final class TracingRequestListener extends AbstractTracingRequestListener * This method is called for each subrequest handled by the framework and * starts a new {@see Transaction}. * - * @param RequestListenerRequestEvent $event The event + * @param RequestEvent $event The event */ - public function handleKernelRequestEvent(RequestListenerRequestEvent $event): void + public function handleKernelRequestEvent(RequestEvent $event): void { if (!$this->isMainRequest($event)) { return; @@ -52,9 +54,9 @@ public function handleKernelRequestEvent(RequestListenerRequestEvent $event): vo * This method is called for each request handled by the framework and * ends the tracing on terminate after the client received the response. * - * @param RequestListenerTerminateEvent $event The event + * @param TerminateEvent $event The event */ - public function handleKernelTerminateEvent(RequestListenerTerminateEvent $event): void + public function handleKernelTerminateEvent(TerminateEvent $event): void { $transaction = $this->hub->getTransaction(); diff --git a/src/EventListener/TracingSubRequestListener.php b/src/EventListener/TracingSubRequestListener.php index 298a8839..b61b15b4 100644 --- a/src/EventListener/TracingSubRequestListener.php +++ b/src/EventListener/TracingSubRequestListener.php @@ -7,6 +7,7 @@ use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; /** * This event listener acts on the sub requests and starts a child span of the @@ -18,9 +19,9 @@ final class TracingSubRequestListener extends AbstractTracingRequestListener * This method is called for each subrequest handled by the framework and * traces each by starting a new {@see Span}. * - * @param SubRequestListenerRequestEvent $event The event + * @param RequestEvent $event The event */ - public function handleKernelRequestEvent(SubRequestListenerRequestEvent $event): void + public function handleKernelRequestEvent(RequestEvent $event): void { if ($this->isMainRequest($event)) { return; diff --git a/src/Resources/config/schema/sentry-1.0.xsd b/src/Resources/config/schema/sentry-1.0.xsd index 79a6aac4..a140d46c 100644 --- a/src/Resources/config/schema/sentry-1.0.xsd +++ b/src/Resources/config/schema/sentry-1.0.xsd @@ -23,6 +23,7 @@ + @@ -91,6 +92,7 @@ + @@ -117,4 +119,8 @@ + + + + diff --git a/src/SentryBundle.php b/src/SentryBundle.php index 4098f8b3..51804144 100644 --- a/src/SentryBundle.php +++ b/src/SentryBundle.php @@ -6,6 +6,7 @@ use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass; use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass; +use Sentry\SentryBundle\DependencyInjection\Compiler\HttpClientTracingPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -13,11 +14,14 @@ final class SentryBundle extends Bundle { public const SDK_IDENTIFIER = 'sentry.php.symfony'; + public const SDK_VERSION = '4.3.1'; + public function build(ContainerBuilder $container): void { parent::build($container); $container->addCompilerPass(new DbalTracingPass()); $container->addCompilerPass(new CacheTracingPass()); + $container->addCompilerPass(new HttpClientTracingPass()); } } diff --git a/src/Tracing/HttpClient/AbstractTraceableHttpClient.php b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php new file mode 100644 index 00000000..f4a2a35e --- /dev/null +++ b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php @@ -0,0 +1,120 @@ +client = $client; + $this->hub = $hub; + } + + /** + * {@inheritdoc} + */ + public function request(string $method, string $url, array $options = []): ResponseInterface + { + $span = null; + $parent = $this->hub->getSpan(); + + if (null !== $parent) { + $headers = $options['headers'] ?? []; + $headers['sentry-trace'] = $parent->toTraceparent(); + + $uri = new Uri($url); + + // Check if the request destination is allow listed in the trace_propagation_targets option. + $client = $this->hub->getClient(); + if (null !== $client) { + $sdkOptions = $client->getOptions(); + + if (\in_array($uri->getHost(), $sdkOptions->getTracePropagationTargets())) { + $headers['baggage'] = $parent->toBaggage(); + } + } + + $options['headers'] = $headers; + + $formattedUri = $this->formatUri($uri); + + $context = new SpanContext(); + $context->setOp('http.client'); + $context->setDescription($method . ' ' . $formattedUri); + $context->setTags([ + 'http.method' => $method, + 'http.url' => $formattedUri, + ]); + + $span = $parent->startChild($context); + } + + return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span); + } + + /** + * {@inheritdoc} + */ + public function stream($responses, float $timeout = null): ResponseStreamInterface + { + if ($responses instanceof AbstractTraceableResponse) { + $responses = [$responses]; + } elseif (!is_iterable($responses)) { + throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses))); + } + + return new ResponseStream(AbstractTraceableResponse::stream($this->client, $responses, $timeout)); + } + + public function reset(): void + { + if ($this->client instanceof ResetInterface) { + $this->client->reset(); + } + } + + /** + * {@inheritdoc} + */ + public function setLogger(LoggerInterface $logger): void + { + if ($this->client instanceof LoggerAwareInterface) { + $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/src/Tracing/HttpClient/AbstractTraceableResponse.php b/src/Tracing/HttpClient/AbstractTraceableResponse.php new file mode 100644 index 00000000..71571294 --- /dev/null +++ b/src/Tracing/HttpClient/AbstractTraceableResponse.php @@ -0,0 +1,131 @@ +client = $client; + $this->response = $response; + $this->span = $span; + } + + public function __destruct() + { + try { + if (method_exists($this->response, '__destruct')) { + $this->response->__destruct(); + } + } finally { + $this->finishSpan(); + } + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Serializing instances of this class is forbidden.'); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Unserializing instances of this class is forbidden.'); + } + + public function getStatusCode(): int + { + return $this->response->getStatusCode(); + } + + public function getHeaders(bool $throw = true): array + { + return $this->response->getHeaders($throw); + } + + public function getContent(bool $throw = true): string + { + try { + return $this->response->getContent($throw); + } finally { + $this->finishSpan(); + } + } + + public function toArray(bool $throw = true): array + { + try { + return $this->response->toArray($throw); + } finally { + $this->finishSpan(); + } + } + + public function cancel(): void + { + $this->response->cancel(); + $this->finishSpan(); + } + + /** + * @internal + * + * @param iterable $responses + * + * @return \Generator + */ + public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator + { + /** @var \SplObjectStorage $traceableMap */ + $traceableMap = new \SplObjectStorage(); + $wrappedResponses = []; + + foreach ($responses as $response) { + if (!$response instanceof self) { + throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($response))); + } + + $traceableMap[$response->response] = $response; + $wrappedResponses[] = $response->response; + } + + foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) { + $traceableResponse = $traceableMap[$response]; + $traceableResponse->finishSpan(); + + yield $traceableResponse => $chunk; + } + } + + private function finishSpan(): void + { + if (null !== $this->span) { + $this->span->finish(); + $this->span = null; + } + } +} diff --git a/src/Tracing/HttpClient/TraceableHttpClientForV4.php b/src/Tracing/HttpClient/TraceableHttpClientForV4.php new file mode 100644 index 00000000..63e31dd9 --- /dev/null +++ b/src/Tracing/HttpClient/TraceableHttpClientForV4.php @@ -0,0 +1,12 @@ +client = $this->client->withOptions($options); + + return $clone; + } +} diff --git a/src/Tracing/HttpClient/TraceableHttpClientForV6.php b/src/Tracing/HttpClient/TraceableHttpClientForV6.php new file mode 100644 index 00000000..ab9e0c8e --- /dev/null +++ b/src/Tracing/HttpClient/TraceableHttpClientForV6.php @@ -0,0 +1,22 @@ +client = $this->client->withOptions($options); + + return $clone; + } +} diff --git a/src/Tracing/HttpClient/TraceableResponseForV4.php b/src/Tracing/HttpClient/TraceableResponseForV4.php new file mode 100644 index 00000000..0cbaedad --- /dev/null +++ b/src/Tracing/HttpClient/TraceableResponseForV4.php @@ -0,0 +1,19 @@ +response->getInfo($type); + } +} diff --git a/src/Tracing/HttpClient/TraceableResponseForV5.php b/src/Tracing/HttpClient/TraceableResponseForV5.php new file mode 100644 index 00000000..0ca572a2 --- /dev/null +++ b/src/Tracing/HttpClient/TraceableResponseForV5.php @@ -0,0 +1,31 @@ +response->getInfo($type); + } + + /** + * {@inheritdoc} + */ + public function toStream(bool $throw = true) + { + return $this->response->toStream($throw); + } +} diff --git a/src/Tracing/HttpClient/TraceableResponseForV6.php b/src/Tracing/HttpClient/TraceableResponseForV6.php new file mode 100644 index 00000000..43dbbbf2 --- /dev/null +++ b/src/Tracing/HttpClient/TraceableResponseForV6.php @@ -0,0 +1,29 @@ +response->getInfo($type); + } + + /** + * {@inheritdoc} + */ + public function toStream(bool $throw = true) + { + return $this->response->toStream($throw); + } +} diff --git a/src/Tracing/Twig/TwigTracingExtension.php b/src/Tracing/Twig/TwigTracingExtension.php index dda1ce1c..ec06e58c 100644 --- a/src/Tracing/Twig/TwigTracingExtension.php +++ b/src/Tracing/Twig/TwigTracingExtension.php @@ -7,7 +7,6 @@ use Sentry\State\HubInterface; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; -use SplObjectStorage; use Twig\Extension\AbstractExtension; use Twig\Profiler\NodeVisitor\ProfilerNodeVisitor; use Twig\Profiler\Profile; @@ -20,7 +19,7 @@ final class TwigTracingExtension extends AbstractExtension private $hub; /** - * @var SplObjectStorage The currently active spans + * @var \SplObjectStorage The currently active spans */ private $spans; @@ -30,7 +29,7 @@ final class TwigTracingExtension extends AbstractExtension public function __construct(HubInterface $hub) { $this->hub = $hub; - $this->spans = new SplObjectStorage(); + $this->spans = new \SplObjectStorage(); } /** diff --git a/src/Transport/TransportFactory.php b/src/Transport/TransportFactory.php index 4a74dce0..6ddd272b 100644 --- a/src/Transport/TransportFactory.php +++ b/src/Transport/TransportFactory.php @@ -6,7 +6,6 @@ use Http\Client\HttpAsyncClient as HttpAsyncClientInterface; use Http\Discovery\Psr17FactoryDiscovery; -use Jean85\PrettyVersions; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; @@ -14,6 +13,7 @@ use Psr\Log\LoggerInterface; use Sentry\HttpClient\HttpClientFactory; use Sentry\Options; +use Sentry\SentryBundle\SentryBundle; use Sentry\Transport\DefaultTransportFactory; use Sentry\Transport\TransportFactoryInterface; use Sentry\Transport\TransportInterface; @@ -52,7 +52,7 @@ public function __construct( $streamFactory, $httpClient, 'sentry.php.symfony', - PrettyVersions::getVersion('sentry/sentry-symfony')->getPrettyVersion() + SentryBundle::SDK_VERSION ), $logger ); diff --git a/src/aliases.php b/src/aliases.php index e5a62c8f..839a55c9 100644 --- a/src/aliases.php +++ b/src/aliases.php @@ -5,85 +5,30 @@ namespace Sentry\SentryBundle; use Doctrine\DBAL\Result; -use Sentry\SentryBundle\EventListener\ErrorListenerExceptionEvent; -use Sentry\SentryBundle\EventListener\RequestListenerControllerEvent; -use Sentry\SentryBundle\EventListener\RequestListenerRequestEvent; -use Sentry\SentryBundle\EventListener\RequestListenerResponseEvent; -use Sentry\SentryBundle\EventListener\RequestListenerTerminateEvent; -use Sentry\SentryBundle\EventListener\SubRequestListenerRequestEvent; use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapter; use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV2; use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV3; use Sentry\SentryBundle\Tracing\Cache\TraceableTagAwareCacheAdapter; use Sentry\SentryBundle\Tracing\Cache\TraceableTagAwareCacheAdapterForV2; use Sentry\SentryBundle\Tracing\Cache\TraceableTagAwareCacheAdapterForV3; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriver; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverForV2; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverForV3; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatement; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatementForV2; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatementForV3; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClient; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClientForV4; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClientForV5; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClientForV6; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableResponse; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableResponseForV4; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableResponseForV5; +use Sentry\SentryBundle\Tracing\HttpClient\TraceableResponseForV6; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\DoctrineProvider; -use Symfony\Component\HttpKernel\Event\ControllerEvent; -use Symfony\Component\HttpKernel\Event\ExceptionEvent; -use Symfony\Component\HttpKernel\Event\FilterControllerEvent; -use Symfony\Component\HttpKernel\Event\FilterResponseEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; -use Symfony\Component\HttpKernel\Event\PostResponseEvent; -use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\HttpKernel\Event\TerminateEvent; -use Symfony\Component\HttpKernel\Kernel; - -if (version_compare(Kernel::VERSION, '4.3.0', '>=')) { - if (!class_exists(ErrorListenerExceptionEvent::class, false)) { - class_alias(ExceptionEvent::class, ErrorListenerExceptionEvent::class); - } - - if (!class_exists(RequestListenerRequestEvent::class, false)) { - class_alias(RequestEvent::class, RequestListenerRequestEvent::class); - } - - if (!class_exists(RequestListenerControllerEvent::class, false)) { - class_alias(ControllerEvent::class, RequestListenerControllerEvent::class); - } - - if (!class_exists(RequestListenerResponseEvent::class, false)) { - class_alias(ResponseEvent::class, RequestListenerResponseEvent::class); - } - - if (!class_exists(RequestListenerTerminateEvent::class, false)) { - class_alias(TerminateEvent::class, RequestListenerTerminateEvent::class); - } - - if (!class_exists(SubRequestListenerRequestEvent::class, false)) { - class_alias(RequestEvent::class, SubRequestListenerRequestEvent::class); - } -} else { - if (!class_exists(ErrorListenerExceptionEvent::class, false)) { - class_alias(GetResponseForExceptionEvent::class, ErrorListenerExceptionEvent::class); - } - - if (!class_exists(RequestListenerRequestEvent::class, false)) { - class_alias(GetResponseEvent::class, RequestListenerRequestEvent::class); - } - - if (!class_exists(RequestListenerControllerEvent::class, false)) { - class_alias(FilterControllerEvent::class, RequestListenerControllerEvent::class); - } - - if (!class_exists(RequestListenerResponseEvent::class, false)) { - class_alias(FilterResponseEvent::class, RequestListenerResponseEvent::class); - } - - if (!class_exists(RequestListenerTerminateEvent::class, false)) { - class_alias(PostResponseEvent::class, RequestListenerTerminateEvent::class); - } - - if (!class_exists(SubRequestListenerRequestEvent::class, false)) { - class_alias(GetResponseEvent::class, SubRequestListenerRequestEvent::class); - } -} +use Symfony\Component\HttpClient\Response\StreamableInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; if (interface_exists(AdapterInterface::class)) { if (!class_exists(DoctrineProvider::class, false) && version_compare(\PHP_VERSION, '8.0.0', '>=')) { @@ -105,12 +50,25 @@ class_alias(TraceableTagAwareCacheAdapterForV2::class, TraceableTagAwareCacheAda } } -if (!class_exists('Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatement')) { +if (!class_exists(TracingStatement::class)) { if (class_exists(Result::class)) { - class_alias(TracingStatementForV3::class, 'Sentry\\SentryBundle\\Tracing\\Doctrine\\DBAL\\TracingStatement'); - class_alias(TracingDriverForV3::class, 'Sentry\\SentryBundle\\Tracing\\Doctrine\\DBAL\\TracingDriver'); + class_alias(TracingStatementForV3::class, TracingStatement::class); + class_alias(TracingDriverForV3::class, TracingDriver::class); } elseif (interface_exists(Result::class)) { - class_alias(TracingStatementForV2::class, 'Sentry\\SentryBundle\\Tracing\\Doctrine\\DBAL\\TracingStatement'); - class_alias(TracingDriverForV2::class, 'Sentry\\SentryBundle\\Tracing\\Doctrine\\DBAL\\TracingDriver'); + class_alias(TracingStatementForV2::class, TracingStatement::class); + class_alias(TracingDriverForV2::class, TracingDriver::class); + } +} + +if (!class_exists(TraceableResponse::class) && interface_exists(ResponseInterface::class)) { + if (!interface_exists(StreamableInterface::class)) { + class_alias(TraceableResponseForV4::class, TraceableResponse::class); + class_alias(TraceableHttpClientForV4::class, TraceableHttpClient::class); + } elseif (version_compare(\PHP_VERSION, '8.0', '>=')) { + class_alias(TraceableResponseForV6::class, TraceableResponse::class); + class_alias(TraceableHttpClientForV6::class, TraceableHttpClient::class); + } else { + class_alias(TraceableResponseForV5::class, TraceableResponse::class); + class_alias(TraceableHttpClientForV5::class, TraceableHttpClient::class); } } diff --git a/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php b/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php new file mode 100644 index 00000000..9f3d12a7 --- /dev/null +++ b/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php @@ -0,0 +1,85 @@ +createContainerBuilder(true, true); + $container->compile(); + + $this->assertSame(TraceableHttpClient::class, $container->findDefinition('http.client')->getClass()); + } + + /** + * @dataProvider processDoesNothingIfConditionsForEnablingTracingAreMissingDataProvider + */ + public function testProcessDoesNothingIfConditionsForEnablingTracingAreMissing(bool $isTracingEnabled, bool $isHttpClientTracingEnabled): void + { + $container = $this->createContainerBuilder($isTracingEnabled, $isHttpClientTracingEnabled); + $container->compile(); + + $this->assertSame(HttpClient::class, $container->getDefinition('http.client')->getClass()); + } + + /** + * @return \Generator + */ + public function processDoesNothingIfConditionsForEnablingTracingAreMissingDataProvider(): \Generator + { + yield [ + true, + false, + ]; + + yield [ + false, + false, + ]; + + yield [ + false, + true, + ]; + } + + private function createContainerBuilder(bool $isTracingEnabled, bool $isHttpClientTracingEnabled): ContainerBuilder + { + $container = new ContainerBuilder(); + $container->addCompilerPass(new HttpClientTracingPass()); + $container->setParameter('sentry.tracing.enabled', $isTracingEnabled); + $container->setParameter('sentry.tracing.http_client.enabled', $isHttpClientTracingEnabled); + + $container->register(HubInterface::class, HubInterface::class) + ->setPublic(true); + + $container->register('http.client', HttpClient::class) + ->setPublic(true) + ->addTag('http_client.client'); + + return $container; + } + + private static function isHttpClientPackageInstalled(): bool + { + return interface_exists(HttpClientInterface::class); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 5f73cfde..76aaeaf8 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -12,6 +12,7 @@ use Symfony\Component\Cache\CacheItem; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Messenger\MessageBusInterface; final class ConfigurationTest extends TestCase @@ -25,6 +26,7 @@ public function testProcessConfigurationWithDefaultConfiguration(): void 'options' => [ 'integrations' => [], 'prefixes' => array_merge(['%kernel.project_dir%'], array_filter(explode(\PATH_SEPARATOR, get_include_path() ?: ''))), + 'trace_propagation_targets' => [], 'environment' => '%kernel.environment%', 'release' => PrettyVersions::getRootPackageVersion()->getPrettyVersion(), 'tags' => [], @@ -52,6 +54,9 @@ public function testProcessConfigurationWithDefaultConfiguration(): void 'cache' => [ 'enabled' => class_exists(CacheItem::class), ], + 'http_client' => [ + 'enabled' => class_exists(HttpClient::class), + ], 'console' => [ 'excluded_commands' => ['messenger:consume'], ], diff --git a/tests/DependencyInjection/Fixtures/php/full.php b/tests/DependencyInjection/Fixtures/php/full.php index ae7f861e..cf52ebf9 100644 --- a/tests/DependencyInjection/Fixtures/php/full.php +++ b/tests/DependencyInjection/Fixtures/php/full.php @@ -17,6 +17,7 @@ 'sample_rate' => 1, 'traces_sample_rate' => 1, 'traces_sampler' => 'App\\Sentry\\Tracing\\TracesSampler', + 'trace_propagation_targets' => ['website.invalid'], 'attach_stacktrace' => true, 'context_lines' => 0, 'enable_compression' => true, @@ -51,6 +52,9 @@ 'enabled' => false, 'connections' => ['default'], ], + 'http_client' => [ + 'enabled' => false, + ], 'twig' => [ 'enabled' => false, ], diff --git a/tests/DependencyInjection/Fixtures/php/http_client_tracing_enabled.php b/tests/DependencyInjection/Fixtures/php/http_client_tracing_enabled.php new file mode 100644 index 00000000..639e2f9a --- /dev/null +++ b/tests/DependencyInjection/Fixtures/php/http_client_tracing_enabled.php @@ -0,0 +1,14 @@ +loadFromExtension('sentry', [ + 'tracing' => [ + 'http_client' => [ + 'enabled' => true, + ], + ], +]); diff --git a/tests/DependencyInjection/Fixtures/xml/full.xml b/tests/DependencyInjection/Fixtures/xml/full.xml index cab3d83a..bab488bb 100644 --- a/tests/DependencyInjection/Fixtures/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/xml/full.xml @@ -36,6 +36,7 @@ max-request-body-size="none" > App\Sentry\Integration\FooIntegration + website.invalid %kernel.project_dir% development %kernel.cache_dir% @@ -49,6 +50,7 @@ + app:command diff --git a/tests/DependencyInjection/Fixtures/xml/http_client_tracing_enabled.xml b/tests/DependencyInjection/Fixtures/xml/http_client_tracing_enabled.xml new file mode 100644 index 00000000..cb0621ab --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/http_client_tracing_enabled.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/tests/DependencyInjection/Fixtures/yml/full.yml b/tests/DependencyInjection/Fixtures/yml/full.yml index 19981f21..d0799679 100644 --- a/tests/DependencyInjection/Fixtures/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/yml/full.yml @@ -12,6 +12,8 @@ sentry: sample_rate: 1 traces_sample_rate: 1 traces_sampler: App\Sentry\Tracing\TracesSampler + trace_propagation_targets: + - 'website.invalid' attach_stacktrace: true context_lines: 0 enable_compression: true @@ -50,6 +52,8 @@ sentry: enabled: false cache: enabled: false + http_client: + enabled: false console: excluded_commands: - app:command diff --git a/tests/DependencyInjection/Fixtures/yml/http_client_tracing_enabled.yml b/tests/DependencyInjection/Fixtures/yml/http_client_tracing_enabled.yml new file mode 100644 index 00000000..50d56ede --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/http_client_tracing_enabled.yml @@ -0,0 +1,4 @@ +sentry: + tracing: + http_client: + enabled: true diff --git a/tests/DependencyInjection/SentryExtensionTest.php b/tests/DependencyInjection/SentryExtensionTest.php index eaafea6d..d2055e70 100644 --- a/tests/DependencyInjection/SentryExtensionTest.php +++ b/tests/DependencyInjection/SentryExtensionTest.php @@ -5,7 +5,6 @@ namespace Sentry\SentryBundle\Tests\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Jean85\PrettyVersions; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; use Sentry\ClientInterface; @@ -35,6 +34,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\ErrorHandler\Error\FatalError; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; @@ -202,6 +202,7 @@ public function testClientIsCreatedFromOptions(): void 'sample_rate' => 1, 'traces_sample_rate' => 1, 'traces_sampler' => new Reference('App\\Sentry\\Tracing\\TracesSampler'), + 'trace_propagation_targets' => ['website.invalid'], 'attach_stacktrace' => true, 'context_lines' => 0, 'enable_compression' => true, @@ -243,7 +244,7 @@ public function testClientIsCreatedFromOptions(): void $this->assertCount(6, $methodCalls); $this->assertDefinitionMethodCallAt($methodCalls[0], 'setSdkIdentifier', [SentryBundle::SDK_IDENTIFIER]); - $this->assertDefinitionMethodCallAt($methodCalls[1], 'setSdkVersion', [PrettyVersions::getVersion('sentry/sentry-symfony')->getPrettyVersion()]); + $this->assertDefinitionMethodCallAt($methodCalls[1], 'setSdkVersion', [SentryBundle::SDK_VERSION]); $this->assertDefinitionMethodCallAt($methodCalls[2], 'setTransportFactory', [new Reference('App\\Sentry\\Transport\\TransportFactory')]); $this->assertDefinitionMethodCallAt($methodCalls[5], 'setLogger', [new Reference('app.logger')]); @@ -380,6 +381,18 @@ public function testTwigTracingExtensionIsConfiguredWhenTwigTracingIsEnabled(): $this->assertTrue($container->hasDefinition(TwigTracingExtension::class)); } + public function testHttpClientTracingExtensionIsConfiguredWhenHttpClientTracingIsEnabled(): void + { + if (!class_exists(HttpClient::class)) { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Http client tracing support cannot be enabled because the symfony/http-client Composer package is not installed.'); + } + + $container = $this->createContainerFromFixture('http_client_tracing_enabled'); + + $this->assertTrue($container->getParameter('sentry.tracing.http_client.enabled')); + } + public function testTwigTracingExtensionIsRemovedWhenTwigTracingIsDisabled(): void { $container = $this->createContainerFromFixture('full'); diff --git a/tests/EventListener/ErrorListenerTest.php b/tests/EventListener/ErrorListenerTest.php index 163ca961..f97ff438 100644 --- a/tests/EventListener/ErrorListenerTest.php +++ b/tests/EventListener/ErrorListenerTest.php @@ -10,9 +10,7 @@ use Sentry\State\HubInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ExceptionEvent; -use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\Kernel; final class ErrorListenerTest extends TestCase { @@ -33,16 +31,13 @@ protected function setUp(): void } /** - * @dataProvider handleExceptionEventForSymfonyVersionAtLeast43DataProvider - * @dataProvider handleExceptionEventForSymfonyVersionLowerThan43DataProvider - * - * @param ExceptionEvent|GetResponseForExceptionEvent $event + * @dataProvider handleExceptionEventDataProvider */ - public function testHandleExceptionEvent($event): void + public function testHandleExceptionEvent(ExceptionEvent $event): void { $this->hub->expects($this->once()) ->method('captureException') - ->with($event instanceof ExceptionEvent ? $event->getThrowable() : $event->getException()); + ->with($event->getThrowable()); $this->listener->handleExceptionEvent($event); } @@ -50,31 +45,8 @@ public function testHandleExceptionEvent($event): void /** * @return \Generator */ - public function handleExceptionEventForSymfonyVersionLowerThan43DataProvider(): \Generator + public function handleExceptionEventDataProvider(): \Generator { - if (version_compare(Kernel::VERSION, '4.3.0', '>=')) { - return; - } - - yield [ - new GetResponseForExceptionEvent( - $this->createMock(HttpKernelInterface::class), - new Request(), - HttpKernelInterface::MASTER_REQUEST, - new \Exception() - ), - ]; - } - - /** - * @return \Generator - */ - public function handleExceptionEventForSymfonyVersionAtLeast43DataProvider(): \Generator - { - if (version_compare(Kernel::VERSION, '4.3.0', '<')) { - return; - } - yield [ new ExceptionEvent( $this->createMock(HttpKernelInterface::class), diff --git a/tests/EventListener/Fixtures/UserWithIdentifierStub.php b/tests/EventListener/Fixtures/UserWithIdentifierStub.php index bdeb2aec..9b82b8ea 100644 --- a/tests/EventListener/Fixtures/UserWithIdentifierStub.php +++ b/tests/EventListener/Fixtures/UserWithIdentifierStub.php @@ -8,6 +8,16 @@ final class UserWithIdentifierStub implements UserInterface { + /** + * @var string + */ + private $username; + + public function __construct(string $username = 'foo_user') + { + $this->username = $username; + } + public function getUserIdentifier(): string { return $this->getUsername(); @@ -15,7 +25,7 @@ public function getUserIdentifier(): string public function getUsername(): string { - return 'foo_user'; + return $this->username; } public function getRoles(): array diff --git a/tests/EventListener/MessengerListenerTest.php b/tests/EventListener/MessengerListenerTest.php index 04976652..af7b8462 100644 --- a/tests/EventListener/MessengerListenerTest.php +++ b/tests/EventListener/MessengerListenerTest.php @@ -9,7 +9,6 @@ use Sentry\ClientInterface; use Sentry\Event; use Sentry\SentryBundle\EventListener\MessengerListener; -use Sentry\SentryBundle\Tests\End2End\App\Kernel; use Sentry\State\HubInterface; use Sentry\State\Scope; use Symfony\Component\Messenger\Envelope; @@ -194,10 +193,6 @@ public function testHandleWorkerMessageHandledEvent(): void private function getMessageFailedEvent(Envelope $envelope, string $receiverName, \Throwable $error, bool $retry): WorkerMessageFailedEvent { - if (version_compare(Kernel::VERSION, '4.4.0', '<')) { - return new WorkerMessageFailedEvent($envelope, $receiverName, $error, $retry); - } - $event = new WorkerMessageFailedEvent($envelope, $receiverName, $error); if ($retry) { diff --git a/tests/EventListener/RequestListenerTest.php b/tests/EventListener/RequestListenerTest.php index 863dddbd..9ee2803a 100644 --- a/tests/EventListener/RequestListenerTest.php +++ b/tests/EventListener/RequestListenerTest.php @@ -17,13 +17,12 @@ use Sentry\UserDataBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; -use Symfony\Component\HttpKernel\Event\FilterControllerEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -52,12 +51,12 @@ protected function setUp(): void } /** - * @dataProvider handleKernelRequestEventForSymfonyVersionAtLeast43DataProvider - * @dataProvider handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider - * - * @param GetResponseEvent|RequestEvent $requestEvent + * @dataProvider handleKernelRequestEventDataProvider + * @dataProvider handleKernelRequestEventForSymfonyVersionLowerThan54DataProvider + * @dataProvider handleKernelRequestEventForSymfonyVersionEqualTo54DataProvider + * @dataProvider handleKernelRequestEventForSymfonyVersionGreaterThan54DataProvider */ - public function testHandleKernelRequestEvent($requestEvent, ?ClientInterface $client, ?TokenInterface $token, ?UserDataBag $expectedUser): void + public function testHandleKernelRequestEvent(RequestEvent $requestEvent, ?ClientInterface $client, ?TokenInterface $token, ?UserDataBag $expectedUser): void { $scope = new Scope(); @@ -86,14 +85,10 @@ public function testHandleKernelRequestEvent($requestEvent, ?ClientInterface $cl /** * @return \Generator */ - public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider(): \Generator + public function handleKernelRequestEventDataProvider(): \Generator { - if (version_compare(Kernel::VERSION, '4.3.0', '>=')) { - return; - } - yield 'event.requestType != MASTER_REQUEST' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::SUB_REQUEST @@ -104,7 +99,7 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider ]; yield 'options.send_default_pii = FALSE' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -115,7 +110,7 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider ]; yield 'token IS NULL' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -125,8 +120,29 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider UserDataBag::createFromUserIpAddress('127.0.0.1'), ]; + yield 'request.clientIp IS NULL' => [ + new RequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request(), + HttpKernelInterface::MASTER_REQUEST + ), + $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), + null, + new UserDataBag(), + ]; + } + + /** + * @return \Generator + */ + public function handleKernelRequestEventForSymfonyVersionLowerThan54DataProvider(): \Generator + { + if (version_compare(Kernel::VERSION, '5.4.0', '>=')) { + return; + } + yield 'token.authenticated = FALSE' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -137,7 +153,7 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider ]; yield 'token.authenticated = TRUE && token.user IS NULL' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -148,7 +164,7 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider ]; yield 'token.authenticated = TRUE && token.user INSTANCEOF string' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -159,7 +175,7 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider ]; yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method DOES NOT EXISTS' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -169,19 +185,8 @@ public function handleKernelRequestEventForSymfonyVersionLowerThan43DataProvider new UserDataBag(null, null, '127.0.0.1', 'foo_user'), ]; - yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method EXISTS' => [ - new GetResponseEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - yield 'token.authenticated = TRUE && token.user INSTANCEOF object && __toString() method EXISTS' => [ - new GetResponseEvent( + new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST @@ -195,62 +200,102 @@ public function __toString(): string }), new UserDataBag(null, null, '127.0.0.1', 'foo_user'), ]; + + yield 'token.authenticated = TRUE && token INSTANCEOF SwitchUserToken' => [ + new RequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), + HttpKernelInterface::MASTER_REQUEST + ), + $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), + new SwitchUserToken( + new UserWithIdentifierStub(), + '', + 'user_provider', + ['ROLE_USER'], + new AuthenticatedTokenStub(new UserWithIdentifierStub('foo_user_impersonator')) + ), + UserDataBag::createFromArray([ + 'ip_address' => '127.0.0.1', + 'username' => 'foo_user', + 'impersonator_username' => 'foo_user_impersonator', + ]), + ]; } /** * @return \Generator */ - public function handleKernelRequestEventForSymfonyVersionAtLeast43DataProvider(): \Generator + public function handleKernelRequestEventForSymfonyVersionEqualTo54DataProvider(): \Generator { - if (version_compare(Kernel::VERSION, '4.3.0', '<')) { + if (version_compare(Kernel::VERSION, '5.4.0', '!=')) { return; } - yield 'event.requestType != MASTER_REQUEST' => [ + yield 'token.authenticated = FALSE' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::SUB_REQUEST + HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - null, - null, + new UnauthenticatedTokenStub(), + UserDataBag::createFromUserIpAddress('127.0.0.1'), ]; - yield 'options.send_default_pii = FALSE' => [ + yield 'token.authenticated = TRUE && token.user IS NULL' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => false])), - null, - null, + $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), + new AuthenticatedTokenStub(null), + UserDataBag::createFromUserIpAddress('127.0.0.1'), ]; - yield 'token IS NULL' => [ + yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method EXISTS' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - null, - UserDataBag::createFromUserIpAddress('127.0.0.1'), + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new UserDataBag(null, null, '127.0.0.1', 'foo_user'), ]; - yield 'token.authenticated = FALSE' => [ + yield 'token.authenticated = TRUE && token INSTANCEOF SwitchUserToken' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new UnauthenticatedTokenStub(), - UserDataBag::createFromUserIpAddress('127.0.0.1'), + new SwitchUserToken( + new UserWithIdentifierStub(), + 'main', + ['ROLE_USER'], + new AuthenticatedTokenStub(new UserWithIdentifierStub('foo_user_impersonator')) + ), + UserDataBag::createFromArray([ + 'ip_address' => '127.0.0.1', + 'username' => 'foo_user', + 'impersonator_username' => 'foo_user_impersonator', + ]), ]; + } - yield 'token.authenticated = TRUE && token.user IS NULL' => [ + /** + * @return \Generator + */ + public function handleKernelRequestEventForSymfonyVersionGreaterThan54DataProvider(): \Generator + { + if (version_compare(Kernel::VERSION, '5.4.0', '<')) { + return; + } + + yield 'token.user IS NULL' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), @@ -261,47 +306,7 @@ public function handleKernelRequestEventForSymfonyVersionAtLeast43DataProvider() UserDataBag::createFromUserIpAddress('127.0.0.1'), ]; - if (version_compare(Kernel::VERSION, '6.0.0', '<')) { - yield 'token.authenticated = TRUE && token.user INSTANCEOF string' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub('foo_user'), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - - yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method DOES NOT EXISTS' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new UserWithoutIdentifierStub()), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - - yield 'token.authenticated = TRUE && token.user INSTANCEOF object && __toString() method EXISTS' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new class() implements \Stringable { - public function __toString(): string - { - return 'foo_user'; - } - }), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - } - - yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method EXISTS' => [ + yield 'token.user INSTANCEOF UserInterface' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), @@ -312,26 +317,33 @@ public function __toString(): string new UserDataBag(null, null, '127.0.0.1', 'foo_user'), ]; - yield 'request.clientIp IS NULL' => [ + yield 'token INSTANCEOF SwitchUserToken' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), - new Request(), + new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - null, - new UserDataBag(), + new SwitchUserToken( + new UserWithIdentifierStub(), + 'main', + ['ROLE_USER'], + new AuthenticatedTokenStub(new UserWithIdentifierStub('foo_user_impersonator')) + ), + UserDataBag::createFromArray([ + 'ip_address' => '127.0.0.1', + 'username' => 'foo_user', + 'impersonator_username' => 'foo_user_impersonator', + ]), ]; } /** - * @dataProvider handleKernelControllerEventWithSymfonyVersionAtLeast43DataProvider - * @dataProvider handleKernelControllerEventWithSymfonyVersionLowerThan43DataProvider + * @dataProvider handleKernelControllerEventDataProvider * - * @param ControllerEvent|FilterControllerEvent $controllerEvent - * @param array $expectedTags + * @param array $expectedTags */ - public function testHandleKernelControllerEvent($controllerEvent, array $expectedTags): void + public function testHandleKernelControllerEvent(ControllerEvent $controllerEvent, array $expectedTags): void { $scope = new Scope(); @@ -352,12 +364,8 @@ public function testHandleKernelControllerEvent($controllerEvent, array $expecte /** * @return \Generator */ - public function handleKernelControllerEventWithSymfonyVersionAtLeast43DataProvider(): \Generator + public function handleKernelControllerEventDataProvider(): \Generator { - if (version_compare(Kernel::VERSION, '4.3.0', '<')) { - return; - } - yield 'event.requestType != MASTER_REQUEST' => [ new ControllerEvent( $this->createMock(HttpKernelInterface::class), @@ -394,51 +402,6 @@ static function () { ]; } - /** - * @return \Generator - */ - public function handleKernelControllerEventWithSymfonyVersionLowerThan43DataProvider(): \Generator - { - if (version_compare(Kernel::VERSION, '4.3.0', '>=')) { - return; - } - - yield 'event.requestType != MASTER_REQUEST' => [ - new FilterControllerEvent( - $this->createMock(HttpKernelInterface::class), - static function () { - }, - new Request([], [], ['_route' => 'homepage']), - HttpKernelInterface::SUB_REQUEST - ), - [], - ]; - - yield 'event.requestType = MASTER_REQUEST && request.attributes._route NOT EXISTS ' => [ - new FilterControllerEvent( - $this->createMock(HttpKernelInterface::class), - static function () { - }, - new Request(), - HttpKernelInterface::MASTER_REQUEST - ), - [], - ]; - - yield 'event.requestType = MASTER_REQUEST && request.attributes._route EXISTS' => [ - new FilterControllerEvent( - $this->createMock(HttpKernelInterface::class), - static function () { - }, - new Request([], [], ['_route' => 'homepage']), - HttpKernelInterface::MASTER_REQUEST - ), - [ - 'route' => 'homepage', - ], - ]; - } - private function getMockedClientWithOptions(Options $options): ClientInterface { $client = $this->createMock(ClientInterface::class); diff --git a/tests/EventListener/SubRequestListenerTest.php b/tests/EventListener/SubRequestListenerTest.php index c67dda25..c8c0b915 100644 --- a/tests/EventListener/SubRequestListenerTest.php +++ b/tests/EventListener/SubRequestListenerTest.php @@ -12,10 +12,8 @@ use Sentry\State\Scope; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\Kernel; class SubRequestListenerTest extends TestCase { @@ -38,12 +36,9 @@ protected function setUp(): void } /** - * @dataProvider handleKernelRequestEventWithSymfonyVersionAtLeast43DataProvider - * @dataProvider handleKernelRequestEventWithSymfonyVersionLowerThan43DataProvider - * - * @param RequestEvent|GetResponseEvent $event + * @dataProvider handleKernelRequestEventDataProvider */ - public function testHandleKernelRequestEvent($event): void + public function testHandleKernelRequestEvent(RequestEvent $event): void { $this->hub->expects($this->isMainRequest($event) ? $this->never() : $this->once()) ->method('pushScope') @@ -55,12 +50,8 @@ public function testHandleKernelRequestEvent($event): void /** * @return \Generator */ - public function handleKernelRequestEventWithSymfonyVersionAtLeast43DataProvider(): \Generator + public function handleKernelRequestEventDataProvider(): \Generator { - if (version_compare(Kernel::VERSION, '4.3.0', '<')) { - return; - } - yield [ new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST), ]; @@ -70,24 +61,6 @@ public function handleKernelRequestEventWithSymfonyVersionAtLeast43DataProvider( ]; } - /** - * @return \Generator - */ - public function handleKernelRequestEventWithSymfonyVersionLowerThan43DataProvider(): \Generator - { - if (version_compare(Kernel::VERSION, '4.3.0', '>=')) { - return; - } - - yield [ - new GetResponseEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST), - ]; - - yield [ - new GetResponseEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::SUB_REQUEST), - ]; - } - /** * @dataProvider handleKernelFinishRequestEventDataProvider * diff --git a/tests/EventListener/TracingConsoleListenerTest.php b/tests/EventListener/TracingConsoleListenerTest.php index 313200af..3987ea04 100644 --- a/tests/EventListener/TracingConsoleListenerTest.php +++ b/tests/EventListener/TracingConsoleListenerTest.php @@ -4,7 +4,6 @@ namespace Sentry\SentryBundle\Tests\EventListener; -use Generator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Sentry\SentryBundle\EventListener\TracingConsoleListener; @@ -65,9 +64,9 @@ public function testHandleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHub } /** - * @return Generator + * @return \Generator */ - public function handleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHubDataProvider(): Generator + public function handleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHubDataProvider(): \Generator { $transactionContext = new TransactionContext(); $transactionContext->setOp('console.command'); @@ -121,9 +120,9 @@ public function testHandleConsoleCommandEventStartsChildSpanIfSpanIsSetOnHub(Com } /** - * @return Generator + * @return \Generator */ - public function handleConsoleCommandEventStartsChildSpanIfSpanIsSetOnHubDataProvider(): Generator + public function handleConsoleCommandEventStartsChildSpanIfSpanIsSetOnHubDataProvider(): \Generator { yield [ new Command(), diff --git a/tests/EventListener/TracingRequestListenerTest.php b/tests/EventListener/TracingRequestListenerTest.php index 9b0d4923..f5cc839c 100644 --- a/tests/EventListener/TracingRequestListenerTest.php +++ b/tests/EventListener/TracingRequestListenerTest.php @@ -8,9 +8,6 @@ use PHPUnit\Framework\TestCase; use Sentry\ClientInterface; use Sentry\Options; -use Sentry\SentryBundle\EventListener\RequestListenerRequestEvent; -use Sentry\SentryBundle\EventListener\RequestListenerResponseEvent; -use Sentry\SentryBundle\EventListener\RequestListenerTerminateEvent; use Sentry\SentryBundle\EventListener\TracingRequestListener; use Sentry\State\HubInterface; use Sentry\Tracing\DynamicSamplingContext; @@ -23,6 +20,9 @@ use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use Symfony\Component\HttpKernel\Event\TerminateEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\Routing\Route; @@ -75,7 +75,7 @@ public function testHandleKernelRequestEvent(Options $options, Request $request, ->method('setSpan') ->with($transaction); - $this->listener->handleKernelRequestEvent(new RequestListenerRequestEvent( + $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST @@ -409,7 +409,7 @@ public function testHandleKernelRequestEventDoesNothingIfRequestTypeIsSubRequest $this->hub->expects($this->never()) ->method('startTransaction'); - $this->listener->handleKernelRequestEvent(new RequestListenerRequestEvent( + $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::SUB_REQUEST @@ -424,7 +424,7 @@ public function testHandleResponseRequestEvent(): void ->method('getSpan') ->willReturn($transaction); - $this->listener->handleKernelResponseEvent(new RequestListenerResponseEvent( + $this->listener->handleKernelResponseEvent(new ResponseEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST, @@ -441,7 +441,7 @@ public function testHandleResponseRequestEventDoesNothingIfNoTransactionIsSetOnH ->method('getSpan') ->willReturn(null); - $this->listener->handleKernelResponseEvent(new RequestListenerResponseEvent( + $this->listener->handleKernelResponseEvent(new ResponseEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST, @@ -460,7 +460,7 @@ public function testHandleKernelTerminateEvent(): void ->method('getTransaction') ->willReturn($transaction); - $this->listener->handleKernelTerminateEvent(new RequestListenerTerminateEvent( + $this->listener->handleKernelTerminateEvent(new TerminateEvent( $this->createMock(HttpKernelInterface::class), new Request(), new Response() @@ -475,7 +475,7 @@ public function testHandleKernelTerminateEventDoesNothingIfNoTransactionIsSetOnH ->method('getTransaction') ->willReturn(null); - $this->listener->handleKernelTerminateEvent(new RequestListenerTerminateEvent( + $this->listener->handleKernelTerminateEvent(new TerminateEvent( $this->createMock(HttpKernelInterface::class), new Request(), new Response() diff --git a/tests/EventListener/TracingSubRequestListenerTest.php b/tests/EventListener/TracingSubRequestListenerTest.php index eb77f58a..9503e2a3 100644 --- a/tests/EventListener/TracingSubRequestListenerTest.php +++ b/tests/EventListener/TracingSubRequestListenerTest.php @@ -6,8 +6,6 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Sentry\SentryBundle\EventListener\RequestListenerResponseEvent; -use Sentry\SentryBundle\EventListener\SubRequestListenerRequestEvent; use Sentry\SentryBundle\EventListener\TracingSubRequestListener; use Sentry\State\HubInterface; use Sentry\Tracing\Span; @@ -15,6 +13,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; final class TracingSubRequestListenerTest extends TestCase @@ -57,7 +57,7 @@ public function testHandleKernelRequestEvent(Request $request, Span $expectedSpa })) ->willReturnSelf(); - $this->listener->handleKernelRequestEvent(new SubRequestListenerRequestEvent( + $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::SUB_REQUEST @@ -143,7 +143,7 @@ public function testHandleKernelRequestEventDoesNothingIfRequestTypeIsMasterRequ $this->hub->expects($this->never()) ->method('getSpan'); - $this->listener->handleKernelRequestEvent(new SubRequestListenerRequestEvent( + $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST @@ -156,7 +156,7 @@ public function testHandleKernelRequestEventDoesNothingIfNoSpanIsSetOnHub(): voi ->method('getSpan') ->willReturn(null); - $this->listener->handleKernelRequestEvent(new SubRequestListenerRequestEvent( + $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::SUB_REQUEST @@ -216,7 +216,7 @@ public function testHandleResponseRequestEvent(): void ->method('getSpan') ->willReturn($span); - $this->listener->handleKernelResponseEvent(new RequestListenerResponseEvent( + $this->listener->handleKernelResponseEvent(new ResponseEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::SUB_REQUEST, @@ -233,7 +233,7 @@ public function testHandleResponseRequestEventDoesNothingIfNoTransactionIsSetOnH ->method('getSpan') ->willReturn(null); - $this->listener->handleKernelResponseEvent(new RequestListenerResponseEvent( + $this->listener->handleKernelResponseEvent(new ResponseEvent( $this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::SUB_REQUEST, diff --git a/tests/Tracing/HttpClient/TraceableHttpClientTest.php b/tests/Tracing/HttpClient/TraceableHttpClientTest.php new file mode 100644 index 00000000..06636d26 --- /dev/null +++ b/tests/Tracing/HttpClient/TraceableHttpClientTest.php @@ -0,0 +1,303 @@ +hub = $this->createMock(HubInterface::class); + $this->decoratedHttpClient = $this->createMock(TestableHttpClientInterface::class); + $this->httpClient = new TraceableHttpClient($this->decoratedHttpClient, $this->hub); + } + + public function testRequest(): void + { + $transaction = new Transaction(new TransactionContext()); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $mockResponse = new MockResponse(); + $decoratedHttpClient = new MockHttpClient($mockResponse); + $httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub); + $response = $httpClient->request('GET', 'https://username:password@www.example.com/test-page?foo=bar#baz'); + + $this->assertInstanceOf(AbstractTraceableResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('GET', $response->getInfo('http_method')); + $this->assertSame('https://username:password@www.example.com/test-page?foo=bar#baz', $response->getInfo('url')); + $this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); + $this->assertArrayNotHasKey('baggage', $mockResponse->getRequestOptions()['normalized_headers']); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + $expectedTags = [ + 'http.method' => 'GET', + 'http.url' => 'https://www.example.com/test-page', + ]; + + $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()); + } + + public function testRequestDoesNotContainBaggageHeader(): void + { + $options = new Options([ + 'dsn' => 'http://public:secret@example.com/sentry/1', + 'trace_propagation_targets' => ['non-matching-host.invalid'], + ]); + $client = $this->createMock(ClientInterface::class); + $client + ->expects($this->once()) + ->method('getOptions') + ->willReturn($options); + + $transaction = new Transaction(new TransactionContext()); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $mockResponse = new MockResponse(); + $decoratedHttpClient = new MockHttpClient($mockResponse); + $httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub); + $response = $httpClient->request('PUT', 'https://www.example.com/test-page'); + + $this->assertInstanceOf(AbstractTraceableResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('PUT', $response->getInfo('http_method')); + $this->assertSame('https://www.example.com/test-page', $response->getInfo('url')); + $this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); + $this->assertArrayNotHasKey('baggage', $mockResponse->getRequestOptions()['normalized_headers']); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + $expectedTags = [ + 'http.method' => 'PUT', + 'http.url' => 'https://www.example.com/test-page', + ]; + + $this->assertCount(2, $spans); + $this->assertNull($spans[1]->getEndTimestamp()); + $this->assertSame('http.client', $spans[1]->getOp()); + $this->assertSame('PUT https://www.example.com/test-page', $spans[1]->getDescription()); + $this->assertSame($expectedTags, $spans[1]->getTags()); + } + + public function testRequestDoesContainBaggageHeader(): void + { + $options = new Options([ + 'dsn' => 'http://public:secret@example.com/sentry/1', + 'trace_propagation_targets' => ['www.example.com'], + ]); + $client = $this->createMock(ClientInterface::class); + $client + ->expects($this->once()) + ->method('getOptions') + ->willReturn($options); + + $transaction = new Transaction(new TransactionContext()); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $mockResponse = new MockResponse(); + $decoratedHttpClient = new MockHttpClient($mockResponse); + $httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub); + $response = $httpClient->request('POST', 'https://www.example.com/test-page'); + + $this->assertInstanceOf(AbstractTraceableResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('POST', $response->getInfo('http_method')); + $this->assertSame('https://www.example.com/test-page', $response->getInfo('url')); + $this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); + $this->assertSame(['baggage: ' . $transaction->toBaggage()], $mockResponse->getRequestOptions()['normalized_headers']['baggage']); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + $expectedTags = [ + 'http.method' => 'POST', + 'http.url' => 'https://www.example.com/test-page', + ]; + + $this->assertCount(2, $spans); + $this->assertNull($spans[1]->getEndTimestamp()); + $this->assertSame('http.client', $spans[1]->getOp()); + $this->assertSame('POST https://www.example.com/test-page', $spans[1]->getDescription()); + $this->assertSame($expectedTags, $spans[1]->getTags()); + } + + public function testStream(): void + { + $transaction = new Transaction(new TransactionContext()); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $decoratedHttpClient = new MockHttpClient(new MockResponse(['foo', 'bar'])); + $httpClient = new TraceableHttpClient($decoratedHttpClient, $this->hub); + $response = $httpClient->request('GET', 'https://www.example.com/test-page'); + $chunks = []; + + foreach ($httpClient->stream($response) as $chunkResponse => $chunk) { + $this->assertSame($response, $chunkResponse); + + $chunks[] = $chunk->getContent(); + } + + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + $expectedTags = [ + 'http.method' => 'GET', + 'http.url' => 'https://www.example.com/test-page', + ]; + + $this->assertSame('foobar', implode('', $chunks)); + $this->assertCount(2, $spans); + $this->assertNotNull($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()); + + $loopIndex = 0; + + foreach ($httpClient->stream($response) as $chunk) { + ++$loopIndex; + } + + $this->assertSame(1, $loopIndex); + } + + public function testStreamThrowsExceptionIfResponsesArgumentIsInvalid(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('"Sentry\\SentryBundle\\Tracing\\HttpClient\\AbstractTraceableHttpClient::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "stdClass" given.'); + + $this->httpClient->stream(new \stdClass()); + } + + public function testSetLogger(): void + { + $logger = new NullLogger(); + + $this->decoratedHttpClient->expects($this->once()) + ->method('setLogger') + ->with($logger); + + $this->httpClient->setLogger($logger); + } + + public function testReset(): void + { + $this->decoratedHttpClient->expects($this->once()) + ->method('reset'); + + $this->httpClient->reset(); + } + + public function testWithOptions(): void + { + if (!method_exists(MockHttpClient::class, 'withOptions')) { + self::markTestSkipped(); + } + + $transaction = new Transaction(new TransactionContext()); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->exactly(2)) + ->method('getSpan') + ->willReturn($transaction); + + $responses = [ + new MockResponse(), + new MockResponse(), + ]; + + $decoratedHttpClient = new MockHttpClient($responses, 'https://www.example.com'); + $httpClient1 = new TraceableHttpClient($decoratedHttpClient, $this->hub); + $httpClient2 = $httpClient1->withOptions(['base_uri' => 'https://www.example.org']); + + $this->assertNotSame($httpClient1, $httpClient2); + + $response = $httpClient1->request('GET', 'test-page'); + + $this->assertInstanceOf(AbstractTraceableResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('GET', $response->getInfo('http_method')); + $this->assertSame('https://www.example.com/test-page', $response->getInfo('url')); + + $response = $httpClient2->request('GET', 'test-page'); + + $this->assertInstanceOf(AbstractTraceableResponse::class, $response); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('GET', $response->getInfo('http_method')); + $this->assertSame('https://www.example.org/test-page', $response->getInfo('url')); + } + + private static function isHttpClientPackageInstalled(): bool + { + return interface_exists(HttpClientInterface::class); + } +} + +interface TestableHttpClientInterface extends HttpClientInterface, LoggerAwareInterface, ResetInterface +{ +} diff --git a/tests/Tracing/HttpClient/TraceableResponseTest.php b/tests/Tracing/HttpClient/TraceableResponseTest.php new file mode 100644 index 00000000..e67616d5 --- /dev/null +++ b/tests/Tracing/HttpClient/TraceableResponseTest.php @@ -0,0 +1,141 @@ +client = $this->createMock(HttpClientInterface::class); + $this->hub = $this->createMock(HubInterface::class); + } + + public function testInstanceCannotBeSerialized(): void + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Serializing instances of this class is forbidden.'); + + serialize(new TraceableResponse($this->client, new MockResponse(), null)); + } + + public function testInstanceCannotBeUnserialized(): void + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('Unserializing instances of this class is forbidden.'); + + unserialize(sprintf('O:%u:"%s":0:{}', \strlen(TraceableResponse::class), TraceableResponse::class)); + } + + public function testDestructor(): void + { + $transaction = new Transaction(new TransactionContext(), $this->hub); + $context = new SpanContext(); + $span = $transaction->startChild($context); + $response = new TraceableResponse($this->client, new MockResponse(), $span); + + // Call gc to invoke destructors at the right time. + unset($response); + + gc_mem_caches(); + gc_collect_cycles(); + + $this->assertNotNull($span->getEndTimestamp()); + } + + public function testGetStatusCode(): void + { + $response = new TraceableResponse($this->client, new MockResponse(), null); + + $this->assertSame(200, $response->getStatusCode()); + } + + public function testGetHeaders(): void + { + $expectedHeaders = ['content-length' => ['0']]; + $response = new TraceableResponse($this->client, new MockResponse('', ['response_headers' => $expectedHeaders]), null); + + $this->assertSame($expectedHeaders, $response->getHeaders()); + } + + public function testGetContent(): void + { + $span = new Span(); + $httpClient = new MockHttpClient(new MockResponse('foobar')); + $response = new TraceableResponse($httpClient, $httpClient->request('GET', 'https://www.example.org/'), $span); + + $this->assertSame('foobar', $response->getContent()); + $this->assertNotNull($span->getEndTimestamp()); + } + + public function testToArray(): void + { + $span = new Span(); + $httpClient = new MockHttpClient(new MockResponse('{"foo":"bar"}')); + $response = new TraceableResponse($this->client, $httpClient->request('GET', 'https://www.example.org/'), $span); + + $this->assertSame(['foo' => 'bar'], $response->toArray()); + $this->assertNotNull($span->getEndTimestamp()); + } + + public function testCancel(): void + { + $span = new Span(); + $response = new TraceableResponse($this->client, new MockResponse(), $span); + + $response->cancel(); + + $this->assertTrue($response->getInfo('canceled')); + $this->assertNotNull($span->getEndTimestamp()); + } + + public function testGetInfo(): void + { + $response = new TraceableResponse($this->client, new MockResponse(), null); + + $this->assertSame(200, $response->getInfo('http_code')); + } + + public function testToStream(): void + { + $httpClient = new MockHttpClient(new MockResponse('foobar')); + $response = new TraceableResponse($this->client, $httpClient->request('GET', 'https://www.example.org/'), null); + + if (!method_exists($response, 'toStream')) { + $this->markTestSkipped('The TraceableResponse::toStream() method is not supported'); + } + + $this->assertSame('foobar', stream_get_contents($response->toStream())); + } + + public function testStreamThrowsExceptionIfResponsesArgumentIsInvalid(): void + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage('"Sentry\\SentryBundle\\Tracing\\HttpClient\\TraceableHttpClient::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "stdClass" given.'); + + iterator_to_array(TraceableResponse::stream($this->client, [new \stdClass()], null)); + } +}