diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cea3cf83..da978ff3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,14 +21,6 @@ jobs: php: [ "8.1", "8.0", "7.4", "7.3", "7.2" ] packages: # All versions below should be test on PHP ^7.1 (Sentry SDK requirement) - - { laravel: 5.1.*, testbench: 3.1.*, phpunit: 5.7.* } - - { laravel: 5.2.*, testbench: 3.2.*, phpunit: 5.7.* } - - { laravel: 5.3.*, testbench: 3.3.*, phpunit: 5.7.* } - - { laravel: 5.4.*, testbench: 3.4.*, phpunit: 5.7.* } - - { laravel: 5.5.*, testbench: 3.5.*, phpunit: 6.5.* } - - { laravel: 5.6.*, testbench: 3.6.*, phpunit: 7.5.* } - - { laravel: 5.7.*, testbench: 3.7.*, phpunit: 7.5.* } - - { laravel: 5.8.*, testbench: 3.8.*, phpunit: 7.5.* } - { laravel: ^6.0, testbench: 4.7.*, phpunit: 8.4.* } - { laravel: ^7.0, testbench: 5.1.*, phpunit: 8.4.* } @@ -48,43 +40,11 @@ jobs: - php: "7.2" packages: { laravel: ^8.0, testbench: ^6.0, phpunit: 9.3.* } - - php: "8.0" - packages: { laravel: 5.1.*, testbench: 3.1.*, phpunit: 5.7.* } - - php: "8.0" - packages: { laravel: 5.2.*, testbench: 3.2.*, phpunit: 5.7.* } - - php: "8.0" - packages: { laravel: 5.3.*, testbench: 3.3.*, phpunit: 5.7.* } - - php: "8.0" - packages: { laravel: 5.4.*, testbench: 3.4.*, phpunit: 5.7.* } - - php: "8.0" - packages: { laravel: 5.5.*, testbench: 3.5.*, phpunit: 6.5.* } - - php: "8.0" - packages: { laravel: 5.6.*, testbench: 3.6.*, phpunit: 7.5.* } - - php: "8.0" - packages: { laravel: 5.7.*, testbench: 3.7.*, phpunit: 7.5.* } - - php: "8.0" - packages: { laravel: 5.8.*, testbench: 3.8.*, phpunit: 7.5.* } - php: "8.0" packages: { laravel: ^6.0, testbench: 4.7.*, phpunit: 8.4.* } - php: "8.0" packages: { laravel: ^7.0, testbench: 5.1.*, phpunit: 8.4.* } - - php: "8.1" - packages: { laravel: 5.1.*, testbench: 3.1.*, phpunit: 5.7.* } - - php: "8.1" - packages: { laravel: 5.2.*, testbench: 3.2.*, phpunit: 5.7.* } - - php: "8.1" - packages: { laravel: 5.3.*, testbench: 3.3.*, phpunit: 5.7.* } - - php: "8.1" - packages: { laravel: 5.4.*, testbench: 3.4.*, phpunit: 5.7.* } - - php: "8.1" - packages: { laravel: 5.5.*, testbench: 3.5.*, phpunit: 6.5.* } - - php: "8.1" - packages: { laravel: 5.6.*, testbench: 3.6.*, phpunit: 7.5.* } - - php: "8.1" - packages: { laravel: 5.7.*, testbench: 3.7.*, phpunit: 7.5.* } - - php: "8.1" - packages: { laravel: 5.8.*, testbench: 3.8.*, phpunit: 7.5.* } - php: "8.1" packages: { laravel: ^6.0, testbench: 4.7.*, phpunit: 8.4.* } - php: "8.1" diff --git a/CHANGELOG.md b/CHANGELOG.md index 4630d591..fcd95b74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ ## Unreleased +## 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) @@ -9,6 +36,7 @@ ## 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 diff --git a/README.md b/README.md index 30be5cbd..29089b1c 100644 --- a/README.md +++ b/README.md @@ -78,15 +78,21 @@ try { ## Laravel Version Compatibility -- Laravel `<= 4.2.x` is supported until `0.8.x` -- Laravel `<= 5.7.x` on PHP `<= 7.0` is supported until `0.11.x` -- Laravel `>= 5.x.x` on PHP `>= 7.1` is supported in all versions +The Laravel versions listed below are all currently supported: + - Laravel `>= 6.x.x` on PHP `>= 7.2` is supported starting from `1.2.0` - Laravel `>= 7.x.x` on PHP `>= 7.2` is supported starting from `1.7.0` - Laravel `>= 8.x.x` on PHP `>= 7.3` is supported starting from `1.9.0` - Laravel `>= 9.x.x` on PHP `>= 8.0` is supported starting from `2.11.0` -Please note that of version `>= 2.0.0` we require PHP Version `>= 7.2` because we are using our new [PHP SDK](https://github.com/getsentry/sentry-php) underneath. +Please note that starting with version `>= 2.0.0` we require PHP Version `>= 7.2` because we are using our new [PHP SDK](https://github.com/getsentry/sentry-php) underneath. + +The Laravel and Lumen version listed below were supported in previous versions: + +- Laravel `<= 4.2.x` is supported until `0.8.x` +- Laravel `<= 5.7.x` on PHP `<= 7.0` is supported until `0.11.x` +- Laravel `>= 5.x.x` on PHP `>= 7.1` is supported until `2.14.x` +- Laravel Lumen is supported until `2.14.x` ## Contributing to the SDK diff --git a/composer.json b/composer.json index 108447d4..c3643221 100644 --- a/composer.json +++ b/composer.json @@ -22,21 +22,24 @@ ], "require": { "php": "^7.2 | ^8.0", - "illuminate/support": "5.0 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0", - "sentry/sentry": "^3.3", + "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0", + "sentry/sentry": "^3.9", "sentry/sdk": "^3.1", "symfony/psr-http-message-bridge": "^1.0 | ^2.0", "nyholm/psr7": "^1.0" }, + "conflict": { + "laravel/lumen-framework": "*" + }, "autoload": { "psr-0": { "Sentry\\Laravel\\": "src/" } }, "require-dev": { - "phpunit/phpunit": "^5.7 | ^6.5 | ^7.5 | ^8.4 | ^9.3", - "laravel/framework": "5.0 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0", - "orchestra/testbench": "3.1 - 3.8 | ^4.7 | ^5.1 | ^6.0 | ^7.0", + "phpunit/phpunit": "^8.4 | ^9.3", + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0", + "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0", "friendsofphp/php-cs-fixer": "^3.11", "mockery/mockery": "^1.3" }, @@ -61,8 +64,8 @@ }, "extra": { "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-master": "2.x-dev", + "dev-master": "3.x-dev", + "dev-2.x": "2.x-dev", "dev-1.x": "1.x-dev", "dev-0.x": "0.x-dev" }, diff --git a/config/sentry.php b/config/sentry.php index b0e4b700..08c918bf 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -45,6 +45,9 @@ // Indicates if the tracing integrations supplied by Sentry should be loaded 'default_integrations' => true, + + // Indicates that requests without a matching route should be traced + 'missing_routes' => false, ], // @see: https://docs.sentry.io/platforms/php/configuration/options/#send-default-pii @@ -52,6 +55,4 @@ 'traces_sample_rate' => (float)(env('SENTRY_TRACES_SAMPLE_RATE', 0.0)), - 'controllers_base_namespace' => env('SENTRY_CONTROLLERS_BASE_NAMESPACE', 'App\\Http\\Controllers'), - ]; diff --git a/src/Sentry/Laravel/Console/PublishCommand.php b/src/Sentry/Laravel/Console/PublishCommand.php index 04cd0d54..19f253ef 100644 --- a/src/Sentry/Laravel/Console/PublishCommand.php +++ b/src/Sentry/Laravel/Console/PublishCommand.php @@ -10,13 +10,6 @@ class PublishCommand extends Command { - /** - * Laravel 5.0.x: The name and signature of the console command. - * - * @var string - */ - protected $name = 'sentry:publish'; - /** * The name and signature of the console command. * diff --git a/src/Sentry/Laravel/Console/TestCommand.php b/src/Sentry/Laravel/Console/TestCommand.php index d921317d..67c60847 100644 --- a/src/Sentry/Laravel/Console/TestCommand.php +++ b/src/Sentry/Laravel/Console/TestCommand.php @@ -12,17 +12,11 @@ use Sentry\State\HubInterface; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; use Throwable; class TestCommand extends Command { - /** - * Laravel 5.0.x: The name and signature of the console command. - * - * @var string - */ - protected $name = 'sentry:test'; - /** * The name and signature of the console command. * @@ -145,6 +139,7 @@ public function log($level, $message, array $context = []): void $transactionContext = new TransactionContext(); $transactionContext->setSampled(true); $transactionContext->setName('Sentry Test Transaction'); + $transactionContext->setSource(TransactionSource::custom()); $transactionContext->setOp('sentry.test'); $transaction = $hub->startTransaction($transactionContext); diff --git a/src/Sentry/Laravel/EventHandler.php b/src/Sentry/Laravel/EventHandler.php index 5ad15db6..e2f32924 100644 --- a/src/Sentry/Laravel/EventHandler.php +++ b/src/Sentry/Laravel/EventHandler.php @@ -3,24 +3,19 @@ namespace Sentry\Laravel; use Exception; -use Illuminate\Auth\Events\Authenticated; -use Illuminate\Console\Events\CommandFinished; -use Illuminate\Console\Events\CommandStarting; +use Illuminate\Auth\Events as AuthEvents; +use Illuminate\Console\Events as ConsoleEvents; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Events\QueryExecuted; +use Illuminate\Database\Events as DatabaseEvents; use Illuminate\Http\Request; -use Illuminate\Log\Events\MessageLogged; -use Illuminate\Queue\Events\JobExceptionOccurred; -use Illuminate\Queue\Events\JobProcessed; -use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Queue\Events\WorkerStopping; +use Illuminate\Log\Events as LogEvents; +use Illuminate\Queue\Events as QueueEvents; use Illuminate\Queue\QueueManager; -use Illuminate\Routing\Events\RouteMatched; -use Illuminate\Routing\Route; +use Illuminate\Routing\Events as RoutingEvents; use Laravel\Octane\Events as Octane; use Laravel\Sanctum\Events as Sanctum; use RuntimeException; @@ -38,17 +33,11 @@ class EventHandler * @var array */ protected static $eventHandlerMap = [ - 'router.matched' => 'routerMatched', // Until Laravel 5.1 - 'Illuminate\Routing\Events\RouteMatched' => 'routeMatched', // Since Laravel 5.2 - - 'illuminate.query' => 'query', // Until Laravel 5.1 - 'Illuminate\Database\Events\QueryExecuted' => 'queryExecuted', // Since Laravel 5.2 - - 'illuminate.log' => 'log', // Until Laravel 5.3 - 'Illuminate\Log\Events\MessageLogged' => 'messageLogged', // Since Laravel 5.4 - - 'Illuminate\Console\Events\CommandStarting' => 'commandStarting', // Since Laravel 5.5 - 'Illuminate\Console\Events\CommandFinished' => 'commandFinished', // Since Laravel 5.5 + LogEvents\MessageLogged::class => 'messageLogged', + RoutingEvents\RouteMatched::class => 'routeMatched', + DatabaseEvents\QueryExecuted::class => 'queryExecuted', + ConsoleEvents\CommandStarting::class => 'commandStarting', + ConsoleEvents\CommandFinished::class => 'commandFinished', ]; /** @@ -57,8 +46,8 @@ class EventHandler * @var array */ protected static $authEventHandlerMap = [ - 'Illuminate\Auth\Events\Authenticated' => 'authenticated', // Since Laravel 5.3 - 'Laravel\Sanctum\Events\TokenAuthenticated' => 'sanctumTokenAuthenticated', // Since Sanctum 2.13 + AuthEvents\Authenticated::class => 'authenticated', + Sanctum\TokenAuthenticated::class => 'sanctumTokenAuthenticated', // Since Sanctum 2.13 ]; /** @@ -67,10 +56,10 @@ class EventHandler * @var array */ protected static $queueEventHandlerMap = [ - 'Illuminate\Queue\Events\JobProcessed' => 'queueJobProcessed', // Since Laravel 5.2 - 'Illuminate\Queue\Events\JobProcessing' => 'queueJobProcessing', // Since Laravel 5.2 - 'Illuminate\Queue\Events\WorkerStopping' => 'queueWorkerStopping', // Since Laravel 5.2 - 'Illuminate\Queue\Events\JobExceptionOccurred' => 'queueJobExceptionOccurred', // Since Laravel 5.2 + QueueEvents\JobProcessed::class => 'queueJobProcessed', + QueueEvents\JobProcessing::class => 'queueJobProcessing', + QueueEvents\WorkerStopping::class => 'queueWorkerStopping', + QueueEvents\JobExceptionOccurred::class => 'queueJobExceptionOccurred', ]; /** @@ -79,17 +68,17 @@ class EventHandler * @var array */ protected static $octaneEventHandlerMap = [ - 'Laravel\Octane\Events\RequestReceived' => 'octaneRequestReceived', - 'Laravel\Octane\Events\RequestTerminated' => 'octaneRequestTerminated', + Octane\RequestReceived::class => 'octaneRequestReceived', + Octane\RequestTerminated::class => 'octaneRequestTerminated', - 'Laravel\Octane\Events\TaskReceived' => 'octaneTaskReceived', - 'Laravel\Octane\Events\TaskTerminated' => 'octaneTaskTerminated', + Octane\TaskReceived::class => 'octaneTaskReceived', + Octane\TaskTerminated::class => 'octaneTaskTerminated', - 'Laravel\Octane\Events\TickReceived' => 'octaneTickReceived', - 'Laravel\Octane\Events\TickTerminated' => 'octaneTickTerminated', + Octane\TickReceived::class => 'octaneTickReceived', + Octane\TickTerminated::class => 'octaneTickTerminated', - 'Laravel\Octane\Events\WorkerErrorOccurred' => 'octaneWorkerErrorOccurred', - 'Laravel\Octane\Events\WorkerStopping' => 'octaneWorkerStopping', + Octane\WorkerErrorOccurred::class => 'octaneWorkerErrorOccurred', + Octane\WorkerStopping::class => 'octaneWorkerStopping', ]; /** @@ -151,9 +140,9 @@ class EventHandler /** * Indicates if we pushed a scope for the queue. * - * @var bool + * @var int */ - private $pushedQueueScope = false; + private $pushedQueueScopeCount = 0; /** * Indicates if we pushed a scope for Octane. @@ -184,76 +173,40 @@ public function __construct(Container $container, array $config) /** * Attach all event handlers. */ - public function subscribe(): void + public function subscribe(Dispatcher $dispatcher): void { - /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ - try { - $dispatcher = $this->container->make(Dispatcher::class); - - foreach (static::$eventHandlerMap as $eventName => $handler) { - $dispatcher->listen($eventName, [$this, $handler]); - } - } catch (BindingResolutionException $e) { - // If we cannot resolve the event dispatcher we also cannot listen to events + foreach (static::$eventHandlerMap as $eventName => $handler) { + $dispatcher->listen($eventName, [$this, $handler]); } } /** * Attach all authentication event handlers. */ - public function subscribeAuthEvents(): void + public function subscribeAuthEvents(Dispatcher $dispatcher): void { - /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ - try { - $dispatcher = $this->container->make(Dispatcher::class); - - foreach (static::$authEventHandlerMap as $eventName => $handler) { - $dispatcher->listen($eventName, [$this, $handler]); - } - } catch (BindingResolutionException $e) { - // If we cannot resolve the event dispatcher we also cannot listen to events + foreach (static::$authEventHandlerMap as $eventName => $handler) { + $dispatcher->listen($eventName, [$this, $handler]); } } /** * Attach all queue event handlers. */ - public function subscribeOctaneEvents(): void + public function subscribeOctaneEvents(Dispatcher $dispatcher): void { - /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ - try { - $dispatcher = $this->container->make(Dispatcher::class); - - foreach (static::$octaneEventHandlerMap as $eventName => $handler) { - $dispatcher->listen($eventName, [$this, $handler]); - } - } catch (BindingResolutionException $e) { - // If we cannot resolve the event dispatcher we also cannot listen to events + foreach (static::$octaneEventHandlerMap as $eventName => $handler) { + $dispatcher->listen($eventName, [$this, $handler]); } } /** * Attach all queue event handlers. - * - * @param \Illuminate\Queue\QueueManager $queue */ - public function subscribeQueueEvents(QueueManager $queue): void + public function subscribeQueueEvents(Dispatcher $dispatcher): void { - $queue->looping(function () { - $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedQueueScope); - - $this->pushedQueueScope = false; - }); - - /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ - try { - $dispatcher = $this->container->make(Dispatcher::class); - - foreach (static::$queueEventHandlerMap as $eventName => $handler) { - $dispatcher->listen($eventName, [$this, $handler]); - } - } catch (BindingResolutionException $e) { - // If we cannot resolve the event dispatcher we also cannot listen to events + foreach (static::$queueEventHandlerMap as $eventName => $handler) { + $dispatcher->listen($eventName, [$this, $handler]); } } @@ -263,7 +216,7 @@ public function subscribeQueueEvents(QueueManager $queue): void * @param string $method * @param array $arguments */ - public function __call($method, $arguments) + public function __call(string $method, array $arguments) { $handlerMethod = "{$method}Handler"; @@ -278,14 +231,9 @@ public function __call($method, $arguments) } } - /** - * Until Laravel 5.1 - * - * @param Route $route - */ - protected function routerMatchedHandler(Route $route) + protected function routeMatchedHandler(RoutingEvents\RouteMatched $match): void { - $routeName = Integration::extractNameForRoute($route); + [$routeName] = Integration::extractNameAndSourceForRoute($match->route); Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, @@ -297,106 +245,32 @@ protected function routerMatchedHandler(Route $route) Integration::setTransaction($routeName); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Routing\Events\RouteMatched $match - */ - protected function routeMatchedHandler(RouteMatched $match) - { - $this->routerMatchedHandler($match->route); - } - - /** - * Until Laravel 5.1 - * - * @param string $query - * @param array $bindings - * @param int $time - * @param string $connectionName - */ - protected function queryHandler($query, $bindings, $time, $connectionName) + protected function queryExecutedHandler(DatabaseEvents\QueryExecuted $query): void { if (!$this->recordSqlQueries) { return; } - $this->addQueryBreadcrumb($query, $bindings, $time, $connectionName); - } - - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Database\Events\QueryExecuted $query - */ - protected function queryExecutedHandler(QueryExecuted $query) - { - if (!$this->recordSqlQueries) { - return; - } - - $this->addQueryBreadcrumb($query->sql, $query->bindings, $query->time, $query->connectionName); - } - - /** - * Helper to add an query breadcrumb. - * - * @param string $query - * @param array $bindings - * @param float|null $time - * @param string $connectionName - */ - private function addQueryBreadcrumb($query, $bindings, $time, $connectionName) - { - $data = ['connectionName' => $connectionName]; + $data = ['connectionName' => $query->connectionName]; - if ($time !== null) { - $data['executionTimeMs'] = $time; + if ($query->time !== null) { + $data['executionTimeMs'] = $query->time; } if ($this->recordSqlBindings) { - $data['bindings'] = $bindings; + $data['bindings'] = $query->bindings; } Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'sql.query', - $query, + $query->sql, $data )); } - /** - * Until Laravel 5.3 - * - * @param string $level - * @param string $message - * @param array|null $context - */ - protected function logHandler($level, $message, $context) - { - $this->addLogBreadcrumb($level, $message, is_array($context) ? $context : []); - } - - /** - * Since Laravel 5.4 - * - * @param \Illuminate\Log\Events\MessageLogged $logEntry - */ - protected function messageLoggedHandler(MessageLogged $logEntry) - { - $this->addLogBreadcrumb($logEntry->level, $logEntry->message, $logEntry->context); - } - - /** - * Helper to add an log breadcrumb. - * - * @param string $level Log level. May be any standard. - * @param string|null $message Log message. - * @param array $context Log context. - */ - private function addLogBreadcrumb(string $level, ?string $message, array $context = []): void + protected function messageLoggedHandler(LogEvents\MessageLogged $logEntry): void { if (!$this->recordLaravelLogs) { return; @@ -405,35 +279,25 @@ private function addLogBreadcrumb(string $level, ?string $message, array $contex // A log message with `null` as value will not be recorded by Laravel // however empty strings are logged so we mimick that behaviour to // check for `null` to stay consistent with how Laravel logs it - if ($message === null) { + if ($logEntry->message === null) { return; } Integration::addBreadcrumb(new Breadcrumb( - $this->logLevelToBreadcrumbLevel($level), + $this->logLevelToBreadcrumbLevel($logEntry->level), Breadcrumb::TYPE_DEFAULT, - 'log.' . $level, - $message, - $context + 'log.' . $logEntry->level, + $logEntry->message, + $logEntry->context )); } - /** - * Since Laravel 5.3 - * - * @param \Illuminate\Auth\Events\Authenticated $event - */ - protected function authenticatedHandler(Authenticated $event) + protected function authenticatedHandler(AuthEvents\Authenticated $event): void { $this->configureUserScopeFromModel($event->user); } - /** - * Since Sanctum 2.13 - * - * @param \Laravel\Sanctum\Events\TokenAuthenticated $event - */ - protected function sanctumTokenAuthenticatedHandler(Sanctum\TokenAuthenticated $event) + protected function sanctumTokenAuthenticatedHandler(Sanctum\TokenAuthenticated $event): void { $this->configureUserScopeFromModel($event->token->tokenable); } @@ -480,18 +344,11 @@ private function configureUserScopeFromModel($authUser): void }); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\JobProcessing $event - */ - protected function queueJobProcessingHandler(JobProcessing $event) + protected function queueJobProcessingHandler(QueueEvents\JobProcessing $event): void { - $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedQueueScope); - $this->prepareScopeForTaskWithinLongRunningProcess(); - $this->pushedQueueScope = true; + ++$this->pushedQueueScopeCount; if (!$this->recordQueueInfo) { return; @@ -518,43 +375,27 @@ protected function queueJobProcessingHandler(JobProcessing $event) )); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\JobExceptionOccurred $event - */ - protected function queueJobExceptionOccurredHandler(JobExceptionOccurred $event) + protected function queueJobExceptionOccurredHandler(QueueEvents\JobExceptionOccurred $event): void { + $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedQueueScopeCount > 0); + $this->afterTaskWithinLongRunningProcess(); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\JobProcessed $event - */ - protected function queueJobProcessedHandler(JobProcessed $event) + protected function queueJobProcessedHandler(QueueEvents\JobProcessed $event): void { + $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedQueueScopeCount > 0); + $this->afterTaskWithinLongRunningProcess(); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\WorkerStopping $event - */ - protected function queueWorkerStoppingHandler(WorkerStopping $event) + protected function queueWorkerStoppingHandler(QueueEvents\WorkerStopping $event): void { // Flush any and all events that were possibly generated by queue jobs Integration::flushEvents(); } - /** - * Since Laravel 5.5 - * - * @param \Illuminate\Console\Events\CommandStarting $event - */ - protected function commandStartingHandler(CommandStarting $event) + protected function commandStartingHandler(ConsoleEvents\CommandStarting $event): void { if ($event->command) { Integration::configureScope(static function (Scope $scope) use ($event): void { @@ -577,12 +418,7 @@ protected function commandStartingHandler(CommandStarting $event) } } - /** - * Since Laravel 5.5 - * - * @param \Illuminate\Console\Events\CommandFinished $event - */ - protected function commandFinishedHandler(CommandFinished $event) + protected function commandFinishedHandler(ConsoleEvents\CommandFinished $event): void { if ($this->recordCommandInfo) { Integration::addBreadcrumb(new Breadcrumb( diff --git a/src/Sentry/Laravel/Http/SetRequestMiddleware.php b/src/Sentry/Laravel/Http/SetRequestMiddleware.php index bbdfca4e..ffa85e6a 100644 --- a/src/Sentry/Laravel/Http/SetRequestMiddleware.php +++ b/src/Sentry/Laravel/Http/SetRequestMiddleware.php @@ -4,11 +4,10 @@ use Closure; use Illuminate\Container\Container; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Http\Request; -use Nyholm\Psr7\Factory\Psr17Factory; use Psr\Http\Message\ServerRequestInterface; use Sentry\State\HubInterface; -use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory; /** * This middleware caches a PSR-7 version of the request as early as possible. @@ -21,7 +20,7 @@ public function handle(Request $request, Closure $next) $container = Container::getInstance(); if ($container->bound(HubInterface::class)) { - $psrRequest = $this->resolvePsrRequest($request); + $psrRequest = $this->resolvePsrRequest($container); if ($psrRequest !== null) { $container->instance(LaravelRequestFetcher::CONTAINER_PSR7_INSTANCE_KEY, $psrRequest); @@ -31,26 +30,12 @@ public function handle(Request $request, Closure $next) return $next($request); } - /** - * This code was copied from the Laravel codebase which was introduced in Laravel 6. - * - * The reason we have it copied here is because older (<6.0) versions of Laravel use a different - * method to construct the PSR-7 request object which requires other packages to create that object - * but most importantly it does not function when those packages are not available resulting in errors - * - * So long story short, this is here to backport functionality to Laravel <6.0 - * if we drop support for those versions in the future we can reconsider this and - * move back to using the container binding provided by Laravel for the PSR-7 object - * - * @see https://github.com/laravel/framework/blob/cb550b5bdc2b2c4cf077082adabde0144a72d190/src/Illuminate/Routing/RoutingServiceProvider.php#L127-L146 - */ - private function resolvePsrRequest(Request $request): ?ServerRequestInterface + private function resolvePsrRequest(Container $container): ?ServerRequestInterface { - if (class_exists(Psr17Factory::class) && class_exists(PsrHttpFactory::class)) { - $psr17Factory = new Psr17Factory; - - return (new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory)) - ->createRequest($request); + try { + return $container->make(ServerRequestInterface::class); + } catch (BindingResolutionException $e) { + // This happens if Laravel doesn't have the correct classes available to construct the PSR-7 object } return null; diff --git a/src/Sentry/Laravel/Integration.php b/src/Sentry/Laravel/Integration.php index f400aaf5..a9df0948 100644 --- a/src/Sentry/Laravel/Integration.php +++ b/src/Sentry/Laravel/Integration.php @@ -3,9 +3,8 @@ namespace Sentry\Laravel; use Illuminate\Routing\Route; -use Illuminate\Support\Str; use Sentry\SentrySdk; -use Sentry\Tracing\Span; +use Sentry\Tracing\TransactionSource; use function Sentry\addBreadcrumb; use function Sentry\configureScope; use Sentry\Breadcrumb; @@ -20,11 +19,6 @@ class Integration implements IntegrationInterface */ private static $transaction; - /** - * @var null|string - */ - private static $baseControllerNamespace; - /** * {@inheritdoc} */ @@ -93,14 +87,6 @@ public static function setTransaction(?string $transaction): void self::$transaction = $transaction; } - /** - * @param null|string $namespace - */ - public static function setControllersBaseNamespace(?string $namespace): void - { - self::$baseControllerNamespace = $namespace !== null ? trim($namespace, '\\') : null; - } - /** * Block until all async events are processed for the HTTP transport. * @@ -117,150 +103,63 @@ public static function flushEvents(): void } /** - * Extract the readable name for a route. + * Extract the readable name for a route and the transaction source for where that route name came from. * * @param \Illuminate\Routing\Route $route * - * @return string - */ - public static function extractNameForRoute(Route $route): string - { - $routeName = null; - - // someaction (route name/alias) - if ($route->getName()) { - $routeName = self::extractNameForNamedRoute($route->getName()); - } - - // Some\Controller@someAction (controller action) - if (empty($routeName) && $route->getActionName()) { - $routeName = self::extractNameForActionRoute($route->getActionName()); - } - - // /someaction // Fallback to the url - if (empty($routeName) || $routeName === 'Closure') { - $routeName = '/' . ltrim($route->uri(), '/'); - } - - return $routeName; - } - - /** - * Extract the readable name for a Lumen route. - * - * @param array $routeData The array of route data - * @param string $path The path of the request + * @return array{0: string, 1: \Sentry\Tracing\TransactionSource} * - * @return string + * @internal This helper is used in various places to extra meaninful info from a Laravel Route object. */ - public static function extractNameForLumenRoute(array $routeData, string $path): string + public static function extractNameAndSourceForRoute(Route $route): array { - $routeName = null; - - $route = $routeData[1] ?? []; - - // someaction (route name/alias) - if (!empty($route['as'])) { - $routeName = self::extractNameForNamedRoute($route['as']); - } - - // Some\Controller@someAction (controller action) - if (empty($routeName) && !empty($route['uses'])) { - $routeName = self::extractNameForActionRoute($route['uses']); - } - - // /someaction // Fallback to the url - if (empty($routeName) || $routeName === 'Closure') { - $routeUri = array_reduce( - array_keys($routeData[2]), - static function ($carry, $key) use ($routeData) { - return str_replace($routeData[2][$key], "{{$key}}", $carry); - }, - $path - ); - - $routeName = '/' . ltrim($routeUri, '/'); - } - - return $routeName; + return [ + '/' . ltrim($route->uri(), '/'), + TransactionSource::route(), + ]; } /** - * Take a route name and return it only if it's a usable route name. - * - * @param string $name + * Retrieve the meta tags with tracing information to link this request to front-end requests. + * This propagates the Dynamic Sampling Context. * - * @return string|null + * @return string */ - private static function extractNameForNamedRoute(string $name): ?string + public static function sentryMeta(): string { - // Laravel 7 route caching generates a route names if the user didn't specify one - // theirselfs to optimize route matching. These route names are useless to the - // developer so if we encounter a generated route name we discard the value - if (Str::contains($name, 'generated::')) { - return null; - } - - // If the route name ends with a `.` we assume an incomplete group name prefix - // we discard this value since it will most likely not mean anything to the - // developer and will be duplicated by other unnamed routes in the group - if (Str::endsWith($name, '.')) { - return null; - } - - return $name; + return self::sentryTracingMeta() . self::sentryBaggageMeta(); } /** - * Take a controller action and strip away the base namespace if needed. - * - * @param string $action + * Retrieve the `sentry-trace` meta tag with tracing information to link this request to front-end requests. * * @return string */ - private static function extractNameForActionRoute(string $action): string + public static function sentryTracingMeta(): string { - $routeName = ltrim($action, '\\'); + $span = SentrySdk::getCurrentHub()->getSpan(); - $baseNamespace = self::$baseControllerNamespace ?? ''; - - if (empty($baseNamespace)) { - return $routeName; + if ($span === null) { + return ''; } - // Strip away the base namespace from the action name - // @see: Str::after, but this is not available before Laravel 5.4 so we use a inlined version - return array_reverse(explode($baseNamespace . '\\', $routeName, 2))[0]; + return sprintf('', $span->toTraceparent()); } /** - * Retrieve the meta tags with tracing information to link this request to front-end requests. + * Retrieve the `baggage` meta tag with information to link this request to front-end requests. + * This propagates the Dynamic Sampling Context. * * @return string */ - public static function sentryTracingMeta(): string + public static function sentryBaggageMeta(): string { - $span = self::currentTracingSpan(); + $span = SentrySdk::getCurrentHub()->getSpan(); if ($span === null) { return ''; } - $content = sprintf('', $span->toTraceparent()); - // $content .= sprintf('', $span->getDescription()); - - return $content; - } - - /** - * Get the current active tracing span from the scope. - * - * @return \Sentry\Tracing\Span|null - * - * @internal This is used internally as an easy way to retrieve the current active tracing span. - */ - public static function currentTracingSpan(): ?Span - { - return SentrySdk::getCurrentHub()->getSpan(); + return sprintf('', $span->toBaggage()); } } diff --git a/src/Sentry/Laravel/LogChannel.php b/src/Sentry/Laravel/LogChannel.php index 77d3b65a..30d926f6 100644 --- a/src/Sentry/Laravel/LogChannel.php +++ b/src/Sentry/Laravel/LogChannel.php @@ -14,7 +14,7 @@ class LogChannel extends LogManager * * @return Logger */ - public function __invoke(array $config): Logger + public function __invoke(array $config = []): Logger { $handler = new SentryHandler( $this->app->make(HubInterface::class), diff --git a/src/Sentry/Laravel/ServiceProvider.php b/src/Sentry/Laravel/ServiceProvider.php index 596c4cac..5e01a2c6 100644 --- a/src/Sentry/Laravel/ServiceProvider.php +++ b/src/Sentry/Laravel/ServiceProvider.php @@ -2,11 +2,12 @@ namespace Sentry\Laravel; +use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Http\Kernel as HttpKernelInterface; use Illuminate\Foundation\Application as Laravel; use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Log\LogManager; -use Laravel\Lumen\Application as Lumen; use RuntimeException; use Sentry\ClientBuilder; use Sentry\ClientBuilderInterface; @@ -34,7 +35,8 @@ class ServiceProvider extends BaseServiceProvider 'integrations', // This is kept for backwards compatibility and can be dropped in a future breaking release 'breadcrumbs.sql_bindings', - // The base namespace for controllers to strip of the beginning of controller class names + + // This config option is no longer in use but to prevent errors when upgrading we leave it here to be discarded 'controllers_base_namespace', ]; @@ -48,10 +50,7 @@ public function boot(): void if ($this->hasDsnSet()) { $this->bindEvents(); - if ($this->app instanceof Lumen) { - $this->app->middleware(SetRequestMiddleware::class); - $this->app->middleware(SetRequestIpMiddleware::class); - } elseif ($this->app->bound(HttpKernelInterface::class)) { + if ($this->app->bound(HttpKernelInterface::class)) { /** @var \Illuminate\Foundation\Http\Kernel $httpKernel */ $httpKernel = $this->app->make(HttpKernelInterface::class); @@ -78,10 +77,6 @@ public function boot(): void */ public function register(): void { - if ($this->app instanceof Lumen) { - $this->app->configure(static::$abstract); - } - $this->mergeConfigFrom(__DIR__ . '/../../../config/sentry.php', static::$abstract); $this->configureAndRegisterClient(); @@ -102,18 +97,25 @@ protected function bindEvents(): void $handler = new EventHandler($this->app, $userConfig); - $handler->subscribe(); + try { + /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ + $dispatcher = $this->app->make(Dispatcher::class); - if ($this->app->bound('octane')) { - $handler->subscribeOctaneEvents(); - } + $handler->subscribe($dispatcher); - if ($this->app->bound('queue')) { - $handler->subscribeQueueEvents($this->app->make('queue')); - } + if ($this->app->bound('octane')) { + $handler->subscribeOctaneEvents($dispatcher); + } - if (isset($userConfig['send_default_pii']) && $userConfig['send_default_pii'] !== false) { - $handler->subscribeAuthEvents(); + if ($this->app->bound('queue')) { + $handler->subscribeQueueEvents($dispatcher, $this->app->make('queue')); + } + + if (isset($userConfig['send_default_pii']) && $userConfig['send_default_pii'] !== false) { + $handler->subscribeAuthEvents($dispatcher); + } + } catch (BindingResolutionException $e) { + // If we cannot resolve the event dispatcher we also cannot listen to events } } @@ -133,12 +135,6 @@ protected function registerArtisanCommands(): void */ protected function configureAndRegisterClient(): void { - $userConfig = $this->getUserConfig(); - - if (isset($userConfig['controllers_base_namespace'])) { - Integration::setControllersBaseNamespace($userConfig['controllers_base_namespace']); - } - $this->app->bind(ClientBuilderInterface::class, function () { $basePath = base_path(); $userConfig = $this->getUserConfig(); @@ -169,7 +165,7 @@ protected function configureAndRegisterClient(): void return $clientBuilder; }); - $this->app->singleton(HubInterface::class, function ($app) { + $this->app->singleton(HubInterface::class, function () { /** @var \Sentry\ClientBuilderInterface $clientBuilder */ $clientBuilder = $this->app->make(ClientBuilderInterface::class); @@ -177,7 +173,7 @@ protected function configureAndRegisterClient(): void $userIntegrations = $this->resolveIntegrationsFromUserConfig(); - $options->setIntegrations(function (array $integrations) use ($options, $userIntegrations, $app) { + $options->setIntegrations(function (array $integrations) use ($options, $userIntegrations) { if ($options->hasDefaultIntegrations()) { // Remove the default error and fatal exception listeners to let Laravel handle those // itself. These event are still bubbling up through the documented changes in the users diff --git a/src/Sentry/Laravel/Tracing/EventHandler.php b/src/Sentry/Laravel/Tracing/EventHandler.php index 054dc62e..3850d540 100644 --- a/src/Sentry/Laravel/Tracing/EventHandler.php +++ b/src/Sentry/Laravel/Tracing/EventHandler.php @@ -3,22 +3,25 @@ namespace Sentry\Laravel\Tracing; use Exception; -use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Events as DatabaseEvents; +use Illuminate\Http\Client\Events as HttpClientEvents; use Illuminate\Queue\Events as QueueEvents; use Illuminate\Queue\Queue; use Illuminate\Queue\QueueManager; +use Illuminate\Routing\Events as RoutingEvents; use RuntimeException; use Sentry\Laravel\Integration; use Sentry\SentrySdk; +use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; class EventHandler { + public const QUEUE_PAYLOAD_BAGGAGE_DATA = 'sentry_baggage_data'; public const QUEUE_PAYLOAD_TRACE_PARENT_DATA = 'sentry_trace_parent_data'; /** @@ -27,8 +30,14 @@ class EventHandler * @var array */ protected static $eventHandlerMap = [ - 'illuminate.query' => 'query', // Until Laravel 5.1 - DatabaseEvents\QueryExecuted::class => 'queryExecuted', // Since Laravel 5.2 + RoutingEvents\RouteMatched::class => 'routeMatched', + DatabaseEvents\QueryExecuted::class => 'queryExecuted', + HttpClientEvents\RequestSending::class => 'httpClientRequestSending', + HttpClientEvents\ResponseReceived::class => 'httpClientResponseReceived', + HttpClientEvents\ConnectionFailed::class => 'httpClientConnectionFailed', + DatabaseEvents\TransactionBeginning::class => 'transactionBeginning', + DatabaseEvents\TransactionCommitted::class => 'transactionCommitted', + DatabaseEvents\TransactionRolledBack::class => 'transactionRolledBack', ]; /** @@ -37,18 +46,11 @@ class EventHandler * @var array */ protected static $queueEventHandlerMap = [ - QueueEvents\JobProcessing::class => 'queueJobProcessing', // Since Laravel 5.2 - QueueEvents\JobProcessed::class => 'queueJobProcessed', // Since Laravel 5.2 - QueueEvents\JobExceptionOccurred::class => 'queueJobExceptionOccurred', // Since Laravel 5.2 + QueueEvents\JobProcessing::class => 'queueJobProcessing', + QueueEvents\JobProcessed::class => 'queueJobProcessed', + QueueEvents\JobExceptionOccurred::class => 'queueJobExceptionOccurred', ]; - /** - * The Laravel container. - * - * @var \Illuminate\Contracts\Container\Container - */ - private $container; - /** * Indicates if we should we add SQL queries as spans. * @@ -78,18 +80,18 @@ class EventHandler private $traceQueueJobsAsTransactions; /** - * Holds a reference to the parent queue job span. + * Hold the stack of parent spans that need to be put back on the scope. * - * @var \Sentry\Tracing\Span|null + * @var array */ - private $parentQueueJobSpan; + private $parentSpanStack = []; /** - * Holds a reference to the current queue job span or transaction. + * Hold the stack of current spans that need to be finished still. * - * @var \Sentry\Tracing\Transaction|\Sentry\Tracing\Span|null + * @var array */ - private $currentQueueJobSpan; + private $currentSpanStack = []; /** * The backtrace helper. @@ -100,78 +102,64 @@ class EventHandler /** * EventHandler constructor. - * - * @param \Illuminate\Contracts\Container\Container $container - * @param \Sentry\Laravel\Tracing\BacktraceHelper $backtraceHelper - * @param array $config */ - public function __construct(Container $container, BacktraceHelper $backtraceHelper, array $config) + public function __construct(array $config, BacktraceHelper $backtraceHelper) { - $this->container = $container; - $this->backtraceHelper = $backtraceHelper; - $this->traceSqlQueries = ($config['sql_queries'] ?? true) === true; $this->traceSqlQueryOrigins = ($config['sql_origin'] ?? true) === true; $this->traceQueueJobs = ($config['queue_jobs'] ?? false) === true; $this->traceQueueJobsAsTransactions = ($config['queue_job_transactions'] ?? false) === true; + + $this->backtraceHelper = $backtraceHelper; } /** * Attach all event handlers. + * + * @uses self::routeMatchedHandler() + * @uses self::queryExecutedHandler() + * @uses self::transactionBeginningHandler() + * @uses self::transactionCommittedHandler() + * @uses self::transactionRolledBackHandler() + * @uses self::httpClientRequestSendingHandler() + * @uses self::httpClientResponseReceivedHandler() + * @uses self::httpClientConnectionFailedHandler() */ - public function subscribe(): void + public function subscribe(Dispatcher $dispatcher): void { - try { - /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ - $dispatcher = $this->container->make(Dispatcher::class); - - foreach (static::$eventHandlerMap as $eventName => $handler) { - $dispatcher->listen($eventName, [$this, $handler]); - } - } catch (BindingResolutionException $e) { - // If we cannot resolve the event dispatcher we also cannot listen to events + foreach (static::$eventHandlerMap as $eventName => $handler) { + $dispatcher->listen($eventName, [$this, $handler]); } } /** * Attach all queue event handlers. * - * @param \Illuminate\Queue\QueueManager $queue + * @uses self::queueJobProcessingHandler() + * @uses self::queueJobProcessedHandler() + * @uses self::queueJobExceptionOccurredHandler() */ - public function subscribeQueueEvents(QueueManager $queue): void + public function subscribeQueueEvents(Dispatcher $dispatcher, QueueManager $queue): void { // If both types of queue job tracing is disabled also do not register the events if (!$this->traceQueueJobs && !$this->traceQueueJobsAsTransactions) { return; } - // The payload create callback was introduced in Laravel 5.7 so we need to guard against older versions - if (method_exists(Queue::class, 'createPayloadUsing')) { - Queue::createPayloadUsing(static function (?string $connection, ?string $queue, ?array $payload): ?array { - $currentSpan = Integration::currentTracingSpan(); + Queue::createPayloadUsing(static function (?string $connection, ?string $queue, ?array $payload): ?array { + $currentSpan = SentrySdk::getCurrentHub()->getSpan(); - if ($currentSpan !== null && $payload !== null) { - $payload[self::QUEUE_PAYLOAD_TRACE_PARENT_DATA] = $currentSpan->toTraceparent(); - } - - return $payload; - }); - } + if ($currentSpan !== null && $payload !== null) { + $payload[self::QUEUE_PAYLOAD_TRACE_PARENT_DATA] = $currentSpan->toTraceparent(); + $payload[self::QUEUE_PAYLOAD_BAGGAGE_DATA] = $currentSpan->toBaggage(); + } - $queue->looping(function () { - $this->afterQueuedJob(); + return $payload; }); - try { - /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ - $dispatcher = $this->container->make(Dispatcher::class); - - foreach (static::$queueEventHandlerMap as $eventName => $handler) { - $dispatcher->listen($eventName, [$this, $handler]); - } - } catch (BindingResolutionException $e) { - // If we cannot resolve the event dispatcher we also cannot listen to events + foreach (static::$queueEventHandlerMap as $eventName => $handler) { + $dispatcher->listen($eventName, [$this, $handler]); } } @@ -181,7 +169,7 @@ public function subscribeQueueEvents(QueueManager $queue): void * @param string $method * @param array $arguments */ - public function __call($method, $arguments) + public function __call(string $method, array $arguments) { $handlerMethod = "{$method}Handler"; @@ -191,47 +179,32 @@ public function __call($method, $arguments) try { call_user_func_array([$this, $handlerMethod], $arguments); - } catch (Exception $exception) { - // Ignore + } catch (Exception $e) { + // Ignore to prevent bubbling up errors in the SDK } } - /** - * Until Laravel 5.1 - * - * @param string $query - * @param array $bindings - * @param int $time - * @param string $connectionName - */ - protected function queryHandler($query, $bindings, $time, $connectionName): void + protected function routeMatchedHandler(RoutingEvents\RouteMatched $match): void { - $this->recordQuerySpan($query, $time); - } + $transaction = SentrySdk::getCurrentHub()->getTransaction(); - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Database\Events\QueryExecuted $query - */ - protected function queryExecutedHandler(DatabaseEvents\QueryExecuted $query): void - { - $this->recordQuerySpan($query->sql, $query->time); + if ($transaction === null) { + return; + } + + [$transactionName, $transactionSource] = Integration::extractNameAndSourceForRoute($match->route); + + $transaction->setName($transactionName); + $transaction->getMetadata()->setSource($transactionSource); } - /** - * Helper to add an query breadcrumb. - * - * @param string $query - * @param float|null $time - */ - private function recordQuerySpan($query, $time): void + protected function queryExecutedHandler(DatabaseEvents\QueryExecuted $query): void { if (!$this->traceSqlQueries) { return; } - $parentSpan = Integration::currentTracingSpan(); + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no tracing span active there is no need to handle the event if ($parentSpan === null) { @@ -240,12 +213,12 @@ private function recordQuerySpan($query, $time): void $context = new SpanContext(); $context->setOp('db.sql.query'); - $context->setDescription($query); - $context->setStartTimestamp(microtime(true) - $time / 1000); - $context->setEndTimestamp($context->getStartTimestamp() + $time / 1000); + $context->setDescription($query->sql); + $context->setStartTimestamp(microtime(true) - $query->time / 1000); + $context->setEndTimestamp($context->getStartTimestamp() + $query->time / 1000); if ($this->traceSqlQueryOrigins) { - $queryOrigin = $this->resolveQueryOriginFromBacktrace($context); + $queryOrigin = $this->resolveQueryOriginFromBacktrace(); if ($queryOrigin !== null) { $context->setData(['sql.origin' => $queryOrigin]); @@ -273,14 +246,79 @@ private function resolveQueryOriginFromBacktrace(): ?string return "{$filePath}:{$firstAppFrame->getLine()}"; } - /* - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\JobProcessing $event - */ - protected function queueJobProcessingHandler(QueueEvents\JobProcessing $event) + protected function transactionBeginningHandler(DatabaseEvents\TransactionBeginning $event): void + { + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); + + if ($parentSpan === null) { + return; + } + + $context = new SpanContext; + $context->setOp('db.transaction'); + + $this->pushSpan($parentSpan->startChild($context)); + } + + protected function transactionCommittedHandler(DatabaseEvents\TransactionCommitted $event): void + { + $span = $this->popSpan(); + + if ($span !== null) { + $span->finish(); + $span->setStatus(SpanStatus::ok()); + } + } + + protected function transactionRolledBackHandler(DatabaseEvents\TransactionRolledBack $event): void + { + $span = $this->popSpan(); + + if ($span !== null) { + $span->finish(); + $span->setStatus(SpanStatus::internalError()); + } + } + + protected function httpClientRequestSendingHandler(HttpClientEvents\RequestSending $event): void + { + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); + + if ($parentSpan === null) { + return; + } + + $context = new SpanContext; + + $context->setOp('http.client'); + $context->setDescription($event->request->method() . ' ' . $event->request->url()); + + $this->pushSpan($parentSpan->startChild($context)); + } + + protected function httpClientResponseReceivedHandler(HttpClientEvents\ResponseReceived $event): void + { + $span = $this->popSpan(); + + if ($span !== null) { + $span->finish(); + $span->setHttpStatus($event->response->status()); + } + } + + protected function httpClientConnectionFailedHandler(HttpClientEvents\ConnectionFailed $event): void + { + $span = $this->popSpan(); + + if ($span !== null) { + $span->finish(); + $span->setStatus(SpanStatus::internalError()); + } + } + + protected function queueJobProcessingHandler(QueueEvents\JobProcessing $event): void { - $parentSpan = Integration::currentTracingSpan(); + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no tracing span active and we don't trace jobs as transactions there is no need to handle the event if ($parentSpan === null && !$this->traceQueueJobsAsTransactions) { @@ -293,11 +331,10 @@ protected function queueJobProcessingHandler(QueueEvents\JobProcessing $event) } if ($parentSpan === null) { + $baggage = $event->job->payload()[self::QUEUE_PAYLOAD_BAGGAGE_DATA] ?? null; $traceParent = $event->job->payload()[self::QUEUE_PAYLOAD_TRACE_PARENT_DATA] ?? null; - $context = $traceParent === null - ? new TransactionContext - : TransactionContext::fromSentryTrace($traceParent); + $context = TransactionContext::fromHeaders($traceParent ?? '', $baggage ?? ''); // If the parent transaction was not sampled we also stop the queue job from being recorded if ($context->getParentSampled() === false) { @@ -307,24 +344,19 @@ protected function queueJobProcessingHandler(QueueEvents\JobProcessing $event) $context = new SpanContext; } + $resolvedJobName = $event->job->resolveName(); + $job = [ 'job' => $event->job->getName(), 'queue' => $event->job->getQueue(), + 'resolved' => $resolvedJobName, 'attempts' => $event->job->attempts(), 'connection' => $event->connectionName, ]; - // Resolve name exists only from Laravel 5.3+ - $resolvedJobName = method_exists($event->job, 'resolveName') - ? $event->job->resolveName() - : null; - - if ($resolvedJobName !== null) { - $job['resolved'] = $resolvedJobName; - } - if ($context instanceof TransactionContext) { - $context->setName($resolvedJobName ?? $event->job->getName()); + $context->setName($resolvedJobName); + $context->setSource(TransactionSource::task()); } $context->setOp('queue.process'); @@ -333,47 +365,57 @@ protected function queueJobProcessingHandler(QueueEvents\JobProcessing $event) // When the parent span is null we start a new transaction otherwise we start a child of the current span if ($parentSpan === null) { - $this->currentQueueJobSpan = SentrySdk::getCurrentHub()->startTransaction($context); + $span = SentrySdk::getCurrentHub()->startTransaction($context); } else { - $this->currentQueueJobSpan = $parentSpan->startChild($context); + $span = $parentSpan->startChild($context); } - $this->parentQueueJobSpan = $parentSpan; - - SentrySdk::getCurrentHub()->setSpan($this->currentQueueJobSpan); + $this->pushSpan($span); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\JobExceptionOccurred $event - */ - protected function queueJobExceptionOccurredHandler(QueueEvents\JobExceptionOccurred $event) + protected function queueJobExceptionOccurredHandler(QueueEvents\JobExceptionOccurred $event): void { $this->afterQueuedJob(SpanStatus::internalError()); } - /** - * Since Laravel 5.2 - * - * @param \Illuminate\Queue\Events\JobProcessed $event - */ - protected function queueJobProcessedHandler(QueueEvents\JobProcessed $event) + protected function queueJobProcessedHandler(QueueEvents\JobProcessed $event): void { $this->afterQueuedJob(SpanStatus::ok()); } private function afterQueuedJob(?SpanStatus $status = null): void { - if ($this->currentQueueJobSpan === null) { + $span = $this->popSpan(); + + if ($span === null) { return; } - $this->currentQueueJobSpan->setStatus($status); - $this->currentQueueJobSpan->finish(); - $this->currentQueueJobSpan = null; + $span->setStatus($status); + $span->finish(); + } + + private function pushSpan(Span $span): void + { + $hub = SentrySdk::getCurrentHub(); + + $this->parentSpanStack[] = $hub->getSpan(); + + $hub->setSpan($span); + + $this->currentSpanStack[] = $span; + } + + private function popSpan(): ?Span + { + if (count($this->currentSpanStack) === 0) { + return null; + } + + $parent = array_pop($this->parentSpanStack); + + SentrySdk::getCurrentHub()->setSpan($parent); - SentrySdk::getCurrentHub()->setSpan($this->parentQueueJobSpan); - $this->parentQueueJobSpan = null; + return array_pop($this->currentSpanStack); } } diff --git a/src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php b/src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php index a05f113c..3a36257a 100644 --- a/src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php +++ b/src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php @@ -13,6 +13,7 @@ use Sentry\Laravel\Integration; use Sentry\SentrySdk; use Sentry\Tracing\SpanContext; +use Sentry\Tracing\TransactionSource; class LighthouseIntegration implements IntegrationInterface { @@ -56,7 +57,7 @@ public function setupOnce(): void public function handleStartRequest(StartRequest $startRequest): void { - $this->previousSpan = Integration::currentTracingSpan(); + $this->previousSpan = SentrySdk::getCurrentHub()->getSpan(); if ($this->previousSpan === null) { return; @@ -157,7 +158,7 @@ private function updateTransactionName(): void return; } - array_walk($groupedOperations, static function (array &$operations, string $operationType) { + array_walk($groupedOperations, static function (&$operations, string $operationType) { sort($operations, SORT_STRING); $operations = "{$operationType}{" . implode(',', $operations) . '}'; @@ -168,6 +169,7 @@ private function updateTransactionName(): void $transactionName = 'lighthouse?' . implode('&', $groupedOperations); $transaction->setName($transactionName); + $transaction->getMetadata()->setSource(TransactionSource::custom()); Integration::setTransaction($transactionName); } diff --git a/src/Sentry/Laravel/Tracing/Middleware.php b/src/Sentry/Laravel/Tracing/Middleware.php index 24eb8341..33d2afe2 100644 --- a/src/Sentry/Laravel/Tracing/Middleware.php +++ b/src/Sentry/Laravel/Tracing/Middleware.php @@ -4,14 +4,13 @@ use Closure; use Illuminate\Http\Request; -use Illuminate\Routing\Route; -use Sentry\Laravel\Integration; use Sentry\SentrySdk; use Sentry\State\HubInterface; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TransactionContext; -use Symfony\Component\HttpFoundation\Response; +use Sentry\Tracing\TransactionSource; +use Symfony\Component\HttpFoundation\Response as SymfonyResponse; class Middleware { @@ -44,7 +43,7 @@ class Middleware * * @return mixed */ - public function handle($request, Closure $next) + public function handle(Request $request, Closure $next) { if (app()->bound(HubInterface::class)) { $this->startTransaction($request, app(HubInterface::class)); @@ -56,14 +55,19 @@ public function handle($request, Closure $next) /** * Handle the application termination. * - * @param \Illuminate\Http\Request $request - * @param \Symfony\Component\HttpFoundation\Response $response + * @param \Illuminate\Http\Request $request + * @param mixed $response * * @return void */ - public function terminate($request, $response): void + public function terminate(Request $request, $response): void { if ($this->transaction !== null && app()->bound(HubInterface::class)) { + // 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) { + return; + } + if ($this->appSpan !== null) { $this->appSpan->finish(); } @@ -72,11 +76,7 @@ public function terminate($request, $response): void // If the transaction is not on the scope during finish, the trace.context is wrong SentrySdk::getCurrentHub()->setSpan($this->transaction); - if ($request instanceof Request) { - $this->hydrateRequestData($request); - } - - if ($response instanceof Response) { + if ($response instanceof SymfonyResponse) { $this->hydrateResponseData($response); } @@ -90,9 +90,8 @@ public function terminate($request, $response): void * @param float|null $timestamp The unix timestamp of the booted event, default to `microtime(true)` if not `null`. * * @return void - * @internal This method should only be invoked right after the application has finished "booting": - * For Laravel this is from the application `booted` callback. - * For Lumen this is right before returning from the `bootstrap/app.php` file. + * + * @internal This method should only be invoked right after the application has finished "booting". */ public function setBootedTimestamp(?float $timestamp = null): void { @@ -102,18 +101,23 @@ public function setBootedTimestamp(?float $timestamp = null): void private function startTransaction(Request $request, HubInterface $sentry): void { $requestStartTime = $request->server('REQUEST_TIME_FLOAT', microtime(true)); - $sentryTraceHeader = $request->header('sentry-trace'); - $context = $sentryTraceHeader - ? TransactionContext::fromSentryTrace($sentryTraceHeader) - : new TransactionContext; + $context = TransactionContext::fromHeaders( + $request->header('sentry-trace', ''), + $request->header('baggage', '') + ); + + $requestPath = '/' . ltrim($request->path(), '/'); $context->setOp('http.server'); + $context->setName($requestPath); + $context->setSource(TransactionSource::url()); + $context->setStartTimestamp($requestStartTime); + $context->setData([ - 'url' => '/' . ltrim($request->path(), '/'), + 'url' => $requestPath, 'method' => strtoupper($request->method()), ]); - $context->setStartTimestamp($requestStartTime); $this->transaction = $sentry->startTransaction($context); @@ -122,7 +126,7 @@ private function startTransaction(Request $request, HubInterface $sentry): void $bootstrapSpan = $this->addAppBootstrapSpan($request); - $appContextStart = new SpanContext(); + $appContextStart = new SpanContext; $appContextStart->setOp('middleware.handle'); $appContextStart->setStartTimestamp($bootstrapSpan ? $bootstrapSpan->getEndTimestamp() : microtime(true)); @@ -143,7 +147,7 @@ private function addAppBootstrapSpan(Request $request): ?Span return null; } - $spanContextStart = new SpanContext(); + $spanContextStart = new SpanContext; $spanContextStart->setOp('app.bootstrap'); $spanContextStart->setStartTimestamp($laravelStartTime); $spanContextStart->setEndTimestamp($this->bootedTimestamp); @@ -175,56 +179,8 @@ private function addBootDetailTimeSpans(Span $bootstrap): void $bootstrap->startChild($autoload); } - private function hydrateRequestData(Request $request): void - { - $route = $request->route(); - - if ($route instanceof Route) { - $this->updateTransactionNameIfDefault( - Integration::extractNameForRoute($route) - ); - - $this->transaction->setData([ - 'name' => $route->getName(), - 'action' => $route->getActionName(), - 'method' => $request->getMethod(), - ]); - } elseif (is_array($route) && count($route) === 3) { - $this->updateTransactionNameIfDefault( - Integration::extractNameForLumenRoute($route, $request->path()) - ); - - $action = $route[1] ?? []; - - $this->transaction->setData([ - 'name' => $action['as'] ?? null, - 'action' => $action['uses'] ?? 'Closure', - 'method' => $request->getMethod(), - ]); - } - - $this->updateTransactionNameIfDefault('/' . ltrim($request->path(), '/')); - } - - private function hydrateResponseData(Response $response): void + private function hydrateResponseData(SymfonyResponse $response): void { $this->transaction->setHttpStatus($response->getStatusCode()); } - - private function updateTransactionNameIfDefault(?string $name): void - { - // Ignore empty names (and `null`) for caller convenience - if (empty($name)) { - return; - } - - // If the transaction already has a name other than the default - // ignore the new name, this will most occur if the user has set a - // transaction name themself before the application reaches this point - if ($this->transaction->getName() !== TransactionContext::DEFAULT_NAME) { - return; - } - - $this->transaction->setName($name); - } } diff --git a/src/Sentry/Laravel/Tracing/Routing/TracingCallableDispatcherTracing.php b/src/Sentry/Laravel/Tracing/Routing/TracingCallableDispatcherTracing.php new file mode 100644 index 00000000..7ccb1062 --- /dev/null +++ b/src/Sentry/Laravel/Tracing/Routing/TracingCallableDispatcherTracing.php @@ -0,0 +1,24 @@ +dispatcher = $dispatcher; + } + + public function dispatch(Route $route, $callable) + { + return $this->wrapRouteDispatch(function () use ($route, $callable) { + return $this->dispatcher->dispatch($route, $callable); + }, $route); + } +} diff --git a/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.php b/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.php new file mode 100644 index 00000000..1cedda2c --- /dev/null +++ b/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.php @@ -0,0 +1,29 @@ +dispatcher = $dispatcher; + } + + public function dispatch(Route $route, $controller, $method) + { + return $this->wrapRouteDispatch(function () use ($route, $controller, $method) { + return $this->dispatcher->dispatch($route, $controller, $method); + }, $route); + } + + public function getMiddleware($controller, $method) + { + return $this->dispatcher->getMiddleware($controller, $method); + } +} diff --git a/src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.php b/src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.php new file mode 100644 index 00000000..98212973 --- /dev/null +++ b/src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.php @@ -0,0 +1,36 @@ +getSpan(); + + // When there is no active transaction we can skip doing anything and just immediately return the callable + if ($parentSpan === null) { + return $dispatch(); + } + + $context = new SpanContext; + $context->setOp('http.route'); + $context->setDescription($route->getActionName()); + + $span = $parentSpan->startChild($context); + + SentrySdk::getCurrentHub()->setSpan($span); + + try { + return $dispatch(); + } finally { + $span->finish(); + + SentrySdk::getCurrentHub()->setSpan($parentSpan); + } + } +} diff --git a/src/Sentry/Laravel/Tracing/ServiceProvider.php b/src/Sentry/Laravel/Tracing/ServiceProvider.php index 3235b3c4..cd56d2d4 100644 --- a/src/Sentry/Laravel/Tracing/ServiceProvider.php +++ b/src/Sentry/Laravel/Tracing/ServiceProvider.php @@ -2,16 +2,20 @@ namespace Sentry\Laravel\Tracing; +use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Http\Kernel as HttpKernelInterface; use Illuminate\Contracts\View\Engine; use Illuminate\Contracts\View\View; use Illuminate\Foundation\Http\Kernel as HttpKernel; -use Illuminate\Queue\QueueManager; +use Illuminate\Routing\Contracts\CallableDispatcher; +use Illuminate\Routing\Contracts\ControllerDispatcher; use Illuminate\View\Engines\EngineResolver; use Illuminate\View\Factory as ViewFactory; use InvalidArgumentException; -use Laravel\Lumen\Application as Lumen; use Sentry\Laravel\BaseServiceProvider; +use Sentry\Laravel\Tracing\Routing\TracingCallableDispatcherTracing; +use Sentry\Laravel\Tracing\Routing\TracingControllerDispatcherTracing; use Sentry\Serializer\RepresentationSerializer; class ServiceProvider extends BaseServiceProvider @@ -29,9 +33,9 @@ public function boot(): void $this->bindViewEngine($tracingConfig); - if ($this->app instanceof Lumen) { - $this->app->middleware(Middleware::class); - } elseif ($this->app->bound(HttpKernelInterface::class)) { + $this->decorateRoutingDispatchers(); + + if ($this->app->bound(HttpKernelInterface::class)) { /** @var \Illuminate\Foundation\Http\Kernel $httpKernel */ $httpKernel = $this->app->make(HttpKernelInterface::class); @@ -55,27 +59,29 @@ public function register(): void return new BacktraceHelper($options, new RepresentationSerializer($options)); }); - if (!$this->app instanceof Lumen) { - $this->app->booted(function () { - $this->app->make(Middleware::class)->setBootedTimestamp(); - }); - } + $this->app->booted(function () { + $this->app->make(Middleware::class)->setBootedTimestamp(); + }); } private function bindEvents(array $tracingConfig): void { $handler = new EventHandler( - $this->app, - $this->app->make(BacktraceHelper::class), - $tracingConfig + $tracingConfig, + $this->app->make(BacktraceHelper::class) ); - $handler->subscribe(); + try { + /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ + $dispatcher = $this->app->make(Dispatcher::class); - if ($this->app->bound('queue')) { - $handler->subscribeQueueEvents( - $this->app->make('queue') - ); + $handler->subscribe($dispatcher); + + if ($this->app->bound('queue')) { + $handler->subscribeQueueEvents($dispatcher, $this->app->make('queue')); + } + } catch (BindingResolutionException $e) { + // If we cannot resolve the event dispatcher we also cannot listen to events } } @@ -119,4 +125,15 @@ private function wrapViewEngine(Engine $realEngine): Engine return new ViewEngineDecorator($realEngine, $viewFactory); } + + private function decorateRoutingDispatchers(): void + { + $this->app->extend(CallableDispatcher::class, static function (CallableDispatcher $dispatcher) { + return new TracingCallableDispatcherTracing($dispatcher); + }); + + $this->app->extend(ControllerDispatcher::class, static function (ControllerDispatcher $dispatcher) { + return new TracingControllerDispatcherTracing($dispatcher); + }); + } } diff --git a/src/Sentry/Laravel/Tracing/ViewEngineDecorator.php b/src/Sentry/Laravel/Tracing/ViewEngineDecorator.php index 79e262b5..a153243f 100644 --- a/src/Sentry/Laravel/Tracing/ViewEngineDecorator.php +++ b/src/Sentry/Laravel/Tracing/ViewEngineDecorator.php @@ -4,7 +4,6 @@ use Illuminate\Contracts\View\Engine; use Illuminate\View\Factory; -use Sentry\Laravel\Integration; use Sentry\SentrySdk; use Sentry\Tracing\SpanContext; @@ -29,7 +28,7 @@ public function __construct(Engine $engine, Factory $viewFactory) */ public function get($path, array $data = []): string { - $parentSpan = Integration::currentTracingSpan(); + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); if ($parentSpan === null) { return $this->engine->get($path, $data); diff --git a/test/Sentry/ClientBuilderDecoratorTest.php b/test/Sentry/ClientBuilderDecoratorTest.php new file mode 100644 index 00000000..dc23ae2f --- /dev/null +++ b/test/Sentry/ClientBuilderDecoratorTest.php @@ -0,0 +1,27 @@ +extend(ClientBuilderInterface::class, function (ClientBuilderInterface $clientBuilder) { + $clientBuilder->getOptions()->setEnvironment('from_service_container'); + + return $clientBuilder; + }); + } + + public function testClientHasEnvironmentSetFromDecorator(): void + { + $this->assertEquals( + 'from_service_container', + $this->getClientFromContainer()->getOptions()->getEnvironment() + ); + } +} diff --git a/test/Sentry/CommandInfoInBreadcrumbsTest.php b/test/Sentry/EventHandler/ConsoleEventsTest.php similarity index 58% rename from test/Sentry/CommandInfoInBreadcrumbsTest.php rename to test/Sentry/EventHandler/ConsoleEventsTest.php index 7947656f..ccad1562 100644 --- a/test/Sentry/CommandInfoInBreadcrumbsTest.php +++ b/test/Sentry/EventHandler/ConsoleEventsTest.php @@ -1,19 +1,16 @@ shouldSkip()) { - $this->markTestSkipped('Laravel version <5.5 does not contain the events tested.'); - } - $this->resetApplicationWithConfig([ 'sentry.breadcrumbs.command_info' => true, ]); @@ -28,12 +25,8 @@ public function testCommandInfoAreRecordedWhenEnabled() $this->assertEquals('--foo=bar', $lastBreadcrumb->getMetadata()['input']); } - public function testCommandInfoAreRecordedWhenDisabled() + public function testCommandBreadcrumIsNotRecordedWhenDisabled(): void { - if ($this->shouldSkip()) { - $this->markTestSkipped('Laravel version <5.5 does not contain the events tested.'); - } - $this->resetApplicationWithConfig([ 'sentry.breadcrumbs.command_info' => false, ]); @@ -45,14 +38,9 @@ public function testCommandInfoAreRecordedWhenDisabled() $this->assertEmpty($this->getCurrentBreadcrumbs()); } - private function dispatchCommandStartEvent() + private function dispatchCommandStartEvent(): void { - $dispatcher = $this->app['events']; - - $method = method_exists($dispatcher, 'dispatch') ? 'dispatch' : 'fire'; - - $this->app['events']->$method( - CommandStarting::class, + $this->dispatchLaravelEvent( new CommandStarting( 'test:command', new ArgvInput(['artisan', '--foo=bar']), @@ -60,9 +48,4 @@ private function dispatchCommandStartEvent() ) ); } - - private function shouldSkip() - { - return !class_exists(CommandStarting::class); - } } diff --git a/test/Sentry/EventHandler/DatabaseEventsTest.php b/test/Sentry/EventHandler/DatabaseEventsTest.php new file mode 100644 index 00000000..0c517298 --- /dev/null +++ b/test/Sentry/EventHandler/DatabaseEventsTest.php @@ -0,0 +1,97 @@ +resetApplicationWithConfig([ + 'sentry.breadcrumbs.sql_queries' => true, + ]); + + $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.sql_queries')); + + $this->dispatchLaravelEvent(new QueryExecuted( + $query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;', + ['1'], + 10, + $this->getMockedConnection() + )); + + $lastBreadcrumb = $this->getLastBreadcrumb(); + + $this->assertEquals($query, $lastBreadcrumb->getMessage()); + } + + public function testSqlBindingsAreRecordedWhenEnabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.breadcrumbs.sql_bindings' => true, + ]); + + $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.sql_bindings')); + + $this->dispatchLaravelEvent(new QueryExecuted( + $query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;', + $bindings = ['1'], + 10, + $this->getMockedConnection() + )); + + $lastBreadcrumb = $this->getLastBreadcrumb(); + + $this->assertEquals($query, $lastBreadcrumb->getMessage()); + $this->assertEquals($bindings, $lastBreadcrumb->getMetadata()['bindings']); + } + + public function testSqlQueriesAreRecordedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.breadcrumbs.sql_queries' => false, + ]); + + $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.sql_queries')); + + $this->dispatchLaravelEvent(new QueryExecuted( + 'SELECT * FROM breadcrumbs WHERE bindings = ?;', + ['1'], + 10, + $this->getMockedConnection() + )); + + $this->assertEmpty($this->getCurrentBreadcrumbs()); + } + + public function testSqlBindingsAreRecordedWhenDisabled(): void + { + $this->resetApplicationWithConfig([ + 'sentry.breadcrumbs.sql_bindings' => false, + ]); + + $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.sql_bindings')); + + $this->dispatchLaravelEvent(new QueryExecuted( + $query = 'SELECT * FROM breadcrumbs WHERE bindings <> ?;', + ['1'], + 10, + $this->getMockedConnection() + )); + + $lastBreadcrumb = $this->getLastBreadcrumb(); + + $this->assertEquals($query, $lastBreadcrumb->getMessage()); + $this->assertFalse(isset($lastBreadcrumb->getMetadata()['bindings'])); + } + + private function getMockedConnection() + { + return Mockery::mock(Connection::class) + ->shouldReceive('getName')->andReturn('test'); + } +} diff --git a/test/Sentry/LaravelLogsInBreadcrumbsTest.php b/test/Sentry/EventHandler/LogEventsTest.php similarity index 62% rename from test/Sentry/LaravelLogsInBreadcrumbsTest.php rename to test/Sentry/EventHandler/LogEventsTest.php index 6fa09ebd..3f0b19cf 100644 --- a/test/Sentry/LaravelLogsInBreadcrumbsTest.php +++ b/test/Sentry/EventHandler/LogEventsTest.php @@ -1,10 +1,13 @@ resetApplicationWithConfig([ 'sentry.breadcrumbs.logs' => true, @@ -12,11 +15,11 @@ public function testLaravelLogsAreRecordedWhenEnabled() $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.logs')); - $this->dispatchLaravelEvent('illuminate.log', [ + $this->dispatchLaravelEvent(new MessageLogged( $level = 'debug', $message = 'test message', - $context = ['1'], - ]); + $context = ['1'] + )); $lastBreadcrumb = $this->getLastBreadcrumb(); @@ -25,7 +28,7 @@ public function testLaravelLogsAreRecordedWhenEnabled() $this->assertEquals($context, $lastBreadcrumb->getMetadata()); } - public function testLaravelLogsAreRecordedWhenDisabled() + public function testLaravelLogsAreRecordedWhenDisabled(): void { $this->resetApplicationWithConfig([ 'sentry.breadcrumbs.logs' => false, @@ -33,11 +36,7 @@ public function testLaravelLogsAreRecordedWhenDisabled() $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.logs')); - $this->dispatchLaravelEvent('illuminate.log', [ - $level = 'debug', - $message = 'test message', - $context = ['1'], - ]); + $this->dispatchLaravelEvent(new MessageLogged('debug', 'test message')); $this->assertEmpty($this->getCurrentBreadcrumbs()); } diff --git a/test/Sentry/EventHandlerTest.php b/test/Sentry/EventHandlerTest.php index 4d1472c8..d5113527 100644 --- a/test/Sentry/EventHandlerTest.php +++ b/test/Sentry/EventHandlerTest.php @@ -9,42 +9,41 @@ class EventHandlerTest extends TestCase { - use ExpectsException; - - public function test_missing_event_handler_throws_exception() + public function testMissingEventHandlerThrowsException(): void { $handler = new EventHandler($this->app, []); - $this->safeExpectException(RuntimeException::class); + $this->expectException(RuntimeException::class); + /** @noinspection PhpUndefinedMethodInspection */ $handler->thisIsNotAHandlerAndShouldThrowAnException(); } - public function test_all_mapped_event_handlers_exist() + public function testAllMappedEventHandlersExist(): void { $this->tryAllEventHandlerMethods( - $this->getStaticPropertyValueFromClass(EventHandler::class, 'eventHandlerMap') + $this->getEventHandlerMapFromEventHandler('eventHandlerMap') ); } - public function test_all_mapped_auth_event_handlers_exist() + public function testAllMappedAuthEventHandlersExist(): void { $this->tryAllEventHandlerMethods( - $this->getStaticPropertyValueFromClass(EventHandler::class, 'authEventHandlerMap') + $this->getEventHandlerMapFromEventHandler('authEventHandlerMap') ); } - public function test_all_mapped_queue_event_handlers_exist() + public function testAllMappedQueueEventHandlersExist(): void { $this->tryAllEventHandlerMethods( - $this->getStaticPropertyValueFromClass(EventHandler::class, 'queueEventHandlerMap') + $this->getEventHandlerMapFromEventHandler('queueEventHandlerMap') ); } - public function test_all_mapped_octane_event_handlers_exist() + public function testAllMappedOctaneEventHandlersExist(): void { $this->tryAllEventHandlerMethods( - $this->getStaticPropertyValueFromClass(EventHandler::class, 'octaneEventHandlerMap') + $this->getEventHandlerMapFromEventHandler('octaneEventHandlerMap') ); } @@ -61,12 +60,12 @@ private function tryAllEventHandlerMethods(array $methods): void } } - private function getStaticPropertyValueFromClass($className, $attributeName) + private function getEventHandlerMapFromEventHandler($eventHandlerMapName) { - $class = new ReflectionClass($className); + $class = new ReflectionClass(EventHandler::class); $attributes = $class->getStaticProperties(); - return $attributes[$attributeName]; + return $attributes[$eventHandlerMapName]; } } diff --git a/test/Sentry/ExpectsException.php b/test/Sentry/ExpectsException.php deleted file mode 100644 index 67f657b7..00000000 --- a/test/Sentry/ExpectsException.php +++ /dev/null @@ -1,25 +0,0 @@ -expectException($class); - - return; - } - - if (method_exists($this, 'setExpectedException')) { - $this->setExpectedException($class); - - return; - } - - throw new RuntimeException('Could not expect an exception.'); - } -} diff --git a/test/Sentry/Integration/ExceptionContextIntegrationTest.php b/test/Sentry/Integration/ExceptionContextIntegrationTest.php index 664bab35..2adeb583 100644 --- a/test/Sentry/Integration/ExceptionContextIntegrationTest.php +++ b/test/Sentry/Integration/ExceptionContextIntegrationTest.php @@ -6,12 +6,11 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\Laravel\Integration\ExceptionContextIntegration; -use Sentry\Laravel\Tests\SentryLaravelTestCase; -use Sentry\SentrySdk; +use Sentry\Laravel\Tests\TestCase; use Sentry\State\Scope; use function Sentry\withScope; -class ExceptionContextIntegrationTest extends SentryLaravelTestCase +class ExceptionContextIntegrationTest extends TestCase { public function testExceptionContextIntegrationIsRegistered(): void { @@ -58,7 +57,7 @@ public function invokeDataProvider(): iterable ]; } - private function generateExceptionWithContext($context) + private 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 c116bfc2..590ca4ca 100644 --- a/test/Sentry/IntegrationTest.php +++ b/test/Sentry/IntegrationTest.php @@ -9,9 +9,10 @@ use Sentry\Event; use Sentry\Laravel\Integration; use Sentry\State\Scope; +use Sentry\Tracing\TransactionSource; use function Sentry\withScope; -class IntegrationTest extends SentryLaravelTestCase +class IntegrationTest extends TestCase { public function testIntegrationIsRegistered(): void { @@ -22,10 +23,6 @@ public function testIntegrationIsRegistered(): void public function testTransactionIsSetWhenRouteMatchedEventIsFired(): void { - if (!class_exists(RouteMatched::class)) { - $this->markTestSkipped('RouteMatched event class does not exist on this version of Laravel.'); - } - Integration::setTransaction(null); $event = new RouteMatched( @@ -38,17 +35,6 @@ public function testTransactionIsSetWhenRouteMatchedEventIsFired(): void $this->assertSame($routeUrl, Integration::getTransaction()); } - public function testTransactionIsSetWhenRouterMatchedEventIsFired(): void - { - Integration::setTransaction(null); - - $this->dispatchLaravelEvent('router.matched', [ - new Route('GET', $routeUrl = '/sentry-router-matched-event', []), - ]); - - $this->assertSame($routeUrl, Integration::getTransaction()); - } - public function testTransactionIsAppliedToEventWithoutTransaction(): void { Integration::setTransaction($transaction = 'some-transaction-name'); @@ -103,36 +89,11 @@ public function testTransactionIsNotAppliedToEventWhenTransactionIsAlreadySet(): }); } - public function testExtractingNameForRouteWithName(): void - { - $route = (new Route('GET', '/foo', []))->name($routeName = 'foo-bar'); - - $this->assertSame($routeName, Integration::extractNameForRoute($route)); - } - - public function testExtractingNameForRouteWithAction(): void - { - $route = (new Route('GET', '/foo', [ - 'controller' => $controller = 'SomeController@someAction', - ])); - - $this->assertSame($controller, Integration::extractNameForRoute($route)); - } - public function testExtractingNameForRouteWithoutName(): void { $route = new Route('GET', $url = '/foo', []); - $this->assertSame($url, Integration::extractNameForRoute($route)); - } - - public function testExtractingNameForRouteWithActionAndName(): void - { - $route = (new Route('GET', '/foo', [ - 'controller' => 'SomeController@someAction', - ]))->name($routeName = 'foo-bar'); - - $this->assertSame($routeName, Integration::extractNameForRoute($route)); + $this->assetRouteNameAndSource($route, $url, TransactionSource::route()); } public function testExtractingNameForRouteWithAutoGeneratedName(): void @@ -140,105 +101,21 @@ public function testExtractingNameForRouteWithAutoGeneratedName(): void // We fake a generated name here, Laravel generates them each starting with `generated::` $route = (new Route('GET', $url = '/foo', []))->name('generated::KoAePbpBofo01ey4'); - $this->assertSame($url, Integration::extractNameForRoute($route)); + $this->assetRouteNameAndSource($route, $url, TransactionSource::route()); } public function testExtractingNameForRouteWithIncompleteGroupName(): void { $route = (new Route('GET', $url = '/foo', []))->name('group-name.'); - $this->assertSame($url, Integration::extractNameForRoute($route)); - } - - public function testExtractingNameForRouteWithStrippedBaseNamespaceFromAction(): void - { - Integration::setControllersBaseNamespace('BaseNamespace'); - - $route = (new Route('GET', '/foo', [ - 'controller' => 'BaseNamespace\\SomeController@someAction', - ])); - - $this->assertSame('SomeController@someAction', Integration::extractNameForRoute($route)); - - Integration::setControllersBaseNamespace(null); - } - - public function testExtractingNameForLumenRouteWithName(): void - { - $route = [0, ['as' => $routeName = 'foo-bar'], []]; - - $this->assertSame($routeName, Integration::extractNameForLumenRoute($route, '/some-route')); + $this->assetRouteNameAndSource($route, $url, TransactionSource::route()); } - public function testExtractingNameForLumenRouteWithAction(): void + private function assetRouteNameAndSource(Route $route, string $expectedName, TransactionSource $expectedSource): void { - $route = [0, ['uses' => $controller = 'SomeController@someAction'], []]; - - $this->assertSame($controller, Integration::extractNameForLumenRoute($route, '/some-route')); - } - - public function testExtractingNameForLumenRouteWithoutName(): void - { - $url = '/some-route'; - - $this->assertSame($url, Integration::extractNameForLumenRoute([0, [], []], $url)); - } - - public function testExtractingNameForLumenRouteWithParamInUrl(): void - { - $route = [1, [], ['param1' => 'foo']]; - - $url = '/foo/bar/baz'; - - $this->assertSame('/{param1}/bar/baz', Integration::extractNameForLumenRoute($route, $url)); - } - - public function testExtractingNameForLumenRouteWithParamsInUrl(): void - { - $route = [1, [], ['param1' => 'foo', 'param2' => 'bar']]; - - $url = '/foo/bar/baz'; - - $this->assertSame('/{param1}/{param2}/baz', Integration::extractNameForLumenRoute($route, $url)); - } - - public function testExtractingNameForLumenRouteWithActionAndName(): void - { - $route = [0, [ - 'as' => $routeName = 'foo-bar', - 'uses' => 'SomeController@someAction', - ], []]; - - $this->assertSame($routeName, Integration::extractNameForLumenRoute($route, '/some-route')); - } - - public function testExtractingNameForLumenRouteWithAutoGeneratedName(): void - { - // We fake a generated name here, Laravel generates them each starting with `generated::` - $route = [0, ['as' => 'generated::KoAePbpBofo01ey4'], []]; - - $url = '/some-route'; - - $this->assertSame($url, Integration::extractNameForLumenRoute($route, $url)); - } - - public function testExtractingNameForLumenRouteWithIncompleteGroupName(): void - { - $route = [0, ['as' => 'group-name.'], []]; - - $url = '/some-route'; - - $this->assertSame($url, Integration::extractNameForLumenRoute($route, $url)); - } - - public function testExtractingNameForLumenRouteWithStrippedBaseNamespaceFromAction(): void - { - Integration::setControllersBaseNamespace('BaseNamespace'); - - $route = [0, ['uses' => 'BaseNamespace\\SomeController@someAction'], []]; - - $this->assertSame('SomeController@someAction', Integration::extractNameForLumenRoute($route, '/some-route')); + [$actualName, $actualSource] = Integration::extractNameAndSourceForRoute($route); - Integration::setControllersBaseNamespace(null); + $this->assertSame($expectedName, $actualName); + $this->assertSame($expectedSource, $actualSource); } } diff --git a/test/Sentry/IntegrationsOptionTest.php b/test/Sentry/Laravel/LaravelIntegrationsOptionTest.php similarity index 52% rename from test/Sentry/IntegrationsOptionTest.php rename to test/Sentry/Laravel/LaravelIntegrationsOptionTest.php index eb47b129..fc5ed404 100644 --- a/test/Sentry/IntegrationsOptionTest.php +++ b/test/Sentry/Laravel/LaravelIntegrationsOptionTest.php @@ -1,19 +1,18 @@ resetApplicationWithConfig([ 'sentry.integrations' => [ @@ -30,10 +29,10 @@ public function testCustomIntegrationIsResolvedFromContainerByAlias() ], ]); - $this->assertNotNull($this->getHubFromContainer()->getClient()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + $this->assertNotNull($this->getClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); } - public function testCustomIntegrationIsResolvedFromContainerByClass() + public function testCustomIntegrationIsResolvedFromContainerByClass(): void { $this->resetApplicationWithConfig([ 'sentry.integrations' => [ @@ -41,10 +40,10 @@ public function testCustomIntegrationIsResolvedFromContainerByClass() ], ]); - $this->assertNotNull($this->getHubFromContainer()->getClient()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + $this->assertNotNull($this->getClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); } - public function testCustomIntegrationByInstance() + public function testCustomIntegrationByInstance(): void { $this->resetApplicationWithConfig([ 'sentry.integrations' => [ @@ -52,15 +51,12 @@ public function testCustomIntegrationByInstance() ], ]); - $this->assertNotNull($this->getHubFromContainer()->getClient()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + $this->assertNotNull($this->getClientFromContainer()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); } - /** - * Throws \ReflectionException in <=5.8 and \Illuminate\Contracts\Container\BindingResolutionException since 6.0 - */ - public function testCustomIntegrationThrowsExceptionIfNotResolvable() + public function testCustomIntegrationThrowsExceptionIfNotResolvable(): void { - $this->safeExpectException(Exception::class); + $this->expectException(BindingResolutionException::class); $this->resetApplicationWithConfig([ 'sentry.integrations' => [ @@ -69,9 +65,9 @@ public function testCustomIntegrationThrowsExceptionIfNotResolvable() ]); } - public function testIncorrectIntegrationEntryThrowsException() + public function testIncorrectIntegrationEntryThrowsException(): void { - $this->safeExpectException(RuntimeException::class); + $this->expectException(RuntimeException::class); $this->resetApplicationWithConfig([ 'sentry.integrations' => [ @@ -81,18 +77,16 @@ static function () { ]); } - public function testDisabledIntegrationsAreNotPresent() + public function testDisabledIntegrationsAreNotPresent(): void { - $integrations = $this->getHubFromContainer()->getClient()->getOptions()->getIntegrations(); - - foreach ($integrations as $integration) { - $this->ensureIsNotDisabledIntegration($integration); - } + $client = $this->getClientFromContainer(); - $this->assertTrue(true, 'Not all disabled integrations are actually disabled.'); + $this->assertNull($client->getIntegration(ErrorListenerIntegration::class)); + $this->assertNull($client->getIntegration(ExceptionListenerIntegration::class)); + $this->assertNull($client->getIntegration(FatalErrorListenerIntegration::class)); } - public function testDisabledIntegrationsAreNotPresentWithCustomIntegrations() + public function testDisabledIntegrationsAreNotPresentWithCustomIntegrations(): void { $this->resetApplicationWithConfig([ 'sentry.integrations' => [ @@ -100,10 +94,13 @@ public function testDisabledIntegrationsAreNotPresentWithCustomIntegrations() ], ]); - $this->assertNotNull($this->getHubFromContainer()->getClient()->getIntegration(IntegrationsOptionTestIntegrationStub::class)); - $this->assertNull($this->getHubFromContainer()->getClient()->getIntegration(ErrorListenerIntegration::class)); - $this->assertNull($this->getHubFromContainer()->getClient()->getIntegration(ExceptionListenerIntegration::class)); - $this->assertNull($this->getHubFromContainer()->getClient()->getIntegration(FatalErrorListenerIntegration::class)); + $client = $this->getClientFromContainer(); + + $this->assertNotNull($client->getIntegration(IntegrationsOptionTestIntegrationStub::class)); + + $this->assertNull($client->getIntegration(ErrorListenerIntegration::class)); + $this->assertNull($client->getIntegration(ExceptionListenerIntegration::class)); + $this->assertNull($client->getIntegration(FatalErrorListenerIntegration::class)); } } diff --git a/test/Sentry/Laravel/LogChannelTest.php b/test/Sentry/LogChannelTest.php similarity index 54% rename from test/Sentry/Laravel/LogChannelTest.php rename to test/Sentry/LogChannelTest.php index 26727660..c2514438 100644 --- a/test/Sentry/Laravel/LogChannelTest.php +++ b/test/Sentry/LogChannelTest.php @@ -1,48 +1,36 @@ skipIfLogManagerNotAvailable(); - $logChannel = new LogChannel($this->app); - $logger = $logChannel([]); + + $logger = $logChannel(); $this->assertContainsOnlyInstancesOf(SentryHandler::class, $logger->getHandlers()); } - public function test_creating_handler_with_action_level_config() + public function testCreatingHandlerWithActionLevelConfig(): void { - $this->skipIfLogManagerNotAvailable(); - $logChannel = new LogChannel($this->app); + $logger = $logChannel(['action_level' => 'critical']); $this->assertContainsOnlyInstancesOf(FingersCrossedHandler::class, $logger->getHandlers()); $currentHandler = current($logger->getHandlers()); + $this->assertInstanceOf(SentryHandler::class, $currentHandler->getHandler()); $loggerWithoutActionLevel = $logChannel(['action_level' => null]); $this->assertContainsOnlyInstancesOf(SentryHandler::class, $loggerWithoutActionLevel->getHandlers()); } - - private function skipIfLogManagerNotAvailable() - { - if (class_exists(LogManager::class)) { - return; - } - - $this->markTestSkipped('Laravel version <=5.5 does not contain the LogManager required for this functionality.'); - } } diff --git a/test/Sentry/ServiceClientBuilderDecoratorTest.php b/test/Sentry/ServiceClientBuilderDecoratorTest.php deleted file mode 100644 index 93342b85..00000000 --- a/test/Sentry/ServiceClientBuilderDecoratorTest.php +++ /dev/null @@ -1,35 +0,0 @@ -set('sentry.dsn', 'http://publickey:secretkey@sentry.dev/123'); - - $app->extend(ClientBuilderInterface::class, function (ClientBuilderInterface $clientBuilder) { - $clientBuilder->getOptions()->setEnvironment('from_service_container'); - - return $clientBuilder; - }); - } - - protected function getPackageProviders($app) - { - return [ - ServiceProvider::class, - ]; - } - - public function testClientHasCustomSerializer() - { - /** @var \Sentry\Options $options */ - $options = $this->app->make('sentry')->getClient()->getOptions(); - - $this->assertEquals('from_service_container', $options->getEnvironment()); - } -} diff --git a/test/Sentry/ServiceProviderTest.php b/test/Sentry/ServiceProviderTest.php index 218d3121..afd95ba3 100644 --- a/test/Sentry/ServiceProviderTest.php +++ b/test/Sentry/ServiceProviderTest.php @@ -2,43 +2,44 @@ namespace Sentry\Laravel\Tests; +use Orchestra\Testbench\TestCase; use Sentry\Laravel\Facade; use Sentry\Laravel\ServiceProvider; use Sentry\State\HubInterface; -class ServiceProviderTest extends \Orchestra\Testbench\TestCase +class ServiceProviderTest extends TestCase { - protected function getEnvironmentSetUp($app) + protected function getEnvironmentSetUp($app): void { - $app['config']->set('sentry.dsn', 'http://publickey:secretkey@sentry.dev/123'); + $app['config']->set('sentry.dsn', 'https://publickey:secretkey@sentry.dev/123'); $app['config']->set('sentry.error_types', E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED); } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ ServiceProvider::class, ]; } - protected function getPackageAliases($app) + protected function getPackageAliases($app): array { return [ 'Sentry' => Facade::class, ]; } - public function testIsBound() + public function testIsBound(): void { $this->assertTrue(app()->bound('sentry')); - $this->assertInstanceOf(HubInterface::class, app('sentry')); $this->assertSame(app('sentry'), Facade::getFacadeRoot()); + $this->assertInstanceOf(HubInterface::class, app('sentry')); } /** * @depends testIsBound */ - public function testEnvironment() + public function testEnvironment(): void { $this->assertEquals('testing', app('sentry')->getClient()->getOptions()->getEnvironment()); } @@ -46,12 +47,12 @@ public function testEnvironment() /** * @depends testIsBound */ - public function testDsnWasSetFromConfig() + public function testDsnWasSetFromConfig(): void { /** @var \Sentry\Options $options */ $options = app('sentry')->getClient()->getOptions(); - $this->assertEquals('http://sentry.dev', $options->getDsn()->getScheme() . '://' . $options->getDsn()->getHost()); + $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()); @@ -60,7 +61,7 @@ public function testDsnWasSetFromConfig() /** * @depends testIsBound */ - public function testErrorTypesWasSetFromConfig() + public function testErrorTypesWasSetFromConfig(): void { $this->assertEquals( E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED, diff --git a/test/Sentry/ServiceProviderWithCustomAliasTest.php b/test/Sentry/ServiceProviderWithCustomAliasTest.php index 2f44f3d7..c47b928e 100644 --- a/test/Sentry/ServiceProviderWithCustomAliasTest.php +++ b/test/Sentry/ServiceProviderWithCustomAliasTest.php @@ -2,33 +2,34 @@ namespace Sentry\Laravel\Tests; +use Orchestra\Testbench\TestCase; use Sentry\Laravel\Facade; use Sentry\Laravel\ServiceProvider; use Sentry\State\HubInterface; -class ServiceProviderWithCustomAliasTest extends \Orchestra\Testbench\TestCase +class ServiceProviderWithCustomAliasTest extends TestCase { - protected function getEnvironmentSetUp($app) + protected function getEnvironmentSetUp($app): void { $app['config']->set('custom-sentry.dsn', 'http://publickey:secretkey@sentry.dev/123'); $app['config']->set('custom-sentry.error_types', E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED); } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ CustomSentryServiceProvider::class, ]; } - protected function getPackageAliases($app) + protected function getPackageAliases($app): array { return [ 'CustomSentry' => CustomSentryFacade::class, ]; } - public function testIsBound() + public function testIsBound(): void { $this->assertTrue(app()->bound('custom-sentry')); $this->assertInstanceOf(HubInterface::class, app('custom-sentry')); @@ -38,7 +39,7 @@ public function testIsBound() /** * @depends testIsBound */ - public function testEnvironment() + public function testEnvironment(): void { $this->assertEquals('testing', app('custom-sentry')->getClient()->getOptions()->getEnvironment()); } @@ -46,7 +47,7 @@ public function testEnvironment() /** * @depends testIsBound */ - public function testDsnWasSetFromConfig() + public function testDsnWasSetFromConfig(): void { /** @var \Sentry\Options $options */ $options = app('custom-sentry')->getClient()->getOptions(); @@ -60,7 +61,7 @@ public function testDsnWasSetFromConfig() /** * @depends testIsBound */ - public function testErrorTypesWasSetFromConfig() + public function testErrorTypesWasSetFromConfig(): void { $this->assertEquals( E_ALL ^ E_DEPRECATED ^ E_USER_DEPRECATED, @@ -76,7 +77,7 @@ class CustomSentryServiceProvider extends ServiceProvider class CustomSentryFacade extends Facade { - protected static function getFacadeAccessor() + protected static function getFacadeAccessor(): string { return 'custom-sentry'; } diff --git a/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php b/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php index 6866b425..0c1e0a00 100644 --- a/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php +++ b/test/Sentry/ServiceProviderWithEnvironmentFromConfigTest.php @@ -2,36 +2,36 @@ namespace Sentry; -use Sentry\Laravel\Tests\SentryLaravelTestCase; +use Sentry\Laravel\Tests\TestCase; -class ServiceProviderWithEnvironmentFromConfigTest extends SentryLaravelTestCase +class ServiceProviderWithEnvironmentFromConfigTest extends TestCase { - public function testSentryEnvironmentDefaultsToLaravelEnvironment() + public function testSentryEnvironmentDefaultsToLaravelEnvironment(): void { $this->assertEquals('testing', app()->environment()); } - public function testEmptySentryEnvironmentDefaultsToLaravelEnvironment() + public function testEmptySentryEnvironmentDefaultsToLaravelEnvironment(): void { $this->resetApplicationWithConfig([ 'sentry.environment' => '', ]); - $this->assertEquals('testing', $this->getHubFromContainer()->getClient()->getOptions()->getEnvironment()); + $this->assertEquals('testing', $this->getClientFromContainer()->getOptions()->getEnvironment()); $this->resetApplicationWithConfig([ 'sentry.environment' => null, ]); - $this->assertEquals('testing', $this->getHubFromContainer()->getClient()->getOptions()->getEnvironment()); + $this->assertEquals('testing', $this->getClientFromContainer()->getOptions()->getEnvironment()); } - public function testSentryEnvironmentDefaultGetsOverriddenByConfig() + public function testSentryEnvironmentDefaultGetsOverriddenByConfig(): void { $this->resetApplicationWithConfig([ - 'sentry.environment' => 'not_testing', + 'sentry.environment' => 'override_env', ]); - $this->assertEquals('not_testing', $this->getHubFromContainer()->getClient()->getOptions()->getEnvironment()); + $this->assertEquals('override_env', $this->getClientFromContainer()->getOptions()->getEnvironment()); } } diff --git a/test/Sentry/ServiceProviderWithoutDsnTest.php b/test/Sentry/ServiceProviderWithoutDsnTest.php index 247a3d6f..60c26ae9 100644 --- a/test/Sentry/ServiceProviderWithoutDsnTest.php +++ b/test/Sentry/ServiceProviderWithoutDsnTest.php @@ -7,19 +7,19 @@ class ServiceProviderWithoutDsnTest extends \Orchestra\Testbench\TestCase { - protected function getEnvironmentSetUp($app) + protected function getEnvironmentSetUp($app): void { $app['config']->set('sentry.dsn', null); } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ ServiceProvider::class, ]; } - public function testIsBound() + public function testIsBound(): void { $this->assertTrue(app()->bound('sentry')); } @@ -27,7 +27,7 @@ public function testIsBound() /** * @depends testIsBound */ - public function testDsnIsNotSet() + public function testDsnIsNotSet(): void { $this->assertNull(app('sentry')->getClient()->getOptions()->getDsn()); } @@ -35,8 +35,8 @@ public function testDsnIsNotSet() /** * @depends testIsBound */ - public function testDidNotRegisterEvents() + public function testDidNotRegisterEvents(): void { - $this->assertEquals(false, app('events')->hasListeners('router.matched') && app('events')->hasListeners(RouteMatched::class)); + $this->assertEquals(false, app('events')->hasListeners(RouteMatched::class)); } } diff --git a/test/Sentry/SqlBindingsInBreadcrumbsTest.php b/test/Sentry/SqlBindingsInBreadcrumbsTest.php deleted file mode 100644 index 65affa33..00000000 --- a/test/Sentry/SqlBindingsInBreadcrumbsTest.php +++ /dev/null @@ -1,48 +0,0 @@ -resetApplicationWithConfig([ - 'sentry.breadcrumbs.sql_bindings' => true, - ]); - - $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.sql_bindings')); - - $this->dispatchLaravelEvent('illuminate.query', [ - $query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;', - $bindings = ['1'], - 10, - 'test', - ]); - - $lastBreadcrumb = $this->getLastBreadcrumb(); - - $this->assertEquals($query, $lastBreadcrumb->getMessage()); - $this->assertEquals($bindings, $lastBreadcrumb->getMetadata()['bindings']); - } - - public function testSqlBindingsAreRecordedWhenDisabled() - { - $this->resetApplicationWithConfig([ - 'sentry.breadcrumbs.sql_bindings' => false, - ]); - - $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.sql_bindings')); - - $this->dispatchLaravelEvent('illuminate.query', [ - $query = 'SELECT * FROM breadcrumbs WHERE bindings <> ?;', - ['1'], - 10, - 'test', - ]); - - $lastBreadcrumb = $this->getLastBreadcrumb(); - - $this->assertEquals($query, $lastBreadcrumb->getMessage()); - $this->assertFalse(isset($lastBreadcrumb->getMetadata()['bindings'])); - } -} diff --git a/test/Sentry/SqlBindingsInBreadcrumbsWithOldConfigKeyDisabledTest.php b/test/Sentry/SqlBindingsInBreadcrumbsWithOldConfigKeyDisabledTest.php deleted file mode 100644 index c60295bf..00000000 --- a/test/Sentry/SqlBindingsInBreadcrumbsWithOldConfigKeyDisabledTest.php +++ /dev/null @@ -1,36 +0,0 @@ -all(); - - $config['sentry']['breadcrumbs.sql_bindings'] = false; - - $app['config'] = new Repository($config); - } - - public function testSqlBindingsAreRecordedWhenDisabledByOldConfigKey() - { - $this->assertFalse($this->app['config']->get('sentry')['breadcrumbs.sql_bindings']); - - $this->dispatchLaravelEvent('illuminate.query', [ - $query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;', - ['1'], - 10, - 'test', - ]); - - $lastBreadcrumb = $this->getLastBreadcrumb(); - - $this->assertEquals($query, $lastBreadcrumb->getMessage()); - $this->assertFalse(isset($lastBreadcrumb->getMetadata()['bindings'])); - } -} diff --git a/test/Sentry/SqlBindingsInBreadcrumbsWithOldConfigKeyEnabledTest.php b/test/Sentry/SqlBindingsInBreadcrumbsWithOldConfigKeyEnabledTest.php deleted file mode 100644 index 3850ac32..00000000 --- a/test/Sentry/SqlBindingsInBreadcrumbsWithOldConfigKeyEnabledTest.php +++ /dev/null @@ -1,36 +0,0 @@ -all(); - - $config['sentry']['breadcrumbs.sql_bindings'] = true; - - $app['config'] = new Repository($config); - } - - public function testSqlBindingsAreRecordedWhenEnabledByOldConfigKey() - { - $this->assertTrue($this->app['config']->get('sentry')['breadcrumbs.sql_bindings']); - - $this->dispatchLaravelEvent('illuminate.query', [ - $query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;', - $bindings = ['1'], - 10, - 'test', - ]); - - $lastBreadcrumb = $this->getLastBreadcrumb(); - - $this->assertEquals($query, $lastBreadcrumb->getMessage()); - $this->assertEquals($bindings, $lastBreadcrumb->getMetadata()['bindings']); - } -} diff --git a/test/Sentry/SqlQueriesInBreadcrumbsTest.php b/test/Sentry/SqlQueriesInBreadcrumbsTest.php deleted file mode 100644 index 2d718e95..00000000 --- a/test/Sentry/SqlQueriesInBreadcrumbsTest.php +++ /dev/null @@ -1,44 +0,0 @@ -resetApplicationWithConfig([ - 'sentry.breadcrumbs.sql_queries' => true, - ]); - - $this->assertTrue($this->app['config']->get('sentry.breadcrumbs.sql_queries')); - - $this->dispatchLaravelEvent('illuminate.query', [ - $query = 'SELECT * FROM breadcrumbs WHERE bindings = ?;', - ['1'], - 10, - 'test', - ]); - - $lastBreadcrumb = $this->getLastBreadcrumb(); - - $this->assertEquals($query, $lastBreadcrumb->getMessage()); - } - - public function testSqlQueriesAreRecordedWhenDisabled() - { - $this->resetApplicationWithConfig([ - 'sentry.breadcrumbs.sql_queries' => false, - ]); - - $this->assertFalse($this->app['config']->get('sentry.breadcrumbs.sql_queries')); - - $this->dispatchLaravelEvent('illuminate.query', [ - 'SELECT * FROM breadcrumbs WHERE bindings <> ?;', - ['1'], - 10, - 'test', - ]); - - $this->assertEmpty($this->getCurrentBreadcrumbs()); - } -} diff --git a/test/Sentry/SentryLaravelTestCase.php b/test/Sentry/TestCase.php similarity index 81% rename from test/Sentry/SentryLaravelTestCase.php rename to test/Sentry/TestCase.php index eab9b670..c47f99ea 100644 --- a/test/Sentry/SentryLaravelTestCase.php +++ b/test/Sentry/TestCase.php @@ -4,6 +4,7 @@ use ReflectionMethod; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\State\Scope; use ReflectionProperty; use Sentry\Laravel\Tracing; @@ -11,14 +12,14 @@ use Sentry\Laravel\ServiceProvider; use Orchestra\Testbench\TestCase as LaravelTestCase; -abstract class SentryLaravelTestCase extends LaravelTestCase +abstract class TestCase extends LaravelTestCase { protected $setupConfig = [ // Set config here before refreshing the app to set it in the container before Sentry is loaded // or use the `$this->resetApplicationWithConfig([ /* config */ ]);` helper method ]; - protected function getEnvironmentSetUp($app) + protected function getEnvironmentSetUp($app): void { $app['config']->set('sentry.dsn', 'http://publickey:secretkey@sentry.dev/123'); @@ -27,7 +28,7 @@ protected function getEnvironmentSetUp($app) } } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ ServiceProvider::class, @@ -35,21 +36,16 @@ protected function getPackageProviders($app) ]; } - protected function resetApplicationWithConfig(array $config) + protected function resetApplicationWithConfig(array $config): void { $this->setupConfig = $config; $this->refreshApplication(); } - protected function dispatchLaravelEvent($event, array $payload = []) + protected function dispatchLaravelEvent($event, array $payload = []): void { - $dispatcher = $this->app['events']; - - // Laravel 5.4+ uses the dispatch method to dispatch/fire events - return method_exists($dispatcher, 'dispatch') - ? $dispatcher->dispatch($event, $payload) - : $dispatcher->fire($event, $payload); + $this->app['events']->dispatch($event, $payload); } protected function getHubFromContainer(): HubInterface @@ -57,6 +53,11 @@ protected function getHubFromContainer(): HubInterface return $this->app->make('sentry'); } + protected function getClientFromContainer(): ClientInterface + { + return $this->getHubFromContainer()->getClient(); + } + protected function getCurrentScope(): Scope { $hub = $this->getHubFromContainer(); diff --git a/test/Sentry/Tracing/EventHandlerTest.php b/test/Sentry/Tracing/EventHandlerTest.php index f71faac8..8fad3b4f 100644 --- a/test/Sentry/Tracing/EventHandlerTest.php +++ b/test/Sentry/Tracing/EventHandlerTest.php @@ -3,35 +3,40 @@ namespace Sentry\Laravel\Tests\Tracing; use ReflectionClass; -use Sentry\Laravel\Tests\SentryLaravelTestCase; +use Sentry\Laravel\Tests\TestCase; use Sentry\Laravel\Tracing\BacktraceHelper; use RuntimeException; -use Sentry\Laravel\Tests\ExpectsException; use Sentry\Laravel\Tracing\EventHandler; -class EventHandlerTest extends SentryLaravelTestCase +class EventHandlerTest extends TestCase { - use ExpectsException; - - public function test_missing_event_handler_throws_exception() + public function testMissingEventHandlerThrowsException(): void { - $this->safeExpectException(RuntimeException::class); + $this->expectException(RuntimeException::class); - $handler = new EventHandler($this->app, $this->app->make(BacktraceHelper::class), []); + $handler = new EventHandler([], $this->app->make(BacktraceHelper::class)); + /** @noinspection PhpUndefinedMethodInspection */ $handler->thisIsNotAHandlerAndShouldThrowAnException(); } - public function test_all_mapped_event_handlers_exist() + public function testAllMappedEventHandlersExist(): void + { + $this->tryAllEventHandlerMethods( + $this->getEventHandlerMapFromEventHandler('eventHandlerMap') + ); + } + + public function testAllMappedQueueEventHandlersExist(): void { $this->tryAllEventHandlerMethods( - $this->getStaticPropertyValueFromClass(EventHandler::class, 'eventHandlerMap') + $this->getEventHandlerMapFromEventHandler('queueEventHandlerMap') ); } private function tryAllEventHandlerMethods(array $methods): void { - $handler = new EventHandler($this->app, $this->app->make(BacktraceHelper::class), []); + $handler = new EventHandler([], $this->app->make(BacktraceHelper::class)); $methods = array_map(static function ($method) { return "{$method}Handler"; @@ -42,12 +47,12 @@ private function tryAllEventHandlerMethods(array $methods): void } } - private function getStaticPropertyValueFromClass($className, $attributeName) + private function getEventHandlerMapFromEventHandler($eventHandlerMapName) { - $class = new ReflectionClass($className); + $class = new ReflectionClass(EventHandler::class); $attributes = $class->getStaticProperties(); - return $attributes[$attributeName]; + return $attributes[$eventHandlerMapName]; } }