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 6f0e9021..8873858e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,18 @@ ## Unreleased +- feat: Add support for Dynamic Sampling (#661) +- Add support for tracing of the Symfony HTTP client requests (#606) +- Support logging the impersonator user, if any (#647) +- ref: Use constant for the SDK version (#662) + ## 4.3.1 (2022-10-10) fix: Update span ops (#655) ## 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 da763c3e..fd3fac4a 100644 --- a/composer.json +++ b/composer.json @@ -26,16 +26,16 @@ "php": "^7.2||^8.0", "jean85/pretty-package-versions": "^1.5 || ^2.0", "php-http/discovery": "^1.11", - "sentry/sdk": "^3.2", + "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": { @@ -97,7 +98,7 @@ }, "extra": { "branch-alias": { - "dev-master": "4.3.x-dev", + "dev-master": "4.4.x-dev", "releases/3.2.x": "3.2.x-dev", "releases/2.x": "2.x-dev", "releases/1.x": "1.x-dev" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fcf117af..9e7d25eb 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: "#^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/TraceableResponseForV5.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..05d06550 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 @@ -184,6 +185,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..110b855b 100644 --- a/src/DependencyInjection/SentryExtension.php +++ b/src/DependencyInjection/SentryExtension.php @@ -5,7 +5,6 @@ namespace Sentry\SentryBundle\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Jean85\PrettyVersions; use LogicException; use Psr\Log\NullLogger; use Sentry\Client; @@ -37,6 +36,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 +74,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 +139,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]) @@ -247,6 +248,21 @@ private function registerCacheTracingConfiguration(ContainerBuilder $container, $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/TracingConsoleListener.php b/src/EventListener/TracingConsoleListener.php index 563bf610..5efbaf26 100644 --- a/src/EventListener/TracingConsoleListener.php +++ b/src/EventListener/TracingConsoleListener.php @@ -9,6 +9,7 @@ use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; @@ -61,6 +62,7 @@ public function handleConsoleCommandEvent(ConsoleCommandEvent $event): void $transactionContext = new TransactionContext(); $transactionContext->setOp('console.command'); $transactionContext->setName($this->getSpanName($command)); + $transactionContext->setSource(TransactionSource::task()); $span = $this->hub->startTransaction($transactionContext); } else { diff --git a/src/EventListener/TracingRequestListener.php b/src/EventListener/TracingRequestListener.php index 3166dabf..4ed7264d 100644 --- a/src/EventListener/TracingRequestListener.php +++ b/src/EventListener/TracingRequestListener.php @@ -6,7 +6,10 @@ use Sentry\Tracing\Transaction; 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 @@ -20,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; @@ -34,9 +37,13 @@ public function handleKernelRequestEvent(RequestListenerRequestEvent $event): vo /** @var float $requestStartTime */ $requestStartTime = $request->server->get('REQUEST_TIME_FLOAT', microtime(true)); - $context = TransactionContext::fromSentryTrace($request->headers->get('sentry-trace', '')); + $context = TransactionContext::fromHeaders( + $request->headers->get('sentry-trace', ''), + $request->headers->get('baggage', '') + ); $context->setOp('http.server'); $context->setName(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo())); + $context->setSource(TransactionSource::url()); $context->setStartTimestamp($requestStartTime); $context->setTags($this->getTags($request)); @@ -47,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..9240c5c6 100644 --- a/src/Resources/config/schema/sentry-1.0.xsd +++ b/src/Resources/config/schema/sentry-1.0.xsd @@ -91,6 +91,7 @@ + @@ -117,4 +118,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..3ad27ad4 --- /dev/null +++ b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php @@ -0,0 +1,98 @@ +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(); + $options['headers'] = $headers; + + $context = new SpanContext(); + $context->setOp('http.client'); + $context->setDescription('HTTP ' . $method); + $context->setTags([ + 'http.method' => $method, + 'http.url' => $url, + ]); + + $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); + } + } +} 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/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/Twig/SentryExtension.php b/src/Twig/SentryExtension.php index 8a405c44..8bb9a072 100644 --- a/src/Twig/SentryExtension.php +++ b/src/Twig/SentryExtension.php @@ -30,6 +30,7 @@ public function getFunctions(): array { return [ new TwigFunction('sentry_trace_meta', [$this, 'getTraceMeta'], ['is_safe' => ['html']]), + new TwigFunction('sentry_baggage_meta', [$this, 'getBaggageMeta'], ['is_safe' => ['html']]), ]; } @@ -42,4 +43,14 @@ public function getTraceMeta(): string return sprintf('', null !== $span ? $span->toTraceparent() : ''); } + + /** + * Returns an HTML meta tag named `baggage`. + */ + public function getBaggageMeta(): string + { + $span = $this->hub->getSpan(); + + return sprintf('', null !== $span ? $span->toBaggage() : ''); + } } 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..9b853130 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 @@ -52,6 +53,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..82c71ef8 100644 --- a/tests/DependencyInjection/Fixtures/php/full.php +++ b/tests/DependencyInjection/Fixtures/php/full.php @@ -51,6 +51,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..e9240f89 100644 --- a/tests/DependencyInjection/Fixtures/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/xml/full.xml @@ -49,6 +49,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..e3c86290 100644 --- a/tests/DependencyInjection/Fixtures/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/yml/full.yml @@ -50,6 +50,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..30a17193 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; @@ -243,7 +243,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 +380,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 ab26c9fd..313200af 100644 --- a/tests/EventListener/TracingConsoleListenerTest.php +++ b/tests/EventListener/TracingConsoleListenerTest.php @@ -12,6 +12,7 @@ use Sentry\Tracing\Span; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; @@ -71,6 +72,7 @@ public function handleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHubData $transactionContext = new TransactionContext(); $transactionContext->setOp('console.command'); $transactionContext->setName(''); + $transactionContext->setSource(TransactionSource::task()); yield [ new Command(), @@ -80,6 +82,7 @@ public function handleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHubData $transactionContext = new TransactionContext(); $transactionContext->setOp('console.command'); $transactionContext->setName('app:command'); + $transactionContext->setSource(TransactionSource::task()); yield [ new Command('app:command'), diff --git a/tests/EventListener/TracingRequestListenerTest.php b/tests/EventListener/TracingRequestListenerTest.php index 17c8c793..f5cc839c 100644 --- a/tests/EventListener/TracingRequestListenerTest.php +++ b/tests/EventListener/TracingRequestListenerTest.php @@ -8,19 +8,21 @@ 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; use Sentry\Tracing\SpanId; use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TraceId; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; 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; @@ -73,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 @@ -85,11 +87,15 @@ public function testHandleKernelRequestEvent(Options $options, Request $request, */ public function handleKernelRequestEventDataProvider(): \Generator { + $samplingContext = DynamicSamplingContext::fromHeader(''); + $samplingContext->freeze(); + $transactionContext = new TransactionContext(); $transactionContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $transactionContext->setParentSpanId(new SpanId('566e3688a61d4bc8')); $transactionContext->setParentSampled(true); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -100,6 +106,7 @@ public function handleKernelRequestEventDataProvider(): \Generator 'route' => '', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); yield 'request.headers.sentry-trace EXISTS' => [ new Options(), @@ -117,8 +124,47 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext, ]; + $samplingContext = DynamicSamplingContext::fromHeader('sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-sample_rate=1'); + $samplingContext->freeze(); + + $transactionContext = new TransactionContext(); + $transactionContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); + $transactionContext->setParentSpanId(new SpanId('566e3688a61d4bc8')); + $transactionContext->setParentSampled(true); + $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); + $transactionContext->setOp('http.server'); + $transactionContext->setStartTimestamp(1613493597.010275); + $transactionContext->setTags([ + 'net.host.port' => '80', + 'http.method' => 'GET', + 'http.url' => 'http://www.example.com/', + 'http.flavor' => '1.1', + 'route' => '', + 'net.host.name' => 'www.example.com', + ]); + $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); + + yield 'request.headers.sentry-trace and headers.baggage EXISTS' => [ + new Options(), + Request::create( + 'http://www.example.com', + 'GET', + [], + [], + [], + [ + 'REQUEST_TIME_FLOAT' => 1613493597.010275, + 'HTTP_sentry-trace' => '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', + 'HTTP_baggage' => 'sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-sample_rate=1', + ] + ), + $transactionContext, + ]; + $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -141,6 +187,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://127.0.0.1/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -171,6 +218,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/path'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -194,6 +242,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/path'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -217,6 +266,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -240,6 +290,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -263,6 +314,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -286,6 +338,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -309,6 +362,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -332,6 +386,7 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://:/'); + $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); $transactionContext->setStartTimestamp(1613493597.010275); $transactionContext->setTags([ @@ -354,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 @@ -369,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, @@ -386,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, @@ -405,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() @@ -420,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..29552488 --- /dev/null +++ b/tests/Tracing/HttpClient/TraceableHttpClientTest.php @@ -0,0 +1,210 @@ +hub = $this->createMock(HubInterface::class); + $this->decoratedHttpClient = $this->createMock(TestableHttpClientInterface::class); + $this->httpClient = new TraceableHttpClient($this->decoratedHttpClient, $this->hub); + } + + public function testRequest(): void + { + $samplingContext = DynamicSamplingContext::fromHeader('sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-sample_rate=1'); + + $transactionContext = new TransactionContext(); + $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); + + $transaction = new Transaction($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://www.example.com/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')); + $this->assertSame(['sentry-trace: ' . $transaction->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); + $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('HTTP GET', $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('HTTP GET', $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)); + } +} diff --git a/tests/Twig/SentryExtensionTest.php b/tests/Twig/SentryExtensionTest.php index 5dd06c52..fe7f4894 100644 --- a/tests/Twig/SentryExtensionTest.php +++ b/tests/Twig/SentryExtensionTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Sentry\SentryBundle\Twig\SentryExtension; use Sentry\State\HubInterface; +use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\Span; use Sentry\Tracing\SpanId; use Sentry\Tracing\TraceId; @@ -71,6 +72,42 @@ public function traceMetaFunctionDataProvider(): \Generator ]; } + /** + * @dataProvider baggageMetaFunctionDataProvider + */ + public function testBaggageMetaFunction(?Span $span, string $expectedTemplate): void + { + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($span); + + $environment = new Environment(new ArrayLoader(['foo.twig' => '{{ sentry_baggage_meta() }}'])); + $environment->addExtension(new SentryExtension($this->hub)); + + $this->assertSame($expectedTemplate, $environment->render('foo.twig')); + } + + /** + * @return \Generator + */ + public function baggageMetaFunctionDataProvider(): \Generator + { + yield [ + null, + '', + ]; + + $samplingContext = DynamicSamplingContext::fromHeader('sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-sample_rate=1'); + $transactionContext = new TransactionContext(); + $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); + $transaction = new Transaction($transactionContext); + + yield [ + $transaction, + '', + ]; + } + private static function isTwigBundlePackageInstalled(): bool { return class_exists(TwigBundle::class);