From 89507634a2b0271b26e2c7c5765fa1f6efd8f659 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 1 May 2023 08:04:30 +0200 Subject: [PATCH 1/4] Reintroduce Lumen support --- README.md | 1 - composer.json | 3 -- src/Sentry/Laravel/Integration.php | 28 ++++++++++++- src/Sentry/Laravel/ServiceProvider.php | 11 ++++- .../Laravel/Tracing/ServiceProvider.php | 14 ++++--- test/Sentry/IntegrationTest.php | 41 +++++++++++++++++-- 6 files changed, 82 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cd5df754..eb891bd3 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,6 @@ 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 b1eef3d8..943de2b0 100644 --- a/composer.json +++ b/composer.json @@ -28,9 +28,6 @@ "symfony/psr-http-message-bridge": "^1.0 | ^2.0", "nyholm/psr7": "^1.0" }, - "conflict": { - "laravel/lumen-framework": "*" - }, "autoload": { "psr-0": { "Sentry\\Laravel\\": "src/" diff --git a/src/Sentry/Laravel/Integration.php b/src/Sentry/Laravel/Integration.php index af888a80..faa58dff 100644 --- a/src/Sentry/Laravel/Integration.php +++ b/src/Sentry/Laravel/Integration.php @@ -113,7 +113,7 @@ public static function flushEvents(): void * * @return array{0: string, 1: \Sentry\Tracing\TransactionSource} * - * @internal This helper is used in various places to extra meaninful info from a Laravel Route object. + * @internal This helper is used in various places to extract meaningful info from a Laravel Route object. */ public static function extractNameAndSourceForRoute(Route $route): array { @@ -123,6 +123,32 @@ public static function extractNameAndSourceForRoute(Route $route): array ]; } + /** + * Extract the readable name for a Lumen route and the transaction source for where that route name came from. + * + * @param array $routeData The array of route data + * @param string $path The path of the request + * + * @return array{0: string, 1: \Sentry\Tracing\TransactionSource} + * + * @internal This helper is used in various places to extract meaningful info from Lumen route data. + */ + public static function extractNameAndSourceForLumenRoute(array $routeData, string $path): array + { + $routeUri = array_reduce( + array_keys($routeData[2]), + static function ($carry, $key) use ($routeData) { + return str_replace($routeData[2][$key], "{{$key}}", $carry); + }, + $path + ); + + return [ + '/' . ltrim($routeUri, '/'), + TransactionSource::route(), + ]; + } + /** * Retrieve the meta tags with tracing information to link this request to front-end requests. * This propagates the Dynamic Sampling Context. diff --git a/src/Sentry/Laravel/ServiceProvider.php b/src/Sentry/Laravel/ServiceProvider.php index dac85af9..799b201f 100644 --- a/src/Sentry/Laravel/ServiceProvider.php +++ b/src/Sentry/Laravel/ServiceProvider.php @@ -8,6 +8,7 @@ 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; @@ -61,8 +62,10 @@ public function boot(): void $this->setupFeatures(); - if ($this->app->bound(HttpKernelInterface::class)) { - /** @var \Illuminate\Foundation\Http\Kernel $httpKernel */ + if ($this->app instanceof Lumen) { + $this->app->middleware(SetRequestMiddleware::class); + $this->app->middleware(SetRequestIpMiddleware::class); + } elseif ($this->app->bound(HttpKernelInterface::class)) { $httpKernel = $this->app->make(HttpKernelInterface::class); if ($httpKernel instanceof HttpKernel) { @@ -88,6 +91,10 @@ 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(); diff --git a/src/Sentry/Laravel/Tracing/ServiceProvider.php b/src/Sentry/Laravel/Tracing/ServiceProvider.php index cd518864..e7a1c44a 100644 --- a/src/Sentry/Laravel/Tracing/ServiceProvider.php +++ b/src/Sentry/Laravel/Tracing/ServiceProvider.php @@ -13,6 +13,7 @@ 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; @@ -31,9 +32,11 @@ public function boot(): void return; } - $this->app->booted(function () { - $this->app->make(Middleware::class)->setBootedTimestamp(); - }); + if (!$this->app instanceof Lumen) { + $this->app->booted(function () { + $this->app->make(Middleware::class)->setBootedTimestamp(); + }); + } $tracingConfig = $this->getUserConfig()['tracing'] ?? []; @@ -43,8 +46,9 @@ public function boot(): void $this->decorateRoutingDispatchers(); - if ($this->app->bound(HttpKernelInterface::class)) { - /** @var \Illuminate\Foundation\Http\Kernel $httpKernel */ + if ($this->app instanceof Lumen) { + $this->app->middleware(Middleware::class); + } elseif ($this->app->bound(HttpKernelInterface::class)) { $httpKernel = $this->app->make(HttpKernelInterface::class); if ($httpKernel instanceof HttpKernel) { diff --git a/test/Sentry/IntegrationTest.php b/test/Sentry/IntegrationTest.php index 3cf7ee74..c879bc8b 100644 --- a/test/Sentry/IntegrationTest.php +++ b/test/Sentry/IntegrationTest.php @@ -95,7 +95,7 @@ public function testExtractingNameForRouteWithoutName(): void { $route = new Route('GET', $url = '/foo', []); - $this->assetRouteNameAndSource($route, $url, TransactionSource::route()); + $this->assertRouteNameAndSource($route, $url, TransactionSource::route()); } public function testExtractingNameForRouteWithAutoGeneratedName(): void @@ -103,14 +103,39 @@ 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->assetRouteNameAndSource($route, $url, TransactionSource::route()); + $this->assertRouteNameAndSource($route, $url, TransactionSource::route()); } public function testExtractingNameForRouteWithIncompleteGroupName(): void { $route = (new Route('GET', $url = '/foo', []))->name('group-name.'); - $this->assetRouteNameAndSource($route, $url, TransactionSource::route()); + $this->assertRouteNameAndSource($route, $url, TransactionSource::route()); + } + + public function testExtractingNameForLumenRouteWithoutName(): void + { + $url = '/some-route'; + + $this->assertLumenRouteNameAndSource([0, [], []], $url, $url, TransactionSource::route()); + } + + public function testExtractingNameForLumenRouteWithParamInUrl(): void + { + $route = [1, [], ['param1' => 'foo']]; + + $url = '/foo/bar/baz'; + + $this->assertLumenRouteNameAndSource($route, $url, '/{param1}/bar/baz', TransactionSource::route()); + } + + public function testExtractingNameForLumenRouteWithParamsInUrl(): void + { + $route = [1, [], ['param1' => 'foo', 'param2' => 'bar']]; + + $url = '/foo/bar/baz'; + + $this->assertLumenRouteNameAndSource($route, $url, '/{param1}/{param2}/baz', TransactionSource::route()); } public function testExceptionReportedUsingReportHelperIsNotMarkedAsUnhandled(): void @@ -143,11 +168,19 @@ public function testExceptionIsNotMarkedAsUnhandled(): void $this->assertFalse($hint->mechanism->isHandled()); } - private function assetRouteNameAndSource(Route $route, string $expectedName, TransactionSource $expectedSource): void + private function assertRouteNameAndSource(Route $route, string $expectedName, TransactionSource $expectedSource): void { [$actualName, $actualSource] = Integration::extractNameAndSourceForRoute($route); $this->assertSame($expectedName, $actualName); $this->assertSame($expectedSource, $actualSource); } + + private function assertLumenRouteNameAndSource(array $routeData, string $path, string $expectedName, TransactionSource $expectedSource): void + { + [$actualName, $actualSource] = Integration::extractNameAndSourceForLumenRoute($routeData, $path); + + $this->assertSame($expectedName, $actualName); + $this->assertSame($expectedSource, $actualSource); + } } From 843edbf436793ffdf5bbf87a14daf8fed72abf6c Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 1 May 2023 08:55:49 +0200 Subject: [PATCH 2/4] Set correct transaction name for Lumen --- src/Sentry/Laravel/ServiceProvider.php | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/Sentry/Laravel/ServiceProvider.php b/src/Sentry/Laravel/ServiceProvider.php index 799b201f..e25b1351 100644 --- a/src/Sentry/Laravel/ServiceProvider.php +++ b/src/Sentry/Laravel/ServiceProvider.php @@ -7,11 +7,14 @@ use Illuminate\Contracts\Http\Kernel as HttpKernelInterface; use Illuminate\Foundation\Application as Laravel; use Illuminate\Foundation\Http\Kernel as HttpKernel; +use Illuminate\Http\Request; use Illuminate\Log\LogManager; use Laravel\Lumen\Application as Lumen; use RuntimeException; use Sentry\ClientBuilder; use Sentry\ClientBuilderInterface; +use Sentry\Event; +use Sentry\EventHint; use Sentry\Integration as SdkIntegration; use Sentry\Laravel\Console\PublishCommand; use Sentry\Laravel\Console\TestCommand; @@ -22,6 +25,7 @@ use Sentry\SentrySdk; use Sentry\State\Hub; use Sentry\State\HubInterface; +use Sentry\Tracing\TransactionMetadata; class ServiceProvider extends BaseServiceProvider { @@ -188,6 +192,39 @@ protected function configureAndRegisterClient(): void $options['environment'] = $this->app->environment(); } + if ($this->app instanceof Lumen) { + $wrapBeforeSend = function (?callable $userBeforeSend) { + return function (Event $event, ?EventHint $eventHint) use ($userBeforeSend) { + $request = $this->app->make(Request::class); + + if ($request !== null) { + $route = $request->route(); + + if ($route !== null) { + [$routeName, $transactionSource] = Integration::extractNameAndSourceForLumenRoute($request->route(), $request->path()); + + $event->setTransaction($routeName); + + $transactionMetadata = $event->getSdkMetadata('transaction_metadata'); + + if ($transactionMetadata instanceof TransactionMetadata) { + $transactionMetadata->setSource($transactionSource); + } + } + } + + if ($userBeforeSend !== null) { + return $userBeforeSend($event, $eventHint); + } + + return $event; + }; + }; + + $options['before_send'] = $wrapBeforeSend($options['before_send'] ?? null); + $options['before_send_transaction'] = $wrapBeforeSend($options['before_send_transaction'] ?? null); + } + $clientBuilder = ClientBuilder::create($options); // Set the Laravel SDK identifier and version From 4460541ce3671f44548c58874958dadb98943046 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 1 May 2023 09:02:00 +0200 Subject: [PATCH 3/4] Improve Lumen route extraction when paramaters have same values --- src/Sentry/Laravel/Integration.php | 6 +++++- test/Sentry/IntegrationTest.php | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Laravel/Integration.php b/src/Sentry/Laravel/Integration.php index faa58dff..432fa7ba 100644 --- a/src/Sentry/Laravel/Integration.php +++ b/src/Sentry/Laravel/Integration.php @@ -138,7 +138,11 @@ public static function extractNameAndSourceForLumenRoute(array $routeData, strin $routeUri = array_reduce( array_keys($routeData[2]), static function ($carry, $key) use ($routeData) { - return str_replace($routeData[2][$key], "{{$key}}", $carry); + $search = '/' . preg_quote($routeData[2][$key], '/') . '/'; + + // Replace the first occurrence of the route parameter value with the key name + // This is by no means a perfect solution, but it's the best we can do with the data we have + return preg_replace($search, "{{$key}}", $carry, 1); }, $path ); diff --git a/test/Sentry/IntegrationTest.php b/test/Sentry/IntegrationTest.php index c879bc8b..c3cd1bd2 100644 --- a/test/Sentry/IntegrationTest.php +++ b/test/Sentry/IntegrationTest.php @@ -138,6 +138,15 @@ public function testExtractingNameForLumenRouteWithParamsInUrl(): void $this->assertLumenRouteNameAndSource($route, $url, '/{param1}/{param2}/baz', TransactionSource::route()); } + public function testExtractingNameForLumenRouteWithParamsWithSameValueInUrl(): void + { + $route = [1, [], ['param1' => 'foo', 'param2' => 'foo']]; + + $url = '/foo/foo/bar'; + + $this->assertLumenRouteNameAndSource($route, $url, '/{param1}/{param2}/bar', TransactionSource::route()); + } + public function testExceptionReportedUsingReportHelperIsNotMarkedAsUnhandled(): void { $testException = new RuntimeException('This was handled'); From 4a7241f24198219210cba579b7d06a9774a268ea Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Mon, 8 May 2023 11:48:40 +0200 Subject: [PATCH 4/4] Update README --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index eb891bd3..e8652039 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ This is the official Laravel SDK for [Sentry](https://sentry.io/) ## Getting Started -The installation step below work on the latest versions of the Laravel framework (8.x, 9.x and 10.x). +The installation steps below work on the latest versions of the Laravel framework (8.x, 9.x and 10.x). -For other Laravel or Lumen versions see: +For older Laravel versions and Lumen see: - [Laravel 8.x & 9.x & 10.x](https://docs.sentry.io/platforms/php/guides/laravel/) - [Laravel 6.x & 7.x](https://docs.sentry.io/platforms/php/guides/laravel/other-versions/laravel6-7/) @@ -37,9 +37,9 @@ Install the `sentry/sentry-laravel` package: composer require sentry/sentry-laravel ``` -Enable capturing unhandled exception to report to Sentry by making the following change to your `App/Exceptions/Handler.php`: +Enable capturing unhandled exception to report to Sentry by making the following change to your `app/Exceptions/Handler.php`: -```php {filename:App/Exceptions/Handler.php} +```php {filename:app/Exceptions/Handler.php} use Sentry\Laravel\Integration; public function register(): void @@ -50,7 +50,7 @@ public function register(): void } ``` -> Alternatively, you can configure Sentry in your [Laravel Log Channel](https://docs.sentry.io/platforms/php/guides/laravel/usage/#log-channels), allowing you to log `info` and `debug` as well. +> Alternatively, you can configure Sentry as a [Laravel Log Channel](https://docs.sentry.io/platforms/php/guides/laravel/usage/#log-channels), allowing you to capture `info` and `debug` logs as well. ### Configure @@ -78,11 +78,11 @@ try { } ``` -- To learn more about how to use the SDK [refer to our docs](https://docs.sentry.io/platforms/php/guides/laravel/) +To learn more about how to use the SDK [refer to our docs](https://docs.sentry.io/platforms/php/guides/laravel/). ## Laravel Version Compatibility -The Laravel versions listed below are all currently supported: +The Laravel and Lumen versions listed below are all currently supported: - Laravel `>= 10.x.x` on PHP `>= 8.1` is supported starting from `3.2.0` - Laravel `>= 9.x.x` on PHP `>= 8.0` is supported starting from `2.11.0` @@ -92,7 +92,7 @@ The Laravel versions listed below are all currently supported: 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: +The Laravel versions listed below were supported in previous versions of the Sentry SDK for Laravel: - Laravel `<= 4.2.x` is supported until `0.8.x` - Laravel `<= 5.7.x` on PHP `<= 7.0` is supported until `0.11.x`