├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── UPGRADE-3.0.md ├── UPGRADE-4.0.md ├── UPGRADE-5.0.md ├── composer.json ├── psalm-baseline.xml ├── psalm.xml ├── scripts └── bump-version.sh └── src ├── Command └── SentryTestCommand.php ├── DependencyInjection ├── Compiler │ ├── AddLoginListenerTagPass.php │ ├── CacheTracingPass.php │ ├── DbalTracingPass.php │ └── HttpClientTracingPass.php ├── Configuration.php └── SentryExtension.php ├── ErrorTypesParser.php ├── EventListener ├── AbstractTracingRequestListener.php ├── ConsoleListener.php ├── ErrorListener.php ├── KernelEventForwardCompatibilityTrait.php ├── LoginListener.php ├── MessengerListener.php ├── RequestListener.php ├── SubRequestListener.php ├── TracingConsoleListener.php ├── TracingRequestListener.php └── TracingSubRequestListener.php ├── Integration ├── IntegrationConfigurator.php └── RequestFetcher.php ├── Resources └── config │ ├── schema │ └── sentry-1.0.xsd │ └── services.xml ├── SentryBundle.php ├── Tracing ├── Cache │ ├── TraceableCacheAdapterForV2.php │ ├── TraceableCacheAdapterForV3.php │ ├── TraceableCacheAdapterTrait.php │ ├── TraceableTagAwareCacheAdapterForV2.php │ └── TraceableTagAwareCacheAdapterForV3.php ├── Doctrine │ └── DBAL │ │ ├── AbstractTracingStatement.php │ │ ├── Compatibility │ │ └── MiddlewareInterface.php │ │ ├── ConnectionConfigurator.php │ │ ├── TracingDriverConnectionFactoryForV2V3.php │ │ ├── TracingDriverConnectionFactoryForV4.php │ │ ├── TracingDriverConnectionFactoryInterface.php │ │ ├── TracingDriverConnectionForV2V3.php │ │ ├── TracingDriverConnectionForV4.php │ │ ├── TracingDriverConnectionInterface.php │ │ ├── TracingDriverForV2.php │ │ ├── TracingDriverForV3.php │ │ ├── TracingDriverForV4.php │ │ ├── TracingDriverMiddleware.php │ │ ├── TracingServerInfoAwareDriverConnection.php │ │ ├── TracingStatementForV2.php │ │ ├── TracingStatementForV3.php │ │ └── TracingStatementForV4.php ├── HttpClient │ ├── AbstractTraceableHttpClient.php │ ├── AbstractTraceableResponse.php │ ├── TraceableHttpClientForV4.php │ ├── TraceableHttpClientForV5.php │ ├── TraceableHttpClientForV6.php │ ├── TraceableResponseForV4.php │ ├── TraceableResponseForV5.php │ └── TraceableResponseForV6.php └── Twig │ └── TwigTracingExtension.php ├── Twig └── SentryExtension.php └── aliases.php /AUTHORS: -------------------------------------------------------------------------------- 1 | http://github.com/getsentry/sentry-symfony/contributors 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 5.2.0 4 | 5 | The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v5.2.0. 6 | 7 | ### Features 8 | 9 | - Allow to configure the logger via the `sentry.yaml` configuration file [(#899)](https://github.com/getsentry/sentry-symfony/pull/899) 10 | 11 | ```yaml 12 | sentry: 13 | dsn: "%env(SENTRY_DSN)%" 14 | options: 15 | logger: "sentry.logger" 16 | 17 | services: 18 | sentry.logger: 19 | class: 'Sentry\Logger\DebugFileLogger' 20 | arguments: 21 | $filePath: '../../var/log/sentry.log' 22 | ``` 23 | 24 | ### Bug Fixes 25 | 26 | - Fixed updating the user context when a route is marked as stateless [(#910)](https://github.com/getsentry/sentry-symfony/pull/910) 27 | 28 | ### Misc 29 | 30 | - Remove `symfony/security-core` and `symfony/security-http` as dependencies [(#912)](https://github.com/getsentry/sentry-symfony/pull/912) 31 | 32 | ## 5.1.0 33 | 34 | The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v5.1.0. 35 | 36 | ### Features 37 | 38 | - The SDK was updated to support PHP 8.4 [(#893)](https://github.com/getsentry/sentry-symfony/pull/893) 39 | - Set the status for CLI command transactions based on the exit code [(#891)](https://github.com/getsentry/sentry-symfony/pull/891) 40 | 41 | ### Bug Fixes 42 | 43 | - Fix including request data on transactions [(#879)](https://github.com/getsentry/sentry-symfony/pull/879) 44 | 45 | ## 5.0.1 46 | 47 | The Sentry SDK team is happy to announce the immediate availability of Sentry Symfony SDK v5.0.1. 48 | 49 | ### Bug Fixes 50 | 51 | - Add missing `setCallbackWrapper` method to `TraceableCacheAdapterTrait` [(#841)](https://github.com/getsentry/sentry-symfony/pull/841) 52 | - Fix detection of the `symfony/http-client` being installed [(#858)](https://github.com/getsentry/sentry-symfony/pull/858) 53 | 54 | ## 5.0.0 55 | 56 | The Sentry SDK team is thrilled to announce the immediate availability of Sentry Symfony SDK v5.0.0. 57 | 58 | ### Breaking Change 59 | 60 | 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. 61 | 62 | This version adds support for the underlying [Sentry PHP SDK v4.0](https://github.com/getsentry/sentry-php). 63 | 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. 64 | 65 | - This version exclusively uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/) to send event data to Sentry. 66 | 67 | If you are using [sentry.io](https://sentry.io), no action is needed. 68 | If you are using an on-premise/self-hosted installation of Sentry, the minimum requirement is now version `>= v20.6.0`. 69 | 70 | - You need to have `ext-curl` installed to use the SDK. 71 | 72 | - The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. 73 | Previously, both `Symfony\Component\ErrorHandler\Error\FatalError` and `Symfony\Component\Debug\Exception\FatalErrorException` were ignored by default. 74 | To continue ignoring these exceptions, make the following changes to the config file: 75 | 76 | ```yaml 77 | // config/packages/sentry.yaml 78 | 79 | sentry: 80 | options: 81 | ignore_exceptions: 82 | - 'Symfony\Component\ErrorHandler\Error\FatalError' 83 | - 'Symfony\Component\Debug\Exception\FatalErrorException' 84 | ``` 85 | 86 | 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. 87 | 88 | ### Features 89 | 90 | - Add support for Sentry Developer Metrics [(#1619)](https://github.com/getsentry/sentry-php/pull/1619) 91 | 92 | ```php 93 | use function Sentry\metrics; 94 | 95 | // Add 4 to a counter named hits 96 | metrics()->increment(key: 'hits', value: 4); 97 | 98 | // Add 25 to a distribution named response_time with unit milliseconds 99 | metrics()->distribution(key: 'response_time', value: 25, unit: MetricsUnit::millisecond()); 100 | 101 | // Add 2 to gauge named parallel_requests, tagged with type: "a" 102 | metrics()->gauge(key: 'parallel_requests', value: 2, tags: ['type': 'a']); 103 | 104 | // Add a user's email to a set named users.sessions, tagged with role: "admin" 105 | metrics()->set('users.sessions', 'jane.doe@example.com', null, ['role' => User::admin()]); 106 | ``` 107 | 108 | Metrics are automatically sent to Sentry at the end of a request, hooking into Symfony's `kernel.terminate` event. 109 | 110 | - Add new fluent APIs [(#1601)](https://github.com/getsentry/sentry-php/pull/1601) 111 | 112 | ```php 113 | // Before 114 | $transactionContext = new TransactionContext(); 115 | $transactionContext->setName('GET /example'); 116 | $transactionContext->setOp('http.server'); 117 | 118 | // After 119 | $transactionContext = (new TransactionContext()) 120 | ->setName('GET /example'); 121 | ->setOp('http.server'); 122 | ``` 123 | 124 | - Simplify the breadcrumb API [(#1603)](https://github.com/getsentry/sentry-php/pull/1603) 125 | 126 | ```php 127 | // Before 128 | \Sentry\addBreadcrumb( 129 | new \Sentry\Breadcrumb( 130 | \Sentry\Breadcrumb::LEVEL_INFO, 131 | \Sentry\Breadcrumb::TYPE_DEFAULT, 132 | 'auth', // category 133 | 'User authenticated', // message (optional) 134 | ['user_id' => $userId] // data (optional) 135 | ) 136 | ); 137 | 138 | // After 139 | \Sentry\addBreadcrumb( 140 | category: 'auth', 141 | message: 'User authenticated', // optional 142 | metadata: ['user_id' => $userId], // optional 143 | level: Breadcrumb::LEVEL_INFO, // set by default 144 | type: Breadcrumb::TYPE_DEFAULT, // set by default 145 | ); 146 | ``` 147 | 148 | - New default cURL HTTP client [(#1589)](https://github.com/getsentry/sentry-php/pull/1589) 149 | 150 | The SDK now ships with its own HTTP client based on cURL. A few new options were added. 151 | 152 | ```yaml 153 | // config/packages/sentry.yaml 154 | 155 | sentry: 156 | options: 157 | - http_proxy_authentication: 'username:password' // user name and password to use for proxy authentication 158 | - http_ssl_verify_peer: false // default true, verify the peer's SSL certificate 159 | - http_compression: false // default true, http request body compression 160 | ``` 161 | 162 | To use a different client, you may use the `http_client` option. 163 | To use a different transport, you may use the `transport` option. A custom transport must implement the `TransportInterface`. 164 | If you use the `transport` option, the `http_client` option has no effect. 165 | 166 | ### Misc 167 | 168 | - The abandoned package `php-http/message-factory` was removed. 169 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Sentry 4 | 5 |

6 | 7 | # Contributing to the Sentry SDK for Symfony 8 | 9 | We welcome contributions to `sentry-symfony` by the community. 10 | 11 | Please search the [issue tracker](https://github.com/getsentry/sentry-symfony/issues) before creating a new issue (a problem or an improvement request). Please also ask in our [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr) before submitting a new issue. There is a ton of great people in our Discord community ready to help you! 12 | 13 | If you feel that you can fix or implement it yourself, please read on to learn how to submit your changes. 14 | 15 | ## Submitting changes 16 | 17 | - Setup the development environment. 18 | - Clone the `sentry-symfony` repository and prepare necessary changes. 19 | - Add tests for your changes to `tests/`. 20 | - Run tests and make sure all of them pass. 21 | - Submit a pull request, referencing any issues it addresses. 22 | 23 | We will review your pull request as soon as possible. 24 | Thank you for contributing! 25 | 26 | ## Development environment 27 | 28 | ### Clone the repository 29 | 30 | ```bash 31 | git clone git@github.com:getsentry/sentry-symfony.git 32 | ``` 33 | 34 | Make sure that you have PHP 7.2+ installed. Version 7.4 or higher is required to run style checkers. On macOS, we recommend using brew to install PHP. For Windows, we recommend an official PHP.net release. 35 | 36 | ### Install the dependencies 37 | 38 | Dependencies are managed through [Composer](https://getcomposer.org/): 39 | 40 | ```bash 41 | composer install 42 | ``` 43 | 44 | ### Running tests 45 | 46 | Tests can then be run via [PHPUnit](https://phpunit.de/): 47 | 48 | ```bash 49 | vendor/bin/phpunit 50 | ``` 51 | 52 | ## Releasing a new version 53 | 54 | (only relevant for Sentry employees) 55 | 56 | Prerequisites: 57 | 58 | - All changes that should be released must be in the `master` branch. 59 | - Every commit should follow the [Commit Message Format](https://develop.sentry.dev/commit-messages/#commit-message-format) convention. 60 | 61 | Manual Process: 62 | 63 | - Update CHANGELOG.md with the version to be released. Example commit: https://github.com/getsentry/sentry-symfony/commit/d1d2895c028676113cb191516b80b91abcde31f3. 64 | - On GitHub in the `sentry-symfony` repository go to "Actions" select the "Release" workflow. 65 | - Click on "Run workflow" on the right side and make sure the `master` branch is selected. 66 | - Set "Version to release" input field. Here you decide if it is a major, minor or patch release. (See "Versioning Policy" below) 67 | - Click "Run Workflow" 68 | 69 | This will trigger [Craft](https://github.com/getsentry/craft) to prepare everything needed for a release. (For more information see [craft prepare](https://github.com/getsentry/craft#craft-prepare-preparing-a-new-release)) At the end of this process, a release issue is created in the [Publish](https://github.com/getsentry/publish) repository. (Example release issue: https://github.com/getsentry/publish/issues/815) 70 | 71 | Now one of the persons with release privileges (most probably your engineering manager) will review this Issue and then add the `accepted` label to the issue. 72 | 73 | There are always two persons involved in a release. 74 | 75 | If you are in a hurry and the release should be out immediately there is a Slack channel called `#proj-release-approval` where you can see your release issue and where you can ping people to please have a look immediately. 76 | 77 | When the release issue is labeled `accepted` [Craft](https://github.com/getsentry/craft) is triggered again to publish the release to all the right platforms. (See [craft publish](https://github.com/getsentry/craft#craft-publish-publishing-the-release) for more information). At the end of this process, the release issue on GitHub will be closed and the release is completed! Congratulations! 78 | 79 | There is a sequence diagram visualizing all this in the [README.md](https://github.com/getsentry/publish) of the `Publish` repository. 80 | 81 | ### Versioning Policy 82 | 83 | This project follows [semver](https://semver.org/), with three additions: 84 | 85 | - Semver says that major version `0` can include breaking changes at any time. Still, it is common practice to assume that only `0.x` releases (minor versions) can contain breaking changes while `0.x.y` releases (patch versions) are used for backwards-compatible changes (bugfixes and features). This project also follows that practice. 86 | 87 | - All undocumented APIs are considered internal. They are not part of this contract. 88 | 89 | - Certain features (e.g. integrations) may be explicitly called out as "experimental" or "unstable" in the documentation. They come with their own versioning policy described in the documentation. 90 | 91 | We recommend pinning your version requirements against `1.x.*` or `1.x.y`. 92 | Either one of the following is fine: 93 | 94 | ```json 95 | "sentry/sentry": "^1.0", 96 | "sentry/sentry": "^1", 97 | ``` 98 | 99 | A major release `N` implies the previous release `N-1` will no longer receive updates. We generally do not backport bugfixes to older versions unless they are security relevant. However, feel free to ask for backports of specific commits on the bug tracker. 100 | 101 | ## Commit message format guidelines 102 | 103 | See the documentation on commit messages here: 104 | 105 | https://develop.sentry.dev/commit-messages/#commit-message-format -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Functional Software, Inc. dba Sentry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Sentry 4 | 5 |

6 | 7 | _Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_ 8 | 9 | # Official Sentry SDK for Symfony 10 | 11 | [![Stable release][Last stable image]][Packagist link] 12 | [![License](https://poser.pugx.org/sentry/sentry-symfony/license)](https://packagist.org/packages/sentry/sentry-symfony) 13 | [![Total Downloads](https://poser.pugx.org/sentry/sentry-symfony/downloads)](https://packagist.org/packages/sentry/sentry-symfony) 14 | [![Monthly Downloads](https://poser.pugx.org/sentry/sentry-symfony/d/monthly)](https://packagist.org/packages/sentry/sentry-symfony) 15 | 16 | [![CI](https://github.com/getsentry/sentry-symfony/actions/workflows/tests.yaml/badge.svg)](https://github.com/getsentry/sentry-symfony/actions/workflows/tests.yaml) 17 | [![Coverage Status][Master Code Coverage Image]][Master Code Coverage] 18 | [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/cWnMQeA) 19 | 20 | This is the official Symfony SDK for [Sentry](https://getsentry.com/). 21 | 22 | ## Getting Started 23 | 24 | ### Install 25 | 26 | Install the SDK using [Composer](https://getcomposer.org/). 27 | 28 | ```bash 29 | composer require sentry/sentry-symfony 30 | ``` 31 | 32 | ### Configure 33 | 34 | Add the [Sentry DSN](https://docs.sentry.io/quickstart/#configure-the-dsn) to your `.env` file. 35 | 36 | ``` 37 | ###> sentry/sentry-symfony ### 38 | SENTRY_DSN="https://public@sentry.example.com/1" 39 | ###< sentry/sentry-symfony ### 40 | ``` 41 | 42 | ### Usage 43 | 44 | ```php 45 | use function Sentry\captureException; 46 | 47 | try { 48 | $this->functionThatMayFail(); 49 | } catch (\Throwable $exception) { 50 | captureException($exception); 51 | } 52 | ``` 53 | 54 | ## Contributing to the SDK 55 | 56 | Please refer to [CONTRIBUTING.md](CONTRIBUTING.md). 57 | 58 | ### Thanks to all the people who contributed so far! 59 | 60 | 61 | 62 | 63 | 64 | ## Getting help/support 65 | 66 | 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! 67 | 68 | ## Resources 69 | 70 | - [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/quickstart/) 71 | - [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) 72 | - [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry) 73 | - [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) 74 | 75 | ## License 76 | 77 | Licensed under the MIT license, see [`LICENSE`](LICENSE) 78 | 79 | [Last stable image]: https://poser.pugx.org/sentry/sentry-symfony/version.svg 80 | [Packagist link]: https://packagist.org/packages/sentry/sentry-symfony 81 | [Master Code Coverage]: https://codecov.io/gh/getsentry/sentry-symfony/branch/master 82 | [Master Code Coverage Image]: https://img.shields.io/codecov/c/github/getsentry/sentry-symfony/master?logo=codecov 83 | 84 | -------------------------------------------------------------------------------- /UPGRADE-3.0.md: -------------------------------------------------------------------------------- 1 | # Upgrade to 3.0 2 | The 3.0 major release of this bundle has some major changes. This document will try to list them all, to help users 3 | during the upgrade path. 4 | 5 | ## Sentry SDK 2.0 6 | The major change is in the fact that we now require the `sentry/sdk` metapackage; this, in turn, requires the original 7 | `sentry/sentry` package, but at major version 2. 8 | This new version has been completely rewritten: if you use this bundle and you interact directly with the underlying SDK 9 | and client, you should read through the [relative upgrade document](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-2.0.md). 10 | 11 | ## Changes in the configuration 12 | There are only two BC impacting the configuration: 13 | * the `skip_capture` option has been **removed**: the same feature has been implemented in the new SDK, with the PHP `excluded_exceptions` option; you just have to move your values from `sentry.skip_capture` to `sentry.options.excluded_exceptions` 14 | * Symfony internal exceptions are no longer ignored by default: Sentry will start getting events for 404 or 403, or even 403 which are followed by a (remembered) login. Add new values to the `sentry.options.excluded_exceptions` option; notice that it works with an `instanceof` against the exception, so you could ignore multiple kind of events using a common ancestor class. 15 | * The `sentry.options` values reflect the options of the PHP SDK; many of those have been removed, and there are some new ones. You can read the [appropriate section of the upgrade document](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-2.0.md#client-options), with the exception of the `server` to `dsn` migration, which is still handled in the same way by the bundle, under `sentry.dsn` 16 | * A new notable option is `send_default_pii`: it's a new option of the new SDK with by default is turned off, for GDPR compliance. You need to set `sentry.options.send_default_pii: true` to have the user's username and IP address attached to the events, as in the 2.0 bundle. 17 | 18 | ## HTTPlug 19 | Since SDK 2.0 uses HTTPlug to remain transport-agnostic, you need to have installed two packages that provides 20 | [`php-http/async-client-implementation`](https://packagist.org/providers/php-http/async-client-implementation) 21 | and [`http-message-implementation`](https://packagist.org/providers/psr/http-message-implementation). 22 | 23 | The metapackage already solves this need, requiring the Curl client and Guzzle's message factories. 24 | 25 | If instead you want to use a different HTTP client or message factory, you'll need to require manually those additional 26 | packages: 27 | 28 | ```bash 29 | composer require sentry/sentry:^2.0 php-http/guzzle6-adapter guzzlehttp/psr7 30 | ``` 31 | 32 | The `sentry/sentry` package is required directly to override `sentry/sdk`, and the other two packages are up to your choice; 33 | in the current example, we're using both Guzzle's components (client and message factory). 34 | 35 | ## Changes in the services 36 | Due to the SDK changes, and to follow newer Symfony best practices, the services exposed by the bundle are completely 37 | changed: 38 | 39 | * All services are now private; declare public aliases to access them if needed; you can still use the Sentry SDK global 40 | functions if you want just to capture messages manually without injecting Sentry services in your code 41 | * All services uses the full qualified name of their interfaces to name them 42 | * The `ExceptionListener` has been split and renamed: we now have a simpler `ErrorListener`, and three other listeners 43 | dedicated to enriching events of data (`RequestListener`, `SubRequestListener` and `ConsoleListener`) 44 | * The listeners are now `final`; append your own listeners to override their behavior 45 | * The `SentrySymfonyEvents::PRE_CAPTURE` and `SentrySymfonyEvents::SET_USER_CONTEXT` events are dropped; if you want to inject data into your events, write your own listener in a similar fashion to `RequestListener`, using the `Hub` and the `Scope` to handle how and when the new information is attached to events 46 | * The listeners are registered with a priority of `1`; they will run just before the default priority of `0`, to ease 47 | the registration of custom listener that will change `Scope` data 48 | * Configuration options of the bundle are now aligned with the new ones of the 2.0 SDK 49 | 50 | ## New services 51 | This is a brief description of the services registered by this bundle: 52 | 53 | * `Sentry\State\HubInterface`: this is the central root of the SDK; it's the `Hub` that the bundle will instantiate at 54 | startup, and the current one too if you do not change it 55 | * `Sentry\ClientInterface`: this is the proper client; compared to the 1.x SDK version it's a lot more slimmed down, 56 | since a lot of the stuff has been split in separated components, so you probably will not interact with it as much as 57 | in the past. You also have to remind you that the client is bound to the `Hub`, and has to be changed there if you want 58 | to use a different one automatically in error reporting 59 | * `Sentry\ClientBuilderInterface`: this is the factory that builds the client; you can call its methods to change all 60 | the settings and dependencies that will be injected in the latter created client. You can use this service to obtain more 61 | customized clients for your needs 62 | * `Sentry\Options`: this class holds all the configuration used in the client and other SDK components; it's populated 63 | starting from the bundle configuration 64 | -------------------------------------------------------------------------------- /UPGRADE-4.0.md: -------------------------------------------------------------------------------- 1 | # Upgrade 3.x to 4.0 2 | 3 | - Bumped the required version of `sentry/sentry` to `^3.0`: this version relies on the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/); if you are 4 | 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. For further details read the [UPGRADE-3.0.md](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-3.0.md) document of `sentry/sentry`. 5 | - Added the `$hub` argument to the constructor of the `SubRequestListener` class. 6 | - Renamed the `ConsoleListener` class to `ConsoleCommandListener`. 7 | - Renamed the `ConsoleListener::onConsoleCommand` method to `ConsoleCommandListener::handleConsoleCommandEvent`. 8 | - Renamed the `ErrorListener::onException` method to `ErrorListener::handleExceptionEvent`. 9 | - Removed the `ErrorListener::onKernelException` method. 10 | - Removed the `ErrorListener::onConsoleError` method. 11 | - Renamed the `RequestListener::onKernelRequest` method to `RequestListener::handleKernelRequestEvent`. 12 | - Renamed the `RequestListener::onKernelController` method to `RequestListener::handleKernelControllerEvent`. 13 | - Renamed the `SubRequestListener::onKernelRequest` method to `SubRequestListener::handleKernelRequestEvent`. 14 | - Renamed the `SubRequestListener::onKernelFinishRequest` method to `SubRequestListener::handleKernelFinishRequestEvent`. 15 | - Removed the `Sentry\FlushableClientInterface` service alias. 16 | - Removed the `sentry.listener_priorities` configuration option. 17 | 18 | Before: 19 | 20 | ```yaml 21 | sentry: 22 | listener_priorities: 23 | request: 10 24 | ``` 25 | 26 | After: 27 | 28 | ```php 29 | use Sentry\SentryBundle\EventListener\RequestListener; 30 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 31 | use Symfony\Component\DependencyInjection\ContainerBuilder; 32 | use Symfony\Component\HttpKernel\KernelEvents; 33 | 34 | final class ChangeSentryListenerPriorityPass implements CompilerPassInterface 35 | { 36 | public function process(ContainerBuilder $container) 37 | { 38 | $definition = $container->getDefinition(RequestListener::class); 39 | $definitionTags = $definition->getTags(); 40 | 41 | foreach ($definitionTags['kernel.event_listener'] as &$tags) { 42 | if (KernelEvents::REQUEST === $tags['event']) { 43 | $tags['priority'] = 10; 44 | } 45 | } 46 | 47 | $definition->setTags($definitionTags); 48 | } 49 | } 50 | ``` 51 | 52 | - Removed the `sentry.options.excluded_exceptions` configuration option. 53 | 54 | Before: 55 | 56 | ```yaml 57 | sentry: 58 | options: 59 | excluded_exceptions: 60 | - RuntimeException 61 | ``` 62 | 63 | After: 64 | 65 | ```yaml 66 | sentry: 67 | options: 68 | integrations: 69 | - 'Sentry\Integration\IgnoreErrorsIntegration' 70 | 71 | services: 72 | Sentry\Integration\IgnoreErrorsIntegration: 73 | arguments: 74 | $options: 75 | ignore_exceptions: 76 | - RuntimeException 77 | ``` 78 | 79 | - Changed the priority of the `ConsoleCommandListener::handleConsoleErrorEvent` listener to `-64`. 80 | - Changed the priority of the `ConsoleCommandListener::::handleConsoleCommandEvent` listener to `128`. 81 | - Changed the priority of the `MessengerListener::handleWorkerMessageFailedEvent` listener to `50`. 82 | - Changed the priority of the `RequestListener::handleKernelRequestEvent` listener to `5`. 83 | - Changed the priority of the `RequestListener::handleKernelControllerEvent` listener to `10`. 84 | - Changed the priority of the `SubRequestListener::handleKernelRequestEvent` listener to `3`. 85 | - Changed the priority of the `SubRequestListener::handleKernelFinishRequestEvent` listener to `5`. 86 | - Changed the type of the `sentry.options.before_send` configuration option from `scalar` to `string`. The value must always be the name of the container service to call without the `@` prefix. 87 | 88 | Before 89 | 90 | ```yaml 91 | sentry: 92 | options: 93 | before_send: '@app.sentry.before_send' 94 | ``` 95 | 96 | ```yaml 97 | sentry: 98 | options: 99 | before_send: 'App\Sentry\BeforeSend::__invoke' 100 | ``` 101 | 102 | ```yaml 103 | sentry: 104 | options: 105 | before_send: ['App\Sentry\BeforeSend', '__invoke'] 106 | ``` 107 | 108 | After 109 | 110 | ```yaml 111 | sentry: 112 | options: 113 | before_send: 'app.sentry.before_send' 114 | ``` 115 | 116 | - Changed the type of the `sentry.options.before_breadcrumb` configuration option from `scalar` to `string`. The value must always be the name of the container service to call without the `@` prefix. 117 | 118 | Before 119 | 120 | ```yaml 121 | sentry: 122 | options: 123 | before_breadcrumb: '@app.sentry.before_breadcrumb' 124 | ``` 125 | 126 | ```yaml 127 | sentry: 128 | options: 129 | before_breadcrumb: 'App\Sentry\BeforeBreadcrumb::__invoke' 130 | ``` 131 | 132 | ```yaml 133 | sentry: 134 | options: 135 | before_breadcrumb: ['App\Sentry\BeforeBreadcrumb', '__invoke'] 136 | ``` 137 | 138 | After 139 | 140 | ```yaml 141 | sentry: 142 | options: 143 | before_breadcrumb: 'app.sentry.before_breadcrumb' 144 | ``` 145 | 146 | - Changed the type of the `sentry.options.class_serializers` configuration option from an array of `scalar` values to an array of `string` values. The value must always be the name of the container service to call without the `@` prefix. 147 | 148 | Before 149 | 150 | ```yaml 151 | sentry: 152 | options: 153 | class_serializers: 154 | App\FooClass: '@app.sentry.foo_class_serializer' 155 | ``` 156 | 157 | ```yaml 158 | sentry: 159 | options: 160 | class_serializers: 161 | App\FooClass: 'App\Sentry\FooClassSerializer::__invoke' 162 | ``` 163 | 164 | ```yaml 165 | sentry: 166 | options: 167 | class_serializers: 168 | App\FooClass: ['App\Sentry\FooClassSerializer', 'invoke'] 169 | ``` 170 | 171 | After 172 | 173 | ```yaml 174 | sentry: 175 | options: 176 | class_serializers: 177 | App\FooClass: 'app.sentry.foo_class_serializer' 178 | ``` 179 | 180 | - Changed the type of the `sentry.options.integrations` configuration option from an array of `scalar` values to an array of `string` values. The value must always be the name of the container service to call without the `@` prefix. 181 | 182 | Before 183 | 184 | ```yaml 185 | sentry: 186 | options: 187 | integrations: 188 | - '@app.sentry.foo_integration' 189 | ``` 190 | 191 | After 192 | 193 | ```yaml 194 | sentry: 195 | options: 196 | integrations: 197 | - 'app.sentry.foo_integration' 198 | ``` 199 | 200 | - Removed the `ClientBuilderConfigurator` class. 201 | - Removed the `SentryBundle::getSdkVersion()` method. 202 | - Removed the `SentryBundle::getCurrentHub()` method, use `SentrySdk::getCurrentHub()` instead. 203 | - Removed the `Sentry\ClientBuilderInterface` and `Sentry\Options` services. 204 | - Refactorized the `ErrorTypesParser` class and made it `@internal`. 205 | - Removed the `sentry.monolog` configuration option. 206 | 207 | Before 208 | 209 | ```yaml 210 | sentry: 211 | monolog: 212 | level: !php/const Monolog\Logger::ERROR 213 | bubble: false 214 | error_handler: 215 | enabled: true 216 | ``` 217 | 218 | After 219 | 220 | ```yaml 221 | services: 222 | Sentry\Monolog\Handler: 223 | arguments: 224 | $hub: '@Sentry\State\HubInterface' 225 | $level: !php/const Monolog\Logger::ERROR 226 | $bubble: false 227 | ``` 228 | -------------------------------------------------------------------------------- /UPGRADE-5.0.md: -------------------------------------------------------------------------------- 1 | # Upgrade 4.x to 5.0 2 | 3 | This version adds support for the underlying [Sentry PHP SDK v4.0](https://github.com/getsentry/sentry-php). 4 | 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. 5 | 6 | - This version exclusively uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/) to send event data to Sentry. 7 | 8 | If you are using [sentry.io](https://sentry.io), no action is needed. 9 | If you are using an on-premise/self-hosted installation of Sentry, the minimum requirement is now version `>= v20.6.0`. 10 | 11 | - You need to have `ext-curl` installed to use the SDK. 12 | 13 | - The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. 14 | Previously, both `Symfony\Component\ErrorHandler\Error\FatalError` and `Symfony\Component\Debug\Exception\FatalErrorException` were ignored by default. 15 | To continue ignoring these exceptions, make the following changes to your `config/packages/sentry.yaml` file: 16 | 17 | ```yaml 18 | // config/packages/sentry.yaml 19 | 20 | sentry: 21 | options: 22 | ignore_exceptions: 23 | - 'Symfony\Component\ErrorHandler\Error\FatalError' 24 | - 'Symfony\Component\Debug\Exception\FatalErrorException' 25 | ``` 26 | 27 | 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. 28 | 29 | - Removed support for `guzzlehttp/psr7: ^1.8.4`. 30 | 31 | - The `RequestFetcher` now relies on `guzzlehttp/psr7: ^2.1.1`. 32 | 33 | - Continue traces from the W3C `traceparent` request header. 34 | - Inject the W3C `traceparent` header on outgoing HTTP client calls. 35 | - Added `Sentry\SentryBundle\Twig\SentryExtension::getW3CTraceMeta()`. 36 | 37 | - 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 `[]`. 38 | 39 | - Added the `sentry.options.enable_tracing` option. 40 | - Added the `sentry.options.attach_metric_code_locations` option. 41 | - Added the `sentry.options.spotlight` option. 42 | - Added the `sentry.options.spotlight_url` option. 43 | - Added the `sentry.options.transport` option. 44 | - Added the `sentry.options.http_client` option. 45 | - Added the `sentry.options.http_proxy_authentication` option. 46 | - Added the `sentry.options.http_ssl_verify_peer` option. 47 | - Added the `sentry.options.http_compression` option. 48 | 49 | - Removed the `sentry.transport_factory` option. Use `sentry.options.transport` to use a custom transport. 50 | - Removed the `sentry.options.send_attempts` option. You may use a custom transport if you rely on this behaviour. 51 | - Removed the `sentry.options.enable_compression` option. Use `sentry.options.http_compression` instead. 52 | 53 | - Removed `Sentry\SentryBundle\Transport\TransportFactory`. 54 | - Removed `Sentry\State\HubInterface\Sentry\State\HubInterface`. 55 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sentry/sentry-symfony", 3 | "type": "symfony-bundle", 4 | "description": "Symfony integration for Sentry (http://getsentry.com)", 5 | "keywords": ["logging", "errors", "symfony", "sentry"], 6 | "homepage": "http://getsentry.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Sentry", 11 | "email": "accounts@sentry.io" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.2||^8.0", 16 | "guzzlehttp/psr7": "^2.1.1", 17 | "jean85/pretty-package-versions": "^1.5||^2.0", 18 | "sentry/sentry": "^4.11.0", 19 | "symfony/cache-contracts": "^1.1||^2.4||^3.0", 20 | "symfony/config": "^4.4.20||^5.0.11||^6.0||^7.0", 21 | "symfony/console": "^4.4.20||^5.0.11||^6.0||^7.0", 22 | "symfony/dependency-injection": "^4.4.20||^5.0.11||^6.0||^7.0", 23 | "symfony/event-dispatcher": "^4.4.20||^5.0.11||^6.0||^7.0", 24 | "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0||^7.0", 25 | "symfony/polyfill-php80": "^1.22", 26 | "symfony/psr-http-message-bridge": "^1.2||^2.0||^6.4||^7.0" 27 | }, 28 | "require-dev": { 29 | "doctrine/dbal": "^2.13||^3.3||^4.0", 30 | "doctrine/doctrine-bundle": "^2.6", 31 | "friendsofphp/php-cs-fixer": "^2.19||^3.40", 32 | "masterminds/html5": "^2.8", 33 | "phpstan/extension-installer": "^1.0", 34 | "phpstan/phpstan": "1.12.5", 35 | "phpstan/phpstan-phpunit": "1.4.0", 36 | "phpstan/phpstan-symfony": "1.4.10", 37 | "phpunit/phpunit": "^8.5.40||^9.6.21", 38 | "symfony/browser-kit": "^4.4.20||^5.0.11||^6.0||^7.0", 39 | "symfony/cache": "^4.4.20||^5.0.11||^6.0||^7.0", 40 | "symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0||^7.0", 41 | "symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", 42 | "symfony/http-client": "^4.4.20||^5.0.11||^6.0||^7.0", 43 | "symfony/messenger": "^4.4.20||^5.0.11||^6.0||^7.0", 44 | "symfony/monolog-bundle": "^3.4", 45 | "symfony/phpunit-bridge": "^5.2.6||^6.0||^7.0", 46 | "symfony/process": "^4.4.20||^5.0.11||^6.0||^7.0", 47 | "symfony/security-core": "^4.4.20||^5.0.11||^6.0||^7.0", 48 | "symfony/security-http": "^4.4.20||^5.0.11||^6.0||^7.0", 49 | "symfony/twig-bundle": "^4.4.20||^5.0.11||^6.0||^7.0", 50 | "symfony/yaml": "^4.4.20||^5.0.11||^6.0||^7.0", 51 | "vimeo/psalm": "^4.3||^5.16.0" 52 | }, 53 | "suggest": { 54 | "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler.", 55 | "doctrine/doctrine-bundle": "Allow distributed tracing of database queries using Sentry.", 56 | "symfony/twig-bundle": "Allow distributed tracing of Twig template rendering using Sentry.", 57 | "symfony/cache": "Allow distributed tracing of cache pools using Sentry." 58 | }, 59 | "autoload": { 60 | "files": [ 61 | "src/aliases.php" 62 | ], 63 | "psr-4": { 64 | "Sentry\\SentryBundle\\": "src/" 65 | } 66 | }, 67 | "autoload-dev": { 68 | "psr-4": { 69 | "Sentry\\SentryBundle\\Tests\\": "tests/" 70 | } 71 | }, 72 | "scripts": { 73 | "check": [ 74 | "@cs-check", 75 | "@phpstan", 76 | "@psalm", 77 | "@tests" 78 | ], 79 | "tests": "vendor/bin/phpunit --verbose", 80 | "cs-check": "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", 81 | "cs-fix": "vendor/bin/php-cs-fixer fix --verbose --diff", 82 | "phpstan": "vendor/bin/phpstan analyse", 83 | "psalm": "vendor/bin/psalm" 84 | }, 85 | "config": { 86 | "sort-packages": true, 87 | "allow-plugins": { 88 | "composer/package-versions-deprecated": true, 89 | "phpstan/extension-installer": true 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | getServerVersion())]]> 39 | 40 | 41 | 42 | getDatabasePlatform($versionProvider)]]> 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /scripts/bump-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | # if [ "$(uname -s)" != "Linux" ]; then 5 | # echo "Please use the GitHub Action." 6 | # exit 1 7 | # fi 8 | 9 | SCRIPT_DIR="$( dirname "$0" )" 10 | cd $SCRIPT_DIR/.. 11 | 12 | OLD_VERSION="${1}" 13 | NEW_VERSION="${2}" 14 | 15 | echo "Current version: $OLD_VERSION" 16 | echo "Bumping version: $NEW_VERSION" 17 | 18 | function replace() { 19 | ! grep "$2" $3 20 | perl -i -pe "s/$1/$2/g" $3 21 | grep "$2" $3 # verify that replacement was successful 22 | } 23 | 24 | replace "SDK_VERSION = '[0-9.]+'" "SDK_VERSION = '$NEW_VERSION'" ./src/SentryBundle.php 25 | -------------------------------------------------------------------------------- /src/Command/SentryTestCommand.php: -------------------------------------------------------------------------------- 1 | hub = $hub ?? SentrySdk::getCurrentHub(); 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output): int 35 | { 36 | $client = $this->hub->getClient(); 37 | 38 | if (null === $client) { 39 | $output->writeln('No client found'); 40 | $output->writeln('Your DSN is probably missing, check your configuration'); 41 | 42 | return 1; 43 | } 44 | 45 | $dsn = $client->getOptions()->getDsn(); 46 | 47 | if (null === $dsn) { 48 | $output->writeln('No DSN configured in the current client, please check your configuration'); 49 | $output->writeln('To debug further, try bin/console debug:config sentry'); 50 | 51 | return 1; 52 | } 53 | 54 | $output->writeln('DSN correctly configured in the current client'); 55 | $output->writeln('Sending test message...'); 56 | 57 | $eventId = $this->hub->captureMessage('This is a test message from the Sentry bundle'); 58 | 59 | if (null === $eventId) { 60 | $output->writeln('Message not sent!'); 61 | $output->writeln('Check your DSN or your before_send callback if used'); 62 | 63 | return 1; 64 | } 65 | 66 | $output->writeln("Message sent successfully with ID $eventId"); 67 | 68 | return 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/AddLoginListenerTagPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition(LoginListener::class)) { 21 | return; 22 | } 23 | $listenerDefinition = $container->getDefinition(LoginListener::class); 24 | 25 | if (class_exists(LoginSuccessEvent::class)) { 26 | $listenerDefinition->addTag('kernel.event_listener', [ 27 | 'event' => LoginSuccessEvent::class, 28 | 'method' => 'handleLoginSuccessEvent', 29 | ]); 30 | } elseif (class_exists(AuthenticationSuccessEvent::class)) { 31 | $listenerDefinition->addTag('kernel.event_listener', [ 32 | 'event' => AuthenticationSuccessEvent::class, 33 | 'method' => 'handleAuthenticationSuccessEvent', 34 | ]); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CacheTracingPass.php: -------------------------------------------------------------------------------- 1 | getParameter('sentry.tracing.cache.enabled')) { 22 | return; 23 | } 24 | 25 | foreach ($container->findTaggedServiceIds('cache.pool') as $serviceId => $tags) { 26 | $cachePoolDefinition = $container->getDefinition($serviceId); 27 | 28 | if ($cachePoolDefinition->isAbstract()) { 29 | continue; 30 | } 31 | 32 | $definitionClass = $this->resolveDefinitionClass($container, $cachePoolDefinition); 33 | 34 | if (null === $definitionClass) { 35 | continue; 36 | } 37 | 38 | if (is_subclass_of($definitionClass, TagAwareAdapterInterface::class)) { 39 | $traceableCachePoolDefinition = new ChildDefinition('sentry.tracing.traceable_tag_aware_cache_adapter'); 40 | } else { 41 | $traceableCachePoolDefinition = new ChildDefinition('sentry.tracing.traceable_cache_adapter'); 42 | } 43 | 44 | $traceableCachePoolDefinition->setDecoratedService($serviceId); 45 | $traceableCachePoolDefinition->replaceArgument(1, new Reference($serviceId . '.traceable.inner')); 46 | 47 | $container->setDefinition($serviceId . '.traceable', $traceableCachePoolDefinition); 48 | } 49 | } 50 | 51 | private function resolveDefinitionClass(ContainerBuilder $container, Definition $definition): ?string 52 | { 53 | $class = $definition->getClass(); 54 | 55 | while (null === $class && $definition instanceof ChildDefinition) { 56 | $definition = $container->findDefinition($definition->getParent()); 57 | $class = $definition->getClass(); 58 | } 59 | 60 | return $class; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DbalTracingPass.php: -------------------------------------------------------------------------------- 1 | hasParameter('doctrine.connections') 29 | || !$container->getParameter('sentry.tracing.enabled') 30 | || !$container->getParameter('sentry.tracing.dbal.enabled') 31 | ) { 32 | return; 33 | } 34 | 35 | $this->assertRequiredDbalVersion(); 36 | 37 | /** @var string[] $connectionsToTrace */ 38 | $connectionsToTrace = $container->getParameter('sentry.tracing.dbal.connections'); 39 | 40 | /** @var array $connections */ 41 | $connections = $container->getParameter('doctrine.connections'); 42 | 43 | if (empty($connectionsToTrace)) { 44 | $connectionsToTrace = array_keys($connections); 45 | } 46 | 47 | foreach ($connectionsToTrace as $connectionName) { 48 | if (!\in_array(\sprintf(self::CONNECTION_SERVICE_NAME_FORMAT, $connectionName), $connections, true)) { 49 | throw new \InvalidArgumentException(\sprintf('The Doctrine connection "%s" does not exists and cannot be instrumented.', $connectionName)); 50 | } 51 | 52 | if (class_exists(Result::class)) { 53 | $this->configureConnectionForDoctrineDBALVersion3($container, $connectionName); 54 | } else { 55 | $this->configureConnectionForDoctrineDBALVersion2($container, $connectionName); 56 | } 57 | } 58 | } 59 | 60 | private function configureConnectionForDoctrineDBALVersion3(ContainerBuilder $container, string $connectionName): void 61 | { 62 | $tracingMiddlewareDefinition = $container->getDefinition(TracingDriverMiddleware::class); 63 | $tracingMiddlewareDefinition->addTag('doctrine.middleware', ['connection' => $connectionName]); 64 | } 65 | 66 | private function configureConnectionForDoctrineDBALVersion2(ContainerBuilder $container, string $connectionName): void 67 | { 68 | $connectionDefinition = $container->getDefinition(\sprintf(self::CONNECTION_SERVICE_NAME_FORMAT, $connectionName)); 69 | $connectionDefinition->setConfigurator([new Reference(ConnectionConfigurator::class), 'configure']); 70 | } 71 | 72 | private function assertRequiredDbalVersion(): void 73 | { 74 | if (interface_exists(Result::class)) { 75 | // DBAL ^2.13 76 | return; 77 | } 78 | 79 | if (class_exists(Result::class)) { 80 | // DBAL ^3 81 | return; 82 | } 83 | 84 | throw new \LogicException('Tracing support cannot be enabled as the Doctrine DBAL 2.13+ package is not installed. Try running "composer require doctrine/dbal:^2.13".'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/HttpClientTracingPass.php: -------------------------------------------------------------------------------- 1 | getParameter('sentry.tracing.enabled') || !$container->getParameter('sentry.tracing.http_client.enabled')) { 32 | return; 33 | } 34 | 35 | $decoratedService = $this->getDecoratedService($container); 36 | 37 | if (null === $decoratedService) { 38 | return; 39 | } 40 | 41 | $container->register(TraceableHttpClient::class, TraceableHttpClient::class) 42 | ->setArgument(0, new Reference(TraceableHttpClient::class . '.inner')) 43 | ->setArgument(1, new Reference(HubInterface::class)) 44 | ->setDecoratedService($decoratedService[0], null, $decoratedService[1]); 45 | } 46 | 47 | /** 48 | * @return array{string, int}|null 49 | */ 50 | private function getDecoratedService(ContainerBuilder $container): ?array 51 | { 52 | // Starting from Symfony 6.3, the raw HTTP client that serves as adapter 53 | // for the transport is registered as a separate service, so that the 54 | // scoped clients can inject it before any decoration is applied on them. 55 | // Since we need to access the full URL of the request, and such information 56 | // is available after the `ScopingHttpClient` class did its job, we have 57 | // to decorate such service. For more details, see https://github.com/symfony/symfony/pull/49513. 58 | if ($container->hasDefinition('http_client.transport')) { 59 | return ['http_client.transport', -15]; 60 | } 61 | 62 | // On versions of Symfony prior to 6.3, when the mock client is in-use, 63 | // each HTTP client is decorated by referencing explicitly the innermost 64 | // service rather than by using the standard decoration feature. Hence, 65 | // we have to look for the specific names of those services, and decorate 66 | // them instead of the raw HTTP client. 67 | foreach (self::MOCK_HTTP_CLIENT_SERVICE_IDS as $httpClientServiceId) { 68 | if ($container->hasDefinition($httpClientServiceId)) { 69 | return [$httpClientServiceId, 15]; 70 | } 71 | } 72 | 73 | if ($container->hasDefinition('http_client')) { 74 | return ['http_client', 15]; 75 | } 76 | 77 | return null; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode() 31 | : $treeBuilder->root('sentry'); 32 | 33 | $inAppExcludes = [ 34 | '%kernel.cache_dir%', 35 | '%kernel.project_dir%/vendor', 36 | ]; 37 | 38 | if (Kernel::VERSION_ID >= 50200) { 39 | $inAppExcludes[] = '%kernel.build_dir%'; 40 | } 41 | 42 | $rootNode 43 | ->children() 44 | ->scalarNode('dsn') 45 | ->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.') 46 | ->end() 47 | ->booleanNode('register_error_listener')->defaultTrue()->end() 48 | ->booleanNode('register_error_handler')->defaultTrue()->end() 49 | ->scalarNode('logger') 50 | ->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.') 51 | ->defaultNull() 52 | ->end() 53 | ->arrayNode('options') 54 | ->addDefaultsIfNotSet() 55 | ->fixXmlConfig('integration') 56 | ->fixXmlConfig('trace_propagation_target') 57 | ->fixXmlConfig('tag') 58 | ->fixXmlConfig('class_serializer') 59 | ->fixXmlConfig('ignore_exception') 60 | ->fixXmlConfig('ignore_transaction') 61 | ->fixXmlConfig('prefix', 'prefixes') 62 | ->children() 63 | ->arrayNode('integrations') 64 | ->scalarPrototype()->end() 65 | ->end() 66 | ->booleanNode('default_integrations')->end() 67 | ->arrayNode('prefixes') 68 | ->defaultValue(array_merge(['%kernel.project_dir%'], array_filter(explode(\PATH_SEPARATOR, get_include_path() ?: '')))) 69 | ->scalarPrototype()->end() 70 | ->end() 71 | ->floatNode('sample_rate') 72 | ->min(0.0) 73 | ->max(1.0) 74 | ->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.') 75 | ->end() 76 | ->booleanNode('enable_tracing')->end() 77 | ->floatNode('traces_sample_rate') 78 | ->min(0.0) 79 | ->max(1.0) 80 | ->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.') 81 | ->end() 82 | ->scalarNode('traces_sampler')->end() 83 | ->floatNode('profiles_sample_rate') 84 | ->min(0.0) 85 | ->max(1.0) 86 | ->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') 87 | ->end() 88 | ->booleanNode('attach_stacktrace')->end() 89 | ->booleanNode('attach_metric_code_locations')->end() 90 | ->integerNode('context_lines')->min(0)->end() 91 | ->scalarNode('environment') 92 | ->cannotBeEmpty() 93 | ->defaultValue('%kernel.environment%') 94 | ->end() 95 | ->scalarNode('logger')->end() 96 | ->booleanNode('spotlight')->end() 97 | ->scalarNode('spotlight_url')->end() 98 | ->scalarNode('release') 99 | ->cannotBeEmpty() 100 | ->defaultValue('%env(default::SENTRY_RELEASE)%') 101 | ->end() 102 | ->scalarNode('server_name')->end() 103 | ->arrayNode('ignore_exceptions') 104 | ->scalarPrototype()->end() 105 | ->beforeNormalization()->castToArray()->end() 106 | ->end() 107 | ->arrayNode('ignore_transactions') 108 | ->scalarPrototype()->end() 109 | ->beforeNormalization()->castToArray()->end() 110 | ->end() 111 | ->scalarNode('before_send')->end() 112 | ->scalarNode('before_send_transaction')->end() 113 | ->scalarNode('before_send_check_in')->end() 114 | ->scalarNode('before_send_metrics')->end() 115 | ->variableNode('trace_propagation_targets')->end() 116 | ->arrayNode('tags') 117 | ->useAttributeAsKey('name') 118 | ->normalizeKeys(false) 119 | ->scalarPrototype()->end() 120 | ->end() 121 | ->scalarNode('error_types') 122 | ->beforeNormalization() 123 | ->always(\Closure::fromCallable([ErrorTypesParser::class, 'parse'])) 124 | ->end() 125 | ->end() 126 | ->integerNode('max_breadcrumbs') 127 | ->min(0) 128 | ->max(Options::DEFAULT_MAX_BREADCRUMBS) 129 | ->end() 130 | ->variableNode('before_breadcrumb')->end() 131 | ->arrayNode('in_app_exclude') 132 | ->scalarPrototype()->end() 133 | ->beforeNormalization()->castToArray()->end() 134 | ->defaultValue($inAppExcludes) 135 | ->end() 136 | ->arrayNode('in_app_include') 137 | ->scalarPrototype()->end() 138 | ->beforeNormalization()->castToArray()->end() 139 | ->end() 140 | ->booleanNode('send_default_pii')->end() 141 | ->integerNode('max_value_length')->min(0)->end() 142 | ->scalarNode('transport')->end() 143 | ->scalarNode('http_client')->end() 144 | ->scalarNode('http_proxy')->end() 145 | ->scalarNode('http_proxy_authentication')->end() 146 | ->floatNode('http_connect_timeout') 147 | ->min(0) 148 | ->info('The maximum number of seconds to wait while trying to connect to a server. It works only when using the default transport.') 149 | ->end() 150 | ->floatNode('http_timeout') 151 | ->min(0) 152 | ->info('The maximum execution time for the request+response as a whole. It works only when using the default transport.') 153 | ->end() 154 | ->booleanNode('http_ssl_verify_peer')->end() 155 | ->booleanNode('http_compression')->end() 156 | ->booleanNode('capture_silenced_errors')->end() 157 | ->enumNode('max_request_body_size') 158 | ->values([ 159 | 'none', 160 | 'small', 161 | 'medium', 162 | 'always', 163 | ]) 164 | ->end() 165 | ->arrayNode('class_serializers') 166 | ->useAttributeAsKey('class') 167 | ->normalizeKeys(false) 168 | ->scalarPrototype()->end() 169 | ->end() 170 | ->end() 171 | ->end() 172 | ->end(); 173 | 174 | $this->addMessengerSection($rootNode); 175 | $this->addDistributedTracingSection($rootNode); 176 | 177 | return $treeBuilder; 178 | } 179 | 180 | private function addMessengerSection(ArrayNodeDefinition $rootNode): void 181 | { 182 | $rootNode 183 | ->children() 184 | ->arrayNode('messenger') 185 | ->{interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}() 186 | ->children() 187 | ->booleanNode('capture_soft_fails')->defaultTrue()->end() 188 | ->end() 189 | ->end() 190 | ->end(); 191 | } 192 | 193 | private function addDistributedTracingSection(ArrayNodeDefinition $rootNode): void 194 | { 195 | $rootNode 196 | ->children() 197 | ->arrayNode('tracing') 198 | ->canBeDisabled() 199 | ->addDefaultsIfNotSet() 200 | ->children() 201 | ->arrayNode('dbal') 202 | ->{class_exists(DoctrineBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}() 203 | ->fixXmlConfig('connection') 204 | ->children() 205 | ->arrayNode('connections') 206 | ->scalarPrototype()->end() 207 | ->end() 208 | ->end() 209 | ->end() 210 | ->arrayNode('twig') 211 | ->{class_exists(TwigBundle::class) ? 'canBeDisabled' : 'canBeEnabled'}() 212 | ->end() 213 | ->arrayNode('cache') 214 | ->{class_exists(CacheItem::class) ? 'canBeDisabled' : 'canBeEnabled'}() 215 | ->end() 216 | ->arrayNode('http_client') 217 | ->{class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}() 218 | ->end() 219 | ->arrayNode('console') 220 | ->addDefaultsIfNotSet() 221 | ->fixXmlConfig('excluded_command') 222 | ->children() 223 | ->arrayNode('excluded_commands') 224 | ->scalarPrototype()->end() 225 | ->defaultValue(['messenger:consume']) 226 | ->end() 227 | ->end() 228 | ->end() 229 | ->end() 230 | ->end() 231 | ->end(); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/ErrorTypesParser.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 30 | } 31 | 32 | /** 33 | * This method is called once a response for the current HTTP request is 34 | * created, but before it is sent off to the client. Its use is mainly for 35 | * gathering information like the HTTP status code and attaching them as 36 | * tags of the span/transaction. 37 | * 38 | * @param ResponseEvent $event The event 39 | */ 40 | public function handleKernelResponseEvent(ResponseEvent $event): void 41 | { 42 | $response = $event->getResponse(); 43 | $span = $this->hub->getSpan(); 44 | 45 | if (null === $span) { 46 | return; 47 | } 48 | 49 | $span->setHttpStatus($response->getStatusCode()); 50 | } 51 | 52 | /** 53 | * Gets the name of the route or fallback to the controller FQCN if the 54 | * route is anonymous (e.g. a subrequest). 55 | * 56 | * @param Request $request The HTTP request 57 | */ 58 | protected function getRouteName(Request $request): string 59 | { 60 | $route = $request->attributes->get('_route'); 61 | 62 | if ($route instanceof Route) { 63 | $route = $route->getPath(); 64 | } 65 | 66 | if (null === $route) { 67 | $route = $request->attributes->get('_controller'); 68 | 69 | if (\is_array($route) && \is_callable($route, true)) { 70 | $route = \sprintf('%s::%s', \is_object($route[0]) ? get_debug_type($route[0]) : $route[0], $route[1]); 71 | } 72 | } 73 | 74 | return \is_string($route) ? $route : ''; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/EventListener/ConsoleListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 44 | $this->captureErrors = $captureErrors; 45 | } 46 | 47 | /** 48 | * Handles the execution of a console command by pushing a new {@see Scope}. 49 | * 50 | * @param ConsoleCommandEvent $event The event 51 | */ 52 | public function handleConsoleCommandEvent(ConsoleCommandEvent $event): void 53 | { 54 | $scope = $this->hub->pushScope(); 55 | $command = $event->getCommand(); 56 | $input = $event->getInput(); 57 | 58 | if (null !== $command && null !== $command->getName()) { 59 | $scope->setTag('console.command', $command->getName()); 60 | } 61 | 62 | if ($input instanceof ArgvInput) { 63 | $scope->setExtra('Full command', (string) $input); 64 | } 65 | } 66 | 67 | /** 68 | * Handles the termination of a console command by popping the {@see Scope}. 69 | * 70 | * @param ConsoleTerminateEvent $event The event 71 | */ 72 | public function handleConsoleTerminateEvent(ConsoleTerminateEvent $event): void 73 | { 74 | $this->hub->popScope(); 75 | } 76 | 77 | /** 78 | * Handles an error that happened while running a console command. 79 | * 80 | * @param ConsoleErrorEvent $event The event 81 | */ 82 | public function handleConsoleErrorEvent(ConsoleErrorEvent $event): void 83 | { 84 | $this->hub->configureScope(function (Scope $scope) use ($event): void { 85 | $scope->setTag('console.command.exit_code', (string) $event->getExitCode()); 86 | 87 | if ($this->captureErrors) { 88 | $hint = EventHint::fromArray([ 89 | 'exception' => $event->getError(), 90 | 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false), 91 | ]); 92 | 93 | $this->hub->captureEvent(Event::createEvent(), $hint); 94 | } 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/EventListener/ErrorListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 31 | } 32 | 33 | /** 34 | * Handles an exception that happened while running the application. 35 | * 36 | * @param ExceptionEvent $event The event 37 | */ 38 | public function handleExceptionEvent(ExceptionEvent $event): void 39 | { 40 | $hint = EventHint::fromArray([ 41 | 'exception' => $event->getThrowable(), 42 | 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false), 43 | ]); 44 | 45 | $this->hub->captureEvent(Event::createEvent(), $hint); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/EventListener/KernelEventForwardCompatibilityTrait.php: -------------------------------------------------------------------------------- 1 | isMainRequest() 20 | : $event->isMasterRequest() 21 | ; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/EventListener/LoginListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 41 | $this->tokenStorage = $tokenStorage; 42 | } 43 | 44 | /** 45 | * This method is called for each request handled by the framework and 46 | * fills the Sentry scope with information about the current user. 47 | */ 48 | public function handleKernelRequestEvent(RequestEvent $event): void 49 | { 50 | if ( 51 | null === $this->tokenStorage 52 | || !$this->isMainRequest($event) 53 | || $event->getRequest()->attributes->get('_stateless') 54 | ) { 55 | return; 56 | } 57 | 58 | $token = $this->tokenStorage->getToken(); 59 | 60 | if (null !== $token) { 61 | $this->updateUserContext($token); 62 | } 63 | } 64 | 65 | /** 66 | * This method is called after authentication was fully successful. It allows 67 | * to set information like the username of the currently authenticated user 68 | * and of the impersonator, if any, on the Sentry's context. 69 | */ 70 | public function handleLoginSuccessEvent(LoginSuccessEvent $event): void 71 | { 72 | $this->updateUserContext($event->getAuthenticatedToken()); 73 | } 74 | 75 | /** 76 | * This method is called when an authentication provider authenticates the 77 | * user. It is the event closest to {@see LoginSuccessEvent} in versions of 78 | * the framework where it doesn't exist. 79 | */ 80 | public function handleAuthenticationSuccessEvent(AuthenticationSuccessEvent $event): void 81 | { 82 | $this->updateUserContext($event->getAuthenticationToken()); 83 | } 84 | 85 | private function updateUserContext(TokenInterface $token): void 86 | { 87 | if (!$this->isTokenAuthenticated($token)) { 88 | return; 89 | } 90 | 91 | $client = $this->hub->getClient(); 92 | 93 | if (null === $client || !$client->getOptions()->shouldSendDefaultPii()) { 94 | return; 95 | } 96 | 97 | $this->hub->configureScope(function (Scope $scope) use ($token): void { 98 | $user = $scope->getUser() ?? new UserDataBag(); 99 | 100 | if (null === $user->getId()) { 101 | $user->setId($this->getUserIdentifier($token->getUser())); 102 | } 103 | 104 | $impersonatorUser = $this->getImpersonatorUser($token); 105 | 106 | if (null !== $impersonatorUser) { 107 | $user->setMetadata('impersonator_username', $impersonatorUser); 108 | } 109 | 110 | $scope->setUser($user); 111 | }); 112 | } 113 | 114 | private function isTokenAuthenticated(TokenInterface $token): bool 115 | { 116 | if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated(false)) { 117 | return false; 118 | } 119 | 120 | return null !== $token->getUser(); 121 | } 122 | 123 | /** 124 | * @param UserInterface|\Stringable|string|null $user 125 | */ 126 | private function getUserIdentifier($user): ?string 127 | { 128 | if ($user instanceof UserInterface) { 129 | if (method_exists($user, 'getUserIdentifier')) { 130 | return $user->getUserIdentifier(); 131 | } 132 | 133 | if (method_exists($user, 'getUsername')) { 134 | return $user->getUsername(); 135 | } 136 | } 137 | 138 | if (\is_string($user)) { 139 | return $user; 140 | } 141 | 142 | if (\is_object($user) && method_exists($user, '__toString')) { 143 | return (string) $user; 144 | } 145 | 146 | return null; 147 | } 148 | 149 | private function getImpersonatorUser(TokenInterface $token): ?string 150 | { 151 | if ($token instanceof SwitchUserToken) { 152 | return $this->getUserIdentifier($token->getOriginalToken()->getUser()); 153 | } 154 | 155 | return null; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/EventListener/MessengerListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 41 | $this->captureSoftFails = $captureSoftFails; 42 | } 43 | 44 | /** 45 | * This method is called for each message that failed to be handled. 46 | * 47 | * @param WorkerMessageFailedEvent $event The event 48 | */ 49 | public function handleWorkerMessageFailedEvent(WorkerMessageFailedEvent $event): void 50 | { 51 | if (!$this->captureSoftFails && $event->willRetry()) { 52 | return; 53 | } 54 | 55 | $this->hub->withScope(function (Scope $scope) use ($event): void { 56 | $envelope = $event->getEnvelope(); 57 | $exception = $event->getThrowable(); 58 | 59 | $scope->setTag('messenger.receiver_name', $event->getReceiverName()); 60 | $scope->setTag('messenger.message_class', \get_class($envelope->getMessage())); 61 | 62 | /** @var BusNameStamp|null $messageBusStamp */ 63 | $messageBusStamp = $envelope->last(BusNameStamp::class); 64 | 65 | if (null !== $messageBusStamp) { 66 | $scope->setTag('messenger.message_bus', $messageBusStamp->getBusName()); 67 | } 68 | 69 | $this->captureException($exception, $event->willRetry()); 70 | }); 71 | 72 | $this->flushClient(); 73 | } 74 | 75 | /** 76 | * This method is called for each handled message. 77 | * 78 | * @param WorkerMessageHandledEvent $event The event 79 | */ 80 | public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event): void 81 | { 82 | // Flush normally happens at shutdown... which only happens in the worker if it is run with a lifecycle limit 83 | // such as --time=X or --limit=Y. Flush immediately in a background worker. 84 | $this->flushClient(); 85 | } 86 | 87 | /** 88 | * Creates Sentry events from the given exception. 89 | * 90 | * Unpacks multiple exceptions wrapped in a HandlerFailedException and notifies 91 | * Sentry of each individual exception. 92 | * 93 | * If the message will be retried the exceptions will be marked as handled 94 | * in Sentry. 95 | */ 96 | private function captureException(\Throwable $exception, bool $willRetry): void 97 | { 98 | if ($exception instanceof WrappedExceptionsInterface) { 99 | $exception = $exception->getWrappedExceptions(); 100 | } elseif ($exception instanceof HandlerFailedException && method_exists($exception, 'getNestedExceptions')) { 101 | $exception = $exception->getNestedExceptions(); 102 | } elseif ($exception instanceof DelayedMessageHandlingException && method_exists($exception, 'getExceptions')) { 103 | $exception = $exception->getExceptions(); 104 | } 105 | 106 | if (\is_array($exception)) { 107 | foreach ($exception as $nestedException) { 108 | $this->captureException($nestedException, $willRetry); 109 | } 110 | 111 | return; 112 | } 113 | 114 | $hint = EventHint::fromArray([ 115 | 'exception' => $exception, 116 | 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, $willRetry), 117 | ]); 118 | 119 | $this->hub->captureEvent(Event::createEvent(), $hint); 120 | } 121 | 122 | private function flushClient(): void 123 | { 124 | $client = $this->hub->getClient(); 125 | 126 | if (null !== $client) { 127 | $client->flush(); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/EventListener/RequestListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 35 | } 36 | 37 | /** 38 | * This method is called for each request handled by the framework and 39 | * fills the Sentry scope with information about the current user. 40 | * 41 | * @param RequestEvent $event The event 42 | */ 43 | public function handleKernelRequestEvent(RequestEvent $event): void 44 | { 45 | if (!$this->isMainRequest($event)) { 46 | return; 47 | } 48 | 49 | $client = $this->hub->getClient(); 50 | 51 | if (null === $client || !$client->getOptions()->shouldSendDefaultPii()) { 52 | return; 53 | } 54 | 55 | $this->hub->configureScope(static function (Scope $scope) use ($event): void { 56 | $user = $scope->getUser() ?? new UserDataBag(); 57 | 58 | if (null === $user->getIpAddress()) { 59 | $user->setIpAddress($event->getRequest()->getClientIp()); 60 | } 61 | 62 | $scope->setUser($user); 63 | }); 64 | } 65 | 66 | /** 67 | * This method is called for each request handled by the framework and 68 | * sets the route on the current Sentry scope. 69 | * 70 | * @param ControllerEvent $event The event 71 | */ 72 | public function handleKernelControllerEvent(ControllerEvent $event): void 73 | { 74 | if (!$this->isMainRequest($event)) { 75 | return; 76 | } 77 | 78 | $route = $event->getRequest()->attributes->get('_route'); 79 | 80 | if (!\is_string($route)) { 81 | return; 82 | } 83 | 84 | $this->hub->configureScope(static function (Scope $scope) use ($route): void { 85 | $scope->setTag('route', $route); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/EventListener/SubRequestListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 32 | } 33 | 34 | /** 35 | * This method is called for each subrequest handled by the framework and 36 | * pushes a new {@see \Sentry\State\Scope} onto the stack. 37 | * 38 | * @param RequestEvent $event The event 39 | */ 40 | public function handleKernelRequestEvent(RequestEvent $event): void 41 | { 42 | if ($this->isMainRequest($event)) { 43 | return; 44 | } 45 | 46 | $this->hub->pushScope(); 47 | } 48 | 49 | /** 50 | * This method is called for each subrequest handled by the framework and 51 | * pops a {@see \Sentry\State\Scope} from the stack. 52 | * 53 | * @param FinishRequestEvent $event The event 54 | */ 55 | public function handleKernelFinishRequestEvent(FinishRequestEvent $event): void 56 | { 57 | if ($this->isMainRequest($event)) { 58 | return; 59 | } 60 | 61 | $this->hub->popScope(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/EventListener/TracingConsoleListener.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 43 | $this->excludedCommands = $excludedCommands; 44 | } 45 | 46 | /** 47 | * Handles the execution of a console command by starting a new {@see Transaction} 48 | * if it doesn't exists, or a child {@see Span} if it does. 49 | * 50 | * @param ConsoleCommandEvent $event The event 51 | */ 52 | public function handleConsoleCommandEvent(ConsoleCommandEvent $event): void 53 | { 54 | $command = $event->getCommand(); 55 | 56 | if ($this->isCommandExcluded($command)) { 57 | return; 58 | } 59 | 60 | $currentSpan = $this->hub->getSpan(); 61 | 62 | if (null === $currentSpan) { 63 | $span = $this->hub->startTransaction( 64 | TransactionContext::make() 65 | ->setOp('console.command') 66 | ->setOrigin('auto.console') 67 | ->setName($this->getSpanName($command)) 68 | ->setSource(TransactionSource::task()) 69 | ); 70 | } else { 71 | $span = $currentSpan->startChild( 72 | SpanContext::make() 73 | ->setOp('console.command') 74 | ->setOrigin('auto.console') 75 | ->setDescription($this->getSpanName($command)) 76 | ); 77 | } 78 | 79 | $this->hub->setSpan($span); 80 | } 81 | 82 | /** 83 | * Handles the termination of a console command by stopping the active {@see Span} 84 | * or {@see Transaction}. 85 | * 86 | * @param ConsoleTerminateEvent $event The event 87 | */ 88 | public function handleConsoleTerminateEvent(ConsoleTerminateEvent $event): void 89 | { 90 | if ($this->isCommandExcluded($event->getCommand())) { 91 | return; 92 | } 93 | 94 | $span = $this->hub->getSpan(); 95 | 96 | if (null !== $span) { 97 | $span->setStatus(0 === $event->getExitCode() ? SpanStatus::ok() : SpanStatus::internalError()); 98 | $span->finish(); 99 | } 100 | } 101 | 102 | private function getSpanName(?Command $command): string 103 | { 104 | if (null === $command || null === $command->getName()) { 105 | return ''; 106 | } 107 | 108 | return $command->getName(); 109 | } 110 | 111 | private function isCommandExcluded(?Command $command): bool 112 | { 113 | if (null === $command) { 114 | return true; 115 | } 116 | 117 | return \in_array($command->getName(), $this->excludedCommands, true); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/EventListener/TracingRequestListener.php: -------------------------------------------------------------------------------- 1 | requestFetcher = $requestFetcher; 36 | } 37 | 38 | /** 39 | * This method is called for each subrequest handled by the framework and 40 | * starts a new {@see Transaction}. 41 | * 42 | * @param RequestEvent $event The event 43 | */ 44 | public function handleKernelRequestEvent(RequestEvent $event): void 45 | { 46 | if (!$this->isMainRequest($event)) { 47 | return; 48 | } 49 | 50 | /** @var Request $request */ 51 | $request = $event->getRequest(); 52 | 53 | if ($this->requestFetcher instanceof RequestFetcher) { 54 | $this->requestFetcher->setRequest($request); 55 | } 56 | 57 | /** @var float $requestStartTime */ 58 | $requestStartTime = $request->server->get('REQUEST_TIME_FLOAT', microtime(true)); 59 | 60 | $context = continueTrace( 61 | $request->headers->get('sentry-trace') ?? $request->headers->get('traceparent', ''), 62 | $request->headers->get('baggage', '') 63 | ); 64 | 65 | $context->setOp('http.server'); 66 | $context->setOrigin('auto.http.server'); 67 | 68 | $routeName = $request->attributes->get('_route'); 69 | if (null !== $routeName && \is_string($routeName)) { 70 | $context->setName(\sprintf('%s %s', $request->getMethod(), $routeName)); 71 | $context->setSource(TransactionSource::route()); 72 | } else { 73 | $context->setName(\sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo())); 74 | $context->setSource(TransactionSource::url()); 75 | } 76 | 77 | $context->setStartTimestamp($requestStartTime); 78 | $context->setData($this->getData($request)); 79 | 80 | $this->hub->setSpan($this->hub->startTransaction($context)); 81 | } 82 | 83 | /** 84 | * This method is called for each request handled by the framework and 85 | * ends the tracing on terminate after the client received the response. 86 | * 87 | * @param TerminateEvent $event The event 88 | */ 89 | public function handleKernelTerminateEvent(TerminateEvent $event): void 90 | { 91 | $transaction = $this->hub->getTransaction(); 92 | 93 | if (null === $transaction) { 94 | return; 95 | } 96 | 97 | $transaction->finish(); 98 | metrics()->flush(); 99 | 100 | if ($this->requestFetcher instanceof RequestFetcher) { 101 | $this->requestFetcher->setRequest(null); 102 | } 103 | } 104 | 105 | /** 106 | * Gets the data to attach to the transaction. 107 | * 108 | * @param Request $request The HTTP request 109 | * 110 | * @return array 111 | */ 112 | private function getData(Request $request): array 113 | { 114 | $client = $this->hub->getClient(); 115 | $httpFlavor = $this->getHttpFlavor($request); 116 | 117 | $data = [ 118 | 'net.host.port' => (string) $request->getPort(), 119 | 'http.request.method' => $request->getMethod(), 120 | 'http.url' => $request->getUri(), 121 | 'route' => $this->getRouteName($request), 122 | ]; 123 | 124 | if (null !== $httpFlavor) { 125 | $data['http.flavor'] = $httpFlavor; 126 | } 127 | 128 | if (false !== filter_var($request->getHost(), \FILTER_VALIDATE_IP)) { 129 | $data['net.host.ip'] = $request->getHost(); 130 | } else { 131 | $data['net.host.name'] = $request->getHost(); 132 | } 133 | 134 | if (null !== $request->getClientIp() && null !== $client && $client->getOptions()->shouldSendDefaultPii()) { 135 | $data['net.peer.ip'] = $request->getClientIp(); 136 | } 137 | 138 | return $data; 139 | } 140 | 141 | /** 142 | * Gets the HTTP flavor from the request. 143 | * 144 | * @param Request $request The HTTP request 145 | */ 146 | private function getHttpFlavor(Request $request): ?string 147 | { 148 | $protocolVersion = $request->getProtocolVersion(); 149 | 150 | if (null !== $protocolVersion && str_starts_with($protocolVersion, 'HTTP/')) { 151 | return substr($protocolVersion, \strlen('HTTP/')); 152 | } 153 | 154 | return $protocolVersion; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/EventListener/TracingSubRequestListener.php: -------------------------------------------------------------------------------- 1 | isMainRequest($event)) { 27 | return; 28 | } 29 | 30 | $request = $event->getRequest(); 31 | $span = $this->hub->getSpan(); 32 | 33 | if (null === $span) { 34 | return; 35 | } 36 | 37 | $this->hub->setSpan( 38 | $span->startChild( 39 | SpanContext::make() 40 | ->setOp('http.server') 41 | ->setData([ 42 | 'http.request.method' => $request->getMethod(), 43 | 'http.url' => $request->getUri(), 44 | 'route' => $this->getRouteName($request), 45 | ]) 46 | ->setOrigin('auto.http.server') 47 | ->setDescription(\sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo())) 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * This method is called for each subrequest handled by the framework and 54 | * ends the tracing. 55 | * 56 | * @param FinishRequestEvent $event The event 57 | */ 58 | public function handleKernelFinishRequestEvent(FinishRequestEvent $event): void 59 | { 60 | if ($this->isMainRequest($event)) { 61 | return; 62 | } 63 | 64 | $span = $this->hub->getSpan(); 65 | 66 | if (null === $span) { 67 | return; 68 | } 69 | 70 | $span->finish(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Integration/IntegrationConfigurator.php: -------------------------------------------------------------------------------- 1 | true, 19 | ExceptionListenerIntegration::class => true, 20 | FatalErrorListenerIntegration::class => true, 21 | ]; 22 | 23 | /** 24 | * @var IntegrationInterface[] 25 | */ 26 | private $userIntegrations; 27 | 28 | /** 29 | * @var bool 30 | */ 31 | private $registerErrorHandler; 32 | 33 | /** 34 | * @param IntegrationInterface[] $userIntegrations 35 | */ 36 | public function __construct(array $userIntegrations, bool $registerErrorHandler) 37 | { 38 | $this->userIntegrations = $userIntegrations; 39 | $this->registerErrorHandler = $registerErrorHandler; 40 | } 41 | 42 | /** 43 | * @see IntegrationRegistry::getIntegrationsToSetup() 44 | * 45 | * @param IntegrationInterface[] $defaultIntegrations 46 | * 47 | * @return IntegrationInterface[] 48 | */ 49 | public function __invoke(array $defaultIntegrations): array 50 | { 51 | $integrations = []; 52 | 53 | $userIntegrationsClasses = array_map('get_class', $this->userIntegrations); 54 | $pickedIntegrationsClasses = []; 55 | 56 | foreach ($defaultIntegrations as $defaultIntegration) { 57 | $integrationClassName = \get_class($defaultIntegration); 58 | 59 | if (!$this->registerErrorHandler && isset(self::ERROR_HANDLER_INTEGRATIONS[$integrationClassName])) { 60 | continue; 61 | } 62 | 63 | if (!\in_array($integrationClassName, $userIntegrationsClasses, true) && !isset($pickedIntegrationsClasses[$integrationClassName])) { 64 | $integrations[] = $defaultIntegration; 65 | $pickedIntegrationsClasses[$integrationClassName] = true; 66 | } 67 | } 68 | 69 | foreach ($this->userIntegrations as $userIntegration) { 70 | $integrationClassName = \get_class($userIntegration); 71 | 72 | if (!isset($pickedIntegrationsClasses[$integrationClassName])) { 73 | $integrations[] = $userIntegration; 74 | $pickedIntegrationsClasses[$integrationClassName] = true; 75 | } 76 | } 77 | 78 | return $integrations; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Integration/RequestFetcher.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 46 | $this->httpMessageFactory = $httpMessageFactory ?? new PsrHttpFactory( 47 | new HttpFactory(), 48 | new HttpFactory(), 49 | new HttpFactory(), 50 | new HttpFactory() 51 | ); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function fetchRequest(): ?ServerRequestInterface 58 | { 59 | $request = $this->currentRequest ?? $this->requestStack->getCurrentRequest(); 60 | 61 | if (null === $request) { 62 | return null; 63 | } 64 | 65 | try { 66 | return $this->httpMessageFactory->createRequest($request); 67 | } catch (\Throwable $exception) { 68 | return null; 69 | } 70 | } 71 | 72 | public function setRequest(?Request $request): void 73 | { 74 | $this->currentRequest = $request; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Resources/config/schema/sentry-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/SentryBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new DbalTracingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); 26 | $container->addCompilerPass(new CacheTracingPass()); 27 | $container->addCompilerPass(new HttpClientTracingPass()); 28 | $container->addCompilerPass(new AddLoginListenerTagPass()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tracing/Cache/TraceableCacheAdapterForV2.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | use TraceableCacheAdapterTrait; 25 | 26 | /** 27 | * @param HubInterface $hub The current hub 28 | * @param AdapterInterface $decoratedAdapter The decorated cache adapter 29 | */ 30 | public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapter) 31 | { 32 | $this->hub = $hub; 33 | $this->decoratedAdapter = $decoratedAdapter; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | * 39 | * @param mixed[] $metadata 40 | * 41 | * @return mixed 42 | */ 43 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) 44 | { 45 | return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { 46 | if (!$this->decoratedAdapter instanceof CacheInterface) { 47 | 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)); 48 | } 49 | 50 | return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); 51 | }, $key); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tracing/Cache/TraceableCacheAdapterForV3.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | use TraceableCacheAdapterTrait; 25 | 26 | /** 27 | * @param HubInterface $hub The current hub 28 | * @param AdapterInterface $decoratedAdapter The decorated cache adapter 29 | */ 30 | public function __construct(HubInterface $hub, AdapterInterface $decoratedAdapter) 31 | { 32 | $this->hub = $hub; 33 | $this->decoratedAdapter = $decoratedAdapter; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | * 39 | * @param mixed[] $metadata 40 | */ 41 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 42 | { 43 | return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { 44 | if (!$this->decoratedAdapter instanceof CacheInterface) { 45 | 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)); 46 | } 47 | 48 | return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); 49 | }, $key); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Tracing/Cache/TraceableCacheAdapterTrait.php: -------------------------------------------------------------------------------- 1 | traceFunction('cache.get_item', function () use ($key): CacheItem { 42 | return $this->decoratedAdapter->getItem($key); 43 | }, $key); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getItems(array $keys = []): iterable 50 | { 51 | return $this->traceFunction('cache.get_items', function () use ($keys): iterable { 52 | return $this->decoratedAdapter->getItems($keys); 53 | }); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function clear(string $prefix = ''): bool 60 | { 61 | return $this->traceFunction('cache.clear', function () use ($prefix): bool { 62 | return $this->decoratedAdapter->clear($prefix); 63 | }, $prefix); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function delete(string $key): bool 70 | { 71 | return $this->traceFunction('cache.delete_item', function () use ($key): bool { 72 | if (!$this->decoratedAdapter instanceof CacheInterface) { 73 | 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)); 74 | } 75 | 76 | return $this->decoratedAdapter->delete($key); 77 | }, $key); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function hasItem($key): bool 84 | { 85 | return $this->traceFunction('cache.has_item', function () use ($key): bool { 86 | return $this->decoratedAdapter->hasItem($key); 87 | }, $key); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function deleteItem($key): bool 94 | { 95 | return $this->traceFunction('cache.delete_item', function () use ($key): bool { 96 | return $this->decoratedAdapter->deleteItem($key); 97 | }, $key); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function deleteItems(array $keys): bool 104 | { 105 | return $this->traceFunction('cache.delete_items', function () use ($keys): bool { 106 | return $this->decoratedAdapter->deleteItems($keys); 107 | }); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function save(CacheItemInterface $item): bool 114 | { 115 | return $this->traceFunction('cache.save', function () use ($item): bool { 116 | return $this->decoratedAdapter->save($item); 117 | }); 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function saveDeferred(CacheItemInterface $item): bool 124 | { 125 | return $this->traceFunction('cache.save_deferred', function () use ($item): bool { 126 | return $this->decoratedAdapter->saveDeferred($item); 127 | }); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function commit(): bool 134 | { 135 | return $this->traceFunction('cache.commit', function (): bool { 136 | return $this->decoratedAdapter->commit(); 137 | }); 138 | } 139 | 140 | /** 141 | * {@inheritdoc} 142 | */ 143 | public function prune(): bool 144 | { 145 | return $this->traceFunction('cache.prune', function (): bool { 146 | if (!$this->decoratedAdapter instanceof PruneableInterface) { 147 | return false; 148 | } 149 | 150 | return $this->decoratedAdapter->prune(); 151 | }); 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function reset(): void 158 | { 159 | if ($this->decoratedAdapter instanceof ResettableInterface) { 160 | $this->decoratedAdapter->reset(); 161 | } 162 | } 163 | 164 | /** 165 | * @phpstan-template TResult 166 | * 167 | * @phpstan-param \Closure(): TResult $callback 168 | * 169 | * @phpstan-return TResult 170 | */ 171 | private function traceFunction(string $spanOperation, \Closure $callback, ?string $spanDescription = null) 172 | { 173 | $span = $this->hub->getSpan(); 174 | 175 | if (null !== $span) { 176 | $spanContext = SpanContext::make() 177 | ->setOp($spanOperation) 178 | ->setOrigin('auto.cache'); 179 | 180 | if (null !== $spanDescription) { 181 | $spanContext->setDescription(urldecode($spanDescription)); 182 | } 183 | 184 | $span = $span->startChild($spanContext); 185 | } 186 | 187 | try { 188 | return $callback(); 189 | } finally { 190 | if (null !== $span) { 191 | $span->finish(); 192 | } 193 | } 194 | } 195 | 196 | /** 197 | * @phpstan-param \Closure(CacheItem): CacheItem $callback 198 | * @phpstan-param string $key 199 | * 200 | * @phpstan-return callable(): CacheItem 201 | */ 202 | private function setCallbackWrapper(callable $callback, string $key): callable 203 | { 204 | return function () use ($callback, $key): CacheItem { 205 | return $callback($this->decoratedAdapter->getItem($key)); 206 | }; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/Tracing/Cache/TraceableTagAwareCacheAdapterForV2.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | use TraceableCacheAdapterTrait; 26 | 27 | /** 28 | * @param HubInterface $hub The current hub 29 | * @param TagAwareAdapterInterface $decoratedAdapter The decorated cache adapter 30 | */ 31 | public function __construct(HubInterface $hub, TagAwareAdapterInterface $decoratedAdapter) 32 | { 33 | $this->hub = $hub; 34 | $this->decoratedAdapter = $decoratedAdapter; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | * 40 | * @param mixed[] $metadata 41 | * 42 | * @return mixed 43 | */ 44 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null) 45 | { 46 | return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { 47 | if (!$this->decoratedAdapter instanceof CacheInterface) { 48 | 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)); 49 | } 50 | 51 | return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); 52 | }, $key); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function invalidateTags(array $tags): bool 59 | { 60 | return $this->traceFunction('cache.invalidate_tags', function () use ($tags): bool { 61 | return $this->decoratedAdapter->invalidateTags($tags); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Tracing/Cache/TraceableTagAwareCacheAdapterForV3.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | use TraceableCacheAdapterTrait; 26 | 27 | /** 28 | * @param HubInterface $hub The current hub 29 | * @param TagAwareAdapterInterface $decoratedAdapter The decorated cache adapter 30 | */ 31 | public function __construct(HubInterface $hub, TagAwareAdapterInterface $decoratedAdapter) 32 | { 33 | $this->hub = $hub; 34 | $this->decoratedAdapter = $decoratedAdapter; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | * 40 | * @param mixed[] $metadata 41 | */ 42 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 43 | { 44 | return $this->traceFunction('cache.get_item', function () use ($key, $callback, $beta, &$metadata) { 45 | if (!$this->decoratedAdapter instanceof CacheInterface) { 46 | 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)); 47 | } 48 | 49 | return $this->decoratedAdapter->get($key, $callback, $beta, $metadata); 50 | }, $key); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function invalidateTags(array $tags): bool 57 | { 58 | return $this->traceFunction('cache.invalidate_tags', function () use ($tags): bool { 59 | return $this->decoratedAdapter->invalidateTags($tags); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/AbstractTracingStatement.php: -------------------------------------------------------------------------------- 1 | The span data 36 | */ 37 | protected $spanData; 38 | 39 | /** 40 | * Constructor. 41 | * 42 | * @param HubInterface $hub The current hub 43 | * @param Statement $decoratedStatement The decorated statement 44 | * @param string $sqlQuery The SQL query executed by the decorated statement 45 | * @param array $spanData The span data 46 | */ 47 | public function __construct(HubInterface $hub, Statement $decoratedStatement, string $sqlQuery, array $spanData) 48 | { 49 | $this->hub = $hub; 50 | $this->decoratedStatement = $decoratedStatement; 51 | $this->sqlQuery = $sqlQuery; 52 | $this->spanData = $spanData; 53 | } 54 | 55 | /** 56 | * Calls the given callback by passing to it the specified arguments and 57 | * wrapping its execution into a child {@see Span} of the current one. 58 | * 59 | * @param callable $callback The function to call 60 | * @param mixed ...$args The arguments to pass to the callback 61 | * 62 | * @phpstan-template T 63 | * 64 | * @phpstan-param callable(mixed...): T $callback 65 | * 66 | * @phpstan-return T 67 | */ 68 | protected function traceFunction(SpanContext $spanContext, callable $callback, ...$args) 69 | { 70 | $span = $this->hub->getSpan(); 71 | 72 | if (null !== $span) { 73 | $span = $span->startChild($spanContext); 74 | } 75 | 76 | try { 77 | return $callback(...$args); 78 | } finally { 79 | if (null !== $span) { 80 | $span->finish(); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/Compatibility/MiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | tracingDriverMiddleware = $tracingDriverMiddleware; 27 | } 28 | 29 | /** 30 | * Configures the given connection by wrapping its driver into an instance 31 | * of the {@see TracingDriver} class. This is done using the reflection, 32 | * and as such should be limited only to the versions of Doctrine DBAL that 33 | * are lower than 3.0. Since 3.0 onwards, the concept of driver middlewares 34 | * has been introduced which allows the same thing we're doing here, but in 35 | * a more appropriate and "legal" way. 36 | * 37 | * @param Connection $connection The connection to configure 38 | */ 39 | public function configure(Connection $connection): void 40 | { 41 | $reflectionProperty = new \ReflectionProperty($connection, '_driver'); 42 | $reflectionProperty->setAccessible(true); 43 | $reflectionProperty->setValue($connection, $this->tracingDriverMiddleware->wrap($reflectionProperty->getValue($connection))); 44 | $reflectionProperty->setAccessible(false); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV2V3.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function create(Connection $connection, AbstractPlatform $databasePlatform, array $params): TracingDriverConnectionInterface 42 | { 43 | $tracingDriverConnection = new TracingDriverConnection( 44 | $this->hub, 45 | $connection, 46 | $this->getDatabasePlatform($databasePlatform), 47 | $params 48 | ); 49 | 50 | if ($connection instanceof ServerInfoAwareConnection) { 51 | $tracingDriverConnection = new TracingServerInfoAwareDriverConnection($tracingDriverConnection); 52 | } 53 | 54 | return $tracingDriverConnection; 55 | } 56 | 57 | private function getDatabasePlatform(AbstractPlatform $databasePlatform): string 58 | { 59 | // https://github.com/open-telemetry/opentelemetry-specification/blob/33113489fb5a1b6da563abb4ffa541447b87f515/specification/trace/semantic_conventions/database.md#connection-level-attributes 60 | switch (true) { 61 | case $databasePlatform instanceof AbstractMySQLPlatform: 62 | return 'mysql'; 63 | 64 | case $databasePlatform instanceof DB2Platform: 65 | return 'db2'; 66 | 67 | case $databasePlatform instanceof OraclePlatform: 68 | return 'oracle'; 69 | 70 | case $databasePlatform instanceof PostgreSQLPlatform: 71 | return 'postgresql'; 72 | 73 | case $databasePlatform instanceof SqlitePlatform: 74 | return 'sqlite'; 75 | 76 | case $databasePlatform instanceof SQLServerPlatform: 77 | return 'mssql'; 78 | 79 | default: 80 | return 'other_sql'; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryForV4.php: -------------------------------------------------------------------------------- 1 | hub = $hub; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function create(Connection $connection, AbstractPlatform $databasePlatform, array $params): TracingDriverConnectionInterface 41 | { 42 | $tracingDriverConnection = new TracingDriverConnection( 43 | $this->hub, 44 | $connection, 45 | $this->getDatabasePlatform($databasePlatform), 46 | $params 47 | ); 48 | 49 | return $tracingDriverConnection; 50 | } 51 | 52 | private function getDatabasePlatform(AbstractPlatform $databasePlatform): string 53 | { 54 | // https://github.com/open-telemetry/opentelemetry-specification/blob/33113489fb5a1b6da563abb4ffa541447b87f515/specification/trace/semantic_conventions/database.md#connection-level-attributes 55 | switch (true) { 56 | case $databasePlatform instanceof AbstractMySQLPlatform: 57 | return 'mysql'; 58 | 59 | case $databasePlatform instanceof DB2Platform: 60 | return 'db2'; 61 | 62 | case $databasePlatform instanceof OraclePlatform: 63 | return 'oracle'; 64 | 65 | case $databasePlatform instanceof PostgreSQLPlatform: 66 | return 'postgresql'; 67 | 68 | case $databasePlatform instanceof SQLitePlatform: 69 | return 'sqlite'; 70 | 71 | case $databasePlatform instanceof SQLServerPlatform: 72 | return 'mssql'; 73 | 74 | default: 75 | return 'other_sql'; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverConnectionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | $params The params of the connection 26 | * 27 | * @phpstan-param ConnectionParams $params 28 | */ 29 | public function create(Connection $connection, AbstractPlatform $databasePlatform, array $params): TracingDriverConnectionInterface; 30 | } 31 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV2V3.php: -------------------------------------------------------------------------------- 1 | = 2.10 and >= 3.3. 18 | * 19 | * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams 20 | */ 21 | final class TracingDriverConnectionForV2V3 implements TracingDriverConnectionInterface 22 | { 23 | /** 24 | * @internal 25 | */ 26 | public const SPAN_OP_CONN_PREPARE = 'db.sql.prepare'; 27 | 28 | /** 29 | * @internal 30 | */ 31 | public const SPAN_OP_CONN_QUERY = 'db.sql.query'; 32 | 33 | /** 34 | * @internal 35 | */ 36 | public const SPAN_OP_CONN_EXEC = 'db.sql.exec'; 37 | 38 | /** 39 | * @internal 40 | */ 41 | public const SPAN_OP_CONN_BEGIN_TRANSACTION = 'db.sql.transaction.begin'; 42 | 43 | /** 44 | * @internal 45 | */ 46 | public const SPAN_OP_TRANSACTION_COMMIT = 'db.sql.transaction.commit'; 47 | 48 | /** 49 | * @internal 50 | */ 51 | public const SPAN_OP_TRANSACTION_ROLLBACK = 'db.sql.transaction.rollback'; 52 | 53 | /** 54 | * @var HubInterface The current hub 55 | */ 56 | private $hub; 57 | 58 | /** 59 | * @var DriverConnectionInterface The decorated connection 60 | */ 61 | private $decoratedConnection; 62 | 63 | /** 64 | * @var array The data to attach to the span 65 | */ 66 | private $spanData; 67 | 68 | /** 69 | * Constructor. 70 | * 71 | * @param HubInterface $hub The current hub 72 | * @param DriverConnectionInterface $decoratedConnection The connection to decorate 73 | * @param string $databasePlatform The name of the database platform 74 | * @param array $params The connection params 75 | * 76 | * @phpstan-param ConnectionParams $params 77 | */ 78 | public function __construct( 79 | HubInterface $hub, 80 | DriverConnectionInterface $decoratedConnection, 81 | string $databasePlatform, 82 | array $params 83 | ) { 84 | $this->hub = $hub; 85 | $this->decoratedConnection = $decoratedConnection; 86 | $this->spanData = $this->getSpanData($databasePlatform, $params); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function prepare($sql): Statement 93 | { 94 | $statement = $this->traceFunction(self::SPAN_OP_CONN_PREPARE, $sql, function () use ($sql): Statement { 95 | return $this->decoratedConnection->prepare($sql); 96 | }); 97 | 98 | return new TracingStatement($this->hub, $statement, $sql, $this->spanData); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function query(?string $sql = null, ...$args): Result 105 | { 106 | return $this->traceFunction(self::SPAN_OP_CONN_QUERY, $sql, function () use ($sql, $args): Result { 107 | return $this->decoratedConnection->query($sql, ...$args); 108 | }); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | * 114 | * @return mixed 115 | */ 116 | public function quote($value, $type = ParameterType::STRING) 117 | { 118 | return $this->decoratedConnection->quote($value, $type); 119 | } 120 | 121 | /** 122 | * {@inheritdoc} 123 | */ 124 | public function exec($sql): int 125 | { 126 | return $this->traceFunction(self::SPAN_OP_CONN_EXEC, $sql, function () use ($sql): int { 127 | return $this->decoratedConnection->exec($sql); 128 | }); 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | * 134 | * @return string|int|false 135 | */ 136 | public function lastInsertId($name = null) 137 | { 138 | return $this->decoratedConnection->lastInsertId($name); 139 | } 140 | 141 | /** 142 | * {@inheritdoc} 143 | */ 144 | public function beginTransaction(): bool 145 | { 146 | return $this->traceFunction(self::SPAN_OP_CONN_BEGIN_TRANSACTION, 'BEGIN TRANSACTION', function (): bool { 147 | return $this->decoratedConnection->beginTransaction(); 148 | }); 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function commit(): bool 155 | { 156 | return $this->traceFunction(self::SPAN_OP_TRANSACTION_COMMIT, 'COMMIT', function (): bool { 157 | return $this->decoratedConnection->commit(); 158 | }); 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | public function rollBack(): bool 165 | { 166 | return $this->traceFunction(self::SPAN_OP_TRANSACTION_ROLLBACK, 'ROLLBACK', function (): bool { 167 | return $this->decoratedConnection->rollBack(); 168 | }); 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | * 174 | * @return resource|object 175 | */ 176 | public function getNativeConnection() 177 | { 178 | if (!method_exists($this->decoratedConnection, 'getNativeConnection')) { 179 | throw new \BadMethodCallException(\sprintf('The connection "%s" does not support accessing the native connection.', \get_class($this->decoratedConnection))); 180 | } 181 | 182 | return $this->decoratedConnection->getNativeConnection(); 183 | } 184 | 185 | /** 186 | * {@inheritdoc} 187 | */ 188 | public function errorCode(): ?string 189 | { 190 | if (method_exists($this->decoratedConnection, 'errorCode')) { 191 | return $this->decoratedConnection->errorCode(); 192 | } 193 | 194 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function errorInfo(): array 201 | { 202 | if (method_exists($this->decoratedConnection, 'errorInfo')) { 203 | return $this->decoratedConnection->errorInfo(); 204 | } 205 | 206 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 207 | } 208 | 209 | public function getWrappedConnection(): DriverConnectionInterface 210 | { 211 | return $this->decoratedConnection; 212 | } 213 | 214 | /** 215 | * @phpstan-template T 216 | * 217 | * @phpstan-param \Closure(): T $callback 218 | * 219 | * @phpstan-return T 220 | */ 221 | private function traceFunction(string $spanOperation, string $spanDescription, \Closure $callback) 222 | { 223 | $span = $this->hub->getSpan(); 224 | 225 | if (null !== $span) { 226 | $span = $span->startChild( 227 | SpanContext::make() 228 | ->setOp($spanOperation) 229 | ->setData($this->spanData) 230 | ->setOrigin('auto.db') 231 | ->setDescription($spanDescription) 232 | ); 233 | } 234 | 235 | try { 236 | return $callback(); 237 | } finally { 238 | if (null !== $span) { 239 | $span->finish(); 240 | } 241 | } 242 | } 243 | 244 | /** 245 | * Gets a map of key-value pairs that will be set as the span data. 246 | * 247 | * @param array $params The connection params 248 | * 249 | * @return array 250 | * 251 | * @phpstan-param ConnectionParams $params 252 | * 253 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md 254 | */ 255 | private function getSpanData(string $databasePlatform, array $params): array 256 | { 257 | $data = ['db.system' => $databasePlatform]; 258 | 259 | if (isset($params['user'])) { 260 | $data['db.user'] = $params['user']; 261 | } 262 | 263 | if (isset($params['dbname'])) { 264 | $data['db.name'] = $params['dbname']; 265 | } 266 | 267 | if (isset($params['host']) && !empty($params['host']) && !isset($params['memory'])) { 268 | if (false === filter_var($params['host'], \FILTER_VALIDATE_IP)) { 269 | $data['server.address'] = $params['host']; 270 | } else { 271 | $data['server.address'] = $params['host']; 272 | } 273 | } 274 | 275 | if (isset($params['port'])) { 276 | $data['server.port'] = (string) $params['port']; 277 | } 278 | 279 | if (isset($params['unix_socket'])) { 280 | $data['server.socket.address'] = 'Unix'; 281 | } elseif (isset($params['memory'])) { 282 | $data['server.socket.address'] = 'inproc'; 283 | } 284 | 285 | return $data; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverConnectionForV4.php: -------------------------------------------------------------------------------- 1 | = 4.0. 17 | * 18 | * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams 19 | */ 20 | final class TracingDriverConnectionForV4 implements TracingDriverConnectionInterface 21 | { 22 | /** 23 | * @internal 24 | */ 25 | public const SPAN_OP_CONN_PREPARE = 'db.sql.prepare'; 26 | 27 | /** 28 | * @internal 29 | */ 30 | public const SPAN_OP_CONN_QUERY = 'db.sql.query'; 31 | 32 | /** 33 | * @internal 34 | */ 35 | public const SPAN_OP_CONN_EXEC = 'db.sql.exec'; 36 | 37 | /** 38 | * @internal 39 | */ 40 | public const SPAN_OP_CONN_BEGIN_TRANSACTION = 'db.sql.transaction.begin'; 41 | 42 | /** 43 | * @internal 44 | */ 45 | public const SPAN_OP_TRANSACTION_COMMIT = 'db.sql.transaction.commit'; 46 | 47 | /** 48 | * @internal 49 | */ 50 | public const SPAN_OP_TRANSACTION_ROLLBACK = 'db.sql.transaction.rollback'; 51 | 52 | /** 53 | * @var HubInterface The current hub 54 | */ 55 | private $hub; 56 | 57 | /** 58 | * @var DriverConnectionInterface The decorated connection 59 | */ 60 | private $decoratedConnection; 61 | 62 | /** 63 | * @var array The data to attach to the span 64 | */ 65 | private $spanData; 66 | 67 | /** 68 | * Constructor. 69 | * 70 | * @param HubInterface $hub The current hub 71 | * @param DriverConnectionInterface $decoratedConnection The connection to decorate 72 | * @param string $databasePlatform The name of the database platform 73 | * @param array $params The connection params 74 | * 75 | * @phpstan-param ConnectionParams $params 76 | */ 77 | public function __construct( 78 | HubInterface $hub, 79 | DriverConnectionInterface $decoratedConnection, 80 | string $databasePlatform, 81 | array $params 82 | ) { 83 | $this->hub = $hub; 84 | $this->decoratedConnection = $decoratedConnection; 85 | $this->spanData = $this->getSpanData($databasePlatform, $params); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function prepare($sql): Statement 92 | { 93 | $statement = $this->traceFunction(self::SPAN_OP_CONN_PREPARE, $sql, function () use ($sql): Statement { 94 | return $this->decoratedConnection->prepare($sql); 95 | }); 96 | 97 | return new TracingStatement($this->hub, $statement, $sql, $this->spanData); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function query(?string $sql = null, ...$args): Result 104 | { 105 | return $this->traceFunction(self::SPAN_OP_CONN_QUERY, $sql, function () use ($sql, $args): Result { 106 | return $this->decoratedConnection->query($sql, ...$args); 107 | }); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function quote(string $value): string 114 | { 115 | return $this->decoratedConnection->quote($value); 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | */ 121 | public function exec(string $sql): int|string 122 | { 123 | return $this->traceFunction(self::SPAN_OP_CONN_EXEC, $sql, function () use ($sql): int|string { 124 | return $this->decoratedConnection->exec($sql); 125 | }); 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | * 131 | * @return string|int 132 | */ 133 | public function lastInsertId(): string|int 134 | { 135 | return $this->decoratedConnection->lastInsertId(); 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function beginTransaction(): void 142 | { 143 | $this->traceFunction(self::SPAN_OP_CONN_BEGIN_TRANSACTION, 'BEGIN TRANSACTION', function (): void { 144 | $this->decoratedConnection->beginTransaction(); 145 | }); 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | */ 151 | public function commit(): void 152 | { 153 | $this->traceFunction(self::SPAN_OP_TRANSACTION_COMMIT, 'COMMIT', function (): void { 154 | $this->decoratedConnection->commit(); 155 | }); 156 | } 157 | 158 | /** 159 | * {@inheritdoc} 160 | */ 161 | public function rollBack(): void 162 | { 163 | $this->traceFunction(self::SPAN_OP_TRANSACTION_ROLLBACK, 'ROLLBACK', function (): void { 164 | $this->decoratedConnection->rollBack(); 165 | }); 166 | } 167 | 168 | /** 169 | * {@inheritdoc} 170 | * 171 | * @return resource|object 172 | */ 173 | public function getNativeConnection() 174 | { 175 | return $this->decoratedConnection->getNativeConnection(); 176 | } 177 | 178 | /** 179 | * {@inheritdoc} 180 | */ 181 | public function errorCode(): ?string 182 | { 183 | if (method_exists($this->decoratedConnection, 'errorCode')) { 184 | return $this->decoratedConnection->errorCode(); 185 | } 186 | 187 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 188 | } 189 | 190 | /** 191 | * {@inheritdoc} 192 | */ 193 | public function errorInfo(): array 194 | { 195 | if (method_exists($this->decoratedConnection, 'errorInfo')) { 196 | return $this->decoratedConnection->errorInfo(); 197 | } 198 | 199 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 200 | } 201 | 202 | public function getWrappedConnection(): DriverConnectionInterface 203 | { 204 | return $this->decoratedConnection; 205 | } 206 | 207 | public function getServerVersion(): string 208 | { 209 | return $this->decoratedConnection->getServerVersion(); 210 | } 211 | 212 | /** 213 | * @phpstan-template T 214 | * 215 | * @phpstan-param \Closure(): T $callback 216 | * 217 | * @phpstan-return T 218 | */ 219 | private function traceFunction(string $spanOperation, string $spanDescription, \Closure $callback) 220 | { 221 | $span = $this->hub->getSpan(); 222 | 223 | if (null !== $span) { 224 | $span = $span->startChild( 225 | SpanContext::make() 226 | ->setOp($spanOperation) 227 | ->setData($this->spanData) 228 | ->setOrigin('auto.db') 229 | ->setDescription($spanDescription) 230 | ); 231 | } 232 | 233 | try { 234 | return $callback(); 235 | } finally { 236 | if (null !== $span) { 237 | $span->finish(); 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Gets a map of key-value pairs that will be set as the span data. 244 | * 245 | * @param array $params The connection params 246 | * 247 | * @return array 248 | * 249 | * @phpstan-param ConnectionParams $params 250 | * 251 | * @see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md 252 | */ 253 | private function getSpanData(string $databasePlatform, array $params): array 254 | { 255 | $data = ['db.system' => $databasePlatform]; 256 | 257 | if (isset($params['user'])) { 258 | $data['db.user'] = $params['user']; 259 | } 260 | 261 | if (isset($params['dbname'])) { 262 | $data['db.name'] = $params['dbname']; 263 | } 264 | 265 | if (isset($params['host']) && !empty($params['host']) && !isset($params['memory'])) { 266 | if (false === filter_var($params['host'], \FILTER_VALIDATE_IP)) { 267 | $data['server.address'] = $params['host']; 268 | } else { 269 | $data['server.address'] = $params['host']; 270 | } 271 | } 272 | 273 | if (isset($params['port'])) { 274 | $data['server.port'] = (string) $params['port']; 275 | } 276 | 277 | if (isset($params['unix_socket'])) { 278 | $data['server.socket.address'] = 'Unix'; 279 | } elseif (isset($params['memory'])) { 280 | $data['server.socket.address'] = 'inproc'; 281 | } 282 | 283 | return $data; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverConnectionInterface.php: -------------------------------------------------------------------------------- 1 | decoratedDriver = $decoratedDriver; 42 | $this->connectionFactory = $connectionFactory; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function connect(array $params, $username = null, $password = null, array $driverOptions = []): TracingDriverConnectionInterface 49 | { 50 | return $this->connectionFactory->create( 51 | $this->decoratedDriver->connect($params, $username, $password, $driverOptions), 52 | $this->decoratedDriver->getDatabasePlatform(), 53 | $params 54 | ); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getDatabasePlatform(): AbstractPlatform 61 | { 62 | return $this->decoratedDriver->getDatabasePlatform(); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function getSchemaManager(Connection $conn, ?AbstractPlatform $platform = null): AbstractSchemaManager 69 | { 70 | return $this->decoratedDriver->getSchemaManager($conn, $platform); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function getName(): string 77 | { 78 | return $this->decoratedDriver->getName(); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function getDatabase(Connection $conn): ?string 85 | { 86 | return $this->decoratedDriver->getDatabase($conn); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function createDatabasePlatformForVersion($version): AbstractPlatform 93 | { 94 | if ($this->decoratedDriver instanceof VersionAwarePlatformDriver) { 95 | return $this->decoratedDriver->createDatabasePlatformForVersion($version); 96 | } 97 | 98 | return $this->getDatabasePlatform(); 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function convertException($message, DriverException $exception): DBALDriverException 105 | { 106 | if ($this->decoratedDriver instanceof ExceptionConverterDriver) { 107 | return $this->decoratedDriver->convertException($message, $exception); 108 | } 109 | 110 | return new DBALDriverException($message, $exception); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverForV3.php: -------------------------------------------------------------------------------- 1 | = 3.0. 14 | * 15 | * @internal 16 | * 17 | * @phpstan-import-type Params from \Doctrine\DBAL\DriverManager as ConnectionParams 18 | */ 19 | final class TracingDriverForV3 extends AbstractDriverMiddleware 20 | { 21 | /** 22 | * @var TracingDriverConnectionFactoryInterface The connection factory 23 | */ 24 | private $connectionFactory; 25 | 26 | /** 27 | * Constructor. 28 | * 29 | * @param TracingDriverConnectionFactoryInterface $connectionFactory The connection factory 30 | * @param Driver $decoratedDriver The instance of the driver to decorate 31 | */ 32 | public function __construct(TracingDriverConnectionFactoryInterface $connectionFactory, Driver $decoratedDriver) 33 | { 34 | parent::__construct($decoratedDriver); 35 | 36 | $this->connectionFactory = $connectionFactory; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | * 42 | * @phpstan-param ConnectionParams $params 43 | */ 44 | public function connect(array $params): TracingDriverConnectionInterface 45 | { 46 | return $this->connectionFactory->create( 47 | parent::connect($params), 48 | $this->getDatabasePlatform(), 49 | $params 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverForV4.php: -------------------------------------------------------------------------------- 1 | = 4.0. 15 | * 16 | * @internal 17 | * 18 | * @psalm-import-type Params from \Doctrine\DBAL\DriverManager 19 | */ 20 | final class TracingDriverForV4 extends AbstractDriverMiddleware 21 | { 22 | /** 23 | * @var TracingDriverConnectionFactoryInterface The connection factory 24 | */ 25 | private $connectionFactory; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param TracingDriverConnectionFactoryInterface $connectionFactory The connection factory 31 | * @param Driver $decoratedDriver The instance of the driver to decorate 32 | */ 33 | public function __construct(TracingDriverConnectionFactoryInterface $connectionFactory, Driver $decoratedDriver) 34 | { 35 | parent::__construct($decoratedDriver); 36 | 37 | $this->connectionFactory = $connectionFactory; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | * 43 | * @psalm-param Params $params All connection parameters. 44 | */ 45 | public function connect(array $params): TracingDriverConnectionInterface 46 | { 47 | $connection = parent::connect($params); 48 | $versionProvider = new StaticServerVersionProvider($connection->getServerVersion()); 49 | 50 | return $this->connectionFactory->create( 51 | $connection, 52 | $this->getDatabasePlatform($versionProvider), 53 | $params 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingDriverMiddleware.php: -------------------------------------------------------------------------------- 1 | connectionFactory = $hubOrConnectionFactory; 33 | } elseif ($hubOrConnectionFactory instanceof HubInterface) { 34 | @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); 35 | 36 | $this->connectionFactory = new TracingDriverConnectionFactory($hubOrConnectionFactory); 37 | } else { 38 | 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)); 39 | } 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function wrap(Driver $driver): Driver 46 | { 47 | return new TracingDriver($this->connectionFactory, $driver); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingServerInfoAwareDriverConnection.php: -------------------------------------------------------------------------------- 1 | decoratedConnection = $decoratedConnection; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function prepare($sql): Statement 40 | { 41 | return $this->decoratedConnection->prepare($sql); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function query(?string $sql = null, ...$args): Result 48 | { 49 | return $this->decoratedConnection->query($sql, ...$args); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | * 55 | * @return mixed 56 | */ 57 | public function quote($value, $type = ParameterType::STRING) 58 | { 59 | return $this->decoratedConnection->quote($value, $type); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function exec($sql): int 66 | { 67 | return $this->decoratedConnection->exec($sql); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | * 73 | * @return string|int|false 74 | */ 75 | public function lastInsertId($name = null) 76 | { 77 | return $this->decoratedConnection->lastInsertId($name); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function beginTransaction(): bool 84 | { 85 | return $this->decoratedConnection->beginTransaction(); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function commit(): bool 92 | { 93 | return $this->decoratedConnection->commit(); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function rollBack(): bool 100 | { 101 | return $this->decoratedConnection->rollBack(); 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function getServerVersion(): string 108 | { 109 | $wrappedConnection = $this->getWrappedConnection(); 110 | 111 | if (!$wrappedConnection instanceof ServerInfoAwareConnection) { 112 | throw new \BadMethodCallException(\sprintf('The wrapped connection must be an instance of the "%s" interface.', ServerInfoAwareConnection::class)); 113 | } 114 | 115 | return $wrappedConnection->getServerVersion(); 116 | } 117 | 118 | /** 119 | * {@inheritdoc} 120 | * 121 | * @return resource|object 122 | */ 123 | public function getNativeConnection() 124 | { 125 | if (!method_exists($this->decoratedConnection, 'getNativeConnection')) { 126 | throw new \BadMethodCallException(\sprintf('The connection "%s" does not support accessing the native connection.', \get_class($this->decoratedConnection))); 127 | } 128 | 129 | return $this->decoratedConnection->getNativeConnection(); 130 | } 131 | 132 | /** 133 | * {@inheritdoc} 134 | */ 135 | public function requiresQueryForServerVersion(): bool 136 | { 137 | $wrappedConnection = $this->getWrappedConnection(); 138 | 139 | if (!$wrappedConnection instanceof ServerInfoAwareConnection) { 140 | throw new \BadMethodCallException(\sprintf('The wrapped connection must be an instance of the "%s" interface.', ServerInfoAwareConnection::class)); 141 | } 142 | 143 | if (!method_exists($wrappedConnection, 'requiresQueryForServerVersion')) { 144 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 145 | } 146 | 147 | return $wrappedConnection->requiresQueryForServerVersion(); 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | public function errorCode(): ?string 154 | { 155 | if (method_exists($this->decoratedConnection, 'errorCode')) { 156 | return $this->decoratedConnection->errorCode(); 157 | } 158 | 159 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function errorInfo(): array 166 | { 167 | if (method_exists($this->decoratedConnection, 'errorInfo')) { 168 | return $this->decoratedConnection->errorInfo(); 169 | } 170 | 171 | throw new \BadMethodCallException(\sprintf('The %s() method is not supported on Doctrine DBAL 3.0.', __METHOD__)); 172 | } 173 | 174 | public function getWrappedConnection(): Connection 175 | { 176 | return $this->decoratedConnection->getWrappedConnection(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingStatementForV2.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TracingStatementForV2 extends AbstractTracingStatement implements \IteratorAggregate, Statement 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function getIterator(): \Traversable 22 | { 23 | return $this->decoratedStatement; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function closeCursor(): bool 30 | { 31 | return $this->decoratedStatement->closeCursor(); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function columnCount(): int 38 | { 39 | return $this->decoratedStatement->columnCount(); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null): bool 46 | { 47 | return $this->decoratedStatement->setFetchMode($fetchMode, $arg2, $arg3); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function fetch($fetchMode = null, $cursorOrientation = \PDO::FETCH_ORI_NEXT, $cursorOffset = 0) 54 | { 55 | return $this->decoratedStatement->fetch($fetchMode, $cursorOrientation, $cursorOffset); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) 62 | { 63 | return $this->decoratedStatement->fetchAll($fetchMode, $fetchArgument, $ctorArgs); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function fetchColumn($columnIndex = 0) 70 | { 71 | return $this->decoratedStatement->fetchColumn($columnIndex); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function errorCode() 78 | { 79 | return $this->decoratedStatement->errorCode(); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function errorInfo(): array 86 | { 87 | return $this->decoratedStatement->errorInfo(); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function rowCount(): int 94 | { 95 | return $this->decoratedStatement->rowCount(); 96 | } 97 | 98 | /** 99 | * {@inheritdoc} 100 | */ 101 | public function bindValue($param, $value, $type = ParameterType::STRING): bool 102 | { 103 | return $this->decoratedStatement->bindValue($param, $value, $type); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool 110 | { 111 | return $this->decoratedStatement->bindParam($param, $variable, $type, $length ?? 0, ...\array_slice(\func_get_args(), 4)); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function execute($params = null): bool 118 | { 119 | $spanContext = SpanContext::make() 120 | ->setOp(self::SPAN_OP_STMT_EXECUTE) 121 | ->setData($this->spanData) 122 | ->setOrigin('auto.db') 123 | ->setDescription($this->sqlQuery); 124 | 125 | return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingStatementForV3.php: -------------------------------------------------------------------------------- 1 | decoratedStatement->bindValue($param, $value, $type); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool 29 | { 30 | return $this->decoratedStatement->bindParam($param, $variable, $type, ...\array_slice(\func_get_args(), 3)); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function execute($params = null): Result 37 | { 38 | $spanContext = SpanContext::make() 39 | ->setOp(self::SPAN_OP_STMT_EXECUTE) 40 | ->setData($this->spanData) 41 | ->setOrigin('auto.db') 42 | ->setDescription($this->sqlQuery); 43 | 44 | return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute'], $params); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tracing/Doctrine/DBAL/TracingStatementForV4.php: -------------------------------------------------------------------------------- 1 | decoratedStatement->bindValue($param, $value, $type); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function execute(): Result 29 | { 30 | $spanContext = SpanContext::make() 31 | ->setOp(self::SPAN_OP_STMT_EXECUTE) 32 | ->setData($this->spanData) 33 | ->setOrigin('auto.db') 34 | ->setDescription($this->sqlQuery); 35 | 36 | return $this->traceFunction($spanContext, [$this->decoratedStatement, 'execute']); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/AbstractTraceableHttpClient.php: -------------------------------------------------------------------------------- 1 | client = $client; 44 | $this->hub = $hub; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function request(string $method, string $url, array $options = []): ResponseInterface 51 | { 52 | $uri = new Uri($url); 53 | $headers = $options['headers'] ?? []; 54 | 55 | $span = $this->hub->getSpan(); 56 | $client = $this->hub->getClient(); 57 | 58 | if (null === $span) { 59 | if (self::shouldAttachTracingHeaders($client, $uri)) { 60 | $headers['baggage'] = getBaggage(); 61 | $headers['sentry-trace'] = getTraceparent(); 62 | $headers['traceparent'] = getW3CTraceparent(); 63 | } 64 | 65 | $options['headers'] = $headers; 66 | 67 | return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span); 68 | } 69 | 70 | $partialUri = Uri::fromParts([ 71 | 'scheme' => $uri->getScheme(), 72 | 'host' => $uri->getHost(), 73 | 'port' => $uri->getPort(), 74 | 'path' => $uri->getPath(), 75 | ]); 76 | 77 | $context = SpanContext::make() 78 | ->setOp('http.client') 79 | ->setOrigin('auto.http.client') 80 | ->setDescription($method . ' ' . (string) $partialUri); 81 | 82 | $contextData = [ 83 | 'http.url' => (string) $partialUri, 84 | 'http.request.method' => $method, 85 | ]; 86 | if ('' !== $uri->getQuery()) { 87 | $contextData['http.query'] = $uri->getQuery(); 88 | } 89 | if ('' !== $uri->getFragment()) { 90 | $contextData['http.fragment'] = $uri->getFragment(); 91 | } 92 | $context->setData($contextData); 93 | 94 | $childSpan = $span->startChild($context); 95 | 96 | if (self::shouldAttachTracingHeaders($client, $uri)) { 97 | $headers['baggage'] = $childSpan->toBaggage(); 98 | $headers['sentry-trace'] = $childSpan->toTraceparent(); 99 | $headers['traceparent'] = $childSpan->toW3CTraceparent(); 100 | } 101 | 102 | $options['headers'] = $headers; 103 | 104 | return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $childSpan); 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function stream($responses, ?float $timeout = null): ResponseStreamInterface 111 | { 112 | if ($responses instanceof AbstractTraceableResponse) { 113 | $responses = [$responses]; 114 | } elseif (!is_iterable($responses)) { 115 | throw new \TypeError(\sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses))); 116 | } 117 | 118 | return new ResponseStream(AbstractTraceableResponse::stream($this->client, $responses, $timeout)); 119 | } 120 | 121 | public function reset(): void 122 | { 123 | if ($this->client instanceof ResetInterface) { 124 | $this->client->reset(); 125 | } 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function setLogger(LoggerInterface $logger): void 132 | { 133 | if ($this->client instanceof LoggerAwareInterface) { 134 | $this->client->setLogger($logger); 135 | } 136 | } 137 | 138 | private static function shouldAttachTracingHeaders(?ClientInterface $client, Uri $uri): bool 139 | { 140 | if (null !== $client) { 141 | $sdkOptions = $client->getOptions(); 142 | 143 | // Check if the request destination is allow listed in the trace_propagation_targets option. 144 | if ( 145 | null === $sdkOptions->getTracePropagationTargets() 146 | || \in_array($uri->getHost(), $sdkOptions->getTracePropagationTargets()) 147 | ) { 148 | return true; 149 | } 150 | } 151 | 152 | return false; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/AbstractTraceableResponse.php: -------------------------------------------------------------------------------- 1 | client = $client; 36 | $this->response = $response; 37 | $this->span = $span; 38 | } 39 | 40 | public function __destruct() 41 | { 42 | try { 43 | if (method_exists($this->response, '__destruct')) { 44 | $this->response->__destruct(); 45 | } 46 | } finally { 47 | $this->finishSpan(); 48 | } 49 | } 50 | 51 | public function __sleep(): array 52 | { 53 | throw new \BadMethodCallException('Serializing instances of this class is forbidden.'); 54 | } 55 | 56 | public function __wakeup() 57 | { 58 | throw new \BadMethodCallException('Unserializing instances of this class is forbidden.'); 59 | } 60 | 61 | public function getStatusCode(): int 62 | { 63 | return $this->response->getStatusCode(); 64 | } 65 | 66 | public function getHeaders(bool $throw = true): array 67 | { 68 | return $this->response->getHeaders($throw); 69 | } 70 | 71 | public function getContent(bool $throw = true): string 72 | { 73 | try { 74 | return $this->response->getContent($throw); 75 | } finally { 76 | $this->finishSpan(); 77 | } 78 | } 79 | 80 | public function toArray(bool $throw = true): array 81 | { 82 | try { 83 | return $this->response->toArray($throw); 84 | } finally { 85 | $this->finishSpan(); 86 | } 87 | } 88 | 89 | public function cancel(): void 90 | { 91 | $this->response->cancel(); 92 | $this->finishSpan(); 93 | } 94 | 95 | /** 96 | * @internal 97 | * 98 | * @param iterable $responses 99 | * 100 | * @return \Generator 101 | */ 102 | public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator 103 | { 104 | /** @var \SplObjectStorage $traceableMap */ 105 | $traceableMap = new \SplObjectStorage(); 106 | $wrappedResponses = []; 107 | 108 | foreach ($responses as $response) { 109 | if (!$response instanceof self) { 110 | throw new \TypeError(\sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($response))); 111 | } 112 | 113 | $traceableMap[$response->response] = $response; 114 | $wrappedResponses[] = $response->response; 115 | } 116 | 117 | foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) { 118 | $traceableResponse = $traceableMap[$response]; 119 | $traceableResponse->finishSpan(); 120 | 121 | yield $traceableResponse => $chunk; 122 | } 123 | } 124 | 125 | private function finishSpan(): void 126 | { 127 | if (null === $this->span) { 128 | return; 129 | } 130 | 131 | // We finish the span (which means setting the span end timestamp) first 132 | // to ensure the measured time is as close as possible to the duration of 133 | // the HTTP request 134 | $this->span->finish(); 135 | 136 | /** @var int $statusCode */ 137 | $statusCode = $this->response->getInfo('http_code'); 138 | 139 | // If the returned status code is 0, it means that this info isn't available 140 | // yet (e.g. an error happened before the request was sent), hence we cannot 141 | // determine what happened. 142 | if (0 === $statusCode) { 143 | $this->span->setStatus(SpanStatus::unknownError()); 144 | } else { 145 | $this->span->setStatus(SpanStatus::createFromHttpStatusCode($statusCode)); 146 | } 147 | 148 | $this->span = null; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/TraceableHttpClientForV4.php: -------------------------------------------------------------------------------- 1 | client = $this->client->withOptions($options); 19 | 20 | return $clone; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/TraceableHttpClientForV6.php: -------------------------------------------------------------------------------- 1 | client = $this->client->withOptions($options); 19 | 20 | return $clone; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/TraceableResponseForV4.php: -------------------------------------------------------------------------------- 1 | response->getInfo($type); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/TraceableResponseForV5.php: -------------------------------------------------------------------------------- 1 | response->getInfo($type); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function toStream(bool $throw = true) 28 | { 29 | return $this->response->toStream($throw); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Tracing/HttpClient/TraceableResponseForV6.php: -------------------------------------------------------------------------------- 1 | response->getInfo($type); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function toStream(bool $throw = true) 26 | { 27 | return $this->response->toStream($throw); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tracing/Twig/TwigTracingExtension.php: -------------------------------------------------------------------------------- 1 | The currently active spans 23 | */ 24 | private $spans; 25 | 26 | /** 27 | * @param HubInterface $hub The current hub 28 | */ 29 | public function __construct(HubInterface $hub) 30 | { 31 | $this->hub = $hub; 32 | $this->spans = new \SplObjectStorage(); 33 | } 34 | 35 | /** 36 | * This method is called before the execution of a block, a macro or a 37 | * template. 38 | * 39 | * @param Profile $profile The profiling data 40 | */ 41 | public function enter(Profile $profile): void 42 | { 43 | $transaction = $this->hub->getTransaction(); 44 | 45 | if (null === $transaction) { 46 | return; 47 | } 48 | 49 | $this->spans[$profile] = $transaction->startChild( 50 | SpanContext::make() 51 | ->setOp('view.render') 52 | ->setOrigin('auto.view') 53 | ->setDescription($this->getSpanDescription($profile)) 54 | ); 55 | } 56 | 57 | /** 58 | * This method is called when the execution of a block, a macro or a 59 | * template is finished. 60 | * 61 | * @param Profile $profile The profiling data 62 | */ 63 | public function leave(Profile $profile): void 64 | { 65 | if (!isset($this->spans[$profile])) { 66 | return; 67 | } 68 | 69 | $this->spans[$profile]->finish(); 70 | 71 | unset($this->spans[$profile]); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getNodeVisitors(): array 78 | { 79 | return [ 80 | new ProfilerNodeVisitor(self::class), 81 | ]; 82 | } 83 | 84 | /** 85 | * Gets a short description for the span. 86 | * 87 | * @param Profile $profile The profiling data 88 | */ 89 | private function getSpanDescription(Profile $profile): string 90 | { 91 | switch (true) { 92 | case $profile->isRoot(): 93 | return $profile->getName(); 94 | 95 | case $profile->isTemplate(): 96 | return $profile->getTemplate(); 97 | 98 | default: 99 | return \sprintf('%s::%s(%s)', $profile->getTemplate(), $profile->getType(), $profile->getName()); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Twig/SentryExtension.php: -------------------------------------------------------------------------------- 1 | ['html']]), 31 | new TwigFunction('sentry_w3c_trace_meta', [$this, 'getW3CTraceMeta'], ['is_safe' => ['html']]), 32 | new TwigFunction('sentry_baggage_meta', [$this, 'getBaggageMeta'], ['is_safe' => ['html']]), 33 | ]; 34 | } 35 | 36 | /** 37 | * Returns an HTML meta tag named `sentry-trace`. 38 | */ 39 | public function getTraceMeta(): string 40 | { 41 | return \sprintf('', getTraceparent()); 42 | } 43 | 44 | /** 45 | * Returns an HTML meta tag named `traceparent`. 46 | */ 47 | public function getW3CTraceMeta(): string 48 | { 49 | return \sprintf('', getW3CTraceparent()); 50 | } 51 | 52 | /** 53 | * Returns an HTML meta tag named `baggage`. 54 | */ 55 | public function getBaggageMeta(): string 56 | { 57 | return \sprintf('', getBaggage()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/aliases.php: -------------------------------------------------------------------------------- 1 | =')) { 45 | if (!class_exists(TraceableCacheAdapter::class, false)) { 46 | class_alias(TraceableCacheAdapterForV3::class, TraceableCacheAdapter::class); 47 | } 48 | 49 | if (!class_exists(TraceableTagAwareCacheAdapter::class, false)) { 50 | class_alias(TraceableTagAwareCacheAdapterForV3::class, TraceableTagAwareCacheAdapter::class); 51 | } 52 | } else { 53 | if (!class_exists(TraceableCacheAdapter::class, false)) { 54 | class_alias(TraceableCacheAdapterForV2::class, TraceableCacheAdapter::class); 55 | } 56 | 57 | if (!class_exists(TraceableTagAwareCacheAdapter::class, false)) { 58 | class_alias(TraceableTagAwareCacheAdapterForV2::class, TraceableTagAwareCacheAdapter::class); 59 | } 60 | } 61 | } 62 | 63 | if (!class_exists(TracingStatement::class)) { 64 | if (class_exists(Result::class) && !interface_exists(VersionAwarePlatformDriver::class)) { 65 | class_alias(TracingStatementForV4::class, TracingStatement::class); 66 | class_alias(TracingDriverForV4::class, TracingDriver::class); 67 | class_alias(TracingDriverConnectionForV4::class, TracingDriverConnection::class); 68 | class_alias(TracingDriverConnectionFactoryForV4::class, TracingDriverConnectionFactory::class); 69 | } elseif (class_exists(Result::class)) { 70 | class_alias(TracingStatementForV3::class, TracingStatement::class); 71 | class_alias(TracingDriverForV3::class, TracingDriver::class); 72 | class_alias(TracingDriverConnectionForV2V3::class, TracingDriverConnection::class); 73 | class_alias(TracingDriverConnectionFactoryForV2V3::class, TracingDriverConnectionFactory::class); 74 | } elseif (interface_exists(Result::class)) { 75 | class_alias(TracingStatementForV2::class, TracingStatement::class); 76 | class_alias(TracingDriverForV2::class, TracingDriver::class); 77 | class_alias(TracingDriverConnectionForV2V3::class, TracingDriverConnection::class); 78 | class_alias(TracingDriverConnectionFactoryForV2V3::class, TracingDriverConnectionFactory::class); 79 | } 80 | } 81 | 82 | if (!class_exists(TraceableResponse::class) && class_exists(HttpClient::class)) { 83 | if (!interface_exists(StreamableInterface::class)) { 84 | class_alias(TraceableResponseForV4::class, TraceableResponse::class); 85 | class_alias(TraceableHttpClientForV4::class, TraceableHttpClient::class); 86 | } elseif (method_exists(HttpClientInterface::class, 'withOptions')) { 87 | class_alias(TraceableResponseForV6::class, TraceableResponse::class); 88 | class_alias(TraceableHttpClientForV6::class, TraceableHttpClient::class); 89 | } else { 90 | class_alias(TraceableResponseForV5::class, TraceableResponse::class); 91 | class_alias(TraceableHttpClientForV5::class, TraceableHttpClient::class); 92 | } 93 | } 94 | --------------------------------------------------------------------------------