fjffk简å�•的咖啡店科开å�‘2方法2 环æ»�皆山也。其西å�—诸峰,林壑尤美,望之蔚然而深秀者,ç�…ç�Šä¹Ÿã€‚山行六七里,æ¸�闻水声潺潺,而泻出于两峰之间者,酿泉也 ??????????????????????????????? ??????????????/????????????????? ............/................... .........................../... ??????????????????????????????? >>>>>>>>>>>>>/>>>>>>>>>>>>>>>>> ÿØÿà JFIF    ÿÛ „  ( %!1!%)+//.383,7(-.+  -%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ  Â¥2" ÿÄ     ÿÄ J    ! 1AQ"aqÂ�2‘#BR‚¡Ã�Ñ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ   ÿÄ *  !1AQa‘"2q3Â�±ð#b¡ÿÚ   ? ¼QxJQaÃ�uò¸Zö Úü8,Ã�Úú "SSn<rçù–´âE—^ªBÖ9À\†¸Â�ÔÂ�Ã�T­ÃÛ5 ëd´³Ã�#Ã�;Þ38ϔÂ� ¶H£M:wÃŽ3…³…âpÔF&‚FK¸9„â4àGEõªfÂ�ÿ ‘ñ(ßw­pŽF|È¥ù®häðÃ�Ѷ¹‘[Ã’inÙW¶ùñYËœQ{›K"išÒ[Ú8žë\F¹@-?v"ÔUâ€�,ìöžkÿ {I‡£šÃ�?e rÂ�íV ?> ......................................... ............................................................................. <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>/>>>>>>>>>>>>>>>>>> <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dsdfPK\sW]§J£zDDLICENSEnu„[µü¤MIT License Copyright (c) 2016 Functional Software, Inc. dba Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK\sW]7öØ README.mdnu„[µü¤
Sentry for Laravel
_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_ # Official Sentry SDK for Laravel [![CI](https://github.com/getsentry/sentry-laravel/actions/workflows/ci.yaml/badge.svg)](https://github.com/getsentry/sentry-laravel/actions/workflows/ci.yaml) [![Latest Stable Version](https://poser.pugx.org/sentry/sentry-laravel/v/stable)](https://packagist.org/packages/sentry/sentry-laravel) [![License](https://poser.pugx.org/sentry/sentry-laravel/license)](https://packagist.org/packages/sentry/sentry-laravel) [![Total Downloads](https://poser.pugx.org/sentry/sentry-laravel/downloads)](https://packagist.org/packages/sentry/sentry-laravel) [![Monthly Downloads](https://poser.pugx.org/sentry/sentry-laravel/d/monthly)](https://packagist.org/packages/sentry/sentry-laravel) [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/cWnMQeA) This is the official Laravel SDK for [Sentry](https://sentry.io). ## Getting Started The installation steps below work on versions 12.x and 11.x of the Laravel framework. For older Laravel versions and Lumen see: - [Laravel 12.x & 11.x](https://docs.sentry.io/platforms/php/guides/laravel/) - [Laravel 10.x & 9.x & 8.x](https://docs.sentry.io/platforms/php/guides/laravel/other-versions/laravel8-10/) - [Laravel 7.x & 6.x](https://docs.sentry.io/platforms/php/guides/laravel/other-versions/laravel6-7/) - [Laravel 5.x](https://docs.sentry.io/platforms/php/guides/laravel/other-versions/laravel5/) - [Laravel 4.x](https://docs.sentry.io/platforms/php/guides/laravel/other-versions/laravel4/) - [Lumen](https://docs.sentry.io/platforms/php/guides/laravel/other-versions/lumen/) ### Install Install the `sentry/sentry-laravel` package: ```bash composer require sentry/sentry-laravel ``` Enable capturing unhandled exception to report to Sentry by making the following change to your `bootstrap/app.php`: ```php {filename:bootstrap/app.php} withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // }) ->withExceptions(function (Exceptions $exceptions) { Integration::handles($exceptions); })->create(); ``` > 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 Configure the Sentry DSN with this command: ```shell php artisan sentry:publish --dsn=___PUBLIC_DSN___ ``` It creates the config file (`config/sentry.php`) and adds the `DSN` to your `.env` file. ```shell {filename:.env} SENTRY_LARAVEL_DSN=___PUBLIC_DSN___ ``` ### Usage ```php use function Sentry\captureException; try { $this->functionThatMayFail(); } catch (\Throwable $exception) { captureException($exception); } ``` 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 and Lumen versions listed below are all currently supported: - Laravel `>= 11.x.x` on PHP `>= 8.2` is supported starting from `4.3.0` - 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` - Laravel `>= 8.x.x` on PHP `>= 7.3` is supported starting from `1.9.0` - Laravel `>= 7.x.x` on PHP `>= 7.2` is supported starting from `1.7.0` - Laravel `>= 6.x.x` on PHP `>= 7.2` is supported starting from `1.2.0` 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 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` ## Contributing to the SDK Please refer to [CONTRIBUTING.md](CONTRIBUTING.md). ### Thanks to all the people who contributed so far! ## Getting Help/Support If you need help setting up or configuring the Laravel SDK (or anything else in the Sentry universe) please head over to the [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr). There is a ton of great people in our Discord community ready to help you! ## Resources - [![Documentation](https://img.shields.io/badge/documentation-sentry.io-green.svg)](https://docs.sentry.io/quickstart/) - [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/Ww9hbqr) - [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](http://stackoverflow.com/questions/tagged/sentry) - [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) ## License Licensed under the MIT license, see [`LICENSE`](LICENSE). PK\sW]�c¥.php-cs-fixer.phpnu„[µü¤in(__DIR__ . '/src') ; $config = new PhpCsFixer\Config; $config ->setRules([ '@PSR2' => true, 'nullable_type_declaration_for_default_null_value' => true, ]) ->setFinder($finder) ; return $config; PK\sW] ½AYY phpstan.neonnu„[µü¤includes: - phpstan-baseline.neon parameters: level: 1 paths: - src PK\sW]ö¹*Ä))phpstan-baseline.neonnu„[µü¤parameters: ignoreErrors: - message: "#^Class Laravel\\\\Octane\\\\Events\\\\RequestReceived not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\RequestTerminated not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\TaskReceived not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\TaskTerminated not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\TickReceived not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\TickTerminated not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\WorkerErrorOccurred not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Octane\\\\Events\\\\WorkerStopping not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Class Laravel\\\\Sanctum\\\\Events\\\\TokenAuthenticated not found\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneRequestReceivedHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\RequestReceived\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneRequestTerminatedHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\RequestTerminated\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneTaskReceivedHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\TaskReceived\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneTaskTerminatedHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\TaskTerminated\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneTickReceivedHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\TickReceived\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneTickTerminatedHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\TickTerminated\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneWorkerErrorOccurredHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\WorkerErrorOccurred\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:octaneWorkerStoppingHandler\\(\\) has invalid type Laravel\\\\Octane\\\\Events\\\\WorkerStopping\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$event of method Sentry\\\\Laravel\\\\EventHandler\\:\\:sanctumTokenAuthenticatedHandler\\(\\) has invalid type Laravel\\\\Sanctum\\\\Events\\\\TokenAuthenticated\\.$#" count: 1 path: src/Sentry/Laravel/EventHandler.php - message: "#^Parameter \\$request of method Sentry\\\\Laravel\\\\Features\\\\LivewirePackageIntegration\\:\\:handleComponentBooted\\(\\) has invalid type Livewire\\\\Request\\.$#" count: 1 path: src/Sentry/Laravel/Features/LivewirePackageIntegration.php - message: "#^Call to protected method resolve\\(\\) of class Illuminate\\\\Filesystem\\\\FilesystemManager\\.$#" count: 1 path: src/Sentry/Laravel/Features/Storage/Integration.php - message: "#^Class Laravel\\\\Lumen\\\\Application not found\\.$#" count: 3 path: src/Sentry/Laravel/ServiceProvider.php - message: "#^Class GraphQL\\\\Language\\\\AST\\\\DocumentNode not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Class GraphQL\\\\Language\\\\AST\\\\OperationDefinitionNode not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Class Nuwave\\\\Lighthouse\\\\Events\\\\EndExecution not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Class Nuwave\\\\Lighthouse\\\\Events\\\\EndRequest not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Class Nuwave\\\\Lighthouse\\\\Events\\\\StartExecution not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Class Nuwave\\\\Lighthouse\\\\Events\\\\StartRequest not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:extractOperationDefinitionNode\\(\\) has invalid return type GraphQL\\\\Language\\\\AST\\\\OperationDefinitionNode\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Parameter \\$endExecution of method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:handleEndExecution\\(\\) has invalid type Nuwave\\\\Lighthouse\\\\Events\\\\EndExecution\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Parameter \\$endRequest of method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:handleEndRequest\\(\\) has invalid type Nuwave\\\\Lighthouse\\\\Events\\\\EndRequest\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Parameter \\$operation of method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:extractOperationNames\\(\\) has invalid type GraphQL\\\\Language\\\\AST\\\\OperationDefinitionNode\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Parameter \\$query of method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:extractOperationDefinitionNode\\(\\) has invalid type GraphQL\\\\Language\\\\AST\\\\DocumentNode\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Parameter \\$startExecution of method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:handleStartExecution\\(\\) has invalid type Nuwave\\\\Lighthouse\\\\Events\\\\StartExecution\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Parameter \\$startRequest of method Sentry\\\\Laravel\\\\Tracing\\\\Integrations\\\\LighthouseIntegration\\:\\:handleStartRequest\\(\\) has invalid type Nuwave\\\\Lighthouse\\\\Events\\\\StartRequest\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.php - message: "#^Class Laravel\\\\Lumen\\\\Application not found\\.$#" count: 1 path: src/Sentry/Laravel/Tracing/Middleware.php - message: "#^Class Laravel\\\\Lumen\\\\Application not found\\.$#" count: 2 path: src/Sentry/Laravel/Tracing/ServiceProvider.php PK\sW]Fÿ}4››src/Sentry/Laravel/Version.phpnu„[µü¤hub = $hub; $this->reportExceptions = $reportExceptions; $this->useFormattedMessage = $useFormattedMessage; } /** * {@inheritdoc} */ public function handleBatch(array $records): void { $level = $this->level; // filter records based on their level $records = array_filter( $records, function ($record) use ($level) { return $record['level'] >= $level; } ); if (!$records) { return; } // the record with the highest severity is the "main" one $record = array_reduce( $records, function ($highest, $record) { if ($highest === null || $record['level'] > $highest['level']) { return $record; } return $highest; } ); // the other ones are added as a context item $logs = []; foreach ($records as $r) { $logs[] = $this->processRecord($r); } if ($logs) { $record['context']['logs'] = (string)$this->getBatchFormatter()->formatBatch($logs); } $this->handle($record); } /** * Sets the formatter for the logs generated by handleBatch(). * * @param FormatterInterface $formatter * * @return \Sentry\Laravel\SentryHandler */ public function setBatchFormatter(FormatterInterface $formatter): self { $this->batchFormatter = $formatter; return $this; } /** * Gets the formatter for the logs generated by handleBatch(). */ public function getBatchFormatter(): FormatterInterface { if (!$this->batchFormatter) { $this->batchFormatter = $this->getDefaultBatchFormatter(); } return $this->batchFormatter; } /** * Translates Monolog log levels to Sentry Severity. * * @param int $logLevel * * @return \Sentry\Severity */ protected function getLogLevel(int $logLevel): Severity { return $this->getSeverityFromLevel($logLevel); } /** * {@inheritdoc} * @suppress PhanTypeMismatchArgument */ protected function doWrite($record): void { $exception = $record['context']['exception'] ?? null; $isException = $exception instanceof Throwable; unset($record['context']['exception']); if (!$this->reportExceptions && $isException) { return; } $this->hub->withScope( function (Scope $scope) use ($record, $isException, $exception) { $context = !empty($record['context']) && is_array($record['context']) ? $record['context'] : []; if (!empty($context)) { $this->consumeContextAndApplyToScope($scope, $context); } if (!empty($record['extra']) && is_array($record['extra'])) { foreach ($record['extra'] as $key => $extra) { $scope->setExtra($key, $extra); } } $logger = !empty($context['logger']) && is_string($context['logger']) ? $context['logger'] : null; unset($context['logger']); // At this point we consumed everything we could from the context // what remains we add as `log_context` to the event as a whole if (!empty($context)) { $scope->setExtra('log_context', $context); } $scope->addEventProcessor( function (Event $event) use ($record, $logger) { $event->setLevel($this->getLogLevel($record['level'])); $event->setLogger($logger ?? $record['channel']); if (!empty($this->environment) && !$event->getEnvironment()) { $event->setEnvironment($this->environment); } if (!empty($this->release) && !$event->getRelease()) { $event->setRelease($this->release); } if (isset($record['datetime']) && $record['datetime'] instanceof DateTimeInterface) { $event->setTimestamp($record['datetime']->getTimestamp()); } return $event; } ); if ($isException) { $this->hub->captureException($exception); } else { $this->hub->captureMessage( $this->useFormattedMessage || empty($record['message']) ? $record['formatted'] : $record['message'] ); } } ); } /** * {@inheritDoc} */ protected function getDefaultFormatter(): FormatterInterface { return new LineFormatter('[%channel%] %message%'); } /** * Gets the default formatter for the logs generated by handleBatch(). * * @return FormatterInterface */ protected function getDefaultBatchFormatter(): FormatterInterface { return new LineFormatter(); } /** * Set the release. * * @param string $value * * @return self */ public function setRelease($value): self { $this->release = $value; return $this; } /** * Set the current application environment. * * @param string $value * * @return self */ public function setEnvironment($value): self { $this->environment = $value; return $this; } /** * Add a breadcrumb. * * @link https://docs.sentry.io/learn/breadcrumbs/ * * @param \Sentry\Breadcrumb $crumb * * @return \Sentry\Laravel\SentryHandler */ public function addBreadcrumb(Breadcrumb $crumb): self { $this->hub->addBreadcrumb($crumb); return $this; } /** * Consumes the context and applies it to the scope. * * @param \Sentry\State\Scope $scope * @param array $context * * @return void */ private function consumeContextAndApplyToScope(Scope $scope, array &$context): void { if (!empty($context['extra']) && is_array($context['extra'])) { foreach ($context['extra'] as $key => $value) { $scope->setExtra($key, $value); } unset($context['extra']); } if (!empty($context['tags']) && is_array($context['tags'])) { foreach ($context['tags'] as $tag => $value) { // Ignore tags with a value that is not a string or can be casted to a string if (!$this->valueCanBeString($value)) { continue; } $scope->setTag($tag, (string)$value); } unset($context['tags']); } if (!empty($context['fingerprint']) && is_array($context['fingerprint'])) { $scope->setFingerprint($context['fingerprint']); unset($context['fingerprint']); } if (!empty($context['user']) && is_array($context['user'])) { try { $scope->setUser($context['user']); unset($context['user']); } catch (TypeError $e) { // In some cases the context can be invalid, in that case we ignore it and // choose to not send it to Sentry in favor of not breaking the application } } } /** * Check if the value passed can be cast to a string. * * @param mixed $value * * @return bool */ private function valueCanBeString($value): bool { return is_string($value) || is_scalar($value) || (is_object($value) && method_exists($value, '__toString')); } } PK\sW]±û--'src/Sentry/Laravel/Logs/LogsHandler.phpnu„[µü¤level; // filter records based on their level $records = array_filter( $records, function ($record) use ($level) { return $record['level'] >= $level; } ); if (!$records) { return; } // the record with the highest severity is the "main" one $record = array_reduce( $records, function ($highest, $record) { if ($highest === null || $record['level'] > $highest['level']) { return $record; } return $highest; } ); // the other ones are added as a context item $logs = []; foreach ($records as $r) { $logs[] = $this->processRecord($r); } if ($logs) { $record['context']['logs'] = (string)$this->getBatchFormatter()->formatBatch($logs); } $this->handle($record); } /** * Sets the formatter for the logs generated by handleBatch(). * * @param FormatterInterface $formatter * * @return \Sentry\Laravel\SentryHandler */ public function setBatchFormatter(FormatterInterface $formatter): self { $this->batchFormatter = $formatter; return $this; } /** * Gets the formatter for the logs generated by handleBatch(). */ public function getBatchFormatter(): FormatterInterface { if (!$this->batchFormatter) { $this->batchFormatter = new LineFormatter(); } return $this->batchFormatter; } /** * {@inheritdoc} * @suppress PhanTypeMismatchArgument */ protected function doWrite($record): void { $context = $record['context']; $exception = $context['exception'] ?? null; if ($exception instanceof Throwable) { // Unset the exception object from the log context unset($context['exception']); } logger()->aggregator()->add( // This seems a little bit of a roundabout way to get the log level, but this is done for compatibility self::getLogLevelFromSeverity( self::getSeverityFromLevel($record['level']) ), $record['message'], [], array_merge($context, $record['extra'], ['sentry.origin' => 'auto.logger.monolog']) ); } private static function getLogLevelFromSeverity(Severity $severity): LogLevel { switch ($severity) { case Severity::debug(): return LogLevel::debug(); case Severity::warning(): return LogLevel::warn(); case Severity::error(): return LogLevel::error(); case Severity::fatal(): return LogLevel::fatal(); default: return LogLevel::info(); } } } PK\sW]xÈï�  &src/Sentry/Laravel/Logs/LogChannel.phpnu„[µü¤parseChannel($config), [ $this->prepareHandler($handler, $config), ] ); } } PK\sW]«æ„¤ÚÚ!src/Sentry/Laravel/LogChannel.phpnu„[µü¤app->make(HubInterface::class), $config['level'] ?? Logger::DEBUG, $config['bubble'] ?? true, $config['report_exceptions'] ?? true, isset($config['formatter']) && $config['formatter'] !== 'default' ); if (isset($config['action_level'])) { $handler = new FingersCrossedHandler($handler, $config['action_level']); // Consume the `action_level` config option since newer Laravel versions also support this option // and will wrap the handler again in another `FingersCrossedHandler` if we leave the option set // See: https://github.com/laravel/framework/pull/40305 (release v8.79.0) unset($config['action_level']); } return new Logger( $this->parseChannel($config), [ $this->prepareHandler($handler, $config), ] ); } } PK\sW]6è Àp/p/"src/Sentry/Laravel/Integration.phpnu„[µü¤getIntegration(self::class); if (!$self instanceof self) { return $event; } if (empty($event->getTransaction())) { $event->setTransaction(self::getTransaction()); } return $event; }); } /** * Convenience method to register the exception handler with Laravel 11.0 and up. */ public static function handles(Exceptions $exceptions): void { $exceptions->reportable(static function (Throwable $exception) { self::captureUnhandledException($exception); }); } /** * Adds a breadcrumb if the integration is enabled for Laravel. * * @param Breadcrumb $breadcrumb */ public static function addBreadcrumb(Breadcrumb $breadcrumb): void { $self = SentrySdk::getCurrentHub()->getIntegration(self::class); if (!$self instanceof self) { return; } addBreadcrumb($breadcrumb); } /** * Configures the scope if the integration is enabled for Laravel. * * @param callable $callback */ public static function configureScope(callable $callback): void { $self = SentrySdk::getCurrentHub()->getIntegration(self::class); if (!$self instanceof self) { return; } configureScope($callback); } /** * @return null|string */ public static function getTransaction(): ?string { return self::$transaction; } /** * @param null|string $transaction */ public static function setTransaction(?string $transaction): void { self::$transaction = $transaction; } /** * Block until all events are processed by the PHP SDK client. * * @internal This is not part of the public API and is here temporarily until * the underlying issue can be resolved, this method will be removed. */ public static function flushEvents(): void { $client = SentrySdk::getCurrentHub()->getClient(); if ($client !== null) { $client->flush(); Logs::getInstance()->flush(); } } /** * Extract the readable name for a route and the transaction source for where that route name came from. * * @param \Illuminate\Routing\Route $route * * @return array{0: string, 1: \Sentry\Tracing\TransactionSource} * * @internal This helper is used in various places to extract meaningful info from a Laravel Route object. */ public static function extractNameAndSourceForRoute(Route $route): array { return [ '/' . ltrim($route->uri(), '/'), TransactionSource::route(), ]; } /** * 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. * * @return string */ public static function sentryMeta(): string { return self::sentryTracingMeta() . self::sentryW3CTracingMeta() . self::sentryBaggageMeta(); } /** * Retrieve the `sentry-trace` meta tag with tracing information to link this request to front-end requests. * * @return string */ public static function sentryTracingMeta(): string { return sprintf('', getTraceparent()); } /** * Retrieve the `traceparent` meta tag with tracing information to link this request to front-end requests. * * @deprecated since version 4.14. To be removed in version 5.0. * @return string */ public static function sentryW3CTracingMeta(): string { return ''; } /** * 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 sentryBaggageMeta(): string { return sprintf('', getBaggage()); } /** * Capture a unhandled exception and report it to Sentry. * * @param \Throwable $throwable * * @return \Sentry\EventId|null */ public static function captureUnhandledException(Throwable $throwable): ?EventId { // We instruct users to call `captureUnhandledException` in their exception handler, however this does not mean // the exception was actually unhandled. Laravel has the `report` helper function that is used to report to a log // file or Sentry, but that means they are handled otherwise they wouldn't have been routed through `report`. So to // prevent marking those as "unhandled" we try and make an educated guess if the call to `captureUnhandledException` // came from the `report` helper and shouldn't be marked as "unhandled" even though the come to us here to be reported $handled = self::makeAnEducatedGuessIfTheExceptionMaybeWasHandled(); $hint = EventHint::fromArray([ 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, $handled), ]); return SentrySdk::getCurrentHub()->captureException($throwable, $hint); } /** * Returns a callback that can be passed to `Model::handleMissingAttributeViolationUsing` to report missing attribute violations to Sentry. * * @param callable|null $callback Optional callback to be called after the violation is reported to Sentry. * @param bool $suppressDuplicateReports Whether to suppress duplicate reports of the same violation. * @param bool $reportAfterResponse Whether to delay sending the report to after the response has been sent. * * @return callable */ public static function missingAttributeViolationReporter(?callable $callback = null, bool $suppressDuplicateReports = true, bool $reportAfterResponse = true): callable { return new ModelViolationReports\MissingAttributeModelViolationReporter($callback, $suppressDuplicateReports, $reportAfterResponse); } /** * Returns a callback that can be passed to `Model::handleLazyLoadingViolationUsing` to report lazy loading violations to Sentry. * * @param callable|null $callback Optional callback to be called after the violation is reported to Sentry. * @param bool $suppressDuplicateReports Whether to suppress duplicate reports of the same violation. * @param bool $reportAfterResponse Whether to delay sending the report to after the response has been sent. * * @return callable */ public static function lazyLoadingViolationReporter(?callable $callback = null, bool $suppressDuplicateReports = true, bool $reportAfterResponse = true): callable { return new ModelViolationReports\LazyLoadingModelViolationReporter($callback, $suppressDuplicateReports, $reportAfterResponse); } /** * Returns a callback that can be passed to `Model::handleDiscardedAttributeViolationUsing` to report discarded attribute violations to Sentry. * * @param callable|null $callback Optional callback to be called after the violation is reported to Sentry. * @param bool $suppressDuplicateReports Whether to suppress duplicate reports of the same violation. * @param bool $reportAfterResponse Whether to delay sending the report to after the response has been sent. * * @return callable */ public static function discardedAttributeViolationReporter(?callable $callback = null, bool $suppressDuplicateReports = true, bool $reportAfterResponse = true): callable { return new ModelViolationReports\DiscardedAttributeViolationReporter($callback, $suppressDuplicateReports, $reportAfterResponse); } /** * Try to make an educated guess if the call came from the Laravel `report` helper. * * @see https://github.com/laravel/framework/blob/008a4dd49c3a13343137d2bc43297e62006c7f29/src/Illuminate/Foundation/helpers.php#L667-L682 * * @return bool */ private static function makeAnEducatedGuessIfTheExceptionMaybeWasHandled(): bool { // We limit the amount of backtrace frames since it is very unlikely to be any deeper $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20); // We are looking for `$handler->report()` to be called from the `report()` function foreach ($trace as $frameIndex => $frame) { // We need a frame with a class and function defined, we can skip frames missing either if (!isset($frame['class'], $frame['function'])) { continue; } // Check if the frame was indeed `$handler->report()` if ($frame['type'] !== '->' || $frame['function'] !== 'report') { continue; } // Make sure we have a next frame, we could have reached the end of the trace if (!isset($trace[$frameIndex + 1])) { continue; } // The next frame should contain the call to the `report()` helper function $nextFrame = $trace[$frameIndex + 1]; // If a class was set or the function name is not `report` we can skip this frame if (isset($nextFrame['class']) || !isset($nextFrame['function']) || $nextFrame['function'] !== 'report') { continue; } // If we reached this point we can be pretty sure the `report` function was called // and we can come to the educated conclusion the exception was indeed handled return true; } // If we reached this point we can be pretty sure the `report` function was not called return false; } } PK\sW]í…×ó'8'8#src/Sentry/Laravel/EventHandler.phpnu„[µü¤ 'messageLogged', RoutingEvents\RouteMatched::class => 'routeMatched', DatabaseEvents\QueryExecuted::class => 'queryExecuted', ]; /** * Map authentication event handlers to events. * * @var array */ protected static $authEventHandlerMap = [ AuthEvents\Authenticated::class => 'authenticated', Sanctum\TokenAuthenticated::class => 'sanctumTokenAuthenticated', // Since Sanctum 2.13 ]; /** * Map Octane event handlers to events. * * @var array */ protected static $octaneEventHandlerMap = [ Octane\RequestReceived::class => 'octaneRequestReceived', Octane\RequestTerminated::class => 'octaneRequestTerminated', Octane\TaskReceived::class => 'octaneTaskReceived', Octane\TaskTerminated::class => 'octaneTaskTerminated', Octane\TickReceived::class => 'octaneTickReceived', Octane\TickTerminated::class => 'octaneTickTerminated', Octane\WorkerErrorOccurred::class => 'octaneWorkerErrorOccurred', Octane\WorkerStopping::class => 'octaneWorkerStopping', ]; /** * The Laravel container. * * @var \Illuminate\Contracts\Container\Container */ private $container; /** * Indicates if we should add SQL queries to the breadcrumbs. * * @var bool */ private $recordSqlQueries; /** * Indicates if we should add query bindings to the breadcrumbs. * * @var bool */ private $recordSqlBindings; /** * Indicates if we should add Laravel logs to the breadcrumbs. * * @var bool */ private $recordLaravelLogs; /** * Indicates if we should add tick info to the breadcrumbs. * * @var bool */ private $recordOctaneTickInfo; /** * Indicates if we should add task info to the breadcrumbs. * * @var bool */ private $recordOctaneTaskInfo; /** * Indicates if we pushed a scope for Octane. * * @var bool */ private $pushedOctaneScope = false; /** * EventHandler constructor. * * @param \Illuminate\Contracts\Container\Container $container * @param array $config */ public function __construct(Container $container, array $config) { $this->container = $container; $this->recordSqlQueries = ($config['breadcrumbs.sql_queries'] ?? $config['breadcrumbs']['sql_queries'] ?? true) === true; $this->recordSqlBindings = ($config['breadcrumbs.sql_bindings'] ?? $config['breadcrumbs']['sql_bindings'] ?? false) === true; $this->recordLaravelLogs = ($config['breadcrumbs.logs'] ?? $config['breadcrumbs']['logs'] ?? true) === true; $this->recordOctaneTickInfo = ($config['breadcrumbs.octane_tick_info'] ?? $config['breadcrumbs']['octane_tick_info'] ?? true) === true; $this->recordOctaneTaskInfo = ($config['breadcrumbs.octane_task_info'] ?? $config['breadcrumbs']['octane_task_info'] ?? true) === true; } /** * Attach all event handlers. */ public function subscribe(Dispatcher $dispatcher): void { foreach (static::$eventHandlerMap as $eventName => $handler) { $dispatcher->listen($eventName, [$this, $handler]); } } /** * Attach all authentication event handlers. */ public function subscribeAuthEvents(Dispatcher $dispatcher): void { foreach (static::$authEventHandlerMap as $eventName => $handler) { $dispatcher->listen($eventName, [$this, $handler]); } } /** * Attach all Octane event handlers. */ public function subscribeOctaneEvents(Dispatcher $dispatcher): void { foreach (static::$octaneEventHandlerMap as $eventName => $handler) { $dispatcher->listen($eventName, [$this, $handler]); } } /** * Pass through the event and capture any errors. * * @param string $method * @param array $arguments */ public function __call(string $method, array $arguments) { $handlerMethod = "{$method}Handler"; if (!method_exists($this, $handlerMethod)) { throw new RuntimeException("Missing event handler: {$handlerMethod}"); } try { $this->{$handlerMethod}(...$arguments); } catch (Exception $exception) { // Ignore } } protected function routeMatchedHandler(RoutingEvents\RouteMatched $match): void { $routeAlias = $match->route->action['as'] ?? ''; // Ignore the route if it is the route for the Laravel Folio package // We handle that route separately in the FolioPackageIntegration if ($routeAlias === 'laravel-folio') { return; } Middleware::signalRouteWasMatched(); [$routeName] = Integration::extractNameAndSourceForRoute($match->route); Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_NAVIGATION, 'route', $routeName )); Integration::setTransaction($routeName); } protected function queryExecutedHandler(DatabaseEvents\QueryExecuted $query): void { if (!$this->recordSqlQueries) { return; } $data = ['connectionName' => $query->connectionName]; if ($query->time !== null) { $data['executionTimeMs'] = $query->time; } if ($this->recordSqlBindings) { $data['bindings'] = $query->bindings; } Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'db.sql.query', $query->sql, $data )); } protected function messageLoggedHandler(LogEvents\MessageLogged $logEntry): void { if (!$this->recordLaravelLogs) { return; } // 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 ($logEntry->message === null) { return; } Integration::addBreadcrumb(new Breadcrumb( $this->logLevelToBreadcrumbLevel($logEntry->level), Breadcrumb::TYPE_DEFAULT, 'log.' . $logEntry->level, $logEntry->message, $logEntry->context )); } protected function authenticatedHandler(AuthEvents\Authenticated $event): void { $this->configureUserScopeFromModel($event->user); } protected function sanctumTokenAuthenticatedHandler(Sanctum\TokenAuthenticated $event): void { $this->configureUserScopeFromModel($event->token->tokenable); } /** * Configures the user scope with the user data and values from the HTTP request. * * @param mixed $authUser * * @return void */ private function configureUserScopeFromModel($authUser): void { $userData = []; // If the user is a Laravel Eloquent model we try to extract some common fields from it if ($authUser instanceof Model) { $email = null; if ($this->modelHasAttribute($authUser, 'email')) { $email = $authUser->getAttribute('email'); } elseif ($this->modelHasAttribute($authUser, 'mail')) { $email = $authUser->getAttribute('mail'); } $username = $this->modelHasAttribute($authUser, 'username') ? (string)$authUser->getAttribute('username') : null; $userData = [ 'id' => $authUser instanceof Authenticatable ? $authUser->getAuthIdentifier() : $authUser->getKey(), 'email' => $email, 'username' => $username, ]; } try { /** @var \Illuminate\Http\Request $request */ $request = $this->container->make('request'); if ($request instanceof Request) { $ipAddress = $request->ip(); if ($ipAddress !== null) { $userData['ip_address'] = $ipAddress; } } } catch (BindingResolutionException $e) { // If there is no request bound we cannot get the IP address from it } Integration::configureScope(static function (Scope $scope) use ($userData): void { $scope->setUser(array_filter($userData)); }); } private function modelHasAttribute(Model $model, string $key): bool { // Taken from: https://github.com/laravel/framework/blob/v11.41.3/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L445 // Laravel 11 introduced the `hasAttribute` method we are (almost) mirroring here since it's not available in earlier Laravel versions we support return array_key_exists($key, $model->getAttributes()) || $model->hasGetMutator($key) || (method_exists($model, 'hasAttributeMutator') && $model->hasAttributeMutator($key)); } protected function octaneRequestReceivedHandler(Octane\RequestReceived $event): void { $this->prepareScopeForOctane(); } protected function octaneRequestTerminatedHandler(Octane\RequestTerminated $event): void { $this->cleanupScopeForOctane(); } protected function octaneTaskReceivedHandler(Octane\TaskReceived $event): void { $this->prepareScopeForOctane(); if (!$this->recordOctaneTaskInfo) { return; } Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'octane.task', 'Processing Octane task' )); } protected function octaneTaskTerminatedHandler(Octane\TaskTerminated $event): void { $this->cleanupScopeForOctane(); } protected function octaneTickReceivedHandler(Octane\TickReceived $event): void { $this->prepareScopeForOctane(); if (!$this->recordOctaneTickInfo) { return; } Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'octane.tick', 'Processing Octane tick' )); } protected function octaneTickTerminatedHandler(Octane\TickTerminated $event): void { $this->cleanupScopeForOctane(); } protected function octaneWorkerErrorOccurredHandler(Octane\WorkerErrorOccurred $event): void { $this->afterTaskWithinLongRunningProcess(); } protected function octaneWorkerStoppingHandler(Octane\WorkerStopping $event): void { $this->afterTaskWithinLongRunningProcess(); } private function prepareScopeForOctane(): void { $this->cleanupScopeForOctane(); $this->prepareScopeForTaskWithinLongRunningProcess(); $this->pushedOctaneScope = true; } private function cleanupScopeForOctane(): void { $this->cleanupScopeForTaskWithinLongRunningProcessWhen($this->pushedOctaneScope); $this->pushedOctaneScope = false; } /** * Translates common log levels to Sentry breadcrumb levels. * * @param string $level Log level. Maybe any standard. * * @return string Breadcrumb level. */ private function logLevelToBreadcrumbLevel(string $level): string { switch (strtolower($level)) { case 'debug': return Breadcrumb::LEVEL_DEBUG; case 'warning': return Breadcrumb::LEVEL_WARNING; case 'error': return Breadcrumb::LEVEL_ERROR; case 'critical': case 'alert': case 'emergency': return Breadcrumb::LEVEL_FATAL; case 'info': case 'notice': default: return Breadcrumb::LEVEL_INFO; } } /** * Should be called after a task within a long running process has ended so events can be flushed. */ private function afterTaskWithinLongRunningProcess(): void { Integration::flushEvents(); } /** * Should be called before starting a task within a long running process, this is done to prevent * the task to have effect on the scope for the next task to run within the long running process. */ private function prepareScopeForTaskWithinLongRunningProcess(): void { SentrySdk::getCurrentHub()->pushScope(); // When a job starts, we want to make sure the scope is cleared of breadcrumbs SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) { $scope->clearBreadcrumbs(); }); } /** * Cleanup a previously prepared scope. * * @param bool $when Only cleanup the scope when this is true. * * @see prepareScopeForTaskWithinLongRunningProcess */ private function cleanupScopeForTaskWithinLongRunningProcessWhen(bool $when): void { if (!$when) { return; } $this->afterTaskWithinLongRunningProcess(); SentrySdk::getCurrentHub()->popScope(); } } PK\sW]Œ @­­6src/Sentry/Laravel/Console/AboutCommandIntegration.phpnu„[µü¤getClient(); if ($client === null) { return [ 'Enabled' => 'NOT CONFIGURED', 'Laravel SDK Version' => Version::SDK_VERSION, 'PHP SDK Version' => Client::SDK_VERSION, ]; } $options = $client->getOptions(); // Note: order is not important since Laravel orders these alphabetically return [ 'Enabled' => $options->getDsn() ? 'YES' : 'MISSING DSN', 'Environment' => $options->getEnvironment() ?: 'NOT SET', 'Laravel SDK Version' => Version::SDK_VERSION, 'PHP SDK Version' => Client::SDK_VERSION, 'Release' => $options->getRelease() ?: 'NOT SET', 'Sample Rate Errors' => $this->formatSampleRate($options->getSampleRate()), 'Sample Rate Performance Monitoring' => $this->formatSampleRate($options->getTracesSampleRate(), $options->getTracesSampler() !== null), 'Sample Rate Profiling' => $this->formatSampleRate($options->getProfilesSampleRate()), 'Send Default PII' => $options->shouldSendDefaultPii() ? 'ENABLED' : 'DISABLED', ]; } private function formatSampleRate(?float $sampleRate, bool $hasSamplerCallback = false): string { if ($hasSamplerCallback) { return 'CUSTOM SAMPLER'; } if ($sampleRate === null) { return 'NOT SET'; } return number_format($sampleRate * 100) . '%'; } } PK\sW]oº‘¼¼-src/Sentry/Laravel/Console/PublishCommand.phpnu„[µü¤option('dsn'); if (!empty($dsn) || !$this->isEnvKeySet('SENTRY_LARAVEL_DSN')) { if (empty($dsn)) { $dsnFromInput = $this->askForDsnInput(); if (empty($dsnFromInput)) { $this->error('Please provide a valid DSN using the `--dsn` option or setting `SENTRY_LARAVEL_DSN` in your `.env` file!'); return 1; } $dsn = $dsnFromInput; } $env['SENTRY_LARAVEL_DSN'] = $dsn; $arg['--dsn'] = $dsn; } $sendDefaultPii = $this->confirm( "Do you want to include information such as request headers, IP address and the authenticated user to events collected by the SDK?\n You can read more about this on https://docs.sentry.io/platforms/php/guides/laravel/data-management/data-collected/", $this->option('with-send-default-pii') === true ); if ($sendDefaultPii) { $env['SENTRY_SEND_DEFAULT_PII'] = 'true'; } elseif ($this->isEnvKeySet('SENTRY_SEND_DEFAULT_PII')) { $env['SENTRY_SEND_DEFAULT_PII'] = 'false'; } $testCommandPrompt = 'Do you want to send a test event to Sentry?'; if ($this->confirm('Enable Performance Monitoring?', !$this->option('without-performance-monitoring'))) { $testCommandPrompt = 'Do you want to send a test event & transaction to Sentry?'; $env['SENTRY_TRACES_SAMPLE_RATE'] = '1.0'; $arg['--transaction'] = true; } elseif ($this->isEnvKeySet('SENTRY_TRACES_SAMPLE_RATE')) { $env['SENTRY_TRACES_SAMPLE_RATE'] = '0'; } if ($this->confirm($testCommandPrompt, !$this->option('without-test'))) { $testResult = $this->call('sentry:test', $arg); if ($testResult === 1) { return 1; } } $this->info('Publishing Sentry config...'); $this->call('vendor:publish', ['--provider' => ServiceProvider::class]); if (!$this->setEnvValues($env)) { return 1; } return 0; } private function setEnvValues(array $values): bool { $envFilePath = app()->environmentFilePath(); $envFileContents = file_get_contents($envFilePath); if (!$envFileContents) { $this->error('Could not read `.env` file!'); return false; } if (count($values) > 0) { foreach ($values as $envKey => $envValue) { if ($this->isEnvKeySet($envKey, $envFileContents)) { $envFileContents = preg_replace("/^{$envKey}=\"?.*?\"?(\s|$)/m", "{$envKey}={$envValue}\n", $envFileContents); $this->info("Updated {$envKey} with new value in your `.env` file."); } else { // Ensure there is a newline before writing env variables if (substr($envFileContents, -1) !== "\n") { $envFileContents .= "\n"; } $envFileContents .= "{$envKey}={$envValue}\n"; $this->info("Added {$envKey} to your `.env` file."); } } } if (!file_put_contents($envFilePath, $envFileContents)) { $this->error('Updating the `.env` file failed!'); return false; } return true; } private function isEnvKeySet(string $envKey, ?string $envFileContents = null): bool { $envFileContents = $envFileContents ?? file_get_contents(app()->environmentFilePath()); return (bool)preg_match("/^{$envKey}=\"?.*?\"?(\s|$)/m", $envFileContents); } private function askForDsnInput(): string { if ($this->option('no-interaction')) { return ''; } while (true) { $this->info(''); $this->question('Please paste the DSN here'); $dsn = $this->ask('DSN'); // In case someone copies it with SENTRY_LARAVEL_DSN= or SENTRY_DSN= $dsn = Str::after($dsn, '='); try { Dsn::createFromString($dsn); return $dsn; } catch (Exception $e) { // Not a valid DSN do it again $this->error('The DSN is not valid, please make sure to paste a valid DSN!'); } } } } PK\sW]®“F2c#c#*src/Sentry/Laravel/Console/TestCommand.phpnu„[µü¤error('You need to enable the PHP cURL extension (ext-curl).'); return 1; } /** * Maximize error reporting. * 2048 is \E_STRICT which has been deprecated in PHP 8.4 */ $old_error_reporting = error_reporting(E_ALL | 2048); $dsn = $this->option('dsn'); $laravelClient = null; try { $laravelClient = app(HubInterface::class)->getClient(); } catch (Throwable $e) { // Ignore any errors related to getting the client from the Laravel container // These errors will surface later in the process but we should not crash here } // If the DSN was not passed as option to the command we use the registered client to get the DSN from the Laravel config if ($dsn === null) { $dsnObject = $laravelClient === null ? null : $laravelClient->getOptions()->getDsn(); if ($dsnObject !== null) { $dsn = (string)$dsnObject; $this->info('DSN discovered from Laravel config or `.env` file!'); } } // No DSN found from the command line or config if (!$dsn) { $this->error('Could not discover DSN!'); $this->printDebugTips(); return 1; } $options = [ 'dsn' => $dsn, 'before_send' => static function (Event $event): ?Event { foreach ($event->getExceptions() as $exception) { $stacktrace = $exception->getStacktrace(); if ($stacktrace === null) { continue; } foreach ($stacktrace->getFrames() as $frame) { if (str_starts_with($frame->getAbsoluteFilePath(), __DIR__)) { $frame->setIsInApp(true); } } } return $event; }, // We include this file as "in-app" so that the events generated have something to show 'in_app_include' => [__DIR__], 'in_app_exclude' => [base_path('artisan'), base_path('vendor')], 'traces_sample_rate' => 1.0, ]; if ($laravelClient !== null) { // Some options are taken from the client as configured by the user $options = array_merge($options, [ 'release' => $laravelClient->getOptions()->getRelease(), 'environment' => $laravelClient->getOptions()->getEnvironment(), 'http_client' => $laravelClient->getOptions()->getHttpClient(), 'http_proxy' => $laravelClient->getOptions()->getHttpProxy(), 'http_proxy_authentication' => $laravelClient->getOptions()->getHttpProxyAuthentication(), 'http_connect_timeout' => $laravelClient->getOptions()->getHttpConnectTimeout(), 'http_timeout' => $laravelClient->getOptions()->getHttpTimeout(), 'http_ssl_verify_peer' => $laravelClient->getOptions()->getHttpSslVerifyPeer(), 'http_compression' => $laravelClient->getOptions()->isHttpCompressionEnabled(), ]); } try { $clientBuilder = ClientBuilder::create($options); } catch (Exception $e) { $this->error($e->getMessage()); return 1; } // Set the Laravel SDK identifier and version $clientBuilder->setSdkIdentifier(Version::SDK_IDENTIFIER); $clientBuilder->setSdkVersion(Version::SDK_VERSION); // We set a logger so we can surface errors thrown internally by the SDK $clientBuilder->setLogger(new class($this) extends AbstractLogger { private $command; public function __construct(TestCommand $command) { $this->command = $command; } public function log($level, $message, array $context = []): void { // Only show debug, info and notice messages in verbose mode $verbosity = in_array($level, ['debug', 'info', 'notice'], true) ? OutputInterface::VERBOSITY_VERBOSE : OutputInterface::VERBOSITY_NORMAL; $this->command->info("SDK({$level}): {$message}", $verbosity); if (in_array($level, ['error', 'critical'], true)) { $this->command->logErrorMessageFromSDK($message); } } }); // We create a new Hub and Client to prevent user configuration from affecting the test command $hub = new Hub($clientBuilder->getClient()); $this->info('Sending test event...'); $exception = $this->generateTestException($this->name, ['foo' => 'bar']); $eventId = $hub->captureException($exception); if (!$eventId) { $this->error('There was an error sending the event.'); $this->printDebugTips(); return 1; } $this->info("Test event sent with ID: {$eventId}"); if ($this->option('transaction')) { $this->clearErrorMessagesFromSDK(); $transaction = $hub->startTransaction( TransactionContext::make() ->setOp('sentry.test') ->setName('Sentry Test Transaction') ->setOrigin('auto.test.transaction') ->setSource(TransactionSource::custom()) ->setSampled(true) ); $span = $transaction->startChild( SpanContext::make() ->setOp('sentry.sent') ->setOrigin('auto.test.span') ); $this->info('Sending transaction...'); $span->finish(); $transactionId = $transaction->finish(); if (!$transactionId) { $this->error('There was an error sending the transaction.'); $this->printDebugTips(); return 1; } $this->info("Transaction sent with ID: {$transactionId}"); } error_reporting($old_error_reporting); return 0; } /** * Generate an example exception to send to Sentry. */ protected function generateTestException(string $command, array $arg): Exception { // Do something silly try { throw new Exception('This is a test exception sent from the Sentry Laravel SDK.'); } catch (Exception $exception) { return $exception; } } public function logErrorMessageFromSDK(string $message): void { $this->errorMessages[] = $message; } private function clearErrorMessagesFromSDK(): void { $this->errorMessages = []; } private function printDebugTips(): void { $probablySSLError = false; foreach ($this->errorMessages as $logMessage) { if (Str::contains($logMessage, ['SSL certificate problem', 'certificate has expired'])) { $probablySSLError = true; } } if ($probablySSLError) { $this->warn('The problem might be related to the Let\'s Encrypt root certificate that expired and your machine not having an up-to-date enough OpenSSL version or still having the expired root in your certificate authority store.'); $this->warn('For more information you can check out this forum post from Let\'s Encrypt that contains helpful links on how to resolve this for your environment: https://community.letsencrypt.org/t/production-chain-changes/150739/4'); } elseif (count($this->errorMessages) > 0) { $this->error('Please check the error message from the SDK above for further hints about what went wrong.'); } else { $this->error('Please check if your DSN is set properly in your `.env` as `SENTRY_LARAVEL_DSN` or in your config file `config/sentry.php`.'); } } } PK\sW]™wª__Isrc/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.phpnu„[µü¤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); } } PK\sW]Ðî æ¨¨Gsrc/Sentry/Laravel/Tracing/Routing/TracingCallableDispatcherTracing.phpnu„[µü¤dispatcher = $dispatcher; } public function dispatch(Route $route, $callable) { return $this->wrapRouteDispatch(function () use ($route, $callable) { return $this->dispatcher->dispatch($route, $callable); }, $route); } } PK\sW]¤ë]HËË?src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.phpnu„[µü¤getSpan(); // If there is no sampled span there is no need to wrap the dispatch if ($parentSpan === null || !$parentSpan->getSampled()) { return $dispatch(); } // The action name can be a Closure curiously enough... so we guard againt that here // @see: https://github.com/getsentry/sentry-laravel/issues/917 $action = $route->getActionName() instanceof Closure ? 'Closure' : $route->getActionName(); $span = $parentSpan->startChild( SpanContext::make() ->setOp('http.route') ->setOrigin('auto.http.server') ->setDescription($action) ); SentrySdk::getCurrentHub()->setSpan($span); try { return $dispatch(); } finally { $span->finish(); SentrySdk::getCurrentHub()->setSpan($parentSpan); } } } PK\sW]Âj9¥¨ ¨ .src/Sentry/Laravel/Tracing/BacktraceHelper.phpnu„[µü¤options = $options; $this->frameBuilder = new FrameBuilder($options, $representationSerializer); } /** * Find the first in app frame for a given backtrace. * * @param array> $backtrace The backtrace * * @phpstan-param list $backtrace */ public function findFirstInAppFrameForBacktrace(array $backtrace): ?Frame { $file = Frame::INTERNAL_FRAME_FILENAME; $line = 0; foreach ($backtrace as $backtraceFrame) { $frame = $this->frameBuilder->buildFromBacktraceFrame($file, $line, $backtraceFrame); if ($frame->isInApp()) { return $frame; } $file = $backtraceFrame['file'] ?? Frame::INTERNAL_FRAME_FILENAME; $line = $backtraceFrame['line'] ?? 0; } return null; } /** * Takes a frame and if it's a compiled view path returns the original view path. * * @param \Sentry\Frame $frame * * @return string|null */ public function getOriginalViewPathForFrameOfCompiledViewPath(Frame $frame): ?string { // Check if we are dealing with a frame for a cached view path if (!Str::startsWith($frame->getFile(), '/storage/framework/views/')) { return null; } // If for some reason the file does not exists, skip resolving if (!file_exists($frame->getAbsoluteFilePath())) { return null; } $viewFileContents = file_get_contents($frame->getAbsoluteFilePath()); preg_match('/PATH (?.*?) ENDPATH/', $viewFileContents, $matches); // No path comment found in the file, must be a very old Laravel version if (empty($matches['originalPath'])) { return null; } return $this->stripPrefixFromFilePath($matches['originalPath']); } /** * Removes from the given file path the specified prefixes. * * @param string $filePath The path to the file */ private function stripPrefixFromFilePath(string $filePath): string { foreach ($this->options->getPrefixes() as $prefix) { if (Str::startsWith($filePath, $prefix)) { return mb_substr($filePath, mb_strlen($prefix)); } } return $filePath; } } PK\sW]Þ½õ^Z Z Asrc/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.phpnu„[µü¤ */ private $operations; /** @var \Sentry\Tracing\Span|null */ private $previousSpan; /** @var \Sentry\Tracing\Span|null */ private $requestSpan; /** @var \Sentry\Tracing\Span|null */ private $operationSpan; /** @var \Illuminate\Contracts\Events\Dispatcher */ private $eventDispatcher; /** * Indicates if, when building the transaction name, the operation name should be ignored. * * @var bool */ private $ignoreOperationName; public function __construct(EventDispatcher $eventDispatcher, bool $ignoreOperationName = false) { $this->eventDispatcher = $eventDispatcher; $this->ignoreOperationName = $ignoreOperationName; } public function setupOnce(): void { if (!$this->isApplicable()) { return; } $this->eventDispatcher->listen(StartRequest::class, [$this, 'handleStartRequest']); $this->eventDispatcher->listen(StartExecution::class, [$this, 'handleStartExecution']); $this->eventDispatcher->listen(EndExecution::class, [$this, 'handleEndExecution']); $this->eventDispatcher->listen(EndRequest::class, [$this, 'handleEndRequest']); Scope::addGlobalEventProcessor(function (Event $event): Event { $currentHub = SentrySdk::getCurrentHub(); $integration = $currentHub->getIntegration(self::class); $client = $currentHub->getClient(); // The client bound to the current hub, if any, could not have this // integration enabled. If this is the case, bail out if (null === $integration || null === $client) { return $event; } $this->processEvent($event, $client->getOptions()); return $event; }); } private function processEvent(Event $event, Options $options): void { // Detect if we are processing a GraphQL request, if not skip processing the event if ($event->getTransaction() === null || !Str::startsWith($event->getTransaction(), 'lighthouse?')) { return; } $requestData = $event->getRequest(); // Make sure we have the request data and it contains the query if (!isset($requestData['data']['query'])) { return; } // https://develop.sentry.dev/sdk/features/#graphql-client-integrations $requestData['api_target'] = 'graphql'; $event->setRequest($requestData); } public function handleStartRequest(StartRequest $startRequest): void { $this->previousSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($this->previousSpan === null || !$this->previousSpan->getSampled()) { return; } $context = SpanContext::make() ->setOp('graphql.request') ->setOrigin('auto.graphql.server'); $this->operations = []; $this->requestSpan = $this->previousSpan->startChild($context); $this->operationSpan = null; SentrySdk::getCurrentHub()->setSpan($this->requestSpan); } public function handleStartExecution(StartExecution $startExecution): void { if ($this->requestSpan === null) { return; } if (!$startExecution->query instanceof DocumentNode) { return; } $operationDefinition = $this->extractOperationDefinitionNode($startExecution->query); if ($operationDefinition === null) { return; } $this->operations[] = [$startExecution->operationName ?? null, $operationDefinition]; $this->updateTransactionName(); $context = SpanContext::make() ->setOp("graphql.{$operationDefinition->operation}") ->setOrigin('auto.graphql.server'); $this->operationSpan = $this->requestSpan->startChild($context); SentrySdk::getCurrentHub()->setSpan($this->operationSpan); } public function handleEndExecution(EndExecution $endExecution): void { if ($this->operationSpan === null) { return; } $this->operationSpan->finish(); $this->operationSpan = null; SentrySdk::getCurrentHub()->setSpan($this->requestSpan); } public function handleEndRequest(EndRequest $endRequest): void { if ($this->requestSpan === null) { return; } $this->requestSpan->finish(); $this->requestSpan = null; SentrySdk::getCurrentHub()->setSpan($this->previousSpan); $this->previousSpan = null; $this->operations = []; } private function updateTransactionName(): void { $transaction = SentrySdk::getCurrentHub()->getTransaction(); if ($transaction === null) { return; } $groupedOperations = []; foreach ($this->operations as [$operationName, $operation]) { if (!isset($groupedOperations[$operation->operation])) { $groupedOperations[$operation->operation] = []; } if ($operationName === null || $this->ignoreOperationName) { $groupedOperations[$operation->operation] = array_merge( $groupedOperations[$operation->operation], $this->extractOperationNames($operation) ); } else { $groupedOperations[$operation->operation][] = $operationName; } } if (empty($groupedOperations)) { return; } array_walk($groupedOperations, static function (&$operations, string $operationType) { sort($operations, SORT_STRING); $operations = "{$operationType}{" . implode(',', $operations) . '}'; }); ksort($groupedOperations, SORT_STRING); $transactionName = 'lighthouse?' . implode('&', $groupedOperations); $transaction->setName($transactionName); $transaction->getMetadata()->setSource(TransactionSource::custom()); Integration::setTransaction($transactionName); } /** * @return array */ private function extractOperationNames(OperationDefinitionNode $operation): array { if (!$this->ignoreOperationName && $operation->name !== null) { return [$operation->name->value]; } $selectionSet = []; /** @var \GraphQL\Language\AST\FieldNode $selection */ foreach ($operation->selectionSet->selections as $selection) { // Not respecting aliases because they are only relevant for clients // and the tracing we extract here is targeted at server developers. $selectionSet[] = $selection->name->value; } sort($selectionSet, SORT_STRING); return $selectionSet; } private function extractOperationDefinitionNode(DocumentNode $query): ?OperationDefinitionNode { foreach ($query->definitions as $definition) { if ($definition instanceof OperationDefinitionNode) { return $definition; } } return null; } private function isApplicable(): bool { if (!class_exists(StartRequest::class) || !class_exists(StartExecution::class)) { return false; } return property_exists(StartExecution::class, 'query'); } } PK\sW]ù�;dÝÝ2src/Sentry/Laravel/Tracing/ViewEngineDecorator.phpnu„[µü¤engine = $engine; $this->viewFactory = $viewFactory; } /** * {@inheritdoc} */ public function get($path, array $data = []): string { $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to wrap the engine call if ($parentSpan === null || !$parentSpan->getSampled()) { return $this->engine->get($path, $data); } $span = $parentSpan->startChild( SpanContext::make() ->setOp('view.render') ->setOrigin('auto.view') ->setDescription($this->viewFactory->shared(self::SHARED_KEY, basename($path))) ); SentrySdk::getCurrentHub()->setSpan($span); $result = $this->engine->get($path, $data); $span->finish(); SentrySdk::getCurrentHub()->setSpan($parentSpan); return $result; } public function __call($name, $arguments) { return $this->engine->{$name}(...$arguments); } } PK\sW]͵ ##+src/Sentry/Laravel/Tracing/EventHandler.phpnu„[µü¤ 'routeMatched', DatabaseEvents\QueryExecuted::class => 'queryExecuted', RoutingEvents\ResponsePrepared::class => 'responsePrepared', RoutingEvents\PreparingResponse::class => 'responsePreparing', DatabaseEvents\TransactionBeginning::class => 'transactionBeginning', DatabaseEvents\TransactionCommitted::class => 'transactionCommitted', DatabaseEvents\TransactionRolledBack::class => 'transactionRolledBack', ]; /** * Indicates if we should we add SQL queries as spans. * * @var bool */ private $traceSqlQueries; /** * Indicates if we should add query bindings to query spans. * * @var bool */ private $traceSqlBindings; /** * Indicates if we should we add SQL query origin data to query spans. * * @var bool */ private $traceSqlQueryOrigin; /** * The threshold in milliseconds to consider a SQL query origin. * * @var int */ private $traceSqlQueryOriginTreshHoldMs; /** * Indicates if we should trace queue job spans. * * @var bool */ private $traceQueueJobs; /** * Indicates if we should trace queue jobs as separate transactions. * * @var bool */ private $traceQueueJobsAsTransactions; /** * Hold the stack of parent spans that need to be put back on the scope. * * @var array */ private $parentSpanStack = []; /** * Hold the stack of current spans that need to be finished still. * * @var array */ private $currentSpanStack = []; /** * EventHandler constructor. */ public function __construct(array $config) { $this->traceSqlQueries = ($config['sql_queries'] ?? true) === true; $this->traceSqlBindings = ($config['sql_bindings'] ?? true) === true; $this->traceSqlQueryOrigin = ($config['sql_origin'] ?? true) === true; $this->traceSqlQueryOriginTreshHoldMs = $config['sql_origin_threshold_ms'] ?? 100; $this->traceQueueJobs = ($config['queue_jobs'] ?? false) === true; $this->traceQueueJobsAsTransactions = ($config['queue_job_transactions'] ?? false) === true; } /** * Attach all event handlers. * * @uses self::routeMatchedHandler() * @uses self::queryExecutedHandler() * @uses self::responsePreparedHandler() * @uses self::responsePreparingHandler() * @uses self::transactionBeginningHandler() * @uses self::transactionCommittedHandler() * @uses self::transactionRolledBackHandler() */ public function subscribe(Dispatcher $dispatcher): void { foreach (static::$eventHandlerMap as $eventName => $handler) { $dispatcher->listen($eventName, [$this, $handler]); } } /** * Pass through the event and capture any errors. * * @param string $method * @param array $arguments */ public function __call(string $method, array $arguments) { $handlerMethod = "{$method}Handler"; if (!method_exists($this, $handlerMethod)) { throw new RuntimeException("Missing tracing event handler: {$handlerMethod}"); } try { $this->{$handlerMethod}(...$arguments); } catch (Exception $e) { // Ignore to prevent bubbling up errors in the SDK } } protected function routeMatchedHandler(RoutingEvents\RouteMatched $match): void { $transaction = SentrySdk::getCurrentHub()->getTransaction(); if ($transaction === null) { return; } [$transactionName, $transactionSource] = Integration::extractNameAndSourceForRoute($match->route); $transaction->setName($transactionName); $transaction->getMetadata()->setSource($transactionSource); } protected function queryExecutedHandler(DatabaseEvents\QueryExecuted $query): void { if (!$this->traceSqlQueries) { return; } $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $context = SpanContext::make() ->setOp('db.sql.query') ->setData([ 'db.name' => $query->connection->getDatabaseName(), 'db.system' => $query->connection->getDriverName(), 'server.address' => $query->connection->getConfig('host'), 'server.port' => $query->connection->getConfig('port'), ]) ->setOrigin('auto.db') ->setDescription($query->sql) ->setStartTimestamp(microtime(true) - $query->time / 1000); $context->setEndTimestamp($context->getStartTimestamp() + $query->time / 1000); if ($this->traceSqlBindings) { $context->setData(array_merge($context->getData(), [ 'db.sql.bindings' => $query->bindings ])); } if ($this->traceSqlQueryOrigin && $query->time >= $this->traceSqlQueryOriginTreshHoldMs) { $queryOrigin = $this->resolveEventOrigin(); if ($queryOrigin !== null) { $context->setData(array_merge($context->getData(), $queryOrigin)); } } $parentSpan->startChild($context); } protected function responsePreparedHandler(RoutingEvents\ResponsePrepared $event): void { $span = $this->popSpan(); if ($span !== null) { $span->finish(); } } protected function responsePreparingHandler(RoutingEvents\PreparingResponse $event): void { // If the response is already a Response object there is no need to handle the event anymore // since there isn't going to be any real work going on, the response is already as prepared // as it can be. So we ignore the event to prevent loggin a very short empty duplicated span if ($event->response instanceof Response) { return; } $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('http.route.response') ->setOrigin('auto.http.server') ) ); } protected function transactionBeginningHandler(DatabaseEvents\TransactionBeginning $event): void { $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('db.transaction') ->setOrigin('auto.db') ) ); } protected function transactionCommittedHandler(DatabaseEvents\TransactionCommitted $event): void { $span = $this->popSpan(); if ($span !== null) { $span->setStatus(SpanStatus::ok()); $span->finish(); } } protected function transactionRolledBackHandler(DatabaseEvents\TransactionRolledBack $event): void { $span = $this->popSpan(); if ($span !== null) { $span->setStatus(SpanStatus::internalError()); $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); return array_pop($this->currentSpanStack); } } PK\sW]º¹2uFF2src/Sentry/Laravel/Tracing/TransactionFinisher.phpnu„[µü¤afterResponse() which are terminating // callbacks themselfs just like we do below. // // This class is registered as a singleton in the container to ensure it's only // instantiated once and the terminating callback is only registered once. // // It should be resolved from the container before the terminating callbacks are called. // Good place is in the `terminate` callback of a middleware for example. // This way we can be 99.9% sure to be the last ones to run. app()->terminating(function () { app(Middleware::class)->finishTransaction(); }); } } PK\sW]/âU.å#å#)src/Sentry/Laravel/Tracing/Middleware.phpnu„[µü¤continueAfterResponse = $continueAfterResponse; } /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * * @return mixed */ public function handle(Request $request, Closure $next) { if (app()->bound(HubInterface::class)) { $this->startTransaction($request); } return $next($request); } /** * Handle the application termination. * * @param \Illuminate\Http\Request $request * @param mixed $response * * @return void */ public function terminate(Request $request, $response): void { // If there is no transaction there is nothing for us to do if ($this->transaction === null) { return; } if ($this->shouldRouteBeIgnored($request)) { $this->discardTransaction(); return; } if ($this->appSpan !== null) { $this->appSpan->finish(); $this->appSpan = null; } if ($response instanceof SymfonyResponse) { $this->hydrateResponseData($response); } if ($this->continueAfterResponse) { // Resolving the transaction finisher class will register the terminating callback // which is responsible for calling `finishTransaction`. We have registered the // class as a singleton to keep the state in the container and away from here // this way we ensure the callback is only registered once even for Octane. app(TransactionFinisher::class); } else { $this->finishTransaction(); } } /** * Set the timestamp of application bootstrap completion. * * @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". */ public function setBootedTimestamp(?float $timestamp = null): void { $this->bootedTimestamp = $timestamp ?? microtime(true); } private function startTransaction(Request $request): void { $hub = SentrySdk::getCurrentHub(); // Prevent starting a new transaction if we are already in a transaction if ($hub->getTransaction() !== null) { return; } // Reset our internal state in case we are handling multiple requests (e.g. in Octane) $this->didRouteMatch = false; // Try $_SERVER['REQUEST_TIME_FLOAT'] then LARAVEL_START and fallback to microtime(true) if neither are defined $requestStartTime = $request->server( 'REQUEST_TIME_FLOAT', defined('LARAVEL_START') ? LARAVEL_START : microtime(true) ); $context = continueTrace( $request->header('sentry-trace', ''), $request->header('baggage', '') ); $requestPath = '/' . ltrim($request->path(), '/'); $context->setOp('http.server'); $context->setName($requestPath); $context->setOrigin('auto.http.server'); $context->setSource(TransactionSource::url()); $context->setStartTimestamp($requestStartTime); $context->setData([ 'url' => $requestPath, 'http.request.method' => strtoupper($request->method()), ]); $transaction = $hub->startTransaction($context); $hub->setSpan($transaction); // If this transaction is not sampled, we can stop here to prevent doing work for nothing if (!$transaction->getSampled()) { return; } $this->transaction = $transaction; $bootstrapSpan = $this->addAppBootstrapSpan(); $this->appSpan = $this->transaction->startChild( SpanContext::make() ->setOp('middleware.handle') ->setOrigin('auto.http.server') ->setStartTimestamp($bootstrapSpan ? $bootstrapSpan->getEndTimestamp() : microtime(true)) ); $hub->setSpan($this->appSpan); } private function discardTransaction(): void { $this->appSpan = null; $this->transaction = null; $this->didRouteMatch = false; $this->bootedTimestamp = null; SentrySdk::getCurrentHub()->setSpan(null); } private function addAppBootstrapSpan(): ?Span { if ($this->bootedTimestamp === null) { return null; } $span = $this->transaction->startChild( SpanContext::make() ->setOp('app.bootstrap') ->setOrigin('auto.http.server') ->setStartTimestamp($this->transaction->getStartTimestamp()) ->setEndTimestamp($this->bootedTimestamp) ); // Add more information about the bootstrap section if possible $this->addBootDetailTimeSpans($span); // Consume the booted timestamp, because we don't want to report the boot(strap) spans more than once $this->bootedTimestamp = null; return $span; } private function addBootDetailTimeSpans(Span $bootstrap): void { // This constant should be defined right after the composer `autoload.php` require statement in `public/index.php` // define('SENTRY_AUTOLOAD', microtime(true)); if (!defined('SENTRY_AUTOLOAD') || !SENTRY_AUTOLOAD) { return; } $bootstrap->startChild( SpanContext::make() ->setOp('app.php.autoload') ->setOrigin('auto.http.server') ->setStartTimestamp($this->transaction->getStartTimestamp()) ->setEndTimestamp(SENTRY_AUTOLOAD) ); } private function hydrateResponseData(SymfonyResponse $response): void { $this->transaction->setHttpStatus($response->getStatusCode()); $this->transaction->setData([ 'http.response.status_code' => $response->getStatusCode(), ]); } public function finishTransaction(): void { // We could end up multiple times here since we register a terminating callback so // double check if we have a transaction before trying to finish it since it could // have already been finished in between being registered and being executed again if ($this->transaction === null) { return; } // Make sure we set the transaction and not have a child span in the Sentry SDK // If the transaction is not on the scope during finish, the trace.context is wrong SentrySdk::getCurrentHub()->setSpan($this->transaction); $this->transaction->finish(); $this->transaction = null; } private function internalSignalRouteWasMatched(): void { $this->didRouteMatch = true; } /** * Indicates if the route should be ignored and the transaction discarded. */ private function shouldRouteBeIgnored(Request $request): bool { // Laravel Lumen doesn't use `illuminate/routing`. // Instead we use the route available on the request to detect if a route was matched. if (app() instanceof LumenApplication) { return $request->route() === null && config('sentry.tracing.missing_routes', false) === false; } // If a route has not been matched we ignore unless we are configured to trace missing routes return !$this->didRouteMatch && config('sentry.tracing.missing_routes', false) === false; } public static function signalRouteWasMatched(): void { if (!app()->bound(self::class)) { return; } app(self::class)->internalSignalRouteWasMatched(); } } PK\sW]/äèQÒÒ.src/Sentry/Laravel/Tracing/ServiceProvider.phpnu„[µü¤hasDsnSet() && !$this->hasSpotlightEnabled()) { return; } if (!$this->app instanceof Lumen) { $this->app->booted(function () { $this->app->make(Middleware::class)->setBootedTimestamp(); }); } $tracingConfig = $this->getTracingConfig(); $this->bindEvents($tracingConfig); $this->bindViewEngine($tracingConfig); $this->decorateRoutingDispatchers(); 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) { $httpKernel->prependMiddleware(Middleware::class); } } } public function register(): void { $this->app->singleton(TransactionFinisher::class); $this->app->singleton(Middleware::class, function () { $continueAfterResponse = ($this->getTracingConfig()['continue_after_response'] ?? true) === true; // Lumen introduced the `terminating` method in version 9.1.4. // We check for it's existence and disable the continue after response feature if it's not available. if (!method_exists($this->app, 'terminating')) { $continueAfterResponse = false; } return new Middleware($continueAfterResponse); }); } private function bindEvents(array $tracingConfig): void { $handler = new EventHandler($tracingConfig); try { /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ $dispatcher = $this->app->make(Dispatcher::class); $handler->subscribe($dispatcher); } catch (BindingResolutionException $e) { // If we cannot resolve the event dispatcher we also cannot listen to events } } private function bindViewEngine($tracingConfig): void { if (($tracingConfig['views'] ?? true) !== true) { return; } $viewEngineWrapper = function (EngineResolver $engineResolver): void { foreach (['file', 'php', 'blade'] as $engineName) { try { $realEngine = $engineResolver->resolve($engineName); $engineResolver->register($engineName, function () use ($realEngine) { return $this->wrapViewEngine($realEngine); }); } catch (InvalidArgumentException $e) { // The `file` engine was introduced in Laravel 5.4. On lower Laravel versions // resolving that driver will throw an `InvalidArgumentException`. We can // ignore this exception because we can't wrap drivers that don't exist } } }; if ($this->app->resolved('view.engine.resolver')) { $viewEngineWrapper($this->app->make('view.engine.resolver')); } else { $this->app->afterResolving('view.engine.resolver', $viewEngineWrapper); } } private function wrapViewEngine(Engine $realEngine): Engine { /** @var ViewFactory $viewFactory */ $viewFactory = $this->app->make('view'); $viewFactory->composer('*', static function (View $view) use ($viewFactory): void { $viewFactory->share(ViewEngineDecorator::SHARED_KEY, $view->name()); }); return new ViewEngineDecorator($realEngine, $viewFactory); } private function getTracingConfig(): array { return $this->getUserConfig()['tracing'] ?? []; } 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); }); } } PK\sW]eãrGG2src/Sentry/Laravel/Http/SetRequestIpMiddleware.phpnu„[µü¤bound(HubInterface::class)) { /** @var \Sentry\State\HubInterface $sentry */ $sentry = $container->make(HubInterface::class); $client = $sentry->getClient(); if ($client !== null && $client->getOptions()->shouldSendDefaultPii()) { $sentry->configureScope(static function (Scope $scope) use ($request): void { $scope->setUser([ 'ip_address' => $request->ip(), ]); }); } } return $next($request); } } PK\sW]Ý[©Ïãã0src/Sentry/Laravel/Http/SetRequestMiddleware.phpnu„[µü¤bound(HubInterface::class)) { $psrRequest = $this->resolvePsrRequest($container); if ($psrRequest !== null) { $container->instance(LaravelRequestFetcher::CONTAINER_PSR7_INSTANCE_KEY, $psrRequest); } } return $next($request); } private function resolvePsrRequest(Container $container): ?ServerRequestInterface { try { return $container->make(ServerRequestInterface::class); } catch (Throwable $e) { // Do not crash if there is an exception thrown while resolving the request object } return null; } } PK\sW]í£øÀll1src/Sentry/Laravel/Http/FlushEventsMiddleware.phpnu„[µü¤bound('request')) { return null; } if ($container->bound(self::CONTAINER_PSR7_INSTANCE_KEY)) { $request = $container->make(self::CONTAINER_PSR7_INSTANCE_KEY); } else { $request = (new RequestFetcher)->fetchRequest(); } if ($request === null) { return null; } $cookies = new Collection($request->getCookieParams()); // We need to filter out the cookies that are not allowed to be sent to Sentry because they are very sensitive $forbiddenCookies = [config('session.cookie'), 'remember_*', 'XSRF-TOKEN']; return $request->withCookieParams( $cookies->map(function ($value, string $key) use ($forbiddenCookies) { if (Str::is($forbiddenCookies, $key)) { return '[Filtered]'; } return $value; })->all() ); } } PK\sW]‰Õ½ÂDD*src/Sentry/Laravel/BaseServiceProvider.phpnu„[µü¤getUserConfig(); return !empty($config['dsn']); } /** * Check if Spotlight was enabled in the config. * * @return bool */ protected function hasSpotlightEnabled(): bool { $config = $this->getUserConfig(); return ($config['spotlight'] ?? false) === true; } /** * Retrieve the user configuration. * * @return array */ protected function getUserConfig(): array { $config = $this->app['config'][static::$abstract]; return empty($config) ? [] : $config; } /** * Checks if the config is set in such a way that performance tracing could be enabled. * * Because of `traces_sampler` being dynamic we can never be 100% confident but that is also not important. * * @deprecated since version 4.6. To be removed in version 5.0. * * @return bool */ protected function couldHavePerformanceTracingEnabled(): bool { $config = $this->getUserConfig(); return !empty($config['traces_sample_rate']) || !empty($config['traces_sampler']) || ($config['spotlight'] ?? false) === true; } } PK\sW]Κff<src/Sentry/Laravel/Integration/LaravelContextIntegration.phpnu„[µü¤getIntegration(self::class); if (!$self instanceof self) { return $event; } if (!in_array($event->getType(), [EventType::event(), EventType::transaction()], true)) { return $event; } $event->setContext('laravel', app(ContextRepository::class)->all()); return $event; }); } } PK\sW]°’ õóóVsrc/Sentry/Laravel/Integration/ModelViolations/DiscardedAttributeViolationReporter.phpnu„[µü¤ $property, 'kind' => 'discarded_attribute', ]; } protected function getViolationException(Model $model, string $property): Exception { return new MassAssignmentException(sprintf( 'Add [%s] to fillable property to allow mass assignment on [%s].', $property, get_class($model) )); } } PK\sW]XÝ™1vvTsrc/Sentry/Laravel/Integration/ModelViolations/LazyLoadingModelViolationReporter.phpnu„[µü¤exists || $model->wasRecentlyCreated) { return false; } return parent::shouldReport($model, $property); } protected function getViolationContext(Model $model, string $property): array { return [ 'relation' => $property, 'kind' => 'lazy_loading', ]; } protected function getViolationException(Model $model, string $property): Exception { return new LazyLoadingViolationException($model, $property); } } PK\sW]ãý(ѳ ³ Isrc/Sentry/Laravel/Integration/ModelViolations/ModelViolationReporter.phpnu„[µü¤ $reportedViolations */ private $reportedViolations = []; public function __construct(?callable $callback, bool $suppressDuplicateReports, bool $reportAfterResponse) { $this->callback = $callback; $this->suppressDuplicateReports = $suppressDuplicateReports; $this->reportAfterResponse = $reportAfterResponse; } /** @param string|array $propertyOrProperties */ public function __invoke(Model $model, $propertyOrProperties): void { $property = is_array($propertyOrProperties) ? implode(', ', $propertyOrProperties) : $propertyOrProperties; if (!$this->shouldReport($model, $property)) { return; } $this->markAsReported($model, $property); $origin = $this->resolveEventOrigin(); if ($this->reportAfterResponse) { app()->terminating(function () use ($model, $property, $origin) { $this->report($model, $property, $origin); }); } else { $this->report($model, $property, $origin); } } abstract protected function getViolationContext(Model $model, string $property): array; abstract protected function getViolationException(Model $model, string $property): Exception; protected function shouldReport(Model $model, string $property): bool { if (!$this->suppressDuplicateReports) { return true; } return !array_key_exists(get_class($model) . $property, $this->reportedViolations); } protected function markAsReported(Model $model, string $property): void { if (!$this->suppressDuplicateReports) { return; } $this->reportedViolations[get_class($model) . $property] = true; } private function report(Model $model, string $property, $origin): void { SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($model, $property, $origin) { $scope->setContext('violation', array_merge([ 'model' => get_class($model), 'origin' => $origin, ], $this->getViolationContext($model, $property))); SentrySdk::getCurrentHub()->captureEvent( tap(Event::createEvent(), static function (Event $event) { $event->setLevel(Severity::warning()); }), EventHint::fromArray([ 'exception' => $this->getViolationException($model, $property), 'mechanism' => new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true), ]) ); }); // Forward the violation to the next handler if there is one if ($this->callback !== null) { call_user_func($this->callback, $model, $property); } } } PK\sW]*«\ssYsrc/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.phpnu„[µü¤ $property, 'kind' => 'missing_attribute', ]; } protected function getViolationException(Model $model, string $property): Exception { return new MissingAttributeException($model, $property); } } PK\sW]ꜗNèè>src/Sentry/Laravel/Integration/ExceptionContextIntegration.phpnu„[µü¤getIntegration(self::class); if (!$self instanceof self) { return $event; } if ($hint === null || $hint->exception === null) { return $event; } if (!method_exists($hint->exception, 'context')) { return $event; } $context = $hint->exception->context(); if (is_array($context)) { $event->setExtra(['exception_context' => $context]); } return $event; }); } } PK\sW]˜ù;ÇFÇF&src/Sentry/Laravel/ServiceProvider.phpnu„[µü¤app->make(HubInterface::class); $this->bootFeatures(); // Only register if a DSN is set or Spotlight is enabled // No events can be sent without a DSN set or Spotlight enabled if ($this->hasDsnSet() || $this->hasSpotlightEnabled()) { $this->bindEvents(); if ($this->app instanceof Lumen) { $this->app->middleware(SetRequestMiddleware::class); $this->app->middleware(SetRequestIpMiddleware::class); $this->app->middleware(FlushEventsMiddleware::class); } elseif ($this->app->bound(HttpKernelInterface::class)) { $httpKernel = $this->app->make(HttpKernelInterface::class); if ($httpKernel instanceof HttpKernel) { $httpKernel->pushMiddleware(SetRequestMiddleware::class); $httpKernel->pushMiddleware(SetRequestIpMiddleware::class); $httpKernel->pushMiddleware(FlushEventsMiddleware::class); } } } if ($this->app->runningInConsole()) { if ($this->app instanceof Laravel) { $this->publishes([ __DIR__ . '/../../../config/sentry.php' => config_path(static::$abstract . '.php'), ], 'sentry-config'); } $this->registerArtisanCommands(); } $this->registerAboutCommandIntegration(); } /** * Register the service provider. */ public function register(): void { if ($this->app instanceof Lumen) { $this->app->configure(static::$abstract); } $this->mergeConfigFrom(__DIR__ . '/../../../config/sentry.php', static::$abstract); $this->app->singleton(DebugFileLogger::class, function () { return new DebugFileLogger(storage_path('logs/sentry.log')); }); $this->configureAndRegisterClient(); $this->registerFeatures(); $this->registerLogChannels(); } /** * Bind to the Laravel event dispatcher to log events. */ protected function bindEvents(): void { $userConfig = $this->getUserConfig(); $handler = new EventHandler($this->app, $userConfig); try { /** @var \Illuminate\Contracts\Events\Dispatcher $dispatcher */ $dispatcher = $this->app->make(Dispatcher::class); $handler->subscribe($dispatcher); if ($this->app->bound('octane')) { $handler->subscribeOctaneEvents($dispatcher); } if (isset($userConfig['send_default_pii']) && $userConfig['send_default_pii'] !== false) { $handler->subscribeAuthEvents($dispatcher); } if (isset($userConfig['enable_logs']) && $userConfig['enable_logs'] === true && method_exists($this->app, 'terminating')) { // Listen to the terminating event to flush the logs before the application ends // This ensures that all logs are sent to Sentry even if the application ends unexpectedly // We need to check for method existence here for Lumen since this method was only introduced in Lumen 9.1.4 $this->app->terminating(static function () { Logs::getInstance()->flush(); }); } } catch (BindingResolutionException $e) { // If we cannot resolve the event dispatcher we also cannot listen to events } } /** * Bind and register all the features. */ protected function registerFeatures(): void { // Register all the features as singletons, so there is only one instance of each feature in the application foreach (self::FEATURES as $feature) { $this->app->singleton($feature); } foreach (self::FEATURES as $feature) { try { /** @var Feature $featureInstance */ $featureInstance = $this->app->make($feature); $featureInstance->register(); } catch (Throwable $e) { // Ensure that features do not break the whole application } } } /** * Register the log channels. */ protected function registerLogChannels(): void { $config = $this->app->make(Repository::class); $logChannels = $config->get('logging.channels', []); if (!array_key_exists('sentry', $logChannels)) { $config->set('logging.channels.sentry', [ 'driver' => 'sentry', ]); } if (!array_key_exists('sentry_logs', $logChannels)) { $config->set('logging.channels.sentry_logs', [ 'driver' => 'sentry_logs', 'level' => $config->get('sentry.logs_channel_level', 'debug'), ]); } } /** * Boot all the features. */ protected function bootFeatures(): void { // Only register if a DSN is set or Spotlight is enabled // No events can be sent without a DSN set or Spotlight enabled $bootActive = $this->hasDsnSet() || $this->hasSpotlightEnabled(); foreach (self::FEATURES as $feature) { try { /** @var Feature $featureInstance */ $featureInstance = $this->app->make($feature); $bootActive ? $featureInstance->boot() : $featureInstance->bootInactive(); } catch (Throwable $e) { // Ensure that features do not break the whole application } } } /** * Register the artisan commands. */ protected function registerArtisanCommands(): void { $this->commands([ TestCommand::class, PublishCommand::class, ]); } /** * Register the `php artisan about` command integration. */ protected function registerAboutCommandIntegration(): void { // The about command is only available in Laravel 9 and up so we need to check if it's available to us if (!class_exists(AboutCommand::class)) { return; } AboutCommand::add('Sentry', AboutCommandIntegration::class); } /** * Configure and register the Sentry client with the container. */ protected function configureAndRegisterClient(): void { $this->app->bind(ClientBuilder::class, function () { $basePath = base_path(); $userConfig = $this->getUserConfig(); foreach (static::LARAVEL_SPECIFIC_OPTIONS as $laravelSpecificOptionName) { unset($userConfig[$laravelSpecificOptionName]); } $options = \array_merge( [ 'prefixes' => [$basePath], 'in_app_exclude' => [ "{$basePath}/vendor", "{$basePath}/artisan", ], ], $userConfig ); // When we get no environment from the (user) configuration we default to the Laravel environment if (empty($options['environment'])) { $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); } foreach (self::OPTIONS_TO_RESOLVE_FROM_CONTAINER as $option) { if (isset($options[$option]) && is_string($options[$option])) { $options[$option] = $this->app->make($options[$option]); } } $clientBuilder = ClientBuilder::create($options); // Set the Laravel SDK identifier and version $clientBuilder->setSdkIdentifier(Version::SDK_IDENTIFIER); $clientBuilder->setSdkVersion(Version::SDK_VERSION); return $clientBuilder; }); $this->app->singleton(HubInterface::class, function () { /** @var \Sentry\ClientBuilder $clientBuilder */ $clientBuilder = $this->app->make(ClientBuilder::class); $options = $clientBuilder->getOptions(); $userConfig = $this->getUserConfig(); /** @var array|callable $userConfig */ $userIntegrationOption = $userConfig['integrations'] ?? []; $userIntegrations = $this->resolveIntegrationsFromUserConfig( \is_array($userIntegrationOption) ? $userIntegrationOption : [], $userConfig['tracing']['default_integrations'] ?? true ); $options->setIntegrations(static function (array $integrations) use ($options, $userIntegrations, $userIntegrationOption): array { 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 // `ExceptionHandler` of their application or through the log channel integration to Sentry $integrations = array_filter($integrations, static function (SdkIntegration\IntegrationInterface $integration): bool { if ($integration instanceof SdkIntegration\ErrorListenerIntegration) { return false; } if ($integration instanceof SdkIntegration\ExceptionListenerIntegration) { return false; } if ($integration instanceof SdkIntegration\FatalErrorListenerIntegration) { return false; } // We also remove the default request integration so it can be readded // after with a Laravel specific request fetcher. This way we can resolve // the request from Laravel instead of constructing it from the global state if ($integration instanceof SdkIntegration\RequestIntegration) { return false; } return true; }); $integrations[] = new SdkIntegration\RequestIntegration( new LaravelRequestFetcher ); } $integrations = array_merge( $integrations, [ new Integration, new Integration\LaravelContextIntegration, new Integration\ExceptionContextIntegration, ], $userIntegrations ); if (\is_callable($userIntegrationOption)) { return $userIntegrationOption($integrations); } return $integrations; }); $hub = new Hub($clientBuilder->getClient()); SentrySdk::setCurrentHub($hub); return $hub; }); $this->app->alias(HubInterface::class, static::$abstract); $this->app->singleton(BacktraceHelper::class, function () { $sentry = $this->app->make(HubInterface::class); $options = $sentry->getClient()->getOptions(); return new BacktraceHelper($options, new RepresentationSerializer($options)); }); } /** * Resolve the integrations from the user configuration with the container. */ private function resolveIntegrationsFromUserConfig(array $userIntegrations, bool $enableDefaultTracingIntegrations): array { $integrationsToResolve = $userIntegrations; if ($enableDefaultTracingIntegrations) { $integrationsToResolve = array_merge($integrationsToResolve, TracingServiceProvider::DEFAULT_INTEGRATIONS); } $integrations = []; foreach ($integrationsToResolve as $userIntegration) { if ($userIntegration instanceof SdkIntegration\IntegrationInterface) { $integrations[] = $userIntegration; } elseif (\is_string($userIntegration)) { $resolvedIntegration = $this->app->make($userIntegration); if (!$resolvedIntegration instanceof SdkIntegration\IntegrationInterface) { throw new RuntimeException( sprintf( 'Sentry integrations must be an instance of `%s` got `%s`.', SdkIntegration\IntegrationInterface::class, get_class($resolvedIntegration) ) ); } $integrations[] = $resolvedIntegration; } else { throw new RuntimeException( sprintf( 'Sentry integrations must either be a valid container reference or an instance of `%s`.', SdkIntegration\IntegrationInterface::class ) ); } } return $integrations; } /** * Get the services provided by the provider. * * @return array */ public function provides() { return [static::$abstract]; } } PK\sW]Äø ²$²$0src/Sentry/Laravel/Features/QueueIntegration.phpnu„[µü¤container()->bound('queue')) { return false; } return $this->isBreadcrumbFeatureEnabled('queue_info') || $this->isTracingFeatureEnabled('queue_jobs') || $this->isTracingFeatureEnabled('queue_job_transactions'); } public function onBoot(Dispatcher $events): void { $events->listen(JobQueueing::class, [$this, 'handleJobQueueingEvent']); $events->listen(JobQueued::class, [$this, 'handleJobQueuedEvent']); $events->listen(JobProcessed::class, [$this, 'handleJobProcessedQueueEvent']); $events->listen(JobProcessing::class, [$this, 'handleJobProcessingQueueEvent']); $events->listen(WorkerStopping::class, [$this, 'handleWorkerStoppingQueueEvent']); $events->listen(JobExceptionOccurred::class, [$this, 'handleJobExceptionOccurredQueueEvent']); if ($this->isTracingFeatureEnabled('queue_jobs') || $this->isTracingFeatureEnabled('queue_job_transactions')) { Queue::createPayloadUsing(function (?string $connection, ?string $queue, ?array $payload): ?array { $parentSpan = SentrySdk::getCurrentHub()->getSpan(); if ($parentSpan !== null && $parentSpan->getSampled()) { $context = (new SpanContext) ->setOp(self::QUEUE_SPAN_OP_QUEUE_PUBLISH) ->setData([ 'messaging.system' => 'laravel', 'messaging.message.id' => $payload['uuid'] ?? null, 'messaging.destination.name' => $this->normalizeQueueName($queue), 'messaging.destination.connection' => $connection, ]) ->setDescription($queue); $this->pushSpan($parentSpan->startChild($context)); } if ($payload !== null) { $payload[self::QUEUE_PAYLOAD_BAGGAGE_DATA] = getBaggage(); $payload[self::QUEUE_PAYLOAD_TRACE_PARENT_DATA] = getTraceparent(); $payload[self::QUEUE_PAYLOAD_PUBLISH_TIME] = microtime(true); } return $payload; }); } } public function handleJobQueueingEvent(JobQueueing $event): void { $currentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no tracing span active there is no need to handle the event if ($currentSpan === null || $currentSpan->getOp() !== self::QUEUE_SPAN_OP_QUEUE_PUBLISH) { return; } $jobName = $event->job; if ($jobName instanceof Closure) { $jobName = 'Closure'; } elseif (is_object($jobName)) { $jobName = get_class($jobName); } $currentSpan ->setDescription($jobName); } public function handleJobQueuedEvent(JobQueued $event): void { $this->maybeFinishSpan(); } public function handleJobProcessedQueueEvent(JobProcessed $event): void { $this->maybeFinishSpan(SpanStatus::ok()); $this->maybePopScope(); } public function handleJobProcessingQueueEvent(JobProcessing $event): void { $this->maybePopScope(); $this->pushScope(); if ($this->isBreadcrumbFeatureEnabled('queue_info')) { $job = [ 'job' => $event->job->getName(), 'queue' => $event->job->getQueue(), 'attempts' => $event->job->attempts(), 'connection' => $event->connectionName, ]; // Resolve name exists only from Laravel 5.3+ if (method_exists($event->job, 'resolveName')) { $job['resolved'] = $event->job->resolveName(); } Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'queue.job', 'Processing queue job', $job )); } $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->isTracingFeatureEnabled('queue_job_transactions')) { return; } // If there is a parent span we can record the job as a child unless the parent is not sample or we are configured to not do so if ($parentSpan !== null && (!$parentSpan->getSampled() || !$this->isTracingFeatureEnabled('queue_jobs'))) { return; } $jobPayload = $event->job->payload(); if ($parentSpan === null) { $baggage = $jobPayload[self::QUEUE_PAYLOAD_BAGGAGE_DATA] ?? null; $traceParent = $jobPayload[self::QUEUE_PAYLOAD_TRACE_PARENT_DATA] ?? null; $context = continueTrace($traceParent ?? '', $baggage ?? ''); // If the parent transaction was not sampled we also stop the queue job from being recorded if ($context->getParentSampled() === false) { return; } } else { $context = new SpanContext; } $resolvedJobName = $event->job->resolveName(); $jobPublishedAt = $jobPayload[self::QUEUE_PAYLOAD_PUBLISH_TIME] ?? null; $job = [ 'messaging.system' => 'laravel', 'messaging.destination.name' => $this->normalizeQueueName($event->job->getQueue()), 'messaging.destination.connection' => $event->connectionName, 'messaging.message.id' => $jobPayload['uuid'] ?? null, 'messaging.message.envelope.size' => strlen($event->job->getRawBody()), 'messaging.message.body.size' => strlen(json_encode($jobPayload['data'] ?? [])), 'messaging.message.retry.count' => $event->job->attempts() - 1, 'messaging.message.receive.latency' => $jobPublishedAt !== null ? microtime(true) - $jobPublishedAt : null, ]; if ($context instanceof TransactionContext) { $context->setName($resolvedJobName); $context->setSource(TransactionSource::task()); } $context->setOp('queue.process'); $context->setData($job); $context->setOrigin('auto.queue'); $context->setStartTimestamp(microtime(true)); // When the parent span is null we start a new transaction otherwise we start a child of the current span if ($parentSpan === null) { $span = SentrySdk::getCurrentHub()->startTransaction($context); } else { $span = $parentSpan->startChild($context); } $this->pushSpan($span); } public function handleWorkerStoppingQueueEvent(WorkerStopping $event): void { Integration::flushEvents(); } public function handleJobExceptionOccurredQueueEvent(JobExceptionOccurred $event): void { $this->maybeFinishSpan(SpanStatus::internalError()); Integration::flushEvents(); } private function normalizeQueueName(?string $queue): string { if ($queue === null) { return ''; } // SQS queues are sometimes formatted like: https://sqs..amazonaws.com// if (filter_var($queue, FILTER_VALIDATE_URL) !== false) { return Str::afterLast($queue, '/'); } // Jobs pushed onto the Redis driver are formatted as queues: return Str::after($queue, 'queues:'); } protected function pushScope(): void { $this->pushScopeTrait(); // When a job starts, we want to make sure the scope is cleared of breadcrumbs // as well as setting a new propagation context. SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) { $scope->clearBreadcrumbs(); $scope->setPropagationContext(PropagationContext::fromDefaults()); }); } } PK\sW]Ñ­(œ œ 2src/Sentry/Laravel/Features/ConsoleIntegration.phpnu„[µü¤listen(ConsoleEvents\CommandStarting::class, [$this, 'commandStarting']); $events->listen(ConsoleEvents\CommandFinished::class, [$this, 'commandFinished']); } public function commandStarting(ConsoleEvents\CommandStarting $event): void { if (!$event->command) { return; } Integration::configureScope(static function (Scope $scope) use ($event): void { $scope->setTag('command', $event->command); }); if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'artisan.command', 'Starting Artisan command: ' . $event->command, [ 'input' => $this->extractConsoleCommandInput($event->input), ] )); } } public function commandFinished(ConsoleEvents\CommandFinished $event): void { if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'artisan.command', 'Finished Artisan command: ' . $event->command, [ 'exit' => $event->exitCode, 'input' => $this->extractConsoleCommandInput($event->input), ] )); } // Flush any and all events that were possibly generated by the command Integration::flushEvents(); Integration::configureScope(static function (Scope $scope): void { $scope->removeTag('command'); }); } /** * Extract the command input arguments if possible. * * @param \Symfony\Component\Console\Input\InputInterface|null $input * * @return string|null */ private function extractConsoleCommandInput(?InputInterface $input): ?string { if ($input instanceof ArgvInput) { return (string)$input; } return null; } } PK\sW]›}ËhÊÊ5src/Sentry/Laravel/Features/HttpClientIntegration.phpnu„[µü¤isTracingFeatureEnabled(self::FEATURE_KEY) || $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); } public function onBoot(Dispatcher $events, Factory $factory): void { if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $events->listen(RequestSending::class, [$this, 'handleRequestSendingHandlerForTracing']); $events->listen(ResponseReceived::class, [$this, 'handleResponseReceivedHandlerForTracing']); $events->listen(ConnectionFailed::class, [$this, 'handleConnectionFailedHandlerForTracing']); // The `globalRequestMiddleware` functionality was introduced in Laravel 10.14 if (method_exists($factory, 'globalRequestMiddleware')) { $factory->globalRequestMiddleware([$this, 'attachTracingHeadersToRequest']); } } if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { $events->listen(ResponseReceived::class, [$this, 'handleResponseReceivedHandlerForBreadcrumb']); $events->listen(ConnectionFailed::class, [$this, 'handleConnectionFailedHandlerForBreadcrumb']); } } public function attachTracingHeadersToRequest(RequestInterface $request) { if ($this->shouldAttachTracingHeaders($request)) { return $request ->withHeader('baggage', getBaggage()) ->withHeader('sentry-trace', getTraceparent()); } return $request; } public function handleRequestSendingHandlerForTracing(RequestSending $event): void { $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $fullUri = $this->getFullUri($event->request->url()); $partialUri = $this->getPartialUri($fullUri); $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('http.client') ->setData([ 'url' => $partialUri, // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http 'http.query' => $fullUri->getQuery(), 'http.fragment' => $fullUri->getFragment(), 'http.request.method' => $event->request->method(), 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), ]) ->setOrigin('auto.http.client') ->setDescription($event->request->method() . ' ' . $partialUri) ) ); } public function handleResponseReceivedHandlerForTracing(ResponseReceived $event): void { $span = $this->maybePopSpan(); if ($span !== null) { $span->setData(array_merge($span->getData(), [ // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http 'http.response.status_code' => $event->response->status(), 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), ])); $span->setHttpStatus($event->response->status()); $span->finish(); } } public function handleConnectionFailedHandlerForTracing(ConnectionFailed $event): void { $this->maybeFinishSpan(SpanStatus::internalError()); } public function handleResponseReceivedHandlerForBreadcrumb(ResponseReceived $event): void { $level = Breadcrumb::LEVEL_INFO; if ($event->response->clientError()) { $level = Breadcrumb::LEVEL_WARNING; } elseif ($event->response->serverError()) { $level = Breadcrumb::LEVEL_ERROR; } $fullUri = $this->getFullUri($event->request->url()); Integration::addBreadcrumb(new Breadcrumb( $level, Breadcrumb::TYPE_HTTP, 'http', null, [ 'url' => $this->getPartialUri($fullUri), // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http 'http.query' => $fullUri->getQuery(), 'http.fragment' => $fullUri->getFragment(), 'http.request.method' => $event->request->method(), 'http.response.status_code' => $event->response->status(), 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), 'http.response.body.size' => $event->response->toPsrResponse()->getBody()->getSize(), ] )); } public function handleConnectionFailedHandlerForBreadcrumb(ConnectionFailed $event): void { $fullUri = $this->getFullUri($event->request->url()); Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_HTTP, 'http', null, [ 'url' => $this->getPartialUri($fullUri), // See: https://develop.sentry.dev/sdk/performance/span-data-conventions/#http 'http.query' => $fullUri->getQuery(), 'http.fragment' => $fullUri->getFragment(), 'http.request.method' => $event->request->method(), 'http.request.body.size' => $event->request->toPsrRequest()->getBody()->getSize(), ] )); } /** * Construct a full URI. * * @param string $url * * @return UriInterface */ private function getFullUri(string $url): UriInterface { return new Uri($url); } /** * Construct a partial URI, excluding the authority, query and fragment parts. * * @param UriInterface $uri * * @return string */ private function getPartialUri(UriInterface $uri): string { return (string)Uri::fromParts([ 'scheme' => $uri->getScheme(), 'host' => $uri->getHost(), 'port' => $uri->getPort(), 'path' => $uri->getPath(), ]); } private function shouldAttachTracingHeaders(RequestInterface $request): bool { $client = SentrySdk::getCurrentHub()->getClient(); if ($client === null) { return false; } $sdkOptions = $client->getOptions(); // Check if the request destination is allow listed in the trace_propagation_targets option. return $sdkOptions->getTracePropagationTargets() === null || in_array($request->getUri()->getHost(), $sdkOptions->getTracePropagationTargets()); } } PK\sW])?#€€Csrc/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.phpnu„[µü¤ */ private $parentSpanStack = []; /** * Hold the stack of current spans that need to be finished still. * * @var array */ private $currentSpanStack = []; protected function pushSpan(Span $span): void { $hub = SentrySdk::getCurrentHub(); $this->parentSpanStack[] = $hub->getSpan(); $hub->setSpan($span); $this->currentSpanStack[] = $span; } protected function pushScope(): void { SentrySdk::getCurrentHub()->pushScope(); ++$this->pushedScopeCount; } protected function maybePopSpan(): ?Span { if (count($this->currentSpanStack) === 0) { return null; } $parent = array_pop($this->parentSpanStack); SentrySdk::getCurrentHub()->setSpan($parent); return array_pop($this->currentSpanStack); } protected function maybePopScope(): void { Integration::flushEvents(); if ($this->pushedScopeCount === 0) { return; } SentrySdk::getCurrentHub()->popScope(); --$this->pushedScopeCount; } protected function maybeFinishSpan(?SpanStatus $status = null): ?Span { $span = $this->maybePopSpan(); if ($span === null) { return null; } if ($status !== null) { $span->setStatus($status); } $span->finish(); return $span; } } PK\sW]As/B7src/Sentry/Laravel/Features/Concerns/WorksWithSpans.phpnu„[µü¤getSpan(); // If the span is not available or not sampled we don't need to do anything if ($parentSpan === null || !$parentSpan->getSampled()) { return null; } return $parentSpan; } /** @param callable(Span $parentSpan): void $callback */ protected function withParentSpanIfSampled(callable $callback): void { $parentSpan = $this->getParentSpanIfSampled(); if ($parentSpan === null) { return; } $callback($parentSpan); } } PK\sW]ÒYö*¦¦<src/Sentry/Laravel/Features/Concerns/ResolvesEventOrigin.phpnu„[µü¤makeBacktraceHelper(); // We limit the backtrace to 20 frames to prevent too much overhead and we'd reasonable expect the origin to be within the first 20 frames $firstAppFrame = $backtraceHelper->findFirstInAppFrameForBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20)); if ($firstAppFrame === null) { return null; } $filePath = $backtraceHelper->getOriginalViewPathForFrameOfCompiledViewPath($firstAppFrame) ?? $firstAppFrame->getFile(); return [ 'code.filepath' => $filePath, 'code.function' => $firstAppFrame->getFunctionName(), 'code.lineno' => $firstAppFrame->getLine(), ]; } protected function resolveEventOriginAsString(): ?string { $origin = $this->resolveEventOrigin(); if ($origin === null) { return null; } return "{$origin['code.filepath']}:{$origin['code.lineno']}"; } private function makeBacktraceHelper(): BacktraceHelper { return $this->container()->make(BacktraceHelper::class); } } PK\sW]‘.l€OO'src/Sentry/Laravel/Features/Feature.phpnu„[µü¤ */ private $isTracingFeatureEnabled = []; /** * In-memory cache for the breadcumb feature flag. * * @var array */ private $isBreadcrumbFeatureEnabled = []; /** * @param Container $container The Laravel application container. */ public function __construct(Container $container) { $this->container = $container; } /** * Indicates if the feature is applicable to the current environment. * * @return bool */ abstract public function isApplicable(): bool; /** * Register the feature in the environment. */ public function register(): void { // ... } /** * Initialize the feature. */ public function boot(): void { if (method_exists($this, 'onBoot') && $this->isApplicable()) { try { $this->container->call([$this, 'onBoot']); } catch (Throwable $exception) { // If the feature setup fails, we don't want to prevent the rest of the SDK from working. } } } /** * Initialize the feature in an inactive state (when no DSN was set). */ public function bootInactive(): void { if (method_exists($this, 'onBootInactive') && $this->isApplicable()) { try { $this->container->call([$this, 'onBootInactive']); } catch (Throwable $exception) { // If the feature setup fails, we don't want to prevent the rest of the SDK from working. } } } /** * Retrieve the Laravel application container. * * @return Container */ protected function container(): Container { return $this->container; } /** * Retrieve the user configuration. * * @return array */ protected function getUserConfig(): array { $config = $this->container['config'][BaseServiceProvider::$abstract]; return empty($config) ? [] : $config; } /** * Should default PII be sent by default. */ protected function shouldSendDefaultPii(): bool { $client = SentrySdk::getCurrentHub()->getClient(); if ($client === null) { return false; } return $client->getOptions()->shouldSendDefaultPii(); } /** * Indicates if the given feature is enabled for tracing. */ protected function isTracingFeatureEnabled(string $feature, bool $default = true): bool { if (!array_key_exists($feature, $this->isTracingFeatureEnabled)) { $this->isTracingFeatureEnabled[$feature] = $this->isFeatureEnabled('tracing', $feature, $default); } return $this->isTracingFeatureEnabled[$feature]; } /** * Indicates if the given feature is enabled for breadcrumbs. */ protected function isBreadcrumbFeatureEnabled(string $feature, bool $default = true): bool { if (!array_key_exists($feature, $this->isBreadcrumbFeatureEnabled)) { $this->isBreadcrumbFeatureEnabled[$feature] = $this->isFeatureEnabled('breadcrumbs', $feature, $default); } return $this->isBreadcrumbFeatureEnabled[$feature]; } /** * Helper to test if a certain feature is enabled in the user config. */ private function isFeatureEnabled(string $category, string $feature, bool $default): bool { $config = $this->getUserConfig()[$category] ?? []; return ($config[$feature] ?? $default) === true; } } PK\sW]×ó++:src/Sentry/Laravel/Features/LivewirePackageIntegration.phpnu„[µü¤isTracingFeatureEnabled(self::FEATURE_KEY) || $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); } public function onBoot(LivewireManager $livewireManager): void { if (class_exists(EventBus::class)) { $this->registerLivewireThreeEventListeners($livewireManager); return; } $this->registerLivewireTwoEventListeners($livewireManager); } private function registerLivewireThreeEventListeners(LivewireManager $livewireManager): void { $livewireManager->listen('mount', function (Component $component, array $data) { if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $this->handleComponentBoot($component); } if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { $this->handleComponentMount($component, $data); } }); $livewireManager->listen('hydrate', function (Component $component) { if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $this->handleComponentBoot($component); } if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { $this->handleComponentHydrate($component); } }); if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $livewireManager->listen('dehydrate', [$this, 'handleComponentDehydrate']); } if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { $livewireManager->listen('call', [$this, 'handleComponentCall']); } } private function registerLivewireTwoEventListeners(LivewireManager $livewireManager): void { $livewireManager->listen('component.booted', [$this, 'handleComponentBooted']); if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $livewireManager->listen('component.boot', function ($component) { $this->handleComponentBoot($component); }); $livewireManager->listen('component.dehydrate', [$this, 'handleComponentDehydrate']); } if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { $livewireManager->listen('component.mount', [$this, 'handleComponentMount']); } } public function handleComponentCall(Component $component, string $method, array $arguments): void { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'livewire', "Component call: {$component->getName()}::{$method}", $this->mapCallArgumentsToMethodParameters($component, $method, $arguments) ?? ['arguments' => $arguments] )); } public function handleComponentBoot(Component $component, ?string $method = null): void { if ($this->isLivewireRequest()) { $this->updateTransactionName($component->getName()); } $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('ui.livewire.component') ->setOrigin('auto.laravel.livewire') ->setDescription( empty($method) ? $component->getName() : "{$component->getName()}::{$method}" ) ) ); } public function handleComponentMount(Component $component, array $data): void { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'livewire', "Component mount: {$component->getName()}", $data )); } public function handleComponentBooted(Component $component, Request $request): void { if (!$this->isLivewireRequest()) { return; } if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'livewire', "Component booted: {$component->getName()}", ['updates' => $request->updates] )); } if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $this->updateTransactionName($component->getName()); } } public function handleComponentHydrate(Component $component): void { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'livewire', "Component hydrate: {$component->getName()}", $component->all() )); } public function handleComponentDehydrate(Component $component): void { $span = $this->maybeFinishSpan(); } private function updateTransactionName(string $componentName): void { $transaction = SentrySdk::getCurrentHub()->getTransaction(); if ($transaction === null) { return; } $transactionName = "livewire?component={$componentName}"; $transaction->setName($transactionName); $transaction->getMetadata()->setSource(TransactionSource::custom()); Integration::setTransaction($transactionName); } private function isLivewireRequest(): bool { try { /** @var \Illuminate\Http\Request $request */ $request = $this->container()->make('request'); if ($request === null) { return false; } return $request->hasHeader('x-livewire'); } catch (\Throwable $e) { // If the request cannot be resolved, it's probably not a Livewire request. return false; } } private function mapCallArgumentsToMethodParameters(Component $component, string $method, array $data): ?array { // If the data is empty there is nothing to do and we can return early // We also do a quick sanity check the method exists to prevent doing more expensive reflection to come to the same conclusion if (empty($data) || !method_exists($component, $method)) { return null; } try { $reflection = new \ReflectionMethod($component, $method); $parameters = []; foreach ($reflection->getParameters() as $parameter) { $defaultValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : ''; $parameters["\${$parameter->getName()}"] = $data[$parameter->getPosition()] ?? $defaultValue; unset($data[$parameter->getPosition()]); } if (!empty($data)) { $parameters['additionalArguments'] = $data; } return $parameters; } catch (\ReflectionException $e) { // If reflection fails, fail the mapping instead of crashing return null; } } } PK\sW]ØŸºµøø@src/Sentry/Laravel/Features/Storage/CloudFilesystemDecorator.phpnu„[µü¤withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } } PK\sW]GÙ™Fèè=src/Sentry/Laravel/Features/Storage/SentryCloudFilesystem.phpnu„[µü¤filesystem = $filesystem; $this->defaultData = $defaultData; $this->recordSpans = $recordSpans; $this->recordBreadcrumbs = $recordBreadcrumbs; } } PK\sW]¯D\G{{9src/Sentry/Laravel/Features/Storage/SentryS3V3Adapter.phpnu„[µü¤getDriver(), $filesystem->getAdapter(), $filesystem->getConfig(), $filesystem->getClient()); $this->filesystem = $filesystem; $this->defaultData = $defaultData; $this->recordSpans = $recordSpans; $this->recordBreadcrumbs = $recordBreadcrumbs; } } PK\sW]¦Îž pp?src/Sentry/Laravel/Features/Storage/SentryFilesystemAdapter.phpnu„[µü¤getDriver(), $filesystem->getAdapter(), $filesystem->getConfig()); $this->filesystem = $filesystem; $this->defaultData = $defaultData; $this->recordSpans = $recordSpans; $this->recordBreadcrumbs = $recordBreadcrumbs; } } PK\sW]Êe¬MM;src/Sentry/Laravel/Features/Storage/FilesystemDecorator.phpnu„[µü¤ $args * @param array $data * * @return mixed */ protected function withSentry(string $method, array $args, ?string $description, array $data) { $op = "file.{$method}"; // See https://develop.sentry.dev/sdk/performance/span-operations/#web-server $data = array_merge($data, $this->defaultData); if ($this->recordBreadcrumbs) { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, $op, $description, $data )); } if ($this->recordSpans) { return trace( function () use ($method, $args) { return $this->filesystem->{$method}(...$args); }, SpanContext::make() ->setOp($op) ->setData($data) ->setOrigin('auto.filesystem') ->setDescription($description) ); } return $this->filesystem->{$method}(...$args); } public function path($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function exists($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function get($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function readStream($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function put($path, $contents, $options = []) { $description = is_string($contents) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($contents))) : $path; return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path', 'options')); } public function putFile($path, $file = null, $options = []) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'file', 'options')); } public function putFileAs($path, $file, $name = null, $options = []) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'file', 'name', 'options')); } public function writeStream($path, $resource, array $options = []) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'options')); } public function getVisibility($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function setVisibility($path, $visibility) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'visibility')); } public function prepend($path, $data, $separator = PHP_EOL) { $description = is_string($data) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($data))) : $path; return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path')); } public function append($path, $data, $separator = PHP_EOL) { $description = is_string($data) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($data))) : $path; return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path')); } public function delete($paths) { [$description, $data] = $this->getDescriptionAndDataForPathOrPaths($paths); return $this->withSentry(__FUNCTION__, func_get_args(), $description, $data); } public function copy($from, $to) { return $this->withSentry(__FUNCTION__, func_get_args(), sprintf('from "%s" to "%s"', $from, $to), compact('from', 'to')); } public function move($from, $to) { return $this->withSentry(__FUNCTION__, func_get_args(), sprintf('from "%s" to "%s"', $from, $to), compact('from', 'to')); } public function size($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function lastModified($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function files($directory = null, $recursive = false) { return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory', 'recursive')); } public function allFiles($directory = null) { return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory')); } public function directories($directory = null, $recursive = false) { return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory', 'recursive')); } public function allDirectories($directory = null) { return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory')); } public function makeDirectory($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function deleteDirectory($directory) { return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory')); } public function __call($name, $arguments) { return $this->filesystem->{$name}(...$arguments); } protected function getDescriptionAndDataForPathOrPaths($pathOrPaths): array { if (is_array($pathOrPaths)) { $description = sprintf('%s paths', count($pathOrPaths)); $data = ['paths' => $pathOrPaths]; } else { $description = $pathOrPaths; $data = ['path' => $pathOrPaths]; } return [$description, $data]; } } PK\sW]òò¾¾3src/Sentry/Laravel/Features/Storage/Integration.phpnu„[µü¤container()->afterResolving(FilesystemManager::class, function (FilesystemManager $filesystemManager): void { $filesystemManager->extend( self::STORAGE_DRIVER_NAME, function (Application $application, array $config) use ($filesystemManager): Filesystem { if (empty($config['sentry_disk_name'])) { throw new RuntimeException(sprintf('Missing `sentry_disk_name` config key for `%s` filesystem driver.', self::STORAGE_DRIVER_NAME)); } if (empty($config['sentry_original_driver'])) { throw new RuntimeException(sprintf('Missing `sentry_original_driver` config key for `%s` filesystem driver.', self::STORAGE_DRIVER_NAME)); } if ($config['sentry_original_driver'] === self::STORAGE_DRIVER_NAME) { throw new RuntimeException(sprintf('`sentry_original_driver` for Sentry storage integration cannot be the `%s` driver.', self::STORAGE_DRIVER_NAME)); } $disk = $config['sentry_disk_name']; $config['driver'] = $config['sentry_original_driver']; unset($config['sentry_original_driver']); $diskResolver = (function (string $disk, array $config) { // This is a "hack" to make sure that the original driver is resolved by the FilesystemManager $oldConfig = config("filesystems.disks.{$disk}"); config(["filesystems.disks.{$disk}" => $config]); /** @var FilesystemManager $this */ $resolved = $this->resolve($disk); config(["filesystems.disks.{$disk}" => $oldConfig]); return $resolved; })->bindTo($filesystemManager, FilesystemManager::class); /** @var Filesystem $originalFilesystem */ $originalFilesystem = $diskResolver($disk, $config); $defaultData = ['disk' => $disk, 'driver' => $config['driver']]; $recordSpans = $config['sentry_enable_spans'] ?? $this->isTracingFeatureEnabled(self::FEATURE_KEY); $recordBreadcrumbs = $config['sentry_enable_breadcrumbs'] ?? $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); if ($originalFilesystem instanceof AwsS3V3Adapter) { return new SentryS3V3Adapter($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs); } if ($originalFilesystem instanceof FilesystemAdapter) { return new SentryFilesystemAdapter($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs); } if ($originalFilesystem instanceof CloudFilesystem) { return new SentryCloudFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs); } return new SentryFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs); } ); }); } /** * Decorates the configuration for a single disk with Sentry driver configuration. * This replaces the driver with a custom driver that will capture performance traces and breadcrumbs. * * The custom driver will be an instance of @see \Sentry\Laravel\Features\Storage\SentryS3V3Adapter * if the original driver is an @see \Illuminate\Filesystem\AwsS3V3Adapter, * and an instance of @see \Sentry\Laravel\Features\Storage\SentryFilesystemAdapter * if the original driver is an @see \Illuminate\Filesystem\FilesystemAdapter. * If the original driver is neither of those, it will be @see \Sentry\Laravel\Features\Storage\SentryFilesystem * or @see \Sentry\Laravel\Features\Storage\SentryCloudFilesystem based on the contract of the original driver. * * You might run into problems if you expect another specific driver class. * * @param array $diskConfig * * @return array */ public static function configureDisk(string $diskName, array $diskConfig, bool $enableSpans = true, bool $enableBreadcrumbs = true): array { $currentDriver = $diskConfig['driver']; if ($currentDriver !== self::STORAGE_DRIVER_NAME) { $diskConfig['driver'] = self::STORAGE_DRIVER_NAME; $diskConfig['sentry_disk_name'] = $diskName; $diskConfig['sentry_original_driver'] = $currentDriver; $diskConfig['sentry_enable_spans'] = $enableSpans; $diskConfig['sentry_enable_breadcrumbs'] = $enableBreadcrumbs; } return $diskConfig; } /** * Decorates the configuration for all disks with Sentry driver configuration. * * @see self::configureDisk() * * @param array> $diskConfigs * * @return array> */ public static function configureDisks(array $diskConfigs, bool $enableSpans = true, bool $enableBreadcrumbs = true): array { $diskConfigsWithSentryDriver = []; foreach ($diskConfigs as $diskName => $diskConfig) { $diskConfigsWithSentryDriver[$diskName] = static::configureDisk($diskName, $diskConfig, $enableSpans, $enableBreadcrumbs); } return $diskConfigsWithSentryDriver; } } PK\sW]òµzçíí8src/Sentry/Laravel/Features/Storage/SentryFilesystem.phpnu„[µü¤filesystem = $filesystem; $this->defaultData = $defaultData; $this->recordSpans = $recordSpans; $this->recordBreadcrumbs = $recordBreadcrumbs; } } PK\sW]2 ž›xxBsrc/Sentry/Laravel/Features/Storage/FilesystemAdapterDecorator.phpnu„[µü¤getDescriptionAndDataForPathOrPaths($path); return $this->withSentry(__FUNCTION__, func_get_args(), $description, $data); } public function assertMissing($path) { [$description, $data] = $this->getDescriptionAndDataForPathOrPaths($path); return $this->withSentry(__FUNCTION__, func_get_args(), $description, $data); } public function assertDirectoryEmpty($path) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } public function temporaryUrl($path, $expiration, array $options = []) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'expiration', 'options')); } public function temporaryUploadUrl($path, $expiration, array $options = []) { return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'expiration', 'options')); } } PK\sW]ìx.I,I,<src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.phpnu„[µü¤ The list of checkins that are currently in progress. */ private $checkInStore = []; private $shouldHandleCheckIn = false; public function register(): void { $startCheckIn = function ( ?string $slug, SchedulingEvent $scheduled, ?int $checkInMargin, ?int $maxRuntime, bool $updateMonitorConfig, ?int $failureIssueThreshold, ?int $recoveryThreshold ) { $this->startCheckIn( $slug, $scheduled, $checkInMargin, $maxRuntime, $updateMonitorConfig, $failureIssueThreshold, $recoveryThreshold ); }; $finishCheckIn = function (?string $slug, SchedulingEvent $scheduled, CheckInStatus $status) { $this->finishCheckIn($slug, $scheduled, $status); }; SchedulingEvent::macro('sentryMonitor', function ( ?string $monitorSlug = null, ?int $checkInMargin = null, ?int $maxRuntime = null, bool $updateMonitorConfig = true, ?int $failureIssueThreshold = null, ?int $recoveryThreshold = null ) use ($startCheckIn, $finishCheckIn) { /** @var SchedulingEvent $this */ if ($monitorSlug === null && empty($this->command) && empty($this->description)) { throw new RuntimeException('The command and description are not set, please set a slug manually for this scheduled command using the `sentryMonitor(\'your-monitor-slug\')` macro.'); } return $this ->before(function () use ( $startCheckIn, $monitorSlug, $checkInMargin, $maxRuntime, $updateMonitorConfig, $failureIssueThreshold, $recoveryThreshold ) { /** @var SchedulingEvent $this */ $startCheckIn( $monitorSlug, $this, $checkInMargin, $maxRuntime, $updateMonitorConfig, $failureIssueThreshold, $recoveryThreshold ); }) ->onSuccess(function () use ($finishCheckIn, $monitorSlug) { /** @var SchedulingEvent $this */ $finishCheckIn($monitorSlug, $this, CheckInStatus::ok()); }) ->onFailure(function () use ($finishCheckIn, $monitorSlug) { /** @var SchedulingEvent $this */ $finishCheckIn($monitorSlug, $this, CheckInStatus::error()); }); }); } public function isApplicable(): bool { return true; } public function onBoot(Dispatcher $events): void { $this->shouldHandleCheckIn = true; $events->listen(ScheduledTaskStarting::class, [$this, 'handleScheduledTaskStarting']); $events->listen(ScheduledTaskFinished::class, [$this, 'handleScheduledTaskFinished']); $events->listen(ScheduledTaskFailed::class, [$this, 'handleScheduledTaskFailed']); } public function onBootInactive(): void { $this->shouldHandleCheckIn = false; } public function useCacheStore(?string $name): void { $this->cacheStore = $name; } public function handleScheduledTaskStarting(ScheduledTaskStarting $event): void { // There is nothing for us to track if it's a background task since it will be handled by a separate process if (!$event->task || $event->task->runInBackground) { return; } // When scheduling a command class the command name will be the most descriptive // When a job is scheduled the command name is `null` and the job class name (or display name) is set as the description // When a closure is scheduled both the command name and description are `null` $name = $this->getCommandNameForScheduled($event->task) ?? $event->task->description ?? 'Closure'; $context = TransactionContext::make() ->setName($name) ->setSource(TransactionSource::task()) ->setOp('console.command.scheduled') ->setStartTimestamp(microtime(true)); $transaction = SentrySdk::getCurrentHub()->startTransaction($context); $this->pushSpan($transaction); } public function handleScheduledTaskFinished(): void { $this->maybeFinishSpan(SpanStatus::ok()); $this->maybePopScope(); } public function handleScheduledTaskFailed(): void { $this->maybeFinishSpan(SpanStatus::internalError()); $this->maybePopScope(); } private function startCheckIn( ?string $slug, SchedulingEvent $scheduled, ?int $checkInMargin, ?int $maxRuntime, bool $updateMonitorConfig, ?int $failureIssueThreshold, ?int $recoveryThreshold ): void { if (!$this->shouldHandleCheckIn) { return; } $checkInSlug = $slug ?? $this->makeSlugForScheduled($scheduled); $checkIn = $this->createCheckIn($checkInSlug, CheckInStatus::inProgress()); if ($updateMonitorConfig || $slug === null) { $timezone = $scheduled->timezone; if ($timezone instanceof DateTimeZone) { $timezone = $timezone->getName(); } $checkIn->setMonitorConfig(new MonitorConfig( MonitorSchedule::crontab($scheduled->getExpression()), $checkInMargin, $maxRuntime, $timezone, $failureIssueThreshold, $recoveryThreshold )); } $cacheKey = $this->buildCacheKey($scheduled->mutexName(), $checkInSlug); $this->checkInStore[$cacheKey] = $checkIn; if ($scheduled->runInBackground) { $this->resolveCache()->put($cacheKey, $checkIn->getId(), $scheduled->expiresAt * 60); } $this->sendCheckIn($checkIn); } private function finishCheckIn(?string $slug, SchedulingEvent $scheduled, CheckInStatus $status): void { if (!$this->shouldHandleCheckIn) { return; } $mutex = $scheduled->mutexName(); $checkInSlug = $slug ?? $this->makeSlugForScheduled($scheduled); $cacheKey = $this->buildCacheKey($mutex, $checkInSlug); $checkIn = $this->checkInStore[$cacheKey] ?? null; if ($checkIn === null && $scheduled->runInBackground) { $checkInId = $this->resolveCache()->get($cacheKey); if ($checkInId !== null) { $checkIn = $this->createCheckIn($checkInSlug, $status, $checkInId); } } // This should never happen (because we should always start before we finish), but better safe than sorry if ($checkIn === null) { return; } // We don't need to keep the checkIn ID stored since we finished executing the command unset($this->checkInStore[$mutex]); if ($scheduled->runInBackground) { $this->resolveCache()->forget($cacheKey); } $checkIn->setStatus($status); $this->sendCheckIn($checkIn); } private function sendCheckIn(CheckIn $checkIn): void { $event = SentryEvent::createCheckIn(); $event->setCheckIn($checkIn); SentrySdk::getCurrentHub()->captureEvent($event); } private function createCheckIn(string $slug, CheckInStatus $status, ?string $id = null): CheckIn { $options = SentrySdk::getCurrentHub()->getClient()->getOptions(); return new CheckIn( Str::limit($slug, 128, ''), $status, $id, $options->getRelease(), $options->getEnvironment() ); } private function buildCacheKey(string $mutex, string $slug): string { // We use the mutex name as part of the cache key to avoid collisions between the same commands with the same schedule but with different slugs return 'sentry:checkIn:' . sha1("{$mutex}:{$slug}"); } private function makeSlugForScheduled(SchedulingEvent $scheduled): string { if (empty($scheduled->command)) { if (!empty($scheduled->description) && class_exists($scheduled->description)) { $generatedSlug = Str::slug( // We reverse the class name to have the class name at the start of the slug instead of at the end (and possibly cut off) implode('_', array_reverse(explode('\\', $scheduled->description))) ); } else { $generatedSlug = Str::slug($scheduled->description); } } else { $generatedSlug = Str::slug( str_replace( // `:` is commonly used in the command name, so we replace it with `-` to avoid it being stripped out by the slug function ':', '-', trim( // The command string always starts with the PHP binary, so we remove it since it's not relevant to the slug Str::after($scheduled->command, ConsoleApplication::phpBinary()) ) ) ); } return "scheduled_{$generatedSlug}"; } private function getCommandNameForScheduled(SchedulingEvent $scheduled): ?string { if (!$scheduled->command) { return null; } // The command string always starts with the PHP binary and artisan binary, so we remove it since it's not relevant to the name return trim( Str::after($scheduled->command, ConsoleApplication::phpBinary() . ' ' . ConsoleApplication::artisanBinary()) ); } private function resolveCache(): Repository { return $this->container()->make(Cache::class)->store($this->cacheStore); } } PK\sW]Ü:?°..0src/Sentry/Laravel/Features/CacheIntegration.phpnu„[µü¤isTracingFeatureEnabled('redis_commands', false) || $this->isTracingFeatureEnabled('cache') || $this->isBreadcrumbFeatureEnabled('cache'); } public function onBoot(Dispatcher $events): void { if ($this->isBreadcrumbFeatureEnabled('cache')) { $events->listen([ Events\CacheHit::class, Events\CacheMissed::class, Events\KeyWritten::class, Events\KeyForgotten::class, ], [$this, 'handleCacheEventsForBreadcrumbs']); } if ($this->isTracingFeatureEnabled('cache')) { $events->listen([ Events\RetrievingKey::class, Events\RetrievingManyKeys::class, Events\CacheHit::class, Events\CacheMissed::class, Events\WritingKey::class, Events\WritingManyKeys::class, Events\KeyWritten::class, Events\KeyWriteFailed::class, Events\ForgettingKey::class, Events\KeyForgotten::class, Events\KeyForgetFailed::class, ], [$this, 'handleCacheEventsForTracing']); } if ($this->isTracingFeatureEnabled('redis_commands', false)) { $events->listen(RedisEvents\CommandExecuted::class, [$this, 'handleRedisCommands']); $this->container()->afterResolving(RedisManager::class, static function (RedisManager $redis): void { $redis->enableEvents(); }); } } public function handleCacheEventsForBreadcrumbs(Events\CacheEvent $event): void { switch (true) { case $event instanceof Events\KeyWritten: $message = 'Written'; break; case $event instanceof Events\KeyForgotten: $message = 'Forgotten'; break; case $event instanceof Events\CacheMissed: $message = 'Missed'; break; case $event instanceof Events\CacheHit: $message = 'Read'; break; default: // In case events are added in the future we do nothing when an unknown event is encountered return; } $displayKey = $this->replaceSessionKey($event->key); Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'cache', "{$message}: {$displayKey}", $event->tags ? ['tags' => $event->tags] : [] )); } public function handleCacheEventsForTracing(Events\CacheEvent $event): void { if ($this->maybeHandleCacheEventAsEndOfSpan($event)) { return; } $this->withParentSpanIfSampled(function (Span $parentSpan) use ($event) { if ($event instanceof Events\RetrievingKey || $event instanceof Events\RetrievingManyKeys) { $keys = $this->normalizeKeyOrKeys( $event instanceof Events\RetrievingKey ? [$event->key] : $event->keys ); $displayKeys = $this->replaceSessionKeys($keys); $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('cache.get') ->setData([ 'cache.key' => $displayKeys, ]) ->setOrigin('auto.cache') ->setDescription(implode(', ', $displayKeys)) ) ); } if ($event instanceof Events\WritingKey || $event instanceof Events\WritingManyKeys) { $keys = $this->normalizeKeyOrKeys( $event instanceof Events\WritingKey ? [$event->key] : $event->keys ); $displayKeys = $this->replaceSessionKeys($keys); $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('cache.put') ->setData([ 'cache.key' => $displayKeys, 'cache.ttl' => $event->seconds, ]) ->setOrigin('auto.cache') ->setDescription(implode(', ', $displayKeys)) ) ); } if ($event instanceof Events\ForgettingKey) { $displayKey = $this->replaceSessionKey($event->key); $this->pushSpan( $parentSpan->startChild( SpanContext::make() ->setOp('cache.remove') ->setData([ 'cache.key' => [$displayKey], ]) ->setOrigin('auto.cache') ->setDescription($displayKey) ) ); } }); } public function handleRedisCommands(RedisEvents\CommandExecuted $event): void { $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $context = SpanContext::make() ->setOp('db.redis') ->setOrigin('auto.cache.redis'); $keyForDescription = ''; // If the first parameter is a string and does not contain a newline we use it as the description since it's most likely a key // This is not a perfect solution but it's the best we can do without understanding the command that was executed if (!empty($event->parameters[0]) && is_string($event->parameters[0]) && !Str::contains($event->parameters[0], "\n")) { $keyForDescription = $this->replaceSessionKey($event->parameters[0]); } $context->setDescription(rtrim(strtoupper($event->command) . ' ' . $keyForDescription)); $context->setStartTimestamp(microtime(true) - $event->time / 1000); $context->setEndTimestamp($context->getStartTimestamp() + $event->time / 1000); $data = [ 'db.redis.connection' => $event->connectionName, ]; if ($this->shouldSendDefaultPii()) { $data['db.redis.parameters'] = $this->replaceSessionKeys($event->parameters); } if ($this->isTracingFeatureEnabled('redis_origin')) { $commandOrigin = $this->resolveEventOrigin(); if ($commandOrigin !== null) { $data = array_merge($data, $commandOrigin); } } $context->setData($data); $parentSpan->startChild($context); } private function maybeHandleCacheEventAsEndOfSpan(Events\CacheEvent $event): bool { // End of span for RetrievingKey and RetrievingManyKeys events if ($event instanceof Events\CacheHit || $event instanceof Events\CacheMissed) { $finishedSpan = $this->maybeFinishSpan(SpanStatus::ok()); if ($finishedSpan !== null && count($finishedSpan->getData()['cache.key'] ?? []) === 1) { $finishedSpan->setData([ 'cache.hit' => $event instanceof Events\CacheHit ]); } return true; } // End of span for WritingKey and WritingManyKeys events if ($event instanceof Events\KeyWritten || $event instanceof Events\KeyWriteFailed) { $finishedSpan = $this->maybeFinishSpan( $event instanceof Events\KeyWritten ? SpanStatus::ok() : SpanStatus::internalError() ); if ($finishedSpan !== null) { $finishedSpan->setData([ 'cache.success' => $event instanceof Events\KeyWritten ]); } return true; } // End of span for ForgettingKey event if ($event instanceof Events\KeyForgotten || $event instanceof Events\KeyForgetFailed) { $this->maybeFinishSpan(); return true; } return false; } /** * Retrieve the current session key if available. */ private function getSessionKey(): ?string { try { /** @var Session $sessionStore */ $sessionStore = $this->container()->make('session.store'); // It is safe for us to get the session ID here without checking if the session is started // because getting the session ID does not start the session. In addition we need the ID before // the session is started because the cache will retrieve the session ID from the cache before the session // is considered started. So if we wait for the session to be started, we will not be able to replace the // session key in the cache operation that is being executed to retrieve the session data from the cache. return $sessionStore->getId(); } catch (\Exception $e) { // We can assume the session store is not available here so there is no session key to retrieve // We capture a generic exception to avoid breaking the application because some code paths can // result in an exception other than the expected `Illuminate\Contracts\Container\BindingResolutionException` return null; } } /** * Replace a session key with a placeholder. */ private function replaceSessionKey(?string $value): string { if (!is_string($value)) { return '{empty key}'; } return $value === $this->getSessionKey() ? '{sessionKey}' : $value; } /** * Replace session keys in an array of keys with placeholders. * * @param string[] $values * * @return mixed[] */ private function replaceSessionKeys(array $values): array { $sessionKey = $this->getSessionKey(); return array_map(static function ($value) use ($sessionKey) { return is_string($value) && $value === $sessionKey ? '{sessionKey}' : $value; }, $values); } /** * Normalize the array of keys to a array of only strings. * * @param string|string[]|array $keyOrKeys * * @return string[] */ private function normalizeKeyOrKeys($keyOrKeys): array { if (is_string($keyOrKeys)) { return [$keyOrKeys]; } return collect($keyOrKeys)->map(function ($value, $key) { return is_string($key) ? $key : $value; })->values()->all(); } } PK\sW]'È   8src/Sentry/Laravel/Features/NotificationsIntegration.phpnu„[µü¤isTracingFeatureEnabled(self::FEATURE_KEY) || $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); } public function onBoot(Dispatcher $events): void { if ($this->isTracingFeatureEnabled(self::FEATURE_KEY)) { $events->listen(NotificationSending::class, [$this, 'handleNotificationSending']); } $events->listen(NotificationSent::class, [$this, 'handleNotificationSent']); } public function handleNotificationSending(NotificationSending $event): void { $parentSpan = SentrySdk::getCurrentHub()->getSpan(); // If there is no sampled span there is no need to handle the event if ($parentSpan === null || !$parentSpan->getSampled()) { return; } $context = SpanContext::make() ->setOp('notification.send') ->setData([ 'id' => $event->notification->id, 'channel' => $event->channel, 'notifiable' => $this->formatNotifiable($event->notifiable), 'notification' => get_class($event->notification), ]) ->setOrigin('auto.laravel.notifications') ->setDescription($event->channel); $this->pushSpan($parentSpan->startChild($context)); } public function handleNotificationSent(NotificationSent $event): void { $this->maybeFinishSpan(SpanStatus::ok()); if ($this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY)) { Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'notification.sent', 'Sent notification', [ 'channel' => $event->channel, 'notifiable' => $this->formatNotifiable($event->notifiable), 'notification' => get_class($event->notification), ] )); } } private function formatNotifiable($notifiable): string { if (is_string($notifiable) || is_numeric($notifiable)) { return (string)$notifiable; } if (is_object($notifiable)) { $result = get_class($notifiable); if ($notifiable instanceof Model) { $result .= "({$notifiable->getKey()})"; } return $result; } return 'unknown'; } } PK\sW]º”ŒRR.src/Sentry/Laravel/Features/LogIntegration.phpnu„[µü¤listen(ViewMatched::class, [$this, 'handleViewMatched']); } public function handleViewMatched(ViewMatched $matched): void { Middleware::signalRouteWasMatched(); $routeName = $this->extractRouteForMatchedView($matched->matchedView, $matched->mountPath); Integration::addBreadcrumb(new Breadcrumb( Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_NAVIGATION, 'folio.route', $routeName )); Integration::setTransaction($routeName); $transaction = SentrySdk::getCurrentHub()->getTransaction(); if ($transaction === null || !$transaction->getSampled()) { return; } $transaction->setName($routeName); $transaction->getMetadata()->setSource(TransactionSource::route()); } private function extractRouteForMatchedView(MatchedView $matchedView, MountPath $mountPath): string { $path = Str::beforeLast('/' . ltrim($mountPath->baseUri . $matchedView->relativePath(), '/'), '.blade.php'); return Str::replace(['[', ']'], ['{', '}'], $path); } } PK\sW]È *q$src/Sentry/Laravel/Util/Filesize.phpnu„[µü¤ [ ... 'sentry_logs' => [ 'driver' => 'sentry_logs', 'level' => env('LOG_LEVEL', 'info'), ], ... ], ``` Add `SENTRY_ENABLE_LOGS=true` to your `.env` file. Use the Log facade to sent your logs to Sentry. To learn more, head over to the [Laravel docs](https://laravel.com/docs/logging). ```php use Illuminate\Support\Facades\Log; Log::channel('sentry_logs')->info('User {id} failed to login.', ['id' => $user->id]); ``` To learn more, head over to our [docs](https://docs.sentry.io/platforms/php/guides/laravel/logs/). ## 4.14.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.14.1. ### Bug Fixes - Ensure there is a newline before writing env variables [(#1002)](https://github.com/getsentry/sentry-laravel/pull/1002) Fixed an issue where the `php artisan sentry:publish` command might not properly add newlines when writing environment variables to the `.env` file. ## 4.14.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.14.0. ### Bug Fixes - Fix tracing when using Laravel Octane [(#997)](https://github.com/getsentry/sentry-laravel/pull/997) When using Laravel Octane, the SDK now correctly cleans up it's state after each request, ensuring that traces are correctly reported. ### Misc - Add `sentry` prefix to publish group name [(#992)](https://github.com/getsentry/sentry-laravel/pull/992) When running `php artisan vendor:publish`, the Sentry package exports are now prefixed with `sentry`, making it easier to distinguish from other packages. - Remove support for `traceparent` header [(#994)](https://github.com/getsentry/sentry-laravel/pull/994) The W3C's `traceparent` header is no longer automatically picked up and emitted by the SDK to prevent non-Sentry SDKs from starting a trace that is unwanted. ## 4.13.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.13.0. ### Features - Add support for Laravel 12.0 [(#980)](https://github.com/getsentry/sentry-laravel/pull/980) ## 4.12.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.12.0. ### Features - Improve generation of slug for scheduled job cron monitoring [(#977)](https://github.com/getsentry/sentry-laravel/pull/977) For scheduled jobs it's no longer needed to manually provide a slug to the [`->sentryMonitor()`](https://docs.sentry.io/platforms/php/guides/laravel/crons/#job-monitoring) call, it will be derived from the job class name. ### Bug Fixes - Fix unable to parse notifiable [(#974)](https://github.com/getsentry/sentry-laravel/pull/974) - Fix triggering a missing attribute violation [(#978)](https://github.com/getsentry/sentry-laravel/pull/978) ### Misc - Disable scheduled task tracing for backgrounded tasks [(#975)](https://github.com/getsentry/sentry-laravel/pull/975) Backgrounded tasks show up a ~1ms transactions right now because we are effectively monitoring the time it takes to start the background process instead of the execution. We are [working on a solution](https://github.com/getsentry/sentry-laravel/pull/976) to this problem, but in the meantime, we are disabling the monitoring of backgrounded tasks (that was introduces in 4.11.0). ## 4.11.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.11.0. ### Features - Add Scheduled Task Tracing [(#968)](https://github.com/getsentry/sentry-laravel/pull/968) ### Bug Fixes - Fix retry count for queued jobs [(#967)](https://github.com/getsentry/sentry-laravel/pull/967) ## 4.10.2 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.10.2. ### Bug Fixes - Fixed a PHP 8.4 deprecation notice when running `php artisan sentry:test` [(#963)](https://github.com/getsentry/sentry-laravel/pull/963) ## 4.10.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.10.1. ### Bug Fixes - Fixed a PHP 8.4 deprecation notice [(#954)](https://github.com/getsentry/sentry-laravel/pull/954) ## 4.10.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.10.0. ### Features - The SDK was updated to support PHP 8.4 [(#952)](https://github.com/getsentry/sentry-laravel/pull/952) ### Misc - The SDK does no longer emit Metrics. All public Metrics APIs are now no-op, internal APIs were removed [(#951)](https://github.com/getsentry/sentry-laravel/pull/951) ## 4.9.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.9.0. ### Misc - Allow the cache store used by the console scheduling integration to be overridden [(#942)](https://github.com/getsentry/sentry-laravel/pull/942) - Set `http` breadcrumb levels based on response code [(#940)](https://github.com/getsentry/sentry-laravel/pull/940) ## 4.8.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.8.0. ### Bug Fixes - Fix `php artisan sentry:publish` mangling the .env file [(#928)](https://github.com/getsentry/sentry-laravel/pull/928) - Fix not (correctly) reporting transactions when using Laravel Octane [(#936)](https://github.com/getsentry/sentry-laravel/pull/936) ### Misc - Improve the stacktrace of the `php artisan sentry:test` event [(#926)](https://github.com/getsentry/sentry-laravel/pull/926) - Remove outdated JS SDK installation step from `php artisan sentry:publish` [(#930)](https://github.com/getsentry/sentry-laravel/pull/930) ## 4.7.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.7.1. ### Bug Fixes - Always remove the `XSRF-TOKEN` cookie value before sending to Sentry [(#920)](https://github.com/getsentry/sentry-laravel/pull/920) - Fix trace durations when using Octane [(#921)](https://github.com/getsentry/sentry-laravel/pull/921) - Handle clousre route names [(#921)](https://github.com/getsentry/sentry-laravel/pull/921) - Don't rely on facades when accessing the Laravel context [(#922)](https://github.com/getsentry/sentry-laravel/pull/922) - Normalize array of cache key names before converting to string [(#923)](https://github.com/getsentry/sentry-laravel/pull/923) ## 4.7.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.7.0. ### Features - Add support for Cache Insights Module [(#914)](https://github.com/getsentry/sentry-laravel/pull/914). To learn more about this module, visit https://docs.sentry.io/product/insights/caches/. This feature requires Laravel v11.11.0 or higher. Cache tracing is enabled by default for new SDK installations. To enable this feature in your existing installation, update your `config/sentry.php` file with `'cache' => env('SENTRY_TRACE_CACHE_ENABLED', true),` under `'tracing'`. ## 4.6.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.6.1. ### Bug Fixes - Fix wrong queue grouping in the queue Insights Module [(#910)](https://github.com/getsentry/sentry-laravel/pull/910) ## 4.6.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.6.0. ### Features - Add support for the Queue Insights Module [(#902)](https://github.com/getsentry/sentry-laravel/pull/902). To learn more about this module, visit https://docs.sentry.io/product/performance/queue-monitoring/. Queue tracing is enabled by default for new SDK installations. To enable this feature in your existing installation, update your `config/sentry.php` file with `'queue_jobs' => env('SENTRY_TRACE_QUEUE_JOBS_ENABLED', true),` or set `SENTRY_TRACE_QUEUE_JOBS_ENABLED=true` in your environment [(#903)](https://github.com/getsentry/sentry-laravel/pull/903) ### Bug Fixes - Check if a span is sampled before creating child spans [(#898)](https://github.com/getsentry/sentry-laravel/pull/898) - Always register the console `sentryMonitor()` macro. This fixes the macro not being available when using Laravel Lumen [(#900)](https://github.com/getsentry/sentry-laravel/pull/900) - Avoid manipulating the config when resolving disks [(#901)](https://github.com/getsentry/sentry-laravel/pull/901) ### Misc - Various Spotlight improvements, such as the addition of a new `SENTRY_SPOTLIGHT` environment variable and not requiring a DSN to be set to use Spotlight [(#892)](https://github.com/getsentry/sentry-laravel/pull/892) ## 4.5.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.5.1. ### Bug Fixes - Fix discarded attribute violation reporter not accepting multiple property names [(#890)](https://github.com/getsentry/sentry-laravel/pull/890) ## 4.5.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.5.0. ### Features - Limit when SQL query origins are being captured [(#881)](https://github.com/getsentry/sentry-laravel/pull/881) We now only capture the origin of a SQL query when the query is slower than 100ms, configurable by the `SENTRY_TRACE_SQL_ORIGIN_THRESHOLD_MS` environment variable. - Add tracing and breadcrumbs for [Notifications](https://laravel.com/docs/11.x/notifications) [(#852)](https://github.com/getsentry/sentry-laravel/pull/852) - Add reporter for `Model::preventAccessingMissingAttributes()` [(#824)](https://github.com/getsentry/sentry-laravel/pull/824) - Make it easier to enable the debug logger [(#880)](https://github.com/getsentry/sentry-laravel/pull/880) You can now enable the debug logger by adding the following to your `config/sentry.php` file: ```php 'logger' => Sentry\Logger\DebugFileLogger::class, // This will log SDK logs to `storage_path('logs/sentry.log')` ``` Only use this in development and testing environments, as it can generate a lot of logs. ### Bug Fixes - Fix Lighthouse operation not detected when query contained a fragment before the operation [(#883)](https://github.com/getsentry/sentry-laravel/pull/883) - Fix an exception being thrown when the username extracted from the authenticated user model is not a string [(#887)](https://github.com/getsentry/sentry-laravel/pull/887) ## 4.4.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.4.1. ### Bug Fixes - Fix `assertExists`/`assertMissing` can throw on the `FilesystemDecorator` [(#877)](https://github.com/getsentry/sentry-laravel/pull/877) ## 4.4.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.4.0. ### Features - Add support for Laravel 11 Context [(#869)](https://github.com/getsentry/sentry-laravel/pull/869) If you are using Laravel 11 and the new "Context" capabilities we now automatically capture that context for you and it will be visible in Sentry. Read more about the feature in the [Laravel documentation](https://laravel.com/docs/11.x/context) and how to use it. ## 4.3.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.3.1. ### Bug Fixes - Add missing methods to `FilesystemDecorator` [(#865)](https://github.com/getsentry/sentry-laravel/pull/865) ## 4.3.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.3.0. ### Features - Add support for Laravel 11.0 [(#845)](https://github.com/getsentry/sentry-laravel/pull/845) If you're upgrading an existing Laravel 10 application to the new Laravel 11 directory structure, you must change how Sentry integrates into the exception handler. Update your `bootstrap/app.php` with: ```php withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { // }) ->withExceptions(function (Exceptions $exceptions) { Integration::handles($exceptions); })->create(); ``` If you plan to perform up-time checks against the new Laravel 11 `/up` health URL, ignore this transaction in your `config/sentry.php` file, as not doing so could consume a substantial amount of your performance unit quota. ```php // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore-transactions 'ignore_transactions' => [ // Ignore Laravel's default health URL '/up', ], ``` ### Bug Fixes - Set `queue.publish` spans as the parent of `queue.process` spans [(#850)](https://github.com/getsentry/sentry-laravel/pull/850) - Consider all `http_*` SDK options from the Laravel client in the test command [(#859)](https://github.com/getsentry/sentry-laravel/pull/859) ## 4.2.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.2.0. ### Features - Add new spans, measuring the time taken to queue a job [(#833)](https://github.com/getsentry/sentry-laravel/pull/833) - Add support for `failure_issue_threshold` & `recovery_threshold` for `sentryMonitor()` method on scheduled commands [(#838)](https://github.com/getsentry/sentry-laravel/pull/838) - Automatically flush metrics when the application terminates [(#841)](https://github.com/getsentry/sentry-laravel/pull/841) - Add support for the W3C traceparent header [(#834)](https://github.com/getsentry/sentry-laravel/pull/834) - Improve `php artisan sentry:test` to show internal log messages by default [(#842)](https://github.com/getsentry/sentry-laravel/pull/842) ## 4.1.2 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.1.2. ### Bug Fixes - Fix unable to set `callable` for `integrations` option [(#826)](https://github.com/getsentry/sentry-laravel/pull/826) - Fix performance traces not being collected for Laravel Lumen unless missing routes are reported [(#822)](https://github.com/getsentry/sentry-laravel/pull/822) - Fix configuration options for queue job tracing not applying correctly [(#820)](https://github.com/getsentry/sentry-laravel/pull/820) ### Misc - Allow newer versions of `symfony/psr-http-message-bridge` dependency [(#829)](https://github.com/getsentry/sentry-laravel/pull/829) ## 4.1.1 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.1.1. ### Bug Fixes - Fix missing `sentryMonitor()` macro when command is called outside the CLI environment [(#812)](https://github.com/getsentry/sentry-laravel/pull/812) - Don't call `terminating()` in Lumen apps below 9.1.4 [(#815)](https://github.com/getsentry/sentry-laravel/pull/815) ## 4.1.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v4.1.0. ### Features - Capture SQL query bindings (parameters) in SQL query spans [(#804)](https://github.com/getsentry/sentry-laravel/pull/804) To enable this feature, update your `config/sentry.php` file or set the `SENTRY_TRACE_SQL_BINDINGS_ENABLED` environment variable to `true`. ```php 'tracing' => [ 'sql_bindings' => true, ], ``` ### Misc - Unify backtrace origin span attributes [(#803)](https://github.com/getsentry/sentry-laravel/pull/803) - Add `ignore_exceptions` & `ignore_transactions` to default config [(#802)](https://github.com/getsentry/sentry-laravel/pull/802) ## 4.0.0 The Sentry SDK team is thrilled to announce the immediate availability of Sentry Laravel SDK v4.0.0. ### Breaking Change This version adds support for the underlying [Sentry PHP SDK v4.0](https://github.com/getsentry/sentry-php). Please refer to the PHP SDK [sentry-php/UPGRADE-4.0.md](https://github.com/getsentry/sentry-php/blob/master/UPGRADE-4.0.md) guide for a complete list of breaking changes. - This version exclusively uses the [envelope endpoint](https://develop.sentry.dev/sdk/envelopes/) to send event data to Sentry. If you are using [sentry.io](https://sentry.io), no action is needed. If you are using an on-premise/self-hosted installation of Sentry, the minimum requirement is now version `>= v20.6.0`. - You need to have `ext-curl` installed to use the SDK. - The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. ```php // config/sentry.php 'ignore_exceptions' => [BadThingsHappenedException::class], ``` This option performs an [`is_a`](https://www.php.net/manual/en/function.is-a.php) check now, so you can also ignore more generic exceptions. ### Features - Enable distributed tracing for outgoing HTTP client requests [(#797)](https://github.com/getsentry/sentry-laravel/pull/797) This feature is only available on Laravel >= 10.14. When making a request using the Laravel `Http` facade, we automatically attach the `sentry-trace` and `baggage` headers. This behaviour can be controlled by setting `trace_propagation_targets` in your `config/sentry.php` file. ```php // config/sentry.php // All requests will contain the tracing headers. This is the default behaviour. 'trace_propagation_targets' => null, // To turn this feature off completely, set the option to an empty array. 'trace_propagation_targets' => [], // To only attach these headers to some requests, you can allow-list certain hosts. 'trace_propagation_targets' => [ 'examlpe.com', 'api.examlpe.com', ], ``` Please make sure to remove any custom code that injected these headers previously. If you are using the `Sentry\Tracing\GuzzleTracingMiddleware` provided by our underlying PHP SDK, you must also remove it. - Add support for Laravel Livewire 3 [(#798)](https://github.com/getsentry/sentry-laravel/pull/798) The SDK now creates traces and breadcrumbs for Livewire 3 as well. Both the class-based and Volt usage are supported. ```php // config/sentry.php 'breadcrumbs' => [ // Capture Livewire components in breadcrumbs 'livewire' => true, ], 'tracing' => [ // Capture Livewire components as spans 'livewire' => true, ], ``` - Add new fluent APIs [(#1601)](https://github.com/getsentry/sentry-php/pull/1601) ```php // Before $spanContext = new SpanContext(); $spanContext->setDescription('myFunction'); $spanContext->setOp('function'); // After $spanContext = (new SpanContext()) ->setDescription('myFunction'); ->setOp('function'); ``` - Simplify the breadcrumb API [(#1603)](https://github.com/getsentry/sentry-php/pull/1603) ```php // Before \Sentry\addBreadcrumb( new \Sentry\Breadcrumb( \Sentry\Breadcrumb::LEVEL_INFO, \Sentry\Breadcrumb::TYPE_DEFAULT, 'auth', // category 'User authenticated', // message (optional) ['user_id' => $userId] // data (optional) ) ); // After \Sentry\addBreadcrumb( category: 'auth', message: 'User authenticated', // optional metadata: ['user_id' => $userId], // optional level: Breadcrumb::LEVEL_INFO, // set by default type: Breadcrumb::TYPE_DEFAULT, // set by default ); ``` - New default cURL HTTP client [(#1589)](https://github.com/getsentry/sentry-php/pull/1589) ### Misc - The abandoned package `php-http/message-factory` was removed. PK\sW]¢#úÄÞÞ composer.jsonnu„[µü¤{ "name": "sentry/sentry-laravel", "type": "library", "description": "Laravel SDK for Sentry (https://sentry.io)", "keywords": [ "sentry", "laravel", "log", "logging", "error-monitoring", "error-handler", "crash-reporting", "crash-reports", "profiling", "tracing" ], "homepage": "https://sentry.io", "license": "MIT", "authors": [ { "name": "Sentry", "email": "accounts@sentry.io" } ], "require": { "php": "^7.2 | ^8.0", "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "sentry/sentry": "^4.16.0", "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0", "nyholm/psr7": "^1.0" }, "autoload": { "psr-0": { "Sentry\\Laravel\\": "src/" } }, "require-dev": { "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4 | ^11.5", "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", "livewire/livewire": "^2.0 | ^3.0", "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", "friendsofphp/php-cs-fixer": "^3.11", "mockery/mockery": "^1.3", "phpstan/phpstan": "^1.10", "laravel/folio": "^1.1", "guzzlehttp/guzzle": "^7.2" }, "autoload-dev": { "psr-4": { "Sentry\\Laravel\\Tests\\": "test/Sentry/" } }, "scripts": { "check": [ "@cs-check", "@phpstan", "@tests" ], "tests": "vendor/bin/phpunit", "cs-check": "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", "cs-fix": "vendor/bin/php-cs-fixer fix --verbose --diff", "phpstan": "vendor/bin/phpstan analyse" }, "extra": { "laravel": { "providers": [ "Sentry\\Laravel\\ServiceProvider", "Sentry\\Laravel\\Tracing\\ServiceProvider" ], "aliases": { "Sentry": "Sentry\\Laravel\\Facade" } } }, "config": { "sort-packages": true }, "minimum-stability": "dev", "prefer-stable": true } PK\sW]ïk¦JJconfig/sentry.phpnu„[µü¤ env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')), // @see https://spotlightjs.com/ // 'spotlight' => env('SENTRY_SPOTLIGHT', false), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#logger // 'logger' => Sentry\Logger\DebugFileLogger::class, // By default this will log to `storage_path('logs/sentry.log')` // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) 'release' => env('SENTRY_RELEASE'), // When left empty or `null` the Laravel environment will be used (usually discovered from `APP_ENV` in your `.env`) 'environment' => env('SENTRY_ENVIRONMENT'), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#sample_rate 'sample_rate' => env('SENTRY_SAMPLE_RATE') === null ? 1.0 : (float) env('SENTRY_SAMPLE_RATE'), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces_sample_rate 'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_TRACES_SAMPLE_RATE'), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#profiles-sample-rate 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float) env('SENTRY_PROFILES_SAMPLE_RATE'), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#enable_logs 'enable_logs' => env('SENTRY_ENABLE_LOGS', false), // The minimum log level that will be sent to Sentry as logs using the `sentry_logs` logging channel 'logs_channel_level' => env('SENTRY_LOG_LEVEL', env('SENTRY_LOGS_LEVEL', env('LOG_LEVEL', 'debug'))), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send_default_pii 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore_exceptions // 'ignore_exceptions' => [], // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#ignore_transactions 'ignore_transactions' => [ // Ignore Laravel's default health URL '/up', ], // Breadcrumb specific configuration 'breadcrumbs' => [ // Capture Laravel logs as breadcrumbs 'logs' => env('SENTRY_BREADCRUMBS_LOGS_ENABLED', true), // Capture Laravel cache events (hits, writes etc.) as breadcrumbs 'cache' => env('SENTRY_BREADCRUMBS_CACHE_ENABLED', true), // Capture Livewire components like routes as breadcrumbs 'livewire' => env('SENTRY_BREADCRUMBS_LIVEWIRE_ENABLED', true), // Capture SQL queries as breadcrumbs 'sql_queries' => env('SENTRY_BREADCRUMBS_SQL_QUERIES_ENABLED', true), // Capture SQL query bindings (parameters) in SQL query breadcrumbs 'sql_bindings' => env('SENTRY_BREADCRUMBS_SQL_BINDINGS_ENABLED', false), // Capture queue job information as breadcrumbs 'queue_info' => env('SENTRY_BREADCRUMBS_QUEUE_INFO_ENABLED', true), // Capture command information as breadcrumbs 'command_info' => env('SENTRY_BREADCRUMBS_COMMAND_JOBS_ENABLED', true), // Capture HTTP client request information as breadcrumbs 'http_client_requests' => env('SENTRY_BREADCRUMBS_HTTP_CLIENT_REQUESTS_ENABLED', true), // Capture send notifications as breadcrumbs 'notifications' => env('SENTRY_BREADCRUMBS_NOTIFICATIONS_ENABLED', true), ], // Performance monitoring specific configuration 'tracing' => [ // Trace queue jobs as their own transactions (this enables tracing for queue jobs) 'queue_job_transactions' => env('SENTRY_TRACE_QUEUE_ENABLED', true), // Capture queue jobs as spans when executed on the sync driver 'queue_jobs' => env('SENTRY_TRACE_QUEUE_JOBS_ENABLED', true), // Capture SQL queries as spans 'sql_queries' => env('SENTRY_TRACE_SQL_QUERIES_ENABLED', true), // Capture SQL query bindings (parameters) in SQL query spans 'sql_bindings' => env('SENTRY_TRACE_SQL_BINDINGS_ENABLED', false), // Capture where the SQL query originated from on the SQL query spans 'sql_origin' => env('SENTRY_TRACE_SQL_ORIGIN_ENABLED', true), // Define a threshold in milliseconds for SQL queries to resolve their origin 'sql_origin_threshold_ms' => env('SENTRY_TRACE_SQL_ORIGIN_THRESHOLD_MS', 100), // Capture views rendered as spans 'views' => env('SENTRY_TRACE_VIEWS_ENABLED', true), // Capture Livewire components as spans 'livewire' => env('SENTRY_TRACE_LIVEWIRE_ENABLED', true), // Capture HTTP client requests as spans 'http_client_requests' => env('SENTRY_TRACE_HTTP_CLIENT_REQUESTS_ENABLED', true), // Capture Laravel cache events (hits, writes etc.) as spans 'cache' => env('SENTRY_TRACE_CACHE_ENABLED', true), // Capture Redis operations as spans (this enables Redis events in Laravel) 'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false), // Capture where the Redis command originated from on the Redis command spans 'redis_origin' => env('SENTRY_TRACE_REDIS_ORIGIN_ENABLED', true), // Capture send notifications as spans 'notifications' => env('SENTRY_TRACE_NOTIFICATIONS_ENABLED', true), // Enable tracing for requests without a matching route (404's) 'missing_routes' => env('SENTRY_TRACE_MISSING_ROUTES_ENABLED', false), // Configures if the performance trace should continue after the response has been sent to the user until the application terminates // This is required to capture any spans that are created after the response has been sent like queue jobs dispatched using `dispatch(...)->afterResponse()` for example 'continue_after_response' => env('SENTRY_TRACE_CONTINUE_AFTER_RESPONSE', true), // Enable the tracing integrations supplied by Sentry (recommended) 'default_integrations' => env('SENTRY_TRACE_DEFAULT_INTEGRATIONS_ENABLED', true), ], ]; PK\sW] á22CONTRIBUTING.mdnu„[µü¤

Sentry

# Contributing to the Sentry SDK for Laravel We welcome contributions to `sentry-laravel` by the community. Please search the [issue tracker](https://github.com/getsentry/sentry-laravel/issues) before creating a new issue (a problem or an improvement request). Please also ask in our [Sentry Community on Discord](https://discord.com/invite/Ww9hbqr) before submitting a new issue. There is a ton of great people in our Discord community ready to help you! If you feel that you can fix or implement it yourself, please read on to learn how to submit your changes. ## Submitting changes - Setup the development environment. - Clone the `sentry-laravel` repository and prepare necessary changes. - Add tests for your changes to `tests/`. - Run tests and make sure all of them pass. - Submit a pull request, referencing any issues it addresses. - Make sure to update the `CHANGELOG.md` file below the `Unreleased` heading. We will review your pull request as soon as possible. Thank you for contributing! ## Development environment ### Requirements Make sure that you have PHP 7.2+ installed. Version 7.4 or higher is required to run style checkers. On macOS, we recommend using brew to install PHP. For Windows, we recommend an official PHP.net release. You may use [make](https://www.gnu.org/software/make) to take advantage of the provided [Makefile](Makefile). ### Clone the repository ```bash git clone git@github.com:getsentry/sentry-laravel.git ``` ### Install the dependencies Dependencies are managed through [Composer](https://getcomposer.org). ```bash composer install ``` ### Running tests Tests can be run via [PHPUnit](https://phpunit.de). ```bash composer tests ``` ### Static analysis Static analysis can be run via [PHPStan](https://phpstan.org). ```bash composer phpstan ``` ### Code style The code is automatically formatted through [php-cs-fixer](https://cs.symfony.com). ```bash composer cs-fix ``` ## Releasing a new version (only relevant for Sentry employees) Prerequisites: - All changes that should be released must be in the `master` branch. - Every commit should follow the [Commit Message Format](https://develop.sentry.dev/commit-messages#commit-message-format) convention. Manual Process: - Update CHANGELOG.md with the version to be released. Example commit: https://github.com/getsentry/sentry-laravel/commit/0c0aabd4976905e279c9e49193265dd51856c219. - On GitHub in the `sentry-laravel` repository go to "Actions" select the "Release" workflow. - Click on "Run workflow" on the right side and make sure the `master` branch is selected. - Set "Version to release" input field. Here you decide if it is a major, minor or patch release. (See "Versioning Policy" below) - Click "Run Workflow" This will trigger [Craft](https://github.com/getsentry/craft) to prepare everything needed for a release. (For more information see [craft prepare](https://github.com/getsentry/craft#craft-prepare-preparing-a-new-release)) At the end of this process, a release issue is created in the [Publish](https://github.com/getsentry/publish) repository. (Example release issue: https://github.com/getsentry/publish/issues/815) Now one of the persons with release privileges (most probably your engineering manager) will review this Issue and then add the `accepted` label to the issue. There are always two persons involved in a release. If you are in a hurry and the release should be out immediately there is a Slack channel called `#proj-release-approval` where you can see your release issue and where you can ping people to please have a look immediately. When the release issue is labeled `accepted` [Craft](https://github.com/getsentry/craft) is triggered again to publish the release to all the right platforms. (See [craft publish](https://github.com/getsentry/craft#craft-publish-publishing-the-release) for more information). At the end of this process, the release issue on GitHub will be closed and the release is completed! Congratulations! There is a sequence diagram visualizing all this in the [README.md](https://github.com/getsentry/publish) of the `Publish` repository. ### Versioning Policy This project follows [semver](https://semver.org), with three additions: - Semver says that major version `0` can include breaking changes at any time. Still, it is common practice to assume that only `0.x` releases (minor versions) can contain breaking changes while `0.x.y` releases (patch versions) are used for backwards-compatible changes (bugfixes and features). This project also follows that practice. - All undocumented APIs are considered internal. They are not part of this contract. - Certain features (e.g. integrations) may be explicitly called out as "experimental" or "unstable" in the documentation. They come with their own versioning policy described in the documentation. We recommend pinning your version requirements against `1.x.*` or `1.x.y`. Either one of the following is fine: ```json "sentry/sentry": "^1.0", "sentry/sentry": "^1", ``` A major release `N` implies the previous release `N-1` will no longer receive updates. We generally do not backport bugfixes to older versions unless they are security relevant. However, feel free to ask for backports of specific commits on the bug tracker. ## Commit message format guidelines See the documentation on commit messages here: https://develop.sentry.dev/commit-messages/#commit-message-format PK\sW]¥­”DD codecov.ymlnu„[µü¤comment: false coverage: status: project: off patch: off PK\sW]§J£zDDLICENSEnu„[µü¤PK\sW]7öØ {README.mdnu„[µü¤PK\sW]�c¥Ç.php-cs-fixer.phpnu„[µü¤PK\sW] ½AYY phpstan.neonnu„[µü¤PK\sW]ö¹*Ä))³phpstan-baseline.neonnu„[µü¤PK\sW]Fÿ}4››!<src/Sentry/Laravel/Version.phpnu„[µü¤PK\sW]„u^œ(($ =src/Sentry/Laravel/SentryHandler.phpnu„[µü¤PK\sW]±û--'xesrc/Sentry/Laravel/Logs/LogsHandler.phpnu„[µü¤PK\sW]xÈï�  &üssrc/Sentry/Laravel/Logs/LogChannel.phpnu„[µü¤PK\sW]«æ„¤ÚÚ!\xsrc/Sentry/Laravel/LogChannel.phpnu„[µü¤PK\sW]6è Àp/p/"‡}src/Sentry/Laravel/Integration.phpnu„[µü¤PK\sW]í…×ó'8'8#I­src/Sentry/Laravel/EventHandler.phpnu„[µü¤PK\sW]Œ @­­6Ãåsrc/Sentry/Laravel/Console/AboutCommandIntegration.phpnu„[µü¤PK\sW]oº‘¼¼-Öísrc/Sentry/Laravel/Console/PublishCommand.phpnu„[µü¤PK\sW]®“F2c#c#*ïsrc/Sentry/Laravel/Console/TestCommand.phpnu„[µü¤PK\sW]™wª__I¬&src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.phpnu„[µü¤PK\sW]Ðî æ¨¨G„*src/Sentry/Laravel/Tracing/Routing/TracingCallableDispatcherTracing.phpnu„[µü¤PK\sW]¤ë]HËË?£-src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.phpnu„[µü¤PK\sW]Âj9¥¨ ¨ .Ý2src/Sentry/Laravel/Tracing/BacktraceHelper.phpnu„[µü¤PK\sW]Þ½õ^Z Z Aã?src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.phpnu„[µü¤PK\sW]ù�;dÝÝ2®`src/Sentry/Laravel/Tracing/ViewEngineDecorator.phpnu„[µü¤PK\sW]͵ ##+ífsrc/Sentry/Laravel/Tracing/EventHandler.phpnu„[µü¤PK\sW]º¹2uFF2JŠsrc/Sentry/Laravel/Tracing/TransactionFinisher.phpnu„[µü¤PK\sW]/âU.å#å#)òŽsrc/Sentry/Laravel/Tracing/Middleware.phpnu„[µü¤PK\sW]/äèQÒÒ.0³src/Sentry/Laravel/Tracing/ServiceProvider.phpnu„[µü¤PK\sW]eãrGG2`Èsrc/Sentry/Laravel/Http/SetRequestIpMiddleware.phpnu„[µü¤PK\sW]Ý[©Ïãã0 Îsrc/Sentry/Laravel/Http/SetRequestMiddleware.phpnu„[µü¤PK\sW]í£øÀll1LÓsrc/Sentry/Laravel/Http/FlushEventsMiddleware.phpnu„[µü¤PK\sW]-&‹fûû1Õsrc/Sentry/Laravel/Http/LaravelRequestFetcher.phpnu„[µü¤PK\sW]‰Õ½ÂDD*uÜsrc/Sentry/Laravel/BaseServiceProvider.phpnu„[µü¤PK\sW]Κff<ãsrc/Sentry/Laravel/Integration/LaravelContextIntegration.phpnu„[µü¤PK\sW]°’ õóóVåçsrc/Sentry/Laravel/Integration/ModelViolations/DiscardedAttributeViolationReporter.phpnu„[µü¤PK\sW]XÝ™1vvT^ësrc/Sentry/Laravel/Integration/ModelViolations/LazyLoadingModelViolationReporter.phpnu„[µü¤PK\sW]ãý(ѳ ³ IXðsrc/Sentry/Laravel/Integration/ModelViolations/ModelViolationReporter.phpnu„[µü¤PK\sW]*«\ssY„þsrc/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.phpnu„[µü¤PK\sW]ꜗNèè>€src/Sentry/Laravel/Integration/ExceptionContextIntegration.phpnu„[µü¤PK\sW]˜ù;ÇFÇF&Ösrc/Sentry/Laravel/ServiceProvider.phpnu„[µü¤PK\sW]Äø ²$²$0óLsrc/Sentry/Laravel/Features/QueueIntegration.phpnu„[µü¤PK\sW]Ñ­(œ œ 2rsrc/Sentry/Laravel/Features/ConsoleIntegration.phpnu„[µü¤PK\sW]›}ËhÊÊ5}src/Sentry/Laravel/Features/HttpClientIntegration.phpnu„[µü¤PK\sW])?#€€C2›src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.phpnu„[µü¤PK\sW]As/B7%£src/Sentry/Laravel/Features/Concerns/WorksWithSpans.phpnu„[µü¤PK\sW]ÒYö*¦¦<ަsrc/Sentry/Laravel/Features/Concerns/ResolvesEventOrigin.phpnu„[µü¤PK\sW]‘.l€OO' ¬src/Sentry/Laravel/Features/Feature.phpnu„[µü¤PK\sW]×ó++:F½src/Sentry/Laravel/Features/LivewirePackageIntegration.phpnu„[µü¤PK\sW]ØŸºµøø@ÛÜsrc/Sentry/Laravel/Features/Storage/CloudFilesystemDecorator.phpnu„[µü¤PK\sW]GÙ™Fèè=CÞsrc/Sentry/Laravel/Features/Storage/SentryCloudFilesystem.phpnu„[µü¤PK\sW]¯D\G{{9˜àsrc/Sentry/Laravel/Features/Storage/SentryS3V3Adapter.phpnu„[µü¤PK\sW]¦Îž pp?|ãsrc/Sentry/Laravel/Features/Storage/SentryFilesystemAdapter.phpnu„[µü¤PK\sW]Êe¬MM;[æsrc/Sentry/Laravel/Features/Storage/FilesystemDecorator.phpnu„[µü¤PK\sW]òò¾¾3src/Sentry/Laravel/Features/Storage/Integration.phpnu„[µü¤PK\sW]òµzçíí84src/Sentry/Laravel/Features/Storage/SentryFilesystem.phpnu„[µü¤PK\sW]2 ž›xxB‰src/Sentry/Laravel/Features/Storage/FilesystemAdapterDecorator.phpnu„[µü¤PK\sW]ìx.I,I,<s!src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.phpnu„[µü¤PK\sW]Ü:?°..0(Nsrc/Sentry/Laravel/Features/CacheIntegration.phpnu„[µü¤PK\sW]'È   8–|src/Sentry/Laravel/Features/NotificationsIntegration.phpnu„[µü¤PK\sW]º”ŒRR.‰src/Sentry/Laravel/Features/LogIntegration.phpnu„[µü¤PK\sW]—¨Þ‰õõ7´‹src/Sentry/Laravel/Features/FolioPackageIntegration.phpnu„[µü¤PK\sW]È *q$“src/Sentry/Laravel/Util/Filesize.phpnu„[µü¤PK\sW]ά^^u–src/Sentry/Laravel/Facade.phpnu„[µü¤PK\sW]ÚXe?W?W  �CHANGELOG.mdnu„[µü¤PK\sW]¢#úÄÞÞ ›ôcomposer.jsonnu„[µü¤PK\sW]ïk¦JJ¶ýconfig/sentry.phpnu„[µü¤PK\sW] á22ACONTRIBUTING.mdnu„[µü¤PK\sW]¥­”DD ²-codecov.ymlnu„[µü¤PKAA1.