diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c862adeb..65dead1e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,10 +10,15 @@ on: permissions: contents: read +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: phpunit: runs-on: ubuntu-latest - timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 @@ -29,7 +34,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -55,14 +60,13 @@ jobs: composer install --no-interaction --prefer-dist --no-progress - name: Run phpunit - run: composer test:ci + run: vendor/bin/phpunit --coverage-clover=coverage.xml - name: Upload code coverage uses: codecov/codecov-action@v3 phpunit-legacy: runs-on: ubuntu-latest - timeout-minutes: 15 env: COMPOSER_NO_INTERACTION: 1 @@ -118,7 +122,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -130,8 +134,9 @@ jobs: - name: Install Composer dependencies run: | # friendsofphp/php-cs-fixer: No need for this package to run phpunit and it conflicts with older Laravel versions + # livewire/livewire: Only supported on Laravel 7.0 and above # laravel/folio: Only supported on PHP 8.1 + Laravel 10.0 and above - composer remove friendsofphp/php-cs-fixer laravel/folio --dev --no-interaction --no-update + composer remove friendsofphp/php-cs-fixer livewire/livewire laravel/folio --dev --no-interaction --no-update # Require the correct versions we want to run phpunit for composer require \ @@ -145,7 +150,7 @@ jobs: composer install --no-interaction --prefer-dist --no-progress - name: Run phpunit - run: composer test:ci + run: vendor/bin/phpunit --coverage-clover=coverage.xml - name: Upload code coverage uses: codecov/codecov-action@v3 diff --git a/.github/workflows/cs.yaml b/.github/workflows/cs.yaml index c6daa762..7d460093 100644 --- a/.github/workflows/cs.yaml +++ b/.github/workflows/cs.yaml @@ -1,11 +1,11 @@ -name: CS +name: Code style and static analysis on: pull_request: push: branches: - master - - release/** + - develop permissions: contents: read @@ -16,16 +16,33 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.2' - coverage: none - name: Install dependencies run: composer update --no-progress --no-interaction --prefer-dist - name: Run script - run: composer phpcs:ci + run: vendor/bin/php-cs-fixer fix --verbose --diff --dry-run + + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + + - name: Install dependencies + run: composer update --no-progress --no-interaction --prefer-dist + + - name: Run script + run: vendor/bin/phpstan analyse diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml index 7a42a89a..f3b0b982 100644 --- a/.github/workflows/publish-release.yaml +++ b/.github/workflows/publish-release.yaml @@ -9,6 +9,10 @@ on: force: description: Force a release even when there are release-blockers (optional) required: false + merge_target: + description: Target branch to merge into. Uses the default branch as a fallback (optional) + required: false + default: master permissions: contents: read @@ -30,3 +34,4 @@ jobs: with: version: ${{ github.event.inputs.version }} force: ${{ github.event.inputs.force }} + merge_target: ${{ github.event.inputs.merge_target }} diff --git a/.github/workflows/static-analysis.yaml b/.github/workflows/static-analysis.yaml deleted file mode 100644 index 8a92d6bb..00000000 --- a/.github/workflows/static-analysis.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Static Analysis - -on: - pull_request: - push: - branches: - - master - - release/** - -permissions: - contents: read - -jobs: - phpstan: - name: PHPStan - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - coverage: none - - - name: Install dependencies - run: composer update --no-progress --no-interaction --prefer-dist - - - name: Run script - run: composer phpstan:ci diff --git a/CHANGELOG.md b/CHANGELOG.md index 0323d4ba..6741f2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,376 +1,67 @@ # Changelog -## 3.8.2 +## 4.0.0 -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.8.2. +The Sentry SDK team is thrilled to announce the immediate availability of Sentry Laravel SDK v4.0.0. -### Bug Fixes +# Breaking Change -- Fix `DateTimeZone` not properly converted to a string when using Cron Monitoring [(#783)](https://github.com/getsentry/sentry-laravel/pull/783) +This version adds support for the underlying [Sentry PHP SDK v4.0](https://github.com/getsentry/sentry-php). +Please refer to the PHP SDK [sentry-php/UPGRADE-4.0.md](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-4.0.md) guide for a complete list of breaking changes. -## 3.8.1 +- This version exclusively uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/) to send event data to Sentry. -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.8.1. + If you are using [sentry.io](https://sentry.io), no action is needed. + If you are using an on-premise/self-hosted installation of Sentry, the minimum requirement is now version `>= v20.6.0`. -### Bug Fixes +- You need to have `ext-curl` installed to use the SDK. -- Fix a deprecation notice caused by passing `null` to `Str::startsWith()` [(#780)](https://github.com/getsentry/sentry-laravel/pull/780) +- The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. -## 3.8.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.8.0. - -### Features - -- Initial support for Laravel Folio [(#738)](https://github.com/getsentry/sentry-laravel/pull/738) - - If you are using Laravel Folio in your application, we'll extract a more meaningful transaction name based on the - `VieMatched` event. - -- The filesystem adapters for the `sentry` driver now extend the well-known Laravel classes they decorate, - `Illuminate\Filesystem\FilesystemAdapter` and `Illuminate\Filesystem\AwsS3V3Adapter`. - - Enabling the feature can be simplified by wrapping the configuration for all disks - with a call to `Sentry\Laravel\Features\Storage\Integration::configureDisks()` - in your `config/filesystems.php` file: - - ```php - 'disks' => Sentry\Laravel\Features\Storage\Integration::configureDisks([ - 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - 'throw' => false, - ], - - // ... - ], /* enableSpans: */ true, /* enableBreadcrumbs: */ true), - ``` - - Alternatively, you can enable this feature only for select disks: - - ```php - 'disks' => [ - 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - 'throw' => false, - ], - - 's3' => Sentry\Laravel\Features\Storage\Integration::configureDisk('s3', [ - // ... - ], /* enableSpans: */ true, /* enableBreadcrumbs: */ true), - ], - ``` - - By default, both spans and breadcrumbs are enabled. - You may disable them by passing the second argument, `$enableSpans` or the third argument, `$enableBreadcrumbs`. - -- Add Laravel artisan about command [(#768)](https://github.com/getsentry/sentry-laravel/pull/768) - -### Bug Fixes - -- Remove usage of `Str::replace` [(#762)](https://github.com/getsentry/sentry-laravel/pull/762) - - - This fixes an issue using Cron Monitoring in Laravel 6, 7 and < 8.41.0 - -### Misc - -- Differentiate between boot and register for features [(#759)](https://github.com/getsentry/sentry-laravel/pull/759) - -- Internal improvements [(#769)](https://github.com/getsentry/sentry-laravel/pull/769) - - - Make feature registration agnostic to the service container. - - Cleanup tests by using the `@define-env` annotation. - - Move the Log channel to a feature and add tests. - - Mark BacktraceHelper as `@internal` and make it lazy for the tracing service provider. - -## 3.7.3 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.7.3. - -### Bug Fixes - -- Fix HTTP client integration consuming the PSR request/response body stream [(#756)](https://github.com/getsentry/sentry-laravel/pull/756) - -## 3.7.2 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.7.2. - -### Bug Fixes - -- Fix `app.bootstrap` span not starting at the start of the performance transaction [(#734)](https://github.com/getsentry/sentry-laravel/pull/734) - -- Fix `sentry` storage driver not being registered when DSN is not set causing `Driver [sentry] is not supported.` [(#752)](https://github.com/getsentry/sentry-laravel/pull/752) - -## 3.7.1 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.7.1. - -### Bug Fixes - -- Performance traces and breadcrumbs for filesystem access are now turned off by default [(#746)](https://github.com/getsentry/sentry-laravel/pull/746) - - To enable the feature, you'll need to make some changes in your `config/filesystems.php` file for each disk where you want to enable the tracing filesystem driver. - ```php - // For example, if you want to trace the `local` disk, you update the disk config from this: - 'local' => [ - 'driver' => 'local', - 'root' => storage_path('app'), - 'throw' => false, - ], - - // to this: - 'local' => [ - 'driver' => 'sentry', - 'root' => storage_path('app'), - 'throw' => false, + // config/sentry.php - 'sentry_disk_name' => 'local', - 'sentry_original_driver' => 'local', - 'sentry_enable_spans' => true, - 'sentry_enable_breadcrumbs' => true, - ], + 'ignore_exceptions' => [BadThingsHappenedException::class], ``` - For each disk, you replace the `driver` key with `sentry` and add the `sentry_original_driver` key with the original driver name. - For us to construct the original driver, you also need to add the `sentry_disk_name` key with the name of the disk. - In addition, you can specify the optional `sentry_enable_spans` and `sentry_enable_breadcrumbs` config keys to turn off that feature for the disk. - These options are enabled by default. - - Please note that we replace the driver for the disk with a custom driver that will capture performance traces and breadcrumbs. - This means that relying on the disk to be of a specific type might cause problems. - If you rely on the disk being an instance of `Illuminate\Contracts\Filesystem\Filesystem` or `Illuminate\Contracts\Filesystem\Cloud`, there should be no problem. - -## 3.7.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.7.0. - -### Features - -- Tracing without Performance [(#719)](https://github.com/getsentry/sentry-laravel/pull/719) - - The SDK will now continue a trace from incoming HTTP requests, even if performance is not enabled. - To continue a trace outward, you may attach the Sentry tracing headers to any HTTP client request. - You can fetch the required header values by calling `\Sentry\getBaggage()` and `\Sentry\getTraceparent()`. - -- Add performance traces and breadcrumbs for filesystem access [(#726)](https://github.com/getsentry/sentry-laravel/pull/726) - - This behaviour can be changed in your `config/sentry.php` file. - - ```php - 'breadcrumbs' => [ - // Capture storage access as breadcrumbs - 'storage' => true, - ], - `tracing` => [ - // Capture storage access as spans - 'storage' => true, - ], - ``` + 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. -- GraphQL and HTTP client improvements [(#720)](https://github.com/getsentry/sentry-laravel/pull/720) +# Features - This adds an improved visual representation of the request body on the Sentry web interface, as well as - `response_body_size` and `request_body_size` to HTTP client breadcrumbs. - +- Enable distributed tracing for outgoing HTTP client requests [(#797)](https://github.com/getsentry/sentry-laravel/pull/797) -- Less sensitive data send by default [(#732)](https://github.com/getsentry/sentry-laravel/pull/732) + This feature is only available on Laravel >= 10.14. + When making a request using the Laravel `Http` facade, we automatically attach the `sentry-trace` and `baggage` headers. - The SDK will no longer send the value of the configured Laravel session cookie as well as the value of any cookies - starting with `remember_*`. - Additionally, SQL bindings are no longer set on breadcrumbs by default. This behaviour can be changed in your `config/sentry.php` file. + This behaviour can be controlled by setting `trace_propagation_targets` in your `config/sentry.php` file. ```php - 'breadcrumbs' => [ - // Capture bindings on SQL queries logged in breadcrumbs - 'sql_bindings' => false, - ], - ``` + // config/sentry.php -- Make it configurable if performance traces continue after the response has been sent [(#727)](https://github.com/getsentry/sentry-laravel/pull/727) + // All requests will contain the tracing headers. This is the default behaviour. + 'trace_propagation_targets' => null, - This behaviour can be changed in your `config/sentry.php` file. + // To turn this feature off completely, set the option to an empty array. + 'trace_propagation_targets' => [], - ```php - 'tracing' => [ - // Indicates if the performance trace should continue after the response has been sent to the user until the application terminates - // This is required to capture any spans that are created after the response has been sent like queue jobs dispatched using `dispatch(...)->afterResponse()` for example - 'continue_after_response' => true, + // To only attach these headers to some requests, you can allow-list certain hosts. + 'trace_propagation_targets' => [ + 'examlpe.com', + 'api.examlpe.com', ], ``` -- Expose all config settings as environment variables. [(#735)](https://github.com/getsentry/sentry-laravel/pull/735) - - All config values in `config/sentry.php` can be set with environment variables now. - For existing applications, you might need to update your config file with the new defaults manually. - The latest version can be found [here](https://github.com/getsentry/sentry-laravel/blob/master/config/sentry.php). - -### Bug Fixes - -- Handle exceptions being raised while resolving the PSR7 request [(#743)](https://github.com/getsentry/sentry-laravel/pull/743) - -## 3.6.1 + Please make sure to remove any custom code that injected these headers previously. + If you are using the `Sentry\Tracing\GuzzleTracingMiddleware` provided by our underlying PHP SDK, you must also remove it. -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.6.1. +- Add support for Laravel Livewire 3 [(#798)](https://github.com/getsentry/sentry-laravel/pull/798) -### Bug Fixes - -- Fix performance tracing for Lumen applications [(#724)](https://github.com/getsentry/sentry-laravel/pull/724) - -## 3.6.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.6.0. - -### Features - -- Add support for upserting Cron Monitors [(#677)](https://github.com/getsentry/sentry-laravel/pull/677) - - We simplified setting up monitoring for your scheduled tasks. Now, the only requirement is to add the - `sentryMonitor` macro. A corresponding monitor will be created automatically on Sentry. + The SDK now creates traces and breadcrumbs for Livewire 3 as well. + Both the class-based and Volt usage are supported. ```php - protected function schedule(Schedule $schedule) - { - $schedule->command('emails:send') - ->everyHour() - ->sentryMonitor(); // add this line - } - ``` - - You can read more about this in our [docs](https://docs.sentry.io/platforms/php/guides/laravel/crons/). - -## 3.5.1 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.5.1. + // config/sentry.php -### Bug Fixes - -- Prevent registering terminating callback multiple times and guard against finishing already finished transactions [(#717)](https://github.com/getsentry/sentry-laravel/pull/717) - - This fixes the `Call to a member function finish() on null` that could occur when using long running processes like on the CLI or with Laravel Octane. - -## 3.5.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.5.0. - -### Features - -- Improve terminating callbacks visibility [(#707)](https://github.com/getsentry/sentry-laravel/pull/707) - - This adds support for performance traces that happen after the terminable middleware emits the response. - -- Add `http.route.response` span [(#708)](https://github.com/getsentry/sentry-laravel/pull/708) - - > **Note**: This feature requires Laravel `>= 10.13.0` to hail any new spans. - - This will add a `http.route.response` span which identifies possible actions taken to prepare the response, especially when using `Illuminate\Contracts\Support\Responsable` classes. - -### Bug Fixes - -- Refactor queue integration [(#692)](https://github.com/getsentry/sentry-laravel/pull/692) - - This improves and fixes some edge cases around scope management when running Laravel Queues. - -- Improve `lazyLoadingViolationReporter` helper [(#709)](https://github.com/getsentry/sentry-laravel/pull/709) - - Check for `$model->exists` and `$model->wasRecentlyCreated` before capturing the event. - -### Misc - -- Bump the underlying PHP SDK to version `^3.19` to enforce an upgrade to fix an issue in libcurl `>= v8.1.0` [(#711)](https://github.com/getsentry/sentry-laravel/pull/711) - -## 3.4.1 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.4.1. - -### Bug Fixes - -- Fix missing `sentryMonitor` macro if no DSN is set [(#698)](https://github.com/getsentry/sentry-laravel/pull/698) - -## 3.4.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.4.0. - -### Features - -- Add helper to report lazy loading violations [(#678)](https://github.com/getsentry/sentry-laravel/pull/678) - - We've added a new helper that makes reporting lazy loading violation to Sentry in production very easy: - - ```php - // See: https://laravel.com/docs/10.x/eloquent-relationships#preventing-lazy-loading - \Illuminate\Database\Eloquent\Model::preventLazyLoading(); - - // In production we just report the lazy loading violation instead of crashing the application since it's a performance issue not a security issue - if (app()->isProduction()) { - \Illuminate\Database\Eloquent\Model::handleLazyLoadingViolationUsing( - \Sentry\Laravel\Integration::lazyLoadingViolationReporter(), - ); - } - ``` - - We let you know which model and relation caused the lazy loading violation and we also include the origin of the violation so you can find the culprit quickly. - -- Reintroduce Laravel Lumen support [(#685)](https://github.com/getsentry/sentry-laravel/pull/685) - - We dropped support for Laravel Lumen (10.x) in v3.0.0, but we are happy to announce that it is back! - -## 3.3.3 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.3.3. - -### Bug Fixes - -- Fix `CheckIn` constructor argument order [(#680)](https://github.com/getsentry/sentry-laravel/pull/680) -- Fix missing breadcrumbs for jobs throwing an exception [(#633)](https://github.com/getsentry/sentry-laravel/pull/633) - -## 3.3.2 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.3.2. - -### Bug Fixes - -- Fix "Object of class Closure could not be converted to string" error when tracing `redis_commands` [(#668)](https://github.com/getsentry/sentry-laravel/pull/668) - -## 3.3.1 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.3.1. - -### Bug Fixes - -- Fix scheduled commands running in the background not reporting success/failure [(#664)](https://github.com/getsentry/sentry-laravel/pull/664) - -## 3.3.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.3.0. -This release adds initial support for [Cron Monitoring](https://docs.sentry.io/product/crons/) as well as new performance spans and breadcrumbs. - -> **Warning** -> Cron Monitoring is currently in beta. Beta features are still in-progress and may have bugs. We recognize the irony. -> If you have any questions or feedback, please email us at crons-feedback@sentry.io, reach out via Discord (#cronjobs), or open an issue. - -### Features - -- Add inital support for Cron Monitoring [(#659)](https://github.com/getsentry/sentry-laravel/pull/659) - - After creating your Cron Monitor on https://sentry.io, you can add the `sentryMonitor()` macro to your scheduled tasks defined in your `app/Console/Kernel.php` file. - This will let Sentry know if your scheduled task started, whether the task was successful or failed, and its duration. - - ```php - protected function schedule(Schedule $schedule) - { - $schedule->command('emails:send') - ->everyHour() - ->sentryMonitor(''); // add this line - } - ``` - -- Add Livewire tracing integration [(#657)](https://github.com/getsentry/sentry-laravel/pull/657) - - You can enable this feature by adding new config options to your `config/sentry.php` file. - - ```php 'breadcrumbs' => [ // Capture Livewire components in breadcrumbs 'livewire' => true, @@ -381,451 +72,46 @@ This release adds initial support for [Cron Monitoring](https://docs.sentry.io/p ], ``` -- Add Redis operation spans & cache event breadcrumbs [(#656)](https://github.com/getsentry/sentry-laravel/pull/656) - - You can enable this feature by adding new config options to your `config/sentry.php` file. +- Add new fluent APIs [(#1601)](https://github.com/getsentry/sentry-php/pull/1601) ```php - 'breadcrumbs' => [ - // Capture Laravel cache events in breadcrumbs - 'cache' => true, - ], - 'tracing' => [ - // Capture Redis operations as spans (this enables Redis events in Laravel) - 'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false), - - // Try to find out where the Redis command originated from and add it to the command spans - 'redis_origin' => true, - ], - -- Add HTTP client request breadcrumbs [(#640)](https://github.com/getsentry/sentry-laravel/pull/640) + // Before + $spanContext = new SpanContext(); + $spanContext->setDescription('myFunction'); + $spanContext->setOp('function'); + + // After + $spanContext = (new SpanContext()) + ->setDescription('myFunction'); + ->setOp('function'); + ``` - You can enable this feature by adding a new config option to your `config/sentry.php` file. +- Simplify the breadcrumb API [(#1603)](https://github.com/getsentry/sentry-php/pull/1603) ```php - 'breadcrumbs' => [ - // Capture HTTP client requests information in breadcrumbs - 'http_client_requests' => true, - ], - -- Offer the installation of a JavaScript SDK when running `sentry:publish` [(#647)](https://github.com/getsentry/sentry-laravel/pull/647) - -### Bug Fixes - -- Fix a log channel context crash when unexpected values are passed [(#651)](https://github.com/getsentry/sentry-laravel/pull/651) - -### Misc - -- The SDK is now licensed under MIT [(#654)](https://github.com/getsentry/sentry-php/pull/654) - - Read more about Sentry's licensing [here](https://open.sentry.io/licensing/). - -## 3.2.0 - -The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.2.0. -This release adds support for Laravel 10. - -### Features - -- Add support for Laravel 10 [(#630)](https://github.com/getsentry/sentry-laravel/pull/630) - - Thanks to [@jnoordsij](https://github.com/jnoordsij) for their contribution. -- Add `tracing.http_client_requests` option [(#641)](https://github.com/getsentry/sentry-laravel/pull/641) - - You can now disable HTTP client tracing in your `config/sentry.php` file - - ```php - 'tracing' => [ - 'http_client_requests' => true|false, // This feature is enabled by default - ], - ``` - -## 3.1.3 - -- Increase debug trace limit count to 20 in `Integration::makeAnEducatedGuessIfTheExceptionMaybeWasHandled()` (#622) - - Look futher into the backtrace to check if `report()` was called. -- Run the testsuite against PHP 8.2 (#624) - -## 3.1.2 - -- Set `traces_sample_rate` to `null` by default (#616) - - Make sure to update your `config/sentry.php`. - - Replace - ``` - 'traces_sample_rate' => (float)(env('SENTRY_TRACES_SAMPLE_RATE', 0.0)), - ``` - with - ``` - 'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_TRACES_SAMPLE_RATE'), - ``` -- Fix exceptions sent via the `report()` helper being marked as unhandled (#617) - -## 3.1.1 - -- Fix missing scope information on unhandled exceptions (#611) - -## 3.1.0 - -- Unhandled exceptions are now correctly marked as `handled: false` and displayed as such on the issues list and detail page (#608) - - Make sure to update your `App/Exceptions/Handler.php` file to enable this new behaviour. See https://docs.sentry.io/platforms/php/guides/laravel/ - -## 3.0.1 - -- Remove incorrect checks if performance tracing should be enabled and rely on the transaction sampling decision instead (#600) -- Fix `SENTRY_RELEASE` .env variable not working when using config caching (#603) - -## 3.0.0 - -**New features** - -- We are now creating more spans to give you better insights into the performance of your application - - Add a `http.client` span. This span indicates the time that is spent when using the Laravel HTTP client (#585) - - Add a `http.route` span. This span indicates the time that is spent inside a controller method or route closure (#593) - - Add a `db.transaction` span. This span indicates the time that is spent inside a database transaction (#594) -- Add support for [Dynamic Sampling](https://docs.sentry.io/product/data-management-settings/dynamic-sampling/), allowing developers to set a server-side sampling rate without the need to re-deploy their applications - - Add support for Dynamic Sampling (#572) - -**Breaking changes** - -- Laravel Lumen is no longer supported - - Drop support for Laravel Lumen (#579) -- Laravel versions 5.0 - 5.8 are no longer supported - - Drop support for Laravel 5.x (#581) -- Remove `Sentry\Integration::extractNameForRoute()`, it's alternative `Sentry\Integration::extractNameAndSourceForRoute()` is marked as `@internal` (#580) -- Remove internal `Sentry\Integration::currentTracingSpan()`, use `SentrySdk::getCurrentHub()->getSpan()` if you were using this internal method (#592) - -**Other changes** - -- Set the tracing transaction name on the `Illuminate\Routing\Events\RouteMatched` instead of at the end of the request (#580) -- Remove extracting route name or controller for transaction names (#583). This unifies the transaction names to a more concise format. -- Simplify Sentry meta tag retrieval, by adding `Sentry\Laravel\Integration::sentryMeta()` (#586) -- Fix tracing with nested queue jobs (mostly when running jobs in the `sync` driver) (#592) - -## 2.14.2 - -- Fix extracting command input resulting in errors when calling Artisan commands programatically with `null` as an argument value (#589) - -## 2.14.1 - -- Fix not setting the correct SDK ID and version when running the `sentry:test` command (#582) -- Transaction names now only show the parameterized URL (`/some/{route}`) instead of the route name or controller class (#583) - -## 2.14.0 - -- Fix not listening to queue events because `QueueManager` is registered as `queue` in the container and not by it's class name (#568) -- Fix status code not populated on transaction if response did not inherit from `Illuminate\Http\Response` like `Illuminate\Http\JsonResponse` (#573) -- Align Span Operations with new spec (#574) -- Fix broken `SetRequestMiddleware` on Laravel < 6.0 (#575) -- Also extract the authenticated user `email` and `username` attributes if available (#577) - -## 2.13.0 - -- Only catch `BindingResolutionException` when trying to get the PSR-7 request object from the container - -## 2.12.1 - -- Fix incorrect `release` and `environment` values when using the `sentry:test` command - -## 2.12.0 - -- Add support for normalized route names when using [Laravel Lumen](https://lumen.laravel.com/docs/9.x) (#449) -- Add support for adding the user ID to the user scope when using [Laravel Sanctum](https://laravel.com/docs/9.x/sanctum) (#542) -- Allow configuration of the [`send_default_pii`](https://docs.sentry.io/platforms/php/configuration/options/#send-default-pii) SDK option with the `SENTRY_SEND_DEFAULT_PII` env variable - -## 2.11.1 - -- Fix deprecation notice in route name extraction (#543) - -## 2.11.0 - -- Add support for Laravel 9 (#534) -- Fix double wrapping the log channel in a `FingersCrossedHandler` on Laravel `v8.97` and newer when `action_level` option is set on the Log channel config (#534) -- Update span operation names to match what Sentry server is expecting (#533) - -## 2.10.2 - -- Fix `sentry:test` command not having correct exit code on success - -## 2.10.1 - -- Fix compatibility with Laravel <= 6 of the `sentry:test` and `sentry:publish` commands - -## 2.10.0 - -- Improve output and DX for `sentry:test` and `sentry:publish` commands (#522) - -## 2.9.0 - -- Add support for Laravel Octane (#495) -- Fix bug in Sentry log channel handler checking an undefined variable resulting in an error (#515) -- Add `action_level` configuration option for Sentry log channel which configures a Monolog `FingersCrossedHandler` (#516) - -## 2.8.0 - -- Update phpdoc on facade for better IDE autocompletion (#504) -- Exceptions captured using log channels (Monolog) will now have the correct severity set (#505) -- Tags passed through log channels (Monolog) context are cast as string to prevent type errors (#507) -- Add options to the `artisan sentry:publish` command to better support `--no-interaction` mode (#509) - -## 2.7.0 - -- Replace type hint of concrete type (`Sentry\State\Hub`) with interface (`Sentry\State\HubInterface`) in `SentryHandler` constructor (#496) -- Use latest version of the Sentry PHP SDK (#499) - -## 2.6.0 - -- Add all log context as `log_context` to events when using the log channel (#489) -- Add integration to improve performance tracing for [Laravel Lighthouse](https://github.com/nuwave/lighthouse) (#490) - -## 2.5.3 - -- Correctly call flush on the PHP SDK client (#484) -- Fix errors on Laravel `5.x` caused by Laravel not using `nyholm/psr7` to generate PSR-7 request but older `zendframework/zend-diactoros` package which might not be available - -## 2.5.2 - -- Fix problem with parsing uploaded files from request after they have been moved (#487) - -## 2.5.1 - -- Fix problem with queue tracing when triggered from unit tests or when missing a queue name in the event - -## 2.5.0 - -- Add `sql.origin` to SQL query spans with the file and line where the SQL query originated from (#398) -- Remove wrapper around the context of log entry breadcrumbs (#405) -- Ensure user integrations are always executed after SDK integrations (#474) -- Fix repeated booted callback registration from performance tracing middleware (#475) -- Add tracing support for queue jobs, enable with `SENTRY_TRACE_QUEUE_ENABLED=true` (#478) -- Add options to disable parts of performance tracing (#478) -- Remove string representation of exception from exceptions logged through log channels (#482) -- Use message from Monolog record to prevent bloating the log message being recorded with timestamps and log log level (#482) -- Add `report_exceptions` option to the Sentry log channel that can be set to `false` to not report exceptions (#482) - -## 2.4.2 - -- Avoid collision if another package has bound `sentry` in the Laravel container (#467) - -## 2.4.1 - -- Fix type hints incompatible with Laravel Lumen (#462) - -## 2.4.0 - -- Read the request IP from the Laravel request to make it more accurate when behind a reverse proxy (requires [trusted proxies](https://laravel.com/docs/8.x/requests#configuring-trusted-proxies) to be setup correctly) (#419) -- Get request information (like the URL) from the Laravel request instead of constructing it from the global state (#419) -- Fix generated route name not correctly ignored when using prefix (#441) -- Fix overwriting the transaction name if it's set by the user (#442) -- Add result from optional `context(): array` method on captured exception to the event sent to Sentry (#457) -- Fix not overwriting the event transaction name if it was an empty string (#460) -- Bump Sentry SDK to `3.2.*` - -## 2.3.1 - -- Fix problems when enabling tracing on Laravel Lumen (#416) -- PHP 8 Support (#431) - -## 2.3.0 - -- Bump Sentry SDK to `3.1.*` (#420) - -## 2.2.0 - -- Fix incorrectly stripped base controller action from transaction name (#406) -- Move tracing request/response data hydration to the tracing middleware (#408) - -## 2.1.1 - -- Fix for potential `Undefined index: controllers_base_namespace.` notice - -## 2.1.0 - -- Added a option (`controllers_base_namespace`) to strip away the controller base namespace for cleaner transaction names (#393) -- Fix incompatibility with other packages that also decorate the view engine, like Livewire (#395) - -## 2.0.1 - -- Improve performance tracing by nesting `view.render` spans and adding a `app.handle` span showing how long the actual application code runs after Laravel bootstrapping (#387) -- Improve UX of `sentry:publish` command - -## 2.0.0 - -**Breaking Change**: This version uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/). If you are -using an on-premise installation it requires Sentry version `>= v20.6.0` to work. If you are using -[sentry.io](https://sentry.io) nothing will change and no action is needed. - -**Tracing API / Monitor Performance** - -In this version we released API for Tracing. `\Sentry\startTransaction` is your entry point for manual instrumentation. -More information can be found in our [Performance](https://docs.sentry.io/platforms/php/guides/laravel/performance/) docs. - -- Using `^3.0` of Sentry PHP SDK -- Add support for Tracing, enable it by setting `traces_sample_rate` in the config to a value > 0 (the value should be larger than `0.0` and smaller or equal than `1.0` (to send everything)) - -## 2.0.0-beta1 - -**Breaking Change**: This version uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/). If you are -using an on-premise installation it requires Sentry version `>= v20.6.0` to work. If you are using -[sentry.io](https://sentry.io) nothing will change and no action is needed. - -- Using `3.0.0-beta1` of Sentry PHP SDK -- Add support for Tracing, enable it by setting `traces_sample_rate` in the config to a value > 0 (the value should be larger than `0.0` and smaller or equal than `1.0` (to send everything)) - -## 1.9.0 - -- Respect the `SENTRY_ENVIRONMENT` environment variable to override the Laravel environment (#354) -- Support for Laravel 8 (#374) - -## 1.8.0 - -- Add `send_default_pii` option by default to published config file (#340) -- Update `.gitattributes` to exclude more files from dist release (#341) -- Ignore log breadcrumbs when `null` is the message logged (#345) -- Fix `breadcrumbs.queue_info` controlling breadcrumbs generated by commands (#350) -- Add `breadcrumbs.command_info` to control breadcrumbs generated by commands (#350) -- Fixed scope data in queue jobs being lost in some cases (#351) - -## 1.7.1 - -- Discard Laravel 7 route cache generated route names (#337) - -## 1.7.0 - -- Support for Laravel 7 (#330) - -## 1.6.2 - -- Fix for default integrations not disabled (#327) - -## 1.6.1 - -- Fix queue events with missing handler suffix (#322) - -## 1.6.0 - -- Use default breadcrumb type for handled events (#303) -- Support Sentry SDK ^2.3 (and drop support for older versions) (#316) -- Fix queue events to correctly flush events when not running a queue deamon (#318) - -## 1.5.0 - -- Fix throwing errors when installed when config cache is active (6214338) -- Allow any log level to create breadcrumbs (#297) -- Allow decorating the `ClientBuilderInterface` from the `register` method of a Service Provider (#290) - -## 1.4.1 - -- Fix default Monolog logger level being invalid when using the Log channel (#287) - -## 1.4.0 - -- Add the query execution time to the query breadcrumb (#283) -- Do not register default error and fatal listeners to prevent duplicated events (#280) - -## 1.3.1 - -- Fix compatibility with sentry/sentry 2.2+ (#276) - -## 1.3.0 - -- Add compatibility with sentry/sentry 2.2+ (#273) - -## 1.2.1 - -- Fix fatal error when user context is not an array when using log channels (#272) - -## 1.2.0 - -- Support for Laravel 6 (#269) - -## 1.1.1 - -- Fix custom container alias (#263) - -## 1.1.0 - -- Register alias `HubInterface` to container (#249) -- Resolve `integrations` option from the container (#239) - -## 1.0.2 - -- Track Artisan command invocation in breadcrumb (#232) -- Fixed `sql_bindings` configuration fallback (#231) -- Fixed events generated in queue worker not sending until worker exits (#228) -- Add phpDoc methods to the facade for better autocompletion (#226) -- Fallback to `SENTRY_DSN` if defined in env (#224) - -## 1.0.1 - -- Fix the configuration syntax for the sql bindings in breadcrumbs configuration option to be compatible with Laravel (#207) -- Prevent registering events when no DSN is set (#205) - -## 1.0.0 - -- This version requires `sentry/sentry` `>= 2.0` and also PHP `>= 7.1` -- Support for Laravel 5.8 -- Be advised `app('sentry')` now no longer returns the "old" `Raven_Client` instead it will return `\Sentry\State\Hub` - -Please see [Docs](https://docs.sentry.io/platforms/php/laravel/) for detailed usage. - -## 0.11.0 - -- Correctly merge the user config with the default configuration file (#163) -- Listen for queue events and flush the send queue and breadcrum queue (#153) -- Add tag with the console command name to the event (#146) - -## 0.10.1 - -- Fix support for Laravel 5.0. - -## 0.10.0 - -- Support for Laravel 5.7. - -## 0.9.2 - -- The `sentry:test` artisan command no longer requires the secret key in the DSN (secret key in DSN deprecated since Sentry 9). - -## 0.9.1 - -- Allow setting custom formatter for the log channel. (#145) - -## 0.9.0 - -This version no longer supports Laravel 4.x, version `0.8.x` will of course still work for Laravel 4. - -- Set 'user_context' configuration default to false. (#132) -- Update `SENTRY_DSN` env variable name to `SENTRY_LARAVEL_DSN`. (#130) -- Improved default app_path for Lumen to include entire application code, excluding vendor. (#128) -- Remove Laravel 4 support. (#123) -- Add support for Laravel 5.6 log channels. (#122) -- Type hint Laravel contracts instead of implementation. (#107) - -## 0.8.0 - -- Improved default app_path to include entire application code, excluding vendor. (#89) -- Fix for auth context not working properly on Laravel >=5.3. (#81) -- Support Laravel auto-discovery. (#78) - -## 0.7.0 - -- Added 'sentry:test' to Artisan. (#65) -- Added 'user_context' configuration to disable automatic collection. (#55) - -## 0.6.1 - -- Various fixes for query event breadcrumbs. (#54) - -## 0.6.0 - -- Support for Laravel 5.4. - -## 0.5.0 + // Before + \Sentry\addBreadcrumb( + new \Sentry\Breadcrumb( + \Sentry\Breadcrumb::LEVEL_INFO, + \Sentry\Breadcrumb::TYPE_DEFAULT, + 'auth', // category + 'User authenticated', // message (optional) + ['user_id' => $userId] // data (optional) + ) + ); + + // After + \Sentry\addBreadcrumb( + category: 'auth', + message: 'User authenticated', // optional + metadata: ['user_id' => $userId], // optional + level: Breadcrumb::LEVEL_INFO, // set by default + type: Breadcrumb::TYPE_DEFAULT, // set by default + ); + ``` -- Require sentry/sentry >= 1.6.0. -- Allow overriding abstract type Sentry is bound to in service container. +- New default cURL HTTP client [(#1589)](https://github.com/getsentry/sentry-php/pull/1589) -## 0.4.0 +# Misc -- Require sentry/sentry >= 1.5.0. -- Added support for Illuminate SQL queries in breadcrumbs. -- Replaced Monolog breadcrumb handler with Illuminate log handler. -- Added route transaction names. +- The abandoned package `php-http/message-factory` was removed. diff --git a/codecov.yml b/codecov.yml index 7303073c..45f5faba 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,5 @@ comment: false coverage: status: - project: - default: - threshold: 0.1% # allow for 0.1% reduction of coverage without failing + project: off + patch: off diff --git a/composer.json b/composer.json index 9ae61e4c..a477bc8d 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,9 @@ "error-monitoring", "error-handler", "crash-reporting", - "crash-reports" + "crash-reports", + "profiling", + "tracing" ], "homepage": "https://sentry.io", "license": "MIT", @@ -23,8 +25,7 @@ "require": { "php": "^7.2 | ^8.0", "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", - "sentry/sentry": "^3.20.1", - "sentry/sdk": "^3.4", + "sentry/sentry": "^4.0", "symfony/psr-http-message-bridge": "^1.0 | ^2.0", "nyholm/psr7": "^1.0" }, @@ -34,13 +35,15 @@ } }, "require-dev": { - "phpunit/phpunit": "^8.4 | ^9.3", + "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4", "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "livewire/livewire": "^2.0 | ^3.0", "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0", "friendsofphp/php-cs-fixer": "^3.11", "mockery/mockery": "^1.3", "phpstan/phpstan": "^1.10", - "laravel/folio": "^1.0" + "laravel/folio": "^1.0", + "guzzlehttp/guzzle": "^7.2" }, "autoload-dev": { "psr-4": { @@ -48,21 +51,17 @@ } }, "scripts": { - "test": "vendor/bin/phpunit", - "tests": "@test", - "test:ci": "vendor/bin/phpunit --coverage-clover=coverage.xml", - "phpcs": "vendor/bin/php-cs-fixer fix", - "phpcs:ci": "vendor/bin/php-cs-fixer fix --dry-run --diff", - "phpstan": "vendor/bin/phpstan analyse", - "phpstan:ci": "vendor/bin/phpstan analyse --error-format github" + "check": [ + "@cs-check", + "@phpstan", + "@tests" + ], + "tests": "vendor/bin/phpunit --verbose", + "cs-check": "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", + "cs-fix": "vendor/bin/php-cs-fixer fix --verbose --diff", + "phpstan": "vendor/bin/phpstan analyse" }, "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev", - "dev-0.x": "0.x-dev" - }, "laravel": { "providers": [ "Sentry\\Laravel\\ServiceProvider", @@ -73,12 +72,8 @@ } } }, - "prefer-stable": true, - "minimum-stability": "dev", "config": { - "allow-plugins": { - "kylekatarnls/update-helper": false, - "php-http/discovery": false - } - } + "sort-packages": true + }, + "prefer-stable": true } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c5a17963..37376b27 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -90,31 +90,6 @@ parameters: count: 1 path: src/Sentry/Laravel/EventHandler.php - - - message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBoot\\(\\) has invalid type Livewire\\\\Component\\.$#" - count: 1 - path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php - - - - message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Component\\.$#" - count: 1 - path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php - - - - message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentDehydrate\\(\\) has invalid type Livewire\\\\Component\\.$#" - count: 1 - path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php - - - - message: "#^Parameter \\$component of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentMount\\(\\) has invalid type Livewire\\\\Component\\.$#" - count: 1 - path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php - - - - message: "#^Parameter \\$livewireManager of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:onBoot\\(\\) has invalid type Livewire\\\\LivewireManager\\.$#" - count: 1 - path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php - - message: "#^Parameter \\$request of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Request\\.$#" count: 1 diff --git a/src/Sentry/Laravel/EventHandler.php b/src/Sentry/Laravel/EventHandler.php index 58b2b1b7..7a9eddaa 100644 --- a/src/Sentry/Laravel/EventHandler.php +++ b/src/Sentry/Laravel/EventHandler.php @@ -11,7 +11,6 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Events as DatabaseEvents; -use Illuminate\Http\Client\Events as HttpClientEvents; use Illuminate\Http\Request; use Illuminate\Log\Events as LogEvents; use Illuminate\Routing\Events as RoutingEvents; @@ -19,7 +18,7 @@ use Laravel\Sanctum\Events as Sanctum; use RuntimeException; use Sentry\Breadcrumb; -use Sentry\Laravel\Util\WorksWithUris; +use Sentry\Laravel\Tracing\Middleware; use Sentry\SentrySdk; use Sentry\State\Scope; use Symfony\Component\Console\Input\ArgvInput; @@ -27,8 +26,6 @@ class EventHandler { - use WorksWithUris; - /** * Map event handlers to events. * @@ -40,8 +37,6 @@ class EventHandler DatabaseEvents\QueryExecuted::class => 'queryExecuted', ConsoleEvents\CommandStarting::class => 'commandStarting', ConsoleEvents\CommandFinished::class => 'commandFinished', - HttpClientEvents\ResponseReceived::class => 'httpClientResponseReceived', - HttpClientEvents\ConnectionFailed::class => 'httpClientConnectionFailed', ]; /** @@ -122,13 +117,6 @@ class EventHandler */ private $recordOctaneTaskInfo; - /** - * Indicates if we should add HTTP client requests info to the breadcrumbs. - * - * @var bool - */ - private $recordHttpClientRequests; - /** * Indicates if we pushed a scope for Octane. * @@ -152,7 +140,6 @@ public function __construct(Container $container, array $config) $this->recordCommandInfo = ($config['breadcrumbs.command_info'] ?? $config['breadcrumbs']['command_info'] ?? true) === true; $this->recordOctaneTickInfo = ($config['breadcrumbs.octane_tick_info'] ?? $config['breadcrumbs']['octane_tick_info'] ?? true) === true; $this->recordOctaneTaskInfo = ($config['breadcrumbs.octane_task_info'] ?? $config['breadcrumbs']['octane_task_info'] ?? true) === true; - $this->recordHttpClientRequests = ($config['breadcrumbs.http_client_requests'] ?? $config['breadcrumbs']['http_client_requests'] ?? true) === true; } /** @@ -216,6 +203,8 @@ protected function routeMatchedHandler(RoutingEvents\RouteMatched $match): void return; } + Middleware::signalRouteWasMatched(); + [$routeName] = Integration::extractNameAndSourceForRoute($match->route); Integration::addBreadcrumb(new Breadcrumb( @@ -275,59 +264,6 @@ protected function messageLoggedHandler(LogEvents\MessageLogged $logEntry): void )); } - protected function httpClientResponseReceivedHandler(HttpClientEvents\ResponseReceived $event): void - { - if (!$this->recordHttpClientRequests) { - return; - } - - $level = Breadcrumb::LEVEL_INFO; - if ($event->response->failed()) { - $level = Breadcrumb::LEVEL_ERROR; - } - - $fullUri = $this->getFullUri($event->request->url()); - - Integration::addBreadcrumb(new Breadcrumb( - $level, - Breadcrumb::TYPE_HTTP, - 'http', - null, - [ - 'url' => $this->getPartialUri($fullUri), - 'http.request.method' => $event->request->method(), - 'http.response.status_code' => $event->response->status(), - 'http.query' => $fullUri->getQuery(), - 'http.fragment' => $fullUri->getFragment(), - 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), - 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), - ] - )); - } - - protected function httpClientConnectionFailedHandler(HttpClientEvents\ConnectionFailed $event): void - { - if (!$this->recordHttpClientRequests) { - return; - } - - $fullUri = $this->getFullUri($event->request->url()); - - Integration::addBreadcrumb(new Breadcrumb( - Breadcrumb::LEVEL_ERROR, - Breadcrumb::TYPE_HTTP, - 'http', - null, - [ - 'url' => $this->getPartialUri($fullUri), - 'http.request.method' => $event->request->method(), - 'http.query' => $fullUri->getQuery(), - 'http.fragment' => $fullUri->getFragment(), - 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), - ] - )); - } - protected function authenticatedHandler(AuthEvents\Authenticated $event): void { $this->configureUserScopeFromModel($event->user); diff --git a/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.php b/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.php new file mode 100644 index 00000000..922458f7 --- /dev/null +++ b/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.php @@ -0,0 +1,75 @@ + + */ + private $parentSpanStack = []; + + /** + * Hold the stack of current spans that need to be finished still. + * + * @var array + */ + private $currentSpanStack = []; + + protected function pushSpan(Span $span): void + { + $hub = SentrySdk::getCurrentHub(); + + $this->parentSpanStack[] = $hub->getSpan(); + + $hub->setSpan($span); + + $this->currentSpanStack[] = $span; + } + + protected function pushScope(): void + { + SentrySdk::getCurrentHub()->pushScope(); + + ++$this->pushedScopeCount; + } + + protected function maybePopSpan(): ?Span + { + if (count($this->currentSpanStack) === 0) { + return null; + } + + $parent = array_pop($this->parentSpanStack); + + SentrySdk::getCurrentHub()->setSpan($parent); + + return array_pop($this->currentSpanStack); + } + + protected function maybePopScope(): void + { + Integration::flushEvents(); + + if ($this->pushedScopeCount === 0) { + return; + } + + SentrySdk::getCurrentHub()->popScope(); + + --$this->pushedScopeCount; + } +} diff --git a/src/Sentry/Laravel/Features/FolioPackageIntegration.php b/src/Sentry/Laravel/Features/FolioPackageIntegration.php index 9031bf96..f035ad7c 100644 --- a/src/Sentry/Laravel/Features/FolioPackageIntegration.php +++ b/src/Sentry/Laravel/Features/FolioPackageIntegration.php @@ -10,6 +10,7 @@ use Laravel\Folio\Pipeline\MatchedView; use Sentry\Breadcrumb; use Sentry\Laravel\Integration; +use Sentry\Laravel\Tracing\Middleware; use Sentry\SentrySdk; use Sentry\Tracing\TransactionSource; @@ -29,6 +30,8 @@ public function onBoot(Dispatcher $events): void public function handleViewMatched(ViewMatched $matched): void { + Middleware::signalRouteWasMatched(); + $routeName = $this->extractRouteForMatchedView($matched->matchedView, $matched->mountPath); Integration::addBreadcrumb(new Breadcrumb( diff --git a/src/Sentry/Laravel/Features/HttpClientIntegration.php b/src/Sentry/Laravel/Features/HttpClientIntegration.php new file mode 100644 index 00000000..be57e7f5 --- /dev/null +++ b/src/Sentry/Laravel/Features/HttpClientIntegration.php @@ -0,0 +1,207 @@ +isTracingFeatureEnabled(self::FEATURE_KEY) + || $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); + } + + public function onBoot(Dispatcher $events, Factory $factory): void + { + if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { + $events->listen(RequestSending::class, [$this, 'handleRequestSendingHandlerForTracing']); + $events->listen(ResponseReceived::class, [$this, 'handleResponseReceivedHandlerForTracing']); + $events->listen(ConnectionFailed::class, [$this, 'handleConnectionFailedHandlerForTracing']); + + // The `globalRequestMiddleware` functionality was introduced in Laravel 10.14 + if (method_exists($factory, 'globalRequestMiddleware')) { + $factory->globalRequestMiddleware([$this, 'attachTracingHeadersToRequest']); + } + } + + if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { + $events->listen(ResponseReceived::class, [$this, 'handleResponseReceivedHandlerForBreadcrumb']); + $events->listen(ConnectionFailed::class, [$this, 'handleConnectionFailedHandlerForBreadcrumb']); + } + } + + public function attachTracingHeadersToRequest(RequestInterface $request) + { + if ($this->shouldAttachTracingHeaders($request)) { + return $request + ->withHeader('baggage', getBaggage()) + ->withHeader('sentry-trace', getTraceparent()); + } + + return $request; + } + + public function handleRequestSendingHandlerForTracing(RequestSending $event): void + { + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); + + // If there is no tracing span active there is no need to handle the event + if ($parentSpan === null) { + return; + } + + $context = new SpanContext; + + $fullUri = $this->getFullUri($event->request->url()); + $partialUri = $this->getPartialUri($fullUri); + + $context->setOp('http.client'); + $context->setDescription($event->request->method() . ' ' . $partialUri); + $context->setData([ + 'url' => $partialUri, + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.query' => $fullUri->getQuery(), + 'http.fragment' => $fullUri->getFragment(), + 'http.request.method' => $event->request->method(), + 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), + ]); + + $this->pushSpan($parentSpan->startChild($context)); + } + + public function handleResponseReceivedHandlerForTracing(ResponseReceived $event): void + { + $span = $this->maybePopSpan(); + + if ($span !== null) { + $span->finish(); + $span->setData(array_merge($span->getData(), [ + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.response.status_code' => $event->response->status(), + 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), + ])); + $span->setHttpStatus($event->response->status()); + } + } + + public function handleConnectionFailedHandlerForTracing(ConnectionFailed $event): void + { + $span = $this->maybePopSpan(); + + if ($span !== null) { + $span->finish(); + $span->setStatus(SpanStatus::internalError()); + } + } + + public function handleResponseReceivedHandlerForBreadcrumb(ResponseReceived $event): void + { + $level = Breadcrumb::LEVEL_INFO; + + if ($event->response->failed()) { + $level = Breadcrumb::LEVEL_ERROR; + } + + $fullUri = $this->getFullUri($event->request->url()); + + Integration::addBreadcrumb(new Breadcrumb( + $level, + Breadcrumb::TYPE_HTTP, + 'http', + null, + [ + 'url' => $this->getPartialUri($fullUri), + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.query' => $fullUri->getQuery(), + 'http.fragment' => $fullUri->getFragment(), + 'http.request.method' => $event->request->method(), + 'http.response.status_code' => $event->response->status(), + 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), + 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), + ] + )); + } + + public function handleConnectionFailedHandlerForBreadcrumb(ConnectionFailed $event): void + { + $fullUri = $this->getFullUri($event->request->url()); + + Integration::addBreadcrumb(new Breadcrumb( + Breadcrumb::LEVEL_ERROR, + Breadcrumb::TYPE_HTTP, + 'http', + null, + [ + 'url' => $this->getPartialUri($fullUri), + // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http + 'http.query' => $fullUri->getQuery(), + 'http.fragment' => $fullUri->getFragment(), + 'http.request.method' => $event->request->method(), + 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), + ] + )); + } + + /** + * Construct a full URI. + * + * @param string $url + * + * @return UriInterface + */ + private function getFullUri(string $url): UriInterface + { + return new Uri($url); + } + + /** + * Construct a partial URI, excluding the authority, query and fragment parts. + * + * @param UriInterface $uri + * + * @return string + */ + private function getPartialUri(UriInterface $uri): string + { + return (string)Uri::fromParts([ + 'scheme' => $uri->getScheme(), + 'host' => $uri->getHost(), + 'port' => $uri->getPort(), + 'path' => $uri->getPath(), + ]); + } + + private function shouldAttachTracingHeaders(RequestInterface $request): bool + { + $client = SentrySdk::getCurrentHub()->getClient(); + if ($client === null) { + return false; + } + + $sdkOptions = $client->getOptions(); + + // Check if the request destination is allow listed in the trace_propagation_targets option. + return $sdkOptions->getTracePropagationTargets() === null + || in_array($request->getUri()->getHost(), $sdkOptions->getTracePropagationTargets()); + } +} diff --git a/src/Sentry/Laravel/Features/LivewirePackageIntegration.php b/src/Sentry/Laravel/Features/LivewirePackageIntegration.php index 3c794bfa..acb46f4b 100644 --- a/src/Sentry/Laravel/Features/LivewirePackageIntegration.php +++ b/src/Sentry/Laravel/Features/LivewirePackageIntegration.php @@ -3,23 +3,21 @@ namespace Sentry\Laravel\Features; use Livewire\Component; +use Livewire\EventBus; use Livewire\LivewireManager; use Livewire\Request; use Sentry\Breadcrumb; +use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans; use Sentry\Laravel\Integration; use Sentry\SentrySdk; -use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TransactionSource; class LivewirePackageIntegration extends Feature { - private const FEATURE_KEY = 'livewire'; - - private const COMPONENT_SPAN_OP = 'ui.livewire.component'; + use TracksPushedScopesAndSpans; - /** @var array */ - private $spanStack = []; + private const FEATURE_KEY = 'livewire'; public function isApplicable(): bool { @@ -32,11 +30,56 @@ public function isApplicable(): bool } public function onBoot(LivewireManager $livewireManager): void + { + if (class_exists(EventBus::class)) { + $this->registerLivewireThreeEventListeners($livewireManager); + + return; + } + + $this->registerLivewireTwoEventListeners($livewireManager); + } + + private function registerLivewireThreeEventListeners(LivewireManager $livewireManager): void + { + $livewireManager->listen('mount', function (Component $component, array $data) { + if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { + $this->handleComponentBoot($component); + } + + if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { + $this->handleComponentMount($component, $data); + } + }); + + $livewireManager->listen('hydrate', function (Component $component) { + if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { + $this->handleComponentBoot($component); + } + + if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { + $this->handleComponentHydrate($component); + } + }); + + if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { + $livewireManager->listen('dehydrate', [$this, 'handleComponentDehydrate']); + } + + if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { + $livewireManager->listen('call', [$this, 'handleComponentCall']); + } + } + + private function registerLivewireTwoEventListeners(LivewireManager $livewireManager): void { $livewireManager->listen('component.booted', [$this, 'handleComponentBooted']); if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { - $livewireManager->listen('component.boot', [$this, 'handleComponentBoot']); + $livewireManager->listen('component.boot', function ($component) { + $this->handleComponentBoot($component); + }); + $livewireManager->listen('component.dehydrate', [$this, 'handleComponentDehydrate']); } @@ -45,23 +88,38 @@ public function onBoot(LivewireManager $livewireManager): void } } - public function handleComponentBoot(Component $component): void + public function handleComponentCall(Component $component, string $method, array $arguments): void + { + Integration::addBreadcrumb(new Breadcrumb( + Breadcrumb::LEVEL_INFO, + Breadcrumb::TYPE_DEFAULT, + 'livewire', + "Component call: {$component->getName()}::{$method}", + $this->mapCallArgumentsToMethodParameters($component, $method, $arguments) ?? ['arguments' => $arguments] + )); + } + + public function handleComponentBoot(Component $component, ?string $method = null): void { - $currentSpan = SentrySdk::getCurrentHub()->getSpan(); + if ($this->isLivewireRequest()) { + $this->updateTransactionName($component->getName()); + } - if ($currentSpan === null) { + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); + + if ($parentSpan === null) { return; } - $this->spanStack[] = $currentSpan; - $context = new SpanContext; - $context->setOp(self::COMPONENT_SPAN_OP); - $context->setDescription($component->getName()); - - $componentSpan = $currentSpan->startChild($context); - - SentrySdk::getCurrentHub()->setSpan($componentSpan); + $context->setOp('ui.livewire.component'); + $context->setDescription( + empty($method) + ? $component->getName() + : "{$component->getName()}::{$method}" + ); + + $this->pushSpan($parentSpan->startChild($context)); } public function handleComponentMount(Component $component, array $data): void @@ -92,23 +150,28 @@ public function handleComponentBooted(Component $component, Request $request): v } if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { - $this->updateTransactionName($component::getName()); + $this->updateTransactionName($component->getName()); } } + public function handleComponentHydrate(Component $component): void + { + Integration::addBreadcrumb(new Breadcrumb( + Breadcrumb::LEVEL_INFO, + Breadcrumb::TYPE_DEFAULT, + 'livewire', + "Component hydrate: {$component->getName()}", + $component->all() + )); + } + public function handleComponentDehydrate(Component $component): void { - $currentSpan = SentrySdk::getCurrentHub()->getSpan(); + $span = $this->maybePopSpan(); - if ($currentSpan === null || empty($this->spanStack)) { - return; + if ($span !== null) { + $span->finish(); } - - $currentSpan->finish(); - - $previousSpan = array_pop($this->spanStack); - - SentrySdk::getCurrentHub()->setSpan($previousSpan); } private function updateTransactionName(string $componentName): void @@ -137,10 +200,41 @@ private function isLivewireRequest(): bool return false; } - return $request->header('x-livewire') === 'true'; + return $request->hasHeader('x-livewire'); } catch (\Throwable $e) { // If the request cannot be resolved, it's probably not a Livewire request. return false; } } + + private function mapCallArgumentsToMethodParameters(Component $component, string $method, array $data): ?array + { + // If the data is empty there is nothing to do and we can return early + // We also do a quick sanity check the method exists to prevent doing more expensive reflection to come to the same conclusion + if (empty($data) || !method_exists($component, $method)) { + return null; + } + + try { + $reflection = new \ReflectionMethod($component, $method); + $parameters = []; + + foreach ($reflection->getParameters() as $parameter) { + $defaultValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : ''; + + $parameters["\${$parameter->getName()}"] = $data[$parameter->getPosition()] ?? $defaultValue; + + unset($data[$parameter->getPosition()]); + } + + if (!empty($data)) { + $parameters['additionalArguments'] = $data; + } + + return $parameters; + } catch (\ReflectionException $e) { + // If reflection fails, fail the mapping instead of crashing + return null; + } + } } diff --git a/src/Sentry/Laravel/Features/QueueIntegration.php b/src/Sentry/Laravel/Features/QueueIntegration.php index 520f882a..28560dc4 100644 --- a/src/Sentry/Laravel/Features/QueueIntegration.php +++ b/src/Sentry/Laravel/Features/QueueIntegration.php @@ -9,11 +9,11 @@ use Illuminate\Queue\Events\WorkerStopping; use Illuminate\Queue\Queue; use Sentry\Breadcrumb; +use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans; use Sentry\Laravel\Integration; use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; -use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TransactionContext; @@ -25,30 +25,13 @@ class QueueIntegration extends Feature { + use TracksPushedScopesAndSpans { + pushScope as private pushScopeTrait; + } + private const QUEUE_PAYLOAD_BAGGAGE_DATA = 'sentry_baggage_data'; private const QUEUE_PAYLOAD_TRACE_PARENT_DATA = 'sentry_trace_parent_data'; - /** - * Hold the number of times the scope was pushed. - * - * @var int - */ - private $pushedScopeCount = 0; - - /** - * Hold the stack of parent spans that need to be put back on the scope. - * - * @var array - */ - private $parentSpanStack = []; - - /** - * Hold the stack of current spans that need to be finished still. - * - * @var array - */ - private $currentSpanStack = []; - public function isApplicable(): bool { if (!$this->container()->bound('queue')) { @@ -181,22 +164,19 @@ public function handleJobExceptionOccurredQueueEvent(JobExceptionOccurred $event Integration::flushEvents(); } - private function pushSpan(Span $span): void + private function finishJobWithStatus(SpanStatus $status): void { - $hub = SentrySdk::getCurrentHub(); - - $this->parentSpanStack[] = $hub->getSpan(); - - $hub->setSpan($span); + $span = $this->maybePopSpan(); - $this->currentSpanStack[] = $span; + if ($span !== null) { + $span->finish(); + $span->setStatus($status); + } } - private function pushScope(): void + protected function pushScope(): void { - SentrySdk::getCurrentHub()->pushScope(); - - ++$this->pushedScopeCount; + $this->pushScopeTrait(); // When a job starts, we want to make sure the scope is cleared of breadcrumbs // as well as setting a new propagation context. @@ -205,40 +185,4 @@ private function pushScope(): void $scope->setPropagationContext(PropagationContext::fromDefaults()); }); } - - private function maybePopSpan(): ?Span - { - if (count($this->currentSpanStack) === 0) { - return null; - } - - $parent = array_pop($this->parentSpanStack); - - SentrySdk::getCurrentHub()->setSpan($parent); - - return array_pop($this->currentSpanStack); - } - - private function maybePopScope(): void - { - Integration::flushEvents(); - - if ($this->pushedScopeCount === 0) { - return; - } - - SentrySdk::getCurrentHub()->popScope(); - - --$this->pushedScopeCount; - } - - private function finishJobWithStatus(SpanStatus $status): void - { - $span = $this->maybePopSpan(); - - if ($span !== null) { - $span->finish(); - $span->setStatus($status); - } - } } diff --git a/src/Sentry/Laravel/ServiceProvider.php b/src/Sentry/Laravel/ServiceProvider.php index 17dfae4e..f239e815 100644 --- a/src/Sentry/Laravel/ServiceProvider.php +++ b/src/Sentry/Laravel/ServiceProvider.php @@ -12,7 +12,6 @@ use Laravel\Lumen\Application as Lumen; use RuntimeException; use Sentry\ClientBuilder; -use Sentry\ClientBuilderInterface; use Sentry\Event; use Sentry\EventHint; use Sentry\Integration as SdkIntegration; @@ -58,8 +57,9 @@ class ServiceProvider extends BaseServiceProvider Features\CacheIntegration::class, Features\QueueIntegration::class, Features\ConsoleIntegration::class, - Features\FolioPackageIntegration::class, Features\Storage\Integration::class, + Features\HttpClientIntegration::class, + Features\FolioPackageIntegration::class, Features\LivewirePackageIntegration::class, ]; @@ -216,7 +216,7 @@ protected function registerAboutCommandIntegration(): void */ protected function configureAndRegisterClient(): void { - $this->app->bind(ClientBuilderInterface::class, function () { + $this->app->bind(ClientBuilder::class, function () { $basePath = base_path(); $userConfig = $this->getUserConfig(); @@ -280,8 +280,8 @@ protected function configureAndRegisterClient(): void }); $this->app->singleton(HubInterface::class, function () { - /** @var \Sentry\ClientBuilderInterface $clientBuilder */ - $clientBuilder = $this->app->make(ClientBuilderInterface::class); + /** @var \Sentry\ClientBuilder $clientBuilder */ + $clientBuilder = $this->app->make(ClientBuilder::class); $options = $clientBuilder->getOptions(); diff --git a/src/Sentry/Laravel/Tracing/EventHandler.php b/src/Sentry/Laravel/Tracing/EventHandler.php index 15314c8e..b184ca82 100644 --- a/src/Sentry/Laravel/Tracing/EventHandler.php +++ b/src/Sentry/Laravel/Tracing/EventHandler.php @@ -5,12 +5,10 @@ use Exception; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Events as DatabaseEvents; -use Illuminate\Http\Client\Events as HttpClientEvents; use Illuminate\Routing\Events as RoutingEvents; use RuntimeException; use Sentry\Laravel\Features\Concerns\ResolvesEventOrigin; use Sentry\Laravel\Integration; -use Sentry\Laravel\Util\WorksWithUris; use Sentry\SentrySdk; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; @@ -19,7 +17,7 @@ class EventHandler { - use WorksWithUris, ResolvesEventOrigin; + use ResolvesEventOrigin; /** * Map event handlers to events. @@ -31,9 +29,6 @@ class EventHandler DatabaseEvents\QueryExecuted::class => 'queryExecuted', RoutingEvents\ResponsePrepared::class => 'responsePrepared', RoutingEvents\PreparingResponse::class => 'responsePreparing', - HttpClientEvents\RequestSending::class => 'httpClientRequestSending', - HttpClientEvents\ResponseReceived::class => 'httpClientResponseReceived', - HttpClientEvents\ConnectionFailed::class => 'httpClientConnectionFailed', DatabaseEvents\TransactionBeginning::class => 'transactionBeginning', DatabaseEvents\TransactionCommitted::class => 'transactionCommitted', DatabaseEvents\TransactionRolledBack::class => 'transactionRolledBack', @@ -67,13 +62,6 @@ class EventHandler */ private $traceQueueJobsAsTransactions; - /** - * Indicates if we should trace HTTP client requests. - * - * @var bool - */ - private $traceHttpClientRequests; - /** * Hold the stack of parent spans that need to be put back on the scope. * @@ -96,8 +84,6 @@ public function __construct(array $config) $this->traceSqlQueries = ($config['sql_queries'] ?? true) === true; $this->traceSqlQueryOrigins = ($config['sql_origin'] ?? true) === true; - $this->traceHttpClientRequests = ($config['http_client_requests'] ?? true) === true; - $this->traceQueueJobs = ($config['queue_jobs'] ?? false) === true; $this->traceQueueJobsAsTransactions = ($config['queue_job_transactions'] ?? false) === true; } @@ -112,9 +98,6 @@ public function __construct(array $config) * @uses self::transactionBeginningHandler() * @uses self::transactionCommittedHandler() * @uses self::transactionRolledBackHandler() - * @uses self::httpClientRequestSendingHandler() - * @uses self::httpClientResponseReceivedHandler() - * @uses self::httpClientConnectionFailedHandler() */ public function subscribe(Dispatcher $dispatcher): void { @@ -282,64 +265,6 @@ protected function transactionRolledBackHandler(DatabaseEvents\TransactionRolled } } - protected function httpClientRequestSendingHandler(HttpClientEvents\RequestSending $event): void - { - if (!$this->traceHttpClientRequests) { - return; - } - - $parentSpan = SentrySdk::getCurrentHub()->getSpan(); - - // If there is no tracing span active there is no need to handle the event - if ($parentSpan === null) { - return; - } - - $context = new SpanContext; - - $fullUri = $this->getFullUri($event->request->url()); - $partialUri = $this->getPartialUri($fullUri); - - $context->setOp('http.client'); - $context->setDescription($event->request->method() . ' ' . $partialUri); - $context->setData([ - 'url' => $partialUri, - 'http.request.method' => $event->request->method(), - 'http.query' => $fullUri->getQuery(), - 'http.fragment' => $fullUri->getFragment(), - ]); - - $this->pushSpan($parentSpan->startChild($context)); - } - - protected function httpClientResponseReceivedHandler(HttpClientEvents\ResponseReceived $event): void - { - if (!$this->traceHttpClientRequests) { - return; - } - - $span = $this->popSpan(); - - if ($span !== null) { - $span->finish(); - $span->setHttpStatus($event->response->status()); - } - } - - protected function httpClientConnectionFailedHandler(HttpClientEvents\ConnectionFailed $event): void - { - if (!$this->traceHttpClientRequests) { - return; - } - - $span = $this->popSpan(); - - if ($span !== null) { - $span->finish(); - $span->setStatus(SpanStatus::internalError()); - } - } - private function pushSpan(Span $span): void { $hub = SentrySdk::getCurrentHub(); diff --git a/src/Sentry/Laravel/Tracing/Middleware.php b/src/Sentry/Laravel/Tracing/Middleware.php index 117e2411..3d4e5b42 100644 --- a/src/Sentry/Laravel/Tracing/Middleware.php +++ b/src/Sentry/Laravel/Tracing/Middleware.php @@ -15,6 +15,9 @@ use function Sentry\continueTrace; +/** + * @internal + */ class Middleware { /** @@ -59,6 +62,13 @@ class Middleware */ private $registeredTerminatingCallback = false; + /** + * Whether a defined route was matched in the application. + * + * @var bool + */ + private $didRouteMatch = false; + /** * Construct the Sentry tracing middleware. * @@ -80,8 +90,8 @@ public function __construct($app, bool $continueAfterResponse = true) */ public function handle(Request $request, Closure $next) { - if (app()->bound(HubInterface::class)) { - $this->startTransaction($request, app(HubInterface::class)); + if ($this->app->bound(HubInterface::class)) { + $this->startTransaction($request, $this->app->make(HubInterface::class)); } return $next($request); @@ -98,12 +108,12 @@ public function handle(Request $request, Closure $next) public function terminate(Request $request, $response): void { // If there is no transaction or the HubInterface is not bound in the container there is nothing for us to do - if ($this->transaction === null || !app()->bound(HubInterface::class)) { + if ($this->transaction === null || !$this->app->bound(HubInterface::class)) { return; } // We stop here if a route has not been matched unless we are configured to trace missing routes - if (config('sentry.tracing.missing_routes', false) === false && $request->route() === null) { + if (!$this->didRouteMatch && config('sentry.tracing.missing_routes', false) === false) { return; } @@ -154,6 +164,9 @@ public function setBootedTimestamp(?float $timestamp = null): void private function startTransaction(Request $request, HubInterface $sentry): void { + // Reset our internal state in case we are handling multiple requests (e.g. in Octane) + $this->didRouteMatch = false; + // Try $_SERVER['REQUEST_TIME_FLOAT'] then LARAVEL_START and fallback to microtime(true) if neither are defined $requestStartTime = $request->server( 'REQUEST_TIME_FLOAT', @@ -260,4 +273,18 @@ private function finishTransaction(): void $this->transaction->finish(); $this->transaction = null; } + + private function internalSignalRouteWasMatched(): void + { + $this->didRouteMatch = true; + } + + public static function signalRouteWasMatched(): void + { + if (!app()->bound(self::class)) { + return; + } + + app(self::class)->internalSignalRouteWasMatched(); + } } diff --git a/src/Sentry/Laravel/Util/WorksWithUris.php b/src/Sentry/Laravel/Util/WorksWithUris.php deleted file mode 100644 index 84937ee5..00000000 --- a/src/Sentry/Laravel/Util/WorksWithUris.php +++ /dev/null @@ -1,38 +0,0 @@ - $uri->getScheme(), - 'host' => $uri->getHost(), - 'port' => $uri->getPort(), - 'path' => $uri->getPath(), - ]); - } -} diff --git a/test/Sentry/ClientBuilderDecoratorTest.php b/test/Sentry/ClientBuilderDecoratorTest.php index a1569467..356302d3 100644 --- a/test/Sentry/ClientBuilderDecoratorTest.php +++ b/test/Sentry/ClientBuilderDecoratorTest.php @@ -2,7 +2,7 @@ namespace Sentry\Laravel\Tests; -use Sentry\ClientBuilderInterface; +use Sentry\ClientBuilder; class ClientBuilderDecoratorTest extends TestCase { @@ -10,7 +10,7 @@ protected function defineEnvironment($app): void { parent::defineEnvironment($app); - $app->extend(ClientBuilderInterface::class, function (ClientBuilderInterface $clientBuilder) { + $app->extend(ClientBuilder::class, function (ClientBuilder $clientBuilder) { $clientBuilder->getOptions()->setEnvironment('from_service_container'); return $clientBuilder; @@ -21,7 +21,7 @@ public function testClientHasEnvironmentSetFromDecorator(): void { $this->assertEquals( 'from_service_container', - $this->getClientFromContainer()->getOptions()->getEnvironment() + $this->getSentryClientFromContainer()->getOptions()->getEnvironment() ); } } diff --git a/test/Sentry/EventHandler/ConsoleEventsTest.php b/test/Sentry/EventHandler/ConsoleEventsTest.php index ccad1562..c5ad179c 100644 --- a/test/Sentry/EventHandler/ConsoleEventsTest.php +++ b/test/Sentry/EventHandler/ConsoleEventsTest.php @@ -19,7 +19,7 @@ public function testCommandBreadcrumbIsRecordedWhenEnabled(): void $this->dispatchCommandStartEvent(); - $lastBreadcrumb = $this->getLastBreadcrumb(); + $lastBreadcrumb = $this->getLastSentryBreadcrumb(); $this->assertEquals('Starting Artisan command: test:command', $lastBreadcrumb->getMessage()); $this->assertEquals('--foo=bar', $lastBreadcrumb->getMetadata()['input']); @@ -35,7 +35,7 @@ public function testCommandBreadcrumIsNotRecordedWhenDisabled(): void $this->dispatchCommandStartEvent(); - $this->assertEmpty($this->getCurrentBreadcrumbs()); + $this->assertEmpty($this->getCurrentSentryBreadcrumbs()); } private function dispatchCommandStartEvent(): void diff --git a/test/Sentry/EventHandler/DatabaseEventsTest.php b/test/Sentry/EventHandler/DatabaseEventsTest.php index 0c517298..531306e8 100644 --- a/test/Sentry/EventHandler/DatabaseEventsTest.php +++ b/test/Sentry/EventHandler/DatabaseEventsTest.php @@ -24,7 +24,7 @@ public function testSqlQueriesAreRecordedWhenEnabled(): void $this->getMockedConnection() )); - $lastBreadcrumb = $this->getLastBreadcrumb(); + $lastBreadcrumb = $this->getLastSentryBreadcrumb(); $this->assertEquals($query, $lastBreadcrumb->getMessage()); } @@ -44,7 +44,7 @@ public function testSqlBindingsAreRecordedWhenEnabled(): void $this->getMockedConnection() )); - $lastBreadcrumb = $this->getLastBreadcrumb(); + $lastBreadcrumb = $this->getLastSentryBreadcrumb(); $this->assertEquals($query, $lastBreadcrumb->getMessage()); $this->assertEquals($bindings, $lastBreadcrumb->getMetadata()['bindings']); @@ -65,7 +65,7 @@ public function testSqlQueriesAreRecordedWhenDisabled(): void $this->getMockedConnection() )); - $this->assertEmpty($this->getCurrentBreadcrumbs()); + $this->assertEmpty($this->getCurrentSentryBreadcrumbs()); } public function testSqlBindingsAreRecordedWhenDisabled(): void @@ -83,7 +83,7 @@ public function testSqlBindingsAreRecordedWhenDisabled(): void $this->getMockedConnection() )); - $lastBreadcrumb = $this->getLastBreadcrumb(); + $lastBreadcrumb = $this->getLastSentryBreadcrumb(); $this->assertEquals($query, $lastBreadcrumb->getMessage()); $this->assertFalse(isset($lastBreadcrumb->getMetadata()['bindings'])); diff --git a/test/Sentry/EventHandler/LogEventsTest.php b/test/Sentry/EventHandler/LogEventsTest.php index 3f0b19cf..2847477e 100644 --- a/test/Sentry/EventHandler/LogEventsTest.php +++ b/test/Sentry/EventHandler/LogEventsTest.php @@ -21,7 +21,7 @@ public function testLaravelLogsAreRecordedWhenEnabled(): void $context = ['1'] )); - $lastBreadcrumb = $this->getLastBreadcrumb(); + $lastBreadcrumb = $this->getLastSentryBreadcrumb(); $this->assertEquals($level, $lastBreadcrumb->getLevel()); $this->assertEquals($message, $lastBreadcrumb->getMessage()); @@ -38,6 +38,6 @@ public function testLaravelLogsAreRecordedWhenDisabled(): void $this->dispatchLaravelEvent(new MessageLogged('debug', 'test message')); - $this->assertEmpty($this->getCurrentBreadcrumbs()); + $this->assertEmpty($this->getCurrentSentryBreadcrumbs()); } } diff --git a/test/Sentry/EventHandler/QueueEventsTest.php b/test/Sentry/EventHandler/QueueEventsTest.php index 1b068200..91f1ca77 100644 --- a/test/Sentry/EventHandler/QueueEventsTest.php +++ b/test/Sentry/EventHandler/QueueEventsTest.php @@ -15,18 +15,18 @@ public function testQueueJobPushesAndPopsScopeWithBreadcrumbs(): void { dispatch(new QueueEventsTestJobWithBreadcrumb); - $this->assertCount(0, $this->getCurrentBreadcrumbs()); + $this->assertCount(0, $this->getCurrentSentryBreadcrumbs()); } public function testQueueJobThatReportsPushesAndPopsScopeWithBreadcrumbs(): void { dispatch(new QueueEventsTestJobThatReportsAnExceptionWithBreadcrumb); - $this->assertCount(0, $this->getCurrentBreadcrumbs()); + $this->assertCount(0, $this->getCurrentSentryBreadcrumbs()); - $this->assertNotNull($this->getLastEvent()); + $this->assertNotNull($this->getLastSentryEvent()); - $event = $this->getLastEvent(); + $event = $this->getLastSentryEvent(); $this->assertCount(2, $event->getBreadcrumbs()); } @@ -41,12 +41,12 @@ public function testQueueJobThatThrowsLeavesPushedScopeWithBreadcrumbs(): void // We still expect to find the breadcrumbs from the job here so they are attached to reported exceptions - $this->assertCount(2, $this->getCurrentBreadcrumbs()); + $this->assertCount(2, $this->getCurrentSentryBreadcrumbs()); - $firstBreadcrumb = $this->getCurrentBreadcrumbs()[0]; + $firstBreadcrumb = $this->getCurrentSentryBreadcrumbs()[0]; $this->assertEquals('queue.job', $firstBreadcrumb->getCategory()); - $secondBreadcrumb = $this->getCurrentBreadcrumbs()[1]; + $secondBreadcrumb = $this->getCurrentSentryBreadcrumbs()[1]; $this->assertEquals('test', $secondBreadcrumb->getCategory()); } @@ -66,12 +66,12 @@ public function testQueueJobsThatThrowPopsAndPushesScopeWithBreadcrumbsBeforeNew // We only expect to find the breadcrumbs from the second job here - $this->assertCount(2, $this->getCurrentBreadcrumbs()); + $this->assertCount(2, $this->getCurrentSentryBreadcrumbs()); - $firstBreadcrumb = $this->getCurrentBreadcrumbs()[0]; + $firstBreadcrumb = $this->getCurrentSentryBreadcrumbs()[0]; $this->assertEquals('queue.job', $firstBreadcrumb->getCategory()); - $secondBreadcrumb = $this->getCurrentBreadcrumbs()[1]; + $secondBreadcrumb = $this->getCurrentSentryBreadcrumbs()[1]; $this->assertEquals('test #2', $secondBreadcrumb->getMessage()); } @@ -83,7 +83,7 @@ public function testQueueJobsWithBreadcrumbSetInBetweenKeepsNonJobBreadcrumbsOnC dispatch(new QueueEventsTestJobWithBreadcrumb); - $this->assertCount(1, $this->getCurrentBreadcrumbs()); + $this->assertCount(1, $this->getCurrentSentryBreadcrumbs()); } } diff --git a/test/Sentry/Features/CacheIntegrationTest.php b/test/Sentry/Features/CacheIntegrationTest.php index 80ae9e13..2d5dcac3 100644 --- a/test/Sentry/Features/CacheIntegrationTest.php +++ b/test/Sentry/Features/CacheIntegrationTest.php @@ -11,29 +11,29 @@ public function testCacheBreadcrumbForWriteAndHitIsRecorded(): void { Cache::put($key = 'foo', 'bar'); - $this->assertEquals("Written: {$key}", $this->getLastBreadcrumb()->getMessage()); + $this->assertEquals("Written: {$key}", $this->getLastSentryBreadcrumb()->getMessage()); Cache::get('foo'); - $this->assertEquals("Read: {$key}", $this->getLastBreadcrumb()->getMessage()); + $this->assertEquals("Read: {$key}", $this->getLastSentryBreadcrumb()->getMessage()); } public function testCacheBreadcrumbForWriteAndForgetIsRecorded(): void { Cache::put($key = 'foo', 'bar'); - $this->assertEquals("Written: {$key}", $this->getLastBreadcrumb()->getMessage()); + $this->assertEquals("Written: {$key}", $this->getLastSentryBreadcrumb()->getMessage()); Cache::forget($key); - $this->assertEquals("Forgotten: {$key}", $this->getLastBreadcrumb()->getMessage()); + $this->assertEquals("Forgotten: {$key}", $this->getLastSentryBreadcrumb()->getMessage()); } public function testCacheBreadcrumbForMissIsRecorded(): void { Cache::get($key = 'foo'); - $this->assertEquals("Missed: {$key}", $this->getLastBreadcrumb()->getMessage()); + $this->assertEquals("Missed: {$key}", $this->getLastSentryBreadcrumb()->getMessage()); } public function testCacheBreadcrumbIsNotRecordedWhenDisabled(): void @@ -46,6 +46,6 @@ public function testCacheBreadcrumbIsNotRecordedWhenDisabled(): void Cache::get('foo'); - $this->assertEmpty($this->getCurrentBreadcrumbs()); + $this->assertEmpty($this->getCurrentSentryBreadcrumbs()); } } diff --git a/test/Sentry/Features/ConsoleIntegrationTest.php b/test/Sentry/Features/ConsoleIntegrationTest.php index 5c8c5b95..110c6b99 100644 --- a/test/Sentry/Features/ConsoleIntegrationTest.php +++ b/test/Sentry/Features/ConsoleIntegrationTest.php @@ -13,16 +13,18 @@ class ConsoleIntegrationTest extends TestCase public function testScheduleMacro(): void { /** @var Event $scheduledEvent */ - $scheduledEvent = $this->getScheduler()->call(function () {})->sentryMonitor('test-monitor'); + $scheduledEvent = $this->getScheduler() + ->call(function () {}) + ->sentryMonitor('test-monitor'); $scheduledEvent->run($this->app); // We expect a total of 2 events to be sent to Sentry: // 1. The start check-in event // 2. The finish check-in event - $this->assertEquals(2, $this->getEventsCount()); + $this->assertSentryCheckInCount(2); - $finishCheckInEvent = $this->getLastEvent(); + $finishCheckInEvent = $this->getLastSentryEvent(); $this->assertNotNull($finishCheckInEvent->getCheckIn()); $this->assertEquals('test-monitor', $finishCheckInEvent->getCheckIn()->getMonitorSlug()); @@ -48,9 +50,9 @@ public function testScheduleMacroWithTimeZone(): void // We expect a total of 2 events to be sent to Sentry: // 1. The start check-in event // 2. The finish check-in event - $this->assertEquals(2, $this->getEventsCount()); + $this->assertSentryCheckInCount(2); - $finishCheckInEvent = $this->getLastEvent(); + $finishCheckInEvent = $this->getLastSentryEvent(); $this->assertNotNull($finishCheckInEvent->getCheckIn()); $this->assertEquals($expectedTimezone, $finishCheckInEvent->getCheckIn()->getMonitorConfig()->getTimezone()); @@ -66,9 +68,9 @@ public function testScheduleMacroAutomaticSlug(): void // We expect a total of 2 events to be sent to Sentry: // 1. The start check-in event // 2. The finish check-in event - $this->assertEquals(2, $this->getEventsCount()); + $this->assertSentryCheckInCount(2); - $finishCheckInEvent = $this->getLastEvent(); + $finishCheckInEvent = $this->getLastSentryEvent(); $this->assertNotNull($finishCheckInEvent->getCheckIn()); $this->assertEquals('scheduled_artisan-inspire', $finishCheckInEvent->getCheckIn()->getMonitorSlug()); @@ -89,7 +91,7 @@ public function testScheduleMacroWithoutDsnSet(): void $scheduledEvent->run($this->app); - $this->assertEquals(0, $this->getEventsCount()); + $this->assertSentryCheckInCount(0); } public function testScheduleMacroIsRegistered(): void diff --git a/test/Sentry/Features/FolioPackageIntegrationTest.php b/test/Sentry/Features/FolioPackageIntegrationTest.php index 244b48d6..02be8ede 100644 --- a/test/Sentry/Features/FolioPackageIntegrationTest.php +++ b/test/Sentry/Features/FolioPackageIntegrationTest.php @@ -3,6 +3,7 @@ namespace Sentry\Features; use Laravel\Folio\Folio; +use Sentry\EventType; use Sentry\Laravel\Integration; use Illuminate\Config\Repository; use Sentry\Laravel\Tests\TestCase; @@ -21,7 +22,11 @@ protected function setUp(): void protected function defineRoutes($router): void { - Folio::path(__DIR__ . '/../../stubs/folio')->uri('/folio'); + $folioStubPath = __DIR__ . '/../../stubs/folio'; + + Folio::route($folioStubPath); + + Folio::path($folioStubPath)->uri('/folio'); } protected function defineEnvironment($app): void @@ -45,13 +50,75 @@ protected function defineDatabaseMigrations(): void $this->loadLaravelMigrations(); } + /** @define-env envSamplingAllTransactions */ + public function testFolioCatchAllRouteCreatesTransaction(): void + { + $this->get('/')->assertOk(); + + $this->assertSentryTransactionCount(1); + + $transaction = $this->getLastSentryEvent(); + + $this->assertEquals('/index', $transaction->getTransaction()); + $this->assertEquals(EventType::transaction(), $transaction->getType()); + } + + /** @define-env envSamplingAllTransactions */ + public function testFolioCatchAllRouteWithoutHandlerDropsTransaction(): void + { + $this->get('/non-existing-route')->assertNotFound(); + + $this->assertSentryTransactionCount(0); + } + + /** @define-env envSamplingAllTransactions */ + public function testFolioCatchAllRouteThrowingNotFoundDropsTransaction(): void + { + $this->get('/user/420')->assertNotFound(); + + // Unfortunately it's not possible to detect a matching route since the Folio router bails early + // So even though the `/user/[id].blade.php` view exists we can't detect it and thus drop the transaction + $this->assertSentryTransactionCount(0); + } + + /** @define-env envSamplingAllTransactions */ + public function testFolioPathRouteCreatesTransaction(): void + { + $this->get('/folio')->assertOk(); + + $this->assertSentryTransactionCount(1); + + $transaction = $this->getLastSentryEvent(); + + $this->assertEquals('/folio/index', $transaction->getTransaction()); + $this->assertEquals(EventType::transaction(), $transaction->getType()); + } + + /** @define-env envSamplingAllTransactions */ + public function testFolioPathRouteWithoutHandlerDropsTransaction(): void + { + $this->get('/folio/non-existing-route')->assertNotFound(); + + $this->assertSentryTransactionCount(0); + } + + /** @define-env envSamplingAllTransactions */ + public function testFolioPathRouteThrowingNotFoundDropsTransaction(): void + { + $this->get('/folio/user/420')->assertNotFound(); + + // Unfortunately it's not possible to detect a matching route since the Folio router bails early + // So even though the `/user/[id].blade.php` view exists we can't detect it and thus drop the transaction + $this->assertSentryTransactionCount(0); + } + public function testFolioBreadcrumbIsRecorded(): void { $this->get('/folio'); - $this->assertCount(1, $this->getCurrentBreadcrumbs()); + $this->assertCount(1, $this->getCurrentSentryBreadcrumbs()); - $lastBreadcrumb = $this->getLastBreadcrumb(); + $lastBreadcrumb = $this->getLastSentryBreadcrumb(); $this->assertEquals('folio.route', $lastBreadcrumb->getCategory()); $this->assertEquals('navigation', $lastBreadcrumb->getType()); diff --git a/test/Sentry/Features/HttpClientIntegrationTest.php b/test/Sentry/Features/HttpClientIntegrationTest.php index 8e217b73..edd61698 100644 --- a/test/Sentry/Features/HttpClientIntegrationTest.php +++ b/test/Sentry/Features/HttpClientIntegrationTest.php @@ -7,7 +7,9 @@ use Illuminate\Http\Client\Events\ResponseReceived; use Illuminate\Http\Client\Request; use Illuminate\Http\Client\Response; +use Illuminate\Support\Facades\Http; use Sentry\Laravel\Tests\TestCase; +use Sentry\Tracing\SpanStatus; class HttpClientIntegrationTest extends TestCase { @@ -27,9 +29,9 @@ public function testHttpClientBreadcrumbIsRecordedForResponseReceivedEvent(): vo new Response(new PsrResponse(200, [], 'response')) )); - $this->assertCount(1, $this->getCurrentBreadcrumbs()); + $this->assertCount(1, $this->getCurrentSentryBreadcrumbs()); - $metadata = $this->getLastBreadcrumb()->getMetadata(); + $metadata = $this->getLastSentryBreadcrumb()->getMetadata(); $this->assertEquals('GET', $metadata['http.request.method']); $this->assertEquals('https://example.com', $metadata['url']); @@ -45,9 +47,107 @@ public function testHttpClientBreadcrumbDoesntConsumeBodyStream(): void $response = new Response(new PsrResponse(200, [], 'response')) )); - $this->assertCount(1, $this->getCurrentBreadcrumbs()); + $this->assertCount(1, $this->getCurrentSentryBreadcrumbs()); $this->assertEquals('request', $request->toPsrRequest()->getBody()->getContents()); $this->assertEquals('response', $response->toPsrResponse()->getBody()->getContents()); } + + public function testHttpClientBreadcrumbIsNotRecordedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.breadcrumbs.http_client_requests' => false, + ]); + + $this->dispatchLaravelEvent(new ResponseReceived( + new Request(new PsrRequest('GET', 'https://example.com', [], 'request')), + new Response(new PsrResponse(200, [], 'response')) + )); + + $this->assertEmpty($this->getCurrentSentryBreadcrumbs()); + } + + public function testHttpClientSpanIsRecorded(): void + { + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertEquals('http.client', $span->getOp()); + $this->assertEquals('GET https://example.com', $span->getDescription()); + } + + public function testHttpClientSpanIsRecordedWithCorrectResult(): void + { + $transaction = $this->startTransaction(); + + $client = Http::fake([ + 'example.com/success' => Http::response('OK'), + 'example.com/error' => Http::response('Internal Server Error', 500), + ]); + + $client->get('https://example.com/success'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertEquals('http.client', $span->getOp()); + $this->assertEquals(SpanStatus::ok(), $span->getStatus()); + + $client->get('https://example.com/error'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertEquals('http.client', $span->getOp()); + $this->assertEquals(SpanStatus::internalError(), $span->getStatus()); + } + + public function testHttpClientSpanIsNotRecordedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.tracing.http_client_requests' => false, + ]); + + $transaction = $this->startTransaction(); + + $client = Http::fake(); + + $client->get('https://example.com'); + + /** @var \Sentry\Tracing\Span $span */ + $span = last($transaction->getSpanRecorder()->getSpans()); + + $this->assertNotEquals('http.client', $span->getOp()); + } + + public function testHttpClientRequestTracingHeadersAreAttached(): void + { + if (!method_exists(Http::class, 'globalRequestMiddleware')) { + $this->markTestSkipped('The `globalRequestMiddleware` functionality we rely on was introduced in Laravel 10.14'); + } + + $this->resetApplicationWithConfig([ + 'sentry.trace_propagation_targets' => ['example.com'], + ]); + + $client = Http::fake(); + + $client->get('https://example.com'); + + Http::assertSent(function (Request $request) { + return $request->hasHeader('baggage') && $request->hasHeader('sentry-trace'); + }); + + $client->get('https://no-headers.example.com'); + + Http::assertSent(function (Request $request) { + return !$request->hasHeader('baggage') && !$request->hasHeader('sentry-trace'); + }); + } } diff --git a/test/Sentry/Features/LogIntegrationTest.php b/test/Sentry/Features/LogIntegrationTest.php index 16298f00..8c026fca 100644 --- a/test/Sentry/Features/LogIntegrationTest.php +++ b/test/Sentry/Features/LogIntegrationTest.php @@ -46,9 +46,9 @@ public function testLogChannelGeneratesEvents(): void $logger->info('Sentry Laravel info log message'); - $this->assertEquals(1, $this->getEventsCount()); + $this->assertSentryEventCount(1); - $event = $this->getLastEvent(); + $event = $this->getLastSentryEvent(); $this->assertEquals(Severity::info(), $event->getLevel()); $this->assertEquals('Sentry Laravel info log message', $event->getMessage()); @@ -62,9 +62,9 @@ public function testLogChannelGeneratesEventsOnlyForConfiguredLevel(): void $logger->warning('Sentry Laravel warning log message'); $logger->error('Sentry Laravel error log message'); - $this->assertEquals(1, $this->getEventsCount()); + $this->assertSentryEventCount(1); - $event = $this->getLastEvent(); + $event = $this->getLastSentryEvent(); $this->assertEquals(Severity::error(), $event->getLevel()); $this->assertEquals('Sentry Laravel error log message', $event->getMessage()); diff --git a/test/Sentry/Features/RouteIntegrationTest.php b/test/Sentry/Features/RouteIntegrationTest.php new file mode 100644 index 00000000..e4b99860 --- /dev/null +++ b/test/Sentry/Features/RouteIntegrationTest.php @@ -0,0 +1,46 @@ +group(['prefix' => 'sentry'], function (Router $router) { + $router->get('/ok', function () { + return 'ok'; + }); + + $router->get('/abort/{code}', function (int $code) { + abort($code); + }); + }); + } + + /** @define-env envSamplingAllTransactions */ + public function testTransactionIsRecordedForRoute(): void + { + $this->get('/sentry/ok')->assertOk(); + + $this->assertSentryTransactionCount(1); + } + + /** @define-env envSamplingAllTransactions */ + public function testTransactionIsRecordedForNotFound(): void + { + $this->get('/sentry/abort/404')->assertNotFound(); + + $this->assertSentryTransactionCount(1); + } + + /** @define-env envSamplingAllTransactions */ + public function testTransactionIsDroppedForUndefinedRoute(): void + { + $this->get('/sentry/non-existent-route')->assertNotFound(); + + $this->assertSentryTransactionCount(0); + } +} diff --git a/test/Sentry/Features/StorageIntegrationTest.php b/test/Sentry/Features/StorageIntegrationTest.php index 51a3bb86..343c7765 100644 --- a/test/Sentry/Features/StorageIntegrationTest.php +++ b/test/Sentry/Features/StorageIntegrationTest.php @@ -89,7 +89,7 @@ public function testCreatesBreadcrumbsFor(): void Storage::delete(['foo', 'bar']); Storage::files(); - $breadcrumbs = $this->getCurrentBreadcrumbs(); + $breadcrumbs = $this->getCurrentSentryBreadcrumbs(); $this->assertArrayHasKey(0, $breadcrumbs); $span = $breadcrumbs[0]; @@ -137,7 +137,7 @@ public function testDoesntCreateBreadcrumbsWhenDisabled(): void Storage::exists('foo'); - $this->assertCount(0, $this->getCurrentBreadcrumbs()); + $this->assertCount(0, $this->getCurrentSentryBreadcrumbs()); } public function testDriverWorksWhenDisabled(): void diff --git a/test/Sentry/Integration/ExceptionContextIntegrationTest.php b/test/Sentry/Integration/ExceptionContextIntegrationTest.php index 2adeb583..27f0a118 100644 --- a/test/Sentry/Integration/ExceptionContextIntegrationTest.php +++ b/test/Sentry/Integration/ExceptionContextIntegrationTest.php @@ -14,7 +14,7 @@ class ExceptionContextIntegrationTest extends TestCase { public function testExceptionContextIntegrationIsRegistered(): void { - $integration = $this->getHubFromContainer()->getIntegration(ExceptionContextIntegration::class); + $integration = $this->getSentryHubFromContainer()->getIntegration(ExceptionContextIntegration::class); $this->assertInstanceOf(ExceptionContextIntegration::class, $integration); } @@ -37,7 +37,7 @@ public function testInvoke(Exception $exception, ?array $expectedContext): void }); } - public function invokeDataProvider(): iterable + public static function invokeDataProvider(): iterable { yield 'Exception without context method -> no exception context' => [ new Exception('Exception without context.'), @@ -47,17 +47,17 @@ public function invokeDataProvider(): iterable $context = ['some' => 'context']; yield 'Exception with context method returning array of context' => [ - $this->generateExceptionWithContext($context), + self::generateExceptionWithContext($context), $context, ]; yield 'Exception with context method returning string of context' => [ - $this->generateExceptionWithContext('Invalid context, expects array'), + self::generateExceptionWithContext('Invalid context, expects array'), null, ]; } - private function generateExceptionWithContext($context): Exception + private static function generateExceptionWithContext($context): Exception { return new class($context) extends Exception { private $context; diff --git a/test/Sentry/IntegrationTest.php b/test/Sentry/IntegrationTest.php index 3708274a..fee6ac0a 100644 --- a/test/Sentry/IntegrationTest.php +++ b/test/Sentry/IntegrationTest.php @@ -18,7 +18,7 @@ class IntegrationTest extends TestCase { public function testIntegrationIsRegistered(): void { - $integration = $this->getHubFromContainer()->getIntegration(Integration::class); + $integration = $this->getSentryHubFromContainer()->getIntegration(Integration::class); $this->assertInstanceOf(Integration::class, $integration); } @@ -153,9 +153,9 @@ public function testExceptionReportedUsingReportHelperIsNotMarkedAsUnhandled(): report($testException); - $this->assertEquals(1, $this->getEventsCount()); + $this->assertSentryEventCount(1); - $hint = $this->getLastEventHint(); + $hint = $this->getLastEventSentryHint(); $this->assertEquals($testException, $hint->exception); $this->assertNotNull($hint->mechanism); @@ -168,9 +168,9 @@ public function testExceptionIsNotMarkedAsUnhandled(): void Integration::captureUnhandledException($testException); - $this->assertEquals(1, $this->getEventsCount()); + $this->assertSentryEventCount(1); - $hint = $this->getLastEventHint(); + $hint = $this->getLastEventSentryHint(); $this->assertEquals($testException, $hint->exception); $this->assertNotNull($hint->mechanism); diff --git a/test/Sentry/Laravel/LaravelIntegrationsOptionTest.php b/test/Sentry/Laravel/LaravelIntegrationsOptionTest.php index ed2f487c..59571ebe 100644 --- a/test/Sentry/Laravel/LaravelIntegrationsOptionTest.php +++ b/test/Sentry/Laravel/LaravelIntegrationsOptionTest.php @@ -29,7 +29,7 @@ public function testCustomIntegrationIsResolvedFromContainerByAlias(): void ], ]); - $this->assertNotNull($this->getClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + $this->assertNotNull($this->getSentryClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); } public function testCustomIntegrationIsResolvedFromContainerByClass(): void @@ -40,7 +40,7 @@ public function testCustomIntegrationIsResolvedFromContainerByClass(): void ], ]); - $this->assertNotNull($this->getClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + $this->assertNotNull($this->getSentryClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); } public function testCustomIntegrationByInstance(): void @@ -51,7 +51,7 @@ public function testCustomIntegrationByInstance(): void ], ]); - $this->assertNotNull($this->getClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + $this->assertNotNull($this->getSentryClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); } public function testCustomIntegrationThrowsExceptionIfNotResolvable(): void @@ -79,7 +79,7 @@ static function () { public function testDisabledIntegrationsAreNotPresent(): void { - $client = $this->getClientFromContainer(); + $client = $this->getSentryClientFromContainer(); $this->assertNull($client->getIntegration(ErrorListenerIntegration::class)); $this->assertNull($client->getIntegration(ExceptionListenerIntegration::class)); @@ -94,7 +94,7 @@ public function testDisabledIntegrationsAreNotPresentWithCustomIntegrations(): v ], ]); - $client = $this->getClientFromContainer(); + $client = $this->getSentryClientFromContainer(); $this->assertNotNull($client->getIntegration(IntegrationsOptionTestIntegrationStub::class)); diff --git a/test/Sentry/LogChannelTest.php b/test/Sentry/LogChannelTest.php index 728d711a..fe3e9d15 100644 --- a/test/Sentry/LogChannelTest.php +++ b/test/Sentry/LogChannelTest.php @@ -48,7 +48,7 @@ public function testHandlerWritingExpectedEventsAndContext(array $context, calla $logger->error('test message', $context); - $lastEvent = $this->getLastEvent(); + $lastEvent = $this->getLastSentryEvent(); $this->assertNotNull($lastEvent); $this->assertEquals('test message', $lastEvent->getMessage()); @@ -57,14 +57,14 @@ public function testHandlerWritingExpectedEventsAndContext(array $context, calla $asserter($lastEvent); } - public function handlerDataProvider(): iterable + public static function handlerDataProvider(): iterable { $context = ['foo' => 'bar']; yield [ $context, function (Event $event) use ($context) { - $this->assertEquals($context, $event->getExtra()['log_context']); + self::assertEquals($context, $event->getExtra()['log_context']); }, ]; @@ -73,8 +73,8 @@ function (Event $event) use ($context) { yield [ $context, function (Event $event) use ($context) { - $this->assertEquals($context['fingerprint'], $event->getFingerprint()); - $this->assertEmpty($event->getExtra()); + self::assertEquals($context['fingerprint'], $event->getFingerprint()); + self::assertEmpty($event->getExtra()); }, ]; @@ -83,8 +83,8 @@ function (Event $event) use ($context) { yield [ $context, function (Event $event) use ($context) { - $this->assertNull($event->getUser()); - $this->assertEquals($context, $event->getExtra()['log_context']); + self::assertNull($event->getUser()); + self::assertEquals($context, $event->getExtra()['log_context']); }, ]; @@ -93,9 +93,9 @@ function (Event $event) use ($context) { yield [ $context, function (Event $event) { - $this->assertNotNull($event->getUser()); - $this->assertEquals(123, $event->getUser()->getId()); - $this->assertEmpty($event->getExtra()); + self::assertNotNull($event->getUser()); + self::assertEquals(123, $event->getUser()->getId()); + self::assertEmpty($event->getExtra()); }, ]; @@ -107,11 +107,11 @@ function (Event $event) { yield [ $context, function (Event $event) { - $this->assertSame([ + self::assertSame([ 'foo' => 'bar', 'bar' => '123', ], $event->getTags()); - $this->assertEmpty($event->getExtra()); + self::assertEmpty($event->getExtra()); }, ]; } diff --git a/test/Sentry/ServiceProviderTest.php b/test/Sentry/ServiceProviderTest.php index 2c20ced8..fa720160 100644 --- a/test/Sentry/ServiceProviderTest.php +++ b/test/Sentry/ServiceProviderTest.php @@ -12,7 +12,7 @@ class ServiceProviderTest extends TestCase { protected function defineEnvironment($app): void { - $app['config']->set('sentry.dsn', 'https://publickey:secretkey@sentry.dev/123'); + $app['config']->set('sentry.dsn', 'https://publickey@sentry.dev/123'); $app['config']->set('sentry.error_types', E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED); } @@ -56,7 +56,6 @@ public function testDsnWasSetFromConfig(): void $this->assertEquals('https://sentry.dev', $options->getDsn()->getScheme() . '://' . $options->getDsn()->getHost()); $this->assertEquals(123, $options->getDsn()->getProjectId()); $this->assertEquals('publickey', $options->getDsn()->getPublicKey()); - $this->assertEquals('secretkey', $options->getDsn()->getSecretKey()); } /** diff --git a/test/Sentry/ServiceProviderWithCustomAliasTest.php b/test/Sentry/ServiceProviderWithCustomAliasTest.php index 7d2fa5f6..3a96906c 100644 --- a/test/Sentry/ServiceProviderWithCustomAliasTest.php +++ b/test/Sentry/ServiceProviderWithCustomAliasTest.php @@ -11,7 +11,7 @@ class ServiceProviderWithCustomAliasTest extends TestCase { protected function defineEnvironment($app): void { - $app['config']->set('custom-sentry.dsn', 'http://publickey:secretkey@sentry.dev/123'); + $app['config']->set('custom-sentry.dsn', 'http://publickey@sentry.dev/123'); $app['config']->set('custom-sentry.error_types', E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED); } @@ -55,7 +55,6 @@ public function testDsnWasSetFromConfig(): void $this->assertEquals('http://sentry.dev', $options->getDsn()->getScheme() . '://' . $options->getDsn()->getHost()); $this->assertEquals(123, $options->getDsn()->getProjectId()); $this->assertEquals('publickey', $options->getDsn()->getPublicKey()); - $this->assertEquals('secretkey', $options->getDsn()->getSecretKey()); } /** diff --git a/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php b/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php index 0c1e0a00..2f4c9651 100644 --- a/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php +++ b/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php @@ -17,13 +17,13 @@ public function testEmptySentryEnvironmentDefaultsToLaravelEnvironment(): void 'sentry.environment' => '', ]); - $this->assertEquals('testing', $this->getClientFromContainer()->getOptions()->getEnvironment()); + $this->assertEquals('testing', $this->getSentryClientFromContainer()->getOptions()->getEnvironment()); $this->resetApplicationWithConfig([ 'sentry.environment' => null, ]); - $this->assertEquals('testing', $this->getClientFromContainer()->getOptions()->getEnvironment()); + $this->assertEquals('testing', $this->getSentryClientFromContainer()->getOptions()->getEnvironment()); } public function testSentryEnvironmentDefaultGetsOverriddenByConfig(): void @@ -32,6 +32,6 @@ public function testSentryEnvironmentDefaultGetsOverriddenByConfig(): void 'sentry.environment' => 'override_env', ]); - $this->assertEquals('override_env', $this->getClientFromContainer()->getOptions()->getEnvironment()); + $this->assertEquals('override_env', $this->getSentryClientFromContainer()->getOptions()->getEnvironment()); } } diff --git a/test/Sentry/TestCase.php b/test/Sentry/TestCase.php index 1e9c9b66..b6766421 100644 --- a/test/Sentry/TestCase.php +++ b/test/Sentry/TestCase.php @@ -58,7 +58,7 @@ protected function defineEnvironment($app): void }); if ($config->get('sentry_test.override_dsn') !== true) { - $config->set('sentry.dsn', 'https://publickey:secretkey@sentry.dev/123'); + $config->set('sentry.dsn', 'https://publickey@sentry.dev/123'); } foreach ($this->setupConfig as $key => $value) { @@ -78,6 +78,12 @@ protected function envWithoutDsnSet($app): void $app['config']->set('sentry_test.override_dsn', true); } + /** @param Application $app */ + protected function envSamplingAllTransactions($app): void + { + $app['config']->set('sentry.traces_sample_rate', 1.0); + } + protected function getPackageProviders($app): array { return [ @@ -98,19 +104,19 @@ protected function dispatchLaravelEvent($event, array $payload = []): void $this->app['events']->dispatch($event, $payload); } - protected function getHubFromContainer(): HubInterface + protected function getSentryHubFromContainer(): HubInterface { return $this->app->make('sentry'); } - protected function getClientFromContainer(): ClientInterface + protected function getSentryClientFromContainer(): ClientInterface { - return $this->getHubFromContainer()->getClient(); + return $this->getSentryHubFromContainer()->getClient(); } - protected function getCurrentScope(): Scope + protected function getCurrentSentryScope(): Scope { - $hub = $this->getHubFromContainer(); + $hub = $this->getSentryHubFromContainer(); $method = new ReflectionMethod($hub, 'getScope'); $method->setAccessible(true); @@ -119,9 +125,9 @@ protected function getCurrentScope(): Scope } /** @return array */ - protected function getCurrentBreadcrumbs(): array + protected function getCurrentSentryBreadcrumbs(): array { - $scope = $this->getCurrentScope(); + $scope = $this->getCurrentSentryScope(); $property = new ReflectionProperty($scope, 'breadcrumbs'); $property->setAccessible(true); @@ -129,9 +135,9 @@ protected function getCurrentBreadcrumbs(): array return $property->getValue($scope); } - protected function getLastBreadcrumb(): ?Breadcrumb + protected function getLastSentryBreadcrumb(): ?Breadcrumb { - $breadcrumbs = $this->getCurrentBreadcrumbs(); + $breadcrumbs = $this->getCurrentSentryBreadcrumbs(); if (empty($breadcrumbs)) { return null; @@ -140,7 +146,7 @@ protected function getLastBreadcrumb(): ?Breadcrumb return end($breadcrumbs); } - protected function getLastEvent(): ?Event + protected function getLastSentryEvent(): ?Event { if (empty(self::$lastSentryEvents)) { return null; @@ -149,7 +155,7 @@ protected function getLastEvent(): ?Event return end(self::$lastSentryEvents)[0]; } - protected function getLastEventHint(): ?EventHint + protected function getLastEventSentryHint(): ?EventHint { if (empty(self::$lastSentryEvents)) { return null; @@ -158,19 +164,41 @@ protected function getLastEventHint(): ?EventHint return end(self::$lastSentryEvents)[1]; } - protected function getEventsCount(): int + /** @return array */ + protected function getCapturedSentryEvents(): array + { + return self::$lastSentryEvents; + } + + protected function assertSentryEventCount(int $count): void + { + $this->assertCount($count, array_filter(self::$lastSentryEvents, static function (array $event) { + return $event[0]->getType() === EventType::event(); + })); + } + + protected function assertSentryCheckInCount(int $count): void + { + $this->assertCount($count, array_filter(self::$lastSentryEvents, static function (array $event) { + return $event[0]->getType() === EventType::checkIn(); + })); + } + + protected function assertSentryTransactionCount(int $count): void { - return count(self::$lastSentryEvents); + $this->assertCount($count, array_filter(self::$lastSentryEvents, static function (array $event) { + return $event[0]->getType() === EventType::transaction(); + })); } protected function startTransaction(): Transaction { - $hub = $this->getHubFromContainer(); + $hub = $this->getSentryHubFromContainer(); $transaction = $hub->startTransaction(new TransactionContext); $transaction->initSpanRecorder(); - $this->getCurrentScope()->setSpan($transaction); + $this->getCurrentSentryScope()->setSpan($transaction); return $transaction; }