diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/01-feature.yml similarity index 91% rename from .github/ISSUE_TEMPLATE/feature.yml rename to .github/ISSUE_TEMPLATE/01-feature.yml index 9f331d37..a7aab2fd 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/01-feature.yml @@ -1,6 +1,6 @@ name: πŸ’‘ Feature Request -description: Create a feature request for this SDK. -labels: 'enhancement' +description: Propose new functionality for the SDK +labels: ["Symfony", "Feature"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/02-improvement.yml b/.github/ISSUE_TEMPLATE/02-improvement.yml new file mode 100644 index 00000000..ab64fdca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-improvement.yml @@ -0,0 +1,30 @@ +name: πŸ’‘ Improvement +description: Propose an improvement for existing functionality of the SDK +labels: ["Symfony", "Improvement"] +body: + - type: markdown + attributes: + value: Thanks for taking the time to file a request! Please fill out this form as completely as possible. + - type: textarea + id: problem + attributes: + label: Problem Statement + description: A clear and concise description of what you want and what your use case is. + placeholder: |- + I want to make whirled peas, but Sentry doesn't blend. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Solution Brainstorm + description: We know you have bright ideas to share ... share away, friend. + placeholder: |- + Add a blender to Sentry. + validations: + required: true + - type: markdown + attributes: + value: |- + ## Thanks πŸ™ + Check our [triage docs](https://open.sentry.io/triage/) for what to expect next. diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/03-bug.yml similarity index 97% rename from .github/ISSUE_TEMPLATE/bug.yml rename to .github/ISSUE_TEMPLATE/03-bug.yml index 7d62230e..a4a1d26b 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/03-bug.yml @@ -1,5 +1,6 @@ name: 🐞 Bug Report description: Tell us about something that's not working the way we (probably) intend. +labels: ["Symfony", "Bug"] body: - type: dropdown id: type diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a0982451 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: monthly + - package-ecosystem: "composer" + directory: "/" + allow: + - dependency-name: "*phpstan*" + schedule: + interval: monthly + groups: + composer: + patterns: + - "*phpstan*" diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index ea19fe8d..a1be1e49 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -10,20 +10,31 @@ on: description: Force a release even when there are release-blockers (optional) required: false +permissions: + contents: read + jobs: release: runs-on: ubuntu-latest name: Release version steps: - - uses: actions/checkout@v2 + - name: Get auth token + id: token + uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5 + with: + app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }} + + - name: Checkout + uses: actions/checkout@v4 with: - token: ${{ secrets.GH_RELEASE_PAT }} + token: ${{ steps.token.outputs.token }} fetch-depth: 0 - name: Prepare release uses: getsentry/action-prepare-release@v1 env: - GITHUB_TOKEN: ${{ secrets.GH_RELEASE_PAT }} + GITHUB_TOKEN: ${{ steps.token.outputs.token }} with: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml index bcf4466e..91c0cee4 100644 --- a/.github/workflows/static-analysis.yaml +++ b/.github/workflows/static-analysis.yaml @@ -8,57 +8,66 @@ on: - develop - release/** +permissions: + contents: read + jobs: php-cs-fixer: name: PHP-CS-Fixer runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.3' - name: Install dependencies - run: composer update --no-progress --no-interaction --prefer-dist + uses: ramsey/composer-install@v3 + with: + composer-options: --prefer-dist - name: Run script - run: composer phpcs + run: vendor/bin/php-cs-fixer fix --verbose --diff --dry-run phpstan: name: PHPStan runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' - name: Install dependencies - run: composer update --no-progress --no-interaction --prefer-dist + uses: ramsey/composer-install@v3 + with: + composer-options: --prefer-dist - name: Run script - run: composer phpstan + run: vendor/bin/phpstan analyse psalm: name: Psalm runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.2' + php-version: '8.3' - name: Install dependencies - run: composer update --no-progress --no-interaction --prefer-dist + uses: ramsey/composer-install@v3 + with: + composer-options: --prefer-dist - name: Run script - run: composer psalm -- --php-version=8.0 + run: vendor/bin/psalm --php-version=8.0 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 16c124ff..8c3dabe9 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -1,13 +1,15 @@ -name: Continuous Integration +name: CI on: - pull_request: null + pull_request: push: branches: - master - - develop - release/** +permissions: + contents: read + jobs: tests: name: Tests @@ -24,10 +26,13 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' + - '8.4' symfony-version: - 4.4.* - 5.* - 6.* + - 7.* dependencies: - highest exclude: @@ -37,6 +42,18 @@ jobs: symfony-version: 6.* - php: '7.4' symfony-version: 6.* + - php: '7.2' + symfony-version: 7.* + - php: '7.3' + symfony-version: 7.* + - php: '7.4' + symfony-version: 7.* + - php: '8.0' + symfony-version: 7.* + - php: '8.1' + symfony-version: 7.* + - php: '8.4' + symfony-version: 4.4.* include: - php: '7.2' symfony-version: 4.4.* @@ -47,10 +64,13 @@ jobs: - php: '8.0' symfony-version: 6.* dependencies: lowest + - php: '8.2' + symfony-version: 7.* + dependencies: lowest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 2 @@ -64,12 +84,12 @@ jobs: - name: Setup Problem Matchers for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - name: Update PHPUnit - run: composer require --dev phpunit/phpunit ^9.3.9 --no-update - if: matrix.php == '8.0' && matrix.dependencies == 'lowest' + # These dependencies are not used running the tests but can cause deprecation warnings so we remove them before running the tests + - name: Remove unused dependencies + run: composer remove vimeo/psalm phpstan/phpstan friendsofphp/php-cs-fixer --dev --no-interaction --no-update - name: Install dependencies - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v3 with: dependency-versions: ${{ matrix.dependencies }} composer-options: --prefer-dist @@ -78,9 +98,10 @@ jobs: run: vendor/bin/phpunit --coverage-clover=build/coverage-report.xml - name: Upload code coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: file: build/coverage-report.xml + token: ${{ secrets.CODECOV_TOKEN }} missing-optional-packages-tests: name: Tests without optional packages @@ -99,14 +120,12 @@ jobs: - php: '8.0' dependencies: lowest symfony-version: 4.4.* - - php: '8.1' - dependencies: highest - - php: '8.2' + - php: '8.4' dependencies: highest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -122,7 +141,7 @@ jobs: 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 + uses: ramsey/composer-install@v3 with: dependency-versions: ${{ matrix.dependencies }} composer-options: --prefer-dist @@ -131,6 +150,7 @@ jobs: run: vendor/bin/phpunit --coverage-clover=build/coverage-report.xml - name: Upload code coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: file: build/coverage-report.xml + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 7136604b..3f0de29f 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,10 +14,14 @@ 'imports_order' => ['class', 'function', 'const'], ], 'declare_strict_types' => true, + 'get_class_to_class_keyword' => false, 'random_api_migration' => true, 'yoda_style' => true, 'self_accessor' => false, + 'nullable_type_declaration_for_default_null_value' => false, + 'no_null_property_initialization' => false, 'phpdoc_no_useless_inheritdoc' => false, + 'no_superfluous_phpdoc_tags' => false, 'phpdoc_to_comment' => false, 'phpdoc_align' => [ 'tags' => ['param', 'return', 'throws', 'type', 'var'], @@ -27,6 +31,10 @@ 'method' => 'multi', 'property' => 'multi', ], + 'trailing_comma_in_multiline' => [ + 'after_heredoc' => false, + 'elements' => ['arrays'], + ], ]) ->setFinder( PhpCsFixer\Finder::create() diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b5b361..db5b004b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,410 +1,168 @@ -# Changelog +# CHANGELOG -## 4.10.0 +## 5.2.0 -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.10.0. +The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v5.2.0. ### Features -- Tracing without Performance [(#742)](https://github.com/getsentry/sentry-symfony/pull/742) +- Allow to configure the logger via the `sentry.yaml` configuration file [(#899)](https://github.com/getsentry/sentry-symfony/pull/899) - The SDK will now continue a trace from incoming HTTP requests, even if performance is not enabled. - To continue a trace outward, you may attach the Sentry tracing headers to any HTTP client request. - You can fetch the required header values by calling \Sentry\getBaggage() and \Sentry\getTraceparent(). - -- Add `ignore_exceptions` and `ignore_transactions` options [(#724)](https://github.com/getsentry/sentry-symfony/pull/724) - -### Misc - -- Improve setting logged-in users on the scope [(#720)](https://github.com/getsentry/sentry-symfony/pull/720) -- Move DB span tags to span data [(#743)](https://github.com/getsentry/sentry-symfony/pull/743) -- Set the span status when tracing an HTTP client request [(#748)](https://github.com/getsentry/sentry-symfony/pull/748) - -## 4.9.2 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.9.2. + ```yaml + sentry: + dsn: "%env(SENTRY_DSN)%" + options: + logger: "sentry.logger" + + services: + sentry.logger: + class: 'Sentry\Logger\DebugFileLogger' + arguments: + $filePath: '../../var/log/sentry.log' + ``` ### Bug Fixes -- We decided to revert two previous PRs that aimed to remove deprecation warnings during test runs [(#736)](https://github.com/getsentry/sentry-symfony/pull/736) +- Fixed updating the user context when a route is marked as stateless [(#910)](https://github.com/getsentry/sentry-symfony/pull/910) - - Revert: Add a new Doctrine DBAL tracing driver that does not implement the deprecated `VersionAwarePlatformDriver` [(#723)](https://github.com/getsentry/sentry-symfony/pull/723) - - Revert: Fix a regression in `TracingDriverForV32` by adding `VersionAwarePlatformDriver::createDatabasePlatformForVersion` [(#731)](https://github.com/getsentry/sentry-symfony/pull/731) - -We are sorry for the inconvenience caused by these changes. - -## 4.9.1 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.9.1. - -### Bug Fixes +### Misc -- Fix a regression in `TracingDriverForV32` by adding `VersionAwarePlatformDriver::createDatabasePlatformForVersion` [(#731)](https://github.com/getsentry/sentry-symfony/pull/731) +- Remove `symfony/security-core` and `symfony/security-http` as dependencies [(#912)](https://github.com/getsentry/sentry-symfony/pull/912) -## 4.9.0 +## 5.1.0 -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.9.0. +The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v5.1.0. ### Features -- Add a new Doctrine DBAL tracing driver that does not implement the deprecated `VersionAwarePlatformDriver` [(#723)](https://github.com/getsentry/sentry-symfony/pull/723) - - The driver is automatically picked if `doctrine/dbal` version `3.2.0` or higher is installed. +- The SDK was updated to support PHP 8.4 [(#893)](https://github.com/getsentry/sentry-symfony/pull/893) +- Set the status for CLI command transactions based on the exit code [(#891)](https://github.com/getsentry/sentry-symfony/pull/891) ### Bug Fixes -- Fix config type of `http_connect_timeout`and `http_timeout` options [(#721)](https://github.com/getsentry/sentry-symfony/pull/721) +- Fix including request data on transactions [(#879)](https://github.com/getsentry/sentry-symfony/pull/879) -### Misc +## 5.0.1 -- Bump the underlying PHP SDK to version `^3.19` [(#725)](https://github.com/getsentry/sentry-symfony/pull/725) +The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v5.0.1. -## 4.8.0 +### Bug Fixes -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.8.0. +- Add missing `setCallbackWrapper` method to `TraceableCacheAdapterTrait` [(#841)](https://github.com/getsentry/sentry-symfony/pull/841) +- Fix detection of the `symfony/http-client` being installed [(#858)](https://github.com/getsentry/sentry-symfony/pull/858) -### Features +## 5.0.0 -- Set cache keys as span descriptions [(#677)](https://github.com/getsentry/sentry-symfony/pull/677) +The Sentry SDK team is thrilled to announce the immediate availability of Sentry Symfony SDK v5.0.0. - To better identify the source of a cache operation, we now set the cache key as the description of `cache` op spans. +### Breaking Change -### Bug Fixes +Please refer to the [UPGRADE-5.0.md](https://github.com/getsentry/sentry-symfony/blob/master/UPGRADE-5.0.md) guide for a complete list of breaking changes. -- Add direct dependency for `guzzlehttp/psr7` [(#708)](https://github.com/getsentry/sentry-symfony/pull/708) -- Drop `kernel.build_dir` param below Symfony 5.2 [(#711)](https://github.com/getsentry/sentry-symfony/pull/711) +This version adds support for the underlying [Sentry PHP SDK v4.0](https://github.com/getsentry/sentry-php). +Please refer to the PHP SDK [sentry-php/UPGRADE-4.0.md](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-4.0.md) guide for a complete list of breaking changes. -## 4.7.0 +- This version exclusively uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/) to send event data to Sentry. -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.7.0. + If you are using [sentry.io](https://sentry.io), no action is needed. + If you are using an on-premise/self-hosted installation of Sentry, the minimum requirement is now version `>= v20.6.0`. -### Features +- You need to have `ext-curl` installed to use the SDK. -- Add `profiles_sample_rate` config option [(#698)](https://github.com/getsentry/sentry-symfony/pull/698) +- The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. + Previously, both `Symfony\Component\ErrorHandler\Error\FatalError` and `Symfony\Component\Debug\Exception\FatalErrorException` were ignored by default. + To continue ignoring these exceptions, make the following changes to the config file: - With this new config option, you can now use our new profiling feature in Symfony as well. - Please consult https://github.com/getsentry/sentry-php/releases/3.15.0 for setup instructions. + ```yaml + // config/packages/sentry.yaml -## 4.6.0 + sentry: + options: + ignore_exceptions: + - 'Symfony\Component\ErrorHandler\Error\FatalError' + - 'Symfony\Component\Debug\Exception\FatalErrorException' + ``` -The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v4.6.0. -This release contains a colorful bouquet of new features. + This option performs an [`is_a`](https://www.php.net/manual/en/function.is-a.php) check now, so you can also ignore more generic exceptions. ### Features -- Report exceptions to Sentry as unhandled by default [(#674)](https://github.com/getsentry/sentry-symfony/pull/674) +- Add support for Sentry Developer Metrics [(#1619)](https://github.com/getsentry/sentry-php/pull/1619) - All unhandled exceptions will now be marked as `handled: false`. You can query for such events on the issues list page, - by searching for `error.handled:false`. - -- Exceptions from messages which will be retried are sent to Sentry as handled [(#674)](https://github.com/getsentry/sentry-symfony/pull/674) + ```php + use function Sentry\metrics; - All unhandled exceptions happening on the message bus will now be unpacked and reported individually. - The `WorkerMessageFailedEvent::willRetry` property is used to determine the `handled` value of the event sent to Sentry. + // Add 4 to a counter named hits + metrics()->increment(key: 'hits', value: 4); -- Add `register_error_handler` config option [(#687)](https://github.com/getsentry/sentry-symfony/pull/687) + // Add 25 to a distribution named response_time with unit milliseconds + metrics()->distribution(key: 'response_time', value: 25, unit: MetricsUnit::millisecond()); - With this option, you can disable the global error and exception handlers of the base PHP SDK. - If disabled, only events logged by Monolog will be sent to Sentry. + // Add 2 to gauge named parallel_requests, tagged with type: "a" + metrics()->gauge(key: 'parallel_requests', value: 2, tags: ['type': 'a']); - ```yaml - sentry: - dsn: '%env(SENTRY_DSN)%' - register_error_listener: false - register_error_handler: false - - monolog: - handlers: - sentry: - type: sentry - level: !php/const Monolog\Logger::ERROR - hub_id: Sentry\State\HubInterface + // Add a user's email to a set named users.sessions, tagged with role: "admin" + metrics()->set('users.sessions', 'jane.doe@example.com', null, ['role' => User::admin()]); ``` -- Add `before_send_transaction` [(#691)](https://github.com/getsentry/sentry-symfony/pull/691) + Metrics are automatically sent to Sentry at the end of a request, hooking into Symfony's `kernel.terminate` event. - Similar to `before_send`, you can now apply additional logic for `transaction` events. - You can mutate the `transaction` event before it is sent to Sentry. If your callback returns `null`, - the event is dropped. - - ```yaml - sentry: - options: - before_send_transaction: 'sentry.callback.before_send_transaction' - - services: - sentry.callback.before_send_transaction: - class: 'App\Service\Sentry' - factory: [ '@App\Service\Sentry', 'getBeforeSendTransaction' ] - ``` +- Add new fluent APIs [(#1601)](https://github.com/getsentry/sentry-php/pull/1601) ```php - setName('GET /example'); + $transactionContext->setOp('http.server'); + + // After + $transactionContext = (new TransactionContext()) + ->setName('GET /example'); + ->setOp('http.server'); ``` -- Use the `_route` attribute as the transaction name [(#692)](https://github.com/getsentry/sentry-symfony/pull/692) - - If you're using named routes, the SDK will default to use this attribute as the transaction name. - With this change, you should be able to see a full list of your transactions on the performance page, - instead of `<< unparameterized >>`. - - You may need to update your starred transactions as well as your dashboards due to this change. - -### Bug Fixes - -- Sanatize HTTP client spans [(#690)](https://github.com/getsentry/sentry-symfony/pull/690) - -## 4.5.0 (2022-11-28) - -- Symfony version 3.4 is no longer supported - - Drop Symfony support below 4.4 (#643) -- feat: Add support for tracing of Symfony HTTP client requests (#606) - - feat: Add support for HTTP client baggage propagation (#663) - - ref: Add proper HTTP client span descriptions (#680) -- feat: Support logging the impersonator user, if any (#647) -- ref: Use a constant for the SDK version (#662) - -## 4.4.0 (2022-10-20) - -- feat: Add support for Dynamic Sampling (#665) - -## 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) -- Add `TracingDriverConnectionInterface::getNativeConnection()` method to get the original driver connection (#597) -- Add `options.http_timeout` and `options.http_connect_timeout` configuration options (#593) - -## 4.2.10 (2022-05-17) - -- Fix compatibility issue with Twig >= 3.4.0 (#628) - -## 4.2.9 (2022-05-03) - -- Fix deprecation notice thrown when instrumenting the `PDOStatement::bindParam()` method and passing `$length = null` on DBAL `2.x` (#613) - -## 4.2.8 (2022-03-31) - -- Fix compatibility issue with Doctrine Bundle `>= 2.6.0` (#608) - -## 4.2.7 (2022-02-18) - -- Fix deprecation notice thrown when instrumenting the `PDOStatement::bindParam()` method and passing `$length = null` (#586) - -## 4.2.6 (2022-01-10) - -- Add support for `symfony/cache-contracts` package version `3.x` (#588) - -## 4.2.5 (2021-12-13) - -- Add support for Symfony 6 (#566) -- Fix fatal errors logged twice on Symfony `3.4` (#570) - -## 4.2.4 (2021-10-20) - -- Add return typehints to the methods of the `SentryExtension` class to prepare for Symfony 6 (#563) -- Fix setting the IP address on the user context when it's not available (#565) -- Fix wrong method existence check in `TracingDriverConnection::errorCode()` (#568) -- Fix decoration of the Doctrine DBAL connection when it implemented the `ServerInfoAwareConnection` interface (#567) - -## 4.2.3 (2021-09-21) - -- Fix: Test if `TracingStatement` exists before attempting to create the class alias, otherwise it breaks when opcache is enabled. (#552) -- Fix: Pass logger from `logger` config option to `TransportFactory` (#555) -- Improve the compatibility layer with Doctrine DBAL to avoid deprecations notices (#553) - -## 4.2.2 (2021-08-30) +- Simplify the breadcrumb API [(#1603)](https://github.com/getsentry/sentry-php/pull/1603) -- Fix missing instrumentation of the `Statement::execute()` method of Doctrine DBAL (#548) - -## 4.2.1 (2021-08-24) - -- Fix return type for `TracingDriver::getDatabase()` method (#541) -- Avoid throwing exception from the `TraceableCacheAdapterTrait::prune()` and `TraceableCacheAdapterTrait::reset()` methods when the decorated adapter does not implement the respective interfaces (#543) - -## 4.2.0 (2021-08-12) - -- Log the bus name, receiver name and message class name as event tags when using Symfony Messenger (#492) -- Make the transport factory configurable in the bundle's config (#504) -- Add the `sentry_trace_meta()` Twig function to print the `sentry-trace` HTML meta tag (#510) -- Make the list of commands for which distributed tracing is active configurable (#515) -- Introduce `TracingDriverConnection::getWrappedConnection()` (#536) -- Add the `logger` config option to ease setting a PSR-3 logger to debug the SDK (#538) -- Bump requirement for DBAL tracing to `^2.13|^3`; simplify the DBAL tracing feature (#527) - -## 4.1.4 (2021-06-18) - -- Fix decoration of cache adapters inheriting parent service (#525) -- Fix extraction of the username of the logged-in user in Symfony `5.3` (#518) - -## 4.1.3 (2021-05-31) - -- Fix missing require of the `symfony/cache-contracts` package (#506) - -## 4.1.2 (2021-05-17) - -- Fix the check of the existence of the `CacheItem` class while attempting to enable the cache instrumentation (#501) - -## 4.1.1 (2021-05-10) - -- Fix the conditions to automatically enable the cache instrumentation when possible (#487) -- Fix deprecations triggered by Symfony `5.3` (#489) -- Fix fatal error when the `SERVER_PROTOCOL` header is missing (#495) - -## 4.1.0 (2021-04-19) - -- Avoid failures when the `RequestFetcher` fails to translate the `Request` (#472) -- Add support for distributed tracing of Symfony request events (#423) -- Add support for distributed tracing of Twig template rendering (#430) -- Add support for distributed tracing of SQL queries while using Doctrine DBAL (#426) -- Add support for distributed tracing when running a console command (#455) -- Add support for distributed tracing of cache pools (#477) -- Add the full CLI command string to the extra context (#352) -- Deprecate the `Sentry\SentryBundle\EventListener\ConsoleCommandListener` class in favor of its parent class `Sentry\SentryBundle\EventListener\ConsoleListener` (#429) -- Lower the required version of `symfony/psr-http-message-bridge` to allow installing it on a project that uses Symfony `3.4.x` components only (#480) - -## 4.0.3 (2021-03-03) - -- Fix regression from #454 for `null` value on DSN not disabling Sentry (#457) - -## 4.0.2 (2021-03-03) - -- Add `kernel.project_dir` to `prefixes` default value to trim paths to the project root (#434) -- Fix `null`, `false` or empty value not disabling Sentry (#454) - -## 4.0.1 (2021-01-26) - -- Add missing `capture-soft-fails` option to the XSD schema for the XML config (#417) -- Fix regression that send PII even when the `send_default_pii` option is off (#425) -- Fix capture of console errors when the `register_error_listener` option is disabled (#427) - -## 4.0.0 (2021-01-19) - -**Breaking Change**: This version uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/). If you are -using an on-premise installation it requires Sentry version `>= v20.6.0` to work. If you are using -[sentry.io](https://sentry.io) nothing will change and no action is needed. - -- Enable back all error listeners from base SDK integration (#322) -- Add `options.traces_sampler` and `options.traces_sample_rate` configuration options (#385) -- [BC BREAK] Remove the `options.project_root` configuration option. Instead of setting it, use a combination of `options.in_app_include` and `options.in_app_exclude` (#385) -- [BC BREAK] Remove the `options.excluded_exceptions` configuration option. Instead of setting it, configure the `IgnoreErrorsIntegration` integration (#385) -- [BC BREAK] Refactorize the `ConsoleCommandListener`, `ErrorListener`, `RequestListener` and `SubRequestListener` event listeners (#387) -- Registered the CLI commands as lazy services (#373) -- [BC BREAK] Refactorize the configuration tree and the definitions of some container services (#401) -- Support the XML format for the bundle configuration (#401) -- PHP 8 support (#399, thanks to @Yozhef) -- Retrieve the request from the `RequestStack` when using the `RequestIntegration` integration (#361) -- Reorganize the folder structure and change CS standard (#405) -- [BC BREAK] Remove the `monolog` configuration option. Instead, register the service manually (#406) -- [BC BREAK] Remove the `listener_priorities` configuration option. Instead, use a compiler pass to change the priority of the listeners (#407) -- Prefer usage of the existing `Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface` service for the `RequestFetcher` class (#409) -- [BC BREAK] Change the priorities of the `RequestListener` and `SubRequestListener` listeners (#414) - -## 3.5.3 (2020-10-13) - -- Refactors and fixes class aliases for more robustness (#315 #359, thanks to @guilliamxavier) - -## 3.5.2 (2020-07-08) - -- Use `jean85/pretty-package-versions` `^1.5` to leverage the new `getRootPackageVersion` method (c8799ac) -- Fix support for PHP preloading (#354, thanks to @annuh) -- Fix `capture_soft_fails: false` option for the Messenger (#353) - -## 3.5.1 (2020-05-07) - -- Capture events using the `Hub` in the `MessengerListener` to avoid loosing `Scope` data (#339, thanks to @sh41) -- Capture multiple events if multiple exceptions are generated in a Messenger Worker context (#340, thanks to @emarref) - -## 3.5.0 (2020-05-04) - -- Capture and flush messages in a Messenger Worker context (#326, thanks to @emarref) -- Support Composer 2 (#335) -- Avoid issues with dependency lower bound, fix #331 (#335) - -## 3.4.4 (2020-03-16) - -- Improve `release` option default value (#325) - -## 3.4.3 (2020-02-03) - -- Change default of `in_app_include` to empty, due to getsentry/sentry-php#958 (#311) -- Improve class_alias robustness (#315) - -## 3.4.2 (2020-01-29) - -- Remove space from classname used with `class_alias` (#313) - -## 3.4.1 (2020-01-24) - -- Fix issue due to usage of `class_alias` to fix deprecations, which could break BC layers of third party packages (#309, thanks to @scheb) - -## 3.4.0 (2020-01-20) - -- Add support for `sentry/sentry` 2.3 (#298) -- Drop support for `sentry/sentry` < 2.3 (#298) -- Add support to `in_app_include` client option (#298) -- Remap `excluded_exceptions` option to use the new `IgnoreErrorsIntegration` (#298) - -## 3.3.2 (2020-01-16) - -- Fix issue with exception listener under Symfony 4.3 (#301) - -## 3.3.1 (2020-01-14) - -- Fixed Release - -## 3.3.0 (2020-01-14) - -- Add support for Symfony 5.0 (#266, thanks to @Big-Shark) -- Drop support for Symfony < 3.4 (#277) -- Add default value for the `release` option, using the detected root package version (#291 #292, thanks to @Ocramius) - -## 3.2.1 (2019-12-19) - -- Fix handling of command with no name on `ConsoleListener` (#261) -- Remove check by AuthorizationChecker in `RequestListener` (#264) -- Fixed undefined variable in `RequestListener` (#263) - -## 3.2.0 (2019-10-04) - -- Add forward compatibility with Symfony 5 (#235, thanks to @garak) -- Fix Hub initialization for `ErrorListener` (#243, thanks to @teohhanhui) -- Fix compatibility with sentry/sentry 2.2+ (#244) -- Add support for `class_serializers` option (#245) -- Add support for `max_request_body_size` option (#249) -- Add option to disable the error listener completely (#247, thanks to @HypeMC) -- Add options to register the Monolog Handler (#247, thanks to @HypeMC) - -## 3.1.0 (2019-07-02) + ```php + // Before + \Sentry\addBreadcrumb( + new \Sentry\Breadcrumb( + \Sentry\Breadcrumb::LEVEL_INFO, + \Sentry\Breadcrumb::TYPE_DEFAULT, + 'auth', // category + 'User authenticated', // message (optional) + ['user_id' => $userId] // data (optional) + ) + ); + + // After + \Sentry\addBreadcrumb( + category: 'auth', + message: 'User authenticated', // optional + metadata: ['user_id' => $userId], // optional + level: Breadcrumb::LEVEL_INFO, // set by default + type: Breadcrumb::TYPE_DEFAULT, // set by default + ); + ``` -- Add support for Symfony 2.8 (#233, thanks to @nocive) -- Fix handling of ESI requests (#213, thanks to @franmomu) +- New default cURL HTTP client [(#1589)](https://github.com/getsentry/sentry-php/pull/1589) -## 3.0.0 (2019-05-10) + The SDK now ships with its own HTTP client based on cURL. A few new options were added. -- Add the `sentry:test` command, to test if the Sentry SDK is functioning properly. + ```yaml + // config/packages/sentry.yaml -## 3.0.0-beta2 (2019-03-22) + sentry: + options: + - http_proxy_authentication: 'username:password' // user name and password to use for proxy authentication + - http_ssl_verify_peer: false // default true, verify the peer's SSL certificate + - http_compression: false // default true, http request body compression + ``` -- Disable Sentry's ErrorHandler, and report all errors using Symfony's events (#204) + To use a different client, you may use the `http_client` option. + To use a different transport, you may use the `transport` option. A custom transport must implement the `TransportInterface`. + If you use the `transport` option, the `http_client` option has no effect. -## 3.0.0-beta1 (2019-03-06) +### Misc -The 3.0 major release has multiple breaking changes. The most notable one is the upgrade to the 2.0 base SDK client. -Refer to the [UPGRADE-3.0.md](https://github.com/getsentry/sentry-symfony/blob/master/UPGRADE-3.0.md) document for a -detailed explanation. +- The abandoned package `php-http/message-factory` was removed. diff --git a/Makefile b/Makefile deleted file mode 100644 index 4f116379..00000000 --- a/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -.PHONY: pre-commit-check - -cs: - vendor/bin/php-cs-fixer fix --verbose - -cs-dry-run: - vendor/bin/php-cs-fixer fix --verbose --dry-run - -phpstan: - vendor/bin/phpstan analyze - -psalm: - vendor/bin/psalm - -test: - vendor/bin/phpunit - -pre-commit-check: cs phpstan psalm test - -setup-git: - git config branch.autosetuprebase always diff --git a/README.md b/README.md index 780d82e0..87b0db9e 100644 --- a/README.md +++ b/README.md @@ -13,157 +13,41 @@ _Bad software is everywhere, and we're tired of it. Sentry is on a mission to he [![Total Downloads](https://poser.pugx.org/sentry/sentry-symfony/downloads)](https://packagist.org/packages/sentry/sentry-symfony) [![Monthly Downloads](https://poser.pugx.org/sentry/sentry-symfony/d/monthly)](https://packagist.org/packages/sentry/sentry-symfony) -![CI](https://github.com/getsentry/sentry-symfony/workflows/CI/badge.svg) [![Coverage Status][Master Code Coverage Image]][Master Code Coverage] +[![CI](https://github.com/getsentry/sentry-symfony/actions/workflows/tests.yaml/badge.svg)](https://github.com/getsentry/sentry-symfony/actions/workflows/tests.yaml) +[![Coverage Status][Master Code Coverage Image]][Master Code Coverage] [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/cWnMQeA) This is the official Symfony SDK for [Sentry](https://getsentry.com/). ## Getting Started -Using this `sentry-symfony` SDK provides you with the following benefits: - - * Quickly integrate and configure Sentry for your Symfony app - * Out of the box, each event will contain the following data by default - - The currently authenticated user - - The Symfony environment - ### Install -To install the SDK you will need to be using [Composer]([https://getcomposer.org/) -in your project. To install it please see the [docs](https://getcomposer.org/download/). +Install the SDK using [Composer](https://getcomposer.org/). ```bash composer require sentry/sentry-symfony ``` -If you're using the [Symfony Flex](https://symfony.com/doc/current/setup/flex.html) Composer plugin, you might encounter a message similar to this: - -``` -The recipe for this package comes from the "contrib" repository, which is open to community contributions. -Review the recipe at https://github.com/symfony/recipes-contrib/tree/master/sentry/sentry-symfony/3.0 - -Do you want to execute this recipe? -``` - -Just type `y`, press return, and the procedure will continue. - -**Caution:** Due to a bug in the [`SensioFrameworkExtra`](https://github.com/sensiolabs/SensioFrameworkExtraBundle) bundle, affecting version 6.0 and below, you might run into a missing `Nyholm\Psr7\Factory\Psr17Factory::class` error while executing the commands mentioned above. -If you are not using the PSR-7 bridge, you can work around this issue by changing the configuration of the bundle as follows: - -```yaml -sensio_framework_extra: - psr_message: - enabled: false -``` - -For more details about the issue see https://github.com/sensiolabs/SensioFrameworkExtraBundle/pull/710. - -### Enable the Bundle - -If you installed the package using the Flex recipe, the bundle will be automatically enabled. Otherwise, enable it by adding it to the list -of registered bundles in the `Kernel.php` file of your project: - -```php -class AppKernel extends \Symfony\Component\HttpKernel\Kernel -{ - public function registerBundles(): array - { - return [ - // ... - new \Sentry\SentryBundle\SentryBundle(), - ]; - } - - // ... -} -``` - -The bundle will be enabled in all environments by default. -To enable event reporting, you'll need to add a DSN (see the next step). - ### Configure -Add the [Sentry DSN](https://docs.sentry.io/quickstart/#configure-the-dsn) of your project. -If you're using Symfony 3.4, add the DSN to your `app/config/config_prod.yml` file. -For Symfony 4 or newer, add the DSN to your `config/packages/sentry.yaml` file. - -Keep in mind that by leaving the `dsn` value empty (or undeclared), you will disable Sentry's event reporting. - -```yaml -sentry: - dsn: "https://public:secret@sentry.example.com/1" - messenger: - enabled: true # flushes Sentry messages at the end of each message handling - capture_soft_fails: true # captures exceptions marked for retry too - options: - environment: '%kernel.environment%' - release: '%env(VERSION)%' #your app version -``` - -The parameter `options` allows to fine-tune exceptions. To discover more options, please refer to -[the Unified APIs](https://docs.sentry.io/development/sdk-dev/unified-api/#options) options and -the [PHP specific](https://docs.sentry.io/platforms/php/#php-specific-options) ones. - -#### Optional: use custom HTTP factory/transport - -Since the SDK 2.0 uses HTTPlug to remain transport-agnostic, you need to install two packages that provide -[`php-http/async-client-implementation`](https://packagist.org/providers/php-http/async-client-implementation) -and [`psr/http-message-implementation`](https://packagist.org/providers/psr/http-message-implementation). - -This bundle depends on `sentry/sdk`, which is a metapackage that already solves this need, requiring our suggested HTTP -packages: the Symfony HTTP client (`symfony/http-client`) and Guzzle's message factories (`http-interop/http-factory-guzzle`). - -If you want to use a different HTTP client or message factory, you can override the `sentry/sdk` package by adding the following to your `composer.json` after the `require` section: +Add the [Sentry DSN](https://docs.sentry.io/quickstart/#configure-the-dsn) to your `.env` file. -```json - "replace": { - "sentry/sdk": "*" - } ``` - -For example when you want to use Guzzle's components: - -```bash -composer require php-http/guzzle6-adapter guzzlehttp/psr7 -``` - -If you don't have a compatible HTTP client and/or message factory implementation installed `php-http/discovery` will try to install it for you using a Composer plugin. - -## Maintained versions - - * 4.x is actively maintained and developed on the master branch, and uses Sentry SDK 3.0; - * 3.x is supported only for fixes and uses Sentry SDK 2.0; - * 2.x is no longer maintained; from this version onwards it requires Symfony 3+ and PHP 7.1+; - * 1.x is no longer maintained; you can use it for Symfony < 2.8 and PHP 5.6/7.0; - * 0.8.x is no longer maintained. - -### Upgrading to 4.0 - -The 4.0 version of the bundle uses the newest version (3.x) of the underlying Sentry SDK. If you need to migrate from previous versions, please check the `UPGRADE-4.0.md` document. - -#### Custom serializers - -The option class_serializers can be used to send customized objects serialization. -```yml -sentry: - options: - class_serializers: - YourValueObject: 'ValueObjectSerializer' +###> sentry/sentry-symfony ### +SENTRY_DSN="https://public@sentry.example.com/1" +###< sentry/sentry-symfony ### ``` -Several serializers can be added and the serializable check is done by using the **instanceof** type operator. -The serializer must implement the `__invoke` method, which needs to return an **array**, containing the information that should be send to Sentry. The class name is always sent by default. +### Usage -Serializer example: ```php -final class ValueObjectSerializer -{ - public function __invoke(YourValueObject $vo): array - { - return [ - 'value' => $vo->value() - ]; - } +use function Sentry\captureException; + +try { + $this->functionThatMayFail(); +} catch (\Throwable $exception) { + captureException($exception); } ``` @@ -171,6 +55,12 @@ final class ValueObjectSerializer Please refer to [CONTRIBUTING.md](CONTRIBUTING.md). +### Thanks to all the people who contributed so far! + + + + + ## Getting help/support If you need help setting up or configuring the Symfony SDK (or anything else in the Sentry universe) please head over to the [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr). There is a ton of great people in our Discord community ready to help you! @@ -190,3 +80,4 @@ Licensed under the MIT license, see [`LICENSE`](LICENSE) [Packagist link]: https://packagist.org/packages/sentry/sentry-symfony [Master Code Coverage]: https://codecov.io/gh/getsentry/sentry-symfony/branch/master [Master Code Coverage Image]: https://img.shields.io/codecov/c/github/getsentry/sentry-symfony/master?logo=codecov + diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md new file mode 100644 index 00000000..5737c2cc --- /dev/null +++ b/UPGRADE-5.0.md @@ -0,0 +1,54 @@ +# Upgrade 4.x to 5.0 + +This version adds support for the underlying [Sentry PHP SDK v4.0](https://github.com/getsentry/sentry-php). +Please refer to the PHP SDK [sentry-php/UPGRADE-4.0.md](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-4.0.md) guide for a complete list of breaking changes. + +- This version exclusively uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/) to send event data to Sentry. + + If you are using [sentry.io](https://sentry.io), no action is needed. + If you are using an on-premise/self-hosted installation of Sentry, the minimum requirement is now version `>= v20.6.0`. + +- You need to have `ext-curl` installed to use the SDK. + +- The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. + Previously, both `Symfony\Component\ErrorHandler\Error\FatalError` and `Symfony\Component\Debug\Exception\FatalErrorException` were ignored by default. + To continue ignoring these exceptions, make the following changes to your `config/packages/sentry.yaml` file: + + ```yaml + // config/packages/sentry.yaml + + sentry: + options: + ignore_exceptions: + - 'Symfony\Component\ErrorHandler\Error\FatalError' + - 'Symfony\Component\Debug\Exception\FatalErrorException' + ``` + + This option performs an [`is_a`](https://www.php.net/manual/en/function.is-a.php) check now, so you can also ignore more generic exceptions. + +- Removed support for `guzzlehttp/psr7: ^1.8.4`. + +- The `RequestFetcher` now relies on `guzzlehttp/psr7: ^2.1.1`. + +- Continue traces from the W3C `traceparent` request header. +- Inject the W3C `traceparent` header on outgoing HTTP client calls. +- Added `Sentry\SentryBundle\Twig\SentryExtension::getW3CTraceMeta()`. + +- The new default value for the `sentry.options.trace_propagation_targets` option is now `null`. To not attach any headers to outgoing requests, set this option to `[]`. + +- Added the `sentry.options.enable_tracing` option. +- Added the `sentry.options.attach_metric_code_locations` option. +- Added the `sentry.options.spotlight` option. +- Added the `sentry.options.spotlight_url` option. +- Added the `sentry.options.transport` option. +- Added the `sentry.options.http_client` option. +- Added the `sentry.options.http_proxy_authentication` option. +- Added the `sentry.options.http_ssl_verify_peer` option. +- Added the `sentry.options.http_compression` option. + +- Removed the `sentry.transport_factory` option. Use `sentry.options.transport` to use a custom transport. +- Removed the `sentry.options.send_attempts` option. You may use a custom transport if you rely on this behaviour. +- Removed the `sentry.options.enable_compression` option. Use `sentry.options.http_compression` instead. + +- Removed `Sentry\SentryBundle\Transport\TransportFactory`. +- Removed `Sentry\State\HubInterface\Sentry\State\HubInterface`. diff --git a/composer.json b/composer.json index 55b77290..580a952c 100644 --- a/composer.json +++ b/composer.json @@ -7,64 +7,48 @@ "license": "MIT", "authors": [ { - "name": "David Cramer", - "email": "dcramer@gmail.com" - }, - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" + "name": "Sentry", + "email": "accounts@sentry.io" } ], - "config": { - "sort-packages": true, - "allow-plugins": { - "composer/package-versions-deprecated": true, - "phpstan/extension-installer": true, - "php-http/discovery": false - } - }, "require": { "php": "^7.2||^8.0", - "guzzlehttp/psr7": "^1.7 || ^2.0", - "jean85/pretty-package-versions": "^1.5 || ^2.0", - "sentry/sdk": "^3.4", - "sentry/sentry": "^3.20.1", + "guzzlehttp/psr7": "^2.1.1", + "jean85/pretty-package-versions": "^1.5||^2.0", + "sentry/sentry": "^4.11.0", "symfony/cache-contracts": "^1.1||^2.4||^3.0", - "symfony/config": "^4.4.20||^5.0.11||^6.0", - "symfony/console": "^4.4.20||^5.0.11||^6.0", - "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/config": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/polyfill-php80": "^1.22", - "symfony/psr-http-message-bridge": "^1.2||^2.0", - "symfony/security-core": "^4.4.20||^5.0.11||^6.0", - "symfony/security-http": "^4.4.20||^5.0.11||^6.0" + "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0" }, "require-dev": { - "doctrine/dbal": "^2.13||^3.0", - "doctrine/doctrine-bundle": "^1.12||^2.5", - "friendsofphp/php-cs-fixer": "^2.19||<=3.16.0", - "jangregor/phpstan-prophecy": "^1.0", - "monolog/monolog": "^1.3||^2.0", - "phpspec/prophecy": "!=1.11.0", - "phpspec/prophecy-phpunit": "^1.1||^2.0", + "doctrine/dbal": "^2.13||^3.3||^4.0", + "doctrine/doctrine-bundle": "^2.6", + "friendsofphp/php-cs-fixer": "^2.19||^3.40", + "masterminds/html5": "^2.8", "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-symfony": "^1.0", - "phpunit/phpunit": "^8.5.14||^9.3.9", - "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", + "phpstan/phpstan": "1.12.5", + "phpstan/phpstan-phpunit": "1.4.0", + "phpstan/phpstan-symfony": "1.4.10", + "phpunit/phpunit": "^8.5.40||^9.6.21", + "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0", "symfony/monolog-bundle": "^3.4", - "symfony/phpunit-bridge": "^5.2.6||^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" + "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0", + "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", + "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0", + "vimeo/psalm": "^4.3||^5.16.0" }, "suggest": { "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.", @@ -86,24 +70,23 @@ } }, "scripts": { - "tests": [ - "vendor/bin/phpunit --verbose" + "check": [ + "@cs-check", + "@phpstan", + "@psalm", + "@tests" ], - "phpcs": [ - "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run" - ], - "phpstan": [ - "vendor/bin/phpstan analyse" - ], - "psalm": [ - "vendor/bin/psalm" - ] + "tests": "vendor/bin/phpunit --verbose", + "cs-check": "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", + "cs-fix": "vendor/bin/php-cs-fixer fix --verbose --diff", + "phpstan": "vendor/bin/phpstan analyse", + "psalm": "vendor/bin/psalm" }, - "extra": { - "branch-alias": { - "releases/3.2.x": "3.2.x-dev", - "releases/2.x": "2.x-dev", - "releases/1.x": "1.x-dev" + "config": { + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c04293a5..4313a41a 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,10 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Parameter \\#1 \\.\\.\\.\\$arrays of function array_merge expects array, mixed given\\.$#" - count: 1 - path: src/DependencyInjection/Compiler/DbalTracingPass.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder\\:\\:root\\(\\)\\.$#" count: 1 @@ -12,22 +7,32 @@ parameters: - message: "#^Cannot access offset 'before_breadcrumb' on mixed\\.$#" - count: 3 + count: 1 path: src/DependencyInjection/SentryExtension.php - message: "#^Cannot access offset 'before_send' on mixed\\.$#" - count: 3 + count: 1 + path: src/DependencyInjection/SentryExtension.php + + - + message: "#^Cannot access offset 'before_send_check_in' on mixed\\.$#" + count: 1 + path: src/DependencyInjection/SentryExtension.php + + - + message: "#^Cannot access offset 'before_send_metrics' on mixed\\.$#" + count: 1 path: src/DependencyInjection/SentryExtension.php - message: "#^Cannot access offset 'before_send…' on mixed\\.$#" - count: 3 + count: 1 path: src/DependencyInjection/SentryExtension.php - message: "#^Cannot access offset 'class_serializers' on mixed\\.$#" - count: 3 + count: 1 path: src/DependencyInjection/SentryExtension.php - @@ -50,6 +55,11 @@ parameters: count: 1 path: src/DependencyInjection/SentryExtension.php + - + message: "#^Cannot access offset 'http_client' on mixed\\.$#" + count: 1 + path: src/DependencyInjection/SentryExtension.php + - message: "#^Cannot access offset 'in_app_exclude' on mixed\\.$#" count: 2 @@ -60,13 +70,18 @@ parameters: count: 2 path: src/DependencyInjection/SentryExtension.php + - + message: "#^Cannot access offset 'logger' on mixed\\.$#" + count: 1 + path: src/DependencyInjection/SentryExtension.php + - message: "#^Cannot access offset 'traces_sampler' on mixed\\.$#" - count: 3 + count: 1 path: src/DependencyInjection/SentryExtension.php - - message: "#^Class Symfony\\\\Component\\\\Debug\\\\Exception\\\\FatalErrorException not found\\.$#" + message: "#^Cannot access offset 'transport' on mixed\\.$#" count: 1 path: src/DependencyInjection/SentryExtension.php @@ -82,7 +97,7 @@ parameters: - message: "#^Parameter \\#1 \\$id of class Symfony\\\\Component\\\\DependencyInjection\\\\Reference constructor expects string, mixed given\\.$#" - count: 6 + count: 10 path: src/DependencyInjection/SentryExtension.php - @@ -96,7 +111,7 @@ parameters: path: src/DependencyInjection/SentryExtension.php - - message: "#^Parameter \\#2 \\$callback of function array_filter expects callable\\(mixed\\)\\: mixed, Closure\\(string\\)\\: bool given\\.$#" + message: "#^Parameter \\#2 \\$callback of function array_filter expects \\(callable\\(mixed\\)\\: bool\\)\\|null, Closure\\(string\\)\\: bool given\\.$#" count: 1 path: src/DependencyInjection/SentryExtension.php @@ -135,11 +150,6 @@ parameters: count: 4 path: src/DependencyInjection/SentryExtension.php - - - message: "#^Parameter \\#2 \\$registerErrorListener of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:configureErrorListenerIntegration\\(\\) expects bool, mixed given\\.$#" - count: 1 - path: src/DependencyInjection/SentryExtension.php - - message: "#^Parameter \\#2 \\$useDefaultIntegrations of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:configureRequestIntegration\\(\\) expects bool, mixed given\\.$#" count: 1 @@ -155,100 +165,70 @@ parameters: count: 1 path: src/EventListener/AbstractTracingRequestListener.php - - - message: "#^Class Sentry\\\\SentryBundle\\\\EventListener\\\\ConsoleCommandListener extends @final class Sentry\\\\SentryBundle\\\\EventListener\\\\ConsoleListener\\.$#" - count: 1 - path: src/EventListener/ConsoleCommandListener.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 path: src/EventListener/LoginListener.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" + message: "#^Instanceof between Throwable and Symfony\\\\Component\\\\Messenger\\\\Exception\\\\DelayedMessageHandlingException will always evaluate to false\\.$#" count: 1 - path: src/EventListener/RequestListener.php + path: src/EventListener/MessengerListener.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" + message: "#^Instanceof between Throwable and Symfony\\\\Component\\\\Messenger\\\\Exception\\\\HandlerFailedException will always evaluate to false\\.$#" count: 1 - path: src/EventListener/SubRequestListener.php + path: src/EventListener/MessengerListener.php - - message: "#^Parameter \\#1 \\$driver of method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverMiddleware\\:\\:wrap\\(\\) expects Doctrine\\\\DBAL\\\\Driver, mixed given\\.$#" - count: 1 - path: src/Tracing/Doctrine/DBAL/ConnectionConfigurator.php - - - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnection\\:\\:errorInfo\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php - - - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnection\\:\\:exec\\(\\) has parameter \\$sql with no type specified\\.$#" - count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php - - - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnection\\:\\:prepare\\(\\) has parameter \\$sql with no type specified\\.$#" - count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php - - - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnection\\:\\:query\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php + message: "#^Result of && is always false\\.$#" + count: 2 + path: src/EventListener/MessengerListener.php - - message: "#^Parameter \\#1 \\$sql of method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:query\\(\\) expects string, string\\|null given\\.$#" + message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php + path: src/EventListener/RequestListener.php - - message: "#^Parameter \\#2 \\$spanDescription of method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnection\\:\\:traceFunction\\(\\) expects string, string\\|null given\\.$#" + message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php + path: src/EventListener/SubRequestListener.php - - message: "#^Property Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnection\\:\\:\\$spanTags is never read, only written\\.$#" + message: "#^Parameter \\#1 \\$driver of method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverMiddleware\\:\\:wrap\\(\\) expects Doctrine\\\\DBAL\\\\Driver, mixed given\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingDriverConnection.php + path: src/Tracing/Doctrine/DBAL/ConnectionConfigurator.php - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingServerInfoAwareDriverConnection\\:\\:errorInfo\\(\\) return type has no value type specified in iterable type array\\.$#" + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionForV4\\:\\:errorInfo\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php + path: src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingServerInfoAwareDriverConnection\\:\\:exec\\(\\) has parameter \\$sql with no type specified\\.$#" + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionForV4\\:\\:exec\\(\\) should return int\\|numeric\\-string but returns int\\|string\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php + path: src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingServerInfoAwareDriverConnection\\:\\:prepare\\(\\) has parameter \\$sql with no type specified\\.$#" + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionForV4\\:\\:prepare\\(\\) has parameter \\$sql with no type specified\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php + path: src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php - - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingServerInfoAwareDriverConnection\\:\\:query\\(\\) has parameter \\$args with no type specified\\.$#" + message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionForV4\\:\\:query\\(\\) has parameter \\$args with no type specified\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php + path: src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php - message: "#^Parameter \\#1 \\$sql of method Doctrine\\\\DBAL\\\\Driver\\\\Connection\\:\\:query\\(\\) expects string, string\\|null given\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php - - - - message: "#^Parameter \\#2 \\$callback of method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\AbstractTracingStatement\\:\\:traceFunction\\(\\) expects callable\\(mixed \\.\\.\\.\\)\\: Doctrine\\\\DBAL\\\\Driver\\\\Result, array\\{Doctrine\\\\DBAL\\\\Driver\\\\Statement, 'execute'\\} given\\.$#" - count: 1 - path: src/Tracing/Doctrine/DBAL/TracingStatementForV3.php + path: src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php - - message: "#^Parameter \\#4 \\$length of method Doctrine\\\\DBAL\\\\Driver\\\\Statement\\:\\:bindParam\\(\\) expects int\\|null, mixed given\\.$#" + message: "#^Parameter \\#2 \\$spanDescription of method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionForV4\\:\\:traceFunction\\(\\) expects string, string\\|null given\\.$#" count: 1 - path: src/Tracing/Doctrine/DBAL/TracingStatementForV3.php + path: src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php - message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableHttpClient\\:\\:request\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" @@ -291,9 +271,14 @@ parameters: path: tests/DependencyInjection/ConfigurationTest.php - - message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#" + message: "#^Function Symfony\\\\Component\\\\DependencyInjection\\\\Loader\\\\Configurator\\\\ref not found\\.$#" count: 1 - path: tests/DependencyInjection/SentryExtensionTest.php + path: tests/DependencyInjection/Fixtures/php/release_option_fallback_to_env_var.php + + - + message: "#^Used function Symfony\\\\Component\\\\DependencyInjection\\\\Loader\\\\Configurator\\\\ref not found\\.$#" + count: 1 + path: tests/DependencyInjection/Fixtures/php/release_option_fallback_to_env_var.php - message: "#^Cannot access offset 'dsn' on mixed\\.$#" @@ -306,7 +291,7 @@ parameters: path: tests/DependencyInjection/SentryExtensionTest.php - - message: "#^Class Symfony\\\\Component\\\\Debug\\\\Exception\\\\FatalErrorException not found\\.$#" + message: "#^Cannot access offset 'release' on mixed\\.$#" count: 1 path: tests/DependencyInjection/SentryExtensionTest.php @@ -316,12 +301,17 @@ parameters: path: tests/End2End/TracingEnd2EndTest.php - - message: "#^Call to function method_exists\\(\\) with \\$this\\(Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\AuthenticatedTokenStub\\) and 'setAuthenticated' will always evaluate to false\\.$#" + message: "#^Access to undefined constant Symfony\\\\Component\\\\HttpKernel\\\\HttpKernelInterface\\:\\:MASTER_REQUEST\\.$#" count: 1 + path: tests/EventListener/ErrorListenerTest.php + + - + message: "#^Access to undefined constant Symfony\\\\Component\\\\HttpKernel\\\\HttpKernelInterface\\:\\:MASTER_REQUEST\\.$#" + count: 2 path: tests/EventListener/LoginListenerTest.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: "#^Call to function method_exists\\(\\) with \\$this\\(Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\AuthenticatedTokenStub\\) and 'setAuthenticated' will always evaluate to false\\.$#" count: 1 path: tests/EventListener/LoginListenerTest.php @@ -341,15 +331,30 @@ parameters: path: tests/EventListener/LoginListenerTest.php - - 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/LoginListenerTest.php + message: "#^Access to undefined constant Symfony\\\\Component\\\\HttpKernel\\\\HttpKernelInterface\\:\\:MASTER_REQUEST\\.$#" + count: 6 + path: tests/EventListener/RequestListenerTest.php + + - + message: "#^Access to undefined constant Symfony\\\\Component\\\\HttpKernel\\\\HttpKernelInterface\\:\\:MASTER_REQUEST\\.$#" + count: 2 + path: tests/EventListener/SubRequestListenerTest.php - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 path: tests/EventListener/SubRequestListenerTest.php + - + message: "#^Access to undefined constant Symfony\\\\Component\\\\HttpKernel\\\\HttpKernelInterface\\:\\:MASTER_REQUEST\\.$#" + count: 3 + path: tests/EventListener/TracingRequestListenerTest.php + + - + message: "#^Access to undefined constant Symfony\\\\Component\\\\HttpKernel\\\\HttpKernelInterface\\:\\:MASTER_REQUEST\\.$#" + count: 2 + path: tests/EventListener/TracingSubRequestListenerTest.php + - message: "#^Call to an undefined method TCacheAdapter of Symfony\\\\Component\\\\Cache\\\\Adapter\\\\AdapterInterface\\:\\:delete\\(\\)\\.$#" count: 2 @@ -385,91 +390,26 @@ parameters: count: 1 path: tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php + - + message: "#^Property Sentry\\\\SentryBundle\\\\Tests\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionFactoryV4Test\\:\\:\\$databasePlatform is never read, only written\\.$#" + count: 1 + path: tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV4Test.php + - message: "#^Trying to mock an undefined method errorCode\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Connection\\.$#" count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php + path: tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4Test.php - message: "#^Trying to mock an undefined method errorInfo\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Connection\\.$#" count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php + path: tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4Test.php - message: "#^Parameter \\#1 \\$hubOrConnectionFactory of class Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverMiddleware constructor expects Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionFactoryInterface\\|Sentry\\\\State\\\\HubInterface, null given\\.$#" count: 1 path: tests/Tracing/Doctrine/DBAL/TracingDriverMiddlewareTest.php - - - message: "#^Trying to mock an undefined method errorCode\\(\\) on class Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionInterface\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php - - - - message: "#^Trying to mock an undefined method errorInfo\\(\\) on class Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingDriverConnectionInterface\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php - - - - message: "#^Trying to mock an undefined method requiresQueryForServerVersion\\(\\) on class Sentry\\\\SentryBundle\\\\Tests\\\\Tracing\\\\Doctrine\\\\DBAL\\\\Fixture\\\\ServerInfoAwareConnectionStub\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php - - - - message: "#^Parameter \\#2 \\$decoratedStatement of class Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingStatementForV2 constructor expects Doctrine\\\\DBAL\\\\Driver\\\\Statement, PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&Sentry\\\\SentryBundle\\\\Tests\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingStatementForV2Stub given\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Parameter \\#4 \\$length of method Sentry\\\\SentryBundle\\\\Tracing\\\\Doctrine\\\\DBAL\\\\TracingStatementForV2\\:\\:bindParam\\(\\) expects int\\|null, mixed given\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method closeCursor\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method columnCount\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method errorCode\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method errorInfo\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method fetch\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method fetchAll\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method fetchColumn\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method rowCount\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - count: 1 - path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php - - - - message: "#^Trying to mock an undefined method setFetchMode\\(\\) on class Doctrine\\\\DBAL\\\\Driver\\\\Statement\\.$#" - 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 diff --git a/phpstan.neon b/phpstan.neon index 684f3989..3608367b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,6 +2,7 @@ includes: - phpstan-baseline.neon parameters: + reportUnmatchedIgnoredErrors: true level: 9 paths: - src @@ -12,14 +13,26 @@ parameters: - src/aliases.php - src/Tracing/Cache/TraceableCacheAdapterForV2.php - src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php - - src/Tracing/Doctrine/DBAL/TracingStatementForV2.php + - src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV2V3.php + - src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3.php - src/Tracing/Doctrine/DBAL/TracingDriverForV2.php + - src/Tracing/Doctrine/DBAL/TracingDriverForV3.php + - src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php + - src/Tracing/Doctrine/DBAL/TracingStatementForV2.php + - src/Tracing/Doctrine/DBAL/TracingStatementForV3.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/TracingDriverConnectionFactoryV2Test.php + - tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV3Test.php + - tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3Test.php - tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php + - tests/Tracing/Doctrine/DBAL/TracingDriverForV3Test.php + - tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php + - tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php + - tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php - tests/EventListener/Fixtures/UserWithoutIdentifierStub.php dynamicConstantNames: - Symfony\Component\HttpKernel\Kernel::VERSION diff --git a/phpunit.xml b/phpunit.xml index 72fe2378..e4dadb92 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -20,17 +20,11 @@ src + + src/aliases.php + - - - src - - src/aliases.php - - - - diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 32016223..fc8ebf3e 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,95 +1,60 @@ - + - - FatalErrorException + + - - ConsoleListener + + - - public function __construct(HubInterface $hub, bool $captureErrors = true) + + - - - $event instanceof ExceptionEvent - - - getException - - - - isMasterRequest + + - - iterable + + - - - - - ExceptionConverterDriver - - - - - $this->decoratedDriver->getSchemaManager($conn, $platform) - - - AbstractSchemaManager<T> - - - $params + + + + + + + + + getServerVersion())]]> + + + + getDatabasePlatform($versionProvider)]]> + - - toStream + + - - toStream + + - - TracingDriverForV2 + + - - FilterControllerEvent - FilterResponseEvent - GetResponseEvent - GetResponseEvent - GetResponseForExceptionEvent - 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 89240539..71321eb0 100644 --- a/psalm.xml +++ b/psalm.xml @@ -3,6 +3,8 @@ errorLevel="4" memoizeMethodCallResults="true" errorBaseline="psalm-baseline.xml" + findUnusedBaselineEntry="false" + findUnusedCode="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" @@ -10,7 +12,13 @@ + + + + + + diff --git a/src/Command/SentryTestCommand.php b/src/Command/SentryTestCommand.php index 582f37c2..5c28a46e 100644 --- a/src/Command/SentryTestCommand.php +++ b/src/Command/SentryTestCommand.php @@ -5,18 +5,37 @@ namespace Sentry\SentryBundle\Command; use Sentry\SentrySdk; +use Sentry\State\HubInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * @final since version 4.12 + */ class SentryTestCommand extends Command { + /** + * @var HubInterface + */ + private $hub; + + public function __construct(?HubInterface $hub = null) + { + parent::__construct(); + + if (null === $hub) { + @trigger_error(\sprintf('Not passing an instance of the "%s" interface as argument of the constructor is deprecated since version 4.12 and will not work since version 5.0.', HubInterface::class), \E_USER_DEPRECATED); + } + + $this->hub = $hub ?? SentrySdk::getCurrentHub(); + } + protected function execute(InputInterface $input, OutputInterface $output): int { - $currentHub = SentrySdk::getCurrentHub(); - $client = $currentHub->getClient(); + $client = $this->hub->getClient(); - if (!$client) { + if (null === $client) { $output->writeln('No client found'); $output->writeln('Your DSN is probably missing, check your configuration'); @@ -25,28 +44,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int $dsn = $client->getOptions()->getDsn(); - if ($dsn) { - $output->writeln('DSN correctly configured in the current client'); - } else { + if (null === $dsn) { $output->writeln('No DSN configured in the current client, please check your configuration'); $output->writeln('To debug further, try bin/console debug:config sentry'); return 1; } + $output->writeln('DSN correctly configured in the current client'); $output->writeln('Sending test message...'); - $eventId = $currentHub->captureMessage('This is a test message from the Sentry bundle'); + $eventId = $this->hub->captureMessage('This is a test message from the Sentry bundle'); - if ($eventId) { - $output->writeln("Message sent successfully with ID $eventId"); - } else { + if (null === $eventId) { $output->writeln('Message not sent!'); $output->writeln('Check your DSN or your before_send callback if used'); return 1; } + $output->writeln("Message sent successfully with ID $eventId"); + return 0; } } diff --git a/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php b/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php index 24d3110c..5f935cb6 100644 --- a/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php +++ b/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php @@ -17,9 +17,17 @@ final class AddLoginListenerTagPass implements CompilerPassInterface */ public function process(ContainerBuilder $container): void { + if (!$container->hasDefinition(LoginListener::class)) { + return; + } $listenerDefinition = $container->getDefinition(LoginListener::class); - if (!class_exists(LoginSuccessEvent::class)) { + if (class_exists(LoginSuccessEvent::class)) { + $listenerDefinition->addTag('kernel.event_listener', [ + 'event' => LoginSuccessEvent::class, + 'method' => 'handleLoginSuccessEvent', + ]); + } elseif (class_exists(AuthenticationSuccessEvent::class)) { $listenerDefinition->addTag('kernel.event_listener', [ 'event' => AuthenticationSuccessEvent::class, 'method' => 'handleAuthenticationSuccessEvent', diff --git a/src/DependencyInjection/Compiler/DbalTracingPass.php b/src/DependencyInjection/Compiler/DbalTracingPass.php index d1e4d37f..e4cbcdd7 100644 --- a/src/DependencyInjection/Compiler/DbalTracingPass.php +++ b/src/DependencyInjection/Compiler/DbalTracingPass.php @@ -9,7 +9,6 @@ use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; final class DbalTracingPass implements CompilerPassInterface @@ -20,12 +19,6 @@ final class DbalTracingPass implements CompilerPassInterface */ private const CONNECTION_SERVICE_NAME_FORMAT = 'doctrine.dbal.%s_connection'; - /** - * This is the format used by the DoctrineBundle bundle to register the - * services for each connection's configuration. - */ - private const CONNECTION_CONFIG_SERVICE_NAME_FORMAT = 'doctrine.dbal.%s_connection.configuration'; - /** * {@inheritdoc} */ @@ -52,8 +45,8 @@ public function process(ContainerBuilder $container): void } foreach ($connectionsToTrace as $connectionName) { - if (!\in_array(sprintf(self::CONNECTION_SERVICE_NAME_FORMAT, $connectionName), $connections, true)) { - throw new \InvalidArgumentException(sprintf('The Doctrine connection "%s" does not exists and cannot be instrumented.', $connectionName)); + if (!\in_array(\sprintf(self::CONNECTION_SERVICE_NAME_FORMAT, $connectionName), $connections, true)) { + throw new \InvalidArgumentException(\sprintf('The Doctrine connection "%s" does not exists and cannot be instrumented.', $connectionName)); } if (class_exists(Result::class)) { @@ -66,35 +59,16 @@ public function process(ContainerBuilder $container): void private function configureConnectionForDoctrineDBALVersion3(ContainerBuilder $container, string $connectionName): void { - $configurationDefinition = $container->getDefinition(sprintf(self::CONNECTION_CONFIG_SERVICE_NAME_FORMAT, $connectionName)); - $setMiddlewaresMethodCallArguments = $this->getSetMiddlewaresMethodCallArguments($configurationDefinition); - $setMiddlewaresMethodCallArguments[0] = array_merge($setMiddlewaresMethodCallArguments[0] ?? [], [new Reference(TracingDriverMiddleware::class)]); - - $configurationDefinition - ->removeMethodCall('setMiddlewares') - ->addMethodCall('setMiddlewares', $setMiddlewaresMethodCallArguments); + $tracingMiddlewareDefinition = $container->getDefinition(TracingDriverMiddleware::class); + $tracingMiddlewareDefinition->addTag('doctrine.middleware', ['connection' => $connectionName]); } private function configureConnectionForDoctrineDBALVersion2(ContainerBuilder $container, string $connectionName): void { - $connectionDefinition = $container->getDefinition(sprintf(self::CONNECTION_SERVICE_NAME_FORMAT, $connectionName)); + $connectionDefinition = $container->getDefinition(\sprintf(self::CONNECTION_SERVICE_NAME_FORMAT, $connectionName)); $connectionDefinition->setConfigurator([new Reference(ConnectionConfigurator::class), 'configure']); } - /** - * @return mixed[] - */ - private function getSetMiddlewaresMethodCallArguments(Definition $definition): array - { - foreach ($definition->getMethodCalls() as $methodCall) { - if ('setMiddlewares' === $methodCall[0]) { - return $methodCall[1]; - } - } - - return []; - } - private function assertRequiredDbalVersion(): void { if (interface_exists(Result::class)) { diff --git a/src/DependencyInjection/Compiler/HttpClientTracingPass.php b/src/DependencyInjection/Compiler/HttpClientTracingPass.php index 2464532b..99f7364d 100644 --- a/src/DependencyInjection/Compiler/HttpClientTracingPass.php +++ b/src/DependencyInjection/Compiler/HttpClientTracingPass.php @@ -12,6 +12,17 @@ final class HttpClientTracingPass implements CompilerPassInterface { + /** + * List of service IDs that can be registered in the container by the + * framework when decorating the mock client. The order is from the + * outermost decorator to the innermost. + */ + private const MOCK_HTTP_CLIENT_SERVICE_IDS = [ + 'http_client.retryable.inner.mock_client', + '.debug.http_client.inner.mock_client', + 'http_client.mock_client', + ]; + /** * {@inheritdoc} */ @@ -21,12 +32,48 @@ public function process(ContainerBuilder $container): void 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']); + $decoratedService = $this->getDecoratedService($container); + + if (null === $decoratedService) { + return; } + + $container->register(TraceableHttpClient::class, TraceableHttpClient::class) + ->setArgument(0, new Reference(TraceableHttpClient::class . '.inner')) + ->setArgument(1, new Reference(HubInterface::class)) + ->setDecoratedService($decoratedService[0], null, $decoratedService[1]); + } + + /** + * @return array{string, int}|null + */ + private function getDecoratedService(ContainerBuilder $container): ?array + { + // Starting from Symfony 6.3, the raw HTTP client that serves as adapter + // for the transport is registered as a separate service, so that the + // scoped clients can inject it before any decoration is applied on them. + // Since we need to access the full URL of the request, and such information + // is available after the `ScopingHttpClient` class did its job, we have + // to decorate such service. For more details, see https://github.com/symfony/symfony/pull/49513. + if ($container->hasDefinition('http_client.transport')) { + return ['http_client.transport', -15]; + } + + // On versions of Symfony prior to 6.3, when the mock client is in-use, + // each HTTP client is decorated by referencing explicitly the innermost + // service rather than by using the standard decoration feature. Hence, + // we have to look for the specific names of those services, and decorate + // them instead of the raw HTTP client. + foreach (self::MOCK_HTTP_CLIENT_SERVICE_IDS as $httpClientServiceId) { + if ($container->hasDefinition($httpClientServiceId)) { + return [$httpClientServiceId, 15]; + } + } + + if ($container->hasDefinition('http_client')) { + return ['http_client', 15]; + } + + return null; } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index fa7005fe..ca095895 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,10 +5,8 @@ namespace Sentry\SentryBundle\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Jean85\PrettyVersions; use Sentry\Options; use Sentry\SentryBundle\ErrorTypesParser; -use Sentry\Transport\TransportFactoryInterface; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; @@ -44,7 +42,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode ->children() ->scalarNode('dsn') - ->info('If this value is not provided, the SDK will try to read it from the SENTRY_DSN environment variable. If that variable also does not exist, the SDK will just not send any events.') + ->info('If this value is not provided, the SDK will try to read it from the SENTRY_DSN environment variable. If that variable also does not exist, the SDK will not send any events.') ->end() ->booleanNode('register_error_listener')->defaultTrue()->end() ->booleanNode('register_error_handler')->defaultTrue()->end() @@ -52,10 +50,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('The service ID of the PSR-3 logger used to log messages coming from the SDK client. Be aware that setting the same logger of the application may create a circular loop when an event fails to be sent.') ->defaultNull() ->end() - ->scalarNode('transport_factory') - ->info('The service ID of the transport factory used by the default SDK client.') - ->defaultValue(TransportFactoryInterface::class) - ->end() ->arrayNode('options') ->addDefaultsIfNotSet() ->fixXmlConfig('integration') @@ -70,7 +64,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarPrototype()->end() ->end() ->booleanNode('default_integrations')->end() - ->integerNode('send_attempts')->min(0)->end() ->arrayNode('prefixes') ->defaultValue(array_merge(['%kernel.project_dir%'], array_filter(explode(\PATH_SEPARATOR, get_include_path() ?: '')))) ->scalarPrototype()->end() @@ -80,33 +73,46 @@ public function getConfigTreeBuilder(): TreeBuilder ->max(1.0) ->info('The sampling factor to apply to events. A value of 0 will deny sending any event, and a value of 1 will send all events.') ->end() + ->booleanNode('enable_tracing')->end() ->floatNode('traces_sample_rate') ->min(0.0) ->max(1.0) ->info('The sampling factor to apply to transactions. A value of 0 will deny sending any transaction, and a value of 1 will send all transactions.') ->end() + ->scalarNode('traces_sampler')->end() ->floatNode('profiles_sample_rate') ->min(0.0) ->max(1.0) ->info('The sampling factor to apply to profiles. A value of 0 will deny sending any profiles, and a value of 1 will send all profiles. Profiles are sampled in relation to traces_sample_rate') ->end() - ->scalarNode('traces_sampler')->end() - ->variableNode('trace_propagation_targets')->end() ->booleanNode('attach_stacktrace')->end() + ->booleanNode('attach_metric_code_locations')->end() ->integerNode('context_lines')->min(0)->end() - ->booleanNode('enable_compression')->end() ->scalarNode('environment') ->cannotBeEmpty() ->defaultValue('%kernel.environment%') ->end() ->scalarNode('logger')->end() + ->booleanNode('spotlight')->end() + ->scalarNode('spotlight_url')->end() ->scalarNode('release') ->cannotBeEmpty() - ->defaultValue(PrettyVersions::getRootPackageVersion()->getPrettyVersion()) + ->defaultValue('%env(default::SENTRY_RELEASE)%') ->end() ->scalarNode('server_name')->end() + ->arrayNode('ignore_exceptions') + ->scalarPrototype()->end() + ->beforeNormalization()->castToArray()->end() + ->end() + ->arrayNode('ignore_transactions') + ->scalarPrototype()->end() + ->beforeNormalization()->castToArray()->end() + ->end() ->scalarNode('before_send')->end() ->scalarNode('before_send_transaction')->end() + ->scalarNode('before_send_check_in')->end() + ->scalarNode('before_send_metrics')->end() + ->variableNode('trace_propagation_targets')->end() ->arrayNode('tags') ->useAttributeAsKey('name') ->normalizeKeys(false) @@ -133,7 +139,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->booleanNode('send_default_pii')->end() ->integerNode('max_value_length')->min(0)->end() + ->scalarNode('transport')->end() + ->scalarNode('http_client')->end() ->scalarNode('http_proxy')->end() + ->scalarNode('http_proxy_authentication')->end() ->floatNode('http_connect_timeout') ->min(0) ->info('The maximum number of seconds to wait while trying to connect to a server. It works only when using the default transport.') @@ -142,6 +151,8 @@ public function getConfigTreeBuilder(): TreeBuilder ->min(0) ->info('The maximum execution time for the request+response as a whole. It works only when using the default transport.') ->end() + ->booleanNode('http_ssl_verify_peer')->end() + ->booleanNode('http_compression')->end() ->booleanNode('capture_silenced_errors')->end() ->enumNode('max_request_body_size') ->values([ @@ -156,14 +167,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->normalizeKeys(false) ->scalarPrototype()->end() ->end() - ->arrayNode('ignore_exceptions') - ->scalarPrototype()->end() - ->beforeNormalization()->castToArray()->end() - ->end() - ->arrayNode('ignore_transactions') - ->scalarPrototype()->end() - ->beforeNormalization()->castToArray()->end() - ->end() ->end() ->end() ->end(); diff --git a/src/DependencyInjection/SentryExtension.php b/src/DependencyInjection/SentryExtension.php index 5e559a16..a05a62e8 100644 --- a/src/DependencyInjection/SentryExtension.php +++ b/src/DependencyInjection/SentryExtension.php @@ -5,16 +5,17 @@ namespace Sentry\SentryBundle\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Jean85\PrettyVersions; use Psr\Log\NullLogger; use Sentry\Client; use Sentry\ClientBuilder; -use Sentry\Integration\IgnoreErrorsIntegration; use Sentry\Integration\IntegrationInterface; use Sentry\Integration\RequestFetcherInterface; use Sentry\Integration\RequestIntegration; use Sentry\Options; use Sentry\SentryBundle\EventListener\ConsoleListener; use Sentry\SentryBundle\EventListener\ErrorListener; +use Sentry\SentryBundle\EventListener\LoginListener; use Sentry\SentryBundle\EventListener\MessengerListener; use Sentry\SentryBundle\EventListener\TracingConsoleListener; use Sentry\SentryBundle\EventListener\TracingRequestListener; @@ -25,19 +26,16 @@ use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware; use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension; use Sentry\Serializer\RepresentationSerializer; -use Sentry\Serializer\Serializer; -use Sentry\Transport\TransportFactoryInterface; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Cache\CacheItem; use Symfony\Component\Config\FileLocator; -use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; 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; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; final class SentryExtension extends ConfigurableExtension { @@ -67,6 +65,10 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.xml'); + if (!$container->hasParameter('env(SENTRY_RELEASE)')) { + $container->setParameter('env(SENTRY_RELEASE)', PrettyVersions::getRootPackageVersion()->getPrettyVersion()); + } + $this->registerConfiguration($container, $mergedConfig); $this->registerErrorListenerConfiguration($container, $mergedConfig); $this->registerMessengerListenerConfiguration($container, $mergedConfig['messenger']); @@ -75,6 +77,10 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']); $this->registerCacheTracingConfiguration($container, $mergedConfig['tracing']); $this->registerHttpClientTracingConfiguration($container, $mergedConfig['tracing']); + + if (!interface_exists(TokenStorageInterface::class)) { + $container->removeDefinition(LoginListener::class); + } } /** @@ -94,6 +100,10 @@ private function registerConfiguration(ContainerBuilder $container, array $confi }); } + if (isset($options['logger'])) { + $options['logger'] = new Reference($options['logger']); + } + if (isset($options['traces_sampler'])) { $options['traces_sampler'] = new Reference($options['traces_sampler']); } @@ -106,6 +116,14 @@ private function registerConfiguration(ContainerBuilder $container, array $confi $options['before_send_transaction'] = new Reference($options['before_send_transaction']); } + if (isset($options['before_send_check_in'])) { + $options['before_send_check_in'] = new Reference($options['before_send_check_in']); + } + + if (isset($options['before_send_metrics'])) { + $options['before_send_metrics'] = new Reference($options['before_send_metrics']); + } + if (isset($options['before_breadcrumb'])) { $options['before_breadcrumb'] = new Reference($options['before_breadcrumb']); } @@ -116,6 +134,14 @@ private function registerConfiguration(ContainerBuilder $container, array $confi }, $options['class_serializers']); } + if (isset($options['transport'])) { + $options['transport'] = new Reference($options['transport']); + } + + if (isset($options['http_client'])) { + $options['http_client'] = new Reference($options['http_client']); + } + $container->getDefinition(IntegrationConfigurator::class) ->setArgument(0, $this->configureIntegrationsOption($options['integrations'], $config)) ->setArgument(1, $config['register_error_handler']); @@ -126,10 +152,6 @@ private function registerConfiguration(ContainerBuilder $container, array $confi ->setPublic(false) ->setArgument(0, $options); - $serializer = (new Definition(Serializer::class)) - ->setPublic(false) - ->setArgument(0, new Reference('sentry.client.options')); - $representationSerializerDefinition = (new Definition(RepresentationSerializer::class)) ->setPublic(false) ->setArgument(0, new Reference('sentry.client.options')); @@ -138,15 +160,10 @@ private function registerConfiguration(ContainerBuilder $container, array $confi ? new Reference(NullLogger::class, ContainerBuilder::IGNORE_ON_INVALID_REFERENCE) : new Reference($config['logger']); - $factoryBuilderDefinition = $container->getDefinition(TransportFactoryInterface::class); - $factoryBuilderDefinition->setArgument('$logger', $loggerReference); - $clientBuilderDefinition = (new Definition(ClientBuilder::class)) ->setArgument(0, new Reference('sentry.client.options')) ->addMethodCall('setSdkIdentifier', [SentryBundle::SDK_IDENTIFIER]) ->addMethodCall('setSdkVersion', [SentryBundle::SDK_VERSION]) - ->addMethodCall('setTransportFactory', [new Reference($config['transport_factory'])]) - ->addMethodCall('setSerializer', [$serializer]) ->addMethodCall('setRepresentationSerializer', [$representationSerializerDefinition]) ->addMethodCall('setLogger', [$loggerReference]); @@ -280,36 +297,11 @@ private function configureIntegrationsOption(array $integrations, array $config) return new Reference($value); }, $integrations); - $integrations = $this->configureErrorListenerIntegration($integrations, $config['register_error_listener']); $integrations = $this->configureRequestIntegration($integrations, $config['options']['default_integrations'] ?? true); return $integrations; } - /** - * @param array $integrations - * - * @return array - */ - private function configureErrorListenerIntegration(array $integrations, bool $registerErrorListener): array - { - if ($registerErrorListener && !$this->isIntegrationEnabled(IgnoreErrorsIntegration::class, $integrations)) { - // Prepend this integration to the beginning of the array so that - // we can save some performance by skipping the rest of the integrations - // if the error must be ignored - array_unshift($integrations, new Definition(IgnoreErrorsIntegration::class, [ - [ - 'ignore_exceptions' => [ - FatalError::class, - FatalErrorException::class, - ], - ], - ])); - } - - return $integrations; - } - /** * @param array $integrations * diff --git a/src/ErrorTypesParser.php b/src/ErrorTypesParser.php index aa31d486..b717c641 100644 --- a/src/ErrorTypesParser.php +++ b/src/ErrorTypesParser.php @@ -54,14 +54,28 @@ private static function convertErrorConstants(string $value): string { $output = preg_replace_callback('/(E_[A-Z_]+)/', static function (array $matches) { if (\defined($matches[1])) { - return \constant($matches[1]); + $constant = \constant($matches[1]); + + if (\is_string($constant)) { + return $constant; + } elseif (\is_int($constant)) { + return (string) $constant; + } elseif (\is_array($constant)) { + return implode(' | ', array_map(static function ($value) { + return \is_string($value) ? $value : (string) $value; + }, $constant)); + } elseif (\is_object($constant)) { + return \get_class($constant); + } else { // Non-scalar values + return ''; + } } return $matches[0]; }, $value); if (null === $output) { - throw new \InvalidArgumentException(sprintf('The "%s" value could not be parsed.', $value)); + throw new \InvalidArgumentException(\sprintf('The "%s" value could not be parsed.', $value)); } return $output; diff --git a/src/EventListener/AbstractTracingRequestListener.php b/src/EventListener/AbstractTracingRequestListener.php index 022f2d1b..95c31543 100644 --- a/src/EventListener/AbstractTracingRequestListener.php +++ b/src/EventListener/AbstractTracingRequestListener.php @@ -67,7 +67,7 @@ protected function getRouteName(Request $request): string $route = $request->attributes->get('_controller'); if (\is_array($route) && \is_callable($route, true)) { - $route = sprintf('%s::%s', \is_object($route[0]) ? get_debug_type($route[0]) : $route[0], $route[1]); + $route = \sprintf('%s::%s', \is_object($route[0]) ? get_debug_type($route[0]) : $route[0], $route[1]); } } diff --git a/src/EventListener/ConsoleCommandListener.php b/src/EventListener/ConsoleCommandListener.php deleted file mode 100644 index 1f64ecb3..00000000 --- a/src/EventListener/ConsoleCommandListener.php +++ /dev/null @@ -1,20 +0,0 @@ -tokenStorage || !$this->isMainRequest($event)) { + if ( + null === $this->tokenStorage + || !$this->isMainRequest($event) + || $event->getRequest()->attributes->get('_stateless') + ) { return; } @@ -109,7 +113,7 @@ private function updateUserContext(TokenInterface $token): void private function isTokenAuthenticated(TokenInterface $token): bool { - if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated()) { + if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated(false)) { return false; } diff --git a/src/EventListener/MessengerListener.php b/src/EventListener/MessengerListener.php index f90d5574..b66b962c 100644 --- a/src/EventListener/MessengerListener.php +++ b/src/EventListener/MessengerListener.php @@ -11,7 +11,9 @@ use Sentry\State\Scope; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; +use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException; use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\Exception\WrappedExceptionsInterface; use Symfony\Component\Messenger\Stamp\BusNameStamp; final class MessengerListener @@ -93,8 +95,16 @@ public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event */ private function captureException(\Throwable $exception, bool $willRetry): void { - if ($exception instanceof HandlerFailedException) { - foreach ($exception->getNestedExceptions() as $nestedException) { + if ($exception instanceof WrappedExceptionsInterface) { + $exception = $exception->getWrappedExceptions(); + } elseif ($exception instanceof HandlerFailedException && method_exists($exception, 'getNestedExceptions')) { + $exception = $exception->getNestedExceptions(); + } elseif ($exception instanceof DelayedMessageHandlingException && method_exists($exception, 'getExceptions')) { + $exception = $exception->getExceptions(); + } + + if (\is_array($exception)) { + foreach ($exception as $nestedException) { $this->captureException($nestedException, $willRetry); } diff --git a/src/EventListener/RequestListener.php b/src/EventListener/RequestListener.php index 9c4951fb..3d5948b8 100644 --- a/src/EventListener/RequestListener.php +++ b/src/EventListener/RequestListener.php @@ -11,7 +11,7 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; /** - * This listener ensures that a new {@see \Sentry\State\Scope} is created for + * This listener ensures that a new {@see Scope} is created for * each request and that it is filled with useful information, e.g. the IP * address of the client. */ diff --git a/src/EventListener/TracingConsoleListener.php b/src/EventListener/TracingConsoleListener.php index 5efbaf26..57c187a6 100644 --- a/src/EventListener/TracingConsoleListener.php +++ b/src/EventListener/TracingConsoleListener.php @@ -7,6 +7,7 @@ use Sentry\State\HubInterface; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; +use Sentry\Tracing\SpanStatus; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; use Sentry\Tracing\TransactionSource; @@ -59,18 +60,20 @@ public function handleConsoleCommandEvent(ConsoleCommandEvent $event): void $currentSpan = $this->hub->getSpan(); if (null === $currentSpan) { - $transactionContext = new TransactionContext(); - $transactionContext->setOp('console.command'); - $transactionContext->setName($this->getSpanName($command)); - $transactionContext->setSource(TransactionSource::task()); - - $span = $this->hub->startTransaction($transactionContext); + $span = $this->hub->startTransaction( + TransactionContext::make() + ->setOp('console.command') + ->setOrigin('auto.console') + ->setName($this->getSpanName($command)) + ->setSource(TransactionSource::task()) + ); } else { - $spanContext = new SpanContext(); - $spanContext->setOp('console.command'); - $spanContext->setDescription($this->getSpanName($command)); - - $span = $currentSpan->startChild($spanContext); + $span = $currentSpan->startChild( + SpanContext::make() + ->setOp('console.command') + ->setOrigin('auto.console') + ->setDescription($this->getSpanName($command)) + ); } $this->hub->setSpan($span); @@ -91,6 +94,7 @@ public function handleConsoleTerminateEvent(ConsoleTerminateEvent $event): void $span = $this->hub->getSpan(); if (null !== $span) { + $span->setStatus(0 === $event->getExitCode() ? SpanStatus::ok() : SpanStatus::internalError()); $span->finish(); } } diff --git a/src/EventListener/TracingRequestListener.php b/src/EventListener/TracingRequestListener.php index 56782fc5..f94f9dda 100644 --- a/src/EventListener/TracingRequestListener.php +++ b/src/EventListener/TracingRequestListener.php @@ -4,12 +4,16 @@ namespace Sentry\SentryBundle\EventListener; +use Sentry\Integration\RequestFetcherInterface; +use Sentry\SentryBundle\Integration\RequestFetcher; +use Sentry\State\HubInterface; use Sentry\Tracing\TransactionSource; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Event\TerminateEvent; use function Sentry\continueTrace; +use function Sentry\metrics; /** * This event listener acts on the master requests and starts a transaction @@ -19,6 +23,18 @@ */ final class TracingRequestListener extends AbstractTracingRequestListener { + /** + * @var RequestFetcherInterface|null + */ + private $requestFetcher; + + public function __construct(HubInterface $hub, ?RequestFetcherInterface $requestFetcher = null) + { + parent::__construct($hub); + + $this->requestFetcher = $requestFetcher; + } + /** * This method is called for each subrequest handled by the framework and * starts a new {@see Transaction}. @@ -34,26 +50,32 @@ public function handleKernelRequestEvent(RequestEvent $event): void /** @var Request $request */ $request = $event->getRequest(); + if ($this->requestFetcher instanceof RequestFetcher) { + $this->requestFetcher->setRequest($request); + } + /** @var float $requestStartTime */ $requestStartTime = $request->server->get('REQUEST_TIME_FLOAT', microtime(true)); $context = continueTrace( - $request->headers->get('sentry-trace', ''), + $request->headers->get('sentry-trace') ?? $request->headers->get('traceparent', ''), $request->headers->get('baggage', '') ); + $context->setOp('http.server'); + $context->setOrigin('auto.http.server'); $routeName = $request->attributes->get('_route'); if (null !== $routeName && \is_string($routeName)) { - $context->setName(sprintf('%s %s', $request->getMethod(), $routeName)); + $context->setName(\sprintf('%s %s', $request->getMethod(), $routeName)); $context->setSource(TransactionSource::route()); } else { - $context->setName(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo())); + $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)); + $context->setData($this->getData($request)); $this->hub->setSpan($this->hub->startTransaction($context)); } @@ -73,41 +95,47 @@ public function handleKernelTerminateEvent(TerminateEvent $event): void } $transaction->finish(); + metrics()->flush(); + + if ($this->requestFetcher instanceof RequestFetcher) { + $this->requestFetcher->setRequest(null); + } } /** - * Gets the tags to attach to the transaction. + * Gets the data to attach to the transaction. * * @param Request $request The HTTP request * * @return array */ - private function getTags(Request $request): array + private function getData(Request $request): array { $client = $this->hub->getClient(); $httpFlavor = $this->getHttpFlavor($request); - $tags = [ + + $data = [ 'net.host.port' => (string) $request->getPort(), - 'http.method' => $request->getMethod(), + 'http.request.method' => $request->getMethod(), 'http.url' => $request->getUri(), 'route' => $this->getRouteName($request), ]; if (null !== $httpFlavor) { - $tags['http.flavor'] = $httpFlavor; + $data['http.flavor'] = $httpFlavor; } if (false !== filter_var($request->getHost(), \FILTER_VALIDATE_IP)) { - $tags['net.host.ip'] = $request->getHost(); + $data['net.host.ip'] = $request->getHost(); } else { - $tags['net.host.name'] = $request->getHost(); + $data['net.host.name'] = $request->getHost(); } if (null !== $request->getClientIp() && null !== $client && $client->getOptions()->shouldSendDefaultPii()) { - $tags['net.peer.ip'] = $request->getClientIp(); + $data['net.peer.ip'] = $request->getClientIp(); } - return $tags; + return $data; } /** diff --git a/src/EventListener/TracingSubRequestListener.php b/src/EventListener/TracingSubRequestListener.php index b61b15b4..71198bb1 100644 --- a/src/EventListener/TracingSubRequestListener.php +++ b/src/EventListener/TracingSubRequestListener.php @@ -34,16 +34,19 @@ public function handleKernelRequestEvent(RequestEvent $event): void return; } - $spanContext = new SpanContext(); - $spanContext->setOp('http.server'); - $spanContext->setDescription(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo())); - $spanContext->setTags([ - 'http.method' => $request->getMethod(), - 'http.url' => $request->getUri(), - 'route' => $this->getRouteName($request), - ]); - - $this->hub->setSpan($span->startChild($spanContext)); + $this->hub->setSpan( + $span->startChild( + SpanContext::make() + ->setOp('http.server') + ->setData([ + 'http.request.method' => $request->getMethod(), + 'http.url' => $request->getUri(), + 'route' => $this->getRouteName($request), + ]) + ->setOrigin('auto.http.server') + ->setDescription(\sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo())) + ) + ); } /** diff --git a/src/Integration/RequestFetcher.php b/src/Integration/RequestFetcher.php index f5911b2c..2f2d2cd2 100644 --- a/src/Integration/RequestFetcher.php +++ b/src/Integration/RequestFetcher.php @@ -4,11 +4,12 @@ namespace Sentry\SentryBundle\Integration; -use Http\Discovery\Psr17FactoryDiscovery; +use GuzzleHttp\Psr7\HttpFactory; use Psr\Http\Message\ServerRequestInterface; use Sentry\Integration\RequestFetcherInterface; use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; /** @@ -23,6 +24,11 @@ final class RequestFetcher implements RequestFetcherInterface */ private $requestStack; + /** + * @var Request|null The current request + */ + private $currentRequest; + /** * @var HttpMessageFactoryInterface The factory to convert Symfony requests to PSR-7 requests */ @@ -38,10 +44,10 @@ public function __construct(RequestStack $requestStack, ?HttpMessageFactoryInter { $this->requestStack = $requestStack; $this->httpMessageFactory = $httpMessageFactory ?? new PsrHttpFactory( - Psr17FactoryDiscovery::findServerRequestFactory(), - Psr17FactoryDiscovery::findStreamFactory(), - Psr17FactoryDiscovery::findUploadedFileFactory(), - Psr17FactoryDiscovery::findResponseFactory() + new HttpFactory(), + new HttpFactory(), + new HttpFactory(), + new HttpFactory() ); } @@ -50,7 +56,7 @@ public function __construct(RequestStack $requestStack, ?HttpMessageFactoryInter */ public function fetchRequest(): ?ServerRequestInterface { - $request = $this->requestStack->getCurrentRequest(); + $request = $this->currentRequest ?? $this->requestStack->getCurrentRequest(); if (null === $request) { return null; @@ -62,4 +68,9 @@ public function fetchRequest(): ?ServerRequestInterface return null; } } + + public function setRequest(?Request $request): void + { + $this->currentRequest = $request; + } } diff --git a/src/Resources/config/schema/sentry-1.0.xsd b/src/Resources/config/schema/sentry-1.0.xsd index 78ed5033..3bad5d57 100644 --- a/src/Resources/config/schema/sentry-1.0.xsd +++ b/src/Resources/config/schema/sentry-1.0.xsd @@ -35,28 +35,38 @@ - + + + + + + + + - + + + + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 3f67a790..ff0407e0 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -9,15 +9,6 @@ - - - - - - null - - - @@ -57,6 +48,7 @@ + @@ -91,10 +83,11 @@ - + + @@ -104,6 +97,7 @@ + diff --git a/src/SentryBundle.php b/src/SentryBundle.php index ce44e51f..596b5b94 100644 --- a/src/SentryBundle.php +++ b/src/SentryBundle.php @@ -8,6 +8,7 @@ use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass; use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass; use Sentry\SentryBundle\DependencyInjection\Compiler\HttpClientTracingPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -15,13 +16,13 @@ final class SentryBundle extends Bundle { public const SDK_IDENTIFIER = 'sentry.php.symfony'; - public const SDK_VERSION = '4.10.0'; + public const SDK_VERSION = '5.2.0'; public function build(ContainerBuilder $container): void { parent::build($container); - $container->addCompilerPass(new DbalTracingPass()); + $container->addCompilerPass(new DbalTracingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); $container->addCompilerPass(new CacheTracingPass()); $container->addCompilerPass(new HttpClientTracingPass()); $container->addCompilerPass(new AddLoginListenerTagPass()); diff --git a/src/Tracing/Cache/TraceableCacheAdapterForV2.php b/src/Tracing/Cache/TraceableCacheAdapterForV2.php index 7a5b57c9..e8ed9330 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterForV2.php +++ b/src/Tracing/Cache/TraceableCacheAdapterForV2.php @@ -40,11 +40,11 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte * * @return mixed */ - public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) { return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); + throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); } return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); diff --git a/src/Tracing/Cache/TraceableCacheAdapterForV3.php b/src/Tracing/Cache/TraceableCacheAdapterForV3.php index 564acff3..ef276620 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterForV3.php +++ b/src/Tracing/Cache/TraceableCacheAdapterForV3.php @@ -38,11 +38,11 @@ public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapte * * @param mixed[] $metadata */ - public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed { return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); + throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); } return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); diff --git a/src/Tracing/Cache/TraceableCacheAdapterTrait.php b/src/Tracing/Cache/TraceableCacheAdapterTrait.php index 0bf2bc2f..b2af9a59 100644 --- a/src/Tracing/Cache/TraceableCacheAdapterTrait.php +++ b/src/Tracing/Cache/TraceableCacheAdapterTrait.php @@ -70,7 +70,7 @@ public function delete(string $key): bool { return $this->traceFunction('cache.delete_item', function () use ($key): bool { if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(sprintf('The %s::delete() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); + throw new \BadMethodCallException(\sprintf('The %s::delete() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); } return $this->decoratedAdapter->delete($key); @@ -168,13 +168,15 @@ public function reset(): void * * @phpstan-return TResult */ - private function traceFunction(string $spanOperation, \Closure $callback, string $spanDescription = null) + private function traceFunction(string $spanOperation, \Closure $callback, ?string $spanDescription = null) { $span = $this->hub->getSpan(); if (null !== $span) { - $spanContext = new SpanContext(); - $spanContext->setOp($spanOperation); + $spanContext = SpanContext::make() + ->setOp($spanOperation) + ->setOrigin('auto.cache'); + if (null !== $spanDescription) { $spanContext->setDescription(urldecode($spanDescription)); } @@ -190,4 +192,17 @@ private function traceFunction(string $spanOperation, \Closure $callback, string } } } + + /** + * @phpstan-param \Closure(CacheItem): CacheItem $callback + * @phpstan-param string $key + * + * @phpstan-return callable(): CacheItem + */ + private function setCallbackWrapper(callable $callback, string $key): callable + { + return function () use ($callback, $key): CacheItem { + return $callback($this->decoratedAdapter->getItem($key)); + }; + } } diff --git a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php index a89d911f..62477cc2 100644 --- a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php +++ b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php @@ -41,11 +41,11 @@ public function __construct(HubInterface $hub, TagAwareAdapterInterface $decorat * * @return mixed */ - public function get(string $key, callable $callback, float $beta = null, array &$metadata = null) + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) { return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); + throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); } return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); diff --git a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php index 733b4555..cc5cc7b4 100644 --- a/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php +++ b/src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php @@ -39,11 +39,11 @@ public function __construct(HubInterface $hub, TagAwareAdapterInterface $decorat * * @param mixed[] $metadata */ - public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed { return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { if (!$this->decoratedAdapter instanceof CacheInterface) { - throw new \BadMethodCallException(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); + throw new \BadMethodCallException(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "%s" interface.', self::class, CacheInterface::class)); } return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactory.php b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV2V3.php similarity index 95% rename from src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactory.php rename to src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV2V3.php index 7b5def41..9404f71b 100644 --- a/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactory.php +++ b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV2V3.php @@ -18,7 +18,7 @@ /** * @internal */ -final class TracingDriverConnectionFactory implements TracingDriverConnectionFactoryInterface +final class TracingDriverConnectionFactoryForV2V3 implements TracingDriverConnectionFactoryInterface { /** * @var HubInterface The current hub diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV4.php b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV4.php new file mode 100644 index 00000000..46c64bb6 --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV4.php @@ -0,0 +1,78 @@ +hub = $hub; + } + + /** + * {@inheritdoc} + */ + public function create(Connection $connection, AbstractPlatform $databasePlatform, array $params): TracingDriverConnectionInterface + { + $tracingDriverConnection = new TracingDriverConnection( + $this->hub, + $connection, + $this->getDatabasePlatform($databasePlatform), + $params + ); + + return $tracingDriverConnection; + } + + private function getDatabasePlatform(AbstractPlatform $databasePlatform): string + { + // https://github.com/open-telemetry/opentelemetry-specification/blob/33113489fb5a1b6da563abb4ffa541447b87f515/specification/trace/semantic_conventions/database.md#connection-level-attributes + switch (true) { + case $databasePlatform instanceof AbstractMySQLPlatform: + return 'mysql'; + + case $databasePlatform instanceof DB2Platform: + return 'db2'; + + case $databasePlatform instanceof OraclePlatform: + return 'oracle'; + + case $databasePlatform instanceof PostgreSQLPlatform: + return 'postgresql'; + + case $databasePlatform instanceof SQLitePlatform: + return 'sqlite'; + + case $databasePlatform instanceof SQLServerPlatform: + return 'mssql'; + + default: + return 'other_sql'; + } + } +} diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverConnection.php b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3.php similarity index 83% rename from src/Tracing/Doctrine/DBAL/TracingDriverConnection.php rename to src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3.php index 6233cd17..1010fb5b 100644 --- a/src/Tracing/Doctrine/DBAL/TracingDriverConnection.php +++ b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3.php @@ -14,11 +14,11 @@ /** * This implementation wraps a driver connection and adds distributed tracing * capabilities to Doctrine DBAL. This implementation IS and MUST be compatible - * with all versions of Doctrine DBAL >= 2.10. + * with all versions of Doctrine DBAL >= 2.10 and >= 3.3. * * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams */ -final class TracingDriverConnection implements TracingDriverConnectionInterface +final class TracingDriverConnectionForV2V3 implements TracingDriverConnectionInterface { /** * @internal @@ -60,13 +60,6 @@ final class TracingDriverConnection implements TracingDriverConnectionInterface */ private $decoratedConnection; - /** - * @var array The span tags - * - * @deprecated since version 4.10, to be removed in 5.0. Use $spanData instead. - */ - private $spanTags = []; - /** * @var array The data to attach to the span */ @@ -90,7 +83,7 @@ public function __construct( ) { $this->hub = $hub; $this->decoratedConnection = $decoratedConnection; - $this->spanData = $this->getSpanTags($databasePlatform, $params); + $this->spanData = $this->getSpanData($databasePlatform, $params); } /** @@ -183,7 +176,7 @@ public function rollBack(): bool public function getNativeConnection() { if (!method_exists($this->decoratedConnection, 'getNativeConnection')) { - throw new \BadMethodCallException(sprintf('The connection "%s" does not support accessing the native connection.', \get_class($this->decoratedConnection))); + throw new \BadMethodCallException(\sprintf('The connection "%s" does not support accessing the native connection.', \get_class($this->decoratedConnection))); } return $this->decoratedConnection->getNativeConnection(); @@ -198,7 +191,7 @@ public function errorCode(): ?string return $this->decoratedConnection->errorCode(); } - throw new \BadMethodCallException(sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); } /** @@ -210,7 +203,7 @@ public function errorInfo(): array return $this->decoratedConnection->errorInfo(); } - throw new \BadMethodCallException(sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); } public function getWrappedConnection(): DriverConnectionInterface @@ -230,12 +223,13 @@ private function traceFunction(string $spanOperation, string $spanDescription, \ $span = $this->hub->getSpan(); if (null !== $span) { - $spanContext = new SpanContext(); - $spanContext->setOp($spanOperation); - $spanContext->setDescription($spanDescription); - $spanContext->setData($this->spanData); - - $span = $span->startChild($spanContext); + $span = $span->startChild( + SpanContext::make() + ->setOp($spanOperation) + ->setData($this->spanData) + ->setOrigin('auto.db') + ->setDescription($spanDescription) + ); } try { @@ -258,7 +252,7 @@ private function traceFunction(string $spanOperation, string $spanDescription, \ * * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md */ - private function getSpanTags(string $databasePlatform, array $params): array + private function getSpanData(string $databasePlatform, array $params): array { $data = ['db.system' => $databasePlatform]; @@ -272,20 +266,20 @@ private function getSpanTags(string $databasePlatform, array $params): array if (isset($params['host']) && !empty($params['host']) && !isset($params['memory'])) { if (false === filter_var($params['host'], \FILTER_VALIDATE_IP)) { - $data['net.peer.name'] = $params['host']; + $data['server.address'] = $params['host']; } else { - $data['net.peer.ip'] = $params['host']; + $data['server.address'] = $params['host']; } } if (isset($params['port'])) { - $data['net.peer.port'] = (string) $params['port']; + $data['server.port'] = (string) $params['port']; } if (isset($params['unix_socket'])) { - $data['net.transport'] = 'Unix'; + $data['server.socket.address'] = 'Unix'; } elseif (isset($params['memory'])) { - $data['net.transport'] = 'inproc'; + $data['server.socket.address'] = 'inproc'; } return $data; diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php new file mode 100644 index 00000000..2f506b25 --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php @@ -0,0 +1,285 @@ += 4.0. + * + * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams + */ +final class TracingDriverConnectionForV4 implements TracingDriverConnectionInterface +{ + /** + * @internal + */ + public const SPAN_OP_CONN_PREPARE = 'db.sql.prepare'; + + /** + * @internal + */ + public const SPAN_OP_CONN_QUERY = 'db.sql.query'; + + /** + * @internal + */ + public const SPAN_OP_CONN_EXEC = 'db.sql.exec'; + + /** + * @internal + */ + public const SPAN_OP_CONN_BEGIN_TRANSACTION = 'db.sql.transaction.begin'; + + /** + * @internal + */ + public const SPAN_OP_TRANSACTION_COMMIT = 'db.sql.transaction.commit'; + + /** + * @internal + */ + public const SPAN_OP_TRANSACTION_ROLLBACK = 'db.sql.transaction.rollback'; + + /** + * @var HubInterface The current hub + */ + private $hub; + + /** + * @var DriverConnectionInterface The decorated connection + */ + private $decoratedConnection; + + /** + * @var array The data to attach to the span + */ + private $spanData; + + /** + * Constructor. + * + * @param HubInterface $hub The current hub + * @param DriverConnectionInterface $decoratedConnection The connection to decorate + * @param string $databasePlatform The name of the database platform + * @param array $params The connection params + * + * @phpstan-param ConnectionParams $params + */ + public function __construct( + HubInterface $hub, + DriverConnectionInterface $decoratedConnection, + string $databasePlatform, + array $params + ) { + $this->hub = $hub; + $this->decoratedConnection = $decoratedConnection; + $this->spanData = $this->getSpanData($databasePlatform, $params); + } + + /** + * {@inheritdoc} + */ + public function prepare($sql): Statement + { + $statement = $this->traceFunction(self::SPAN_OP_CONN_PREPARE, $sql, function () use ($sql): Statement { + return $this->decoratedConnection->prepare($sql); + }); + + return new TracingStatement($this->hub, $statement, $sql, $this->spanData); + } + + /** + * {@inheritdoc} + */ + public function query(?string $sql = null, ...$args): Result + { + return $this->traceFunction(self::SPAN_OP_CONN_QUERY, $sql, function () use ($sql, $args): Result { + return $this->decoratedConnection->query($sql, ...$args); + }); + } + + /** + * {@inheritdoc} + */ + public function quote(string $value): string + { + return $this->decoratedConnection->quote($value); + } + + /** + * {@inheritdoc} + */ + public function exec(string $sql): int|string + { + return $this->traceFunction(self::SPAN_OP_CONN_EXEC, $sql, function () use ($sql): int|string { + return $this->decoratedConnection->exec($sql); + }); + } + + /** + * {@inheritdoc} + * + * @return string|int + */ + public function lastInsertId(): string|int + { + return $this->decoratedConnection->lastInsertId(); + } + + /** + * {@inheritdoc} + */ + public function beginTransaction(): void + { + $this->traceFunction(self::SPAN_OP_CONN_BEGIN_TRANSACTION, 'BEGIN TRANSACTION', function (): void { + $this->decoratedConnection->beginTransaction(); + }); + } + + /** + * {@inheritdoc} + */ + public function commit(): void + { + $this->traceFunction(self::SPAN_OP_TRANSACTION_COMMIT, 'COMMIT', function (): void { + $this->decoratedConnection->commit(); + }); + } + + /** + * {@inheritdoc} + */ + public function rollBack(): void + { + $this->traceFunction(self::SPAN_OP_TRANSACTION_ROLLBACK, 'ROLLBACK', function (): void { + $this->decoratedConnection->rollBack(); + }); + } + + /** + * {@inheritdoc} + * + * @return resource|object + */ + public function getNativeConnection() + { + return $this->decoratedConnection->getNativeConnection(); + } + + /** + * {@inheritdoc} + */ + public function errorCode(): ?string + { + if (method_exists($this->decoratedConnection, 'errorCode')) { + return $this->decoratedConnection->errorCode(); + } + + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + } + + /** + * {@inheritdoc} + */ + public function errorInfo(): array + { + if (method_exists($this->decoratedConnection, 'errorInfo')) { + return $this->decoratedConnection->errorInfo(); + } + + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + } + + public function getWrappedConnection(): DriverConnectionInterface + { + return $this->decoratedConnection; + } + + public function getServerVersion(): string + { + return $this->decoratedConnection->getServerVersion(); + } + + /** + * @phpstan-template T + * + * @phpstan-param \Closure(): T $callback + * + * @phpstan-return T + */ + private function traceFunction(string $spanOperation, string $spanDescription, \Closure $callback) + { + $span = $this->hub->getSpan(); + + if (null !== $span) { + $span = $span->startChild( + SpanContext::make() + ->setOp($spanOperation) + ->setData($this->spanData) + ->setOrigin('auto.db') + ->setDescription($spanDescription) + ); + } + + try { + return $callback(); + } finally { + if (null !== $span) { + $span->finish(); + } + } + } + + /** + * Gets a map of key-value pairs that will be set as the span data. + * + * @param array $params The connection params + * + * @return array + * + * @phpstan-param ConnectionParams $params + * + * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + */ + private function getSpanData(string $databasePlatform, array $params): array + { + $data = ['db.system' => $databasePlatform]; + + if (isset($params['user'])) { + $data['db.user'] = $params['user']; + } + + if (isset($params['dbname'])) { + $data['db.name'] = $params['dbname']; + } + + if (isset($params['host']) && !empty($params['host']) && !isset($params['memory'])) { + if (false === filter_var($params['host'], \FILTER_VALIDATE_IP)) { + $data['server.address'] = $params['host']; + } else { + $data['server.address'] = $params['host']; + } + } + + if (isset($params['port'])) { + $data['server.port'] = (string) $params['port']; + } + + if (isset($params['unix_socket'])) { + $data['server.socket.address'] = 'Unix'; + } elseif (isset($params['memory'])) { + $data['server.socket.address'] = 'inproc'; + } + + return $data; + } +} diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverForV3.php b/src/Tracing/Doctrine/DBAL/TracingDriverForV3.php index 6b46bf47..894aecca 100644 --- a/src/Tracing/Doctrine/DBAL/TracingDriverForV3.php +++ b/src/Tracing/Doctrine/DBAL/TracingDriverForV3.php @@ -4,12 +4,8 @@ namespace Sentry\SentryBundle\Tracing\Doctrine\DBAL; -use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Driver\API\ExceptionConverter; -use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Schema\AbstractSchemaManager; -use Doctrine\DBAL\VersionAwarePlatformDriver; +use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware; /** * This is a simple implementation of the {@see Driver} interface that decorates @@ -20,18 +16,13 @@ * * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams */ -final class TracingDriverForV3 implements Driver, VersionAwarePlatformDriver +final class TracingDriverForV3 extends AbstractDriverMiddleware { /** * @var TracingDriverConnectionFactoryInterface The connection factory */ private $connectionFactory; - /** - * @var Driver|VersionAwarePlatformDriver The instance of the decorated driver - */ - private $decoratedDriver; - /** * Constructor. * @@ -40,8 +31,9 @@ final class TracingDriverForV3 implements Driver, VersionAwarePlatformDriver */ public function __construct(TracingDriverConnectionFactoryInterface $connectionFactory, Driver $decoratedDriver) { + parent::__construct($decoratedDriver); + $this->connectionFactory = $connectionFactory; - $this->decoratedDriver = $decoratedDriver; } /** @@ -52,51 +44,9 @@ public function __construct(TracingDriverConnectionFactoryInterface $connectionF public function connect(array $params): TracingDriverConnectionInterface { return $this->connectionFactory->create( - $this->decoratedDriver->connect($params), - $this->decoratedDriver->getDatabasePlatform(), + parent::connect($params), + $this->getDatabasePlatform(), $params ); } - - /** - * {@inheritdoc} - */ - public function getDatabasePlatform(): AbstractPlatform - { - return $this->decoratedDriver->getDatabasePlatform(); - } - - /** - * {@inheritdoc} - * - * @phpstan-template T of AbstractPlatform - * - * @phpstan-param T $platform - * - * @phpstan-return AbstractSchemaManager - */ - public function getSchemaManager(Connection $conn, AbstractPlatform $platform): AbstractSchemaManager - { - return $this->decoratedDriver->getSchemaManager($conn, $platform); - } - - /** - * {@inheritdoc} - */ - public function getExceptionConverter(): ExceptionConverter - { - return $this->decoratedDriver->getExceptionConverter(); - } - - /** - * {@inheritdoc} - */ - public function createDatabasePlatformForVersion($version): AbstractPlatform - { - if ($this->decoratedDriver instanceof VersionAwarePlatformDriver) { - return $this->decoratedDriver->createDatabasePlatformForVersion($version); - } - - return $this->getDatabasePlatform(); - } } diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverForV4.php b/src/Tracing/Doctrine/DBAL/TracingDriverForV4.php new file mode 100644 index 00000000..1f9c4f56 --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/TracingDriverForV4.php @@ -0,0 +1,56 @@ += 4.0. + * + * @internal + * + * @psalm-import-type Params from \Doctrine\DBAL\DriverManager + */ +final class TracingDriverForV4 extends AbstractDriverMiddleware +{ + /** + * @var TracingDriverConnectionFactoryInterface The connection factory + */ + private $connectionFactory; + + /** + * Constructor. + * + * @param TracingDriverConnectionFactoryInterface $connectionFactory The connection factory + * @param Driver $decoratedDriver The instance of the driver to decorate + */ + public function __construct(TracingDriverConnectionFactoryInterface $connectionFactory, Driver $decoratedDriver) + { + parent::__construct($decoratedDriver); + + $this->connectionFactory = $connectionFactory; + } + + /** + * {@inheritdoc} + * + * @psalm-param Params $params All connection parameters. + */ + public function connect(array $params): TracingDriverConnectionInterface + { + $connection = parent::connect($params); + $versionProvider = new StaticServerVersionProvider($connection->getServerVersion()); + + return $this->connectionFactory->create( + $connection, + $this->getDatabasePlatform($versionProvider), + $params + ); + } +} diff --git a/src/Tracing/Doctrine/DBAL/TracingDriverMiddleware.php b/src/Tracing/Doctrine/DBAL/TracingDriverMiddleware.php index 0ef93b70..d9129441 100644 --- a/src/Tracing/Doctrine/DBAL/TracingDriverMiddleware.php +++ b/src/Tracing/Doctrine/DBAL/TracingDriverMiddleware.php @@ -31,11 +31,11 @@ public function __construct($hubOrConnectionFactory) if ($hubOrConnectionFactory instanceof TracingDriverConnectionFactoryInterface) { $this->connectionFactory = $hubOrConnectionFactory; } elseif ($hubOrConnectionFactory instanceof HubInterface) { - @trigger_error(sprintf('Not passing an instance of the "%s" interface as argument of the constructor is deprecated since version 4.2 and will not work since version 5.0.', TracingDriverConnectionFactoryInterface::class), \E_USER_DEPRECATED); + @trigger_error(\sprintf('Not passing an instance of the "%s" interface as argument of the constructor is deprecated since version 4.2 and will not work since version 5.0.', TracingDriverConnectionFactoryInterface::class), \E_USER_DEPRECATED); $this->connectionFactory = new TracingDriverConnectionFactory($hubOrConnectionFactory); } else { - throw new \InvalidArgumentException(sprintf('The constructor requires either an instance of the "%s" interface or an instance of the "%s" interface.', HubInterface::class, TracingDriverConnectionFactoryInterface::class)); + throw new \InvalidArgumentException(\sprintf('The constructor requires either an instance of the "%s" interface or an instance of the "%s" interface.', HubInterface::class, TracingDriverConnectionFactoryInterface::class)); } } diff --git a/src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php b/src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php index de1270bd..30945e97 100644 --- a/src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php +++ b/src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php @@ -109,7 +109,7 @@ public function getServerVersion(): string $wrappedConnection = $this->getWrappedConnection(); if (!$wrappedConnection instanceof ServerInfoAwareConnection) { - throw new \BadMethodCallException(sprintf('The wrapped connection must be an instance of the "%s" interface.', ServerInfoAwareConnection::class)); + throw new \BadMethodCallException(\sprintf('The wrapped connection must be an instance of the "%s" interface.', ServerInfoAwareConnection::class)); } return $wrappedConnection->getServerVersion(); @@ -123,7 +123,7 @@ public function getServerVersion(): string public function getNativeConnection() { if (!method_exists($this->decoratedConnection, 'getNativeConnection')) { - throw new \BadMethodCallException(sprintf('The connection "%s" does not support accessing the native connection.', \get_class($this->decoratedConnection))); + throw new \BadMethodCallException(\sprintf('The connection "%s" does not support accessing the native connection.', \get_class($this->decoratedConnection))); } return $this->decoratedConnection->getNativeConnection(); @@ -137,11 +137,11 @@ public function requiresQueryForServerVersion(): bool $wrappedConnection = $this->getWrappedConnection(); if (!$wrappedConnection instanceof ServerInfoAwareConnection) { - throw new \BadMethodCallException(sprintf('The wrapped connection must be an instance of the "%s" interface.', ServerInfoAwareConnection::class)); + throw new \BadMethodCallException(\sprintf('The wrapped connection must be an instance of the "%s" interface.', ServerInfoAwareConnection::class)); } if (!method_exists($wrappedConnection, 'requiresQueryForServerVersion')) { - throw new \BadMethodCallException(sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); } return $wrappedConnection->requiresQueryForServerVersion(); @@ -156,7 +156,7 @@ public function errorCode(): ?string return $this->decoratedConnection->errorCode(); } - throw new \BadMethodCallException(sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); } /** @@ -168,7 +168,7 @@ public function errorInfo(): array return $this->decoratedConnection->errorInfo(); } - throw new \BadMethodCallException(sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); + throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); } public function getWrappedConnection(): Connection diff --git a/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php b/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php index ca9728ce..a506a17a 100644 --- a/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php +++ b/src/Tracing/Doctrine/DBAL/TracingStatementForV2.php @@ -116,10 +116,11 @@ public function bindParam($param, &$variable, $type = ParameterType::STRING, $le */ public function execute($params = null): bool { - $spanContext = new SpanContext(); - $spanContext->setOp(self::SPAN_OP_STMT_EXECUTE); - $spanContext->setDescription($this->sqlQuery); - $spanContext->setData($this->spanData); + $spanContext = SpanContext::make() + ->setOp(self::SPAN_OP_STMT_EXECUTE) + ->setData($this->spanData) + ->setOrigin('auto.db') + ->setDescription($this->sqlQuery); return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params); } diff --git a/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php b/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php index b2e8f1a0..45487a5a 100644 --- a/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php +++ b/src/Tracing/Doctrine/DBAL/TracingStatementForV3.php @@ -35,10 +35,11 @@ public function bindParam($param, &$variable, $type = ParameterType::STRING, $le */ public function execute($params = null): Result { - $spanContext = new SpanContext(); - $spanContext->setOp(self::SPAN_OP_STMT_EXECUTE); - $spanContext->setDescription($this->sqlQuery); - $spanContext->setData($this->spanData); + $spanContext = SpanContext::make() + ->setOp(self::SPAN_OP_STMT_EXECUTE) + ->setData($this->spanData) + ->setOrigin('auto.db') + ->setDescription($this->sqlQuery); return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params); } diff --git a/src/Tracing/Doctrine/DBAL/TracingStatementForV4.php b/src/Tracing/Doctrine/DBAL/TracingStatementForV4.php new file mode 100644 index 00000000..5537549e --- /dev/null +++ b/src/Tracing/Doctrine/DBAL/TracingStatementForV4.php @@ -0,0 +1,38 @@ +decoratedStatement->bindValue($param, $value, $type); + } + + /** + * {@inheritdoc} + */ + public function execute(): Result + { + $spanContext = SpanContext::make() + ->setOp(self::SPAN_OP_STMT_EXECUTE) + ->setData($this->spanData) + ->setOrigin('auto.db') + ->setDescription($this->sqlQuery); + + return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute']); + } +} diff --git a/src/Tracing/HttpClient/AbstractTraceableHttpClient.php b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php index 8dc86020..d1f83b2c 100644 --- a/src/Tracing/HttpClient/AbstractTraceableHttpClient.php +++ b/src/Tracing/HttpClient/AbstractTraceableHttpClient.php @@ -18,6 +18,7 @@ use function Sentry\getBaggage; use function Sentry\getTraceparent; +use function Sentry\getW3CTraceparent; /** * This is an implementation of the {@see HttpClientInterface} that decorates @@ -58,6 +59,7 @@ public function request(string $method, string $url, array $options = []): Respo if (self::shouldAttachTracingHeaders($client, $uri)) { $headers['baggage'] = getBaggage(); $headers['sentry-trace'] = getTraceparent(); + $headers['traceparent'] = getW3CTraceparent(); } $options['headers'] = $headers; @@ -72,23 +74,29 @@ public function request(string $method, string $url, array $options = []): Respo 'path' => $uri->getPath(), ]); - $context = new SpanContext(); - $context->setOp('http.client'); - $context->setDescription($method . ' ' . (string) $partialUri); - $context->setTags([ - 'http.method' => $method, + $context = SpanContext::make() + ->setOp('http.client') + ->setOrigin('auto.http.client') + ->setDescription($method . ' ' . (string) $partialUri); + + $contextData = [ 'http.url' => (string) $partialUri, - ]); - $context->setData([ - 'http.query' => $uri->getQuery(), - 'http.fragment' => $uri->getFragment(), - ]); + 'http.request.method' => $method, + ]; + if ('' !== $uri->getQuery()) { + $contextData['http.query'] = $uri->getQuery(); + } + if ('' !== $uri->getFragment()) { + $contextData['http.fragment'] = $uri->getFragment(); + } + $context->setData($contextData); $childSpan = $span->startChild($context); if (self::shouldAttachTracingHeaders($client, $uri)) { $headers['baggage'] = $childSpan->toBaggage(); $headers['sentry-trace'] = $childSpan->toTraceparent(); + $headers['traceparent'] = $childSpan->toW3CTraceparent(); } $options['headers'] = $headers; @@ -99,12 +107,12 @@ public function request(string $method, string $url, array $options = []): Respo /** * {@inheritdoc} */ - public function stream($responses, float $timeout = null): ResponseStreamInterface + 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))); + 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)); @@ -134,12 +142,8 @@ private static function shouldAttachTracingHeaders(?ClientInterface $client, Uri // Check if the request destination is allow listed in the trace_propagation_targets option. if ( - null !== $sdkOptions->getTracePropagationTargets() && - // Due to BC, we treat an empty array (the default) as all hosts are allow listed - ( - [] === $sdkOptions->getTracePropagationTargets() || - \in_array($uri->getHost(), $sdkOptions->getTracePropagationTargets()) - ) + null === $sdkOptions->getTracePropagationTargets() + || \in_array($uri->getHost(), $sdkOptions->getTracePropagationTargets()) ) { return true; } diff --git a/src/Tracing/HttpClient/AbstractTraceableResponse.php b/src/Tracing/HttpClient/AbstractTraceableResponse.php index f53ecd8d..aba53b8e 100644 --- a/src/Tracing/HttpClient/AbstractTraceableResponse.php +++ b/src/Tracing/HttpClient/AbstractTraceableResponse.php @@ -107,7 +107,7 @@ public static function stream(HttpClientInterface $client, iterable $responses, 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))); + 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; diff --git a/src/Tracing/HttpClient/TraceableResponseForV5.php b/src/Tracing/HttpClient/TraceableResponseForV5.php index 0ca572a2..c079fe4f 100644 --- a/src/Tracing/HttpClient/TraceableResponseForV5.php +++ b/src/Tracing/HttpClient/TraceableResponseForV5.php @@ -16,7 +16,7 @@ final class TraceableResponseForV5 extends AbstractTraceableResponse implements * * @return mixed */ - public function getInfo(string $type = null) + public function getInfo(?string $type = null) { return $this->response->getInfo($type); } diff --git a/src/Tracing/HttpClient/TraceableResponseForV6.php b/src/Tracing/HttpClient/TraceableResponseForV6.php index 43dbbbf2..64cc7f28 100644 --- a/src/Tracing/HttpClient/TraceableResponseForV6.php +++ b/src/Tracing/HttpClient/TraceableResponseForV6.php @@ -14,7 +14,7 @@ final class TraceableResponseForV6 extends AbstractTraceableResponse implements /** * {@inheritdoc} */ - public function getInfo(string $type = null): mixed + public function getInfo(?string $type = null): mixed { return $this->response->getInfo($type); } diff --git a/src/Tracing/Twig/TwigTracingExtension.php b/src/Tracing/Twig/TwigTracingExtension.php index ec06e58c..e03996d4 100644 --- a/src/Tracing/Twig/TwigTracingExtension.php +++ b/src/Tracing/Twig/TwigTracingExtension.php @@ -46,11 +46,12 @@ public function enter(Profile $profile): void return; } - $spanContext = new SpanContext(); - $spanContext->setOp('view.render'); - $spanContext->setDescription($this->getSpanDescription($profile)); - - $this->spans[$profile] = $transaction->startChild($spanContext); + $this->spans[$profile] = $transaction->startChild( + SpanContext::make() + ->setOp('view.render') + ->setOrigin('auto.view') + ->setDescription($this->getSpanDescription($profile)) + ); } /** @@ -95,7 +96,7 @@ private function getSpanDescription(Profile $profile): string return $profile->getTemplate(); default: - return sprintf('%s::%s(%s)', $profile->getTemplate(), $profile->getType(), $profile->getName()); + return \sprintf('%s::%s(%s)', $profile->getTemplate(), $profile->getType(), $profile->getName()); } } } diff --git a/src/Transport/TransportFactory.php b/src/Transport/TransportFactory.php deleted file mode 100644 index 6ddd272b..00000000 --- a/src/Transport/TransportFactory.php +++ /dev/null @@ -1,65 +0,0 @@ -decoratedTransportFactory = new DefaultTransportFactory( - $streamFactory, - $requestFactory, - new HttpClientFactory( - $uriFactory, - $responseFactory, - $streamFactory, - $httpClient, - 'sentry.php.symfony', - SentryBundle::SDK_VERSION - ), - $logger - ); - } - - public function create(Options $options): TransportInterface - { - return $this->decoratedTransportFactory->create($options); - } -} diff --git a/src/Twig/SentryExtension.php b/src/Twig/SentryExtension.php index ba705155..56fd71b7 100644 --- a/src/Twig/SentryExtension.php +++ b/src/Twig/SentryExtension.php @@ -10,13 +10,14 @@ use function Sentry\getBaggage; use function Sentry\getTraceparent; +use function Sentry\getW3CTraceparent; final class SentryExtension extends AbstractExtension { /** * @param HubInterface $hub The current hub */ - public function __construct(HubInterface $hub = null) + public function __construct(?HubInterface $hub = null) { } @@ -27,6 +28,7 @@ public function getFunctions(): array { return [ new TwigFunction('sentry_trace_meta', [$this, 'getTraceMeta'], ['is_safe' => ['html']]), + new TwigFunction('sentry_w3c_trace_meta', [$this, 'getW3CTraceMeta'], ['is_safe' => ['html']]), new TwigFunction('sentry_baggage_meta', [$this, 'getBaggageMeta'], ['is_safe' => ['html']]), ]; } @@ -36,7 +38,15 @@ public function getFunctions(): array */ public function getTraceMeta(): string { - return sprintf('', getTraceparent()); + return \sprintf('', getTraceparent()); + } + + /** + * Returns an HTML meta tag named `traceparent`. + */ + public function getW3CTraceMeta(): string + { + return \sprintf('', getW3CTraceparent()); } /** @@ -44,6 +54,6 @@ public function getTraceMeta(): string */ public function getBaggageMeta(): string { - return sprintf('', getBaggage()); + return \sprintf('', getBaggage()); } } diff --git a/src/aliases.php b/src/aliases.php index 839a55c9..92272bef 100644 --- a/src/aliases.php +++ b/src/aliases.php @@ -5,6 +5,7 @@ namespace Sentry\SentryBundle; use Doctrine\DBAL\Result; +use Doctrine\DBAL\VersionAwarePlatformDriver; use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapter; use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV2; use Sentry\SentryBundle\Tracing\Cache\TraceableCacheAdapterForV3; @@ -12,11 +13,19 @@ 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\TracingDriverConnection; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionFactory; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionFactoryForV2V3; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionFactoryForV4; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionForV2V3; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionForV4; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverForV2; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverForV3; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverForV4; 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\Doctrine\DBAL\TracingStatementForV4; use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClient; use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClientForV4; use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClientForV5; @@ -27,8 +36,9 @@ use Sentry\SentryBundle\Tracing\HttpClient\TraceableResponseForV6; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\DoctrineProvider; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\Response\StreamableInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; if (interface_exists(AdapterInterface::class)) { if (!class_exists(DoctrineProvider::class, false) && version_compare(\PHP_VERSION, '8.0.0', '>=')) { @@ -51,20 +61,29 @@ class_alias(TraceableTagAwareCacheAdapterForV2::class, TraceableTagAwareCacheAda } if (!class_exists(TracingStatement::class)) { - if (class_exists(Result::class)) { + if (class_exists(Result::class) && !interface_exists(VersionAwarePlatformDriver::class)) { + class_alias(TracingStatementForV4::class, TracingStatement::class); + class_alias(TracingDriverForV4::class, TracingDriver::class); + class_alias(TracingDriverConnectionForV4::class, TracingDriverConnection::class); + class_alias(TracingDriverConnectionFactoryForV4::class, TracingDriverConnectionFactory::class); + } elseif (class_exists(Result::class)) { class_alias(TracingStatementForV3::class, TracingStatement::class); class_alias(TracingDriverForV3::class, TracingDriver::class); + class_alias(TracingDriverConnectionForV2V3::class, TracingDriverConnection::class); + class_alias(TracingDriverConnectionFactoryForV2V3::class, TracingDriverConnectionFactory::class); } elseif (interface_exists(Result::class)) { class_alias(TracingStatementForV2::class, TracingStatement::class); class_alias(TracingDriverForV2::class, TracingDriver::class); + class_alias(TracingDriverConnectionForV2V3::class, TracingDriverConnection::class); + class_alias(TracingDriverConnectionFactoryForV2V3::class, TracingDriverConnectionFactory::class); } } -if (!class_exists(TraceableResponse::class) && interface_exists(ResponseInterface::class)) { +if (!class_exists(TraceableResponse::class) && class_exists(HttpClient::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', '>=')) { + } elseif (method_exists(HttpClientInterface::class, 'withOptions')) { class_alias(TraceableResponseForV6::class, TraceableResponse::class); class_alias(TraceableHttpClientForV6::class, TraceableHttpClient::class); } else { diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php deleted file mode 100644 index 37937e02..00000000 --- a/tests/BaseTestCase.php +++ /dev/null @@ -1,24 +0,0 @@ -hub = $this->createMock(HubInterface::class); + $this->client = $this->createMock(ClientInterface::class); + $this->command = new CommandTester(new SentryTestCommand($this->hub)); } - public function testExecuteSuccessfully(): void + public function testExecute(): void { - $options = new Options(['dsn' => 'http://public:secret@example.com/sentry/1']); - $client = $this->prophesize(ClientInterface::class); - $client->getOptions() - ->willReturn($options); - - $hub = $this->prophesize(HubInterface::class); - $hub->getClient() - ->willReturn($client->reveal()); $lastEventId = EventId::generate(); - $hub->captureMessage(Argument::containingString('test'), Argument::cetera()) - ->shouldBeCalled() - ->willReturn($lastEventId); - SentrySdk::setCurrentHub($hub->reveal()); + $this->client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['dsn' => 'https://public:secret@example.com/sentry/1'])); + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($this->client); + + $this->hub->expects($this->once()) + ->method('captureMessage') + ->with('This is a test message from the Sentry bundle') + ->willReturn($lastEventId); - $commandTester = $this->executeCommand(); + $exitCode = $this->command->execute([]); + $output = $this->command->getDisplay(); - $output = $commandTester->getDisplay(); - $this->assertStringContainsString('DSN correctly configured', $output); - $this->assertStringContainsString('Sending test message', $output); - $this->assertStringContainsString('Message sent', $output); - $this->assertStringContainsString((string) $lastEventId, $output); - $this->assertSame(0, $commandTester->getStatusCode()); + $this->assertSame(0, $exitCode); + $this->assertStringContainsString('DSN correctly configured in the current client', $output); + $this->assertStringContainsString('Sending test message...', $output); + $this->assertStringContainsString("Message sent successfully with ID $lastEventId", $output); } public function testExecuteFailsDueToMissingDSN(): void { - $client = $this->prophesize(ClientInterface::class); - $client->getOptions() + $this->client->expects($this->once()) + ->method('getOptions') ->willReturn(new Options()); - $hub = $this->prophesize(HubInterface::class); - $hub->getClient() - ->willReturn($client->reveal()); + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($this->client); - SentrySdk::setCurrentHub($hub->reveal()); + $exitCode = $this->command->execute([]); + $output = $this->command->getDisplay(); - $commandTester = $this->executeCommand(); - - $this->assertNotSame(0, $commandTester->getStatusCode()); - $output = $commandTester->getDisplay(); - $this->assertStringContainsString('No DSN configured', $output); - $this->assertStringContainsString('try bin/console debug:config sentry', $output); + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('No DSN configured in the current client, please check your configuration', $output); + $this->assertStringContainsString('To debug further, try bin/console debug:config sentry', $output); } public function testExecuteFailsDueToMessageNotSent(): void { - $options = new Options(['dsn' => 'http://public:secret@example.com/sentry/1']); - $client = $this->prophesize(ClientInterface::class); - $client->getOptions() - ->willReturn($options); - - $hub = $this->prophesize(HubInterface::class); - $hub->getClient() - ->willReturn($client->reveal()); - $hub->captureMessage(Argument::containingString('test'), Argument::cetera()) - ->shouldBeCalled() - ->willReturn(null); + $this->client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['dsn' => 'https://public:secret@example.com/sentry/1'])); - SentrySdk::setCurrentHub($hub->reveal()); + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($this->client); + + $this->hub->expects($this->once()) + ->method('captureMessage') + ->with('This is a test message from the Sentry bundle') + ->willReturn(null); - $commandTester = $this->executeCommand(); + $exitCode = $this->command->execute([]); + $output = $this->command->getDisplay(); - $this->assertNotSame(0, $commandTester->getStatusCode()); - $output = $commandTester->getDisplay(); - $this->assertStringContainsString('DSN correctly configured', $output); - $this->assertStringContainsString('Sending test message', $output); - $this->assertStringContainsString('Message not sent', $output); + $this->assertSame(1, $exitCode); + $this->assertStringContainsString('DSN correctly configured in the current client', $output); + $this->assertStringContainsString('Sending test message...', $output); + $this->assertStringContainsString('Message not sent!', $output); + $this->assertStringContainsString('Check your DSN or your before_send callback if used', $output); } public function testExecuteFailsDueToMissingClient(): void { - $hub = $this->prophesize(HubInterface::class); - $hub->getClient() + $this->hub->expects($this->once()) + ->method('getClient') ->willReturn(null); - SentrySdk::setCurrentHub($hub->reveal()); + $exitCode = $this->command->execute([]); + $output = $this->command->getDisplay(); - $commandTester = $this->executeCommand(); - - $this->assertNotSame(0, $commandTester->getStatusCode()); - $output = $commandTester->getDisplay(); + $this->assertSame(1, $exitCode); $this->assertStringContainsString('No client found', $output); - $this->assertStringContainsString('DSN is probably missing', $output); + $this->assertStringContainsString('Your DSN is probably missing, check your configuration', $output); } - private function executeCommand(): CommandTester + /** + * @group legacy + */ + public function testConstructorTriggersDeprecationErrorIfHubIsNotPassedToConstructor(): void { - $command = new SentryTestCommand(); - $command->setName('sentry:test'); - - $application = new Application(); - $application->add($command); - - $command = $application->find('sentry:test'); - $commandTester = new CommandTester($command); - $commandTester->execute([ - 'command' => $command->getName(), - ]); + $this->expectDeprecation('Not passing an instance of the "Sentry\State\HubInterface" interface as argument of the constructor is deprecated since version 4.12 and will not work since version 5.0.'); - return $commandTester; + new SentryTestCommand(); } } diff --git a/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php b/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php index cc91f8f7..6fb3248b 100644 --- a/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php +++ b/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php @@ -28,4 +28,20 @@ public function testProcess(): void $this->assertSame([['event' => AuthenticationSuccessEvent::class, 'method' => 'handleAuthenticationSuccessEvent']], $listenerDefinition->getTag('kernel.event_listener')); } + + public function testProcessLoginSuccess(): void + { + if (!class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('Skipping this test because LoginSuccessEvent does not exist.'); + } + + $container = new ContainerBuilder(); + $container->register(LoginListener::class)->setPublic(true); + $container->addCompilerPass(new AddLoginListenerTagPass()); + $container->compile(); + + $listenerDefinition = $container->getDefinition(LoginListener::class); + + $this->assertSame([['event' => LoginSuccessEvent::class, 'method' => 'handleLoginSuccessEvent']], $listenerDefinition->getTag('kernel.event_listener')); + } } diff --git a/tests/DependencyInjection/Compiler/DbalTracingPassTest.php b/tests/DependencyInjection/Compiler/DbalTracingPassTest.php index 93a6b5d7..6fabc85f 100644 --- a/tests/DependencyInjection/Compiler/DbalTracingPassTest.php +++ b/tests/DependencyInjection/Compiler/DbalTracingPassTest.php @@ -16,72 +16,43 @@ final class DbalTracingPassTest extends DoctrineTestCase { - public function testProcessWithDoctrineDBALVersionAtLeast30(): void + public function testProcessWithDoctrineDBALVersion4(): void { - if (!self::isDoctrineDBALVersion3Installed()) { - $this->markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + if (!self::isDoctrineDBALVersion4Installed()) { + $this->markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 4.0.'); } $container = $this->createContainerBuilder(); - $container->setParameter('sentry.tracing.dbal.connections', ['foo', 'bar', 'baz']); + $container->setParameter('sentry.tracing.dbal.connections', ['foo', 'bar']); + $container->compile(); - $container - ->register('foo.service', \stdClass::class) - ->setPublic(true); + $tracingMiddlewareDefinition = $container->getDefinition(TracingDriverMiddleware::class); + $doctrineMiddlewareTags = $tracingMiddlewareDefinition->getTag('doctrine.middleware'); - $container - ->register('doctrine.dbal.foo_connection.configuration', Configuration::class) - ->setPublic(true); - - $container - ->register('doctrine.dbal.bar_connection.configuration', Configuration::class) - ->addMethodCall('setMiddlewares', [[]]) - ->setPublic(true); + $this->assertCount(2, $doctrineMiddlewareTags); + $this->assertSame(['connection' => 'foo'], $doctrineMiddlewareTags[0]); + $this->assertSame(['connection' => 'bar'], $doctrineMiddlewareTags[1]); + } - $container - ->register('doctrine.dbal.baz_connection.configuration', Configuration::class) - ->addMethodCall('setMiddlewares', [[new Reference('foo.service')]]) - ->setPublic(true); + public function testProcessWithDoctrineDBALVersion3(): void + { + if (!self::isDoctrineDBALVersion3Installed()) { + $this->markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); + } + $container = $this->createContainerBuilder(); + $container->setParameter('sentry.tracing.dbal.connections', ['foo', 'bar']); $container->compile(); - $this->assertEquals( - [ - [ - 'setMiddlewares', - [[new Reference(TracingDriverMiddleware::class)]], - ], - ], - $container->getDefinition('doctrine.dbal.foo_connection.configuration')->getMethodCalls() - ); - - $this->assertEquals( - [ - [ - 'setMiddlewares', - [[new Reference(TracingDriverMiddleware::class)]], - ], - ], - $container->getDefinition('doctrine.dbal.bar_connection.configuration')->getMethodCalls() - ); - - $this->assertEquals( - [ - [ - 'setMiddlewares', - [ - [ - new Reference('foo.service'), - new Reference(TracingDriverMiddleware::class), - ], - ], - ], - ], - $container->getDefinition('doctrine.dbal.baz_connection.configuration')->getMethodCalls() - ); + $tracingMiddlewareDefinition = $container->getDefinition(TracingDriverMiddleware::class); + $doctrineMiddlewareTags = $tracingMiddlewareDefinition->getTag('doctrine.middleware'); + + $this->assertCount(2, $doctrineMiddlewareTags); + $this->assertSame(['connection' => 'foo'], $doctrineMiddlewareTags[0]); + $this->assertSame(['connection' => 'bar'], $doctrineMiddlewareTags[1]); } - public function testProcessWithDoctrineDBALVersionLowerThan30(): void + public function testProcessWithDoctrineDBALVersion2(): void { if (!self::isDoctrineDBALVersion2Installed()) { $this->markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be ^2.13.'); diff --git a/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php b/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php index 9f3d12a7..8b2ccb66 100644 --- a/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php +++ b/tests/DependencyInjection/Compiler/HttpClientTracingPassTest.php @@ -9,7 +9,6 @@ use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClient; use Sentry\State\HubInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; final class HttpClientTracingPassTest extends TestCase @@ -21,12 +20,46 @@ public static function setUpBeforeClass(): void } } - public function testProcess(): void + /** + * @dataProvider processDataProvider + */ + public function testProcess(string $httpClientServiceId): void + { + $container = $this->createContainerBuilder(true, true, $httpClientServiceId); + $container->compile(); + + $this->assertSame(TraceableHttpClient::class, $container->getDefinition($httpClientServiceId)->getClass()); + } + + public function processDataProvider(): \Generator + { + yield 'The framework version is >=6.3' => [ + 'http_client.transport', + ]; + + yield 'The framework version is <6.3 and the mocked HTTP client is decorated by the retryable client' => [ + 'http_client.retryable.inner.mock_client', + ]; + + yield 'The framework version is <6.3 and the mocked HTTP client is decorated by the profiler' => [ + '.debug.http_client.inner.mock_client', + ]; + + yield 'The framework version is <6.3 and the mocked HTTP client is not decorated' => [ + 'http_client.mock_client', + ]; + + yield 'The framework version is <6.3 and the HTTP client is not mocked' => [ + 'http_client', + ]; + } + + public function testProcessDoesNothingIfHttpClientServiceCannotBeFound(): void { - $container = $this->createContainerBuilder(true, true); + $container = $this->createContainerBuilder(true, true, null); $container->compile(); - $this->assertSame(TraceableHttpClient::class, $container->findDefinition('http.client')->getClass()); + $this->assertFalse($container->hasDefinition('http_client')); } /** @@ -34,10 +67,10 @@ public function testProcess(): void */ public function testProcessDoesNothingIfConditionsForEnablingTracingAreMissing(bool $isTracingEnabled, bool $isHttpClientTracingEnabled): void { - $container = $this->createContainerBuilder($isTracingEnabled, $isHttpClientTracingEnabled); + $container = $this->createContainerBuilder($isTracingEnabled, $isHttpClientTracingEnabled, 'http_client.transport'); $container->compile(); - $this->assertSame(HttpClient::class, $container->getDefinition('http.client')->getClass()); + $this->assertSame(HttpClientInterface::class, $container->getDefinition('http_client.transport')->getClass()); } /** @@ -61,7 +94,7 @@ public function processDoesNothingIfConditionsForEnablingTracingAreMissingDataPr ]; } - private function createContainerBuilder(bool $isTracingEnabled, bool $isHttpClientTracingEnabled): ContainerBuilder + private function createContainerBuilder(bool $isTracingEnabled, bool $isHttpClientTracingEnabled, ?string $httpClientServiceId): ContainerBuilder { $container = new ContainerBuilder(); $container->addCompilerPass(new HttpClientTracingPass()); @@ -71,9 +104,10 @@ private function createContainerBuilder(bool $isTracingEnabled, bool $isHttpClie $container->register(HubInterface::class, HubInterface::class) ->setPublic(true); - $container->register('http.client', HttpClient::class) - ->setPublic(true) - ->addTag('http_client.client'); + if (null !== $httpClientServiceId) { + $container->register($httpClientServiceId, HttpClientInterface::class) + ->setPublic(true); + } return $container; } diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 4eb21812..c993f6f2 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -5,7 +5,6 @@ namespace Sentry\SentryBundle\Tests\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; -use Jean85\PrettyVersions; use PHPUnit\Framework\TestCase; use Sentry\SentryBundle\DependencyInjection\Configuration; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -24,12 +23,13 @@ public function testProcessConfigurationWithDefaultConfiguration(): void 'register_error_listener' => true, 'register_error_handler' => true, 'logger' => null, - 'transport_factory' => 'Sentry\\Transport\\TransportFactoryInterface', 'options' => [ 'integrations' => [], 'prefixes' => array_merge(['%kernel.project_dir%'], array_filter(explode(\PATH_SEPARATOR, get_include_path() ?: ''))), 'environment' => '%kernel.environment%', - 'release' => PrettyVersions::getRootPackageVersion()->getPrettyVersion(), + 'release' => '%env(default::SENTRY_RELEASE)%', + 'ignore_exceptions' => [], + 'ignore_transactions' => [], 'tags' => [], 'in_app_exclude' => [ '%kernel.cache_dir%', @@ -38,8 +38,6 @@ public function testProcessConfigurationWithDefaultConfiguration(): void ], 'in_app_include' => [], 'class_serializers' => [], - 'ignore_exceptions' => [], - 'ignore_transactions' => [], ], 'messenger' => [ 'enabled' => interface_exists(MessageBusInterface::class), diff --git a/tests/DependencyInjection/Fixtures/StubEnvVarLoader.php b/tests/DependencyInjection/Fixtures/StubEnvVarLoader.php new file mode 100644 index 00000000..f83c1ac0 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/StubEnvVarLoader.php @@ -0,0 +1,28 @@ + + */ + private $envs = []; + + /** + * @param array $envs + */ + public function __construct(array $envs) + { + $this->envs = $envs; + } + + public function loadEnvVars(): array + { + return $this->envs; + } +} diff --git a/tests/DependencyInjection/Fixtures/php/error_types.php b/tests/DependencyInjection/Fixtures/php/error_types.php index 9c8473e5..6d072037 100644 --- a/tests/DependencyInjection/Fixtures/php/error_types.php +++ b/tests/DependencyInjection/Fixtures/php/error_types.php @@ -7,6 +7,7 @@ /** @var ContainerBuilder $container */ $container->loadFromExtension('sentry', [ 'options' => [ - 'error_types' => \E_ALL & ~(\E_NOTICE | \E_STRICT | \E_DEPRECATED), + // 2048 is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices + 'error_types' => \E_ALL & ~(\E_NOTICE | 2048 | \E_DEPRECATED), ], ]); diff --git a/tests/DependencyInjection/Fixtures/php/full.php b/tests/DependencyInjection/Fixtures/php/full.php index 7c69cc19..aa11c22f 100644 --- a/tests/DependencyInjection/Fixtures/php/full.php +++ b/tests/DependencyInjection/Fixtures/php/full.php @@ -7,27 +7,32 @@ /** @var ContainerBuilder $container */ $container->loadFromExtension('sentry', [ 'dsn' => 'https://examplePublicKey@o0.ingest.sentry.io/0', - 'transport_factory' => 'App\\Sentry\\Transport\\TransportFactory', 'logger' => 'app.logger', 'options' => [ 'integrations' => ['App\\Sentry\\Integration\\FooIntegration'], 'default_integrations' => false, - 'send_attempts' => 1, 'prefixes' => ['%kernel.project_dir%'], 'sample_rate' => 1, + 'enable_tracing' => true, 'traces_sample_rate' => 1, - 'profiles_sample_rate' => 1, 'traces_sampler' => 'App\\Sentry\\Tracing\\TracesSampler', - 'trace_propagation_targets' => ['website.invalid'], + 'profiles_sample_rate' => 1, 'attach_stacktrace' => true, + 'attach_metric_code_locations' => true, 'context_lines' => 0, - 'enable_compression' => true, 'environment' => 'development', - 'logger' => 'php', + 'logger' => Sentry\Logger\DebugStdOutLogger::class, + 'spotlight' => true, + 'spotlight_url' => 'http://localhost:8969', 'release' => '4.0.x-dev', 'server_name' => 'localhost', + 'ignore_exceptions' => ['Symfony\Component\HttpKernel\Exception\BadRequestHttpException'], + 'ignore_transactions' => ['GET tracing_ignored_transaction'], 'before_send' => 'App\\Sentry\\BeforeSendCallback', 'before_send_transaction' => 'App\\Sentry\\BeforeSendTransactionCallback', + 'before_send_check_in' => 'App\\Sentry\\BeforeSendCheckInCallback', + 'before_send_metrics' => 'App\\Sentry\\BeforeSendMetricsCallback', + 'trace_propagation_targets' => ['website.invalid'], 'tags' => [ 'context' => 'development', ], @@ -38,14 +43,17 @@ 'in_app_include' => ['%kernel.project_dir%'], 'send_default_pii' => true, 'max_value_length' => 255, + 'transport' => 'App\\Sentry\\Transport', + 'http_client' => 'App\\Sentry\\HttpClient', 'http_proxy' => 'proxy.example.com:8080', - 'http_timeout' => 10, + 'http_proxy_authentication' => 'user:password', 'http_connect_timeout' => 15, + 'http_timeout' => 10, + 'http_ssl_verify_peer' => true, + 'http_compression' => true, 'capture_silenced_errors' => true, 'max_request_body_size' => 'none', 'class_serializers' => ['App\\FooClass' => 'App\\Sentry\\Serializer\\FooClassSerializer'], - 'ignore_exceptions' => ['Symfony\Component\HttpKernel\Exception\BadRequestHttpException'], - 'ignore_transactions' => ['GET tracing_ignored_transaction'], ], 'messenger' => [ 'enabled' => true, diff --git a/tests/DependencyInjection/Fixtures/php/release_option_fallback_to_composer_version.php b/tests/DependencyInjection/Fixtures/php/release_option_fallback_to_composer_version.php new file mode 100644 index 00000000..ba092c8a --- /dev/null +++ b/tests/DependencyInjection/Fixtures/php/release_option_fallback_to_composer_version.php @@ -0,0 +1,8 @@ +loadFromExtension('sentry', []); diff --git a/tests/DependencyInjection/Fixtures/php/release_option_fallback_to_env_var.php b/tests/DependencyInjection/Fixtures/php/release_option_fallback_to_env_var.php new file mode 100644 index 00000000..aa23806f --- /dev/null +++ b/tests/DependencyInjection/Fixtures/php/release_option_fallback_to_env_var.php @@ -0,0 +1,27 @@ +extension('sentry', []); + + $container->services() + ->set(StubEnvVarLoader::class) + ->tag('container.env_var_loader') + ->args([['SENTRY_RELEASE' => '1.0.x-dev']]) + + ->set(EnvVarProcessor::class) + ->tag('container.env_var_processor') + ->args([ + function_exists('Symfony\\Component\\DependencyInjection\\Loader\\Configurator\\service') ? service('service_container') : ref('service_container'), + tagged_iterator('container.env_var_loader'), + ]); +}; diff --git a/tests/DependencyInjection/Fixtures/php/release_option_from_config.php b/tests/DependencyInjection/Fixtures/php/release_option_from_config.php new file mode 100644 index 00000000..3c594daf --- /dev/null +++ b/tests/DependencyInjection/Fixtures/php/release_option_from_config.php @@ -0,0 +1,12 @@ +loadFromExtension('sentry', [ + 'options' => [ + 'release' => '1.0.x-dev', + ], +]); diff --git a/tests/DependencyInjection/Fixtures/php/release_option_from_env_var.php b/tests/DependencyInjection/Fixtures/php/release_option_from_env_var.php new file mode 100644 index 00000000..6d1b0d82 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/php/release_option_from_env_var.php @@ -0,0 +1,13 @@ +setParameter('env(APP_RELEASE)', '1.0.x-dev'); +$container->loadFromExtension('sentry', [ + 'options' => [ + 'release' => '%env(APP_RELEASE)%', + ], +]); diff --git a/tests/DependencyInjection/Fixtures/xml/error_types.xml b/tests/DependencyInjection/Fixtures/xml/error_types.xml index c0c46ab9..4be73596 100644 --- a/tests/DependencyInjection/Fixtures/xml/error_types.xml +++ b/tests/DependencyInjection/Fixtures/xml/error_types.xml @@ -7,6 +7,6 @@ https://sentry.io/schema/dic/sentry-symfony https://sentry.io/schema/dic/sentry-symfony/sentry-1.0.xsd"> - + diff --git a/tests/DependencyInjection/Fixtures/xml/full.xml b/tests/DependencyInjection/Fixtures/xml/full.xml index f9d84f93..df951eb4 100644 --- a/tests/DependencyInjection/Fixtures/xml/full.xml +++ b/tests/DependencyInjection/Fixtures/xml/full.xml @@ -8,32 +8,40 @@ diff --git a/tests/DependencyInjection/Fixtures/xml/release_option_fallback_to_composer_version.xml b/tests/DependencyInjection/Fixtures/xml/release_option_fallback_to_composer_version.xml new file mode 100644 index 00000000..1ffe0852 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/release_option_fallback_to_composer_version.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/tests/DependencyInjection/Fixtures/xml/release_option_fallback_to_env_var.xml b/tests/DependencyInjection/Fixtures/xml/release_option_fallback_to_env_var.xml new file mode 100644 index 00000000..2c32ce7d --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/release_option_fallback_to_env_var.xml @@ -0,0 +1,27 @@ + + + + + + + + 1.0.x-dev + + + + + + + + + + + + + + + diff --git a/tests/DependencyInjection/Fixtures/xml/release_option_from_config.xml b/tests/DependencyInjection/Fixtures/xml/release_option_from_config.xml new file mode 100644 index 00000000..b10573d2 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/release_option_from_config.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/tests/DependencyInjection/Fixtures/xml/release_option_from_env_var.xml b/tests/DependencyInjection/Fixtures/xml/release_option_from_env_var.xml new file mode 100644 index 00000000..19162356 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/xml/release_option_from_env_var.xml @@ -0,0 +1,16 @@ + + + + + + 1.0.x-dev + + + + + + diff --git a/tests/DependencyInjection/Fixtures/yml/error_types.yml b/tests/DependencyInjection/Fixtures/yml/error_types.yml index 95149254..8c976cd2 100644 --- a/tests/DependencyInjection/Fixtures/yml/error_types.yml +++ b/tests/DependencyInjection/Fixtures/yml/error_types.yml @@ -1,3 +1,3 @@ sentry: options: - error_types: E_ALL & ~(E_NOTICE|E_STRICT|E_DEPRECATED) + error_types: E_ALL & ~(E_NOTICE|2048|E_DEPRECATED) diff --git a/tests/DependencyInjection/Fixtures/yml/full.yml b/tests/DependencyInjection/Fixtures/yml/full.yml index d2f13c5e..8a42e682 100644 --- a/tests/DependencyInjection/Fixtures/yml/full.yml +++ b/tests/DependencyInjection/Fixtures/yml/full.yml @@ -1,29 +1,36 @@ sentry: dsn: https://examplePublicKey@o0.ingest.sentry.io/0 - transport_factory: App\Sentry\Transport\TransportFactory logger: app.logger options: integrations: - App\Sentry\Integration\FooIntegration default_integrations: false - send_attempts: 1 prefixes: - '%kernel.project_dir%' sample_rate: 1 + enable_tracing: true traces_sample_rate: 1 - profiles_sample_rate: 1 traces_sampler: App\Sentry\Tracing\TracesSampler - trace_propagation_targets: - - 'website.invalid' + profiles_sample_rate: 1 attach_stacktrace: true + attach_metric_code_locations: true context_lines: 0 - enable_compression: true environment: development - logger: php + logger: Sentry\Logger\DebugStdOutLogger + spotlight: true + spotlight_url: http://localhost:8969 release: 4.0.x-dev server_name: localhost + ignore_exceptions: + - Symfony\Component\HttpKernel\Exception\BadRequestHttpException + ignore_transactions: + - GET tracing_ignored_transaction before_send: App\Sentry\BeforeSendCallback before_send_transaction: App\Sentry\BeforeSendTransactionCallback + before_send_check_in: App\Sentry\BeforeSendCheckInCallback + before_send_metrics: App\Sentry\BeforeSendMetricsCallback + trace_propagation_targets: + - 'website.invalid' tags: context: development error_types: !php/const E_ALL @@ -35,17 +42,18 @@ sentry: - '%kernel.project_dir%' send_default_pii: true max_value_length: 255 + transport: App\Sentry\Transport + http_client: App\Sentry\HttpClient http_proxy: proxy.example.com:8080 - http_timeout: 10 + http_proxy_authentication: user:password http_connect_timeout: 15 + http_timeout: 10 + http_ssl_verify_peer: true + http_compression: true capture_silenced_errors: true max_request_body_size: 'none' class_serializers: App\FooClass: App\Sentry\Serializer\FooClassSerializer - ignore_exceptions: - - Symfony\Component\HttpKernel\Exception\BadRequestHttpException - ignore_transactions: - - GET tracing_ignored_transaction messenger: enabled: true capture_soft_fails: false diff --git a/tests/DependencyInjection/Fixtures/yml/release_option_fallback_to_composer_version.yml b/tests/DependencyInjection/Fixtures/yml/release_option_fallback_to_composer_version.yml new file mode 100644 index 00000000..64d9acea --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/release_option_fallback_to_composer_version.yml @@ -0,0 +1,2 @@ +sentry: + options: ~ diff --git a/tests/DependencyInjection/Fixtures/yml/release_option_fallback_to_env_var.yml b/tests/DependencyInjection/Fixtures/yml/release_option_fallback_to_env_var.yml new file mode 100644 index 00000000..5ed6fba2 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/release_option_fallback_to_env_var.yml @@ -0,0 +1,16 @@ +services: + Sentry\SentryBundle\Tests\DependencyInjection\Fixtures\StubEnvVarLoader: + arguments: + - { SENTRY_RELEASE: 1.0.x-dev } + tags: + - { name: container.env_var_loader } + + Symfony\Component\DependencyInjection\EnvVarProcessor: + arguments: + - '@service_container' + - !tagged_iterator container.env_var_loader + tags: + - { name: container.env_var_processor } + +sentry: + options: ~ diff --git a/tests/DependencyInjection/Fixtures/yml/release_option_from_config.yml b/tests/DependencyInjection/Fixtures/yml/release_option_from_config.yml new file mode 100644 index 00000000..fa629ca0 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/release_option_from_config.yml @@ -0,0 +1,3 @@ +sentry: + options: + release: 1.0.x-dev diff --git a/tests/DependencyInjection/Fixtures/yml/release_option_from_env_var.yml b/tests/DependencyInjection/Fixtures/yml/release_option_from_env_var.yml new file mode 100644 index 00000000..dd62ce81 --- /dev/null +++ b/tests/DependencyInjection/Fixtures/yml/release_option_from_env_var.yml @@ -0,0 +1,6 @@ +parameters: + env(APP_RELEASE): 1.0.x-dev + +sentry: + options: + release: '%env(APP_RELEASE)%' diff --git a/tests/DependencyInjection/SentryExtensionTest.php b/tests/DependencyInjection/SentryExtensionTest.php index 30c73ccf..db949243 100644 --- a/tests/DependencyInjection/SentryExtensionTest.php +++ b/tests/DependencyInjection/SentryExtensionTest.php @@ -5,14 +5,16 @@ namespace Sentry\SentryBundle\Tests\DependencyInjection; use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; +use Jean85\PrettyVersions; use PHPUnit\Framework\TestCase; use Psr\Log\NullLogger; use Sentry\ClientInterface; -use Sentry\Integration\IgnoreErrorsIntegration; +use Sentry\Logger\DebugStdOutLogger; use Sentry\Options; use Sentry\SentryBundle\DependencyInjection\SentryExtension; use Sentry\SentryBundle\EventListener\ConsoleListener; use Sentry\SentryBundle\EventListener\ErrorListener; +use Sentry\SentryBundle\EventListener\LoginListener; use Sentry\SentryBundle\EventListener\MessengerListener; use Sentry\SentryBundle\EventListener\RequestListener; use Sentry\SentryBundle\EventListener\SubRequestListener; @@ -25,17 +27,17 @@ use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverMiddleware; use Sentry\SentryBundle\Tracing\Twig\TwigTracingExtension; use Sentry\Serializer\RepresentationSerializer; -use Sentry\Serializer\Serializer; -use Sentry\Transport\TransportFactoryInterface; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Console\ConsoleEvents; -use Symfony\Component\Debug\Exception\FatalErrorException; +use Symfony\Component\DependencyInjection\Compiler\ResolveParameterPlaceHoldersPass; +use Symfony\Component\DependencyInjection\Compiler\ResolveTaggedIteratorArgumentPass; +use Symfony\Component\DependencyInjection\Compiler\ValidateEnvPlaceholdersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; 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\HttpClient\TraceableHttpClient; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; @@ -198,25 +200,38 @@ public function testClientIsCreatedFromOptions(): void $container = $this->createContainerFromFixture('full'); $optionsDefinition = $container->getDefinition('sentry.client.options'); $expectedOptions = [ + 'dsn' => 'https://examplePublicKey@o0.ingest.sentry.io/0', 'integrations' => new Reference(IntegrationConfigurator::class), 'default_integrations' => false, - 'send_attempts' => 1, 'prefixes' => [$container->getParameter('kernel.project_dir')], 'sample_rate' => 1, + 'enable_tracing' => true, 'traces_sample_rate' => 1, - 'profiles_sample_rate' => 1, 'traces_sampler' => new Reference('App\\Sentry\\Tracing\\TracesSampler'), - 'trace_propagation_targets' => ['website.invalid'], + 'profiles_sample_rate' => 1, 'attach_stacktrace' => true, + 'attach_metric_code_locations' => true, 'context_lines' => 0, - 'enable_compression' => true, 'environment' => 'development', - 'logger' => 'php', + 'logger' => new Reference(DebugStdOutLogger::class), + 'spotlight' => true, + 'spotlight_url' => 'http://localhost:8969', 'release' => '4.0.x-dev', 'server_name' => 'localhost', + 'ignore_exceptions' => [ + 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException', + ], + 'ignore_transactions' => [ + 'GET tracing_ignored_transaction', + ], 'before_send' => new Reference('App\\Sentry\\BeforeSendCallback'), 'before_send_transaction' => new Reference('App\\Sentry\\BeforeSendTransactionCallback'), - 'tags' => ['context' => 'development'], + 'before_send_check_in' => new Reference('App\\Sentry\\BeforeSendCheckInCallback'), + 'before_send_metrics' => new Reference('App\\Sentry\\BeforeSendMetricsCallback'), + 'trace_propagation_targets' => ['website.invalid'], + 'tags' => [ + 'context' => 'development', + ], 'error_types' => \E_ALL, 'max_breadcrumbs' => 1, 'before_breadcrumb' => new Reference('App\\Sentry\\BeforeBreadcrumbCallback'), @@ -224,21 +239,19 @@ public function testClientIsCreatedFromOptions(): void 'in_app_include' => [$container->getParameter('kernel.project_dir')], 'send_default_pii' => true, 'max_value_length' => 255, + 'transport' => new Reference('App\\Sentry\\Transport'), + 'http_client' => new Reference('App\\Sentry\\HttpClient'), 'http_proxy' => 'proxy.example.com:8080', + 'http_proxy_authentication' => 'user:password', 'http_timeout' => 10, 'http_connect_timeout' => 15, + 'http_ssl_verify_peer' => true, + 'http_compression' => true, 'capture_silenced_errors' => true, 'max_request_body_size' => 'none', 'class_serializers' => [ 'App\\FooClass' => new Reference('App\\Sentry\\Serializer\\FooClassSerializer'), ], - 'dsn' => 'https://examplePublicKey@o0.ingest.sentry.io/0', - 'ignore_exceptions' => [ - 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException', - ], - 'ignore_transactions' => [ - 'GET tracing_ignored_transaction', - ], ]; $this->assertSame(Options::class, $optionsDefinition->getClass()); @@ -246,14 +259,6 @@ public function testClientIsCreatedFromOptions(): void $integrationConfiguratorDefinition = $container->getDefinition(IntegrationConfigurator::class); $expectedIntegrations = [ - new Definition(IgnoreErrorsIntegration::class, [ - [ - 'ignore_exceptions' => [ - FatalError::class, - FatalErrorException::class, - ], - ], - ]), new Reference('App\\Sentry\\Integration\\FooIntegration'), ]; @@ -269,32 +274,15 @@ public function testClientIsCreatedFromOptions(): void $methodCalls = $factory[0]->getMethodCalls(); - $this->assertCount(6, $methodCalls); + $this->assertCount(4, $methodCalls); $this->assertDefinitionMethodCallAt($methodCalls[0], 'setSdkIdentifier', [SentryBundle::SDK_IDENTIFIER]); $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')]); - - $this->assertSame('setSerializer', $methodCalls[3][0]); - $this->assertInstanceOf(Definition::class, $methodCalls[3][1][0]); - $this->assertSame(Serializer::class, $methodCalls[3][1][0]->getClass()); - $this->assertEquals($methodCalls[3][1][0]->getArgument(0), new Reference('sentry.client.options')); - - $this->assertSame('setRepresentationSerializer', $methodCalls[4][0]); - $this->assertInstanceOf(Definition::class, $methodCalls[4][1][0]); - $this->assertSame(RepresentationSerializer::class, $methodCalls[4][1][0]->getClass()); - $this->assertEquals($methodCalls[4][1][0]->getArgument(0), new Reference('sentry.client.options')); - } - - public function testLoggerIsPassedToTransportFactory(): void - { - $container = $this->createContainerFromFixture('full'); - - $transportFactoryDefinition = $container->findDefinition(TransportFactoryInterface::class); - $logger = $transportFactoryDefinition->getArgument('$logger'); + $this->assertDefinitionMethodCallAt($methodCalls[3], 'setLogger', [new Reference('app.logger')]); - $this->assertInstanceOf(Reference::class, $logger); - $this->assertSame('app.logger', $logger->__toString()); + $this->assertSame('setRepresentationSerializer', $methodCalls[2][0]); + $this->assertInstanceOf(Definition::class, $methodCalls[2][1][0]); + $this->assertSame(RepresentationSerializer::class, $methodCalls[2][1][0]->getClass()); + $this->assertEquals($methodCalls[2][1][0]->getArgument(0), new Reference('sentry.client.options')); } public function testErrorTypesOptionIsParsedFromStringToIntegerValue(): void @@ -302,26 +290,8 @@ public function testErrorTypesOptionIsParsedFromStringToIntegerValue(): void $container = $this->createContainerFromFixture('error_types'); $optionsDefinition = $container->getDefinition('sentry.client.options'); - $this->assertSame(\E_ALL & ~(\E_NOTICE | \E_STRICT | \E_DEPRECATED), $optionsDefinition->getArgument(0)['error_types']); - } - - public function testIgnoreErrorsIntegrationIsNotAddedTwiceIfAlreadyConfigured(): void - { - $container = $this->createContainerFromFixture('ignore_errors_integration_overridden'); - $integrations = $container->getDefinition(IntegrationConfigurator::class)->getArgument(0); - $ignoreErrorsIntegrationsCount = 0; - - foreach ($integrations as $integration) { - if ($integration instanceof Reference && IgnoreErrorsIntegration::class === (string) $integration) { - ++$ignoreErrorsIntegrationsCount; - } - - if ($integration instanceof Definition && IgnoreErrorsIntegration::class === $integration->getClass()) { - ++$ignoreErrorsIntegrationsCount; - } - } - - $this->assertSame(1, $ignoreErrorsIntegrationsCount); + // 2048 is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices + $this->assertSame(\E_ALL & ~(\E_NOTICE | 2048 | \E_DEPRECATED), $optionsDefinition->getArgument(0)['error_types']); } /** @@ -369,6 +339,7 @@ public function testInstrumentationIsDisabledWhenTracingIsDisabled(): void $this->assertFalse($container->hasDefinition(TracingDriverMiddleware::class)); $this->assertFalse($container->hasDefinition(ConnectionConfigurator::class)); $this->assertFalse($container->hasDefinition(TwigTracingExtension::class)); + $this->assertFalse($container->hasDefinition(TraceableHttpClient::class)); $this->assertFalse($container->getParameter('sentry.tracing.enabled')); $this->assertEmpty($container->getParameter('sentry.tracing.dbal.connections')); } @@ -445,7 +416,48 @@ public function testLoggerOptionFallbackToNullLoggerIfNotSet(): void $methodCalls = $factory[0]->getMethodCalls(); - $this->assertDefinitionMethodCallAt($methodCalls[5], 'setLogger', [new Reference(NullLogger::class, ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)]); + $this->assertDefinitionMethodCallAt($methodCalls[3], 'setLogger', [new Reference(NullLogger::class, ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)]); + } + + public function testLoginListener(): void + { + $container = $this->createContainerFromFixture('full'); + $this->assertTrue($container->hasDefinition(LoginListener::class)); + } + + /** + * @dataProvider releaseOptionDataProvider + */ + public function testReleaseOption(string $fixtureFile, string $expectedRelease): void + { + $container = $this->createContainerFromFixture($fixtureFile); + $optionsDefinition = $container->getDefinition('sentry.client.options'); + + $this->assertSame(Options::class, $optionsDefinition->getClass()); + $this->assertSame($expectedRelease, $container->resolveEnvPlaceholders($optionsDefinition->getArgument(0)['release'], true)); + } + + public function releaseOptionDataProvider(): \Generator + { + yield 'If the release option is set to a concrete value, then no fallback occurs' => [ + 'release_option_from_config', + '1.0.x-dev', + ]; + + yield 'If the release option is set and references an environment variable, then no fallback occurs' => [ + 'release_option_from_env_var', + '1.0.x-dev', + ]; + + yield 'If the release option is unset and the SENTRY_RELEASE environment variable is set, then the latter is used as fallback' => [ + 'release_option_fallback_to_env_var', + '1.0.x-dev', + ]; + + yield 'If both the release option and the SENTRY_RELEASE environment variable are unset, then the root package version is used as fallback' => [ + 'release_option_fallback_to_composer_version', + PrettyVersions::getRootPackageVersion()->getPrettyVersion(), + ]; } private function createContainerFromFixture(string $fixtureFile): ContainerBuilder @@ -454,14 +466,19 @@ private function createContainerFromFixture(string $fixtureFile): ContainerBuild 'kernel.cache_dir' => __DIR__, 'kernel.build_dir' => __DIR__, 'kernel.project_dir' => __DIR__, + 'kernel.environment' => 'dev', 'doctrine.default_connection' => 'default', 'doctrine.connections' => ['default'], ])); $container->registerExtension(new SentryExtension()); - $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); $container->getCompilerPassConfig()->setAfterRemovingPasses([]); + $container->getCompilerPassConfig()->setOptimizationPasses([ + new ValidateEnvPlaceholdersPass(), + new ResolveParameterPlaceHoldersPass(), + new ResolveTaggedIteratorArgumentPass(), + ]); $this->loadFixture($container, $fixtureFile); diff --git a/tests/DoctrineTestCase.php b/tests/DoctrineTestCase.php index 6d4c7626..350d1ddf 100644 --- a/tests/DoctrineTestCase.php +++ b/tests/DoctrineTestCase.php @@ -7,6 +7,7 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\ResultStatement; +use Doctrine\DBAL\VersionAwarePlatformDriver; use PHPUnit\Framework\TestCase; abstract class DoctrineTestCase extends TestCase @@ -25,7 +26,16 @@ protected static function isDoctrineDBALVersion2Installed(): bool protected static function isDoctrineDBALVersion3Installed(): bool { return self::isDoctrineDBALInstalled() - && !self::isDoctrineDBALVersion2Installed(); + && !self::isDoctrineDBALVersion2Installed() + && interface_exists(VersionAwarePlatformDriver::class); + } + + protected static function isDoctrineDBALVersion4Installed(): bool + { + return self::isDoctrineDBALInstalled() + && !self::isDoctrineDBALVersion2Installed() + && !self::isDoctrineDBALVersion3Installed() + && !interface_exists(VersionAwarePlatformDriver::class); } protected static function isDoctrineBundlePackageInstalled(): bool diff --git a/tests/End2End/App/Controller/TracingController.php b/tests/End2End/App/Controller/TracingController.php index 37579ae4..fa86876a 100644 --- a/tests/End2End/App/Controller/TracingController.php +++ b/tests/End2End/App/Controller/TracingController.php @@ -21,7 +21,7 @@ class TracingController */ private $connection; - public function __construct(HubInterface $hub, Connection $connection = null) + public function __construct(HubInterface $hub, ?Connection $connection = null) { $this->hub = $hub; $this->connection = $connection; diff --git a/tests/End2End/App/Kernel.php b/tests/End2End/App/Kernel.php index 47ab8beb..c3467c61 100644 --- a/tests/End2End/App/Kernel.php +++ b/tests/End2End/App/Kernel.php @@ -44,10 +44,21 @@ public function registerContainerConfiguration(LoaderInterface $loader): void $loader->load(__DIR__ . '/deprecations_for_5.yml'); } + if (self::VERSION_ID >= 50400 && self::VERSION_ID <= 60000) { + // Check if class for Messenger is present (component symfony/messenger is not mandatory) + if (interface_exists(MessageBusInterface::class)) { + $loader->load(__DIR__ . '/deprecations_for_54.yml'); + } + } + if (self::VERSION_ID >= 60000) { $loader->load(__DIR__ . '/deprecations_for_6.yml'); } + if (self::VERSION_ID >= 60400) { + $loader->load(__DIR__ . '/deprecations_for_64.yml'); + } + if (interface_exists(MessageBusInterface::class) && self::VERSION_ID >= 40300) { $loader->load(__DIR__ . '/messenger.yml'); } diff --git a/tests/End2End/App/Messenger/FooMessageHandler.php b/tests/End2End/App/Messenger/FooMessageHandler.php index a2f47549..28518d96 100644 --- a/tests/End2End/App/Messenger/FooMessageHandler.php +++ b/tests/End2End/App/Messenger/FooMessageHandler.php @@ -5,14 +5,13 @@ namespace Sentry\SentryBundle\Tests\End2End\App\Messenger; use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface; -use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -class FooMessageHandler implements MessageHandlerInterface +class FooMessageHandler { public function __invoke(FooMessage $message): void { if (!$message->shouldRetry()) { - throw new class() extends \Exception implements UnrecoverableExceptionInterface { }; + throw new class extends \Exception implements UnrecoverableExceptionInterface { }; } throw new \Exception('This is an intentional failure while handling a message of class ' . \get_class($message)); diff --git a/tests/End2End/App/config.yml b/tests/End2End/App/config.yml index 50aebfec..87aa6cf3 100644 --- a/tests/End2End/App/config.yml +++ b/tests/End2End/App/config.yml @@ -6,23 +6,27 @@ sentry: capture_silenced_errors: false error_types: E_ALL & ~E_USER_DEPRECATED traces_sample_rate: 0 - ignore_exceptions: 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException' + ignore_exceptions: + - 'Symfony\Component\HttpKernel\Exception\BadRequestHttpException' + - 'Symfony\Component\ErrorHandler\Error\FatalError' + - 'Symfony\Component\Debug\Exception\FatalErrorException' ignore_transactions: 'GET tracing_ignored_transaction' + transport: 'Sentry\SentryBundle\Tests\End2End\StubTransport' framework: router: { resource: "%routing_config_dir%/routing.yml" } secret: secret test: ~ + annotations: false + php_errors: + log: true services: test.hub: alias: Sentry\State\HubInterface public: true - Sentry\SentryBundle\Tests\End2End\StubTransportFactory: ~ - - Sentry\Transport\TransportFactoryInterface: - alias: Sentry\SentryBundle\Tests\End2End\StubTransportFactory + Sentry\SentryBundle\Tests\End2End\StubTransport: ~ Sentry\SentryBundle\Tests\End2End\App\Controller\MainController: autowire: true diff --git a/tests/End2End/App/deprecations_for_54.yml b/tests/End2End/App/deprecations_for_54.yml new file mode 100644 index 00000000..abd0417d --- /dev/null +++ b/tests/End2End/App/deprecations_for_54.yml @@ -0,0 +1,3 @@ +framework: + messenger: + reset_on_message: true diff --git a/tests/End2End/App/deprecations_for_64.yml b/tests/End2End/App/deprecations_for_64.yml new file mode 100644 index 00000000..54dd667e --- /dev/null +++ b/tests/End2End/App/deprecations_for_64.yml @@ -0,0 +1,2 @@ +framework: + handle_all_throwables: true diff --git a/tests/End2End/App/messenger.yml b/tests/End2End/App/messenger.yml index 5430583e..c729467d 100644 --- a/tests/End2End/App/messenger.yml +++ b/tests/End2End/App/messenger.yml @@ -7,6 +7,8 @@ services: Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessageHandler: class: \Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessageHandler + tags: + - { name: messenger.message_handler } Sentry\SentryBundle\Tests\End2End\App\Controller\MessengerController: autowire: true diff --git a/tests/End2End/End2EndTest.php b/tests/End2End/End2EndTest.php index 3ec26438..19ddfe06 100644 --- a/tests/End2End/End2EndTest.php +++ b/tests/End2End/End2EndTest.php @@ -198,8 +198,7 @@ public function testNotice(): void public function testCommand(): void { - self::bootKernel(); - $application = new Application(self::$kernel); + $application = new Application(self::bootKernel()); try { $application->doRun(new ArgvInput(['bin/console', 'main-command', '--option1', '--option2=foo', 'bar']), new NullOutput()); @@ -208,10 +207,10 @@ public function testCommand(): void } $this->assertEventCount(1); - $this->assertCount(1, StubTransportFactory::$events); + $this->assertCount(1, StubTransport::$events); $this->assertSame( ['Full command' => 'main-command --option1 --option2=foo bar'], - StubTransportFactory::$events[0]->getExtra() + StubTransport::$events[0]->getExtra() ); } @@ -282,7 +281,7 @@ private function assertEventCount(int $expectedCount): void { $events = file_get_contents(self::SENT_EVENTS_LOG); $this->assertNotFalse($events, 'Cannot read sent events log'); - $listOfEvents = array_filter(explode(StubTransportFactory::SEPARATOR, trim($events))); + $listOfEvents = array_filter(explode(StubTransport::SEPARATOR, trim($events))); $this->assertCount($expectedCount, $listOfEvents, 'Wrong number of events sent: ' . \PHP_EOL . $events); } diff --git a/tests/End2End/StubTransport.php b/tests/End2End/StubTransport.php new file mode 100644 index 00000000..3f3b7644 --- /dev/null +++ b/tests/End2End/StubTransport.php @@ -0,0 +1,52 @@ +getMessage()) { + $message = $event->getMessage(); + } elseif ($event->getExceptions()) { + $message = $event->getExceptions()[0]->getValue(); + } elseif ($event->getTransaction()) { + $message = 'TRACING: ' . $event->getTransaction(); + foreach ($event->getSpans() as $i => $span) { + $message .= \PHP_EOL . $i . ') ' . $span->getDescription(); + } + } else { + $message = 'NO MESSAGE NOR EXCEPTIONS'; + } + + file_put_contents( + End2EndTest::SENT_EVENTS_LOG, + $event->getId() . ': ' . $message . \PHP_EOL . self::SEPARATOR . \PHP_EOL, + \FILE_APPEND + ); + + return new Result(ResultStatus::success(), $event); + } + + public function close(?int $timeout = null): Result + { + return new Result(ResultStatus::success()); + } +} diff --git a/tests/End2End/StubTransportFactory.php b/tests/End2End/StubTransportFactory.php deleted file mode 100644 index 061f068b..00000000 --- a/tests/End2End/StubTransportFactory.php +++ /dev/null @@ -1,61 +0,0 @@ -getMessage()) { - $message = $event->getMessage(); - } elseif ($event->getExceptions()) { - $message = $event->getExceptions()[0]->getValue(); - } elseif ($event->getTransaction()) { - $message = 'TRACING: ' . $event->getTransaction(); - foreach ($event->getSpans() as $i => $span) { - $message .= \PHP_EOL . $i . ') ' . $span->getDescription(); - } - } else { - $message = 'NO MESSAGE NOR EXCEPTIONS'; - } - - file_put_contents( - End2EndTest::SENT_EVENTS_LOG, - $event->getId() . ': ' . $message . \PHP_EOL . StubTransportFactory::SEPARATOR . \PHP_EOL, - \FILE_APPEND - ); - - return new FulfilledPromise(new Response(ResponseStatus::success(), $event)); - } - - public function close(?int $timeout = null): PromiseInterface - { - return new FulfilledPromise(true); - } - }; - } -} diff --git a/tests/End2End/TracingEnd2EndTest.php b/tests/End2End/TracingEnd2EndTest.php index b3204d0c..0a764b32 100644 --- a/tests/End2End/TracingEnd2EndTest.php +++ b/tests/End2End/TracingEnd2EndTest.php @@ -87,7 +87,7 @@ private function assertTracingEventCount(int $expectedCount): void { $events = file_get_contents(self::SENT_EVENTS_LOG); $this->assertNotFalse($events, 'Cannot read sent events log'); - $listOfTracingEvents = array_filter(explode(StubTransportFactory::SEPARATOR, trim($events)), static function (string $elem) { + $listOfTracingEvents = array_filter(explode(StubTransport::SEPARATOR, trim($events)), static function (string $elem) { return str_contains('TRACING', $elem); }); diff --git a/tests/ErrorTypesParserTest.php b/tests/ErrorTypesParserTest.php index e30d6c78..9e3cd532 100644 --- a/tests/ErrorTypesParserTest.php +++ b/tests/ErrorTypesParserTest.php @@ -71,5 +71,8 @@ public function parseThrowsExceptionIfArgumentContainsInvalidCharactersDataProvi yield ['(']; yield [')']; yield ['()']; + // Non scalar values (probably misstypes, but still valid PHP code) + yield ['[8, 8192]']; + yield [\stdClass::class]; } } diff --git a/tests/EventListener/ConsoleCommandListenerTest.php b/tests/EventListener/ConsoleCommandListenerTest.php deleted file mode 100644 index e217dca4..00000000 --- a/tests/EventListener/ConsoleCommandListenerTest.php +++ /dev/null @@ -1,23 +0,0 @@ -createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST, + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST, new \Exception() ), ]; diff --git a/tests/EventListener/Fixtures/UserWithIdentifierStub.php b/tests/EventListener/Fixtures/UserWithIdentifierStub.php index 9b82b8ea..97eccde9 100644 --- a/tests/EventListener/Fixtures/UserWithIdentifierStub.php +++ b/tests/EventListener/Fixtures/UserWithIdentifierStub.php @@ -9,20 +9,29 @@ final class UserWithIdentifierStub implements UserInterface { /** - * @var string + * @var non-empty-string */ private $username; + /** + * @param non-empty-string $username + */ public function __construct(string $username = 'foo_user') { $this->username = $username; } + /** + * @return non-empty-string + */ public function getUserIdentifier(): string { return $this->getUsername(); } + /** + * @return non-empty-string + */ public function getUsername(): string { return $this->username; diff --git a/tests/EventListener/LoginListenerTest.php b/tests/EventListener/LoginListenerTest.php index 44c1838b..da037bef 100644 --- a/tests/EventListener/LoginListenerTest.php +++ b/tests/EventListener/LoginListenerTest.php @@ -88,7 +88,7 @@ public function testHandleKernelRequestEvent(TokenInterface $token, ?UserDataBag $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST )); $event = $scope->applyToEvent(Event::createEvent()); @@ -183,17 +183,33 @@ public function testHandleAuthenticationSuccessEvent(TokenInterface $token, ?Use public function authenticationTokenDataProvider(): \Generator { - yield 'If the username is already set on the User context, then it is not overridden' => [ - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - new UserDataBag('bar_user'), - new UserDataBag('bar_user'), - ]; + if (version_compare(Kernel::VERSION, '5.4', '<')) { + yield 'If the username is already set on the User context, then it is not overridden' => [ + new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub()), + new UserDataBag('bar_user'), + new UserDataBag('bar_user'), + ]; + } else { + yield 'If the username is already set on the User context, then it is not overridden' => [ + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new UserDataBag('bar_user'), + new UserDataBag('bar_user'), + ]; + } - yield 'If the username is not set on the User context, then it is retrieved from the token' => [ - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - null, - new UserDataBag('foo_user'), - ]; + if (version_compare(Kernel::VERSION, '5.4', '<')) { + yield 'If the username is not set on the User context, then it is retrieved from the token' => [ + new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub()), + null, + new UserDataBag('foo_user'), + ]; + } else { + yield 'If the username is not set on the User context, then it is retrieved from the token' => [ + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + null, + new UserDataBag('foo_user'), + ]; + } yield 'If the user is being impersonated, then the username of the impersonator is set on the User context' => [ (static function (): SwitchUserToken { @@ -203,7 +219,8 @@ public function authenticationTokenDataProvider(): \Generator null, 'foo_provider', ['ROLE_USER'], - new AuthenticatedTokenStub(new UserWithIdentifierStub('bar_user')) + // @phpstan-ignore-next-line + new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub('bar_user')) ); } @@ -228,28 +245,69 @@ public function authenticationTokenForSymfonyVersionLowerThan54DataProvider(): \ return; } - yield 'If the user is a string, then the value is used as-is' => [ - new AuthenticatedTokenStub('foo_user'), - null, - new UserDataBag('foo_user'), - ]; + if (version_compare(Kernel::VERSION, '5.0', '<')) { + yield 'If the user is a string, then the value is used as-is' => [ + new LegacyAuthenticatedTokenStub('foo_user'), + null, + new UserDataBag('foo_user'), + ]; + } else { + yield 'If the user is a string, then the value is used as-is' => [ + new AuthenticatedTokenStub('foo_user'), + null, + new UserDataBag('foo_user'), + ]; + } - yield 'If the user is an instance of the UserInterface interface but the getUserIdentifier() method does not exist, then the getUsername() method is invoked' => [ - new AuthenticatedTokenStub(new UserWithoutIdentifierStub()), - null, - new UserDataBag('foo_user'), - ]; + if (version_compare(Kernel::VERSION, '5.0', '<')) { + yield 'If the user is an instance of the UserInterface interface but the getUserIdentifier() method does not exist, then the getUsername() method is invoked' => [ + new LegacyAuthenticatedTokenStub(new UserWithoutIdentifierStub()), + null, + new UserDataBag('foo_user'), + ]; + } else { + yield 'If the user is an instance of the UserInterface interface but the getUserIdentifier() method does not exist, then the getUsername() method is invoked' => [ + new AuthenticatedTokenStub(new UserWithoutIdentifierStub()), + null, + new UserDataBag('foo_user'), + ]; + } - yield 'If the user is an object implementing the Stringable interface, then the __toString() method is invoked' => [ - new AuthenticatedTokenStub(new class() implements \Stringable { - public function __toString(): string - { - return 'foo_user'; - } - }), - null, - new UserDataBag('foo_user'), - ]; + if (version_compare(Kernel::VERSION, '5.0', '<')) { + yield 'If the user is an object implementing the Stringable interface, then the __toString() method is invoked' => [ + new LegacyAuthenticatedTokenStub(new class implements \Stringable { + public function __toString(): string + { + return 'foo_user'; + } + }), + null, + new UserDataBag('foo_user'), + ]; + } else { + yield 'If the user is an object implementing the Stringable interface, then the __toString() method is invoked' => [ + new AuthenticatedTokenStub(new class implements \Stringable { + public function __toString(): string + { + return 'foo_user'; + } + }), + null, + new UserDataBag('foo_user'), + ]; + } + } + + public function testHandleKernelRequestEventDoesNothingIfRequestIsForStatelessRoute(): void + { + $this->tokenStorage->expects($this->never()) + ->method('getToken'); + + $this->listener->handleKernelRequestEvent(new RequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request([], [], ['_stateless' => true]), + HttpKernelInterface::SUB_REQUEST + )); } public function testHandleKernelRequestEventDoesNothingIfRequestIsNotMain(): void @@ -273,7 +331,7 @@ public function testHandleKernelRequestEventDoesNothingIfTokenIsNotSet(): void $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST )); } @@ -312,14 +370,25 @@ public function testHandleLoginSuccessEventDoesNothingIfClientIsNotSetOnHub(): v $this->hub->expects($this->never()) ->method('configureScope'); - $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( - $this->createMock(AuthenticatorInterface::class), - new SelfValidatingPassport(new UserBadge('foo_passport_user')), - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - new Request(), - null, - 'main' - )); + if (version_compare(Kernel::VERSION, '5.4', '<')) { + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub()), + new Request(), + null, + 'main' + )); + } else { + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new Request(), + null, + 'main' + )); + } } public function testHandleLoginSuccessEventDoesNothingIfSendingDefaultPiiIsDisabled(): void @@ -340,14 +409,25 @@ public function testHandleLoginSuccessEventDoesNothingIfSendingDefaultPiiIsDisab $this->hub->expects($this->never()) ->method('configureScope'); - $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( - $this->createMock(AuthenticatorInterface::class), - new SelfValidatingPassport(new UserBadge('foo_passport_user')), - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - new Request(), - null, - 'main' - )); + if (version_compare(Kernel::VERSION, '5.4', '<')) { + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub()), + new Request(), + null, + 'main' + )); + } else { + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new Request(), + null, + 'main' + )); + } } public function testHandleAuthenticationSuccessEventDoesNothingIfTokenIsNotAuthenticated(): void @@ -378,7 +458,11 @@ public function testHandleAuthenticationSuccessEventDoesNothingIfClientIsNotSetO $this->hub->expects($this->never()) ->method('configureScope'); - $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new AuthenticatedTokenStub(new UserWithIdentifierStub()))); + if (version_compare(Kernel::VERSION, '5.4', '<')) { + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub()))); + } else { + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new AuthenticatedTokenStub(new UserWithIdentifierStub()))); + } } public function testHandleAuthenticationSuccessEventDoesNothingIfSendingDefaultPiiIsDisabled(): void @@ -399,7 +483,11 @@ public function testHandleAuthenticationSuccessEventDoesNothingIfSendingDefaultP $this->hub->expects($this->never()) ->method('configureScope'); - $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new AuthenticatedTokenStub(new UserWithIdentifierStub()))); + if (version_compare(Kernel::VERSION, '5.4', '<')) { + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new LegacyAuthenticatedTokenStub(new UserWithIdentifierStub()))); + } else { + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new AuthenticatedTokenStub(new UserWithIdentifierStub()))); + } } } @@ -416,8 +504,47 @@ public function getCredentials(): ?string } } +class LegacyAuthenticatedTokenStub extends AbstractToken +{ + /** + * @var bool + * + * @phpstan-ignore-next-line + */ + private $authenticated = false; + + /** + * @param UserInterface|\Stringable|string|null $user + */ + public function __construct($user) + { + parent::__construct(); + + if (null !== $user) { + // @phpstan-ignore-next-line + $this->setUser($user); + } + + if (version_compare(Kernel::VERSION, '5.4', '<') && method_exists($this, 'setAuthenticated')) { + $this->setAuthenticated(true); + } else { + $this->authenticated = true; + } + } + + public function getCredentials(): ?string + { + return null; + } +} + final class AuthenticatedTokenStub extends AbstractToken { + /** + * @var bool + */ + private $authenticated = false; + /** * @param UserInterface|\Stringable|string|null $user */ @@ -426,14 +553,22 @@ public function __construct($user) parent::__construct(); if (null !== $user) { + // @phpstan-ignore-next-line $this->setUser($user); } - if (method_exists($this, 'setAuthenticated')) { + if (version_compare(Kernel::VERSION, '5.4', '<') && method_exists($this, 'setAuthenticated')) { $this->setAuthenticated(true); + } else { + $this->authenticated = true; } } + public function isAuthenticated(): bool + { + return $this->authenticated; + } + public function getCredentials(): ?string { return null; diff --git a/tests/EventListener/MessengerListenerTest.php b/tests/EventListener/MessengerListenerTest.php index 0404edad..8e850aaf 100644 --- a/tests/EventListener/MessengerListenerTest.php +++ b/tests/EventListener/MessengerListenerTest.php @@ -15,6 +15,7 @@ use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; +use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException; use Symfony\Component\Messenger\Exception\HandlerFailedException; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\BusNameStamp; @@ -113,6 +114,16 @@ public function handleWorkerMessageFailedEventDataProvider(): \Generator false, ]; + yield 'envelope.throwable INSTANCEOF DelayedMessageHandlingException' => [ + $exceptions, + $this->getMessageFailedEvent($envelope, 'receiver', new DelayedMessageHandlingException($exceptions, $envelope), false), + [ + 'messenger.receiver_name' => 'receiver', + 'messenger.message_class' => \get_class($envelope->getMessage()), + ], + false, + ]; + yield 'envelope.throwable INSTANCEOF HandlerFailedException - RETRYING' => [ $exceptions, $this->getMessageFailedEvent($envelope, 'receiver', new HandlerFailedException($envelope, $exceptions), true), diff --git a/tests/EventListener/RequestListenerTest.php b/tests/EventListener/RequestListenerTest.php index 440db6e7..5532df9d 100644 --- a/tests/EventListener/RequestListenerTest.php +++ b/tests/EventListener/RequestListenerTest.php @@ -82,7 +82,7 @@ public function handleKernelRequestEventDataProvider(): \Generator new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => false])), new UserDataBag(), @@ -93,7 +93,7 @@ public function handleKernelRequestEventDataProvider(): \Generator new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), new UserDataBag(), @@ -104,7 +104,7 @@ public function handleKernelRequestEventDataProvider(): \Generator new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), new UserDataBag('foo_user'), @@ -115,7 +115,7 @@ public function handleKernelRequestEventDataProvider(): \Generator new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), new UserDataBag('foo_user', null, '::1'), @@ -168,7 +168,7 @@ static function () { static function () { }, new Request(), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST ), [], ]; @@ -179,7 +179,7 @@ static function () { static function () { }, new Request([], [], ['_route' => 'homepage']), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST ), [ 'route' => 'homepage', diff --git a/tests/EventListener/SubRequestListenerTest.php b/tests/EventListener/SubRequestListenerTest.php index c8c0b915..3b5b63a4 100644 --- a/tests/EventListener/SubRequestListenerTest.php +++ b/tests/EventListener/SubRequestListenerTest.php @@ -53,7 +53,7 @@ public function testHandleKernelRequestEvent(RequestEvent $event): void public function handleKernelRequestEventDataProvider(): \Generator { yield [ - new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST), + new RequestEvent($this->createMock(HttpKernelInterface::class), new Request(), \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST), ]; yield [ @@ -80,7 +80,7 @@ public function testHandleKernelFinishRequestEvent($event): void public function handleKernelFinishRequestEventDataProvider(): \Generator { yield [ - new FinishRequestEvent($this->createMock(HttpKernelInterface::class), new Request(), HttpKernelInterface::MASTER_REQUEST), + new FinishRequestEvent($this->createMock(HttpKernelInterface::class), new Request(), \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST), ]; yield [ diff --git a/tests/EventListener/TracingConsoleListenerTest.php b/tests/EventListener/TracingConsoleListenerTest.php index 3987ea04..b619dfe7 100644 --- a/tests/EventListener/TracingConsoleListenerTest.php +++ b/tests/EventListener/TracingConsoleListenerTest.php @@ -44,6 +44,9 @@ public function testHandleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHub $this->hub->expects($this->once()) ->method('startTransaction') ->with($this->callback(function (TransactionContext $context) use ($expectedTransactionContext): bool { + // This value is random when the metadata is constructed, thus we set it to a fixed expected value since we don't care for the value here + $context->getMetadata()->setSampleRand(0.1337); + $this->assertEquals($expectedTransactionContext, $context); return true; @@ -71,7 +74,9 @@ public function handleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHubData $transactionContext = new TransactionContext(); $transactionContext->setOp('console.command'); $transactionContext->setName(''); + $transactionContext->setOrigin('auto.console'); $transactionContext->setSource(TransactionSource::task()); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield [ new Command(), @@ -81,7 +86,9 @@ public function handleConsoleCommandEventStartsTransactionIfNoSpanIsSetOnHubData $transactionContext = new TransactionContext(); $transactionContext->setOp('console.command'); $transactionContext->setName('app:command'); + $transactionContext->setOrigin('auto.console'); $transactionContext->setSource(TransactionSource::task()); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield [ new Command('app:command'), diff --git a/tests/EventListener/TracingRequestListenerTest.php b/tests/EventListener/TracingRequestListenerTest.php index 5dfa1ff4..1c0c3193 100644 --- a/tests/EventListener/TracingRequestListenerTest.php +++ b/tests/EventListener/TracingRequestListenerTest.php @@ -65,6 +65,9 @@ public function testHandleKernelRequestEvent(Options $options, Request $request, $this->hub->expects($this->once()) ->method('startTransaction') ->with($this->callback(function (TransactionContext $context) use ($expectedTransactionContext): bool { + // This value is random when the metadata is constructed, thus we set it to a fixed expected value since we don't care for the value here + $context->getMetadata()->setSampleRand(0.1337); + $this->assertEquals($expectedTransactionContext, $context); return true; @@ -78,7 +81,7 @@ public function testHandleKernelRequestEvent(Options $options, Request $request, $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), $request, - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST )); } @@ -97,16 +100,18 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => '', 'net.host.name' => 'www.example.com', ]); $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.headers.sentry-trace EXISTS' => [ new Options(), @@ -124,6 +129,45 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext, ]; + $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->setOrigin('auto.http.server'); + $transactionContext->setStartTimestamp(1613493597.010275); + $transactionContext->setData([ + 'net.host.port' => '80', + 'http.request.method' => 'GET', + 'http.url' => 'http://www.example.com/', + 'http.flavor' => '1.1', + 'route' => '', + 'net.host.name' => 'www.example.com', + ]); + $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); + $transactionContext->getMetadata()->setSampleRand(0.1337); + + yield 'request.headers.traceparent EXISTS' => [ + new Options(), + Request::create( + 'http://www.example.com', + 'GET', + [], + [], + [], + [ + 'REQUEST_TIME_FLOAT' => 1613493597.010275, + 'HTTP_traceparent' => '00-566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-01', + ] + ), + $transactionContext, + ]; + $samplingContext = DynamicSamplingContext::fromHeader('sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-sample_rate=1'); $samplingContext->freeze(); @@ -134,16 +178,19 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => '', 'net.host.name' => 'www.example.com', ]); $transactionContext->getMetadata()->setDynamicSamplingContext($samplingContext); + $transactionContext->getMetadata()->setParentSamplingRate(1.0); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.headers.sentry-trace and headers.baggage EXISTS' => [ new Options(), @@ -166,15 +213,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => '', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); $request = Request::create('http://www.example.com'); $request->server->remove('REQUEST_TIME_FLOAT'); @@ -189,15 +238,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://127.0.0.1/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://127.0.0.1/', 'http.flavor' => '1.1', 'route' => '', 'net.host.ip' => '127.0.0.1', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.server.HOST IS IPV4' => [ new Options(), @@ -220,15 +271,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET app_homepage'); $transactionContext->setSource(TransactionSource::route()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/path', 'http.flavor' => '1.1', 'route' => 'app_homepage', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.attributes.route IS STRING' => [ new Options(), @@ -244,15 +297,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/path'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/path', 'http.flavor' => '1.1', 'route' => '/path', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.attributes.route IS INSTANCEOF Symfony\Component\Routing\Route' => [ new Options(), @@ -268,15 +323,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => 'App\\Controller::indexAction', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.attributes._controller IS STRING' => [ new Options(), @@ -292,15 +349,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => 'App\\Controller::indexAction', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.attributes._controller IS CALLABLE (1)' => [ new Options(), @@ -310,21 +369,23 @@ public function handleKernelRequestEventDataProvider(): \Generator $request = Request::create('http://www.example.com/'); $request->server->set('REQUEST_TIME_FLOAT', 1613493597.010275); - $request->attributes->set('_controller', [new class() {}, 'indexAction']); + $request->attributes->set('_controller', [new class {}, 'indexAction']); $transactionContext = new TransactionContext(); $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => 'class@anonymous::indexAction', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.attributes._controller IS CALLABLE (2)' => [ new Options(), @@ -340,15 +401,17 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => '', 'net.host.name' => 'www.example.com', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.attributes._controller IS ARRAY and NOT VALID CALLABLE' => [ new Options(), @@ -364,16 +427,18 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://www.example.com/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '80', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'http.flavor' => '1.1', 'route' => '', 'net.host.name' => 'www.example.com', 'net.peer.ip' => '127.0.0.1', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.server.REMOTE_ADDR EXISTS and client.options.send_default_pii = TRUE' => [ new Options(['send_default_pii' => true]), @@ -388,14 +453,16 @@ public function handleKernelRequestEventDataProvider(): \Generator $transactionContext->setName('GET http://:/'); $transactionContext->setSource(TransactionSource::url()); $transactionContext->setOp('http.server'); + $transactionContext->setOrigin('auto.http.server'); $transactionContext->setStartTimestamp(1613493597.010275); - $transactionContext->setTags([ + $transactionContext->setData([ 'net.host.port' => '', - 'http.method' => 'GET', + 'http.request.method' => 'GET', 'http.url' => 'http://:/', 'route' => '', 'net.host.name' => '', ]); + $transactionContext->getMetadata()->setSampleRand(0.1337); yield 'request.server.SERVER_PROTOCOL NOT EXISTS' => [ new Options(), @@ -427,12 +494,11 @@ public function testHandleResponseRequestEvent(): void $this->listener->handleKernelResponseEvent(new ResponseEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST, + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST, new Response() )); $this->assertSame(SpanStatus::ok(), $transaction->getStatus()); - $this->assertSame(['http.status_code' => '200'], $transaction->getTags()); } public function testHandleResponseRequestEventDoesNothingIfNoTransactionIsSetOnHub(): void @@ -444,7 +510,7 @@ public function testHandleResponseRequestEventDoesNothingIfNoTransactionIsSetOnH $this->listener->handleKernelResponseEvent(new ResponseEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST, + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST, new Response() )); } diff --git a/tests/EventListener/TracingSubRequestListenerTest.php b/tests/EventListener/TracingSubRequestListenerTest.php index 9503e2a3..75effa05 100644 --- a/tests/EventListener/TracingSubRequestListenerTest.php +++ b/tests/EventListener/TracingSubRequestListenerTest.php @@ -75,8 +75,8 @@ public function handleKernelRequestEventDataProvider(): \Generator $span = new Span(); $span->setOp('http.server'); $span->setDescription('GET http://www.example.com/path'); - $span->setTags([ - 'http.method' => 'GET', + $span->setData([ + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/path', 'route' => 'App\\Controller::indexAction', ]); @@ -92,8 +92,8 @@ public function handleKernelRequestEventDataProvider(): \Generator $span = new Span(); $span->setOp('http.server'); $span->setDescription('GET http://www.example.com/'); - $span->setTags([ - 'http.method' => 'GET', + $span->setData([ + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'route' => 'App\\Controller::indexAction', ]); @@ -104,13 +104,13 @@ public function handleKernelRequestEventDataProvider(): \Generator ]; $request = Request::create('http://www.example.com/'); - $request->attributes->set('_controller', [new class() {}, 'indexAction']); + $request->attributes->set('_controller', [new class {}, 'indexAction']); $span = new Span(); $span->setOp('http.server'); $span->setDescription('GET http://www.example.com/'); - $span->setTags([ - 'http.method' => 'GET', + $span->setData([ + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'route' => 'class@anonymous::indexAction', ]); @@ -126,8 +126,8 @@ public function handleKernelRequestEventDataProvider(): \Generator $span = new Span(); $span->setOp('http.server'); $span->setDescription('GET http://www.example.com/'); - $span->setTags([ - 'http.method' => 'GET', + $span->setData([ + 'http.request.method' => 'GET', 'http.url' => 'http://www.example.com/', 'route' => '', ]); @@ -146,7 +146,7 @@ public function testHandleKernelRequestEventDoesNothingIfRequestTypeIsMasterRequ $this->listener->handleKernelRequestEvent(new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST )); } @@ -191,7 +191,7 @@ public function testHandleKernelFinishRequestEventDoesNothingIfRequestTypeIsMast $this->listener->handleKernelFinishRequestEvent(new FinishRequestEvent( $this->createMock(HttpKernelInterface::class), new Request(), - HttpKernelInterface::MASTER_REQUEST + \defined(HttpKernelInterface::class . '::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST )); } @@ -224,7 +224,6 @@ public function testHandleResponseRequestEvent(): void )); $this->assertSame(SpanStatus::ok(), $span->getStatus()); - $this->assertSame(['http.status_code' => '200'], $span->getTags()); } public function testHandleResponseRequestEventDoesNothingIfNoTransactionIsSetOnHub(): void diff --git a/tests/Integration/IntegrationConfiguratorTest.php b/tests/Integration/IntegrationConfiguratorTest.php index 44206b5c..4fd59339 100644 --- a/tests/Integration/IntegrationConfiguratorTest.php +++ b/tests/Integration/IntegrationConfiguratorTest.php @@ -50,7 +50,7 @@ public function integrationsDataProvider(): iterable $environmentIntegration = new EnvironmentIntegration(); $modulesIntegration = new ModulesIntegration(); - $userIntegration1 = new class() implements IntegrationInterface { + $userIntegration1 = new class implements IntegrationInterface { public function setupOnce(): void { } diff --git a/tests/Integration/RequestFetcherTest.php b/tests/Integration/RequestFetcherTest.php index 62597ce3..57916b8c 100644 --- a/tests/Integration/RequestFetcherTest.php +++ b/tests/Integration/RequestFetcherTest.php @@ -2,9 +2,8 @@ declare(strict_types=1); -namespace Sentry\SentryBundle\Test\Integration; +namespace Sentry\SentryBundle\Tests\Integration; -use GuzzleHttp\Psr7\ServerRequest; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -37,16 +36,16 @@ protected function setUp(): void $this->requestFetcher = new RequestFetcher($this->requestStack, $this->httpMessageFactory); } - /** - * @dataProvider fetchRequestDataProvider - */ - public function testFetchRequest(?Request $request, ?ServerRequestInterface $expectedRequest): void + public function testFetchRequest(): void { + $request = Request::create('https://www.example.com'); + $expectedRequest = $this->createMock(ServerRequestInterface::class); + $this->requestStack->expects($this->once()) ->method('getCurrentRequest') ->willReturn($request); - $this->httpMessageFactory->expects(null !== $expectedRequest ? $this->once() : $this->never()) + $this->httpMessageFactory->expects($this->once()) ->method('createRequest') ->with($request) ->willReturn($expectedRequest); @@ -54,7 +53,19 @@ public function testFetchRequest(?Request $request, ?ServerRequestInterface $exp $this->assertSame($expectedRequest, $this->requestFetcher->fetchRequest()); } - public function testFetchRequestFailsSilently(): void + public function testFetchRequestReturnsNullIfTheRequestStackIsEmpty(): void + { + $this->requestStack->expects($this->once()) + ->method('getCurrentRequest') + ->willReturn(null); + + $this->httpMessageFactory->expects($this->never()) + ->method('createRequest'); + + $this->assertNull($this->requestFetcher->fetchRequest()); + } + + public function testFetchRequestReturnsNullIfTheRequestFactoryThrowsAnException(): void { $this->requestStack->expects($this->once()) ->method('getCurrentRequest') @@ -66,20 +77,4 @@ public function testFetchRequestFailsSilently(): void $this->assertNull($this->requestFetcher->fetchRequest()); } - - /** - * @return \Generator - */ - public function fetchRequestDataProvider(): \Generator - { - yield [ - null, - null, - ]; - - yield [ - Request::create('http://www.example.com'), - new ServerRequest('GET', 'http://www.example.com'), - ]; - } } diff --git a/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php b/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php index 75583c8b..4b49194f 100644 --- a/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php +++ b/tests/Tracing/Cache/AbstractTraceableCacheAdapterTest.php @@ -159,7 +159,7 @@ public function testGetThrowsExceptionIfDecoratedAdapterDoesNotImplementTheCache $adapter = $this->createCacheAdapter($this->createMock(static::getAdapterClassFqcn())); $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage(sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "Symfony\\Contracts\\Cache\\CacheInterface" interface.', \get_class($adapter))); + $this->expectExceptionMessage(\sprintf('The %s::get() method is not supported because the decorated adapter does not implement the "Symfony\\Contracts\\Cache\\CacheInterface" interface.', \get_class($adapter))); $adapter->get('foo', static function () {}); } @@ -197,7 +197,7 @@ public function testDeleteThrowsExceptionIfDecoratedAdapterDoesNotImplementTheCa $adapter = $this->createCacheAdapter($this->createMock(static::getAdapterClassFqcn())); $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage(sprintf('The %s::delete() method is not supported because the decorated adapter does not implement the "Symfony\\Contracts\\Cache\\CacheInterface" interface.', \get_class($adapter))); + $this->expectExceptionMessage(\sprintf('The %s::delete() method is not supported because the decorated adapter does not implement the "Symfony\\Contracts\\Cache\\CacheInterface" interface.', \get_class($adapter))); $adapter->delete('foo'); } diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV2Test.php b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV2Test.php new file mode 100644 index 00000000..29bd7218 --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV2Test.php @@ -0,0 +1,104 @@ +hub = $this->createMock(HubInterface::class); + $this->databasePlatform = $this->createMock(AbstractPlatform::class); + $this->tracingDriverConnectionFactory = new TracingDriverConnectionFactory($this->hub); + } + + /** + * @dataProvider createDataProvider + * + * @param class-string $databasePlatformFqcn + */ + public function testCreate(string $databasePlatformFqcn, string $expectedDatabasePlatform): void + { + $connection = $this->createMock(Connection::class); + $databasePlatform = $this->createMock($databasePlatformFqcn); + $driverConnection = $this->tracingDriverConnectionFactory->create($connection, $databasePlatform, []); + $expectedDriverConnection = new TracingDriverConnectionForV2V3($this->hub, $connection, $expectedDatabasePlatform, []); + + $this->assertEquals($expectedDriverConnection, $driverConnection); + } + + public static function createDataProvider(): \Generator + { + yield [ + AbstractMySQLPlatform::class, + 'mysql', + ]; + + yield [ + DB2Platform::class, + 'db2', + ]; + + yield [ + OraclePlatform::class, + 'oracle', + ]; + + yield [ + PostgreSQLPlatform::class, + 'postgresql', + ]; + + yield [ + SqlitePlatform::class, + 'sqlite', + ]; + + yield [ + SQLServerPlatform::class, + 'mssql', + ]; + + yield [ + AbstractPlatform::class, + 'other_sql', + ]; + } +} diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryTest.php b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV3Test.php similarity index 89% rename from tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryTest.php rename to tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV3Test.php index d3a74639..900dd288 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryTest.php +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV3Test.php @@ -17,10 +17,11 @@ use Sentry\SentryBundle\Tests\Tracing\Doctrine\DBAL\Fixture\ServerInfoAwareConnectionStub; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnection; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionFactory; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionForV2V3; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingServerInfoAwareDriverConnection; use Sentry\State\HubInterface; -final class TracingDriverConnectionFactoryTest extends DoctrineTestCase +final class TracingDriverConnectionFactoryV3Test extends DoctrineTestCase { /** * @var MockObject&HubInterface @@ -39,8 +40,8 @@ final class TracingDriverConnectionFactoryTest extends DoctrineTestCase public static function setUpBeforeClass(): void { - if (!self::isDoctrineDBALInstalled()) { - self::markTestSkipped('This test requires the "doctrine/dbal" Composer package.'); + if (!self::isDoctrineDBALVersion3Installed()) { + self::markTestSkipped('This test requires the "doctrine/dbal: ^3.3" Composer package.'); } } @@ -61,7 +62,7 @@ public function testCreate(string $databasePlatformFqcn, string $expectedDatabas $connection = $this->createMock(Connection::class); $databasePlatform = $this->createMock($databasePlatformFqcn); $driverConnection = $this->tracingDriverConnectionFactory->create($connection, $databasePlatform, []); - $expectedDriverConnection = new TracingDriverConnection($this->hub, $connection, $expectedDatabasePlatform, []); + $expectedDriverConnection = new TracingDriverConnectionForV2V3($this->hub, $connection, $expectedDatabasePlatform, []); $this->assertEquals($expectedDriverConnection, $driverConnection); } @@ -106,10 +107,6 @@ public static function createDataProvider(): \Generator public function testCreateWithServerInfoAwareConnection(): void { - if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); - } - $connection = $this->createMock(ServerInfoAwareConnectionStub::class); $driverConnection = $this->tracingDriverConnectionFactory->create($connection, $this->databasePlatform, []); $expectedDriverConnection = new TracingServerInfoAwareDriverConnection(new TracingDriverConnection($this->hub, $connection, 'other_sql', [])); diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV4Test.php b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV4Test.php new file mode 100644 index 00000000..e4357611 --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryV4Test.php @@ -0,0 +1,104 @@ +hub = $this->createMock(HubInterface::class); + $this->databasePlatform = $this->createMock(AbstractPlatform::class); + $this->tracingDriverConnectionFactory = new TracingDriverConnectionFactory($this->hub); + } + + /** + * @dataProvider createDataProvider + * + * @param class-string $databasePlatformFqcn + */ + public function testCreate(string $databasePlatformFqcn, string $expectedDatabasePlatform): void + { + $connection = $this->createMock(Connection::class); + $databasePlatform = $this->createMock($databasePlatformFqcn); + $driverConnection = $this->tracingDriverConnectionFactory->create($connection, $databasePlatform, []); + $expectedDriverConnection = new TracingDriverConnectionForV4($this->hub, $connection, $expectedDatabasePlatform, []); + + $this->assertEquals($expectedDriverConnection, $driverConnection); + } + + public static function createDataProvider(): \Generator + { + yield [ + AbstractMySQLPlatform::class, + 'mysql', + ]; + + yield [ + DB2Platform::class, + 'db2', + ]; + + yield [ + OraclePlatform::class, + 'oracle', + ]; + + yield [ + PostgreSQLPlatform::class, + 'postgresql', + ]; + + yield [ + SQLitePlatform::class, + 'sqlite', + ]; + + yield [ + SQLServerPlatform::class, + 'mssql', + ]; + + yield [ + AbstractPlatform::class, + 'other_sql', + ]; + } +} diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3Test.php similarity index 94% rename from tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php rename to tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3Test.php index b8dae823..3ad7ede2 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionTest.php +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3Test.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\MockObject\MockObject; use Sentry\SentryBundle\Tests\DoctrineTestCase; use Sentry\SentryBundle\Tests\Tracing\Doctrine\DBAL\Fixture\NativeDriverConnectionInterfaceStub; -use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnection; +use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionForV4 as TracingDriverConnection; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionInterface; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingStatement; use Sentry\State\HubInterface; @@ -21,7 +21,7 @@ /** * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams */ -final class TracingDriverConnectionTest extends DoctrineTestCase +final class TracingDriverConnectionForV2V3Test extends DoctrineTestCase { /** * @var MockObject&HubInterface @@ -40,7 +40,10 @@ final class TracingDriverConnectionTest extends DoctrineTestCase public static function setUpBeforeClass(): void { - if (!self::isDoctrineBundlePackageInstalled()) { + if ( + !self::isDoctrineDBALVersion2Installed() + || !self::isDoctrineDBALVersion3Installed() + ) { self::markTestSkipped(); } } @@ -240,8 +243,7 @@ public function testBeginTransaction(array $params, array $expectedData): void ->willReturn($transaction); $this->decoratedConnection->expects($this->once()) - ->method('beginTransaction') - ->willReturn(false); + ->method('beginTransaction'); $this->assertFalse($connection->beginTransaction()); $this->assertNotNull($transaction->getSpanRecorder()); @@ -262,8 +264,7 @@ public function testBeginTransactionDoesNothingIfNoSpanIsSetOnHub(): void ->willReturn(null); $this->decoratedConnection->expects($this->once()) - ->method('beginTransaction') - ->willReturn(false); + ->method('beginTransaction'); $this->assertFalse($this->connection->beginTransaction()); } @@ -287,8 +288,7 @@ public function testCommit(array $params, array $expectedData): void ->willReturn($transaction); $this->decoratedConnection->expects($this->once()) - ->method('commit') - ->willReturn(false); + ->method('commit'); $this->assertFalse($connection->commit()); $this->assertNotNull($transaction->getSpanRecorder()); @@ -309,8 +309,7 @@ public function testCommitDoesNothingIfNoSpanIsSetOnHub(): void ->willReturn(null); $this->decoratedConnection->expects($this->once()) - ->method('commit') - ->willReturn(false); + ->method('commit'); $this->assertFalse($this->connection->commit()); } @@ -334,8 +333,7 @@ public function testRollBack(array $params, array $expectedData): void ->willReturn($transaction); $this->decoratedConnection->expects($this->once()) - ->method('rollBack') - ->willReturn(false); + ->method('rollBack'); $this->assertFalse($connection->rollBack()); $this->assertNotNull($transaction->getSpanRecorder()); @@ -356,8 +354,7 @@ public function testRollBackDoesNothingIfNoSpanIsSetOnHub(): void ->willReturn(null); $this->decoratedConnection->expects($this->once()) - ->method('rollBack') - ->willReturn(false); + ->method('rollBack'); $this->assertFalse($this->connection->rollBack()); } @@ -378,7 +375,7 @@ public function testErrorCode(): void public function testErrorCodeThrowsExceptionIfDecoratedConnectionDoesNotImplementMethod(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } $this->expectException(\BadMethodCallException::class); @@ -403,7 +400,7 @@ public function testErrorInfo(): void public function testErrorInfoThrowsExceptionIfDecoratedConnectionDoesNotImplementMethod(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } $this->expectException(\BadMethodCallException::class); @@ -421,7 +418,7 @@ public function testGetWrappedConnection(): void public function testGetNativeConnection(): void { - $nativeConnection = new class() { + $nativeConnection = new class { }; $decoratedConnection = $this->createMock(NativeDriverConnectionInterfaceStub::class); @@ -466,8 +463,8 @@ public function spanDataDataProvider(): \Generator 'db.system' => 'foo_platform', 'db.user' => 'root', 'db.name' => 'INFORMATION_SCHEMA', - 'net.peer.port' => '3306', - 'net.transport' => 'Unix', + 'server.port' => '3306', + 'server.socket.address' => 'Unix', ], ]; @@ -482,8 +479,8 @@ public function spanDataDataProvider(): \Generator 'db.system' => 'foo_platform', 'db.user' => 'root', 'db.name' => 'INFORMATION_SCHEMA', - 'net.peer.port' => '3306', - 'net.transport' => 'inproc', + 'server.port' => '3306', + 'server.socket.address' => 'inproc', ], ]; @@ -493,7 +490,7 @@ public function spanDataDataProvider(): \Generator ], [ 'db.system' => 'foo_platform', - 'net.peer.name' => 'localhost', + 'server.address' => 'localhost', ], ]; @@ -503,7 +500,7 @@ public function spanDataDataProvider(): \Generator ], [ 'db.system' => 'foo_platform', - 'net.peer.ip' => '127.0.0.1', + 'server.address' => '127.0.0.1', ], ]; } diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4Test.php b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4Test.php new file mode 100644 index 00000000..77490bb3 --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4Test.php @@ -0,0 +1,490 @@ +hub = $this->createMock(HubInterface::class); + $this->decoratedConnection = $this->createMock(DriverConnectionInterface::class); + $this->connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', []); + } + + /** + * @dataProvider spanDataDataProvider + * + * @param array $params + * @param array $expectedData + * + * @phpstan-param ConnectionParams $params + */ + public function testPrepare(array $params, array $expectedData): void + { + $sql = 'SELECT 1 + 1'; + $statement = $this->createMock(DriverStatementInterface::class); + $resultStatement = new TracingStatement($this->hub, $statement, $sql, $expectedData); + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); + + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedConnection->expects($this->once()) + ->method('prepare') + ->with($sql) + ->willReturn($statement); + + $this->assertEquals($resultStatement, $connection->prepare($sql)); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingDriverConnection::SPAN_OP_CONN_PREPARE, $spans[1]->getOp()); + $this->assertSame($sql, $spans[1]->getDescription()); + $this->assertSame($expectedData, $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testPrepareDoesNothingIfNoSpanIsSetOnHub(): void + { + $sql = 'SELECT 1 + 1'; + $statement = $this->createMock(DriverStatementInterface::class); + $resultStatement = new TracingStatement($this->hub, $statement, $sql, ['db.system' => 'foo_platform']); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedConnection->expects($this->once()) + ->method('prepare') + ->with($sql) + ->willReturn($statement); + + $this->assertEquals($resultStatement, $this->connection->prepare($sql)); + } + + /** + * @dataProvider spanDataDataProvider + * + * @param array $params + * @param array $expectedData + * + * @phpstan-param ConnectionParams $params + */ + public function testQuery(array $params, array $expectedData): void + { + $result = $this->createMock(DriverResultInterface::class); + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); + $sql = 'SELECT 1 + 1'; + + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedConnection->expects($this->once()) + ->method('query') + ->with($sql) + ->willReturn($result); + + $this->assertSame($result, $connection->query($sql)); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingDriverConnection::SPAN_OP_CONN_QUERY, $spans[1]->getOp()); + $this->assertSame($sql, $spans[1]->getDescription()); + $this->assertSame($expectedData, $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testQueryDoesNothingIfNoSpanIsSetOnHub(): void + { + $result = $this->createMock(DriverResultInterface::class); + $sql = 'SELECT 1 + 1'; + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedConnection->expects($this->once()) + ->method('query') + ->with($sql) + ->willReturn($result); + + $this->assertSame($result, $this->connection->query($sql)); + } + + public function testQuote(): void + { + $this->decoratedConnection->expects($this->once()) + ->method('quote') + ->with('foo') + ->willReturn('foo'); + + $this->assertSame('foo', $this->connection->quote('foo')); + } + + /** + * @dataProvider spanDataDataProvider + * + * @param array $params + * @param array $expectedData + * + * @phpstan-param ConnectionParams $params + */ + public function testExec(array $params, array $expectedData): void + { + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); + $sql = 'SELECT 1 + 1'; + + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedConnection->expects($this->once()) + ->method('exec') + ->with($sql) + ->willReturn(10); + + $this->assertSame(10, $connection->exec($sql)); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingDriverConnection::SPAN_OP_CONN_EXEC, $spans[1]->getOp()); + $this->assertSame($sql, $spans[1]->getDescription()); + $this->assertSame($expectedData, $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testLastInsertId(): void + { + $this->decoratedConnection->expects($this->once()) + ->method('lastInsertId') + ->willReturn('10'); + + $this->assertSame('10', $this->connection->lastInsertId()); + } + + /** + * @dataProvider spanDataDataProvider + * + * @param array $params + * @param array $expectedData + * + * @phpstan-param ConnectionParams $params + */ + public function testBeginTransaction(array $params, array $expectedData): void + { + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedConnection->expects($this->once()) + ->method('beginTransaction'); + + $connection->beginTransaction(); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingDriverConnection::SPAN_OP_CONN_BEGIN_TRANSACTION, $spans[1]->getOp()); + $this->assertSame('BEGIN TRANSACTION', $spans[1]->getDescription()); + $this->assertSame($expectedData, $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testBeginTransactionDoesNothingIfNoSpanIsSetOnHub(): void + { + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedConnection->expects($this->once()) + ->method('beginTransaction'); + + $this->connection->beginTransaction(); + } + + /** + * @dataProvider spanDataDataProvider + * + * @param array $params + * @param array $expectedData + * + * @phpstan-param ConnectionParams $params + */ + public function testCommit(array $params, array $expectedData): void + { + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedConnection->expects($this->once()) + ->method('commit'); + + $connection->commit(); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingDriverConnection::SPAN_OP_TRANSACTION_COMMIT, $spans[1]->getOp()); + $this->assertSame('COMMIT', $spans[1]->getDescription()); + $this->assertSame($expectedData, $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testCommitDoesNothingIfNoSpanIsSetOnHub(): void + { + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedConnection->expects($this->once()) + ->method('commit'); + + $this->connection->commit(); + } + + /** + * @dataProvider spanDataDataProvider + * + * @param array $params + * @param array $expectedData + * + * @phpstan-param ConnectionParams $params + */ + public function testRollBack(array $params, array $expectedData): void + { + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', $params); + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedConnection->expects($this->once()) + ->method('rollBack'); + + $connection->rollBack(); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingDriverConnection::SPAN_OP_TRANSACTION_ROLLBACK, $spans[1]->getOp()); + $this->assertSame('ROLLBACK', $spans[1]->getDescription()); + $this->assertSame($expectedData, $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testRollBackDoesNothingIfNoSpanIsSetOnHub(): void + { + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedConnection->expects($this->once()) + ->method('rollBack'); + + $this->connection->rollBack(); + } + + public function testErrorCode(): void + { + if (!self::isDoctrineDBALVersion2Installed()) { + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be ^2.13.'); + } + + $this->decoratedConnection->expects($this->once()) + ->method('errorCode') + ->willReturn('1002'); + + $this->assertSame('1002', $this->connection->errorCode()); + } + + public function testErrorCodeThrowsExceptionIfDecoratedConnectionDoesNotImplementMethod(): void + { + if (!self::isDoctrineDBALVersion3Installed()) { + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); + } + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('The Sentry\\SentryBundle\\Tracing\\Doctrine\\DBAL\\TracingDriverConnection::errorCode() method is not supported on Doctrine DBAL 3.0.'); + + $this->connection->errorCode(); + } + + public function testErrorInfo(): void + { + if (!self::isDoctrineDBALVersion2Installed()) { + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be ^2.13.'); + } + + $this->decoratedConnection->expects($this->once()) + ->method('errorInfo') + ->willReturn(['foobar']); + + $this->assertSame(['foobar'], $this->connection->errorInfo()); + } + + public function testErrorInfoThrowsExceptionIfDecoratedConnectionDoesNotImplementMethod(): void + { + if (!self::isDoctrineDBALVersion3Installed()) { + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); + } + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('The Sentry\\SentryBundle\\Tracing\\Doctrine\\DBAL\\TracingDriverConnection::errorInfo() method is not supported on Doctrine DBAL 3.0.'); + + $this->connection->errorInfo(); + } + + public function testGetWrappedConnection(): void + { + $connection = new TracingDriverConnection($this->hub, $this->decoratedConnection, 'foo_platform', []); + + $this->assertSame($this->decoratedConnection, $connection->getWrappedConnection()); + } + + public function testGetNativeConnection(): void + { + $nativeConnection = new class { + }; + + $decoratedConnection = $this->createMock(NativeDriverConnectionInterfaceStub::class); + $decoratedConnection->expects($this->once()) + ->method('getNativeConnection') + ->willReturn($nativeConnection); + + $connection = new TracingDriverConnection($this->hub, $decoratedConnection, 'foo_platform', []); + + $this->assertSame($nativeConnection, $connection->getNativeConnection()); + } + + /** + * @return \Generator + */ + public function spanDataDataProvider(): \Generator + { + yield [ + [], + ['db.system' => 'foo_platform'], + ]; + + yield [ + [ + 'user' => 'root', + 'dbname' => 'INFORMATION_SCHEMA', + 'port' => 3306, + 'unix_socket' => '/var/run/mysqld/mysqld.sock', + ], + [ + 'db.system' => 'foo_platform', + 'db.user' => 'root', + 'db.name' => 'INFORMATION_SCHEMA', + 'server.port' => '3306', + 'server.socket.address' => 'Unix', + ], + ]; + + yield [ + [ + 'user' => 'root', + 'dbname' => 'INFORMATION_SCHEMA', + 'port' => 3306, + 'memory' => true, + ], + [ + 'db.system' => 'foo_platform', + 'db.user' => 'root', + 'db.name' => 'INFORMATION_SCHEMA', + 'server.port' => '3306', + 'server.socket.address' => 'inproc', + ], + ]; + + yield [ + [ + 'host' => 'localhost', + ], + [ + 'db.system' => 'foo_platform', + 'server.address' => 'localhost', + ], + ]; + + yield [ + [ + 'host' => '127.0.0.1', + ], + [ + 'db.system' => 'foo_platform', + 'server.address' => '127.0.0.1', + ], + ]; + } +} diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php b/tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php index efaac777..0f7ba7c3 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php @@ -179,7 +179,7 @@ public function testConvertExceptionWhenDriverDoesNotImplementInterface(): void } } -if (interface_exists(Driver::class)) { +if (interface_exists(Driver::class) && interface_exists(VersionAwarePlatformDriver::class)) { interface StubVersionAwarePlatformDriver extends Driver, VersionAwarePlatformDriver { } diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverForV3Test.php b/tests/Tracing/Doctrine/DBAL/TracingDriverForV3Test.php index af3a857b..06d7f13b 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingDriverForV3Test.php +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverForV3Test.php @@ -4,13 +4,9 @@ namespace Sentry\SentryBundle\Tests\Tracing\Doctrine\DBAL; -use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Driver\API\ExceptionConverter; use Doctrine\DBAL\Driver as DriverInterface; use Doctrine\DBAL\Driver\Connection as DriverConnectionInterface; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Schema\AbstractSchemaManager; -use Doctrine\DBAL\VersionAwarePlatformDriver as VersionAwarePlatformDriverInterface; use PHPUnit\Framework\MockObject\MockObject; use Sentry\SentryBundle\Tests\DoctrineTestCase; use Sentry\SentryBundle\Tracing\Doctrine\DBAL\TracingDriverConnectionFactoryInterface; @@ -27,7 +23,7 @@ final class TracingDriverForV3Test extends DoctrineTestCase public static function setUpBeforeClass(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } } @@ -62,78 +58,4 @@ public function testConnect(): void $this->assertSame($tracingDriverConnection, $driver->connect($params)); } - - public function testGetDatabasePlatform(): void - { - $databasePlatform = $this->createMock(AbstractPlatform::class); - - $decoratedDriver = $this->createMock(DriverInterface::class); - $decoratedDriver->expects($this->once()) - ->method('getDatabasePlatform') - ->willReturn($databasePlatform); - - $driver = new TracingDriverForV3($this->connectionFactory, $decoratedDriver); - - $this->assertSame($databasePlatform, $driver->getDatabasePlatform()); - } - - public function testGetSchemaManager(): void - { - $connection = $this->createMock(Connection::class); - $databasePlatform = $this->createMock(AbstractPlatform::class); - $schemaManager = $this->createMock(AbstractSchemaManager::class); - - $decoratedDriver = $this->createMock(DriverInterface::class); - $decoratedDriver->expects($this->once()) - ->method('getSchemaManager') - ->with($connection, $databasePlatform) - ->willReturn($schemaManager); - - $driver = new TracingDriverForV3($this->connectionFactory, $decoratedDriver); - - $this->assertSame($schemaManager, $driver->getSchemaManager($connection, $databasePlatform)); - } - - public function testGetExceptionConverter(): void - { - $exceptionConverter = $this->createMock(ExceptionConverter::class); - - $decoratedDriver = $this->createMock(DriverInterface::class); - $decoratedDriver->expects($this->once()) - ->method('getExceptionConverter') - ->willReturn($exceptionConverter); - - $driver = new TracingDriverForV3($this->connectionFactory, $decoratedDriver); - - $this->assertSame($exceptionConverter, $driver->getExceptionConverter()); - } - - public function testCreateDatabasePlatformForVersion(): void - { - $databasePlatform = $this->createMock(AbstractPlatform::class); - - $decoratedDriver = $this->createMock(VersionAwarePlatformDriverInterface::class); - $decoratedDriver->expects($this->once()) - ->method('createDatabasePlatformForVersion') - ->with('5.7') - ->willReturn($databasePlatform); - - $driver = new TracingDriverForV3($this->connectionFactory, $decoratedDriver); - - $this->assertSame($databasePlatform, $driver->createDatabasePlatformForVersion('5.7')); - } - - public function testCreateDatabasePlatformForVersionWhenDriverDoesNotImplementInterface(): void - { - $databasePlatform = $this->createMock(AbstractPlatform::class); - - $decoratedDriver = $this->createMock(DriverInterface::class); - $decoratedDriver->expects($this->once()) - ->method('getDatabasePlatform') - ->willReturn($databasePlatform); - - $driver = new TracingDriverForV3($this->connectionFactory, $decoratedDriver); - - $this->assertSame($databasePlatform, $driver->createDatabasePlatformForVersion('5.7')); - } } diff --git a/tests/Tracing/Doctrine/DBAL/TracingDriverForV4Test.php b/tests/Tracing/Doctrine/DBAL/TracingDriverForV4Test.php new file mode 100644 index 00000000..85d872d0 --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingDriverForV4Test.php @@ -0,0 +1,61 @@ += 4.0.'); + } + } + + protected function setUp(): void + { + $this->connectionFactory = $this->createMock(TracingDriverConnectionFactoryInterface::class); + } + + public function testConnect(): void + { + $params = ['host' => 'localhost']; + $databasePlatform = $this->createMock(AbstractPlatform::class); + $driverConnection = $this->createMock(DriverConnectionInterface::class); + $tracingDriverConnection = $this->createMock(TracingDriverConnectionInterface::class); + $decoratedDriver = $this->createMock(DriverInterface::class); + + $decoratedDriver->expects($this->once()) + ->method('connect') + ->with($params) + ->willReturn($driverConnection); + + $decoratedDriver->expects($this->once()) + ->method('getDatabasePlatform') + ->willReturn($databasePlatform); + + $this->connectionFactory->expects($this->once()) + ->method('create') + ->with($driverConnection, $databasePlatform, $params) + ->willReturn($tracingDriverConnection); + + $driver = new TracingDriverForV4($this->connectionFactory, $decoratedDriver); + + $this->assertSame($tracingDriverConnection, $driver->connect($params)); + } +} diff --git a/tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php b/tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php index 29bcbb78..13342222 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php +++ b/tests/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnectionTest.php @@ -29,7 +29,10 @@ final class TracingServerInfoAwareDriverConnectionTest extends DoctrineTestCase public static function setUpBeforeClass(): void { - if (!self::isDoctrineBundlePackageInstalled()) { + if ( + !self::isDoctrineDBALVersion2Installed() + && !self::isDoctrineDBALVersion3Installed() + ) { self::markTestSkipped(); } } @@ -185,7 +188,7 @@ public function testRequiresQueryForServerVersionThrowsExceptionIfWrappedConnect public function testRequiresQueryForServerVersionThrowsExceptionIfWrappedConnectionDoesNotImplementMethod(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } $this->decoratedConnection->expects($this->once()) @@ -214,7 +217,7 @@ public function testErrorCode(): void public function testErrorCodeThrowsExceptionIfDecoratedConnectionDoesNotImplementMethod(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } $this->expectException(\BadMethodCallException::class); @@ -239,7 +242,7 @@ public function testErrorInfo(): void public function testErrorInfoThrowsExceptionIfDecoratedConnectionDoesNotImplementMethod(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } $this->expectException(\BadMethodCallException::class); @@ -261,7 +264,7 @@ public function testGetWrappedConnection(): void public function testGetNativeConnection(): void { - $nativeConnection = new class() { + $nativeConnection = new class { }; $decoratedConnection = $this->createMock(NativeDriverConnectionInterfaceStub::class); diff --git a/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php b/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php index 11924259..722b1c27 100644 --- a/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php +++ b/tests/Tracing/Doctrine/DBAL/TracingStatementForV3Test.php @@ -34,7 +34,7 @@ final class TracingStatementForV3Test extends DoctrineTestCase public static function setUpBeforeClass(): void { if (!self::isDoctrineDBALVersion3Installed()) { - self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.0.'); + self::markTestSkipped('This test requires the version of the "doctrine/dbal" Composer package to be >= 3.3.'); } } @@ -70,7 +70,7 @@ public function testBindParam(): void public function testBindParamForwardsLengthParamOnlyWhenExplicitlySet(): void { $variable = 'bar'; - $decoratedStatement = new class() implements Statement { + $decoratedStatement = new class implements Statement { /** * @var int */ diff --git a/tests/Tracing/Doctrine/DBAL/TracingStatementForV4Test.php b/tests/Tracing/Doctrine/DBAL/TracingStatementForV4Test.php new file mode 100644 index 00000000..476e7938 --- /dev/null +++ b/tests/Tracing/Doctrine/DBAL/TracingStatementForV4Test.php @@ -0,0 +1,97 @@ += 4.0.'); + } + } + + protected function setUp(): void + { + $this->hub = $this->createMock(HubInterface::class); + $this->decoratedStatement = $this->createMock(Statement::class); + $this->statement = new TracingStatementForV4($this->hub, $this->decoratedStatement, 'SELECT 1', ['db.system' => 'sqlite']); + } + + public function testBindValue(): void + { + $this->decoratedStatement->expects($this->once()) + ->method('bindValue') + ->with('foo', 'bar', ParameterType::INTEGER); + + $this->statement->bindValue('foo', 'bar', ParameterType::INTEGER); + } + + public function testExecute(): void + { + $driverResult = $this->createMock(Result::class); + $transaction = new Transaction(new TransactionContext(), $this->hub); + $transaction->initSpanRecorder(); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn($transaction); + + $this->decoratedStatement->expects($this->once()) + ->method('execute') + ->willReturn($driverResult); + + $this->assertSame($driverResult, $this->statement->execute()); + $this->assertNotNull($transaction->getSpanRecorder()); + + $spans = $transaction->getSpanRecorder()->getSpans(); + + $this->assertCount(2, $spans); + $this->assertSame(TracingStatementForV4::SPAN_OP_STMT_EXECUTE, $spans[1]->getOp()); + $this->assertSame('SELECT 1', $spans[1]->getDescription()); + $this->assertSame(['db.system' => 'sqlite'], $spans[1]->getData()); + $this->assertNotNull($spans[1]->getEndTimestamp()); + } + + public function testExecuteDoesNothingIfNoSpanIsSetOnHub(): void + { + $driverResult = $this->createMock(Result::class); + + $this->hub->expects($this->once()) + ->method('getSpan') + ->willReturn(null); + + $this->decoratedStatement->expects($this->once()) + ->method('execute') + ->willReturn($driverResult); + + $this->assertSame($driverResult, $this->statement->execute()); + } +} diff --git a/tests/Tracing/HttpClient/TraceableHttpClientTest.php b/tests/Tracing/HttpClient/TraceableHttpClientTest.php index 5778a31f..4306378e 100644 --- a/tests/Tracing/HttpClient/TraceableHttpClientTest.php +++ b/tests/Tracing/HttpClient/TraceableHttpClientTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerAwareInterface; use Psr\Log\NullLogger; -use Sentry\Client; use Sentry\ClientInterface; use Sentry\Options; use Sentry\SentryBundle\Tracing\HttpClient\AbstractTraceableResponse; @@ -23,7 +22,7 @@ use Sentry\Tracing\TraceId; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; -use Sentry\Transport\NullTransport; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -48,7 +47,7 @@ final class TraceableHttpClientTest extends TestCase public static function setUpBeforeClass(): void { - if (!self::isHttpClientPackageInstalled()) { + if (!class_exists(HttpClient::class)) { self::markTestSkipped('This test requires the "symfony/http-client" Composer package to be installed.'); } } @@ -66,8 +65,7 @@ public function testRequest(): void 'dsn' => 'http://public:secret@example.com/sentry/1', ]); $client = $this->createMock(ClientInterface::class); - $client - ->expects($this->once()) + $client->expects($this->once()) ->method('getOptions') ->willReturn($options); @@ -94,16 +92,15 @@ public function testRequest(): void $this->assertSame(200, $response->getStatusCode()); $this->assertSame('GET', $response->getInfo('http_method')); $this->assertSame('https://username:password@www.example.com/test-page?foo=bar#baz', $response->getInfo('url')); - $this->assertSame(['sentry-trace: ' . $spans[1]->toTraceparent()], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); - $this->assertSame(['baggage: ' . $transaction->toBaggage()], $mockResponse->getRequestOptions()['normalized_headers']['baggage']); + $this->assertSame([\sprintf('sentry-trace: %s', $spans[1]->toTraceparent())], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); + $this->assertSame([\sprintf('traceparent: %s', $spans[1]->toW3CTraceparent())], $mockResponse->getRequestOptions()['normalized_headers']['traceparent']); + $this->assertSame([\sprintf('baggage: %s', $transaction->toBaggage())], $mockResponse->getRequestOptions()['normalized_headers']['baggage']); $this->assertNotNull($transaction->getSpanRecorder()); $spans = $transaction->getSpanRecorder()->getSpans(); - $expectedTags = [ - 'http.method' => 'GET', - 'http.url' => 'https://www.example.com/test-page', - ]; $expectedData = [ + 'http.url' => 'https://www.example.com/test-page', + 'http.request.method' => 'GET', 'http.query' => 'foo=bar', 'http.fragment' => 'baz', ]; @@ -119,7 +116,6 @@ public function testRequest(): void $this->assertSame('http.client', $spans[1]->getOp()); $this->assertSame('GET https://www.example.com/test-page', $spans[1]->getDescription()); $this->assertSame(SpanStatus::ok(), $spans[1]->getStatus()); - $this->assertSame($expectedTags, $spans[1]->getTags()); $this->assertSame($expectedData, $spans[1]->getData()); } @@ -127,7 +123,7 @@ public function testRequestDoesNotContainTracingHeaders(): void { $options = new Options([ 'dsn' => 'http://public:secret@example.com/sentry/1', - 'trace_propagation_targets' => null, + 'trace_propagation_targets' => [], ]); $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) @@ -154,30 +150,35 @@ public function testRequestDoesNotContainTracingHeaders(): void $this->assertSame('PUT', $response->getInfo('http_method')); $this->assertSame('https://www.example.com/test-page', $response->getInfo('url')); $this->assertArrayNotHasKey('sentry-trace', $mockResponse->getRequestOptions()['normalized_headers']); + $this->assertArrayNotHasKey('traceparent', $mockResponse->getRequestOptions()['normalized_headers']); $this->assertArrayNotHasKey('baggage', $mockResponse->getRequestOptions()['normalized_headers']); $this->assertNotNull($transaction->getSpanRecorder()); $spans = $transaction->getSpanRecorder()->getSpans(); - $expectedTags = [ - 'http.method' => 'PUT', + $expectedData = [ 'http.url' => 'https://www.example.com/test-page', + 'http.request.method' => 'PUT', ]; $this->assertCount(2, $spans); $this->assertNull($spans[1]->getEndTimestamp()); $this->assertSame('http.client', $spans[1]->getOp()); $this->assertSame('PUT https://www.example.com/test-page', $spans[1]->getDescription()); - $this->assertSame($expectedTags, $spans[1]->getTags()); + $this->assertSame($expectedData, $spans[1]->getData()); } public function testRequestDoesContainsTracingHeadersWithoutTransaction(): void { - $client = new Client(new Options([ + $options = new Options([ 'dsn' => 'http://public:secret@example.com/sentry/1', 'release' => '1.0.0', 'environment' => 'test', 'trace_propagation_targets' => ['www.example.com'], - ]), new NullTransport()); + ]); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->exactly(5)) + ->method('getOptions') + ->willReturn($options); $propagationContext = PropagationContext::fromDefaults(); $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); @@ -198,25 +199,29 @@ public function testRequestDoesContainsTracingHeadersWithoutTransaction(): void $this->assertSame(200, $response->getStatusCode()); $this->assertSame('POST', $response->getInfo('http_method')); $this->assertSame('https://www.example.com/test-page', $response->getInfo('url')); - $this->assertSame(['sentry-trace: 566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8'], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); - $this->assertSame(['baggage: sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=test'], $mockResponse->getRequestOptions()['normalized_headers']['baggage']); + $this->assertSame([\sprintf('sentry-trace: %s', $propagationContext->toTraceparent())], $mockResponse->getRequestOptions()['normalized_headers']['sentry-trace']); + $this->assertSame([\sprintf('traceparent: %s', $propagationContext->toW3CTraceparent())], $mockResponse->getRequestOptions()['normalized_headers']['traceparent']); + $this->assertSame([\sprintf('baggage: %s', $propagationContext->toBaggage())], $mockResponse->getRequestOptions()['normalized_headers']['baggage']); } public function testRequestSetsUnknownErrorAsSpanStatusIfResponseStatusCodeIsUnavailable(): void { + $options = new Options([ + 'dsn' => 'http://public:secret@example.com/sentry/1', + ]); $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) + $client->expects($this->exactly(2)) ->method('getOptions') - ->willReturn(new Options(['dsn' => 'http://public:secret@example.com/sentry/1'])); + ->willReturn($options); - $transaction = new Transaction(new TransactionContext()); + $transaction = new Transaction(new TransactionContext(), $this->hub); $transaction->initSpanRecorder(); $this->hub->expects($this->once()) ->method('getSpan') ->willReturn($transaction); - $this->hub->expects($this->once()) + $this->hub->expects($this->exactly(2)) ->method('getClient') ->willReturn($client); @@ -269,9 +274,9 @@ public function testStream(): void $this->assertNotNull($transaction->getSpanRecorder()); $spans = $transaction->getSpanRecorder()->getSpans(); - $expectedTags = [ - 'http.method' => 'GET', + $expectedData = [ 'http.url' => 'https://www.example.com/test-page', + 'http.request.method' => 'GET', ]; $this->assertSame('foobar', implode('', $chunks)); @@ -280,7 +285,7 @@ public function testStream(): void $this->assertNotNull($spans[1]->getEndTimestamp()); $this->assertSame('http.client', $spans[1]->getOp()); $this->assertSame('GET https://www.example.com/test-page', $spans[1]->getDescription()); - $this->assertSame($expectedTags, $spans[1]->getTags()); + $this->assertSame($expectedData, $spans[1]->getData()); $loopIndex = 0; @@ -356,13 +361,10 @@ public function testWithOptions(): void $this->assertSame('GET', $response->getInfo('http_method')); $this->assertSame('https://www.example.org/test-page', $response->getInfo('url')); } +} - private static function isHttpClientPackageInstalled(): bool +if (interface_exists(HttpClientInterface::class)) { + interface TestableHttpClientInterface extends HttpClientInterface, LoggerAwareInterface, ResetInterface { - 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 index e67616d5..5a4bb370 100644 --- a/tests/Tracing/HttpClient/TraceableResponseTest.php +++ b/tests/Tracing/HttpClient/TraceableResponseTest.php @@ -12,6 +12,7 @@ use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -28,6 +29,13 @@ final class TraceableResponseTest extends TestCase */ private $hub; + public static function setUpBeforeClass(): void + { + if (!class_exists(HttpClient::class)) { + self::markTestSkipped('This test requires the "symfony/http-client" Composer package to be installed.'); + } + } + protected function setUp(): void { $this->client = $this->createMock(HttpClientInterface::class); @@ -47,7 +55,7 @@ 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)); + unserialize(\sprintf('O:%u:"%s":0:{}', \strlen(TraceableResponse::class), TraceableResponse::class)); } public function testDestructor(): void diff --git a/tests/Transport/TransportFactoryTest.php b/tests/Transport/TransportFactoryTest.php deleted file mode 100644 index 667590dc..00000000 --- a/tests/Transport/TransportFactoryTest.php +++ /dev/null @@ -1,100 +0,0 @@ -create(new Options(['dsn' => 'http://public@example.com/sentry/1'])); - - $this->assertInstanceOf(HttpTransport::class, $transport); - - try { - $transport->send(Event::createEvent())->wait(); - - $this->fail('Failed asserting that the transport returns a rejected promise on error.'); - } catch (RejectionException $exception) { - $this->assertInstanceOf(Response::class, $exception->getReason()); - } - } - - public function testCreateWithCustomFactories(): void - { - $uriFactory = $this->createMock(UriFactoryInterface::class); - $requestFactory = $this->createMock(RequestFactoryInterface::class); - $responseFactory = $this->createMock(ResponseFactoryInterface::class); - $streamFactory = $this->createMock(StreamFactoryInterface::class); - $httpClient = $this->createMock(HttpAsyncClientInterface::class); - $logger = $this->createMock(LoggerInterface::class); - $transportFactory = new TransportFactory( - $uriFactory, - $requestFactory, - $responseFactory, - $streamFactory, - $httpClient, - $logger - ); - - $requestFactory->expects($this->once()) - ->method('createRequest') - ->willReturnCallback(static function (...$arguments): RequestInterface { - return Psr17FactoryDiscovery::findRequestFactory()->createRequest(...$arguments); - }); - - $streamFactory->expects($this->atLeastOnce()) - ->method('createStream') - ->willReturnCallback(static function (...$arguments): StreamInterface { - return Psr17FactoryDiscovery::findStreamFactory()->createStream(...$arguments); - }); - - $httpClient->expects($this->once()) - ->method('sendAsyncRequest') - ->willReturnCallback(static function (RequestInterface $request): HttpPromiseInterface { - return new RejectedPromise(new NetworkException('foo', $request)); - }); - - $logger->expects($this->once()) - ->method('error') - ->withAnyParameters(); - - $event = Event::createEvent(); - $transport = $transportFactory->create(new Options(['dsn' => 'http://public@example.com/sentry/1', 'send_attempts' => 0])); - - try { - $transport->send($event)->wait(); - - $this->fail('Failed asserting that the transport returns a rejected promise on error.'); - } catch (RejectionException $exception) { - /** @var Response $response */ - $response = $exception->getReason(); - - $this->assertInstanceOf(Response::class, $response); - $this->assertSame(ResponseStatus::failed(), $response->getStatus()); - $this->assertSame($event, $response->getEvent()); - } - } -} diff --git a/tests/Twig/SentryExtensionTest.php b/tests/Twig/SentryExtensionTest.php index f5bf7c75..5cbfd25b 100644 --- a/tests/Twig/SentryExtensionTest.php +++ b/tests/Twig/SentryExtensionTest.php @@ -5,7 +5,6 @@ namespace Sentry\SentryBundle\Tests\Twig; use PHPUnit\Framework\TestCase; -use Sentry\Client; use Sentry\ClientInterface; use Sentry\Options; use Sentry\SentryBundle\Twig\SentryExtension; @@ -17,7 +16,6 @@ use Sentry\Tracing\TraceId; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; -use Sentry\Transport\NullTransport; use Symfony\Bundle\TwigBundle\TwigBundle; use Twig\Environment; use Twig\Loader\ArrayLoader; @@ -68,10 +66,55 @@ public function testTraceMetaFunctionWithActiveSpan(): void $transaction = new Transaction(new TransactionContext()); $transaction->setTraceId(new TraceId('a3c01c41d7b94b90aee23edac90f4319')); $transaction->setSpanId(new SpanId('e69c2aef0ec34f2a')); + $transaction->setSampled(true); $hub->setSpan($transaction); - $this->assertSame('', $environment->render('foo.twig')); + $this->assertSame('', $environment->render('foo.twig')); + } + + public function testW3CTraceMetaFunctionWithNoActiveSpan(): void + { + $environment = new Environment(new ArrayLoader(['foo.twig' => '{{ sentry_w3c_trace_meta() }}'])); + $environment->addExtension(new SentryExtension()); + + $propagationContext = PropagationContext::fromDefaults(); + $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); + $propagationContext->setSpanId(new SpanId('566e3688a61d4bc8')); + + $hub = new Hub(null, new Scope($propagationContext)); + + SentrySdk::setCurrentHub($hub); + + $this->assertSame('', $environment->render('foo.twig')); + } + + public function testW3CTraceMetaFunctionWithActiveSpan(): void + { + $environment = new Environment(new ArrayLoader(['foo.twig' => '{{ sentry_w3c_trace_meta() }}'])); + $environment->addExtension(new SentryExtension()); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 1.0, + 'release' => '1.0.0', + 'environment' => 'development', + ])); + + $hub = new Hub($client); + + SentrySdk::setCurrentHub($hub); + + $transaction = new Transaction(new TransactionContext()); + $transaction->setTraceId(new TraceId('a3c01c41d7b94b90aee23edac90f4319')); + $transaction->setSpanId(new SpanId('e69c2aef0ec34f2a')); + $transaction->setSampled(true); + + $hub->setSpan($transaction); + + $this->assertSame('', $environment->render('foo.twig')); } public function testBaggageMetaFunctionWithNoActiveSpan(): void @@ -95,7 +138,7 @@ public function testBaggageMetaFunctionWithNoActiveSpan(): void SentrySdk::setCurrentHub($hub); - $this->assertSame('', $environment->render('foo.twig')); + $this->assertSame(\sprintf('', $propagationContext->toBaggage()), $environment->render('foo.twig')); } public function testBaggageMetaFunctionWithActiveSpan(): void @@ -103,11 +146,14 @@ public function testBaggageMetaFunctionWithActiveSpan(): void $environment = new Environment(new ArrayLoader(['foo.twig' => '{{ sentry_baggage_meta() }}'])); $environment->addExtension(new SentryExtension()); - $client = new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'release' => '1.0.0', - 'environment' => 'development', - ]), new NullTransport()); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 1.0, + 'release' => '1.0.0', + 'environment' => 'development', + ])); $hub = new Hub($client); @@ -118,7 +164,7 @@ public function testBaggageMetaFunctionWithActiveSpan(): void $hub->setSpan($transaction); - $this->assertSame('', $environment->render('foo.twig')); + $this->assertSame(\sprintf('', $transaction->toBaggage()), $environment->render('foo.twig')); } private static function isTwigBundlePackageInstalled(): bool