diff --git a/README.md b/README.md index d427a028..7bf02286 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This is the official Laravel SDK for [Sentry](https://sentry.io). The installation steps below work on versions 8.x, 9.x and 10.x of the Laravel framework. -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/) @@ -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,12 +92,11 @@ 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` - 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 8a852d34..937b3d03 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 7ea2c84b..e241ccee 100644 --- a/src/Sentry/Laravel/Integration.php +++ b/src/Sentry/Laravel/Integration.php @@ -117,7 +117,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 { @@ -127,6 +127,36 @@ 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) { + $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 + ); + + 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..e25b1351 100644 --- a/src/Sentry/Laravel/ServiceProvider.php +++ b/src/Sentry/Laravel/ServiceProvider.php @@ -7,10 +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; @@ -21,6 +25,7 @@ use Sentry\SentrySdk; use Sentry\State\Hub; use Sentry\State\HubInterface; +use Sentry\Tracing\TransactionMetadata; class ServiceProvider extends BaseServiceProvider { @@ -61,8 +66,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 +95,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(); @@ -181,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 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..c3cd1bd2 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,48 @@ 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 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 @@ -143,11 +177,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); + } }