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î%W],Á† DDsentry/LICENSEnu„[µü¤MIT License Copyright (c) 2012 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î%W]FoÐ?® ® sentry/UPGRADE-4.0.mdnu„[µü¤# Upgrade 3.x to 4.0 - 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`. - Added `ext-curl` as a composer requirement. - The `IgnoreErrorsIntegration` integration was removed. Use the `ignore_exceptions` option instead. ```php Sentry\init([ 'ignore_exceptions' => [BadThingsHappenedException::class], ]); ``` This option performs an [`is_a`](https://www.php.net/manual/en/function.is-a.php) check, so you can also ignore more generic exceptions. - Removed support for `symfony/options-resolver: ^3.4.43`. - The `RequestFetcher` now relies on `guzzlehttp/psr7: ^1.8.4|^2.1.1`. - Added new methods to `ClientInterface` ```php public function getCspReportUrl(): ?string; public function getStacktraceBuilder(): StacktraceBuilder; ``` - Added new methods to `HubInterface` ```php public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string; ``` - The new default value for the `trace_propagation_targets` option is now `null`. To not attach any headers to outgoing requests, using the `GuzzleTracingMiddleware`, set this option to `[]`. - The `ignore_exceptions` option now performs a `is_a` check on the provided class strings. - The `send_attempts` option was removed. You may implement a custom transport if you rely on this behaviour. - The `enable_compression` option was removed. Use `http_compression` instead. - The `logger` option now accepts a `Psr\Log\LoggerInterface` instance instead of `string`. - Removed `Options::getSendAttempts/setSendAttempts()`. - Removed `Options::isCompressionEnabled/setEnableCompression()`. Use `Options::isHttpCompressionEnabled/setEnableHttpCompression()` instead. - Removed `SpanContext::fromTraceparent()`. Use `Sentry\continueTrace()` instead. - Removed `TransactionContext::fromSentryTrace()`. Use `Sentry\continueTrace()` instead. - Removed `Sentry\Exception\InvalidArgumentException`. Use `\InvalidArgumentException` instead. - Removed `Sentry\Exception/ExceptionInterface`. - Removed `ClientBuilderInterface()`. - Removed `ClientBuilder::setSerializer()`. - Removed `ClientBuilder::setTransportFactory()`. You can set a custom transport via the `transport` option. - Removed `Client::__construct()` parameter `SerializerInterface $serializer`. - Removed `TransportFactoryInterface`. - Removed `DefaultTransportFactory`. - Removed `HttpClientFactoryInterface`. - Removed `HttpClientFactory`. - Removed `NullTransport`. - Removed `Dsn::getSecretKey()`. - Removed `Dsn::setSecretKey()`. - Removed `Dsn::getStoreApiEndpointUrl()`. - Removed `EventType::default()`. - Removed adding the value of the `logger` option as a tag on the event. If you rely on this behaviour, add the tag manually. - Added return type to `Dsn::getProjectId(): string`. - Changed return type to `Options::getLogger(): ?LoggerInterface`. - Changed parameter type of `Options::setLogger(LoggerInterface $logger)`. PKî%W]Æ™Zö®®sentry/README.mdnu„[µü¤
Sentry for PHP
_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 PHP [![CI](https://github.com/getsentry/sentry-php/workflows/CI/badge.svg?branch=master)](https://github.com/getsentry/sentry-php/actions?query=workflow%3ACI+branch%3Amaster) [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-php/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-php/branch/master) [![Latest Stable Version](https://poser.pugx.org/sentry/sentry/v/stable)](https://packagist.org/packages/sentry/sentry) [![License](https://poser.pugx.org/sentry/sentry/license)](https://packagist.org/packages/sentry/sentry) [![Total Downloads](https://poser.pugx.org/sentry/sentry/downloads)](https://packagist.org/packages/sentry/sentry) [![Monthly Downloads](https://poser.pugx.org/sentry/sentry/d/monthly)](https://packagist.org/packages/sentry/sentry) [![Discord](https://img.shields.io/discord/621778831602221064)](https://discord.gg/cWnMQeA) The Sentry PHP error reporter tracks errors and exceptions that happen during the execution of your application and provides instant notification with detailed information needed to prioritize, identify, reproduce and fix each issue. ## Getting started ### Install Install the SDK using [Composer](https://getcomposer.org/). ```bash composer require sentry/sentry ``` ### Configuration Initialize the SDK as early as possible in your application. ```php \Sentry\init(['dsn' => '___PUBLIC_DSN___' ]); ``` ### Usage ```php try { thisFunctionThrows(); // -> throw new \Exception('foo bar'); } catch (\Exception $exception) { \Sentry\captureException($exception); } ``` ## Official integrations The following integrations are fully supported and maintained by the Sentry team. - [Symfony](https://github.com/getsentry/sentry-symfony) - [Laravel](https://github.com/getsentry/sentry-laravel) ## 3rd party integrations using SDK 4.x The following integrations are available and maintained by members of the Sentry community. - [Drupal](https://www.drupal.org/project/raven) - [WordPress](https://wordpress.org/plugins/wp-sentry-integration/) - Magento 2 by [JustBetter](https://github.com/justbetter/magento2-sentry) or by [Mygento](https://github.com/mygento/module-sentry) - [Joomla!](https://github.com/AlterBrains/sentry-joomla) - ... feel free to be famous, create a port to your favourite platform! ## 3rd party integrations using the old SDK 3.x - [Neos Flow](https://github.com/flownative/flow-sentry) - [ZendFramework](https://github.com/facile-it/sentry-module) - [Yii2](https://github.com/notamedia/yii2-sentry) - [Silverstripe](https://github.com/phptek/silverstripe-sentry) - [CakePHP 3.0 - 4.3](https://github.com/Connehito/cake-sentry) - [CakePHP 4.4+](https://github.com/lordsimal/cakephp-sentry) - [October CMS](https://github.com/OFFLINE-GmbH/oc-sentry-plugin) ## 3rd party integrations using the old SDK 2.x - [Neos Flow](https://github.com/networkteam/Networkteam.SentryClient) - [OXID eShop](https://github.com/OXIDprojects/sentry) - [TYPO3](https://github.com/networkteam/sentry_client) - [CakePHP](https://github.com/Connehito/cake-sentry/tree/3.x) ## 3rd party integrations using the old SDK 1.x - [Neos CMS](https://github.com/networkteam/Netwokteam.Neos.SentryClient) - [OpenCart](https://github.com/BurdaPraha/oc_sentry) - [TYPO3](https://github.com/networkteam/sentry_client/tree/2.1.1) ## Community - [Documentation](https://docs.sentry.io/error-reporting/quickstart/?platform=php) - [Bug Tracker](http://github.com/getsentry/sentry-php/issues) - [Code](http://github.com/getsentry/sentry-php) ## Contributing to the SDK Please make sure to read the [CONTRIBUTING.md](CONTRIBUTING.md) before making a pull request. Thanks to everyone who has contributed to this project so far. ## Getting help/support If you need help setting up or configuring the PHP SDK (or anything else in the Sentry universe) please head over to the [Sentry Community on Discord](https://discord.com/invite/sentry). 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î%W]U>-ãžžsentry/AUTHORSnu„[µü¤The Sentry PHP SDK was originally written by Michael van Tellingen and is maintained by the Sentry Team. http://github.com/getsentry/sentry-php/contributors PKî%W]{>…öîîsentry/scripts/bump-version.shnu„[µü¤#!/bin/bash set -eux if [ "$(uname -s)" != "Linux" ]; then echo "Please use the GitHub Action." exit 1 fi SCRIPT_DIR="$( dirname "$0" )" cd $SCRIPT_DIR/.. OLD_VERSION="${1}" NEW_VERSION="${2}" echo "Current version: $OLD_VERSION" echo "Bumping version: $NEW_VERSION" function replace() { ! grep "$2" $3 perl -i -pe "s/$1/$2/g" $3 grep "$2" $3 # verify that replacement was successful } replace "SDK_VERSION = '[0-9.]+'" "SDK_VERSION = '$NEW_VERSION'" ./src/Client.phpPKî%W]!-Ôrùùsentry/src/SentrySdk.phpnu„[µü¤ */ final class SentrySdk { /** * @var HubInterface|null The current hub */ private static $currentHub; /** * Constructor. */ private function __construct() { } /** * Initializes the SDK by creating a new hub instance each time this method * gets called. */ public static function init(): HubInterface { self::$currentHub = new Hub(); return self::$currentHub; } /** * Gets the current hub. If it's not initialized then creates a new instance * and sets it as current hub. */ public static function getCurrentHub(): HubInterface { if (self::$currentHub === null) { self::$currentHub = new Hub(); } return self::$currentHub; } /** * Sets the current hub. * * @param HubInterface $hub The hub to set */ public static function setCurrentHub(HubInterface $hub): HubInterface { self::$currentHub = $hub; return $hub; } } PKî%W]?8¢´ÌÌ4sentry/src/Serializer/PayloadSerializerInterface.phpnu„[µü¤maxDepth = $maxDepth; if ($mbDetectOrder != null) { $this->mbDetectOrder = $mbDetectOrder; } $this->options = $options; } /** * Serialize an object (recursively) into something safe for data * sanitization and encoding. * * @param mixed $value * * @return string|bool|float|int|mixed[]|null */ protected function serializeRecursively($value, int $_depth = 0) { try { if ($_depth >= $this->maxDepth) { return $this->serializeValue($value); } try { if (@\is_callable($value)) { return $this->serializeCallable($value); } } catch (\Throwable $exception) { // Do nothing on purpose } if (\is_array($value)) { $serializedArray = []; foreach ($value as $k => $v) { $serializedArray[$k] = $this->serializeRecursively($v, $_depth + 1); } return $serializedArray; } if (\is_object($value)) { $classSerializers = $this->resolveClassSerializers($value); // Try each serializer until there is none left or the serializer returned data foreach ($classSerializers as $classSerializer) { try { $serializedObjectData = $classSerializer($value); if (\is_array($serializedObjectData)) { return [ 'class' => \get_class($value), 'data' => $this->serializeRecursively($serializedObjectData, $_depth + 1), ]; } } catch (\Throwable $e) { // Ignore any exceptions generated by a class serializer } } if ($value instanceof \DateTimeInterface) { return $this->formatDate($value); } if ($this->serializeAllObjects || ($value instanceof \stdClass)) { return $this->serializeObject($value, $_depth); } } return $this->serializeValue($value); } catch (\Throwable $error) { if (\is_string($value)) { return $value . ' {serialization error}'; } return '{serialization error}'; } } /** * Find class serializers for a object. * * Registered serializers with the `class_serializers` option take precedence over * objects implementing the `SerializableInterface`. * * @param object $object * * @return array */ protected function resolveClassSerializers($object): array { $serializers = []; foreach ($this->options->getClassSerializers() as $type => $serializer) { if ($object instanceof $type) { $serializers[] = $serializer; } } if ($object instanceof SerializableInterface) { $serializers[] = static function (SerializableInterface $object): ?array { return $object->toSentry(); }; } return $serializers; } /** * @param object $object * @param string[] $hashes * * @return mixed[]|string|bool|float|int|null */ protected function serializeObject($object, int $_depth = 0, array $hashes = []) { if ($_depth >= $this->maxDepth || \in_array(spl_object_hash($object), $hashes, true)) { return $this->serializeValue($object); } $hashes[] = spl_object_hash($object); $serializedObject = []; foreach ($object as $key => &$value) { if (\is_object($value)) { $serializedObject[$key] = $this->serializeObject($value, $_depth + 1, $hashes); } else { $serializedObject[$key] = $this->serializeRecursively($value, $_depth + 1); } } return $serializedObject; } /** * Serializes the given value to a string. * * @param string $value The value to serialize */ protected function serializeString(string $value): string { if ($value === '') { return ''; } // we always guarantee this is coerced, even if we can't detect encoding if ($currentEncoding = mb_detect_encoding($value, $this->mbDetectOrder)) { $encoded = mb_convert_encoding($value, 'UTF-8', $currentEncoding) ?: ''; } else { $encoded = mb_convert_encoding($value, 'UTF-8') ?: ''; } if (mb_strlen($encoded) > $this->options->getMaxValueLength()) { $encoded = mb_substr($encoded, 0, $this->options->getMaxValueLength() - 10, 'UTF-8') . ' {clipped}'; } return $encoded; } /** * @param mixed $value * * @return string|bool|float|int|null */ protected function serializeValue($value) { if (($value === null) || \is_bool($value) || is_numeric($value)) { return $value; } if ($value instanceof \UnitEnum) { $reflection = new \ReflectionObject($value); return 'Enum ' . $reflection->getName() . '::' . $value->name; } if (\is_object($value)) { $reflection = new \ReflectionObject($value); $objectId = null; if ($reflection->hasProperty('id') && ($idProperty = $reflection->getProperty('id'))->isPublic()) { $objectId = $idProperty->getValue($value); } elseif ($reflection->hasMethod('getId') && ($getIdMethod = $reflection->getMethod('getId'))->isPublic()) { try { $objectId = $getIdMethod->invoke($value); } catch (\Throwable $e) { // Do nothing on purpose } } return 'Object ' . $reflection->getName() . (\is_scalar($objectId) ? '(#' . $objectId . ')' : ''); } if (\is_resource($value)) { return 'Resource ' . get_resource_type($value); } try { if (@\is_callable($value)) { return $this->serializeCallable($value); } } catch (\Throwable $exception) { // Do nothing on purpose } if (\is_array($value)) { return 'Array of length ' . \count($value); } if (\is_string($value) || (\is_object($value) && method_exists($value, '__toString'))) { return $this->serializeString((string) $value); } return null; } private function formatDate(\DateTimeInterface $date): string { $hasMicroseconds = $date->format('u') !== '000000'; $formatted = $hasMicroseconds ? $date->format('Y-m-d H:i:s.u') : $date->format('Y-m-d H:i:s'); $className = \get_class($date); $timezone = $date->getTimezone(); if ($timezone && $timezone->getName() !== 'UTC') { $formatted .= ' ' . $date->format('eP'); } return "$className($formatted)"; } /** * @param callable|mixed $callable */ protected function serializeCallable($callable): string { if (\is_string($callable)) { return $callable; } if (!\is_callable($callable)) { throw new \InvalidArgumentException(\sprintf('Expecting callable, got %s', \is_object($callable) ? \get_class($callable) : \gettype($callable))); } try { if (\is_array($callable)) { $reflection = new \ReflectionMethod($callable[0], $callable[1]); $class = $reflection->getDeclaringClass(); } elseif ($callable instanceof \Closure) { $reflection = new \ReflectionFunction($callable); $class = null; } elseif (\is_object($callable) && method_exists($callable, '__invoke')) { $reflection = new \ReflectionMethod($callable, '__invoke'); $class = $reflection->getDeclaringClass(); } else { throw new \InvalidArgumentException('Unrecognized type of callable'); } } catch (\ReflectionException $exception) { return '{unserializable callable, reflection error}'; } $callableType = $reflection->isClosure() ? 'Lambda ' : 'Callable '; $callableReturnType = $reflection->getReturnType(); if ($callableReturnType instanceof \ReflectionNamedType) { $callableType .= $callableReturnType->getName() . ' '; } if ($class) { $callableType .= $class->getName() . '::'; } return $callableType . $reflection->getName() . ' ' . $this->serializeCallableParameters($reflection); } private function serializeCallableParameters(\ReflectionFunctionAbstract $reflection): string { $params = []; foreach ($reflection->getParameters() as &$param) { $reflectionType = $param->getType(); if ($reflectionType instanceof \ReflectionNamedType) { $paramType = $reflectionType->getName(); } else { $paramType = 'mixed'; } if ($param->allowsNull()) { $paramType .= '|null'; } $paramName = ($param->isPassedByReference() ? '&' : '') . $param->getName(); if ($param->isOptional()) { $paramName = '[' . $paramName . ']'; } $params[] = $paramType . ' ' . $paramName; } return '[' . implode('; ', $params) . ']'; } public function getMbDetectOrder(): string { return $this->mbDetectOrder; } /** * @return $this */ public function setMbDetectOrder(string $mbDetectOrder): self { $this->mbDetectOrder = $mbDetectOrder; return $this; } public function setSerializeAllObjects(bool $value): void { $this->serializeAllObjects = $value; } public function getSerializeAllObjects(): bool { return $this->serializeAllObjects; } } PKî%W])øó™™>sentry/src/Serializer/Traits/StacktraceFrameSeralizerTrait.phpnu„[µü¤ * * @psalm-return array{ * filename: string, * lineno: int, * in_app: bool, * abs_path?: string, * function?: string, * raw_function?: string, * pre_context?: string[], * context_line?: string, * post_context?: string[], * vars?: array * } */ protected static function serializeStacktraceFrame(Frame $frame): array { $result = [ 'filename' => $frame->getFile(), 'lineno' => $frame->getLine(), 'in_app' => $frame->isInApp(), ]; if ($frame->getAbsoluteFilePath() !== null) { $result['abs_path'] = $frame->getAbsoluteFilePath(); } if ($frame->getFunctionName() !== null) { $result['function'] = $frame->getFunctionName(); } if ($frame->getRawFunctionName() !== null) { $result['raw_function'] = $frame->getRawFunctionName(); } if (!empty($frame->getPreContext())) { $result['pre_context'] = $frame->getPreContext(); } if ($frame->getContextLine() !== null) { $result['context_line'] = $frame->getContextLine(); } if (!empty($frame->getPostContext())) { $result['post_context'] = $frame->getPostContext(); } if (!empty($frame->getVars())) { $result['vars'] = $frame->getVars(); } return $result; } } PKî%W]€j?óó9sentry/src/Serializer/Traits/BreadcrumbSeralizerTrait.phpnu„[µü¤ * * @psalm-return array{ * type: string, * category: string, * level: string, * timestamp: float, * message?: string, * data?: object * } */ protected static function serializeBreadcrumb(Breadcrumb $breadcrumb): array { $result = [ 'type' => $breadcrumb->getType(), 'category' => $breadcrumb->getCategory(), 'level' => $breadcrumb->getLevel(), 'timestamp' => $breadcrumb->getTimestamp(), ]; if ($breadcrumb->getMessage() !== null) { $result['message'] = $breadcrumb->getMessage(); } if (!empty($breadcrumb->getMetadata())) { $result['data'] = (object) $breadcrumb->getMetadata(); } return $result; } } PKî%W]kŸÆbOO2sentry/src/Serializer/RepresentationSerializer.phpnu„[µü¤serializeRecursively($value); if (is_numeric($value)) { return (string) $value; } if (\is_bool($value)) { return $value ? 'true' : 'false'; } return $value; } /** * This method is overridden to return even basic types as strings. * * @param mixed $value The value that needs to be serialized * * @return string */ protected function serializeValue($value) { if ($value === null) { return 'null'; } if ($value === false) { return 'false'; } if ($value === true) { return 'true'; } if (\is_float($value) && (int) $value == $value) { return $value . '.0'; } if (is_numeric($value)) { return (string) $value; } return (string) parent::serializeValue($value); } } PKî%W]”ñSÅÅ;sentry/src/Serializer/RepresentationSerializerInterface.phpnu„[µü¤ 'profile', 'content_type' => 'application/json', ]; $profile = $event->getSdkMetadata('profile'); if (!$profile instanceof Profile) { return null; } $payload = $profile->getFormattedData($event); if ($payload === null) { return null; } return \sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); } } PKî%W]íi‡ÙEE6sentry/src/Serializer/EnvelopItems/TransactionItem.phpnu„[µü¤, * } */ class TransactionItem implements EnvelopeItemInterface { use BreadcrumbSeralizerTrait; public static function toEnvelopeItem(Event $event): string { $header = [ 'type' => (string) EventType::transaction(), 'content_type' => 'application/json', ]; $payload = [ 'timestamp' => $event->getTimestamp(), 'platform' => 'php', 'sdk' => $event->getSdkPayload(), ]; if ($event->getStartTimestamp() !== null) { $payload['start_timestamp'] = $event->getStartTimestamp(); } if ($event->getLevel() !== null) { $payload['level'] = (string) $event->getLevel(); } if ($event->getTransaction() !== null) { $payload['transaction'] = $event->getTransaction(); } if ($event->getServerName() !== null) { $payload['server_name'] = $event->getServerName(); } if ($event->getRelease() !== null) { $payload['release'] = $event->getRelease(); } if ($event->getEnvironment() !== null) { $payload['environment'] = $event->getEnvironment(); } if (!empty($event->getFingerprint())) { $payload['fingerprint'] = $event->getFingerprint(); } if (!empty($event->getModules())) { $payload['modules'] = $event->getModules(); } if (!empty($event->getExtra())) { $payload['extra'] = $event->getExtra(); } if (!empty($event->getTags())) { $payload['tags'] = $event->getTags(); } $user = $event->getUser(); if ($user !== null) { $payload['user'] = array_merge($user->getMetadata(), [ 'id' => $user->getId(), 'username' => $user->getUsername(), 'email' => $user->getEmail(), 'ip_address' => $user->getIpAddress(), 'segment' => $user->getSegment(), ]); } $osContext = $event->getOsContext(); if ($osContext !== null) { $payload['contexts']['os'] = [ 'name' => $osContext->getName(), 'version' => $osContext->getVersion(), 'build' => $osContext->getBuild(), 'kernel_version' => $osContext->getKernelVersion(), ]; } $runtimeContext = $event->getRuntimeContext(); if ($runtimeContext !== null) { $payload['contexts']['runtime'] = [ 'name' => $runtimeContext->getName(), 'sapi' => $runtimeContext->getSAPI(), 'version' => $runtimeContext->getVersion(), ]; } if (!empty($event->getContexts())) { $payload['contexts'] = array_merge($payload['contexts'] ?? [], $event->getContexts()); } if (!empty($event->getBreadcrumbs())) { $payload['breadcrumbs']['values'] = array_map([self::class, 'serializeBreadcrumb'], $event->getBreadcrumbs()); } if (!empty($event->getRequest())) { $payload['request'] = $event->getRequest(); } $payload['spans'] = array_values(array_map([self::class, 'serializeSpan'], $event->getSpans())); $transactionMetadata = $event->getSdkMetadata('transaction_metadata'); if ($transactionMetadata instanceof TransactionMetadata) { $payload['transaction_info']['source'] = (string) $transactionMetadata->getSource(); } return \sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); } /** * @return array * * @psalm-return array{ * span_id: string, * trace_id: string, * parent_span_id?: string, * start_timestamp: float, * timestamp?: float, * status?: string, * description?: string, * op?: string, * data?: array, * tags?: array * _metrics_summary?: array * } */ protected static function serializeSpan(Span $span): array { $result = [ 'span_id' => (string) $span->getSpanId(), 'trace_id' => (string) $span->getTraceId(), 'start_timestamp' => $span->getStartTimestamp(), 'origin' => $span->getOrigin() ?? 'manual', ]; if ($span->getParentSpanId() !== null) { $result['parent_span_id'] = (string) $span->getParentSpanId(); } if ($span->getEndTimestamp() !== null) { $result['timestamp'] = $span->getEndTimestamp(); } if ($span->getStatus() !== null) { $result['status'] = (string) $span->getStatus(); } if ($span->getDescription() !== null) { $result['description'] = $span->getDescription(); } if ($span->getOp() !== null) { $result['op'] = $span->getOp(); } if (!empty($span->getData())) { $result['data'] = $span->getData(); } if (!empty($span->getTags())) { $result['tags'] = $span->getTags(); } return $result; } } PKî%W]™'ýâÚÚ<sentry/src/Serializer/EnvelopItems/EnvelopeItemInterface.phpnu„[µü¤ (string) $event->getType(), 'content_type' => 'application/json', ]; $payload = [ 'timestamp' => $event->getTimestamp(), 'platform' => 'php', 'sdk' => $event->getSdkPayload(), ]; if ($event->getStartTimestamp() !== null) { $payload['start_timestamp'] = $event->getStartTimestamp(); } if ($event->getLevel() !== null) { $payload['level'] = (string) $event->getLevel(); } if ($event->getLogger() !== null) { $payload['logger'] = $event->getLogger(); } if ($event->getTransaction() !== null) { $payload['transaction'] = $event->getTransaction(); } if ($event->getServerName() !== null) { $payload['server_name'] = $event->getServerName(); } if ($event->getRelease() !== null) { $payload['release'] = $event->getRelease(); } if ($event->getEnvironment() !== null) { $payload['environment'] = $event->getEnvironment(); } if (!empty($event->getFingerprint())) { $payload['fingerprint'] = $event->getFingerprint(); } if (!empty($event->getModules())) { $payload['modules'] = $event->getModules(); } if (!empty($event->getExtra())) { $payload['extra'] = $event->getExtra(); } if (!empty($event->getTags())) { $payload['tags'] = $event->getTags(); } $user = $event->getUser(); if ($user !== null) { $payload['user'] = array_merge($user->getMetadata(), [ 'id' => $user->getId(), 'username' => $user->getUsername(), 'email' => $user->getEmail(), 'ip_address' => $user->getIpAddress(), 'segment' => $user->getSegment(), ]); } $osContext = $event->getOsContext(); if ($osContext !== null) { $payload['contexts']['os'] = [ 'name' => $osContext->getName(), 'version' => $osContext->getVersion(), 'build' => $osContext->getBuild(), 'kernel_version' => $osContext->getKernelVersion(), ]; } $runtimeContext = $event->getRuntimeContext(); if ($runtimeContext !== null) { $payload['contexts']['runtime'] = [ 'name' => $runtimeContext->getName(), 'sapi' => $runtimeContext->getSAPI(), 'version' => $runtimeContext->getVersion(), ]; } if (!empty($event->getContexts())) { $payload['contexts'] = array_merge($payload['contexts'] ?? [], $event->getContexts()); } if (!empty($event->getBreadcrumbs())) { $payload['breadcrumbs']['values'] = array_map([self::class, 'serializeBreadcrumb'], $event->getBreadcrumbs()); } if (!empty($event->getRequest())) { $payload['request'] = $event->getRequest(); } if ($event->getMessage() !== null) { if (empty($event->getMessageParams())) { $payload['message'] = $event->getMessage(); } else { $payload['message'] = [ 'message' => $event->getMessage(), 'params' => $event->getMessageParams(), 'formatted' => $event->getMessageFormatted() ?? Str::vsprintfOrNull($event->getMessage(), $event->getMessageParams()) ?? $event->getMessage(), ]; } } $exceptions = $event->getExceptions(); for ($i = \count($exceptions) - 1; $i >= 0; --$i) { $payload['exception']['values'][] = self::serializeException($exceptions[$i]); } $stacktrace = $event->getStacktrace(); if ($stacktrace !== null) { $payload['stacktrace'] = [ 'frames' => array_map([self::class, 'serializeStacktraceFrame'], $stacktrace->getFrames()), ]; } return \sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); } /** * @return array * * @psalm-return array{ * type: string, * value: string, * stacktrace?: array{ * frames: array> * }, * mechanism?: array{ * type: string, * handled: boolean, * data?: array * } * } */ protected static function serializeException(ExceptionDataBag $exception): array { $exceptionMechanism = $exception->getMechanism(); $exceptionStacktrace = $exception->getStacktrace(); $result = [ 'type' => $exception->getType(), 'value' => $exception->getValue(), ]; if ($exceptionStacktrace !== null) { $result['stacktrace'] = [ 'frames' => array_map([self::class, 'serializeStacktraceFrame'], $exceptionStacktrace->getFrames()), ]; } if ($exceptionMechanism !== null) { $result['mechanism'] = [ 'type' => $exceptionMechanism->getType(), 'handled' => $exceptionMechanism->isHandled(), ]; if ($exceptionMechanism->getData() !== []) { $result['mechanism']['data'] = $exceptionMechanism->getData(); } } return $result; } } PKî%W]Y©hu2sentry/src/Serializer/EnvelopItems/CheckInItem.phpnu„[µü¤ (string) $event->getType(), 'content_type' => 'application/json', ]; $payload = []; $checkIn = $event->getCheckIn(); if ($checkIn !== null) { $payload = [ 'check_in_id' => $checkIn->getId(), 'monitor_slug' => $checkIn->getMonitorSlug(), 'status' => (string) $checkIn->getStatus(), 'duration' => $checkIn->getDuration(), 'release' => $checkIn->getRelease(), 'environment' => $checkIn->getEnvironment(), ]; if ($checkIn->getMonitorConfig() !== null) { $payload['monitor_config'] = $checkIn->getMonitorConfig()->toArray(); } if (!empty($event->getContexts()['trace'])) { $payload['contexts']['trace'] = $event->getContexts()['trace']; } } return \sprintf("%s\n%s", JSON::encode($header), JSON::encode($payload)); } } PKî%W][ËBl¦¦/sentry/src/Serializer/EnvelopItems/LogsItem.phpnu„[µü¤getLogs(); $header = [ 'type' => (string) EventType::logs(), 'item_count' => \count($logs), 'content_type' => 'application/vnd.sentry.items.log+json', ]; return \sprintf( "%s\n%s", JSON::encode($header), JSON::encode([ 'items' => array_map(static function (Log $log): array { return [ 'timestamp' => $log->getTimestamp(), 'trace_id' => $log->getTraceId(), 'level' => (string) $log->getLevel(), 'body' => $log->getBody(), 'attributes' => array_map(static function (Attribute $attribute): array { return [ 'type' => $attribute->getType(), 'value' => $attribute->getValue(), ]; }, $log->attributes()->all()), ]; }, $logs), ]) ); } } PKî%W]Ñ’o-sentry/src/Serializer/SerializerInterface.phpnu„[µü¤serializeRecursively($value); } } PKî%W]¯§ò¼Š Š +sentry/src/Serializer/PayloadSerializer.phpnu„[µü¤options = $options; } /** * {@inheritdoc} */ public function serialize(Event $event): string { // @see https://develop.sentry.dev/sdk/envelopes/#envelope-headers $envelopeHeader = [ 'event_id' => (string) $event->getId(), 'sent_at' => gmdate('Y-m-d\TH:i:s\Z'), 'dsn' => (string) $this->options->getDsn(), 'sdk' => $event->getSdkPayload(), ]; $dynamicSamplingContext = $event->getSdkMetadata('dynamic_sampling_context'); if ($dynamicSamplingContext instanceof DynamicSamplingContext) { $entries = $dynamicSamplingContext->getEntries(); if (!empty($entries)) { $envelopeHeader['trace'] = $entries; } } $items = []; switch ($event->getType()) { case EventType::event(): $items[] = EventItem::toEnvelopeItem($event); break; case EventType::transaction(): $items[] = TransactionItem::toEnvelopeItem($event); if ($event->getSdkMetadata('profile') !== null) { $items[] = ProfileItem::toEnvelopeItem($event); } break; case EventType::checkIn(): $items[] = CheckInItem::toEnvelopeItem($event); break; case EventType::logs(): $items[] = LogsItem::toEnvelopeItem($event); break; } return \sprintf("%s\n%s", JSON::encode($envelopeHeader), implode("\n", array_filter($items))); } } PKî%W]½íµoo&sentry/src/Exception/JsonException.phpnu„[µü¤ */ final class FatalErrorException extends \ErrorException { } PKî%W]Éë{< < sentry/src/Metrics/Metrics.phpnu„[µü¤ $tags * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function increment( string $key, float $value, ?MetricsUnit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 ): void { } /** * @param array $tags * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function distribution( string $key, float $value, ?MetricsUnit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 ): void { } /** * @param array $tags * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function gauge( string $key, float $value, ?MetricsUnit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 ): void { } /** * @param int|string $value * @param array $tags * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function set( string $key, $value, ?MetricsUnit $unit = null, array $tags = [], ?int $timestamp = null, int $stackLevel = 0 ): void { } /** * @template T * * @param callable(): T $callback * @param array $tags * * @return T * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function timing( string $key, callable $callback, array $tags = [], int $stackLevel = 0 ) { return trace( function () use ($callback) { return $callback(); }, SpanContext::make() ->setOp('metric.timing') ->setOrigin('auto.measure.metrics.timing') ->setDescription($key) ); } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function flush(): ?EventId { return null; } } PKî%W]4Œìïw w "sentry/src/Metrics/MetricsUnit.phpnu„[µü¤ A list of cached enum instances */ private static $instances = []; private function __construct(string $value) { $this->value = $value; } public static function nanosecond(): self { return self::getInstance('nanosecond'); } public static function microsecond(): self { return self::getInstance('microsecond'); } public static function millisecond(): self { return self::getInstance('millisecond'); } public static function second(): self { return self::getInstance('second'); } public static function minute(): self { return self::getInstance('minute'); } public static function hour(): self { return self::getInstance('hour'); } public static function day(): self { return self::getInstance('day'); } public static function week(): self { return self::getInstance('week'); } public static function bit(): self { return self::getInstance('bit'); } public static function byte(): self { return self::getInstance('byte'); } public static function kilobyte(): self { return self::getInstance('kilobyte'); } public static function kibibyte(): self { return self::getInstance('kibibyte'); } public static function megabyte(): self { return self::getInstance('megabyte'); } public static function mebibyte(): self { return self::getInstance('mebibyte'); } public static function gigabyte(): self { return self::getInstance('gigabyte'); } public static function gibibyte(): self { return self::getInstance('gibibyte'); } public static function terabyte(): self { return self::getInstance('terabyte'); } public static function tebibyte(): self { return self::getInstance('tebibyte'); } public static function petabyte(): self { return self::getInstance('petabyte'); } public static function pebibyte(): self { return self::getInstance('pebibyte'); } public static function exabyte(): self { return self::getInstance('exabyte'); } public static function exbibyte(): self { return self::getInstance('exbibyte'); } public static function ratio(): self { return self::getInstance('ratio'); } public static function percent(): self { return self::getInstance('percent'); } public static function none(): self { return self::getInstance('none'); } public static function custom(string $unit): self { return new self($unit); } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W]¾ f÷’ ’ "sentry/src/Monolog/LogsHandler.phpnu„[µü¤logLevel = $logLevel ?? LogLevel::debug(); $this->bubble = $bubble; } /** * @param array|LogRecord $record */ public function isHandling($record): bool { return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority(); } /** * @param array|LogRecord $record */ public function handle($record): bool { if (!$this->isHandling($record)) { return false; } // Do not collect logs for exceptions, they should be handled seperately by the `Handler` or `captureException` if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { return false; } Logs::getInstance()->aggregator()->add( self::getSentryLogLevelFromMonologLevel($record['level']), $record['message'], [], array_merge($record['context'], $record['extra'], ['sentry.origin' => 'auto.log.monolog']) ); return $this->bubble === false; } /** * @param array|LogRecord> $records */ public function handleBatch(array $records): void { foreach ($records as $record) { $this->handle($record); } } public function close(): void { Logs::getInstance()->flush(); } /** * @param callable $callback */ public function pushProcessor($callback): void { // noop, this handler does not support processors } /** * @return callable */ public function popProcessor() { // Since we do not support processors, we throw an exception if this method is called throw new \LogicException('You tried to pop from an empty processor stack.'); } public function setFormatter(FormatterInterface $formatter): void { // noop, this handler does not support formatters } public function getFormatter(): FormatterInterface { // To adhere to the interface we need to return a formatter so we return a default one return new LineFormatter(); } public function __destruct() { try { $this->close(); } catch (\Throwable $e) { // Just in case so that the destructor can never fail. } } } PKî%W] JØ& & (sentry/src/Monolog/BreadcrumbHandler.phpnu„[µü¤hub = $hub; parent::__construct($level, $bubble); } /** * @psalm-suppress MoreSpecificImplementedParamType * * @param LogRecord|array{ * level: int, * channel: string, * datetime: \DateTimeImmutable, * message: string, * extra?: array * } $record {@see https://github.com/Seldaek/monolog/blob/main/doc/message-structure.md} */ protected function write($record): void { $breadcrumb = new Breadcrumb( $this->getBreadcrumbLevel($record['level']), $this->getBreadcrumbType($record['level']), $record['channel'], $record['message'], ($record['context'] ?? []) + ($record['extra'] ?? []), $record['datetime']->getTimestamp() ); $this->hub->addBreadcrumb($breadcrumb); } /** * @param Level|int $level */ private function getBreadcrumbLevel($level): string { if ($level instanceof Level) { $level = $level->value; } switch ($level) { case Logger::DEBUG: return Breadcrumb::LEVEL_DEBUG; case Logger::INFO: case Logger::NOTICE: return Breadcrumb::LEVEL_INFO; case Logger::WARNING: return Breadcrumb::LEVEL_WARNING; case Logger::ERROR: return Breadcrumb::LEVEL_ERROR; default: return Breadcrumb::LEVEL_FATAL; } } private function getBreadcrumbType(int $level): string { if ($level >= Logger::ERROR) { return Breadcrumb::TYPE_ERROR; } return Breadcrumb::TYPE_DEFAULT; } } PKî%W]) _%ò ò sentry/src/Monolog/Handler.phpnu„[µü¤ */ final class Handler extends AbstractProcessingHandler { use CompatibilityProcessingHandlerTrait; private const CONTEXT_EXCEPTION_KEY = 'exception'; /** * @var HubInterface */ private $hub; /** * @var bool */ private $fillExtraContext; /** * {@inheritdoc} * * @param HubInterface $hub The hub to which errors are reported */ public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true, bool $fillExtraContext = false) { parent::__construct($level, $bubble); $this->hub = $hub; $this->fillExtraContext = $fillExtraContext; } /** * @param array|LogRecord $record */ protected function doWrite($record): void { $event = Event::createEvent(); $event->setLevel(self::getSeverityFromLevel($record['level'])); $event->setMessage($record['message']); $event->setLogger(\sprintf('monolog.%s', $record['channel'])); $hint = new EventHint(); if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) { $hint->exception = $record['context']['exception']; } $this->hub->withScope(function (Scope $scope) use ($record, $event, $hint): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); $monologContextData = $this->getMonologContextData($record['context']); if ($monologContextData !== []) { $scope->setExtra('monolog.context', $monologContextData); } $monologExtraData = $this->getMonologExtraData($record['extra']); if ($monologExtraData !== []) { $scope->setExtra('monolog.extra', $monologExtraData); } $this->hub->captureEvent($event, $hint); }); } /** * @param mixed[] $context * * @return mixed[] */ private function getMonologContextData(array $context): array { if (!$this->fillExtraContext) { return []; } $contextData = []; foreach ($context as $key => $value) { // We skip the `exception` field because it goes in its own context if ($key === self::CONTEXT_EXCEPTION_KEY) { continue; } $contextData[$key] = $value; } return $contextData; } /** * @param mixed[] $context * * @return mixed[] */ private function getMonologExtraData(array $context): array { if (!$this->fillExtraContext) { return []; } $extraData = []; foreach ($context as $key => $value) { $extraData[$key] = $value; } return $extraData; } } PKî%W]Îå;< < :sentry/src/Monolog/CompatibilityProcessingHandlerTrait.phpnu„[µü¤= 3) { /** * Logic which is used if monolog >= 3 is installed. * * @internal */ trait CompatibilityProcessingHandlerTrait { /** * @param array|LogRecord $record */ abstract protected function doWrite($record): void; /** * {@inheritdoc} */ protected function write(LogRecord $record): void { $this->doWrite($record); } /** * Translates the Monolog level into the Sentry severity. */ private static function getSeverityFromLevel(int $level): Severity { $level = Level::from($level); switch ($level) { case Level::Debug: return Severity::debug(); case Level::Warning: return Severity::warning(); case Level::Error: return Severity::error(); case Level::Critical: case Level::Alert: case Level::Emergency: return Severity::fatal(); case Level::Info: case Level::Notice: default: return Severity::info(); } } } } else { /** * Logic which is used if monolog < 3 is installed. * * @internal */ trait CompatibilityProcessingHandlerTrait { /** * @param array|LogRecord $record */ abstract protected function doWrite($record): void; /** * {@inheritdoc} */ protected function write(array $record): void { $this->doWrite($record); } /** * Translates the Monolog level into the Sentry severity. * * @param Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY $level The Monolog log level */ private static function getSeverityFromLevel(int $level): Severity { switch ($level) { case Logger::DEBUG: return Severity::debug(); case Logger::WARNING: return Severity::warning(); case Logger::ERROR: return Severity::error(); case Logger::CRITICAL: case Logger::ALERT: case Logger::EMERGENCY: return Severity::fatal(); case Logger::INFO: case Logger::NOTICE: default: return Severity::info(); } } } } PKî%W]Ç‘œšÊÊ1sentry/src/Monolog/CompatibilityLogLevelTrait.phpnu„[µü¤= 3) { /** * Logic which is used if monolog >= 3 is installed. * * @internal */ trait CompatibilityLogLevelTrait { /** * Translates the Monolog level into the Sentry LogLevel. */ private static function getSentryLogLevelFromMonologLevel(int $level): LogLevel { $level = Level::from($level); switch ($level) { case Level::Debug: return LogLevel::debug(); case Level::Warning: return LogLevel::warn(); case Level::Error: return LogLevel::error(); case Level::Critical: case Level::Alert: case Level::Emergency: return LogLevel::fatal(); case Level::Info: case Level::Notice: default: return LogLevel::info(); } } } } else { /** * Logic which is used if monolog < 3 is installed. * * @internal */ trait CompatibilityLogLevelTrait { /** * Translates the Monolog level into the Sentry LogLevel. * * @param Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY $level The Monolog log level */ private static function getSentryLogLevelFromMonologLevel(int $level): LogLevel { switch ($level) { case Logger::DEBUG: return LogLevel::debug(); case Logger::WARNING: return LogLevel::warn(); case Logger::ERROR: return LogLevel::error(); case Logger::CRITICAL: case Logger::ALERT: case Logger::EMERGENCY: return LogLevel::fatal(); case Logger::INFO: case Logger::NOTICE: default: return LogLevel::info(); } } } } PKî%W]è~—Ž“ “ sentry/src/StacktraceBuilder.phpnu„[µü¤frameBuilder = new FrameBuilder($options, $representationSerializer); if (PHPConfiguration::isBooleanIniOptionEnabled('zend.exception_ignore_args')) { $options->getLoggerOrNullLogger()->warning('The "zend.exception_ignore_args" PHP setting is enabled which results in missing stack trace arguments, see: https://docs.sentry.io/platforms/php/troubleshooting/#missing-variables-in-stack-traces.'); } } /** * Builds a {@see Stacktrace} object from the given exception. * * @param \Throwable $exception The exception object */ public function buildFromException(\Throwable $exception): Stacktrace { return $this->buildFromBacktrace($exception->getTrace(), $exception->getFile(), $exception->getLine()); } /** * Builds a {@see Stacktrace} object from the given backtrace. * * @param array> $backtrace The backtrace * @param string $file The file where the backtrace originated from * @param int $line The line from which the backtrace originated from * * @psalm-param list $backtrace */ public function buildFromBacktrace(array $backtrace, string $file, int $line): Stacktrace { $frames = []; foreach ($backtrace as $backtraceFrame) { array_unshift($frames, $this->frameBuilder->buildFromBacktraceFrame($file, $line, $backtraceFrame)); $file = $backtraceFrame['file'] ?? Frame::INTERNAL_FRAME_FILENAME; $line = $backtraceFrame['line'] ?? 0; } // Add a final stackframe for the first method ever of this stacktrace array_unshift($frames, $this->frameBuilder->buildFromBacktraceFrame($file, $line, [])); return new Stacktrace($frames); } } PKî%W]Í$¢ ø ø %sentry/src/Transport/ResultStatus.phpnu„[µü¤ */ private static $instances = []; /** * Constructor. * * @param string $value The value of the enum instance */ private function __construct(string $value) { $this->value = $value; } /** * Returns an instance of this enum representing the fact that the event * failed to be sent due to unknown reasons. */ public static function unknown(): self { return self::getInstance('UNKNOWN'); } /** * Returns an instance of this enum representing the fact that event was * skipped from being sent. */ public static function skipped(): self { return self::getInstance('SKIPPED'); } /** * Returns an instance of this enum representing the fact that the event * was sent successfully. */ public static function success(): self { return self::getInstance('SUCCESS'); } /** * Returns an instance of this enum representing the fact that the event * failed to be sent because of API rate limiting. */ public static function rateLimit(): self { return self::getInstance('RATE_LIMIT'); } /** * Returns an instance of this enum representing the fact that the event * failed to be sent because the server was not able to process the request. */ public static function invalid(): self { return self::getInstance('INVALID'); } /** * Returns an instance of this enum representing the fact that the event * failed to be sent because the server returned a server error. */ public static function failed(): self { return self::getInstance('FAILED'); } /** * Returns an instance of this enum according to the given HTTP status code. * * @param int $statusCode The HTTP status code */ public static function createFromHttpStatusCode(int $statusCode): self { switch (true) { case $statusCode >= 200 && $statusCode < 300: return self::success(); case $statusCode === 429: return self::rateLimit(); case $statusCode >= 400 && $statusCode < 500: return self::invalid(); case $statusCode >= 500: return self::failed(); default: return self::unknown(); } } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W]£Ô\üsentry/src/Transport/Result.phpnu„[µü¤status = $status; $this->event = $event; } /** * Gets the status of the sending operation of the event. */ public function getStatus(): ResultStatus { return $this->status; } /** * Gets the instance of the event being sent, or null if it was not available yet. */ public function getEvent(): ?Event { return $this->event; } } PKî%W]Aš2§žž$sentry/src/Transport/RateLimiter.phpnu„[µü¤ The map of time instants for each event category after * which an HTTP request can be retried */ private $rateLimits = []; /** * @var LoggerInterface A PSR-3 logger */ private $logger; public function __construct(?LoggerInterface $logger = null) { $this->logger = $logger ?? new NullLogger(); } public function handleResponse(Response $response): bool { $now = time(); if ($response->hasHeader(self::RATE_LIMITS_HEADER)) { foreach (explode(',', $response->getHeaderLine(self::RATE_LIMITS_HEADER)) as $limit) { /** * $parameters[0] - retry_after * $parameters[1] - categories * $parameters[2] - scope (not used) * $parameters[3] - reason_code (not used) * $parameters[4] - namespaces (only returned if categories contains "metric_bucket"). */ $parameters = explode(':', $limit, 5); $retryAfter = $now + (ctype_digit($parameters[0]) ? (int) $parameters[0] : self::DEFAULT_RETRY_AFTER_SECONDS); foreach (explode(';', $parameters[1]) as $category) { $this->rateLimits[$category ?: 'all'] = $retryAfter; $this->logger->warning( \sprintf('Rate limited exceeded for category "%s", backing off until "%s".', $category, gmdate(\DATE_ATOM, $retryAfter)) ); } } return $this->rateLimits !== []; } if ($response->hasHeader(self::RETRY_AFTER_HEADER)) { $retryAfter = $now + $this->parseRetryAfterHeader($now, $response->getHeaderLine(self::RETRY_AFTER_HEADER)); $this->rateLimits['all'] = $retryAfter; $this->logger->warning( \sprintf('Rate limited exceeded for all categories, backing off until "%s".', gmdate(\DATE_ATOM, $retryAfter)) ); return true; } return false; } /** * @param string|EventType $eventType */ public function isRateLimited($eventType): bool { $disabledUntil = $this->getDisabledUntil($eventType); return $disabledUntil > time(); } /** * @param string|EventType $eventType */ public function getDisabledUntil($eventType): int { $eventType = $eventType instanceof EventType ? (string) $eventType : $eventType; if ($eventType === 'event') { $eventType = self::DATA_CATEGORY_ERROR; } elseif ($eventType === 'log') { $eventType = self::DATA_CATEGORY_LOG_ITEM; } return max($this->rateLimits['all'] ?? 0, $this->rateLimits[$eventType] ?? 0); } private function parseRetryAfterHeader(int $currentTime, string $header): int { if (preg_match('/^\d+$/', $header) === 1) { return (int) $header; } $headerDate = \DateTimeImmutable::createFromFormat(\DateTimeImmutable::RFC1123, $header); if ($headerDate !== false && $headerDate->getTimestamp() >= $currentTime) { return $headerDate->getTimestamp() - $currentTime; } return self::DEFAULT_RETRY_AFTER_SECONDS; } } PKî%W]hd©–ªª&sentry/src/Transport/HttpTransport.phpnu„[µü¤options = $options; $this->httpClient = $httpClient; $this->payloadSerializer = $payloadSerializer; $this->logger = $logger ?? new NullLogger(); $this->rateLimiter = new RateLimiter($this->logger); } /** * {@inheritdoc} */ public function send(Event $event): Result { $this->sendRequestToSpotlight($event); $eventDescription = \sprintf( '%s%s [%s]', $event->getLevel() !== null ? $event->getLevel() . ' ' : '', (string) $event->getType(), (string) $event->getId() ); if ($this->options->getDsn() === null) { $this->logger->info(\sprintf('Skipping %s, because no DSN is set.', $eventDescription), ['event' => $event]); return new Result(ResultStatus::skipped(), $event); } $targetDescription = \sprintf( '%s [project:%s]', $this->options->getDsn()->getHost(), $this->options->getDsn()->getProjectId() ); $this->logger->info(\sprintf('Sending %s to %s.', $eventDescription, $targetDescription), ['event' => $event]); $eventType = $event->getType(); if ($this->rateLimiter->isRateLimited((string) $eventType)) { $this->logger->warning( \sprintf('Rate limit exceeded for sending requests of type "%s".', (string) $eventType), ['event' => $event] ); return new Result(ResultStatus::rateLimit()); } $request = new Request(); $request->setStringBody($this->payloadSerializer->serialize($event)); try { $response = $this->httpClient->sendRequest($request, $this->options); } catch (\Throwable $exception) { $this->logger->error( \sprintf('Failed to send %s to %s. Reason: "%s".', $eventDescription, $targetDescription, $exception->getMessage()), ['exception' => $exception, 'event' => $event] ); return new Result(ResultStatus::failed()); } if ($response->hasError()) { $this->logger->error( \sprintf('Failed to send %s to %s. Reason: "%s".', $eventDescription, $targetDescription, $response->getError()), ['event' => $event] ); return new Result(ResultStatus::unknown()); } $this->rateLimiter->handleResponse($response); $resultStatus = ResultStatus::createFromHttpStatusCode($response->getStatusCode()); $this->logger->info( \sprintf('Sent %s to %s. Result: "%s" (status: %s).', $eventDescription, $targetDescription, strtolower((string) $resultStatus), $response->getStatusCode()), ['response' => $response, 'event' => $event] ); return new Result($resultStatus, $event); } /** * {@inheritdoc} */ public function close(?int $timeout = null): Result { return new Result(ResultStatus::success()); } /** * @internal */ public function getHttpClient(): HttpClientInterface { return $this->httpClient; } private function sendRequestToSpotlight(Event $event): void { if (!$this->options->isSpotlightEnabled()) { return; } $request = new Request(); $request->setStringBody($this->payloadSerializer->serialize($event)); try { $spotLightResponse = SpotlightClient::sendRequest( $request, $this->options->getSpotlightUrl() . '/stream' ); if ($spotLightResponse->hasError()) { $this->logger->info( \sprintf('Failed to send the event to Spotlight. Reason: "%s".', $spotLightResponse->getError()), ['event' => $event] ); } } catch (\Throwable $exception) { $this->logger->info( \sprintf('Failed to send the event to Spotlight. Reason: "%s".', $exception->getMessage()), ['exception' => $exception, 'event' => $event] ); } } } PKî%W]¸®³–ÜÜ+sentry/src/Transport/TransportInterface.phpnu„[µü¤ A list of cached enum instances */ private static $instances = []; private function __construct(string $value) { $this->value = $value; } public static function minute(): self { return self::getInstance('minute'); } public static function hour(): self { return self::getInstance('hour'); } public static function day(): self { return self::getInstance('day'); } public static function week(): self { return self::getInstance('week'); } public static function month(): self { return self::getInstance('month'); } public static function year(): self { return self::getInstance('year'); } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W]1#c™;2;2sentry/src/functions.phpnu„[µü¤, * ignore_transactions?: array, * in_app_exclude?: array, * in_app_include?: array, * integrations?: IntegrationInterface[]|callable(IntegrationInterface[]): IntegrationInterface[], * logger?: LoggerInterface|null, * max_breadcrumbs?: int, * max_request_body_size?: "none"|"never"|"small"|"medium"|"always", * max_value_length?: int, * org_id?: int|null, * prefixes?: array, * profiles_sample_rate?: int|float|null, * release?: string|null, * sample_rate?: float|int, * send_attempts?: int, * send_default_pii?: bool, * server_name?: string, * spotlight?: bool, * spotlight_url?: string, * strict_trace_propagation?: bool, * tags?: array, * trace_propagation_targets?: array|null, * traces_sample_rate?: float|int|null, * traces_sampler?: callable|null, * transport?: callable, * } $options The client options */ function init(array $options = []): void { $client = ClientBuilder::create($options)->getClient(); SentrySdk::init()->bindClient($client); } /** * Captures a message event and sends it to Sentry. * * @param string $message The message * @param Severity|null $level The severity level of the message * @param EventHint|null $hint Object that can contain additional information about the event */ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); } /** * Captures an exception event and sends it to Sentry. * * @param \Throwable $exception The exception * @param EventHint|null $hint Object that can contain additional information about the event */ function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureException($exception, $hint); } /** * Captures a new event using the provided data. * * @param Event $event The event being captured * @param EventHint|null $hint May contain additional information about the event */ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureEvent($event, $hint); } /** * Logs the most recent error (obtained with {@see error_get_last()}). * * @param EventHint|null $hint Object that can contain additional information about the event */ function captureLastError(?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureLastError($hint); } /** * Captures a check-in and sends it to Sentry. * * @param string $slug Identifier of the Monitor * @param CheckInStatus $status The status of the check-in * @param int|float|null $duration The duration of the check-in * @param MonitorConfig|null $monitorConfig Configuration of the Monitor * @param string|null $checkInId A check-in ID from the previous check-in */ function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { return SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** * Execute the given callable while wrapping it in a monitor check-in. * * @param string $slug Identifier of the Monitor * @param callable $callback The callable that is going to be monitored * @param MonitorConfig|null $monitorConfig Configuration of the Monitor * * @return mixed */ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorConfig = null) { $checkInId = SentrySdk::getCurrentHub()->captureCheckIn($slug, CheckInStatus::inProgress(), null, $monitorConfig); $status = CheckInStatus::ok(); $duration = 0; try { $start = microtime(true); $result = $callback(); $duration = microtime(true) - $start; return $result; } catch (\Throwable $e) { $status = CheckInStatus::error(); throw $e; } finally { SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } } /** * Records a new breadcrumb which will be attached to future events. They * will be added to subsequent events to provide more context on user's * actions prior to an error or crash. * * @param Breadcrumb|string $category The category of the breadcrumb, can be a Breadcrumb instance as well (in which case the other parameters are ignored) * @param string|null $message Breadcrumb message * @param array $metadata Additional information about the breadcrumb * @param string $level The error level of the breadcrumb * @param string $type The type of the breadcrumb * @param float|null $timestamp Optional timestamp of the breadcrumb */ function addBreadcrumb($category, ?string $message = null, array $metadata = [], string $level = Breadcrumb::LEVEL_INFO, string $type = Breadcrumb::TYPE_DEFAULT, ?float $timestamp = null): void { SentrySdk::getCurrentHub()->addBreadcrumb( $category instanceof Breadcrumb ? $category : new Breadcrumb($level, $type, $category, $message, $metadata, $timestamp) ); } /** * Calls the given callback passing to it the current scope so that any * operation can be run within its context. * * @param callable $callback The callback to be executed */ function configureScope(callable $callback): void { SentrySdk::getCurrentHub()->configureScope($callback); } /** * Creates a new scope with and executes the given operation within. The scope * is automatically removed once the operation finishes or throws. * * @param callable $callback The callback to be executed * * @psalm-template T * * @psalm-param callable(Scope): T $callback * * @return mixed|void The callback's return value, upon successful execution * * @psalm-return T */ function withScope(callable $callback) { return SentrySdk::getCurrentHub()->withScope($callback); } /** * Starts a new `Transaction` and returns it. This is the entry point to manual * tracing instrumentation. * * A tree structure can be built by adding child spans to the transaction, and * child spans to other spans. To start a new child span within the transaction * or any span, call the respective `startChild()` method. * * Every child span must be finished before the transaction is finished, * otherwise the unfinished spans are discarded. * * The transaction must be finished with a call to its `finish()` method, at * which point the transaction with all its finished child spans will be sent to * Sentry. * * @param TransactionContext $context Properties of the new transaction * @param array $customSamplingContext Additional context that will be passed to the {@see Tracing\SamplingContext} */ function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); } /** * Execute the given callable while wrapping it in a span added as a child to the current transaction and active span. * If there is no transaction active this is a no-op and the scope passed to the trace callable will be unused. * * @template T * * @param callable(Scope): T $trace The callable that is going to be traced * @param SpanContext $context The context of the span to be created * * @return T */ function trace(callable $trace, SpanContext $context) { return SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($context, $trace) { $parentSpan = $scope->getSpan(); // If there is a span set on the scope and it's sampled there is an active transaction. // If that is the case we create the child span and set it on the scope. // Otherwise we only execute the callable without creating a span. if ($parentSpan !== null && $parentSpan->getSampled()) { $span = $parentSpan->startChild($context); $scope->setSpan($span); } try { return $trace($scope); } finally { if (isset($span)) { $span->finish(); $scope->setSpan($parentSpan); } } }); } /** * Creates the current Sentry traceparent string, to be used as a HTTP header value * or HTML meta tag value. * This function is context aware, as in it either returns the traceparent based * on the current span, or the scope's propagation context. */ function getTraceparent(): string { $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); if ($client !== null) { $options = $client->getOptions(); if ($options !== null && $options->isTracingEnabled()) { $span = SentrySdk::getCurrentHub()->getSpan(); if ($span !== null) { return $span->toTraceparent(); } } } $traceParent = ''; $hub->configureScope(function (Scope $scope) use (&$traceParent) { $traceParent = $scope->getPropagationContext()->toTraceparent(); }); return $traceParent; } /** * Creates the current W3C traceparent string, to be used as a HTTP header value * or HTML meta tag value. * This function is context aware, as in it either returns the traceparent based * on the current span, or the scope's propagation context. * * @deprecated since version 4.12. To be removed in version 5.0. */ function getW3CTraceparent(): string { return ''; } /** * Creates the baggage content string, to be used as a HTTP header value * or HTML meta tag value. * This function is context aware, as in it either returns the baggage based * on the current span or the scope's propagation context. */ function getBaggage(): string { $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); if ($client !== null) { $options = $client->getOptions(); if ($options !== null && $options->isTracingEnabled()) { $span = SentrySdk::getCurrentHub()->getSpan(); if ($span !== null) { return $span->toBaggage(); } } } $baggage = ''; $hub->configureScope(function (Scope $scope) use (&$baggage) { $baggage = $scope->getPropagationContext()->toBaggage(); }); return $baggage; } /** * Continue a trace based on HTTP header values. * If the SDK is configured with enabled tracing, * this function returns a populated TransactionContext. * In any other cases, it populates the propagation context on the scope. */ function continueTrace(string $sentryTrace, string $baggage): TransactionContext { $hub = SentrySdk::getCurrentHub(); $hub->configureScope(function (Scope $scope) use ($sentryTrace, $baggage) { $propagationContext = PropagationContext::fromHeaders($sentryTrace, $baggage); $scope->setPropagationContext($propagationContext); }); return TransactionContext::fromHeaders($sentryTrace, $baggage); } /** * Get the Sentry Logs client. */ function logger(): Logs { return Logs::getInstance(); } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ function metrics(): Metrics { return Metrics::getInstance(); } PKî%W]á�ú¨£ £ sentry/src/MonitorConfig.phpnu„[µü¤schedule = $schedule; $this->checkinMargin = $checkinMargin; $this->maxRuntime = $maxRuntime; $this->timezone = $timezone; $this->failureIssueThreshold = $failureIssueThreshold; $this->recoveryThreshold = $recoveryThreshold; } public function getSchedule(): MonitorSchedule { return $this->schedule; } public function setSchedule(MonitorSchedule $schedule): self { $this->schedule = $schedule; return $this; } public function getCheckinMargin(): ?int { return $this->checkinMargin; } public function setCheckinMargin(?int $checkinMargin): self { $this->checkinMargin = $checkinMargin; return $this; } public function getMaxRuntime(): ?int { return $this->maxRuntime; } public function setMaxRuntime(?int $maxRuntime): self { $this->maxRuntime = $maxRuntime; return $this; } public function getTimezone(): ?string { return $this->timezone; } public function setTimezone(?string $timezone): self { $this->timezone = $timezone; return $this; } public function getFailureRecoveryThreshold(): ?int { return $this->failureIssueThreshold; } public function setFailureRecoveryThreshold(?int $failureIssueThreshold): self { $this->failureIssueThreshold = $failureIssueThreshold; return $this; } public function getRecoveryThreshold(): ?int { return $this->recoveryThreshold; } public function setRecoveryThreshold(?int $recoveryThreshold): self { $this->recoveryThreshold = $recoveryThreshold; return $this; } /** * @return array */ public function toArray(): array { return [ 'schedule' => $this->schedule->toArray(), 'checkin_margin' => $this->checkinMargin, 'max_runtime' => $this->maxRuntime, 'timezone' => $this->timezone, 'failure_issue_threshold' => $this->failureIssueThreshold, 'recovery_threshold' => $this->recoveryThreshold, ]; } } PKî%W]#oØÑˆˆ%sentry/src/Context/RuntimeContext.phpnu„[µü¤ */ final class RuntimeContext { /** * @var string The name of the runtime */ private $name; /** * @var string|null The version of the runtime */ private $version; /** * @var string|null The SAPI (Server API) name */ private $sapi; /** * Constructor. * * @param string $name The name of the runtime * @param string|null $version The version of the runtime * @param string|null $sapi The SAPI name of the runtime */ public function __construct(string $name, ?string $version = null, ?string $sapi = null) { if (trim($name) === '') { throw new \InvalidArgumentException('The $name argument cannot be an empty string.'); } $this->name = $name; $this->version = $version; $this->sapi = $sapi; } /** * Gets the name of the runtime. */ public function getName(): string { return $this->name; } /** * Sets the name of the runtime. * * @param string $name The name */ public function setName(string $name): void { if (trim($name) === '') { throw new \InvalidArgumentException('The $name argument cannot be an empty string.'); } $this->name = $name; } /** * Gets the version of the runtime. */ public function getVersion(): ?string { return $this->version; } /** * Sets the version of the runtime. * * @param string|null $version The version */ public function setVersion(?string $version): void { $this->version = $version; } /** * Gets the SAPI of the runtime. */ public function getSAPI(): ?string { return $this->sapi; } /** * Sets the SAPI of the runtime. * * @param string|null $sapi The SAPI name */ public function setSAPI(?string $sapi): void { $this->sapi = $sapi; } } PKî%W]Î v«   sentry/src/Context/OsContext.phpnu„[µü¤ */ final class OsContext { /** * @var string The name of the operating system */ private $name; /** * @var string|null The version of the operating system */ private $version; /** * @var string|null The internal build revision of the operating system */ private $build; /** * @var string|null An independent kernel version string */ private $kernelVersion; /** * @var string|null */ private $machineType; /** * Constructor. * * @param string $name The name of the operating system * @param string|null $version The version of the operating system * @param string|null $build The internal build revision of the operating system * @param string|null $kernelVersion An independent kernel version string * @param string|null $machineType The machine type */ public function __construct( string $name, ?string $version = null, ?string $build = null, ?string $kernelVersion = null, ?string $machineType = null ) { if (trim($name) === '') { throw new \InvalidArgumentException('The $name argument cannot be an empty string.'); } $this->name = $name; $this->version = $version; $this->build = $build; $this->kernelVersion = $kernelVersion; $this->machineType = $machineType; } /** * Gets the name of the operating system. */ public function getName(): string { return $this->name; } /** * Sets the name of the operating system. * * @param string $name The name */ public function setName(string $name): void { if (trim($name) === '') { throw new \InvalidArgumentException('The $name argument cannot be an empty string.'); } $this->name = $name; } /** * Gets the version of the operating system. */ public function getVersion(): ?string { return $this->version; } /** * Sets the version of the operating system. * * @param string|null $version The version */ public function setVersion(?string $version): void { $this->version = $version; } /** * Gets the build of the operating system. */ public function getBuild(): ?string { return $this->build; } /** * Sets the build of the operating system. * * @param string|null $build The build */ public function setBuild(?string $build): void { $this->build = $build; } /** * Gets the version of the kernel of the operating system. */ public function getKernelVersion(): ?string { return $this->kernelVersion; } /** * Sets the version of the kernel of the operating system. * * @param string|null $kernelVersion The kernel version */ public function setKernelVersion(?string $kernelVersion): void { $this->kernelVersion = $kernelVersion; } public function getMachineType(): ?string { return $this->machineType; } /** * @param string|null $machineType The machine type */ public function setMachineType(?string $machineType): void { $this->machineType = $machineType; } } PKî%W],›=�‹‹sentry/src/State/HubAdapter.phpnu„[µü¤getClient(); } /** * {@inheritdoc} */ public function getLastEventId(): ?EventId { return SentrySdk::getCurrentHub()->getLastEventId(); } /** * {@inheritdoc} */ public function pushScope(): Scope { return SentrySdk::getCurrentHub()->pushScope(); } /** * {@inheritdoc} */ public function popScope(): bool { return SentrySdk::getCurrentHub()->popScope(); } /** * {@inheritdoc} */ public function withScope(callable $callback) { return SentrySdk::getCurrentHub()->withScope($callback); } /** * {@inheritdoc} */ public function configureScope(callable $callback): void { SentrySdk::getCurrentHub()->configureScope($callback); } /** * {@inheritdoc} */ public function bindClient(ClientInterface $client): void { SentrySdk::getCurrentHub()->bindClient($client); } /** * {@inheritdoc} */ public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); } /** * {@inheritdoc} */ public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureException($exception, $hint); } /** * {@inheritdoc} */ public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureEvent($event, $hint); } /** * {@inheritdoc} */ public function captureLastError(?EventHint $hint = null): ?EventId { return SentrySdk::getCurrentHub()->captureLastError($hint); } /** * {@inheritdoc} * * @param int|float|null $duration */ public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { return SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** * {@inheritdoc} */ public function addBreadcrumb(Breadcrumb $breadcrumb): bool { return SentrySdk::getCurrentHub()->addBreadcrumb($breadcrumb); } /** * {@inheritdoc} */ public function getIntegration(string $className): ?IntegrationInterface { return SentrySdk::getCurrentHub()->getIntegration($className); } /** * {@inheritdoc} */ public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); } /** * {@inheritdoc} */ public function getTransaction(): ?Transaction { return SentrySdk::getCurrentHub()->getTransaction(); } /** * {@inheritdoc} */ public function getSpan(): ?Span { return SentrySdk::getCurrentHub()->getSpan(); } /** * {@inheritdoc} */ public function setSpan(?Span $span): HubInterface { return SentrySdk::getCurrentHub()->setSpan($span); } /** * @see https://www.php.net/manual/en/language.oop5.cloning.php#object.clone */ public function __clone() { throw new \BadMethodCallException('Cloning is forbidden.'); } /** * @see https://www.php.net/manual/en/language.oop5.magic.php#object.wakeup */ public function __wakeup() { throw new \BadMethodCallException('Unserializing instances of this class is forbidden.'); } /** * @see https://www.php.net/manual/en/language.oop5.magic.php#object.sleep */ public function __sleep() { throw new \BadMethodCallException('Serializing instances of this class is forbidden.'); } } PKî%W]Ú¤!o²²!sentry/src/State/HubInterface.phpnu„[µü¤ $className * * @psalm-return T|null */ public function getIntegration(string $className): ?IntegrationInterface; /** * Starts a new `Transaction` and returns it. This is the entry point to manual * tracing instrumentation. * * A tree structure can be built by adding child spans to the transaction, and * child spans to other spans. To start a new child span within the transaction * or any span, call the respective `startChild()` method. * * Every child span must be finished before the transaction is finished, * otherwise the unfinished spans are discarded. * * The transaction must be finished with a call to its `finish()` method, at * which point the transaction with all its finished child spans will be sent to * Sentry. * * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} */ public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction; /** * Returns the transaction that is on the Hub. */ public function getTransaction(): ?Transaction; /** * Returns the span that is on the Hub. */ public function getSpan(): ?Span; /** * Sets the span on the Hub. */ public function setSpan(?Span $span): HubInterface; } PKî%W]HÚ«“::sentry/src/State/Layer.phpnu„[µü¤client = $client; $this->scope = $scope; } /** * Gets the client held by this layer. */ public function getClient(): ?ClientInterface { return $this->client; } /** * Sets the client held by this layer. * * @param ClientInterface|null $client The client instance * * @return $this */ public function setClient(?ClientInterface $client): self { $this->client = $client; return $this; } /** * Gets the scope held by this layer. */ public function getScope(): Scope { return $this->scope; } /** * Sets the scope held by this layer. * * @param Scope $scope The scope instance * * @return $this */ public function setScope(Scope $scope): self { $this->scope = $scope; return $this; } } PKî%W]:í´ 1 1sentry/src/State/Scope.phpnu„[µü¤> The list of contexts associated to this scope */ private $contexts = []; /** * @var array The list of tags associated to this scope */ private $tags = []; /** * @var array A set of extra data associated to this scope */ private $extra = []; /** * @var string[] List of fingerprints used to group events together in * Sentry */ private $fingerprint = []; /** * @var Severity|null The severity to associate to the events captured in * this scope */ private $level; /** * @var callable[] List of event processors * * @psalm-var array */ private $eventProcessors = []; /** * @var Span|null Set a Span on the Scope */ private $span; /** * @var callable[] List of event processors * * @psalm-var array */ private static $globalEventProcessors = []; public function __construct(?PropagationContext $propagationContext = null) { $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); } /** * Sets a new tag in the tags context. * * @param string $key The key that uniquely identifies the tag * @param string $value The value * * @return $this */ public function setTag(string $key, string $value): self { $this->tags[$key] = $value; return $this; } /** * Merges the given tags into the current tags context. * * @param array $tags The tags to merge into the current context * * @return $this */ public function setTags(array $tags): self { $this->tags = array_merge($this->tags, $tags); return $this; } /** * Removes a given tag from the tags context. * * @param string $key The key that uniquely identifies the tag * * @return $this */ public function removeTag(string $key): self { unset($this->tags[$key]); return $this; } /** * Sets data to the context by a given name. * * @param string $name The name that uniquely identifies the context * @param array $value The value * * @return $this */ public function setContext(string $name, array $value): self { if (!empty($value)) { $this->contexts[$name] = $value; } return $this; } /** * Removes the context from the scope. * * @param string $name The name that uniquely identifies the context * * @return $this */ public function removeContext(string $name): self { unset($this->contexts[$name]); return $this; } /** * Sets a new information in the extra context. * * @param string $key The key that uniquely identifies the information * @param mixed $value The value * * @return $this */ public function setExtra(string $key, $value): self { $this->extra[$key] = $value; return $this; } /** * Merges the given data into the current extras context. * * @param array $extras Data to merge into the current context * * @return $this */ public function setExtras(array $extras): self { $this->extra = array_merge($this->extra, $extras); return $this; } /** * Get the user context. */ public function getUser(): ?UserDataBag { return $this->user; } /** * Merges the given data in the user context. * * @param array|UserDataBag $user The user data * * @return $this */ public function setUser($user): self { if (!\is_array($user) && !$user instanceof UserDataBag) { throw new \TypeError(\sprintf('The $user argument must be either an array or an instance of the "%s" class. Got: "%s".', UserDataBag::class, get_debug_type($user))); } if (\is_array($user)) { $user = UserDataBag::createFromArray($user); } if ($this->user === null) { $this->user = $user; } else { $this->user = $this->user->merge($user); } return $this; } /** * Removes all data of the user context. * * @return $this */ public function removeUser(): self { $this->user = null; return $this; } /** * Sets the list of strings used to dictate the deduplication of this event. * * @param string[] $fingerprint The fingerprint values * * @return $this */ public function setFingerprint(array $fingerprint): self { $this->fingerprint = $fingerprint; return $this; } /** * Sets the severity to apply to all events captured in this scope. * * @param Severity|null $level The severity * * @return $this */ public function setLevel(?Severity $level): self { $this->level = $level; return $this; } /** * Add the given breadcrumb to the scope. * * @param Breadcrumb $breadcrumb The breadcrumb to add * @param int $maxBreadcrumbs The maximum number of breadcrumbs to record * * @return $this */ public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): self { $this->breadcrumbs[] = $breadcrumb; $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); return $this; } /** * Clears all the breadcrumbs. * * @return $this */ public function clearBreadcrumbs(): self { $this->breadcrumbs = []; return $this; } /** * Adds a new event processor that will be called after {@see Scope::applyToEvent} * finished its work. * * @param callable $eventProcessor The event processor * * @return $this */ public function addEventProcessor(callable $eventProcessor): self { $this->eventProcessors[] = $eventProcessor; return $this; } /** * Adds a new event processor that will be called after {@see Scope::applyToEvent} * finished its work. * * @param callable $eventProcessor The event processor */ public static function addGlobalEventProcessor(callable $eventProcessor): void { self::$globalEventProcessors[] = $eventProcessor; } /** * Clears the scope and resets any data it contains. * * @return $this */ public function clear(): self { $this->user = null; $this->level = null; $this->span = null; $this->fingerprint = []; $this->breadcrumbs = []; $this->tags = []; $this->extra = []; $this->contexts = []; return $this; } /** * Applies the current context and fingerprint to the event. If the event has * already some breadcrumbs on it, the ones from this scope won't get merged. * * @param Event $event The event object that will be enriched with scope data */ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event { $event->setFingerprint(array_merge($event->getFingerprint(), $this->fingerprint)); if (empty($event->getBreadcrumbs())) { $event->setBreadcrumb($this->breadcrumbs); } if ($this->level !== null) { $event->setLevel($this->level); } if (!empty($this->tags)) { $event->setTags(array_merge($this->tags, $event->getTags())); } if (!empty($this->extra)) { $event->setExtra(array_merge($this->extra, $event->getExtra())); } if ($this->user !== null) { $user = $event->getUser(); if ($user === null) { $user = $this->user; } else { $user = $this->user->merge($user); } $event->setUser($user); } /** * Apply the trace context to errors if there is a Span on the Scope. * Else fallback to the propagation context. * But do not override a trace context already present. */ if ($this->span !== null) { if (!\array_key_exists('trace', $event->getContexts())) { $event->setContext('trace', $this->span->getTraceContext()); } // Apply the dynamic sampling context to errors if there is a Transaction on the Scope $transaction = $this->span->getTransaction(); if ($transaction !== null) { $event->setSdkMetadata('dynamic_sampling_context', $transaction->getDynamicSamplingContext()); } } else { if (!\array_key_exists('trace', $event->getContexts())) { $event->setContext('trace', $this->propagationContext->getTraceContext()); } $dynamicSamplingContext = $this->propagationContext->getDynamicSamplingContext(); if ($dynamicSamplingContext === null && $options !== null) { $dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $this); } $event->setSdkMetadata('dynamic_sampling_context', $dynamicSamplingContext); } foreach (array_merge($this->contexts, $event->getContexts()) as $name => $data) { $event->setContext($name, $data); } // We create a empty `EventHint` instance to allow processors to always receive a `EventHint` instance even if there wasn't one if ($hint === null) { $hint = new EventHint(); } foreach (array_merge(self::$globalEventProcessors, $this->eventProcessors) as $processor) { $event = $processor($event, $hint); if ($event === null) { return null; } if (!$event instanceof Event) { throw new \InvalidArgumentException(\sprintf('The event processor must return null or an instance of the %s class', Event::class)); } } return $event; } /** * Returns the span that is on the scope. */ public function getSpan(): ?Span { return $this->span; } /** * Sets the span on the scope. * * @param Span|null $span The span * * @return $this */ public function setSpan(?Span $span): self { $this->span = $span; return $this; } /** * Returns the transaction attached to the scope (if there is one). */ public function getTransaction(): ?Transaction { if ($this->span !== null) { return $this->span->getTransaction(); } return null; } public function getPropagationContext(): PropagationContext { return $this->propagationContext; } public function setPropagationContext(PropagationContext $propagationContext): self { $this->propagationContext = $propagationContext; return $this; } public function __clone() { if ($this->user !== null) { $this->user = clone $this->user; } if ($this->propagationContext !== null) { $this->propagationContext = clone $this->propagationContext; } } } PKî%W]Ð 2stack[] = new Layer($client, $scope ?? new Scope()); } /** * {@inheritdoc} */ public function getClient(): ?ClientInterface { return $this->getStackTop()->getClient(); } /** * {@inheritdoc} */ public function getLastEventId(): ?EventId { return $this->lastEventId; } /** * {@inheritdoc} */ public function pushScope(): Scope { $clonedScope = clone $this->getScope(); $this->stack[] = new Layer($this->getClient(), $clonedScope); return $clonedScope; } /** * {@inheritdoc} */ public function popScope(): bool { if (\count($this->stack) === 1) { return false; } return array_pop($this->stack) !== null; } /** * {@inheritdoc} */ public function withScope(callable $callback) { $scope = $this->pushScope(); try { return $callback($scope); } finally { $this->popScope(); } } /** * {@inheritdoc} */ public function configureScope(callable $callback): void { $callback($this->getScope()); } /** * {@inheritdoc} */ public function bindClient(ClientInterface $client): void { $layer = $this->getStackTop(); $layer->setClient($client); } /** * {@inheritdoc} */ public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { $client = $this->getClient(); if ($client !== null) { return $this->lastEventId = $client->captureMessage($message, $level, $this->getScope(), $hint); } return null; } /** * {@inheritdoc} */ public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { $client = $this->getClient(); if ($client !== null) { return $this->lastEventId = $client->captureException($exception, $this->getScope(), $hint); } return null; } /** * {@inheritdoc} */ public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { $client = $this->getClient(); if ($client !== null) { return $this->lastEventId = $client->captureEvent($event, $hint, $this->getScope()); } return null; } /** * {@inheritdoc} */ public function captureLastError(?EventHint $hint = null): ?EventId { $client = $this->getClient(); if ($client !== null) { return $this->lastEventId = $client->captureLastError($this->getScope(), $hint); } return null; } /** * {@inheritdoc} * * @param int|float|null $duration */ public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { $client = $this->getClient(); if ($client === null) { return null; } $options = $client->getOptions(); $event = Event::createCheckIn(); $checkIn = new CheckIn( $slug, $status, $checkInId, $options->getRelease(), $options->getEnvironment(), $duration, $monitorConfig ); $event->setCheckIn($checkIn); $this->captureEvent($event); return $checkIn->getId(); } /** * {@inheritdoc} */ public function addBreadcrumb(Breadcrumb $breadcrumb): bool { $client = $this->getClient(); if ($client === null) { return false; } $options = $client->getOptions(); $beforeBreadcrumbCallback = $options->getBeforeBreadcrumbCallback(); $maxBreadcrumbs = $options->getMaxBreadcrumbs(); if ($maxBreadcrumbs <= 0) { return false; } $breadcrumb = $beforeBreadcrumbCallback($breadcrumb); if ($breadcrumb !== null) { $this->getScope()->addBreadcrumb($breadcrumb, $maxBreadcrumbs); } return $breadcrumb !== null; } /** * {@inheritdoc} */ public function getIntegration(string $className): ?IntegrationInterface { $client = $this->getClient(); if ($client !== null) { return $client->getIntegration($className); } return null; } /** * {@inheritdoc} * * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} */ public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { $transaction = new Transaction($context, $this); $client = $this->getClient(); $options = $client !== null ? $client->getOptions() : null; $logger = $options !== null ? $options->getLoggerOrNullLogger() : new NullLogger(); if ($options === null || !$options->isTracingEnabled()) { $transaction->setSampled(false); $logger->warning(\sprintf('Transaction [%s] was started but tracing is not enabled.', (string) $transaction->getTraceId()), ['context' => $context]); return $transaction; } $samplingContext = SamplingContext::getDefault($context); $samplingContext->setAdditionalContext($customSamplingContext); $sampleSource = 'context'; $sampleRand = $context->getMetadata()->getSampleRand(); if ($transaction->getSampled() === null) { $tracesSampler = $options->getTracesSampler(); if ($tracesSampler !== null) { $sampleRate = $tracesSampler($samplingContext); $sampleSource = 'config:traces_sampler'; } else { $parentSampleRate = $context->getMetadata()->getParentSamplingRate(); if ($parentSampleRate !== null) { $sampleRate = $parentSampleRate; $sampleSource = 'parent:sample_rate'; } else { $sampleRate = $this->getSampleRate( $samplingContext->getParentSampled(), $options->getTracesSampleRate() ?? 0 ); $sampleSource = $samplingContext->getParentSampled() !== null ? 'parent:sampling_decision' : 'config:traces_sample_rate'; } } if (!$this->isValidSampleRate($sampleRate)) { $transaction->setSampled(false); $logger->warning(\sprintf('Transaction [%s] was started but not sampled because sample rate (decided by %s) is invalid.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); return $transaction; } $transaction->getMetadata()->setSamplingRate($sampleRate); // Always overwrite the sample_rate in the DSC $dynamicSamplingContext = $context->getMetadata()->getDynamicSamplingContext(); if ($dynamicSamplingContext !== null) { $dynamicSamplingContext->set('sample_rate', (string) $sampleRate, true); } if ($sampleRate === 0.0) { $transaction->setSampled(false); $logger->info(\sprintf('Transaction [%s] was started but not sampled because sample rate (decided by %s) is %s.', (string) $transaction->getTraceId(), $sampleSource, $sampleRate), ['context' => $context]); return $transaction; } $transaction->setSampled($sampleRand < $sampleRate); } if (!$transaction->getSampled()) { $logger->info(\sprintf('Transaction [%s] was started but not sampled, decided by %s.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); return $transaction; } $logger->info(\sprintf('Transaction [%s] was started and sampled, decided by %s.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); $transaction->initSpanRecorder(); $profilesSampleRate = $options->getProfilesSampleRate(); if ($profilesSampleRate === null) { $logger->info(\sprintf('Transaction [%s] is not profiling because `profiles_sample_rate` option is not set.', (string) $transaction->getTraceId())); } elseif ($this->sample($profilesSampleRate)) { $logger->info(\sprintf('Transaction [%s] started profiling because it was sampled.', (string) $transaction->getTraceId())); $transaction->initProfiler()->start(); } else { $logger->info(\sprintf('Transaction [%s] is not profiling because it was not sampled.', (string) $transaction->getTraceId())); } return $transaction; } /** * {@inheritdoc} */ public function getTransaction(): ?Transaction { return $this->getScope()->getTransaction(); } /** * {@inheritdoc} */ public function setSpan(?Span $span): HubInterface { $this->getScope()->setSpan($span); return $this; } /** * {@inheritdoc} */ public function getSpan(): ?Span { return $this->getScope()->getSpan(); } /** * Gets the scope bound to the top of the stack. */ private function getScope(): Scope { return $this->getStackTop()->getScope(); } /** * Gets the topmost client/layer pair in the stack. */ private function getStackTop(): Layer { return $this->stack[\count($this->stack) - 1]; } private function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float { if ($hasParentBeenSampled === true) { return 1.0; } if ($hasParentBeenSampled === false) { return 0.0; } return $fallbackSampleRate; } /** * @param mixed $sampleRate */ private function sample($sampleRate): bool { if ($sampleRate === 0.0 || $sampleRate === null) { return false; } if ($sampleRate === 1.0) { return true; } return mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() < $sampleRate; } /** * @param mixed $sampleRate */ private function isValidSampleRate($sampleRate): bool { if (!\is_float($sampleRate) && !\is_int($sampleRate)) { return false; } if ($sampleRate < 0 || $sampleRate > 1) { return false; } return true; } } PKî%W]PPßÅ Å sentry/src/Stacktrace.phpnu„[µü¤ */ final class Stacktrace { /** * @var Frame[] The frames that compose the stacktrace */ private $frames = []; /** * Constructor. * * @param Frame[] $frames A non-empty list of stack frames. The list must be * ordered from caller to callee. The last frame is the * one creating the exception */ public function __construct(array $frames) { if (empty($frames)) { throw new \InvalidArgumentException('Expected a non empty list of frames.'); } foreach ($frames as $frame) { if (!$frame instanceof Frame) { throw new \UnexpectedValueException(\sprintf('Expected an instance of the "%s" class. Got: "%s".', Frame::class, get_debug_type($frame))); } } $this->frames = $frames; } /** * Gets the stacktrace frames. * * @return Frame[] */ public function getFrames(): array { return $this->frames; } /** * Gets the frame at the given index. * * @param int $index The index from which the frame should be get * * @throws \OutOfBoundsException */ public function getFrame(int $index): Frame { if ($index < 0 || $index >= \count($this->frames)) { throw new \OutOfBoundsException(); } return $this->frames[$index]; } /** * Adds a new frame to the stacktrace. * * @param Frame $frame The frame */ public function addFrame(Frame $frame): self { array_unshift($this->frames, $frame); return $this; } /** * Removes the frame at the given index from the stacktrace. * * @param int $index The index of the frame * * @throws \OutOfBoundsException If the index is out of range */ public function removeFrame(int $index): self { if (!isset($this->frames[$index])) { throw new \OutOfBoundsException(\sprintf('Cannot remove the frame at index %d.', $index)); } if (\count($this->frames) === 1) { throw new \RuntimeException('Cannot remove all frames from the stacktrace.'); } array_splice($this->frames, $index, 1); return $this; } } PKî%W]H=Ëó¼#¼#sentry/src/FrameBuilder.phpnu„[µü¤options = $options; $this->representationSerializer = $representationSerializer; } /** * Builds a {@see Frame} object from the given backtrace's raw frame. * * @param string $file The file where the frame originated * @param int $line The line at which the frame originated * @param array $backtraceFrame The raw frame * * @psalm-param StacktraceFrame $backtraceFrame */ public function buildFromBacktraceFrame(string $file, int $line, array $backtraceFrame): Frame { // The filename can be in any of these formats: // - // - () : eval()'d code // - () : runtime-created function if (preg_match('/^(.*)\((\d+)\) : (?:eval\(\)\'d code|runtime-created function)$/', $file, $matches)) { $file = $matches[1]; $line = (int) $matches[2]; } $functionName = null; $rawFunctionName = null; $strippedFilePath = $this->stripPrefixFromFilePath($this->options, $file); if (isset($backtraceFrame['class']) && isset($backtraceFrame['function'])) { $functionName = $backtraceFrame['class']; // Skip if no prefixes are set if ($this->options->getPrefixes()) { $prefixStrippedFunctionName = preg_replace_callback('/@anonymous\\x00([^:]+)(:.*)?/', function (array $matches) { return "@anonymous\x00" . $this->stripPrefixFromFilePath($this->options, $matches[1]) . ($matches[2] ?? ''); }, $functionName); if ($prefixStrippedFunctionName) { $functionName = $prefixStrippedFunctionName; } } $rawFunctionName = \sprintf('%s::%s', $backtraceFrame['class'], $backtraceFrame['function']); $functionName = \sprintf('%s::%s', preg_replace('/(?::\d+\$|0x)[a-fA-F0-9]+$/', '', $functionName), $backtraceFrame['function']); } elseif (isset($backtraceFrame['function'])) { $functionName = $backtraceFrame['function']; } // Starting with PHP 8.4 a closure function call is reported as "{closure:filename:line}" instead of just "{closure}", properly strip the prefixes from that format if (\PHP_VERSION_ID >= 80400 && $functionName !== null && $this->options->getPrefixes()) { $prefixStrippedFunctionName = preg_replace_callback('/^\{closure:(.*?):(\d+)}$/', function (array $matches) { return '{closure:' . $this->stripPrefixFromFilePath($this->options, $matches[1]) . ':' . $matches[2] . '}'; }, $functionName); if ($prefixStrippedFunctionName) { $functionName = $prefixStrippedFunctionName; } } return new Frame( $functionName, $strippedFilePath, $line, $rawFunctionName, $file !== Frame::INTERNAL_FRAME_FILENAME ? $file : null, $this->getFunctionArguments($backtraceFrame), $this->isFrameInApp($file, $functionName) ); } /** * Checks whether a certain frame should be marked as "in app" or not. * * @param string $file The file to check * @param string|null $functionName The name of the function */ private function isFrameInApp(string $file, ?string $functionName): bool { if ($file === Frame::INTERNAL_FRAME_FILENAME) { return false; } if ($functionName !== null && substr($functionName, 0, \strlen('Sentry\\')) === 'Sentry\\') { return false; } $excludedAppPaths = $this->options->getInAppExcludedPaths(); $includedAppPaths = $this->options->getInAppIncludedPaths(); $absoluteFilePath = @realpath($file) ?: $file; $isInApp = true; foreach ($excludedAppPaths as $excludedAppPath) { if (mb_substr($absoluteFilePath, 0, mb_strlen($excludedAppPath)) === $excludedAppPath) { $isInApp = false; break; } } foreach ($includedAppPaths as $includedAppPath) { if (mb_substr($absoluteFilePath, 0, mb_strlen($includedAppPath)) === $includedAppPath) { $isInApp = true; break; } } return $isInApp; } /** * Gets the arguments of the function called in the given frame. * * @param array $backtraceFrame The frame data * * @psalm-param StacktraceFrame $backtraceFrame * * @return array */ private function getFunctionArguments(array $backtraceFrame): array { if (!isset($backtraceFrame['function'], $backtraceFrame['args'])) { return []; } $reflectionFunction = null; try { if (isset($backtraceFrame['class'])) { if (method_exists($backtraceFrame['class'], $backtraceFrame['function'])) { $reflectionFunction = new \ReflectionMethod($backtraceFrame['class'], $backtraceFrame['function']); } elseif (isset($backtraceFrame['type']) && $backtraceFrame['type'] === '::') { $reflectionFunction = new \ReflectionMethod($backtraceFrame['class'], '__callStatic'); } else { $reflectionFunction = new \ReflectionMethod($backtraceFrame['class'], '__call'); } } elseif ($backtraceFrame['function'] !== '__lambda_func' && !str_starts_with($backtraceFrame['function'], '{closure') && \function_exists($backtraceFrame['function'])) { $reflectionFunction = new \ReflectionFunction($backtraceFrame['function']); } } catch (\ReflectionException $e) { // Reflection failed, we do nothing instead } $argumentValues = []; if ($reflectionFunction !== null) { $argumentValues = $this->getFunctionArgumentValues($reflectionFunction, $backtraceFrame['args']); } else { foreach ($backtraceFrame['args'] as $parameterPosition => $parameterValue) { $argumentValues['param' . $parameterPosition] = $parameterValue; } } foreach ($argumentValues as $argumentName => $argumentValue) { $argumentValues[$argumentName] = $this->representationSerializer->representationSerialize($argumentValue); } return $argumentValues; } /** * Gets an hashmap indexed by argument name containing all the arguments * passed to the function called in the given frame of the stacktrace. * * @param \ReflectionFunctionAbstract $reflectionFunction A reflection object * @param mixed[] $backtraceFrameArgs The arguments of the frame * * @return array */ private function getFunctionArgumentValues(\ReflectionFunctionAbstract $reflectionFunction, array $backtraceFrameArgs): array { $argumentValues = []; foreach ($reflectionFunction->getParameters() as $reflectionParameter) { $parameterPosition = $reflectionParameter->getPosition(); if ($reflectionParameter->isVariadic()) { // For variadic parameters, collect all remaining arguments into an array $variadicArgs = \array_slice($backtraceFrameArgs, $parameterPosition); $argumentValues[$reflectionParameter->getName()] = array_values($variadicArgs); // Variadic parameter is always the last one, so we can break break; } if (!\array_key_exists($parameterPosition, $backtraceFrameArgs)) { continue; } $argumentValues[$reflectionParameter->getName()] = $backtraceFrameArgs[$parameterPosition]; } return $argumentValues; } } PKî%W]�1ˆùTT"sentry/src/Logs/LogsAggregator.phpnu„[µü¤ $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function add( LogLevel $level, string $message, array $values = [], array $attributes = [] ): void { $timestamp = microtime(true); $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); // There is no need to continue if there is no client if ($client === null) { return; } $options = $client->getOptions(); $sdkLogger = $options->getLogger(); if (!$options->getEnableLogs()) { if ($sdkLogger !== null) { $sdkLogger->info( 'Log will be discarded because "enable_logs" is "false".' ); } return; } $formattedMessage = Str::vsprintfOrNull($message, $values); if ($formattedMessage === null) { // If formatting fails we don't format the message and log the error if ($sdkLogger !== null) { $sdkLogger->warning('Failed to format log message with values.', [ 'message' => $message, 'values' => $values, ]); } $formattedMessage = $message; } $log = (new Log($timestamp, $this->getTraceId($hub), $level, $formattedMessage)) ->setAttribute('sentry.release', $options->getRelease()) ->setAttribute('sentry.environment', $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT) ->setAttribute('sentry.server.address', $options->getServerName()) ->setAttribute('sentry.trace.parent_span_id', $hub->getSpan() ? $hub->getSpan()->getSpanId() : null); if ($client instanceof Client) { $log->setAttribute('sentry.sdk.name', $client->getSdkIdentifier()); $log->setAttribute('sentry.sdk.version', $client->getSdkVersion()); } $hub->configureScope(function (Scope $scope) use ($log) { $user = $scope->getUser(); if ($user !== null) { if ($user->getId() !== null) { $log->setAttribute('user.id', $user->getId()); } if ($user->getEmail() !== null) { $log->setAttribute('user.email', $user->getEmail()); } if ($user->getUsername() !== null) { $log->setAttribute('user.name', $user->getUsername()); } } }); if (\count($values)) { $log->setAttribute('sentry.message.template', $message); foreach ($values as $key => $value) { $log->setAttribute("sentry.message.parameter.{$key}", $value); } } $attributes = Arr::simpleDot($attributes); foreach ($attributes as $key => $value) { $attribute = Attribute::tryFromValue($value); if (!\is_string($key)) { if ($sdkLogger !== null) { $sdkLogger->info( \sprintf("Dropping log attribute with non-string key '%s' and value of type '%s'.", $key, \gettype($value)) ); } continue; } if ($attribute === null) { if ($sdkLogger !== null) { $sdkLogger->info( \sprintf("Dropping log attribute {$key} with value of type '%s' because it is not serializable or an unsupported type.", \gettype($value)) ); } continue; } $log->setAttribute($key, $attribute); } $log = ($options->getBeforeSendLogCallback())($log); if ($log === null) { if ($sdkLogger !== null) { $sdkLogger->info( 'Log will be discarded because the "before_send_log" callback returned "null".', ['log' => $log] ); } return; } if ($sdkLogger !== null) { $sdkLogger->log($log->getPsrLevel(), "Logs item: {$log->getBody()}", $log->attributes()->toSimpleArray()); } $this->logs[] = $log; } public function flush(): ?EventId { if (empty($this->logs)) { return null; } $hub = SentrySdk::getCurrentHub(); $event = Event::createLogs()->setLogs($this->logs); $this->logs = []; return $hub->captureEvent($event); } /** * @return Log[] */ public function all(): array { return $this->logs; } private function getTraceId(HubInterface $hub): string { $span = $hub->getSpan(); if ($span !== null) { return (string) $span->getTraceId(); } $traceId = ''; $hub->configureScope(function (Scope $scope) use (&$traceId) { $traceId = (string) $scope->getPropagationContext()->getTraceId(); }); return $traceId; } } PKî%W]Ûy%ÍÇÇsentry/src/Logs/Logs.phpnu„[µü¤aggregator = new LogsAggregator(); } public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * @param string $message see sprintf for a description of format * @param array $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function trace(string $message, array $values = [], array $attributes = []): void { $this->aggregator->add(LogLevel::trace(), $message, $values, $attributes); } /** * @param string $message see sprintf for a description of format * @param array $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function debug(string $message, array $values = [], array $attributes = []): void { $this->aggregator->add(LogLevel::debug(), $message, $values, $attributes); } /** * @param string $message see sprintf for a description of format * @param array $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function info(string $message, array $values = [], array $attributes = []): void { $this->aggregator->add(LogLevel::info(), $message, $values, $attributes); } /** * @param string $message see sprintf for a description of format * @param array $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function warn(string $message, array $values = [], array $attributes = []): void { $this->aggregator->add(LogLevel::warn(), $message, $values, $attributes); } /** * @param string $message see sprintf for a description of format * @param array $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function error(string $message, array $values = [], array $attributes = []): void { $this->aggregator->add(LogLevel::error(), $message, $values, $attributes); } /** * @param string $message see sprintf for a description of format * @param array $values see sprintf for a description of values * @param array $attributes additional attributes to add to the log */ public function fatal(string $message, array $values = [], array $attributes = []): void { $this->aggregator->add(LogLevel::fatal(), $message, $values, $attributes); } /** * Flush the captured logs and send them to Sentry. */ public function flush(): ?EventId { return $this->aggregator->flush(); } /** * Get the logs aggregator. * * @internal */ public function aggregator(): LogsAggregator { return $this->aggregator; } } PKî%W] SöŒŒsentry/src/Logs/Log.phpnu„[µü¤timestamp = $timestamp; $this->traceId = $traceId; $this->level = $level; $this->body = $body; $this->attributes = new AttributeBag(); } public function getTimestamp(): float { return $this->timestamp; } public function setTimestamp(float $timestamp): self { $this->timestamp = $timestamp; return $this; } public function getTraceId(): string { return $this->traceId; } public function setTraceId(string $traceId): self { $this->traceId = $traceId; return $this; } public function getLevel(): LogLevel { return $this->level; } public function setLevel(LogLevel $level): self { $this->level = $level; return $this; } public function getPsrLevel(): string { return $this->level->toPsrLevel(); } public function getBody(): string { return $this->body; } public function setBody(string $body): self { $this->body = $body; return $this; } public function attributes(): AttributeBag { return $this->attributes; } /** * @param mixed $value */ public function setAttribute(string $key, $value): self { $this->attributes->set($key, $value); return $this; } } PKî%W]æ™Fællsentry/src/Logs/LogLevel.phpnu„[µü¤ A list of cached enum instances */ private static $instances = []; private function __construct(string $value, int $priority) { $this->value = $value; $this->priority = $priority; } public static function trace(): self { return self::getInstance('trace', 10); } public static function debug(): self { return self::getInstance('debug', 20); } public static function info(): self { return self::getInstance('info', 30); } public static function warn(): self { return self::getInstance('warn', 40); } public static function error(): self { return self::getInstance('error', 50); } public static function fatal(): self { return self::getInstance('fatal', 60); } public function __toString(): string { return $this->value; } public function getPriority(): int { return $this->priority; } public function toPsrLevel(): string { switch ($this->value) { case 'trace': case 'debug': return \Psr\Log\LogLevel::DEBUG; case 'warn': return \Psr\Log\LogLevel::WARNING; case 'error': return \Psr\Log\LogLevel::ERROR; case 'fatal': return \Psr\Log\LogLevel::CRITICAL; case 'info': default: return \Psr\Log\LogLevel::INFO; } } private static function getInstance(string $value, int $priority): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value, $priority); } return self::$instances[$value]; } } PKî%W]}cUíÈÈsentry/src/Severity.phpnu„[µü¤ */ final class Severity implements \Stringable { /** * This constant represents the "debug" severity level. * * @internal */ public const DEBUG = 'debug'; /** * This constant represents the "info" severity level. * * @internal */ public const INFO = 'info'; /** * This constant represents the "warning" severity level. * * @internal */ public const WARNING = 'warning'; /** * This constant represents the "error" severity level. * * @internal */ public const ERROR = 'error'; /** * This constant represents the "fatal" severity level. * * @internal */ public const FATAL = 'fatal'; /** * This constant contains the list of allowed enum values. * * @internal */ public const ALLOWED_SEVERITIES = [ self::DEBUG, self::INFO, self::WARNING, self::ERROR, self::FATAL, ]; /** * @var string The value of this enum instance */ private $value; /** * Constructor. * * @param string $value The value this instance represents */ public function __construct(string $value = self::INFO) { if (!\in_array($value, self::ALLOWED_SEVERITIES, true)) { throw new \InvalidArgumentException(\sprintf('The "%s" is not a valid enum value.', $value)); } $this->value = $value; } /** * Translate a PHP Error constant into a Sentry log level group. * * @param int $severity PHP E_* error constant */ public static function fromError(int $severity): self { switch ($severity) { case \E_DEPRECATED: case \E_USER_DEPRECATED: case \E_WARNING: case \E_USER_WARNING: return self::warning(); case \E_ERROR: case \E_PARSE: case \E_CORE_ERROR: case \E_CORE_WARNING: case \E_COMPILE_ERROR: case \E_COMPILE_WARNING: return self::fatal(); case \E_RECOVERABLE_ERROR: case \E_USER_ERROR: return self::error(); case \E_NOTICE: case \E_USER_NOTICE: case 2048: // This is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices return self::info(); default: return self::error(); } } /** * Creates a new instance of this enum for the "debug" value. */ public static function debug(): self { return new self(self::DEBUG); } /** * Creates a new instance of this enum for the "info" value. */ public static function info(): self { return new self(self::INFO); } /** * Creates a new instance of this enum for the "warning" value. */ public static function warning(): self { return new self(self::WARNING); } /** * Creates a new instance of this enum for the "error" value. */ public static function error(): self { return new self(self::ERROR); } /** * Creates a new instance of this enum for the "fatal" value. */ public static function fatal(): self { return new self(self::FATAL); } /** * Returns whether two object instances of this class are equal. * * @param self $other The object to compare */ public function isEqualTo(self $other): bool { return $this->value === (string) $other; } /** * @see https://www.php.net/manual/en/language.oop5.magic.php#object.tostring */ public function __toString(): string { return $this->value; } } PKî%W]õ*½î‰‰%sentry/src/Logger/DebugFileLogger.phpnu„[µü¤filePath = $filePath; } public function write(string $message): void { file_put_contents($this->filePath, $message, \FILE_APPEND); } } PKî%W] 'v˜ÝÝ'sentry/src/Logger/DebugStdOutLogger.phpnu„[µü¤write( \sprintf("sentry/sentry: [%s] %s\n", $level, $formattedMessageAndContext) ); } abstract public function write(string $message): void; } PKî%W]r{Z9• • sentry/src/ClientInterface.phpnu„[µü¤ $className * * @psalm-return T|null */ public function getIntegration(string $className): ?IntegrationInterface; /** * Flushes the queue of events pending to be sent. If a timeout is provided * and the queue takes longer to drain, the promise resolves with `false`. * * @param int|null $timeout Maximum time in seconds the client should wait */ public function flush(?int $timeout = null): Result; /** * Returns the stacktrace builder of the client. */ public function getStacktraceBuilder(): StacktraceBuilder; } PKî%W]èå@®))&sentry/src/Attributes/AttributeBag.phpnu„[µü¤ */ private $attributes = []; /** * @param mixed $value */ public function set(string $key, $value): self { $attribute = $value instanceof Attribute ? $value : Attribute::tryFromValue($value); if ($attribute !== null) { $this->attributes[$key] = $attribute; } return $this; } public function get(string $key): ?Attribute { return $this->attributes[$key] ?? null; } public function forget(string $key): self { unset($this->attributes[$key]); return $this; } /** * @return array */ public function all(): array { return $this->attributes; } /** * Get a simplified representation of the attributes as a key-value array, main purpose is for logging output. * * @return array */ public function toSimpleArray(): array { return array_map(static function (Attribute $attribute) { return $attribute->getValue(); }, $this->attributes); } } PKî%W]4ÂMM#sentry/src/Attributes/Attribute.phpnu„[µü¤value = $value; $this->type = $type; } /** * @return AttributeType */ public function getType(): string { return $this->type; } /** * @return AttributeValue */ public function getValue() { return $this->value; } /** * @param mixed $value * * @throws \InvalidArgumentException thrown when the value cannot be serialized as an attribute */ public static function fromValue($value): self { $attribute = self::tryFromValue($value); if ($attribute === null) { throw new \InvalidArgumentException(\sprintf('Invalid attribute value, %s cannot be serialized', \gettype($value))); } return $attribute; } /** * @param mixed $value */ public static function tryFromValue($value): ?self { if ($value === null) { return null; } if (\is_bool($value)) { return new self($value, 'boolean'); } if (\is_int($value)) { return new self($value, 'integer'); } if (\is_float($value)) { return new self($value, 'double'); } if (\is_string($value) || (\is_object($value) && method_exists($value, '__toString'))) { $stringValue = (string) $value; if (empty($stringValue)) { return null; } return new self($stringValue, 'string'); } return null; } public function __toString(): string { return "{$this->value} ({$this->type})"; } } PKî%W]ΊPsentry/src/EventType.phpnu„[µü¤ */ final class EventType implements \Stringable { /** * @var string The value of the enum instance */ private $value; /** * @var array A list of cached enum instances */ private static $instances = []; private function __construct(string $value) { $this->value = $value; } public static function event(): self { return self::getInstance('event'); } public static function transaction(): self { return self::getInstance('transaction'); } public static function checkIn(): self { return self::getInstance('check_in'); } public static function logs(): self { return self::getInstance('log'); } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public static function metrics(): self { return self::getInstance('metrics'); } /** * List of all cases on the enum. * * @return self[] */ public static function cases(): array { return [ self::event(), self::transaction(), self::checkIn(), self::logs(), self::metrics(), ]; } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W]ò/ÞÞsentry/src/EventId.phpnu„[µü¤ */ final class EventId implements \Stringable { /** * @var string The ID */ private $value; /** * Class constructor. * * @param string $value The ID */ public function __construct(string $value) { if (!preg_match('/^[a-f0-9]{32}$/i', $value)) { throw new \InvalidArgumentException('The $value argument must be a 32 characters long hexadecimal string.'); } $this->value = $value; } /** * Generates a new event ID. * * @copyright Matt Farina MIT License https://github.com/lootils/uuid/blob/master/LICENSE */ public static function generate(): self { return new self(SentryUid::generate()); } public function __toString(): string { return $this->value; } } PKî%W]RÖššsentry/src/Dsn.phpnu„[µü¤ */ final class Dsn implements \Stringable { /** * @var string Regex to match the organization ID in the host. * This only applies to Sentry SaaS DSNs that contain the organization ID. */ private const SENTRY_ORG_ID_REGEX = '/^o(\d+)\./'; /** * @var string The protocol to be used to access the resource */ private $scheme; /** * @var string The host that holds the resource */ private $host; /** * @var int The port on which the resource is exposed */ private $port; /** * @var string The public key to authenticate the SDK */ private $publicKey; /** * @var string The ID of the resource to access */ private $projectId; /** * @var string The specific resource that the web client wants to access */ private $path; /** * @var int|null */ private $orgId; /** * Class constructor. * * @param string $scheme The protocol to be used to access the resource * @param string $host The host that holds the resource * @param int $port The port on which the resource is exposed * @param string $projectId The ID of the resource to access * @param string $path The specific resource that the web client wants to access * @param string $publicKey The public key to authenticate the SDK * @param ?int $orgId The org ID */ private function __construct(string $scheme, string $host, int $port, string $projectId, string $path, string $publicKey, ?int $orgId = null) { $this->scheme = $scheme; $this->host = $host; $this->port = $port; $this->projectId = $projectId; $this->path = $path; $this->publicKey = $publicKey; $this->orgId = $orgId; } /** * Creates an instance of this class by parsing the given string. * * @param string $value The string to parse */ public static function createFromString(string $value): self { $parsedDsn = parse_url($value); if ($parsedDsn === false) { throw new \InvalidArgumentException(\sprintf('The "%s" DSN is invalid.', $value)); } foreach (['scheme', 'host', 'path', 'user'] as $component) { if (!isset($parsedDsn[$component]) || (isset($parsedDsn[$component]) && empty($parsedDsn[$component]))) { throw new \InvalidArgumentException(\sprintf('The "%s" DSN must contain a scheme, a host, a user and a path component.', $value)); } } if (!\in_array($parsedDsn['scheme'], ['http', 'https'], true)) { throw new \InvalidArgumentException(\sprintf('The scheme of the "%s" DSN must be either "http" or "https".', $value)); } $segmentPaths = explode('/', $parsedDsn['path']); $projectId = array_pop($segmentPaths); $lastSlashPosition = strrpos($parsedDsn['path'], '/'); $path = $parsedDsn['path']; if ($lastSlashPosition !== false) { $path = substr($parsedDsn['path'], 0, $lastSlashPosition); } $orgId = null; if (preg_match(self::SENTRY_ORG_ID_REGEX, $parsedDsn['host'], $matches) == 1) { $orgId = (int) $matches[1]; } return new self( $parsedDsn['scheme'], $parsedDsn['host'], $parsedDsn['port'] ?? ($parsedDsn['scheme'] === 'http' ? 80 : 443), $projectId, $path, $parsedDsn['user'], $orgId ); } /** * Gets the protocol to be used to access the resource. */ public function getScheme(): string { return $this->scheme; } /** * Gets the host that holds the resource. */ public function getHost(): string { return $this->host; } /** * Gets the port on which the resource is exposed. */ public function getPort(): int { return $this->port; } /** * Gets the specific resource that the web client wants to access. */ public function getPath(): string { return $this->path; } /** * Gets the ID of the resource to access. */ public function getProjectId(): string { return $this->projectId; } /** * Gets the public key to authenticate the SDK. */ public function getPublicKey(): string { return $this->publicKey; } public function getOrgId(): ?int { return $this->orgId; } /** * Returns the URL of the API for the envelope endpoint. */ public function getEnvelopeApiEndpointUrl(): string { return $this->getBaseEndpointUrl() . '/envelope/'; } /** * Returns the URL of the API for the CSP report endpoint. */ public function getCspReportEndpointUrl(): string { return $this->getBaseEndpointUrl() . '/security/?sentry_key=' . $this->publicKey; } /** * @see https://www.php.net/manual/en/language.oop5.magic.php#object.tostring */ public function __toString(): string { $url = $this->scheme . '://' . $this->publicKey; $url .= '@' . $this->host; if (($this->scheme === 'http' && $this->port !== 80) || ($this->scheme === 'https' && $this->port !== 443)) { $url .= ':' . $this->port; } if ($this->path !== null) { $url .= $this->path; } $url .= '/' . $this->projectId; return $url; } /** * Returns the base url to Sentry from the DSN. */ private function getBaseEndpointUrl(): string { $url = $this->scheme . '://' . $this->host; if (($this->scheme === 'http' && $this->port !== 80) || ($this->scheme === 'https' && $this->port !== 443)) { $url .= ':' . $this->port; } if ($this->path !== null) { $url .= $this->path; } $url .= '/api/' . $this->projectId; return $url; } } PKî%W]σ4¬  sentry/src/CheckIn.phpnu„[µü¤setMonitorSlug($monitorSlug); $this->setStatus($status); $this->setId($id ?? SentryUid::generate()); $this->setRelease($release ?? ''); $this->setEnvironment($environment ?? Event::DEFAULT_ENVIRONMENT); $this->setDuration($duration); $this->setMonitorConfig($monitorConfig); } public function getId(): string { return $this->id; } public function setId(string $id): void { $this->id = $id; } public function getMonitorSlug(): string { return $this->monitorSlug; } public function setMonitorSlug(string $monitorSlug): self { $this->monitorSlug = $monitorSlug; return $this; } public function getStatus(): CheckInStatus { return $this->status; } public function setStatus(CheckInStatus $status): self { $this->status = $status; return $this; } public function getRelease(): ?string { return $this->release; } public function setRelease(string $release): self { $this->release = $release; return $this; } public function getEnvironment(): ?string { return $this->environment; } public function setEnvironment(string $environment): self { $this->environment = $environment; return $this; } /** * @return int|float|null */ public function getDuration() { return $this->duration; } /** * @param int|float|null $duration The duration of the check-in in seconds */ public function setDuration($duration): self { $this->duration = $duration; return $this; } public function getMonitorConfig(): ?MonitorConfig { return $this->monitorConfig; } public function setMonitorConfig(?MonitorConfig $monitorConfig): self { $this->monitorConfig = $monitorConfig; return $this; } } PKî%W]¢Š.’ã ã sentry/src/EventHint.phpnu„[µü¤ */ public $extra = []; /** * Create a EventHint instance from an array of values. * * @psalm-param array{ * exception?: \Throwable|null, * mechanism?: ExceptionMechanism|null, * stacktrace?: Stacktrace|null, * extra?: array * } $hintData */ public static function fromArray(array $hintData): self { $hint = new self(); $exception = $hintData['exception'] ?? null; $mechanism = $hintData['mechanism'] ?? null; $stacktrace = $hintData['stacktrace'] ?? null; $extra = $hintData['extra'] ?? []; if ($exception !== null && !$exception instanceof \Throwable) { throw new \InvalidArgumentException(\sprintf('The value of the "exception" field must be an instance of a class implementing the "%s" interface. Got: "%s".', \Throwable::class, get_debug_type($exception))); } if ($mechanism !== null && !$mechanism instanceof ExceptionMechanism) { throw new \InvalidArgumentException(\sprintf('The value of the "mechanism" field must be an instance of the "%s" class. Got: "%s".', ExceptionMechanism::class, get_debug_type($mechanism))); } if ($stacktrace !== null && !$stacktrace instanceof Stacktrace) { throw new \InvalidArgumentException(\sprintf('The value of the "stacktrace" field must be an instance of the "%s" class. Got: "%s".', Stacktrace::class, get_debug_type($stacktrace))); } if (!\is_array($extra)) { throw new \InvalidArgumentException(\sprintf('The value of the "extra" field must be an array. Got: "%s".', get_debug_type($extra))); } $hint->exception = $exception; $hint->mechanism = $mechanism; $hint->stacktrace = $stacktrace; $hint->extra = $extra; return $hint; } } PKî%W]Þ9ô*11sentry/src/MonitorSchedule.phpnu„[µü¤type = $type; $this->value = $value; $this->unit = $unit; } public static function crontab(string $value): self { return new self(self::TYPE_CRONTAB, $value); } public static function interval(int $value, MonitorScheduleUnit $unit): self { return new self(self::TYPE_INTERVAL, $value, $unit); } public function getType(): string { return $this->type; } public function setType(string $type): self { $this->type = $type; return $this; } /** * @return string|int */ public function getValue() { return $this->value; } /** * @param string|int $value */ public function setValue($value): self { $this->value = $value; return $this; } public function getUnit(): ?MonitorScheduleUnit { return $this->unit; } public function setUnit(?MonitorScheduleUnit $unit): self { $this->unit = $unit; return $this; } /** * @return array */ public function toArray(): array { return [ 'type' => $this->type, 'value' => $this->value, 'unit' => (string) $this->unit, ]; } } PKî%W]6 Z’àCàCsentry/src/Client.phpnu„[µü¤ The stack of integrations * * @psalm-var array, IntegrationInterface> */ private $integrations; /** * @var StacktraceBuilder */ private $stacktraceBuilder; /** * @var string The Sentry SDK identifier */ private $sdkIdentifier; /** * @var string The SDK version of the Client */ private $sdkVersion; /** * Constructor. * * @param Options $options The client configuration * @param TransportInterface $transport The transport * @param string|null $sdkIdentifier The Sentry SDK identifier * @param string|null $sdkVersion The Sentry SDK version * @param RepresentationSerializerInterface|null $representationSerializer The serializer for function arguments * @param LoggerInterface|null $logger The PSR-3 logger */ public function __construct( Options $options, TransportInterface $transport, ?string $sdkIdentifier = null, ?string $sdkVersion = null, ?RepresentationSerializerInterface $representationSerializer = null, ?LoggerInterface $logger = null ) { $this->options = $options; $this->transport = $transport; $this->sdkIdentifier = $sdkIdentifier ?? self::SDK_IDENTIFIER; $this->sdkVersion = $sdkVersion ?? self::SDK_VERSION; $this->stacktraceBuilder = new StacktraceBuilder($options, $representationSerializer ?? new RepresentationSerializer($this->options)); $this->logger = $logger ?? new NullLogger(); $this->integrations = IntegrationRegistry::getInstance()->setupIntegrations($options, $this->logger); } /** * {@inheritdoc} */ public function getOptions(): Options { return $this->options; } /** * {@inheritdoc} */ public function getCspReportUrl(): ?string { $dsn = $this->options->getDsn(); if ($dsn === null) { return null; } $endpoint = $dsn->getCspReportEndpointUrl(); $query = array_filter([ 'sentry_release' => $this->options->getRelease(), 'sentry_environment' => $this->options->getEnvironment(), ]); if (!empty($query)) { $endpoint .= '&' . http_build_query($query, '', '&'); } return $endpoint; } /** * {@inheritdoc} */ public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId { $event = Event::createEvent(); $event->setMessage($message); $event->setLevel($level); return $this->captureEvent($event, $hint, $scope); } /** * {@inheritdoc} */ public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId { $className = \get_class($exception); if ($this->shouldIgnoreException($className)) { $this->logger->info( 'The exception will be discarded because it matches an entry in "ignore_exceptions".', ['className' => $className] ); return null; // short circuit to avoid unnecessary processing } $hint = $hint ?? new EventHint(); if ($hint->exception === null) { $hint->exception = $exception; } return $this->captureEvent(Event::createEvent(), $hint, $scope); } /** * {@inheritdoc} */ public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId { $event = $this->prepareEvent($event, $hint, $scope); if ($event === null) { return null; } try { /** @var Result $result */ $result = $this->transport->send($event); $event = $result->getEvent(); if ($event !== null) { return $event->getId(); } } catch (\Throwable $exception) { $this->logger->error( \sprintf('Failed to send the event to Sentry. Reason: "%s".', $exception->getMessage()), ['exception' => $exception, 'event' => $event] ); } return null; } /** * {@inheritdoc} */ public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId { $error = error_get_last(); if ($error === null || !isset($error['message'][0])) { return null; } $exception = new \ErrorException(@$error['message'], 0, @$error['type'], @$error['file'], @$error['line']); return $this->captureException($exception, $scope, $hint); } /** * {@inheritdoc} * * @psalm-template T of IntegrationInterface */ public function getIntegration(string $className): ?IntegrationInterface { /** @psalm-var T|null */ return $this->integrations[$className] ?? null; } /** * {@inheritdoc} */ public function flush(?int $timeout = null): Result { return $this->transport->close($timeout); } /** * {@inheritdoc} */ public function getStacktraceBuilder(): StacktraceBuilder { return $this->stacktraceBuilder; } /** * @internal */ public function getLogger(): LoggerInterface { return $this->logger; } /** * @internal */ public function getTransport(): TransportInterface { return $this->transport; } public function getSdkIdentifier(): string { return $this->sdkIdentifier; } public function getSdkVersion(): string { return $this->sdkVersion; } /** * Assembles an event and prepares it to be sent of to Sentry. * * @param Event $event The payload that will be converted to an Event * @param EventHint|null $hint May contain additional information about the event * @param Scope|null $scope Optional scope which enriches the Event * * @return Event|null The prepared event object or null if it must be discarded */ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?Event { if ($hint !== null) { if ($hint->exception !== null && empty($event->getExceptions())) { $this->addThrowableToEvent($event, $hint->exception, $hint); } if ($hint->stacktrace !== null && $event->getStacktrace() === null) { $event->setStacktrace($hint->stacktrace); } } $this->addMissingStacktraceToEvent($event); $event->setSdkIdentifier($this->sdkIdentifier); $event->setSdkVersion($this->sdkVersion); $event->setTags(array_merge($this->options->getTags(), $event->getTags())); if ($event->getServerName() === null) { $event->setServerName($this->options->getServerName()); } if ($event->getRelease() === null) { $event->setRelease($this->options->getRelease()); } if ($event->getEnvironment() === null) { $event->setEnvironment($this->options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT); } $eventDescription = \sprintf( '%s%s [%s]', $event->getLevel() !== null ? $event->getLevel() . ' ' : '', (string) $event->getType(), (string) $event->getId() ); $isEvent = EventType::event() === $event->getType(); $sampleRate = $this->options->getSampleRate(); // only sample with the `sample_rate` on errors/messages if ($isEvent && $sampleRate < 1 && mt_rand(1, 100) / 100.0 > $sampleRate) { $this->logger->info(\sprintf('The %s will be discarded because it has been sampled.', $eventDescription), ['event' => $event]); return null; } $event = $this->applyIgnoreOptions($event, $eventDescription); if ($event === null) { return null; } if ($scope !== null) { $beforeEventProcessors = $event; $event = $scope->applyToEvent($event, $hint, $this->options); if ($event === null) { $this->logger->info( \sprintf('The %s will be discarded because one of the event processors returned "null".', $eventDescription), ['event' => $beforeEventProcessors] ); return null; } } $beforeSendCallback = $event; $event = $this->applyBeforeSendCallback($event, $hint); if ($event === null) { $this->logger->info( \sprintf( 'The %s will be discarded because the "%s" callback returned "null".', $eventDescription, $this->getBeforeSendCallbackName($beforeSendCallback) ), ['event' => $beforeSendCallback] ); } return $event; } /** * Checks if an exception should be ignored based on configured patterns. * Supports both class hierarchy matching and regex patterns. * Patterns starting and ending with '/' are treated as regex patterns. */ private function shouldIgnoreException(string $className): bool { foreach ($this->options->getIgnoreExceptions() as $pattern) { // Check for regex pattern (starts with / and ends with / optionally followed by flags) if (preg_match(self::REGEX_PATTERN_DETECTION, $pattern)) { try { if (preg_match($pattern, $className)) { return true; } } catch (\Throwable $e) { // Invalid regex pattern, log and skip $this->logger->warning( \sprintf('Invalid regex pattern in ignore_exceptions: "%s". Error: %s', $pattern, $e->getMessage()) ); continue; } } else { // Class hierarchy check if (is_a($className, $pattern, true)) { return true; } } } return false; } /** * Checks if a transaction should be ignored based on configured patterns. * Supports both exact string matching and regex patterns. * Patterns starting and ending with '/' are treated as regex patterns. */ private function shouldIgnoreTransaction(string $transactionName): bool { foreach ($this->options->getIgnoreTransactions() as $pattern) { // Check for regex pattern (starts with / and ends with / optionally followed by flags) if (preg_match(self::REGEX_PATTERN_DETECTION, $pattern)) { try { if (preg_match($pattern, $transactionName)) { return true; } } catch (\Throwable $e) { // Invalid regex pattern, log and skip $this->logger->warning( \sprintf('Invalid regex pattern in ignore_transactions: "%s". Error: %s', $pattern, $e->getMessage()) ); continue; } } else { // Exact string match if ($transactionName === $pattern) { return true; } } } return false; } private function applyIgnoreOptions(Event $event, string $eventDescription): ?Event { if ($event->getType() === EventType::event()) { $exceptions = $event->getExceptions(); if (empty($exceptions)) { return $event; } foreach ($exceptions as $exception) { if ($this->shouldIgnoreException($exception->getType())) { $this->logger->info( \sprintf('The %s will be discarded because it matches an entry in "ignore_exceptions".', $eventDescription), ['event' => $event] ); return null; } } } if ($event->getType() === EventType::transaction()) { $transactionName = $event->getTransaction(); if ($transactionName === null) { return $event; } if ($this->shouldIgnoreTransaction($transactionName)) { $this->logger->info( \sprintf('The %s will be discarded because it matches a entry in "ignore_transactions".', $eventDescription), ['event' => $event] ); return null; } } return $event; } private function applyBeforeSendCallback(Event $event, ?EventHint $hint): ?Event { switch ($event->getType()) { case EventType::event(): return ($this->options->getBeforeSendCallback())($event, $hint); case EventType::transaction(): return ($this->options->getBeforeSendTransactionCallback())($event, $hint); case EventType::checkIn(): return ($this->options->getBeforeSendCheckInCallback())($event, $hint); default: return $event; } } private function getBeforeSendCallbackName(Event $event): string { switch ($event->getType()) { case EventType::transaction(): return 'before_send_transaction'; case EventType::checkIn(): return 'before_send_check_in'; default: return 'before_send'; } } /** * Optionally adds a missing stacktrace to the Event if the client is configured to do so. * * @param Event $event The Event to add the missing stacktrace to */ private function addMissingStacktraceToEvent(Event $event): void { if (!$this->options->shouldAttachStacktrace()) { return; } // We should not add a stacktrace when the event already has one or contains exceptions if ($event->getStacktrace() !== null || !empty($event->getExceptions())) { return; } $event->setStacktrace($this->stacktraceBuilder->buildFromBacktrace( debug_backtrace(0), __FILE__, __LINE__ - 3 )); } /** * Stores the given exception in the passed event. * * @param Event $event The event that will be enriched with the exception * @param \Throwable $exception The exception that will be processed and added to the event * @param EventHint $hint Contains additional information about the event */ private function addThrowableToEvent(Event $event, \Throwable $exception, EventHint $hint): void { if ($exception instanceof \ErrorException && $event->getLevel() === null) { $event->setLevel(Severity::fromError($exception->getSeverity())); } $exceptions = []; do { $exceptions[] = new ExceptionDataBag( $exception, $this->stacktraceBuilder->buildFromException($exception), $hint->mechanism ?? new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, true, ['code' => $exception->getCode()]) ); } while ($exception = $exception->getPrevious()); $event->setExceptions($exceptions); } } PKî%W]²®ß¸¸"sentry/src/Tracing/SpanContext.phpnu„[µü¤ A List of tags associated to this Span */ private $tags = []; /** * @var array An arbitrary mapping of additional metadata */ private $data = []; /** * @var float|null Timestamp in seconds (epoch time) indicating when the span started */ private $startTimestamp; /** * @var float|null Timestamp in seconds (epoch time) indicating when the span ended */ private $endTimestamp; /** * @var string|null the trace origin of the span */ private $origin; /** * @return self */ public static function make() { return new self(); } public function getDescription(): ?string { return $this->description; } /** * @return $this */ public function setDescription(?string $description) { $this->description = $description; return $this; } public function getOp(): ?string { return $this->op; } /** * @return $this */ public function setOp(?string $op) { $this->op = $op; return $this; } public function getStatus(): ?SpanStatus { return $this->status; } /** * @return $this */ public function setStatus(?SpanStatus $status) { $this->status = $status; return $this; } public function getParentSpanId(): ?SpanId { return $this->parentSpanId; } /** * @return $this */ public function setParentSpanId(?SpanId $parentSpanId) { $this->parentSpanId = $parentSpanId; return $this; } public function getSampled(): ?bool { return $this->sampled; } /** * @return $this */ public function setSampled(?bool $sampled) { $this->sampled = $sampled; return $this; } public function getSpanId(): ?SpanId { return $this->spanId; } /** * @return $this */ public function setSpanId(?SpanId $spanId) { $this->spanId = $spanId; return $this; } public function getTraceId(): ?TraceId { return $this->traceId; } /** * @return $this */ public function setTraceId(?TraceId $traceId) { $this->traceId = $traceId; return $this; } /** * @return array */ public function getTags(): array { return $this->tags; } /** * @param array $tags * * @return $this */ public function setTags(array $tags) { $this->tags = $tags; return $this; } /** * @return array */ public function getData(): array { return $this->data; } /** * @param array $data * * @return $this */ public function setData(array $data) { $this->data = $data; return $this; } public function getStartTimestamp(): ?float { return $this->startTimestamp; } /** * @return $this */ public function setStartTimestamp(?float $startTimestamp) { $this->startTimestamp = $startTimestamp; return $this; } public function getEndTimestamp(): ?float { return $this->endTimestamp; } /** * @return $this */ public function setEndTimestamp(?float $endTimestamp) { $this->endTimestamp = $endTimestamp; return $this; } public function getOrigin(): ?string { return $this->origin; } /** * @return $this */ public function setOrigin(?string $origin) { $this->origin = $origin; return $this; } } PKî%W]>�5+žž.sentry/src/Tracing/GuzzleTracingMiddleware.phpnu„[µü¤getClient(); $parentSpan = $hub->getSpan(); $partialUri = Uri::fromParts([ 'scheme' => $request->getUri()->getScheme(), 'host' => $request->getUri()->getHost(), 'port' => $request->getUri()->getPort(), 'path' => $request->getUri()->getPath(), ]); $spanAndBreadcrumbData = [ 'http.request.method' => $request->getMethod(), 'http.request.body.size' => $request->getBody()->getSize(), ]; if ($request->getUri()->getQuery() !== '') { $spanAndBreadcrumbData['http.query'] = $request->getUri()->getQuery(); } if ($request->getUri()->getFragment() !== '') { $spanAndBreadcrumbData['http.fragment'] = $request->getUri()->getFragment(); } $childSpan = null; if ($parentSpan !== null && $parentSpan->getSampled()) { $spanContext = new SpanContext(); $spanContext->setOp('http.client'); $spanContext->setData($spanAndBreadcrumbData); $spanContext->setOrigin('auto.http.guzzle'); $spanContext->setDescription($request->getMethod() . ' ' . $partialUri); $childSpan = $parentSpan->startChild($spanContext); $hub->setSpan($childSpan); } if (self::shouldAttachTracingHeaders($client, $request)) { $request = $request ->withHeader('sentry-trace', getTraceparent()) ->withHeader('baggage', getBaggage()); } $handlerPromiseCallback = static function ($responseOrException) use ($hub, $spanAndBreadcrumbData, $childSpan, $parentSpan, $partialUri) { if ($childSpan !== null) { // We finish the span (which means setting the span end timestamp) first to ensure the measured time // the span spans is as close to only the HTTP request time and do the data collection afterwards $childSpan->finish(); $hub->setSpan($parentSpan); } $response = null; /** @psalm-suppress UndefinedClass */ if ($responseOrException instanceof ResponseInterface) { $response = $responseOrException; } elseif ($responseOrException instanceof GuzzleRequestException) { $response = $responseOrException->getResponse(); } $breadcrumbLevel = Breadcrumb::LEVEL_INFO; if ($response !== null) { $spanAndBreadcrumbData['http.response.body.size'] = $response->getBody()->getSize(); $spanAndBreadcrumbData['http.response.status_code'] = $response->getStatusCode(); if ($response->getStatusCode() >= 400 && $response->getStatusCode() < 500) { $breadcrumbLevel = Breadcrumb::LEVEL_WARNING; } elseif ($response->getStatusCode() >= 500) { $breadcrumbLevel = Breadcrumb::LEVEL_ERROR; } } if ($childSpan !== null) { if ($response !== null) { $childSpan->setStatus(SpanStatus::createFromHttpStatusCode($response->getStatusCode())); $childSpan->setData($spanAndBreadcrumbData); } else { $childSpan->setStatus(SpanStatus::internalError()); } } $hub->addBreadcrumb(new Breadcrumb( $breadcrumbLevel, Breadcrumb::TYPE_HTTP, 'http', null, array_merge([ 'url' => (string) $partialUri, ], $spanAndBreadcrumbData) )); if ($responseOrException instanceof \Throwable) { throw $responseOrException; } return $responseOrException; }; return $handler($request, $options)->then($handlerPromiseCallback, $handlerPromiseCallback); }; }; } private static function shouldAttachTracingHeaders(?ClientInterface $client, RequestInterface $request): bool { 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î%W]®1ÌëJJ(sentry/src/Tracing/TransactionSource.phpnu„[µü¤ A list of cached enum instances */ private static $instances = []; private function __construct(string $value) { $this->value = $value; } /** * User-defined name. */ public static function custom(): self { return self::getInstance('custom'); } /** * Raw URL, potentially containing identifiers. */ public static function url(): self { return self::getInstance('url'); } /** * Parametrized URL / route. */ public static function route(): self { return self::getInstance('route'); } /** * Name of the view handling the request. */ public static function view(): self { return self::getInstance('view'); } /** * Named after a software component, such as a function or class name. */ public static function component(): self { return self::getInstance('component'); } /** * Name of a background task (e.g. a Celery task). */ public static function task(): self { return self::getInstance('task'); } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W];˜4¾’ ’ *sentry/src/Tracing/TransactionMetadata.phpnu„[µü¤samplingRate = $samplingRate; $this->dynamicSamplingContext = $dynamicSamplingContext; $this->source = $source ?? TransactionSource::custom(); $this->parentSamplingRate = $parentSamplingRate; $this->sampleRand = $sampleRand ?? round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax(), 6); } /** * @return float|int|null */ public function getSamplingRate() { return $this->samplingRate; } /** * @param float|int|null $samplingRate */ public function setSamplingRate($samplingRate): self { $this->samplingRate = $samplingRate; return $this; } public function getParentSamplingRate(): ?float { return $this->parentSamplingRate; } public function setParentSamplingRate(?float $parentSamplingRate): self { $this->parentSamplingRate = $parentSamplingRate; return $this; } public function getSampleRand(): ?float { return $this->sampleRand; } public function setSampleRand(?float $sampleRand): self { $this->sampleRand = $sampleRand; return $this; } public function getDynamicSamplingContext(): ?DynamicSamplingContext { return $this->dynamicSamplingContext; } public function setDynamicSamplingContext(?DynamicSamplingContext $dynamicSamplingContext): self { $this->dynamicSamplingContext = $dynamicSamplingContext; return $this; } public function getSource(): ?TransactionSource { return $this->source; } public function setSource(?TransactionSource $source): self { $this->source = $source; return $this; } } PKî%W]2üæqKKsentry/src/Tracing/TraceId.phpnu„[µü¤value = $value; } /** * Generates a new trace ID. */ public static function generate(): self { return new self(SentryUid::generate()); } public function __toString(): string { return $this->value; } } PKî%W]sè~ƒ==sentry/src/Tracing/SpanId.phpnu„[µü¤value = $value; } /** * Generates a new span ID. */ public static function generate(): self { return new self(substr(SentryUid::generate(), 0, 16)); } /** * Compares whether two objects are equals. * * @param SpanId $other The object to compare */ public function isEqualTo(self $other): bool { return $this->value === $other->value; } public function __toString(): string { return $this->value; } } PKî%W]4'eMš6š6sentry/src/Tracing/Span.phpnu„[µü¤, * } */ class Span { /** * @var SpanId Span ID */ protected $spanId; /** * @var TraceId Trace ID */ protected $traceId; /** * @var string|null Description of the span */ protected $description; /** * @var string|null Operation of the span */ protected $op; /** * @var SpanStatus|null Completion status of the span */ protected $status; /** * @var SpanId|null ID of the parent span */ protected $parentSpanId; /** * @var bool|null Has the sample decision been made? */ protected $sampled; /** * @var array A List of tags associated to this span */ protected $tags = []; /** * @var array An arbitrary mapping of additional metadata */ protected $data = []; /** * @var float Timestamp in seconds (epoch time) indicating when the span started */ protected $startTimestamp; /** * @var float|null Timestamp in seconds (epoch time) indicating when the span ended */ protected $endTimestamp; /** * @var SpanRecorder|null Reference instance to the {@see SpanRecorder} */ protected $spanRecorder; /** * @var Transaction|null The transaction containing this span */ protected $transaction; /** * @var string|null The trace origin of the span. If no origin is set, the span is considered to be "manual". * * @see https://develop.sentry.dev/sdk/performance/trace-origin/ */ protected $origin; /** * Constructor. * * @param SpanContext|null $context The context to create the span with * * @internal */ public function __construct(?SpanContext $context = null) { if ($context === null) { $this->traceId = TraceId::generate(); $this->spanId = SpanId::generate(); $this->startTimestamp = microtime(true); return; } $this->traceId = $context->getTraceId() ?? TraceId::generate(); $this->spanId = $context->getSpanId() ?? SpanId::generate(); $this->startTimestamp = $context->getStartTimestamp() ?? microtime(true); $this->parentSpanId = $context->getParentSpanId(); $this->description = $context->getDescription(); $this->op = $context->getOp(); $this->status = $context->getStatus(); $this->sampled = $context->getSampled(); $this->tags = $context->getTags(); $this->data = $context->getData(); $this->endTimestamp = $context->getEndTimestamp(); $this->origin = $context->getOrigin(); } /** * Sets the ID of the span. * * @param SpanId $spanId The ID */ public function setSpanId(SpanId $spanId): self { $this->spanId = $spanId; return $this; } /** * Gets the ID that determines which trace the span belongs to. */ public function getTraceId(): TraceId { return $this->traceId; } /** * Sets the ID that determines which trace the span belongs to. * * @param TraceId $traceId The ID * * @return $this */ public function setTraceId(TraceId $traceId) { $this->traceId = $traceId; return $this; } /** * Gets the ID that determines which span is the parent of the current one. */ public function getParentSpanId(): ?SpanId { return $this->parentSpanId; } /** * Sets the ID that determines which span is the parent of the current one. * * @param SpanId|null $parentSpanId The ID * * @return $this */ public function setParentSpanId(?SpanId $parentSpanId) { $this->parentSpanId = $parentSpanId; return $this; } /** * Gets the timestamp representing when the measuring started. */ public function getStartTimestamp(): float { return $this->startTimestamp; } /** * Sets the timestamp representing when the measuring started. * * @param float $startTimestamp The timestamp * * @return $this */ public function setStartTimestamp(float $startTimestamp) { $this->startTimestamp = $startTimestamp; return $this; } /** * Gets the timestamp representing when the measuring finished. */ public function getEndTimestamp(): ?float { return $this->endTimestamp; } /** * Gets a description of the span's operation, which uniquely identifies * the span but is consistent across instances of the span. */ public function getDescription(): ?string { return $this->description; } /** * Sets a description of the span's operation, which uniquely identifies * the span but is consistent across instances of the span. * * @param string|null $description The description * * @return $this */ public function setDescription(?string $description) { $this->description = $description; return $this; } /** * Gets a short code identifying the type of operation the span is measuring. */ public function getOp(): ?string { return $this->op; } /** * Sets a short code identifying the type of operation the span is measuring. * * @param string|null $op The short code * * @return $this */ public function setOp(?string $op) { $this->op = $op; return $this; } /** * Gets the status of the span/transaction. */ public function getStatus(): ?SpanStatus { return $this->status; } /** * Sets the status of the span/transaction. * * @param SpanStatus|null $status The status * * @return $this */ public function setStatus(?SpanStatus $status) { $this->status = $status; return $this; } /** * Sets the HTTP status code and the status of the span/transaction. * * @param int $statusCode The HTTP status code * * @return $this */ public function setHttpStatus(int $statusCode) { SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($statusCode) { $scope->setContext('response', [ 'status_code' => $statusCode, ]); }); $status = SpanStatus::createFromHttpStatusCode($statusCode); if ($status !== SpanStatus::unknownError()) { $this->status = $status; } return $this; } /** * Gets a map of tags for this event. * * @return array */ public function getTags(): array { return $this->tags; } /** * Sets a map of tags for this event. This method will merge the given tags with * the existing ones. * * @param array $tags The tags * * @return $this */ public function setTags(array $tags) { $this->tags = array_merge($this->tags, $tags); return $this; } /** * Gets the ID of the span. */ public function getSpanId(): SpanId { return $this->spanId; } /** * Gets the flag determining whether this span should be sampled or not. */ public function getSampled(): ?bool { return $this->sampled; } /** * Sets the flag determining whether this span should be sampled or not. * * @param bool $sampled Whether to sample or not this span * * @return $this */ public function setSampled(?bool $sampled) { $this->sampled = $sampled; return $this; } /** * Gets a map of arbitrary data or a specific key from the map of data attached to this span. * * @param string|null $key Select a specific key from the data to return the value of * @param mixed $default When the $key is not found, return this value * * @return ($key is null ? array : mixed|null) */ public function getData(?string $key = null, $default = null) { if ($key === null) { return $this->data; } return $this->data[$key] ?? $default; } /** * Sets a map of arbitrary data. This method will merge the given data with the existing one. * * @param array $data The data * * @return $this */ public function setData(array $data) { $this->data = array_merge($this->data, $data); return $this; } /** * Gets the data in a format suitable for storage in the "trace" context. * * @return array * * @psalm-return array{ * data?: array, * description?: string, * op?: string, * parent_span_id?: string, * span_id: string, * status?: string, * tags?: array, * trace_id: string, * origin: string, * } */ public function getTraceContext(): array { $result = [ 'span_id' => (string) $this->spanId, 'trace_id' => (string) $this->traceId, 'origin' => $this->origin ?? 'manual', ]; if ($this->parentSpanId !== null) { $result['parent_span_id'] = (string) $this->parentSpanId; } if ($this->description !== null) { $result['description'] = $this->description; } if ($this->op !== null) { $result['op'] = $this->op; } if ($this->status !== null) { $result['status'] = (string) $this->status; } if (!empty($this->data)) { $result['data'] = $this->data; } if (!empty($this->tags)) { $result['tags'] = $this->tags; } return $result; } /** * Sets the finish timestamp on the current span. * * @param float|null $endTimestamp Takes an endTimestamp if the end should not be the time when you call this function * * @return EventId|null Finish for a span always returns null */ public function finish(?float $endTimestamp = null): ?EventId { $this->endTimestamp = $endTimestamp ?? microtime(true); return null; } /** * Creates a new {@see Span} while setting the current ID as `parentSpanId`. * Also the `sampled` decision will be inherited. * * @param SpanContext $context The context of the child span */ public function startChild(SpanContext $context): self { $context = clone $context; $context->setSampled($this->sampled); $context->setParentSpanId($this->spanId); $context->setTraceId($this->traceId); $span = new self($context); $span->transaction = $this->transaction; $span->spanRecorder = $this->spanRecorder; if ($span->spanRecorder !== null) { $span->spanRecorder->add($span); } return $span; } /** * Gets the span recorder attached to this span. * * @internal */ public function getSpanRecorder(): ?SpanRecorder { return $this->spanRecorder; } /** * Detaches the span recorder from this instance. * * @return $this */ public function detachSpanRecorder() { $this->spanRecorder = null; return $this; } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function getMetricsSummary(): array { return []; } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function setMetricsSummary( string $type, string $key, $value, MetricsUnit $unit, array $tags ): void { } /** * Sets the trace origin for this span. */ public function getOrigin(): ?string { return $this->origin; } /** * Sets the trace origin of the span. * * @return $this */ public function setOrigin(?string $origin) { $this->origin = $origin; return $this; } /** * Returns the transaction containing this span. */ public function getTransaction(): ?Transaction { return $this->transaction; } /** * Returns a string that can be used for the `sentry-trace` header & meta tag. */ public function toTraceparent(): string { $sampled = ''; if ($this->sampled !== null) { $sampled = $this->sampled ? '-1' : '-0'; } return \sprintf('%s-%s%s', (string) $this->traceId, (string) $this->spanId, $sampled); } /** * Returns a string that can be used for the W3C `traceparent` header & meta tag. * * @deprecated since version 4.12. To be removed in version 5.0. */ public function toW3CTraceparent(): string { return ''; } /** * Returns a string that can be used for the `baggage` header & meta tag. */ public function toBaggage(): string { $transaction = $this->getTransaction(); if ($transaction !== null) { return (string) $transaction->getDynamicSamplingContext(); } return ''; } } PKî%W]ï�‹>°°4sentry/src/Tracing/Traits/TraceHeaderParserTrait.phpnu„[µü¤[0-9a-f]{32})?-?(?[0-9a-f]{16})?-?(?[01])?[ \\t]*$/i'; /** * Parses the sentry-trace and baggage headers and returns the extracted data. * * @param string $sentryTrace The sentry-trace header value * @param string $baggage The baggage header value * * @return array{ * traceId: TraceId|null, * parentSpanId: SpanId|null, * parentSampled: bool|null, * dynamicSamplingContext: DynamicSamplingContext|null, * sampleRand: float|null, * parentSamplingRate: float|null * } */ protected static function parseTraceAndBaggageHeaders(string $sentryTrace, string $baggage): array { $result = [ 'traceId' => null, 'parentSpanId' => null, 'parentSampled' => null, 'dynamicSamplingContext' => null, 'sampleRand' => null, 'parentSamplingRate' => null, ]; $hasSentryTrace = false; if (preg_match(self::$sentryTraceparentHeaderRegex, $sentryTrace, $matches)) { if (!empty($matches['trace_id'])) { $result['traceId'] = new TraceId($matches['trace_id']); $hasSentryTrace = true; } if (!empty($matches['span_id'])) { $result['parentSpanId'] = new SpanId($matches['span_id']); $hasSentryTrace = true; } if (isset($matches['sampled'])) { $result['parentSampled'] = $matches['sampled'] === '1'; $hasSentryTrace = true; } } $samplingContext = DynamicSamplingContext::fromHeader($baggage); if ($hasSentryTrace && !$samplingContext->hasEntries()) { // The request comes from an old SDK which does not support Dynamic Sampling. // Propagate the Dynamic Sampling Context as is, but frozen, even without sentry-* entries. $samplingContext->freeze(); $result['dynamicSamplingContext'] = $samplingContext; } if ($hasSentryTrace && $samplingContext->hasEntries()) { // The baggage header contains Dynamic Sampling Context data from an upstream SDK. // Propagate this Dynamic Sampling Context. $result['dynamicSamplingContext'] = $samplingContext; } // Store the propagated traces sample rate if ($samplingContext->has('sample_rate')) { $result['parentSamplingRate'] = (float) $samplingContext->get('sample_rate'); } // Store the propagated trace sample rand or generate a new one if ($samplingContext->has('sample_rand')) { $result['sampleRand'] = (float) $samplingContext->get('sample_rand'); } else { if ($samplingContext->has('sample_rate') && $result['parentSampled'] !== null) { if ($result['parentSampled'] === true) { // [0, rate) $result['sampleRand'] = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (float) $samplingContext->get('sample_rate'), 6); } else { // [rate, 1) $result['sampleRand'] = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() * (1 - (float) $samplingContext->get('sample_rate')) + (float) $samplingContext->get('sample_rate'), 6); } } elseif ($result['parentSampled'] !== null) { // [0, 1) $result['sampleRand'] = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax(), 6); } } return $result; } } PKî%W]~ê™ww#sentry/src/Tracing/SpanRecorder.phpnu„[µü¤maxSpans = $maxSpans; } /** * Adds a span to the list of recorded spans or detaches the recorder if the * maximum number of spans to store has been reached. */ public function add(Span $span): self { if (\count($this->spans) > $this->maxSpans) { $span->detachSpanRecorder(); } else { $this->spans[] = $span; } return $this; } /** * Gets all the spans managed by this recorder. * * @return Span[] */ public function getSpans(): array { return $this->spans; } } PKî%W]4æø„zz)sentry/src/Tracing/TransactionContext.phpnu„[µü¤'; /** * @var string Name of the transaction */ private $name; /** * @var bool|null The parent's sampling decision */ private $parentSampled; /** * @var TransactionMetadata The transaction metadata */ private $metadata; /** * Constructor. * * @param string $name The name of the transaction * @param bool|null $parentSampled The parent's sampling decision * @param TransactionMetadata|null $metadata The transaction metadata */ public function __construct( string $name = self::DEFAULT_NAME, ?bool $parentSampled = null, ?TransactionMetadata $metadata = null ) { $this->name = $name; $this->parentSampled = $parentSampled; $this->metadata = $metadata ?? new TransactionMetadata(); } /** * @return self */ public static function make() { return new self(); } /** * Gets the name of the transaction. */ public function getName(): string { return $this->name; } /** * Sets the name of the transaction. * * @param string $name The name */ public function setName(string $name): self { $this->name = $name; return $this; } /** * Gets the parent's sampling decision. */ public function getParentSampled(): ?bool { return $this->parentSampled; } /** * Sets the parent's sampling decision. * * @param bool|null $parentSampled The decision */ public function setParentSampled(?bool $parentSampled): self { $this->parentSampled = $parentSampled; return $this; } /** * Gets the transaction metadata. */ public function getMetadata(): TransactionMetadata { return $this->metadata; } /** * Sets the transaction metadata. * * @param TransactionMetadata $metadata The transaction metadata */ public function setMetadata(TransactionMetadata $metadata): self { $this->metadata = $metadata; return $this; } /** * Sets the transaction source. * * @param TransactionSource $transactionSource The transaction source */ public function setSource(TransactionSource $transactionSource): self { $this->metadata->setSource($transactionSource); return $this; } /** * Returns a context populated with the data of the given environment variables. * * @param string $sentryTrace The sentry-trace value from the environment * @param string $baggage The baggage header value from the environment */ public static function fromEnvironment(string $sentryTrace, string $baggage): self { return self::parseTraceAndBaggage($sentryTrace, $baggage); } /** * Returns a context populated with the data of the given headers. * * @param string $sentryTraceHeader The sentry-trace header from an incoming request * @param string $baggageHeader The baggage header from an incoming request */ public static function fromHeaders(string $sentryTraceHeader, string $baggageHeader): self { return self::parseTraceAndBaggage($sentryTraceHeader, $baggageHeader); } private static function parseTraceAndBaggage(string $sentryTrace, string $baggage): self { $context = new self(); $parsedData = self::parseTraceAndBaggageHeaders($sentryTrace, $baggage); if ($parsedData['traceId'] !== null) { $context->traceId = $parsedData['traceId']; } if ($parsedData['parentSpanId'] !== null) { $context->parentSpanId = $parsedData['parentSpanId']; } if ($parsedData['parentSampled'] !== null) { $context->parentSampled = $parsedData['parentSampled']; } if ($parsedData['dynamicSamplingContext'] !== null) { $context->getMetadata()->setDynamicSamplingContext($parsedData['dynamicSamplingContext']); } if ($parsedData['parentSamplingRate'] !== null) { $context->getMetadata()->setParentSamplingRate($parsedData['parentSamplingRate']); } if ($parsedData['sampleRand'] !== null) { $context->getMetadata()->setSampleRand($parsedData['sampleRand']); } return $context; } } PKî%W]îj’Oõõ&sentry/src/Tracing/SamplingContext.phpnu„[µü¤|null Additional context, depending on where the SDK runs */ private $additionalContext; /** * Returns an instance populated with the data of the transaction context. */ public static function getDefault(TransactionContext $transactionContext): self { $context = new self(); $context->transactionContext = $transactionContext; $context->parentSampled = $transactionContext->getParentSampled(); $context->sampleRand = $transactionContext->getMetadata()->getSampleRand(); return $context; } public function getTransactionContext(): ?TransactionContext { return $this->transactionContext; } /** * Gets the sampling decision from the parent transaction, if any. */ public function getParentSampled(): ?bool { return $this->parentSampled; } public function getSampleRand(): ?float { return $this->sampleRand; } /** * Sets the sampling decision from the parent transaction, if any. */ public function setParentSampled(?bool $parentSampled): self { $this->parentSampled = $parentSampled; return $this; } /** * Sets additional data that will be provided as a second argument to {@link \Sentry\startTransaction()}. * * @param array|null $additionalContext */ public function setAdditionalContext(?array $additionalContext): self { $this->additionalContext = $additionalContext; return $this; } /** * Gets the additional data that will be provided as a second argument to {@link \Sentry\startTransaction()}. * * @return array|null */ public function getAdditionalContext(): ?array { return $this->additionalContext; } } PKî%W]v†Yºº-sentry/src/Tracing/DynamicSamplingContext.phpnu„[µü¤ The dsc entries */ private $entries = []; /** * @var bool Indicates if the dsc is mutable or immutable */ private $isFrozen = false; /** * Construct a new dsc object. */ private function __construct() { } /** * Set a new key value pair on the dsc. * * @param string $key the list member key * @param string $value the list member value */ public function set(string $key, string $value, bool $forceOverwrite = false): self { if ($this->isFrozen && !$forceOverwrite) { return $this; } $this->entries[$key] = $value; return $this; } /** * Check if a key value pair is set on the dsc. * * @param string $key the list member key */ public function has(string $key): bool { return isset($this->entries[$key]); } /** * Get a value from the dsc. * * @param string $key the list member key * @param string|null $default the default value to return if no value exists */ public function get(string $key, ?string $default = null): ?string { return $this->entries[$key] ?? $default; } /** * Mark the dsc as frozen. */ public function freeze(): self { $this->isFrozen = true; return $this; } /** * Indicates that the dsc is frozen and cannot be mutated. */ public function isFrozen(): bool { return $this->isFrozen; } /** * Check if there are any entries set. */ public function hasEntries(): bool { return !empty($this->entries); } /** * Gets the dsc entries. * * @return array */ public function getEntries(): array { return $this->entries; } /** * Parse the baggage header. * * @param string $header the baggage header contents */ public static function fromHeader(string $header): self { $samplingContext = new self(); foreach (explode(',', $header) as $listMember) { if (empty(trim($listMember))) { continue; } $keyValueAndProperties = explode(';', $listMember, 2); $keyValue = trim($keyValueAndProperties[0]); if (!str_contains($keyValue, '=')) { continue; } [$key, $value] = explode('=', $keyValue, 2); if (mb_substr($key, 0, mb_strlen(self::SENTRY_ENTRY_PREFIX)) === self::SENTRY_ENTRY_PREFIX) { $samplingContext->set(rawurldecode(mb_substr($key, mb_strlen(self::SENTRY_ENTRY_PREFIX))), rawurldecode($value)); } } // Once we receive a baggage header with Sentry entries from an upstream SDK, // we freeze the contents so it cannot be mutated anymore by this SDK. // It should only be propagated to the next downstream SDK or the Sentry server itself. $samplingContext->isFrozen = $samplingContext->hasEntries(); return $samplingContext; } /** * Create a dsc object. * * @see https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage-header */ public static function fromTransaction(Transaction $transaction, HubInterface $hub): self { $samplingContext = new self(); $samplingContext->set('trace_id', (string) $transaction->getTraceId()); $sampleRate = $transaction->getMetaData()->getSamplingRate(); if ($sampleRate !== null) { $samplingContext->set('sample_rate', (string) $sampleRate); } // Only include the transaction name if it has good quality if ($transaction->getMetadata()->getSource() !== TransactionSource::url()) { $samplingContext->set('transaction', $transaction->getName()); } $client = $hub->getClient(); if ($client !== null) { $options = $client->getOptions(); if ($options->getDsn() !== null && $options->getDsn()->getPublicKey() !== null) { $samplingContext->set('public_key', $options->getDsn()->getPublicKey()); } if ($options->getDsn() !== null && $options->getDsn()->getOrgId() !== null) { $samplingContext->set('org_id', (string) $options->getDsn()->getOrgId()); } if ($options->getRelease() !== null) { $samplingContext->set('release', $options->getRelease()); } if ($options->getEnvironment() !== null) { $samplingContext->set('environment', $options->getEnvironment()); } } if ($transaction->getSampled() !== null) { $samplingContext->set('sampled', $transaction->getSampled() ? 'true' : 'false'); } if ($transaction->getMetadata()->getSampleRand() !== null) { $samplingContext->set('sample_rand', (string) $transaction->getMetadata()->getSampleRand()); } $samplingContext->freeze(); return $samplingContext; } public static function fromOptions(Options $options, Scope $scope): self { $samplingContext = new self(); $samplingContext->set('trace_id', (string) $scope->getPropagationContext()->getTraceId()); $samplingContext->set('sample_rand', (string) $scope->getPropagationContext()->getSampleRand()); if ($options->getTracesSampleRate() !== null) { $samplingContext->set('sample_rate', (string) $options->getTracesSampleRate()); } if ($options->getDsn() !== null && $options->getDsn()->getPublicKey() !== null) { $samplingContext->set('public_key', $options->getDsn()->getPublicKey()); } if ($options->getDsn() !== null && $options->getDsn()->getOrgId() !== null) { $samplingContext->set('org_id', (string) $options->getDsn()->getOrgId()); } if ($options->getRelease() !== null) { $samplingContext->set('release', $options->getRelease()); } if ($options->getEnvironment() !== null) { $samplingContext->set('environment', $options->getEnvironment()); } $samplingContext->freeze(); return $samplingContext; } /** * Serialize the dsc as a string. */ public function __toString(): string { $result = []; foreach ($this->entries as $key => $value) { $result[] = rawurlencode(self::SENTRY_ENTRY_PREFIX . $key) . '=' . rawurlencode($value); } return implode(',', $result); } } PKî%W]‹Æ^©œœ!sentry/src/Tracing/SpanStatus.phpnu„[µü¤ */ private static $instances = []; /** * Constructor. * * @param string $value The value of the enum instance */ private function __construct(string $value) { $this->value = $value; } /** * Gets an instance of this enum representing the fact that the server returned * 401 Unauthorized (actually does mean unauthenticated according to RFC 7235). */ public static function unauthenticated(): self { return self::getInstance('unauthenticated'); } /** * Gets an instance of this enum representing the fact that the server returned * 403 Forbidden. */ public static function permissionDenied(): self { return self::getInstance('permission_denied'); } /** * Gets an instance of this enum representing the fact that the server returned * 404 Not Found. */ public static function notFound(): self { return self::getInstance('not_found'); } /** * Gets an instance of this enum representing the fact that the server returned * 409 Already exists. */ public static function alreadyExists(): self { return self::getInstance('already_exists'); } /** * Gets an instance of this enum representing the fact that the operation * was rejected because the system is not in a state required for the * operation. */ public static function failedPrecondition(): self { return self::getInstance('failed_precondition'); } /** * Gets an instance of this enum representing the fact that the server returned * 429 Too Many Requests. */ public static function resourceExhausted(): self { return self::getInstance('resource_exhausted'); } /** * Gets an instance of this enum representing the fact that the server returned * 429 Too Many Requests. * * @deprecated since version 4.7. To be removed in version 5.0. Use SpanStatus::resourceExhausted() instead. */ public static function resourceExchausted(): self { return self::resourceExhausted(); } /** * Gets an instance of this enum representing the fact that the server returned * 501 Not Implemented. */ public static function unimplemented(): self { return self::getInstance('unimplemented'); } /** * Gets an instance of this enum representing the fact that the server returned * 503 Service Unavailable. */ public static function unavailable(): self { return self::getInstance('unavailable'); } /** * Gets an instance of this enum representing the fact that the deadline * expired before operation could complete. */ public static function deadlineExceeded(): self { return self::getInstance('deadline_exceeded'); } /** * Gets an instance of this enum representing the fact that the operation * completed successfully. */ public static function ok(): self { return self::getInstance('ok'); } /** * Gets an instance of this enum representing the fact that the server returned * 4xx as response status code. */ public static function invalidArgument(): self { return self::getInstance('invalid_argument'); } /** * Gets an instance of this enum representing the fact that the server returned * 5xx as response status code. */ public static function internalError(): self { return self::getInstance('internal_error'); } /** * Gets an instance of this enum representing the fact that the server returned * with any non-standard HTTP status code. */ public static function unknownError(): self { return self::getInstance('unknown_error'); } /** * Returns an instance of this enum according to the given HTTP status code. * * @param int $statusCode The HTTP status code */ public static function createFromHttpStatusCode(int $statusCode): self { switch (true) { case $statusCode === 401: return self::unauthenticated(); case $statusCode === 403: return self::permissionDenied(); case $statusCode === 404: return self::notFound(); case $statusCode === 409: return self::alreadyExists(); case $statusCode === 413: return self::failedPrecondition(); case $statusCode === 429: return self::resourceExhausted(); case $statusCode === 501: return self::unimplemented(); case $statusCode === 503: return self::unavailable(); case $statusCode === 504: return self::deadlineExceeded(); case $statusCode < 400: return self::ok(); case $statusCode < 500: return self::invalidArgument(); case $statusCode < 600: return self::internalError(); default: return self::unknownError(); } } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W]óƒÚ)"sentry/src/Tracing/Transaction.phpnu„[µü¤hub = $hub ?? SentrySdk::getCurrentHub(); $this->name = $context->getName(); $this->metadata = $context->getMetadata(); $this->transaction = $this; } /** * Gets the name of this transaction. */ public function getName(): string { return $this->name; } /** * Sets the name of this transaction. * * @param string $name The name * * @return $this */ public function setName(string $name): self { $this->name = $name; return $this; } /** * Gets the transaction metadata. */ public function getMetadata(): TransactionMetadata { return $this->metadata; } /** * Gets the transaction dynamic sampling context. */ public function getDynamicSamplingContext(): DynamicSamplingContext { if ($this->metadata->getDynamicSamplingContext() !== null) { return $this->metadata->getDynamicSamplingContext(); } $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, $this->hub); $this->getMetadata()->setDynamicSamplingContext($samplingContext); return $samplingContext; } /** * Attaches a {@see SpanRecorder} to the transaction itself. * * @param int $maxSpans The maximum number of spans that can be recorded */ public function initSpanRecorder(int $maxSpans = 1000): self { if ($this->spanRecorder === null) { $this->spanRecorder = new SpanRecorder($maxSpans); } $this->spanRecorder->add($this); return $this; } public function initProfiler(): Profiler { if ($this->profiler === null) { $client = $this->hub->getClient(); $options = $client !== null ? $client->getOptions() : null; $this->profiler = new Profiler($options); } return $this->profiler; } public function getProfiler(): ?Profiler { return $this->profiler; } public function detachProfiler(): self { $this->profiler = null; return $this; } /** * {@inheritdoc} */ public function finish(?float $endTimestamp = null): ?EventId { if ($this->profiler !== null) { $this->profiler->stop(); } if ($this->endTimestamp !== null) { // Transaction was already finished once and we don't want to re-flush it return null; } parent::finish($endTimestamp); if ($this->sampled !== true) { return null; } $finishedSpans = []; if ($this->spanRecorder !== null) { foreach ($this->spanRecorder->getSpans() as $span) { if ($span->getSpanId() !== $this->getSpanId() && $span->getEndTimestamp() !== null) { $finishedSpans[] = $span; } } } $event = Event::createTransaction(); $event->setSpans($finishedSpans); $event->setStartTimestamp($this->startTimestamp); $event->setTimestamp($this->endTimestamp); $event->setTags($this->tags); $event->setTransaction($this->name); $event->setContext('trace', $this->getTraceContext()); $event->setSdkMetadata('dynamic_sampling_context', $this->getDynamicSamplingContext()); $event->setSdkMetadata('transaction_metadata', $this->getMetadata()); if ($this->profiler !== null) { $profile = $this->profiler->getProfile(); if ($profile !== null) { $event->setSdkMetadata('profile', $profile); } } return $this->hub->captureEvent($event); } } PKî%W]áÁޱ±)sentry/src/Tracing/PropagationContext.phpnu„[µü¤traceId = TraceId::generate(); $context->spanId = SpanId::generate(); $context->parentSpanId = null; $context->parentSampled = null; $context->sampleRand = round(mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax(), 6); $context->dynamicSamplingContext = null; return $context; } public static function fromHeaders(string $sentryTraceHeader, string $baggageHeader): self { return self::parseTraceparentAndBaggage($sentryTraceHeader, $baggageHeader); } public static function fromEnvironment(string $sentryTrace, string $baggage): self { return self::parseTraceparentAndBaggage($sentryTrace, $baggage); } /** * Returns a string that can be used for the `sentry-trace` header & meta tag. */ public function toTraceparent(): string { return \sprintf('%s-%s', (string) $this->traceId, (string) $this->spanId); } /** * Returns a string that can be used for the W3C `traceparent` header & meta tag. * * @deprecated since version 4.12. To be removed in version 5.0. */ public function toW3CTraceparent(): string { return ''; } /** * Returns a string that can be used for the `baggage` header & meta tag. */ public function toBaggage(): string { if ($this->dynamicSamplingContext === null) { $hub = SentrySdk::getCurrentHub(); $client = $hub->getClient(); if ($client !== null) { $options = $client->getOptions(); if ($options !== null) { $hub->configureScope(function (Scope $scope) use ($options) { $this->dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $scope); }); } } } return (string) $this->dynamicSamplingContext; } /** * @return array */ public function getTraceContext(): array { $result = [ 'trace_id' => (string) $this->traceId, 'span_id' => (string) $this->spanId, ]; if ($this->parentSpanId !== null) { $result['parent_span_id'] = (string) $this->parentSpanId; } return $result; } public function getTraceId(): TraceId { return $this->traceId; } public function setTraceId(TraceId $traceId): void { $this->traceId = $traceId; } public function getParentSpanId(): ?SpanId { return $this->parentSpanId; } public function setParentSpanId(?SpanId $parentSpanId): void { $this->parentSpanId = $parentSpanId; } public function getSpanId(): SpanId { return $this->spanId; } public function setSpanId(SpanId $spanId): self { $this->spanId = $spanId; return $this; } public function getDynamicSamplingContext(): ?DynamicSamplingContext { return $this->dynamicSamplingContext; } public function setDynamicSamplingContext(DynamicSamplingContext $dynamicSamplingContext): self { $this->dynamicSamplingContext = $dynamicSamplingContext; return $this; } public function getSampleRand(): ?float { return $this->sampleRand; } public function setSampleRand(?float $sampleRand): self { $this->sampleRand = $sampleRand; return $this; } private static function parseTraceparentAndBaggage(string $traceparent, string $baggage): self { $context = self::fromDefaults(); $parsedData = self::parseTraceAndBaggageHeaders($traceparent, $baggage); if ($parsedData['traceId'] !== null) { $context->traceId = $parsedData['traceId']; } if ($parsedData['parentSpanId'] !== null) { $context->parentSpanId = $parsedData['parentSpanId']; } if ($parsedData['parentSampled'] !== null) { $context->parentSampled = $parsedData['parentSampled']; } if ($parsedData['dynamicSamplingContext'] !== null) { $context->dynamicSamplingContext = $parsedData['dynamicSamplingContext']; } if ($parsedData['sampleRand'] !== null) { $context->sampleRand = $parsedData['sampleRand']; } return $context; } } PKî%W]ò¨ºˆKK(sentry/src/Spotlight/SpotlightClient.phpnu„[µü¤getStringBody(); if ($requestData === null) { throw new \RuntimeException('The request data is empty.'); } $curlHandle = curl_init(); curl_setopt($curlHandle, \CURLOPT_URL, $url); curl_setopt($curlHandle, \CURLOPT_HTTPHEADER, [ 'Content-Type: application/x-sentry-envelope', ]); curl_setopt($curlHandle, \CURLOPT_TIMEOUT, 2.0); curl_setopt($curlHandle, \CURLOPT_CONNECTTIMEOUT, 1.0); curl_setopt($curlHandle, \CURLOPT_ENCODING, ''); curl_setopt($curlHandle, \CURLOPT_POST, true); curl_setopt($curlHandle, \CURLOPT_POSTFIELDS, $requestData); curl_setopt($curlHandle, \CURLOPT_RETURNTRANSFER, true); curl_setopt($curlHandle, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); $body = curl_exec($curlHandle); if ($body === false) { $errorCode = curl_errno($curlHandle); $error = curl_error($curlHandle); if (\PHP_MAJOR_VERSION < 8) { curl_close($curlHandle); } $message = 'cURL Error (' . $errorCode . ') ' . $error; return new Response(0, [], $message); } $statusCode = curl_getinfo($curlHandle, \CURLINFO_HTTP_CODE); if (\PHP_MAJOR_VERSION < 8) { curl_close($curlHandle); } return new Response($statusCode, [], ''); } } PKî%W]@> Y Y !sentry/src/ExceptionMechanism.phpnu„[µü¤ Arbitrary extra data that might help the user * understand the error thrown by this mechanism */ private $data; /** * Class constructor. * * @param string $type Unique identifier of this mechanism determining * rendering and processing of the mechanism data * @param bool $handled Flag indicating whether the exception has been * handled by the user (e.g. via try..catch) * @param array $data Arbitrary extra data that might help the user * understand the error thrown by this mechanism */ public function __construct(string $type, bool $handled, array $data = []) { $this->type = $type; $this->handled = $handled; $this->data = $data; } /** * Returns the unique identifier of this mechanism determining rendering and * processing of the mechanism data. */ public function getType(): string { return $this->type; } /** * Returns the flag indicating whether the exception has been handled by the * user (e.g. via try..catch). */ public function isHandled(): bool { return $this->handled; } /** * Returns arbitrary extra data that might help the user understand the error * thrown by this mechanism. * * @return array */ public function getData(): array { return $this->data; } /** * Sets the arbitrary extra data. * * @param array $data */ public function setData(array $data): self { $this->data = $data; return $this; } } PKî%W]%S÷ŽYY!sentry/src/Profiling/Profiler.phpnu„[µü¤logger = $options !== null ? $options->getLoggerOrNullLogger() : new NullLogger(); $this->profile = new Profile($options); $this->initProfiler(); } public function start(): void { if ($this->profiler !== null) { $this->profiler->start(); } } public function stop(): void { if ($this->profiler !== null) { $this->profiler->stop(); $this->profile->setExcimerLog($this->profiler->flush()); } } public function getProfile(): ?Profile { if ($this->profiler === null) { return null; } return $this->profile; } private function initProfiler(): void { if (!\extension_loaded('excimer')) { $this->logger->warning('The profiler was started but is not available because the "excimer" extension is not loaded.'); return; } $this->profiler = new \ExcimerProfiler(); $this->profile->setStartTimeStamp(microtime(true)); $this->profiler->setEventType(\EXCIMER_REAL); $this->profiler->setPeriod(self::SAMPLE_RATE); $this->profiler->setMaxDepth(self::MAX_STACK_DEPTH); } } PKî%W]áî:,:, sentry/src/Profiling/Profile.phpnu„[µü¤, * samples: array, * stacks: array>, * }, * } * @phpstan-type ExcimerLogStackEntryTrace array{ * file: string, * line: int, * class?: string, * function?: string, * closure_line?: int, * } * @phpstan-type ExcimerLogStackEntry array{ * trace: array, * timestamp: float * } * * @internal */ final class Profile { use PrefixStripper; /** * @var string The version of the profile format */ private const VERSION = '1'; /** * @var string The thread ID */ private const THREAD_ID = '0'; /** * @var int The minimum number of samples required for a profile */ private const MIN_SAMPLE_COUNT = 2; /** * @var int The maximum duration of a profile in seconds */ private const MAX_PROFILE_DURATION = 30; /** * @var float The start time of the profile as a Unix timestamp with microseconds */ private $startTimeStamp; /** * @var \ExcimerLog|array The data of the profile */ private $excimerLog; /** * @var EventId|null The event ID of the profile */ private $eventId; /** * @var Options|null */ private $options; /** * @var LoggerInterface */ private $logger; public function __construct(?Options $options = null) { $this->options = $options; $this->logger = $options !== null ? $options->getLoggerOrNullLogger() : new NullLogger(); } public function setStartTimeStamp(float $startTimeStamp): void { $this->startTimeStamp = $startTimeStamp; } /** * @param \ExcimerLog|array $excimerLog */ public function setExcimerLog($excimerLog): void { $this->excimerLog = $excimerLog; } public function setEventId(EventId $eventId): void { $this->eventId = $eventId; } /** * @return SentryProfile|null */ public function getFormattedData(Event $event): ?array { if (!$this->validateExcimerLog()) { $this->logger->warning('The profile does not contain enough samples, the profile will be discarded.'); return null; } $osContext = $event->getOsContext(); if (!$this->validateOsContext($osContext)) { $this->logger->warning('The OS context is not missing or invalid, the profile will be discarded.'); return null; } $runtimeContext = $event->getRuntimeContext(); if (!$this->validateRuntimeContext($runtimeContext)) { $this->logger->warning('The runtime context is not missing or invalid, the profile will be discarded.'); return null; } if (!$this->validateEvent($event)) { $this->logger->warning('The event is missing a transaction and/or trace ID, the profile will be discarded.'); return null; } $frames = []; $frameHashMap = []; $stacks = []; $stackHashMap = []; $registerStack = static function (array $stack) use (&$stacks, &$stackHashMap): int { $stackHash = md5(serialize($stack)); if (\array_key_exists($stackHash, $stackHashMap) === false) { $stackHashMap[$stackHash] = \count($stacks); $stacks[] = $stack; } return $stackHashMap[$stackHash]; }; $samples = []; $duration = 0; $loggedStacks = $this->prepareStacks(); foreach ($loggedStacks as $stack) { $stackFrames = []; foreach ($stack['trace'] as $frame) { $absolutePath = $frame['file']; $lineno = $frame['line']; $frameKey = "{$absolutePath}:{$lineno}"; $frameIndex = $frameHashMap[$frameKey] ?? null; if ($frameIndex === null) { $file = $this->stripPrefixFromFilePath($this->options, $absolutePath); $module = null; if (isset($frame['class'], $frame['function'])) { // Class::method $function = $frame['class'] . '::' . $frame['function']; $module = $frame['class']; } elseif (isset($frame['function'])) { // {closure} $function = $frame['function']; } else { // /index.php $function = $file; } $frameHashMap[$frameKey] = $frameIndex = \count($frames); $frames[] = [ 'filename' => $file, 'abs_path' => $absolutePath, 'module' => $module, 'function' => $function, 'lineno' => $lineno, ]; } $stackFrames[] = $frameIndex; } $stackId = $registerStack($stackFrames); $duration = $stack['timestamp']; $samples[] = [ 'stack_id' => $stackId, 'thread_id' => self::THREAD_ID, 'elapsed_since_start_ns' => (int) round($duration * 1e+9), ]; } if (!$this->validateMaxDuration((float) $duration)) { $this->logger->warning(\sprintf('The profile is %ss which is longer than the allowed %ss, the profile will be discarded.', (float) $duration, self::MAX_PROFILE_DURATION)); return null; } $startTime = \DateTime::createFromFormat('U.u', number_format($this->startTimeStamp, 4, '.', ''), new \DateTimeZone('UTC')); if ($startTime === false) { $this->logger->warning(\sprintf('The start time (%s) of the profile is not valid, the profile will be discarded.', $this->startTimeStamp)); return null; } return [ 'device' => [ 'architecture' => $osContext->getMachineType(), ], 'event_id' => $this->eventId ? (string) $this->eventId : SentryUid::generate(), 'os' => [ 'name' => $osContext->getName(), 'version' => $osContext->getVersion(), 'build_number' => $osContext->getBuild() ?? '', ], 'platform' => 'php', 'release' => $event->getRelease() ?? '', 'environment' => $event->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, 'runtime' => [ 'name' => $runtimeContext->getName(), 'sapi' => $runtimeContext->getSAPI(), 'version' => $runtimeContext->getVersion(), ], 'timestamp' => $startTime->format(\DATE_RFC3339_EXTENDED), 'transaction' => [ 'id' => (string) $event->getId(), 'name' => $event->getTransaction(), 'trace_id' => $event->getTraceId(), 'active_thread_id' => self::THREAD_ID, ], 'version' => self::VERSION, 'profile' => [ 'frames' => $frames, 'samples' => $samples, 'stacks' => $stacks, ], ]; } /** * This method is mainly used to be able to mock the ExcimerLog class in the tests. * * @return array */ private function prepareStacks(): array { $stacks = []; foreach ($this->excimerLog as $stack) { if ($stack instanceof \ExcimerLogEntry) { $stacks[] = [ 'trace' => $stack->getTrace(), 'timestamp' => $stack->getTimestamp(), ]; } else { /** @var ExcimerLogStackEntry $stack */ $stacks[] = $stack; } } return $stacks; } private function validateExcimerLog(): bool { if (\is_array($this->excimerLog)) { $sampleCount = \count($this->excimerLog); } else { $sampleCount = $this->excimerLog->count(); } return $sampleCount >= self::MIN_SAMPLE_COUNT; } private function validateMaxDuration(float $duration): bool { if ($duration > self::MAX_PROFILE_DURATION) { return false; } return true; } /** * @phpstan-assert-if-true OsContext $osContext * @phpstan-assert-if-true !null $osContext->getVersion() * @phpstan-assert-if-true !null $osContext->getMachineType() */ private function validateOsContext(?OsContext $osContext): bool { if ($osContext === null) { return false; } if ($osContext->getVersion() === null) { return false; } if ($osContext->getMachineType() === null) { return false; } return true; } /** * @phpstan-assert-if-true RuntimeContext $runtimeContext * @phpstan-assert-if-true !null $runtimeContext->getVersion() */ private function validateRuntimeContext(?RuntimeContext $runtimeContext): bool { if ($runtimeContext === null) { return false; } if ($runtimeContext->getVersion() === null) { return false; } return true; } /** * @phpstan-assert-if-true !null $event->getTransaction() * @phpstan-assert-if-true !null $event->getTraceId() */ private function validateEvent(Event $event): bool { if ($event->getTransaction() === null) { return false; } if ($event->getTraceId() === null) { return false; } return true; } } PKî%W]DfyíÒÒ"sentry/src/HttpClient/Response.phpnu„[µü¤statusCode = $statusCode; $this->headers = $headers; $this->error = $error; foreach ($headers as $name => $value) { $this->headerNames[strtolower($name)] = $name; } } public function getStatusCode(): int { return $this->statusCode; } public function isSuccess(): bool { return $this->statusCode >= 200 && $this->statusCode <= 299; } public function hasHeader(string $name): bool { return isset($this->headerNames[strtolower($name)]); } /** * @return string[] */ public function getHeader(string $header): array { if (!$this->hasHeader($header)) { return []; } $header = $this->headerNames[strtolower($header)]; return $this->headers[$header]; } public function getHeaderLine(string $name): string { $value = $this->getHeader($name); if (empty($value)) { return ''; } return implode(',', $value); } public function getError(): string { return $this->error; } public function hasError(): bool { return $this->error !== ''; } } PKî%W]ljïòÅÅ-sentry/src/HttpClient/HttpClientInterface.phpnu„[µü¤stringBody !== null; } public function getStringBody(): ?string { return $this->stringBody; } public function setStringBody(string $stringBody): void { $this->stringBody = $stringBody; } } PKî%W]ç›­ $sentry/src/HttpClient/HttpClient.phpnu„[µü¤sdkIdentifier = $sdkIdentifier; $this->sdkVersion = $sdkVersion; } public function sendRequest(Request $request, Options $options): Response { $dsn = $options->getDsn(); if ($dsn === null) { throw new \RuntimeException('The DSN option must be set to use the HttpClient.'); } $requestData = $request->getStringBody(); if ($requestData === null) { throw new \RuntimeException('The request data is empty.'); } if (!\extension_loaded('curl')) { throw new \RuntimeException('The cURL PHP extension must be enabled to use the HttpClient.'); } $curlHandle = curl_init(); $requestHeaders = Http::getRequestHeaders($dsn, $this->sdkIdentifier, $this->sdkVersion); if ( \extension_loaded('zlib') && $options->isHttpCompressionEnabled() ) { $requestData = gzcompress($requestData, -1, \ZLIB_ENCODING_GZIP); $requestHeaders[] = 'Content-Encoding: gzip'; } $responseHeaders = []; $responseHeaderCallback = function ($curlHandle, $headerLine) use (&$responseHeaders): int { return Http::parseResponseHeaders($headerLine, $responseHeaders); }; curl_setopt($curlHandle, \CURLOPT_URL, $dsn->getEnvelopeApiEndpointUrl()); curl_setopt($curlHandle, \CURLOPT_HTTPHEADER, $requestHeaders); curl_setopt($curlHandle, \CURLOPT_USERAGENT, $this->sdkIdentifier . '/' . $this->sdkVersion); curl_setopt($curlHandle, \CURLOPT_TIMEOUT_MS, $options->getHttpTimeout() * 1000); curl_setopt($curlHandle, \CURLOPT_CONNECTTIMEOUT_MS, $options->getHttpConnectTimeout() * 1000); curl_setopt($curlHandle, \CURLOPT_ENCODING, ''); curl_setopt($curlHandle, \CURLOPT_POST, true); curl_setopt($curlHandle, \CURLOPT_POSTFIELDS, $requestData); curl_setopt($curlHandle, \CURLOPT_RETURNTRANSFER, true); curl_setopt($curlHandle, \CURLOPT_HEADERFUNCTION, $responseHeaderCallback); curl_setopt($curlHandle, \CURLOPT_HTTP_VERSION, \CURL_HTTP_VERSION_1_1); $httpSslVerifyPeer = $options->getHttpSslVerifyPeer(); if (!$httpSslVerifyPeer) { curl_setopt($curlHandle, \CURLOPT_SSL_VERIFYPEER, false); } $httpSslNativeCa = $options->getHttpSslNativeCa(); if ($httpSslNativeCa) { if ( \defined('CURLSSLOPT_NATIVE_CA') && isset(curl_version()['version']) && version_compare(curl_version()['version'], '7.71', '>=') ) { curl_setopt($curlHandle, \CURLOPT_SSL_OPTIONS, \CURLSSLOPT_NATIVE_CA); } } $httpProxy = $options->getHttpProxy(); if ($httpProxy !== null) { curl_setopt($curlHandle, \CURLOPT_PROXY, $httpProxy); curl_setopt($curlHandle, \CURLOPT_HEADEROPT, \CURLHEADER_SEPARATE); } $httpProxyAuthentication = $options->getHttpProxyAuthentication(); if ($httpProxyAuthentication !== null) { curl_setopt($curlHandle, \CURLOPT_PROXYUSERPWD, $httpProxyAuthentication); } /** @var string|false $body */ $body = curl_exec($curlHandle); if ($body === false) { $errorCode = curl_errno($curlHandle); $error = curl_error($curlHandle); if (\PHP_MAJOR_VERSION < 8) { curl_close($curlHandle); } $message = 'cURL Error (' . $errorCode . ') ' . $error; return new Response(0, [], $message); } $statusCode = curl_getinfo($curlHandle, \CURLINFO_HTTP_CODE); if (\PHP_MAJOR_VERSION < 8) { curl_close($curlHandle); } $error = $statusCode >= 400 ? $body : ''; return new Response($statusCode, $responseHeaders, $error); } } PKî%W]EbÒ Ò sentry/src/ExceptionDataBag.phpnu„[µü¤ */ final class ExceptionDataBag { /** * @var string The type of exception, e.g. RuntimeException */ private $type; /** * @var string The value of the exception */ private $value; /** * @var Stacktrace|null An optional stack trace object corresponding to the Stack Trace Interface */ private $stacktrace; /** * @var ExceptionMechanism|null An optional object describing the mechanism that created this exception */ private $mechanism; public function __construct(\Throwable $exception, ?Stacktrace $stacktrace = null, ?ExceptionMechanism $mechanism = null) { $this->type = \get_class($exception); $this->value = $exception->getMessage(); $this->stacktrace = $stacktrace; $this->mechanism = $mechanism; } /** * Gets the type of exception, e.g. RuntimeException. */ public function getType(): string { return $this->type; } /** * Sets the type of the exception. * * @param string $type The exception type */ public function setType(string $type): self { $this->type = $type; return $this; } /** * Gets the value of the exception. */ public function getValue(): string { return $this->value; } /** * Sets the value of the exception. */ public function setValue(string $value): self { $this->value = $value; return $this; } /** * Gets the stack trace object corresponding to the Stack Trace Interface. */ public function getStacktrace(): ?Stacktrace { return $this->stacktrace; } /** * Sets the stack trace object corresponding to the Stack Trace Interface. * * @param Stacktrace $stacktrace The stacktrace */ public function setStacktrace(Stacktrace $stacktrace): self { $this->stacktrace = $stacktrace; return $this; } /** * Gets the object describing the mechanism that created this exception. */ public function getMechanism(): ?ExceptionMechanism { return $this->mechanism; } /** * Sets the object describing the mechanism that created this exception. * * @param ExceptionMechanism|null $mechanism The mechanism that created this exception */ public function setMechanism(?ExceptionMechanism $mechanism): self { $this->mechanism = $mechanism; return $this; } } PKî%W]S†Jœ‡‡sentry/src/ClientBuilder.phpnu„[µü¤options = $options ?? new Options(); } /** * @param array $options The client options, in naked array form */ public static function create(array $options = []): self { return new self(new Options($options)); } public function getOptions(): Options { return $this->options; } public function setRepresentationSerializer(RepresentationSerializerInterface $representationSerializer): self { $this->representationSerializer = $representationSerializer; return $this; } public function getLogger(): ?LoggerInterface { return $this->logger ?? $this->options->getLogger(); } public function setLogger(LoggerInterface $logger): self { $this->logger = $logger; return $this; } public function setSdkIdentifier(string $sdkIdentifier): self { $this->sdkIdentifier = $sdkIdentifier; return $this; } public function setSdkVersion(string $sdkVersion): self { $this->sdkVersion = $sdkVersion; return $this; } public function getTransport(): TransportInterface { return $this->transport ?? $this->options->getTransport() ?? new HttpTransport( $this->options, $this->getHttpClient(), new PayloadSerializer($this->options), $this->getLogger() ); } public function setTransport(TransportInterface $transport): self { $this->transport = $transport; return $this; } public function getHttpClient(): HttpClientInterface { return $this->httpClient ?? $this->options->getHttpClient() ?? new HttpClient($this->sdkIdentifier, $this->sdkVersion); } public function setHttpClient(HttpClientInterface $httpClient): self { $this->httpClient = $httpClient; return $this; } public function getClient(): ClientInterface { return new Client( $this->options, $this->getTransport(), $this->sdkIdentifier, $this->sdkVersion, $this->representationSerializer, $this->getLogger() ); } } PKî%W]P„ú‘I‘Isentry/src/ErrorHandler.phpnu„[µü¤\d+) bytes exhausted[^\r\n]* \(tried to allocate \d+ bytes\)/'; /** * The fatal error types that cannot be silenced using the @ operator in PHP 8+. */ private const PHP8_UNSILENCEABLE_FATAL_ERRORS = \E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_COMPILE_ERROR | \E_USER_ERROR | \E_RECOVERABLE_ERROR; /** * @var self|null The current registered handler (this class is a singleton) */ private static $handlerInstance; /** * @var callable[] List of listeners that will act on each captured error * * @psalm-var (callable(\ErrorException): void)[] */ private $errorListeners = []; /** * @var callable[] List of listeners that will act of each captured fatal error * * @psalm-var (callable(FatalErrorException): void)[] */ private $fatalErrorListeners = []; /** * @var callable[] List of listeners that will act on each captured exception * * @psalm-var (callable(\Throwable): void)[] */ private $exceptionListeners = []; /** * @var \ReflectionProperty A reflection cached instance that points to the * trace property of the exception objects */ private $exceptionReflection; /** * @var callable|null The previous error handler, if any */ private $previousErrorHandler; /** * @var callable|null The previous exception handler, if any * * @psalm-var null|callable(\Throwable): void */ private $previousExceptionHandler; /** * @var bool Whether the error handler has been registered */ private $isErrorHandlerRegistered = false; /** * @var bool Whether the exception handler has been registered */ private $isExceptionHandlerRegistered = false; /** * @var bool Whether the fatal error handler has been registered */ private $isFatalErrorHandlerRegistered = false; /** * @var int|null the amount of bytes of memory to increase the memory limit by when we are capturing a out of memory error, set to null to not increase the memory limit */ private $memoryLimitIncreaseOnOutOfMemoryErrorValue = 5 * 1024 * 1024; // 5 MiB /** * @var Options|null The SDK options */ private $options; /** * @var bool Whether the memory limit has been increased */ private static $didIncreaseMemoryLimit = false; /** * @var string|null A portion of pre-allocated memory data that will be reclaimed in case a fatal error occurs to handle it * * @phpstan-ignore-next-line This property is used to reserve memory for the fatal error handler and is thus never read */ private static $reservedMemory; /** * @var bool Whether the fatal error handler should be disabled */ private static $disableFatalErrorHandler = false; /** * @var string[] List of error levels and their description */ private const ERROR_LEVELS_DESCRIPTION = [ \E_DEPRECATED => 'Deprecated', \E_USER_DEPRECATED => 'User Deprecated', \E_NOTICE => 'Notice', \E_USER_NOTICE => 'User Notice', // This is \E_STRICT which has been deprecated in PHP 8.4 so we should not reference it directly to prevent deprecation notices 2048 => 'Runtime Notice', \E_WARNING => 'Warning', \E_USER_WARNING => 'User Warning', \E_COMPILE_WARNING => 'Compile Warning', \E_CORE_WARNING => 'Core Warning', \E_USER_ERROR => 'User Error', \E_RECOVERABLE_ERROR => 'Catchable Fatal Error', \E_COMPILE_ERROR => 'Compile Error', \E_PARSE => 'Parse Error', \E_ERROR => 'Error', \E_CORE_ERROR => 'Core Error', ]; /** * Constructor. * * @throws \ReflectionException If hooking into the \Exception class to * make the `trace` property accessible fails */ private function __construct() { $this->exceptionReflection = new \ReflectionProperty(\Exception::class, 'trace'); if (\PHP_VERSION_ID < 80100) { $this->exceptionReflection->setAccessible(true); } } /** * Registers the error handler once and returns its instance. */ public static function registerOnceErrorHandler(?Options $options = null): self { if (self::$handlerInstance === null) { self::$handlerInstance = new self(); } self::$handlerInstance->options = $options; if (self::$handlerInstance->isErrorHandlerRegistered) { return self::$handlerInstance; } $errorHandlerCallback = \Closure::fromCallable([self::$handlerInstance, 'handleError']); self::$handlerInstance->isErrorHandlerRegistered = true; self::$handlerInstance->previousErrorHandler = set_error_handler($errorHandlerCallback); if (self::$handlerInstance->previousErrorHandler === null) { restore_error_handler(); // Specifying the error types caught by the error handler with the // first call to the set_error_handler method would cause the PHP // bug https://bugs.php.net/63206 if the handler is not the first // one in the chain of handlers set_error_handler($errorHandlerCallback, \E_ALL); } return self::$handlerInstance; } /** * Registers the fatal error handler and reserves a certain amount of memory * that will be reclaimed to handle the errors (to prevent out of memory * issues while handling them) and returns its instance. * * @param int $reservedMemorySize The amount of memory to reserve for the fatal * error handler expressed in bytes */ public static function registerOnceFatalErrorHandler(int $reservedMemorySize = self::DEFAULT_RESERVED_MEMORY_SIZE): self { if ($reservedMemorySize <= 0) { throw new \InvalidArgumentException('The $reservedMemorySize argument must be greater than 0.'); } if (self::$handlerInstance === null) { self::$handlerInstance = new self(); } if (self::$handlerInstance->isFatalErrorHandlerRegistered) { return self::$handlerInstance; } self::$handlerInstance->isFatalErrorHandlerRegistered = true; self::$reservedMemory = str_repeat('x', $reservedMemorySize); register_shutdown_function(\Closure::fromCallable([self::$handlerInstance, 'handleFatalError'])); return self::$handlerInstance; } /** * Registers the exception handler, effectively replacing the current one * and returns its instance. The previous one will be saved anyway and * called when appropriate. */ public static function registerOnceExceptionHandler(): self { if (self::$handlerInstance === null) { self::$handlerInstance = new self(); } if (self::$handlerInstance->isExceptionHandlerRegistered) { return self::$handlerInstance; } self::$handlerInstance->isExceptionHandlerRegistered = true; self::$handlerInstance->previousExceptionHandler = set_exception_handler(\Closure::fromCallable([self::$handlerInstance, 'handleException'])); return self::$handlerInstance; } /** * Adds a listener to the current error handler that will be called every * time an error is captured. * * @param callable $listener A callable that will act as a listener * and that must accept a single argument * of type \ErrorException * * @psalm-param callable(\ErrorException): void $listener */ public function addErrorHandlerListener(callable $listener): void { $this->errorListeners[] = $listener; } /** * Adds a listener to the current error handler that will be called every * time a fatal error handler is captured. * * @param callable $listener A callable that will act as a listener * and that must accept a single argument * of type \Sentry\Exception\FatalErrorException * * @psalm-param callable(FatalErrorException): void $listener */ public function addFatalErrorHandlerListener(callable $listener): void { $this->fatalErrorListeners[] = $listener; } /** * Adds a listener to the current error handler that will be called every * time an exception is captured. * * @param callable $listener A callable that will act as a listener * and that must accept a single argument * of type \Throwable * * @psalm-param callable(\Throwable): void $listener */ public function addExceptionHandlerListener(callable $listener): void { $this->exceptionListeners[] = $listener; } /** * Sets the amount of memory to increase the memory limit by when we are capturing a out of memory error. * * @param int|null $valueInBytes the number of bytes to increase the memory limit by, or null to not increase the memory limit */ public function setMemoryLimitIncreaseOnOutOfMemoryErrorInBytes(?int $valueInBytes): void { if ($valueInBytes !== null && $valueInBytes <= 0) { throw new \InvalidArgumentException('The $valueInBytes argument must be greater than 0 or null.'); } $this->memoryLimitIncreaseOnOutOfMemoryErrorValue = $valueInBytes; } /** * Handles errors by capturing them through the client according to the * configured bit field. * * @param int $level The level of the error raised, represented by * one of the E_* constants * @param string $message The error message * @param string $file The filename the error was raised in * @param int $line The line number the error was raised at * @param array|null $errcontext The error context (deprecated since PHP 7.2) * * @return bool If the function returns `false` then the PHP native error * handler will be called * * @throws \Throwable */ private function handleError(int $level, string $message, string $file, int $line, ?array $errcontext = []): bool { $isSilencedError = error_reporting() === 0; if (\PHP_MAJOR_VERSION >= 8) { // Starting from PHP8, when a silenced error occurs the `error_reporting()` // function will return a bitmask of fatal errors that are unsilenceable. // If by subtracting from this value those errors the result is 0, we can // conclude that the error was silenced. $isSilencedError = 0 === (error_reporting() & ~self::PHP8_UNSILENCEABLE_FATAL_ERRORS); // However, starting from PHP8 some fatal errors are unsilenceable, // so we have to check for them to avoid reporting any of them as // silenced instead if ($level === (self::PHP8_UNSILENCEABLE_FATAL_ERRORS & $level)) { $isSilencedError = false; } } if ($this->shouldHandleError($level, $isSilencedError)) { if ($isSilencedError) { $errorAsException = new SilencedErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line); } else { $errorAsException = new \ErrorException(self::ERROR_LEVELS_DESCRIPTION[$level] . ': ' . $message, 0, $level, $file, $line); } $backtrace = $this->cleanBacktraceFromErrorHandlerFrames($errorAsException->getTrace(), $errorAsException->getFile(), $errorAsException->getLine()); $this->exceptionReflection->setValue($errorAsException, $backtrace); $this->invokeListeners($this->errorListeners, $errorAsException); } if ($this->previousErrorHandler !== null) { return false !== ($this->previousErrorHandler)($level, $message, $file, $line, $errcontext); } return false; } private function shouldHandleError(int $level, bool $silenced): bool { // If we were not given any options, we should handle all errors if ($this->options === null) { return true; } if ($silenced) { return $this->options->shouldCaptureSilencedErrors(); } return ($this->options->getErrorTypes() & $level) !== 0; } /** * Tries to handle a fatal error if any and relay them to the listeners. * It only tries to do this if we still have some reserved memory at * disposal. This method is used as callback of a shutdown function. */ private function handleFatalError(): void { if (self::$disableFatalErrorHandler) { return; } // Free the reserved memory that allows us to potentially handle OOM errors self::$reservedMemory = null; $error = error_get_last(); if (!empty($error) && $error['type'] & (\E_ERROR | \E_PARSE | \E_CORE_ERROR | \E_CORE_WARNING | \E_COMPILE_ERROR | \E_COMPILE_WARNING)) { // If we did not do so already and we are allowed to increase the memory limit, we do so when we detect an OOM error if (self::$didIncreaseMemoryLimit === false && $this->memoryLimitIncreaseOnOutOfMemoryErrorValue !== null && preg_match(self::OOM_MESSAGE_MATCHER, $error['message'], $matches) === 1 ) { $currentMemoryLimit = (int) $matches['memory_limit']; ini_set('memory_limit', (string) ($currentMemoryLimit + $this->memoryLimitIncreaseOnOutOfMemoryErrorValue)); self::$didIncreaseMemoryLimit = true; } $errorAsException = new FatalErrorException(self::ERROR_LEVELS_DESCRIPTION[$error['type']] . ': ' . $error['message'], 0, $error['type'], $error['file'], $error['line']); $this->exceptionReflection->setValue($errorAsException, []); $this->invokeListeners($this->fatalErrorListeners, $errorAsException); } } /** * Handles the given exception by passing it to all the listeners, * then forwarding it to another handler. * * @param \Throwable $exception The exception to handle * * @throws \Throwable */ private function handleException(\Throwable $exception): void { $this->invokeListeners($this->exceptionListeners, $exception); $previousExceptionHandlerException = $exception; // Unset the previous exception handler to prevent infinite loop in case // we need to handle an exception thrown from it $previousExceptionHandler = $this->previousExceptionHandler; $this->previousExceptionHandler = null; try { if ($previousExceptionHandler !== null) { $previousExceptionHandler($exception); return; } } catch (\Throwable $previousExceptionHandlerException) { // This `catch` statement is here to forcefully override the // $previousExceptionHandlerException variable with the exception // we just caught } // If the instance of the exception we're handling is the same as the one // caught from the previous exception handler then we give it back to the // native PHP handler to prevent an infinite loop if ($exception === $previousExceptionHandlerException) { // Disable the fatal error handler or the error will be reported twice self::$disableFatalErrorHandler = true; throw $exception; } $this->handleException($previousExceptionHandlerException); } /** * Cleans and returns the backtrace without the first frames that belong to * this error handler. * * @param array> $backtrace The backtrace to clear * @param string $file The filename the backtrace was raised in * @param int $line The line number the backtrace was raised at * * @psalm-param list $backtrace * * @return array */ private function cleanBacktraceFromErrorHandlerFrames(array $backtrace, string $file, int $line): array { $cleanedBacktrace = $backtrace; $index = 0; while ($index < \count($backtrace)) { if (isset($backtrace[$index]['file'], $backtrace[$index]['line']) && $backtrace[$index]['line'] === $line && $backtrace[$index]['file'] === $file) { $cleanedBacktrace = \array_slice($cleanedBacktrace, 1 + $index); break; } ++$index; } return $cleanedBacktrace; } /** * Invokes all the listeners and pass the exception to all of them. * * @param callable[] $listeners The array of listeners to be called * @param \Throwable $throwable The exception to be passed onto listeners */ private function invokeListeners(array $listeners, \Throwable $throwable): void { foreach ($listeners as $listener) { try { $listener($throwable); } catch (\Throwable $exception) { // Do nothing as this should be as transparent as possible } } } } PKî%W]gN�‹99sentry/src/CheckInStatus.phpnu„[µü¤ A list of cached enum instances */ private static $instances = []; private function __construct(string $value) { $this->value = $value; } public static function ok(): self { return self::getInstance('ok'); } public static function error(): self { return self::getInstance('error'); } public static function inProgress(): self { return self::getInstance('in_progress'); } public function __toString(): string { return $this->value; } private static function getInstance(string $value): self { if (!isset(self::$instances[$value])) { self::$instances[$value] = new self($value); } return self::$instances[$value]; } } PKî%W]ªj§—œœ7sentry/src/Integration/FrameContextifierIntegration.phpnu„[µü¤ */ final class FrameContextifierIntegration implements IntegrationInterface { /** * @var LoggerInterface A PSR-3 logger */ private $logger; /** * Creates a new instance of this integration. * * @param LoggerInterface|null $logger A PSR-3 logger */ public function __construct(?LoggerInterface $logger = null) { $this->logger = $logger ?? new NullLogger(); } /** * {@inheritdoc} */ public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { $client = SentrySdk::getCurrentHub()->getClient(); if ($client === null) { return $event; } $maxContextLines = $client->getOptions()->getContextLines(); $integration = $client->getIntegration(self::class); if ($integration === null || $maxContextLines === null) { return $event; } $stacktrace = $event->getStacktrace(); if ($stacktrace !== null) { $integration->addContextToStacktraceFrames($maxContextLines, $stacktrace); } foreach ($event->getExceptions() as $exception) { if ($exception->getStacktrace() !== null) { $integration->addContextToStacktraceFrames($maxContextLines, $exception->getStacktrace()); } } return $event; }); } /** * Contextifies the frames of the given stacktrace. * * @param int $maxContextLines The maximum number of lines of code to read * @param Stacktrace $stacktrace The stacktrace object */ private function addContextToStacktraceFrames(int $maxContextLines, Stacktrace $stacktrace): void { foreach ($stacktrace->getFrames() as $frame) { if ($frame->isInternal()) { continue; } $this->addContextToStacktraceFrame($maxContextLines, $frame); } } /** * Contextifies the given frame. * * @param int $maxContextLines The maximum number of lines of code to read */ private function addContextToStacktraceFrame(int $maxContextLines, Frame $frame): void { if ($frame->getAbsoluteFilePath() === null) { return; } $sourceCodeExcerpt = $this->getSourceCodeExcerpt($maxContextLines, $frame->getAbsoluteFilePath(), $frame->getLine()); $frame->setPreContext($sourceCodeExcerpt['pre_context']); $frame->setContextLine($sourceCodeExcerpt['context_line']); $frame->setPostContext($sourceCodeExcerpt['post_context']); } /** * Gets an excerpt of the source code around a given line. * * @param int $maxContextLines The maximum number of lines of code to read * @param string $filePath The file path * @param int $lineNumber The line to centre about * * @return array * * @psalm-return array{ * pre_context: string[], * context_line: string|null, * post_context: string[] * } */ private function getSourceCodeExcerpt(int $maxContextLines, string $filePath, int $lineNumber): array { $frame = [ 'pre_context' => [], 'context_line' => null, 'post_context' => [], ]; $target = max(0, $lineNumber - ($maxContextLines + 1)); $currentLineNumber = $target + 1; try { $file = new \SplFileObject($filePath); $file->seek($target); while (!$file->eof()) { /** @var string $line */ $line = $file->current(); $line = rtrim($line, "\r\n"); if ($currentLineNumber === $lineNumber) { $frame['context_line'] = $line; } elseif ($currentLineNumber < $lineNumber) { $frame['pre_context'][] = $line; } elseif ($currentLineNumber > $lineNumber) { $frame['post_context'][] = $line; } ++$currentLineNumber; if ($currentLineNumber > $lineNumber + $maxContextLines) { break; } $file->next(); } } catch (\Throwable $exception) { $this->logger->warning( \sprintf('Failed to get the source code excerpt for the file "%s".', $filePath), ['exception' => $exception] ); } return $frame; } } PKî%W]­ äpÕÕ7sentry/src/Integration/ExceptionListenerIntegration.phpnu„[µü¤addExceptionHandlerListener(static function (\Throwable $exception): void { $currentHub = SentrySdk::getCurrentHub(); $integration = $currentHub->getIntegration(self::class); // The client bound to the current hub, if any, could not have this // integration enabled. If this is the case, bail out if ($integration === null) { return; } $integration->captureException($currentHub, $exception); }); } } PKî%W] ùÄ::-sentry/src/Integration/ModulesIntegration.phpnu„[µü¤ The list of installed vendors */ private static $packages = []; /** * {@inheritdoc} */ public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); // The integration could be bound to a client that is not the one // attached to the current hub. If this is the case, bail out if ($integration !== null) { $event->setModules(self::getComposerPackages()); } return $event; }); } /** * @return array */ private static function getComposerPackages(): array { if (empty(self::$packages)) { foreach (self::getInstalledPackages() as $package) { try { self::$packages[$package] = PrettyVersions::getVersion($package)->getPrettyVersion(); } catch (\Throwable $exception) { continue; } } } return self::$packages; } /** * @return string[] */ private static function getInstalledPackages(): array { if (class_exists(InstalledVersions::class)) { return InstalledVersions::getInstalledPackages(); } if (class_exists(Versions::class)) { // BC layer for Composer 1, using a transient dependency return array_keys(Versions::VERSIONS); } // this should not happen return ['sentry/sentry']; } } PKî%W]hg´rPP)sentry/src/Integration/RequestFetcher.phpnu„[µü¤options = $options; } /** * {@inheritdoc} */ public function setupOnce(): void { ErrorHandler::registerOnceErrorHandler($this->options) ->addErrorHandlerListener( static function (\ErrorException $exception): void { $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 ($integration === null || $client === null) { return; } if ($exception instanceof SilencedErrorException && !$client->getOptions()->shouldCaptureSilencedErrors()) { return; } if (!$exception instanceof SilencedErrorException && !($client->getOptions()->getErrorTypes() & $exception->getSeverity())) { return; } $integration->captureException($currentHub, $exception); } ); } /** * @internal this is a convenience method to create an instance of this integration for tests */ public static function make(Options $options): self { $integration = new self(); $integration->setOptions($options); return $integration; } } PKî%W]rÉVÜÜ2sentry/src/Integration/RequestFetcherInterface.phpnu„[µü¤ */ final class EnvironmentIntegration implements IntegrationInterface { /** * {@inheritdoc} */ public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); if ($integration !== null) { $event->setRuntimeContext($integration->updateRuntimeContext($event->getRuntimeContext())); $event->setOsContext($integration->updateServerOsContext($event->getOsContext())); } return $event; }); } private function updateRuntimeContext(?RuntimeContext $runtimeContext): RuntimeContext { if ($runtimeContext === null) { $runtimeContext = new RuntimeContext('php'); } if ($runtimeContext->getVersion() === null) { $runtimeContext->setVersion(PHPVersion::parseVersion()); } if ($runtimeContext->getSAPI() === null) { $runtimeContext->setSAPI(\PHP_SAPI); } return $runtimeContext; } private function updateServerOsContext(?OsContext $osContext): ?OsContext { if (!\function_exists('php_uname')) { return $osContext; } if ($osContext === null) { $osContext = new OsContext(php_uname('s')); } if ($osContext->getVersion() === null) { $osContext->setVersion(php_uname('r')); } if ($osContext->getBuild() === null) { $osContext->setBuild(php_uname('v')); } if ($osContext->getKernelVersion() === null) { $osContext->setKernelVersion(php_uname('a')); } if ($osContext->getMachineType() === null) { $osContext->setMachineType(php_uname('m')); } return $osContext; } } PKî%W]TË ùù8sentry/src/Integration/FatalErrorListenerIntegration.phpnu„[µü¤ */ final class FatalErrorListenerIntegration extends AbstractErrorListenerIntegration { /** * {@inheritdoc} */ public function setupOnce(): void { $errorHandler = ErrorHandler::registerOnceFatalErrorHandler(); $errorHandler->addFatalErrorHandlerListener(static function (FatalErrorException $exception): void { $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 ($integration === null || $client === null) { return; } if (!($client->getOptions()->getErrorTypes() & $exception->getSeverity())) { return; } $integration->captureException($currentHub, $exception); }); } } PKî%W]mÜUñ‡‡;sentry/src/Integration/AbstractErrorListenerIntegration.phpnu„[µü¤withScope(function (Scope $scope) use ($hub, $exception): void { $scope->addEventProcessor(\Closure::fromCallable([$this, 'addExceptionMechanismToEvent'])); $hub->captureException($exception); }); } /** * Adds the exception mechanism to the event. * * @param Event $event The event object */ protected function addExceptionMechanismToEvent(Event $event): Event { $exceptions = $event->getExceptions(); foreach ($exceptions as $exception) { $data = []; $mechanism = $exception->getMechanism(); if ($mechanism !== null) { $data = $mechanism->getData(); } $exception->setMechanism(new ExceptionMechanism(ExceptionMechanism::TYPE_GENERIC, false, $data)); } return $event; } } PKî%W]%;ƒss1sentry/src/Integration/TransactionIntegration.phpnu„[µü¤ */ final class TransactionIntegration implements IntegrationInterface { /** * {@inheritdoc} */ public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event, EventHint $hint): Event { $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); // The client bound to the current hub, if any, could not have this // integration enabled. If this is the case, bail out if ($integration === null) { return $event; } if ($event->getTransaction() !== null) { return $event; } if (isset($hint->extra['transaction']) && \is_string($hint->extra['transaction'])) { $event->setTransaction($hint->extra['transaction']); } elseif (isset($_SERVER['PATH_INFO'])) { $event->setTransaction($_SERVER['PATH_INFO']); } return $event; }); } } PKî%W]Mô�óRR.sentry/src/Integration/IntegrationRegistry.phpnu„[µü¤, bool> The registered integrations */ private $integrations = []; private function __construct() { } /** * Gets the current singleton instance or creates a new one if it didn't * exists yet. */ public static function getInstance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Setups the integrations according to the given options. For each integration * the {@see IntegrationInterface::setupOnce()} method will be called only once * during the application lifetime. * * @param Options $options The SDK client options * * @return array, IntegrationInterface> */ public function setupIntegrations(Options $options, LoggerInterface $logger): array { $integrations = []; $installed = []; foreach ($this->getIntegrationsToSetup($options) as $integration) { $integrationName = \get_class($integration); $integrations[$integrationName] = $integration; if ($this->setupIntegration($integration, $options)) { $installed[] = $integrationName; } } if (\count($installed) > 0) { $logger->debug(\sprintf('The "%s" integration(s) have been installed.', implode(', ', $installed))); } return $integrations; } private function setupIntegration(IntegrationInterface $integration, Options $options): bool { $integrationName = \get_class($integration); if (isset($this->integrations[$integrationName])) { return false; } if ($integration instanceof OptionAwareIntegrationInterface) { $integration->setOptions($options); } $integration->setupOnce(); $this->integrations[$integrationName] = true; return true; } /** * @return IntegrationInterface[] */ private function getIntegrationsToSetup(Options $options): array { $integrations = []; $defaultIntegrations = $this->getDefaultIntegrations($options); $userIntegrations = $options->getIntegrations(); if (\is_array($userIntegrations)) { $userIntegrationsClasses = array_map('get_class', $userIntegrations); $pickedIntegrationsClasses = []; foreach ($defaultIntegrations as $defaultIntegration) { $integrationClassName = \get_class($defaultIntegration); if (!\in_array($integrationClassName, $userIntegrationsClasses, true) && !isset($pickedIntegrationsClasses[$integrationClassName])) { $integrations[] = $defaultIntegration; $pickedIntegrationsClasses[$integrationClassName] = true; } } foreach ($userIntegrations as $userIntegration) { $integrationClassName = \get_class($userIntegration); if (!isset($pickedIntegrationsClasses[$integrationClassName])) { $integrations[] = $userIntegration; $pickedIntegrationsClasses[$integrationClassName] = true; } } } else { $integrations = $userIntegrations($defaultIntegrations); if (!\is_array($integrations)) { throw new \UnexpectedValueException(\sprintf('Expected the callback set for the "integrations" option to return a list of integrations. Got: "%s".', get_debug_type($integrations))); } } return $integrations; } /** * @return IntegrationInterface[] */ private function getDefaultIntegrations(Options $options): array { if (!$options->hasDefaultIntegrations()) { return []; } $integrations = [ new RequestIntegration(), new TransactionIntegration(), new FrameContextifierIntegration(), new EnvironmentIntegration(), new ModulesIntegration(), ]; if ($options->getDsn() !== null) { array_unshift($integrations, new ExceptionListenerIntegration(), new ErrorListenerIntegration(), new FatalErrorListenerIntegration()); } return $integrations; } } PKî%W]nü–ú'ú'-sentry/src/Integration/RequestIntegration.phpnu„[µü¤ */ final class RequestIntegration implements IntegrationInterface { /** * This constant represents the size limit in bytes beyond which the body * of the request is not captured when the `max_request_body_size` option * is set to `small`. */ private const REQUEST_BODY_SMALL_MAX_CONTENT_LENGTH = 10 ** 3; /** * This constant represents the size limit in bytes beyond which the body * of the request is not captured when the `max_request_body_size` option * is set to `medium`. */ private const REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH = 10 ** 4; /** * This constant is a map of maximum allowed sizes for each value of the * `max_request_body_size` option. */ private const MAX_REQUEST_BODY_SIZE_OPTION_TO_MAX_LENGTH_MAP = [ 'never' => 0, 'small' => self::REQUEST_BODY_SMALL_MAX_CONTENT_LENGTH, 'medium' => self::REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH, 'always' => \PHP_INT_MAX, ]; /** * This constant defines the default list of headers that may contain * sensitive data and that will be sanitized if sending PII is disabled. */ private const DEFAULT_SENSITIVE_HEADERS = [ 'Authorization', 'Cookie', 'Set-Cookie', 'X-Forwarded-For', 'X-Real-IP', ]; /** * @var RequestFetcherInterface PSR-7 request fetcher */ private $requestFetcher; /** * @var array The options * * @psalm-var array{ * pii_sanitize_headers: string[] * } */ private $options; /** * Constructor. * * @param RequestFetcherInterface|null $requestFetcher PSR-7 request fetcher * @param array $options The options * * @psalm-param array{ * pii_sanitize_headers?: string[] * } $options */ public function __construct(?RequestFetcherInterface $requestFetcher = null, array $options = []) { $resolver = new OptionsResolver(); $this->configureOptions($resolver); $this->requestFetcher = $requestFetcher ?? new RequestFetcher(); $this->options = $resolver->resolve($options); } /** * {@inheritdoc} */ public function setupOnce(): void { 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 ($integration === null || $client === null) { return $event; } $this->processEvent($event, $client->getOptions()); return $event; }); } private function processEvent(Event $event, Options $options): void { $request = $this->requestFetcher->fetchRequest(); if ($request === null) { return; } $requestData = [ 'url' => (string) $request->getUri(), 'method' => $request->getMethod(), ]; if ($request->getUri()->getQuery()) { $requestData['query_string'] = $request->getUri()->getQuery(); } if ($options->shouldSendDefaultPii()) { $serverParams = $request->getServerParams(); if (!empty($serverParams['REMOTE_ADDR'])) { $user = $event->getUser(); $requestData['env']['REMOTE_ADDR'] = $serverParams['REMOTE_ADDR']; if ($user === null) { $user = UserDataBag::createFromUserIpAddress($serverParams['REMOTE_ADDR']); } elseif ($user->getIpAddress() === null) { $user->setIpAddress($serverParams['REMOTE_ADDR']); } $event->setUser($user); } $requestData['cookies'] = $request->getCookieParams(); $requestData['headers'] = $request->getHeaders(); } else { $requestData['headers'] = $this->sanitizeHeaders($request->getHeaders()); } $requestBody = $this->captureRequestBody($options, $request); if (!empty($requestBody)) { $requestData['data'] = $requestBody; } $event->setRequest($requestData); } /** * Removes headers containing potential PII. * * @param array $headers Array containing request headers * * @return array */ private function sanitizeHeaders(array $headers): array { foreach ($headers as $name => $values) { // Cast the header name into a string, to avoid errors on numeric headers $name = (string) $name; if (!\in_array(strtolower($name), $this->options['pii_sanitize_headers'], true)) { continue; } foreach ($values as $headerLine => $headerValue) { $headers[$name][$headerLine] = '[Filtered]'; } } return $headers; } /** * Gets the decoded body of the request, if available. If the Content-Type * header contains "application/json" then the content is decoded and if * the parsing fails then the raw data is returned. If there are submitted * fields or files, all of their information are parsed and returned. * * @param Options $options The options of the client * @param ServerRequestInterface $request The server request * * @return mixed */ private function captureRequestBody(Options $options, ServerRequestInterface $request) { $maxRequestBodySize = $options->getMaxRequestBodySize(); $requestBodySize = (int) $request->getHeaderLine('Content-Length'); if (!$this->isRequestBodySizeWithinReadBounds($requestBodySize, $maxRequestBodySize)) { return null; } $requestData = $request->getParsedBody(); $requestData = array_replace( $this->parseUploadedFiles($request->getUploadedFiles()), \is_array($requestData) ? $requestData : [] ); if (!empty($requestData)) { return $requestData; } $requestBody = ''; $maxLength = self::MAX_REQUEST_BODY_SIZE_OPTION_TO_MAX_LENGTH_MAP[$maxRequestBodySize]; if ($maxLength > 0) { $stream = $request->getBody(); while ($maxLength > 0 && !$stream->eof()) { if ('' === $buffer = $stream->read(min($maxLength, self::REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH))) { break; } $requestBody .= $buffer; $maxLength -= \strlen($buffer); } } if ($request->getHeaderLine('Content-Type') === 'application/json') { try { return JSON::decode($requestBody); } catch (JsonException $exception) { // Fallback to returning the raw data from the request body } } return $requestBody; } /** * Create an array with the same structure as $uploadedFiles, but replacing * each UploadedFileInterface with an array of info. * * @param array $uploadedFiles The uploaded files info from a PSR-7 server request * * @return array */ private function parseUploadedFiles(array $uploadedFiles): array { $result = []; foreach ($uploadedFiles as $key => $item) { if ($item instanceof UploadedFileInterface) { $result[$key] = [ 'client_filename' => $item->getClientFilename(), 'client_media_type' => $item->getClientMediaType(), 'size' => $item->getSize(), ]; } elseif (\is_array($item)) { $result[$key] = $this->parseUploadedFiles($item); } else { throw new \UnexpectedValueException(\sprintf('Expected either an object implementing the "%s" interface or an array. Got: "%s".', UploadedFileInterface::class, \is_object($item) ? \get_class($item) : \gettype($item))); } } return $result; } private function isRequestBodySizeWithinReadBounds(int $requestBodySize, string $maxRequestBodySize): bool { if ($requestBodySize <= 0) { return false; } if ($maxRequestBodySize === 'none' || $maxRequestBodySize === 'never') { return false; } if ($maxRequestBodySize === 'small' && $requestBodySize > self::REQUEST_BODY_SMALL_MAX_CONTENT_LENGTH) { return false; } if ($maxRequestBodySize === 'medium' && $requestBodySize > self::REQUEST_BODY_MEDIUM_MAX_CONTENT_LENGTH) { return false; } return true; } /** * Configures the options of the client. * * @param OptionsResolver $resolver The resolver for the options */ private function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('pii_sanitize_headers', self::DEFAULT_SENSITIVE_HEADERS); $resolver->setAllowedTypes('pii_sanitize_headers', 'string[]'); $resolver->setNormalizer('pii_sanitize_headers', static function (SymfonyOptions $options, array $value): array { return array_map('strtolower', $value); }); } } PKî%W]aSefÞ´Þ´sentry/src/Options.phpnu„[µü¤ */ final class Options { /** * The default maximum number of breadcrumbs that will be sent with an * event. */ public const DEFAULT_MAX_BREADCRUMBS = 100; /** * The default maximum execution time in seconds for the request+response * as a whole. */ public const DEFAULT_HTTP_TIMEOUT = 5; /** * The default maximum number of seconds to wait while trying to connect to a * server. */ public const DEFAULT_HTTP_CONNECT_TIMEOUT = 2; /** * @var array The configuration options */ private $options; /** * @var OptionsResolver The options resolver */ private $resolver; /** * Class constructor. * * @param array $options The configuration options */ public function __construct(array $options = []) { $this->resolver = new OptionsResolver(); $this->configureOptions($this->resolver); $this->options = $this->resolver->resolve($options); if ($this->options['enable_tracing'] === true && $this->options['traces_sample_rate'] === null) { $this->options = array_merge($this->options, ['traces_sample_rate' => 1]); } } /** * Gets the prefixes which should be stripped from filenames to create * relative paths. * * @return string[] */ public function getPrefixes(): array { return $this->options['prefixes']; } /** * Sets the prefixes which should be stripped from filenames to create * relative paths. * * @param string[] $prefixes The prefixes */ public function setPrefixes(array $prefixes): self { $options = array_merge($this->options, ['prefixes' => $prefixes]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the sampling factor to apply to events. A value of 0 will deny * sending any events, and a value of 1 will send 100% of events. */ public function getSampleRate(): float { return $this->options['sample_rate']; } /** * Sets the sampling factor to apply to events. A value of 0 will deny * sending any events, and a value of 1 will send 100% of events. * * @param float $sampleRate The sampling factor */ public function setSampleRate(float $sampleRate): self { $options = array_merge($this->options, ['sample_rate' => $sampleRate]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the sampling factor to apply to transaction. A value of 0 will deny * sending any transaction, and a value of 1 will send 100% of transaction. */ public function getTracesSampleRate(): ?float { return $this->options['traces_sample_rate']; } /** * Sets if tracing should be enabled or not. If null tracesSampleRate takes * precedence. * * @param bool|null $enableTracing Boolean if tracing should be enabled or not * * @deprecated since version 4.7. To be removed in version 5.0 */ public function setEnableTracing(?bool $enableTracing): self { $options = array_merge($this->options, ['enable_tracing' => $enableTracing]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets if tracing is enabled or not. * * @return bool|null If the option `enable_tracing` is set or not * * @deprecated since version 4.7. To be removed in version 5.0 */ public function getEnableTracing(): ?bool { return $this->options['enable_tracing']; } /** * Sets if logs should be enabled or not. * * @param bool|null $enableLogs Boolean if logs should be enabled or not */ public function setEnableLogs(?bool $enableLogs): self { $options = array_merge($this->options, ['enable_logs' => $enableLogs]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets if logs is enabled or not. */ public function getEnableLogs(): bool { return $this->options['enable_logs'] ?? false; } /** * Sets the sampling factor to apply to transactions. A value of 0 will deny * sending any transactions, and a value of 1 will send 100% of transactions. * * @param ?float $sampleRate The sampling factor */ public function setTracesSampleRate(?float $sampleRate): self { $options = array_merge($this->options, ['traces_sample_rate' => $sampleRate]); $this->options = $this->resolver->resolve($options); return $this; } public function getProfilesSampleRate(): ?float { /** @var int|float|null $value */ $value = $this->options['profiles_sample_rate'] ?? null; return $value ?? null; } public function setProfilesSampleRate(?float $sampleRate): self { $options = array_merge($this->options, ['profiles_sample_rate' => $sampleRate]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets whether tracing is enabled or not. The feature is enabled when at * least one of the `traces_sample_rate` and `traces_sampler` options is * set and `enable_tracing` is set and not false. */ public function isTracingEnabled(): bool { if ($this->getEnableTracing() !== null && $this->getEnableTracing() === false) { return false; } return $this->getTracesSampleRate() !== null || $this->getTracesSampler() !== null; } /** * Gets whether the stacktrace will be attached on captureMessage. */ public function shouldAttachStacktrace(): bool { return $this->options['attach_stacktrace']; } /** * Sets whether the stacktrace will be attached on captureMessage. * * @param bool $enable Flag indicating if the stacktrace will be attached to captureMessage calls */ public function setAttachStacktrace(bool $enable): self { $options = array_merge($this->options, ['attach_stacktrace' => $enable]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets whether a metric has their code location attached. * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function shouldAttachMetricCodeLocations(): bool { return $this->options['attach_metric_code_locations']; } /** * Sets whether a metric will have their code location attached. * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function setAttachMetricCodeLocations(bool $enable): self { $options = array_merge($this->options, ['attach_metric_code_locations' => $enable]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the number of lines of code context to capture, or null if none. */ public function getContextLines(): ?int { return $this->options['context_lines']; } /** * Sets the number of lines of code context to capture, or null if none. * * @param int|null $contextLines The number of lines of code */ public function setContextLines(?int $contextLines): self { $options = array_merge($this->options, ['context_lines' => $contextLines]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the environment. */ public function getEnvironment(): ?string { return $this->options['environment']; } /** * Sets the environment. * * @param string|null $environment The environment */ public function setEnvironment(?string $environment): self { $options = array_merge($this->options, ['environment' => $environment]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the list of paths to exclude from in_app detection. * * @return string[] */ public function getInAppExcludedPaths(): array { return $this->options['in_app_exclude']; } /** * Sets the list of paths to exclude from in_app detection. * * @param string[] $paths The list of paths */ public function setInAppExcludedPaths(array $paths): self { $options = array_merge($this->options, ['in_app_exclude' => $paths]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the list of paths which has to be identified as in_app. * * @return string[] */ public function getInAppIncludedPaths(): array { return $this->options['in_app_include']; } /** * Set the list of paths to include in in_app detection. * * @param string[] $paths The list of paths */ public function setInAppIncludedPaths(array $paths): self { $options = array_merge($this->options, ['in_app_include' => $paths]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a PSR-3 compatible logger to log internal debug messages. */ public function getLogger(): ?LoggerInterface { return $this->options['logger']; } /** * Helper to always get a logger instance even if it was not set. */ public function getLoggerOrNullLogger(): LoggerInterface { return $this->getLogger() ?? new NullLogger(); } /** * Sets a PSR-3 compatible logger to log internal debug messages. */ public function setLogger(LoggerInterface $logger): self { $options = array_merge($this->options, ['logger' => $logger]); $this->options = $this->resolver->resolve($options); return $this; } public function isSpotlightEnabled(): bool { return \is_string($this->options['spotlight']) || $this->options['spotlight']; } /** * @param bool|string $enable can be passed a boolean or the Spotlight URL (which will also enable Spotlight) */ public function enableSpotlight($enable): self { $options = array_merge($this->options, ['spotlight' => $enable]); $this->options = $this->resolver->resolve($options); return $this; } public function getSpotlightUrl(): string { if (\is_string($this->options['spotlight'])) { return $this->options['spotlight']; } return $this->options['spotlight_url']; } /** * @return $this * * @deprecated since version 4.11. To be removed in 5.x. You may use `enableSpotlight` instead. */ public function setSpotlightUrl(string $url): self { $options = array_merge($this->options, ['spotlight_url' => $url]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the release tag to be passed with every event sent to Sentry. */ public function getRelease(): ?string { return $this->options['release']; } /** * Sets the release tag to be passed with every event sent to Sentry. * * @param string|null $release The release */ public function setRelease(?string $release): self { $options = array_merge($this->options, ['release' => $release]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the DSN of the Sentry server the authenticated user is bound to. */ public function getDsn(): ?Dsn { return $this->options['dsn']; } /** * Gets the Org ID. */ public function getOrgId(): ?int { return $this->options['org_id']; } /** * Sets the Org ID. */ public function setOrgId(int $orgId): self { $options = array_merge($this->options, ['org_id' => $orgId]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the name of the server the SDK is running on (e.g. the hostname). */ public function getServerName(): string { return $this->options['server_name']; } /** * Sets the name of the server the SDK is running on (e.g. the hostname). * * @param string $serverName The server name */ public function setServerName(string $serverName): self { $options = array_merge($this->options, ['server_name' => $serverName]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a list of exceptions to be ignored and not sent to Sentry. * * @return string[] * * @psalm-return list> */ public function getIgnoreExceptions(): array { return $this->options['ignore_exceptions']; } /** * Sets a list of exceptions to be ignored and not sent to Sentry. * * @param string[] $ignoreErrors The list of exceptions to be ignored */ public function setIgnoreExceptions(array $ignoreErrors): self { $options = array_merge($this->options, ['ignore_exceptions' => $ignoreErrors]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a list of transaction names to be ignored and not sent to Sentry. * * @return string[] */ public function getIgnoreTransactions(): array { return $this->options['ignore_transactions']; } /** * Sets a list of transaction names to be ignored and not sent to Sentry. * * @param string[] $ignoreTransaction The list of transaction names to be ignored */ public function setIgnoreTransactions(array $ignoreTransaction): self { $options = array_merge($this->options, ['ignore_transactions' => $ignoreTransaction]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked before an event is sent to the server. * If `null` is returned it won't be sent. * * @psalm-return callable(Event, ?EventHint): ?Event */ public function getBeforeSendCallback(): callable { return $this->options['before_send']; } /** * Sets a callable to be called to decide whether an event should * be captured or not. * * @param callable $callback The callable * * @psalm-param callable(Event, ?EventHint): ?Event $callback */ public function setBeforeSendCallback(callable $callback): self { $options = array_merge($this->options, ['before_send' => $callback]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked before an transaction is sent to the server. * If `null` is returned it won't be sent. * * @psalm-return callable(Event, ?EventHint): ?Event */ public function getBeforeSendTransactionCallback(): callable { return $this->options['before_send_transaction']; } /** * Sets a callable to be called to decide whether an transaction should * be captured or not. * * @param callable $callback The callable * * @psalm-param callable(Event, ?EventHint): ?Event $callback */ public function setBeforeSendTransactionCallback(callable $callback): self { $options = array_merge($this->options, ['before_send_transaction' => $callback]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked before a check-in is sent to the server. * If `null` is returned it won't be sent. * * @psalm-return callable(Event, ?EventHint): ?Event */ public function getBeforeSendCheckInCallback(): callable { return $this->options['before_send_check_in']; } /** * Sets a callable to be called to decide whether a check-in should * be captured or not. * * @param callable $callback The callable * * @psalm-param callable(Event, ?EventHint): ?Event $callback */ public function setBeforeSendCheckInCallback(callable $callback): self { $options = array_merge($this->options, ['before_send_check_in' => $callback]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked before an log is sent to the server. * If `null` is returned it won't be sent. * * @psalm-return callable(Log): ?Log */ public function getBeforeSendLogCallback(): callable { return $this->options['before_send_log']; } /** * Sets a callable to be called to decide whether a log should * be captured or not. * * @param callable $callback The callable * * @psalm-param callable(Log): ?Log $callback */ public function setBeforeSendLogCallback(callable $callback): self { $options = array_merge($this->options, ['before_send_log' => $callback]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked before metrics are sent to the server. * If `null` is returned it won't be sent. * * @psalm-return callable(Event, ?EventHint): ?Event * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function getBeforeSendMetricsCallback(): callable { return $this->options['before_send_metrics']; } /** * Sets a callable to be called to decide whether metrics should * be send or not. * * @param callable $callback The callable * * @psalm-param callable(Event, ?EventHint): ?Event $callback * * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function setBeforeSendMetricsCallback(callable $callback): self { $options = array_merge($this->options, ['before_send_metrics' => $callback]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets an allow list of trace propagation targets. * * @return string[]|null */ public function getTracePropagationTargets(): ?array { return $this->options['trace_propagation_targets']; } /** * Set an allow list of trace propagation targets. * * @param string[] $tracePropagationTargets Trace propagation targets */ public function setTracePropagationTargets(array $tracePropagationTargets): self { $options = array_merge($this->options, ['trace_propagation_targets' => $tracePropagationTargets]); $this->options = $this->resolver->resolve($options); return $this; } /** * Returns whether strict trace propagation is enabled or not. */ public function isStrictTracePropagationEnabled(): bool { return $this->options['strict_trace_propagation']; } /** * Sets if strict trace propagation should be enabled or not. */ public function enableStrictTracePropagation(bool $strictTracePropagation): self { $options = array_merge($this->options, ['strict_trace_propagation' => $strictTracePropagation]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a list of default tags for events. * * @return array */ public function getTags(): array { return $this->options['tags']; } /** * Sets a list of default tags for events. * * @param array $tags A list of tags */ public function setTags(array $tags): self { $options = array_merge($this->options, ['tags' => $tags]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a bit mask for error_reporting used in {@link ErrorListenerIntegration} to filter which errors to report. */ public function getErrorTypes(): int { return $this->options['error_types'] ?? error_reporting(); } /** * Sets a bit mask for error_reporting used in {@link ErrorListenerIntegration} to filter which errors to report. * * @param int $errorTypes The bit mask */ public function setErrorTypes(int $errorTypes): self { $options = array_merge($this->options, ['error_types' => $errorTypes]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the maximum number of breadcrumbs sent with events. */ public function getMaxBreadcrumbs(): int { return $this->options['max_breadcrumbs']; } /** * Sets the maximum number of breadcrumbs sent with events. * * @param int $maxBreadcrumbs The maximum number of breadcrumbs */ public function setMaxBreadcrumbs(int $maxBreadcrumbs): self { $options = array_merge($this->options, ['max_breadcrumbs' => $maxBreadcrumbs]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked when adding a breadcrumb. * * @psalm-return callable(Breadcrumb): ?Breadcrumb */ public function getBeforeBreadcrumbCallback(): callable { return $this->options['before_breadcrumb']; } /** * Sets a callback that will be invoked when adding a breadcrumb, allowing * to optionally modify it before adding it to future events. Note that you * must return a valid breadcrumb from this callback. If you do not wish to * modify the breadcrumb, simply return it at the end. Returning `null` will * cause the breadcrumb to be dropped. * * @param callable $callback The callback * * @psalm-param callable(Breadcrumb): ?Breadcrumb $callback */ public function setBeforeBreadcrumbCallback(callable $callback): self { $options = array_merge($this->options, ['before_breadcrumb' => $callback]); $this->options = $this->resolver->resolve($options); return $this; } /** * Sets the list of integrations that should be installed after SDK was * initialized or a function that receives default integrations and returns * a new, updated list. * * @param IntegrationInterface[]|callable(IntegrationInterface[]): IntegrationInterface[] $integrations The list or callable */ public function setIntegrations($integrations): self { $options = array_merge($this->options, ['integrations' => $integrations]); $this->options = $this->resolver->resolve($options); return $this; } /** * Returns all configured integrations that will be used by the Client. * * @return IntegrationInterface[]|callable(IntegrationInterface[]): IntegrationInterface[] */ public function getIntegrations() { return $this->options['integrations']; } public function setTransport(TransportInterface $transport): self { $options = array_merge($this->options, ['transport' => $transport]); $this->options = $this->resolver->resolve($options); return $this; } public function getTransport(): ?TransportInterface { return $this->options['transport']; } public function setHttpClient(HttpClientInterface $httpClient): self { $options = array_merge($this->options, ['http_client' => $httpClient]); $this->options = $this->resolver->resolve($options); return $this; } public function getHttpClient(): ?HttpClientInterface { return $this->options['http_client']; } /** * Should default PII be sent by default. */ public function shouldSendDefaultPii(): bool { return $this->options['send_default_pii']; } /** * Sets if default PII should be sent with every event (if possible). * * @param bool $enable Flag indicating if default PII will be sent */ public function setSendDefaultPii(bool $enable): self { $options = array_merge($this->options, ['send_default_pii' => $enable]); $this->options = $this->resolver->resolve($options); return $this; } /** * Returns whether the default integrations are enabled. */ public function hasDefaultIntegrations(): bool { return $this->options['default_integrations']; } /** * Sets whether the default integrations are enabled. * * @param bool $enable Flag indicating whether the default integrations should be enabled */ public function setDefaultIntegrations(bool $enable): self { $options = array_merge($this->options, ['default_integrations' => $enable]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the max length for values in the event payload. */ public function getMaxValueLength(): int { return $this->options['max_value_length']; } /** * Sets the max length for specific values in the event payload. * * @param int $maxValueLength The number of characters after which the values containing text will be truncated */ public function setMaxValueLength(int $maxValueLength): self { $options = array_merge($this->options, ['max_value_length' => $maxValueLength]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the http proxy setting. */ public function getHttpProxy(): ?string { return $this->options['http_proxy']; } /** * Sets the http proxy. Be aware this option only works when curl client is used. * * @param string|null $httpProxy The http proxy */ public function setHttpProxy(?string $httpProxy): self { $options = array_merge($this->options, ['http_proxy' => $httpProxy]); $this->options = $this->resolver->resolve($options); return $this; } public function getHttpProxyAuthentication(): ?string { return $this->options['http_proxy_authentication']; } public function setHttpProxyAuthentication(?string $httpProxy): self { $options = array_merge($this->options, ['http_proxy_authentication' => $httpProxy]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the maximum number of seconds to wait while trying to connect to a server. */ public function getHttpConnectTimeout(): float { return $this->options['http_connect_timeout']; } /** * Sets the maximum number of seconds to wait while trying to connect to a server. * * @param float $httpConnectTimeout The amount of time in seconds */ public function setHttpConnectTimeout(float $httpConnectTimeout): self { $options = array_merge($this->options, ['http_connect_timeout' => $httpConnectTimeout]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the maximum execution time for the request+response as a whole. */ public function getHttpTimeout(): float { return $this->options['http_timeout']; } /** * Sets the maximum execution time for the request+response as a whole. The * value should also include the time for the connect phase, so it should be * greater than the value set for the `http_connect_timeout` option. * * @param float $httpTimeout The amount of time in seconds */ public function setHttpTimeout(float $httpTimeout): self { $options = array_merge($this->options, ['http_timeout' => $httpTimeout]); $this->options = $this->resolver->resolve($options); return $this; } public function getHttpSslVerifyPeer(): bool { return $this->options['http_ssl_verify_peer']; } public function setHttpSslVerifyPeer(bool $httpSslVerifyPeer): self { $options = array_merge($this->options, ['http_ssl_verify_peer' => $httpSslVerifyPeer]); $this->options = $this->resolver->resolve($options); return $this; } public function getHttpSslNativeCa(): bool { return $this->options['http_ssl_native_ca']; } public function setHttpSslNativeCa(bool $httpSslNativeCa): self { $options = array_merge($this->options, ['http_ssl_native_ca' => $httpSslNativeCa]); $this->options = $this->resolver->resolve($options); return $this; } /** * Returns whether the requests should be compressed using GZIP or not. */ public function isHttpCompressionEnabled(): bool { return $this->options['http_compression']; } /** * Sets whether the request should be compressed using JSON or not. */ public function setEnableHttpCompression(bool $enabled): self { $options = array_merge($this->options, ['http_compression' => $enabled]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets whether the silenced errors should be captured or not. * * @return bool If true, errors silenced through the @ operator will be reported, * ignored otherwise */ public function shouldCaptureSilencedErrors(): bool { return $this->options['capture_silenced_errors']; } /** * Sets whether the silenced errors should be captured or not. * * @param bool $shouldCapture If set to true, errors silenced through the @ * operator will be reported, ignored otherwise */ public function setCaptureSilencedErrors(bool $shouldCapture): self { $options = array_merge($this->options, ['capture_silenced_errors' => $shouldCapture]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the limit up to which integrations should capture the HTTP request * body. */ public function getMaxRequestBodySize(): string { return $this->options['max_request_body_size']; } /** * Sets the limit up to which integrations should capture the HTTP request * body. * * @param string $maxRequestBodySize The limit up to which request body are * captured. It can be set to one of the * following values: * * - never: request bodies are never sent * - small: only small request bodies will * be captured where the cutoff for small * depends on the SDK (typically 4KB) * - medium: medium-sized requests and small * requests will be captured. (typically 10KB) * - always: the SDK will always capture the * request body for as long as sentry can * make sense of it */ public function setMaxRequestBodySize(string $maxRequestBodySize): self { $options = array_merge($this->options, ['max_request_body_size' => $maxRequestBodySize]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets the callbacks used to customize how objects are serialized in the payload * of the event. * * @return array */ public function getClassSerializers(): array { return $this->options['class_serializers']; } /** * Sets a list of callables that will be called to customize how objects are * serialized in the event's payload. The list must be a map of FQCN/callable * pairs. * * @param array $serializers The list of serializer callbacks */ public function setClassSerializers(array $serializers): self { $options = array_merge($this->options, ['class_serializers' => $serializers]); $this->options = $this->resolver->resolve($options); return $this; } /** * Gets a callback that will be invoked when we sample a Transaction. * * @psalm-return null|callable(Tracing\SamplingContext): float */ public function getTracesSampler(): ?callable { return $this->options['traces_sampler']; } /** * Sets a callback that will be invoked when we take the sampling decision for Transactions. * Return a number between 0 and 1 to define the sample rate for the provided SamplingContext. * * @param ?callable $sampler The sampler * * @psalm-param null|callable(Tracing\SamplingContext): float $sampler */ public function setTracesSampler(?callable $sampler): self { $options = array_merge($this->options, ['traces_sampler' => $sampler]); $this->options = $this->resolver->resolve($options); return $this; } /** * Configures the options of the client. * * @param OptionsResolver $resolver The resolver for the options * * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException * @throws \Symfony\Component\OptionsResolver\Exception\AccessException */ private function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'integrations' => [], 'default_integrations' => true, 'prefixes' => array_filter(explode(\PATH_SEPARATOR, get_include_path() ?: '')), 'sample_rate' => 1, 'enable_tracing' => null, 'enable_logs' => false, 'traces_sample_rate' => null, 'traces_sampler' => null, 'profiles_sample_rate' => null, 'attach_stacktrace' => false, /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ 'attach_metric_code_locations' => false, 'context_lines' => 5, 'environment' => $_SERVER['SENTRY_ENVIRONMENT'] ?? null, 'logger' => null, 'spotlight' => $_SERVER['SENTRY_SPOTLIGHT'] ?? null, /** * @deprecated since version 4.11. To be removed in 5.0. You may use `spotlight` instead. */ 'spotlight_url' => 'http://localhost:8969', 'release' => $_SERVER['SENTRY_RELEASE'] ?? $_SERVER['AWS_LAMBDA_FUNCTION_VERSION'] ?? null, 'dsn' => $_SERVER['SENTRY_DSN'] ?? null, 'org_id' => null, 'server_name' => gethostname(), 'ignore_exceptions' => [], 'ignore_transactions' => [], 'before_send' => static function (Event $event): Event { return $event; }, 'before_send_transaction' => static function (Event $transaction): Event { return $transaction; }, 'before_send_check_in' => static function (Event $checkIn): Event { return $checkIn; }, 'before_send_log' => static function (Log $log): Log { return $log; }, /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ 'before_send_metrics' => static function (Event $metrics): ?Event { return null; }, 'trace_propagation_targets' => null, 'strict_trace_propagation' => false, 'tags' => [], 'error_types' => null, 'max_breadcrumbs' => self::DEFAULT_MAX_BREADCRUMBS, 'before_breadcrumb' => static function (Breadcrumb $breadcrumb): Breadcrumb { return $breadcrumb; }, 'in_app_exclude' => [], 'in_app_include' => [], 'send_default_pii' => false, 'max_value_length' => 1024, 'transport' => null, 'http_client' => null, 'http_proxy' => null, 'http_proxy_authentication' => null, 'http_connect_timeout' => self::DEFAULT_HTTP_CONNECT_TIMEOUT, 'http_timeout' => self::DEFAULT_HTTP_TIMEOUT, 'http_ssl_verify_peer' => true, 'http_ssl_native_ca' => false, 'http_compression' => true, 'capture_silenced_errors' => false, 'max_request_body_size' => 'medium', 'class_serializers' => [], ]); $resolver->setAllowedTypes('prefixes', 'string[]'); $resolver->setAllowedTypes('sample_rate', ['int', 'float']); $resolver->setAllowedTypes('enable_tracing', ['null', 'bool']); $resolver->setAllowedTypes('enable_logs', 'bool'); $resolver->setAllowedTypes('traces_sample_rate', ['null', 'int', 'float']); $resolver->setAllowedTypes('traces_sampler', ['null', 'callable']); $resolver->setAllowedTypes('profiles_sample_rate', ['null', 'int', 'float']); $resolver->setAllowedTypes('attach_stacktrace', 'bool'); $resolver->setAllowedTypes('attach_metric_code_locations', 'bool'); $resolver->setAllowedTypes('context_lines', ['null', 'int']); $resolver->setAllowedTypes('environment', ['null', 'string']); $resolver->setAllowedTypes('in_app_exclude', 'string[]'); $resolver->setAllowedTypes('in_app_include', 'string[]'); $resolver->setAllowedTypes('logger', ['null', LoggerInterface::class]); $resolver->setAllowedTypes('spotlight', ['bool', 'string', 'null']); $resolver->setAllowedTypes('spotlight_url', 'string'); $resolver->setAllowedTypes('release', ['null', 'string']); $resolver->setAllowedTypes('dsn', ['null', 'string', 'bool', Dsn::class]); $resolver->setAllowedTypes('org_id', ['null', 'int']); $resolver->setAllowedTypes('server_name', 'string'); $resolver->setAllowedTypes('before_send', ['callable']); $resolver->setAllowedTypes('before_send_transaction', ['callable']); $resolver->setAllowedTypes('before_send_log', 'callable'); $resolver->setAllowedTypes('ignore_exceptions', 'string[]'); $resolver->setAllowedTypes('ignore_transactions', 'string[]'); $resolver->setAllowedTypes('trace_propagation_targets', ['null', 'string[]']); $resolver->setAllowedTypes('strict_trace_propagation', 'bool'); $resolver->setAllowedTypes('tags', 'string[]'); $resolver->setAllowedTypes('error_types', ['null', 'int']); $resolver->setAllowedTypes('max_breadcrumbs', 'int'); $resolver->setAllowedTypes('before_breadcrumb', ['callable']); $resolver->setAllowedTypes('integrations', ['Sentry\\Integration\\IntegrationInterface[]', 'callable']); $resolver->setAllowedTypes('send_default_pii', 'bool'); $resolver->setAllowedTypes('default_integrations', 'bool'); $resolver->setAllowedTypes('max_value_length', 'int'); $resolver->setAllowedTypes('transport', ['null', TransportInterface::class]); $resolver->setAllowedTypes('http_client', ['null', HttpClientInterface::class]); $resolver->setAllowedTypes('http_proxy', ['null', 'string']); $resolver->setAllowedTypes('http_proxy_authentication', ['null', 'string']); $resolver->setAllowedTypes('http_connect_timeout', ['int', 'float']); $resolver->setAllowedTypes('http_timeout', ['int', 'float']); $resolver->setAllowedTypes('http_ssl_verify_peer', 'bool'); $resolver->setAllowedTypes('http_ssl_native_ca', 'bool'); $resolver->setAllowedTypes('http_compression', 'bool'); $resolver->setAllowedTypes('capture_silenced_errors', 'bool'); $resolver->setAllowedTypes('max_request_body_size', 'string'); $resolver->setAllowedTypes('class_serializers', 'array'); $resolver->setAllowedValues('max_request_body_size', ['none', 'never', 'small', 'medium', 'always']); $resolver->setAllowedValues('dsn', \Closure::fromCallable([$this, 'validateDsnOption'])); $resolver->setAllowedValues('max_breadcrumbs', \Closure::fromCallable([$this, 'validateMaxBreadcrumbsOptions'])); $resolver->setAllowedValues('class_serializers', \Closure::fromCallable([$this, 'validateClassSerializersOption'])); $resolver->setAllowedValues('context_lines', \Closure::fromCallable([$this, 'validateContextLinesOption'])); $resolver->setNormalizer('dsn', \Closure::fromCallable([$this, 'normalizeDsnOption'])); $resolver->setNormalizer('prefixes', function (SymfonyOptions $options, array $value) { return array_map([$this, 'normalizeAbsolutePath'], $value); }); $resolver->setNormalizer('spotlight', \Closure::fromCallable([$this, 'normalizeBooleanOrUrl'])); $resolver->setNormalizer('in_app_exclude', function (SymfonyOptions $options, array $value) { return array_map([$this, 'normalizeAbsolutePath'], $value); }); $resolver->setNormalizer('in_app_include', function (SymfonyOptions $options, array $value) { return array_map([$this, 'normalizeAbsolutePath'], $value); }); } /** * Normalizes the given path as an absolute path. * * @param string $value The path */ private function normalizeAbsolutePath(string $value): string { $path = @realpath($value); if ($path === false) { $path = $value; } return $path; } /** * @return bool|string */ private function normalizeBooleanOrUrl(SymfonyOptions $options, ?string $booleanOrUrl) { if (empty($booleanOrUrl)) { return false; } if (filter_var($booleanOrUrl, \FILTER_VALIDATE_URL)) { return $booleanOrUrl; } return filter_var($booleanOrUrl, \FILTER_VALIDATE_BOOLEAN); } /** * Normalizes the DSN option by parsing the host, public and secret keys and * an optional path. * * @param SymfonyOptions $options The configuration options * @param string|bool|Dsn|null $value The actual value of the option to normalize */ private function normalizeDsnOption(SymfonyOptions $options, $value): ?Dsn { if ($value === null || \is_bool($value)) { return null; } if ($value instanceof Dsn) { return $value; } switch (strtolower($value)) { case '': case 'false': case '(false)': case 'empty': case '(empty)': case 'null': case '(null)': return null; } return Dsn::createFromString($value); } /** * Validates the DSN option ensuring that all required pieces are set and * that the URL is valid. * * @param string|bool|Dsn|null $dsn The value of the option */ private function validateDsnOption($dsn): bool { if ($dsn === null || $dsn instanceof Dsn) { return true; } if (\is_bool($dsn)) { return $dsn === false; } switch (strtolower($dsn)) { case '': case 'false': case '(false)': case 'empty': case '(empty)': case 'null': case '(null)': return true; } try { Dsn::createFromString($dsn); return true; } catch (\InvalidArgumentException $exception) { return false; } } /** * Validates if the value of the max_breadcrumbs option is valid. * * @param int $value The value to validate */ private function validateMaxBreadcrumbsOptions(int $value): bool { return $value >= 0; } /** * Validates that the values passed to the `class_serializers` option are valid. * * @param mixed[] $serializers The value to validate */ private function validateClassSerializersOption(array $serializers): bool { foreach ($serializers as $class => $serializer) { if (!\is_string($class) || !\is_callable($serializer)) { return false; } } return true; } /** * Validates that the value passed to the "context_lines" option is valid. * * @param int|null $contextLines The value to validate */ private function validateContextLinesOption(?int $contextLines): bool { return $contextLines === null || $contextLines >= 0; } } PKî%W]ÑÖ|>sentry/src/Breadcrumb.phpnu„[µü¤ */ final class Breadcrumb { /** * This constant defines the default breadcrumb type. */ public const TYPE_DEFAULT = 'default'; /** * This constant defines the http breadcrumb type. */ public const TYPE_HTTP = 'http'; /** * This constant defines the user breadcrumb type. */ public const TYPE_USER = 'user'; /** * This constant defines the navigation breadcrumb type. */ public const TYPE_NAVIGATION = 'navigation'; /** * This constant defines the error breadcrumb type. */ public const TYPE_ERROR = 'error'; /** * This constant defines the debug level for a breadcrumb. */ public const LEVEL_DEBUG = 'debug'; /** * This constant defines the info level for a breadcrumb. */ public const LEVEL_INFO = 'info'; /** * This constant defines the warning level for a breadcrumb. */ public const LEVEL_WARNING = 'warning'; /** * This constant defines the error level for a breadcrumb. */ public const LEVEL_ERROR = 'error'; /** * This constant defines the fatal level for a breadcrumb. */ public const LEVEL_FATAL = 'fatal'; /** * This constant defines the list of values allowed to be set as severity * level of the breadcrumb. */ private const ALLOWED_LEVELS = [ self::LEVEL_DEBUG, self::LEVEL_INFO, self::LEVEL_WARNING, self::LEVEL_ERROR, self::LEVEL_FATAL, ]; /** * @var string The category of the breadcrumb */ private $category; /** * @var string The type of breadcrumb */ private $type; /** * @var string|null The message of the breadcrumb */ private $message; /** * @var string The level of the breadcrumb */ private $level; /** * @var array The meta data of the breadcrumb */ private $metadata; /** * @var float The timestamp of the breadcrumb */ private $timestamp; /** * Constructor. * * @param string $level The error level of the breadcrumb * @param string $type The type of the breadcrumb * @param string $category The category of the breadcrumb * @param string|null $message Optional text message * @param array $metadata Additional information about the breadcrumb * @param float|null $timestamp Optional timestamp of the breadcrumb */ public function __construct(string $level, string $type, string $category, ?string $message = null, array $metadata = [], ?float $timestamp = null) { if (!\in_array($level, self::ALLOWED_LEVELS, true)) { throw new \InvalidArgumentException('The value of the $level argument must be one of the Breadcrumb::LEVEL_* constants.'); } $this->type = $type; $this->level = $level; $this->category = $category; $this->message = $message; $this->metadata = $metadata; $this->timestamp = $timestamp ?? microtime(true); } /** * Gets the breadcrumb type. */ public function getType(): string { return $this->type; } /** * Sets the type of the breadcrumb. * * @param string $type The type * * @return static */ public function withType(string $type): self { if ($type === $this->type) { return $this; } $new = clone $this; $new->type = $type; return $new; } /** * Gets the breadcrumb level. */ public function getLevel(): string { return $this->level; } /** * Sets the error level of the breadcrumb. * * @param string $level The level * * @return static */ public function withLevel(string $level): self { if (!\in_array($level, self::ALLOWED_LEVELS, true)) { throw new \InvalidArgumentException('The value of the $level argument must be one of the Breadcrumb::LEVEL_* constants.'); } if ($level === $this->level) { return $this; } $new = clone $this; $new->level = $level; return $new; } /** * Gets the breadcrumb category. */ public function getCategory(): string { return $this->category; } /** * Sets the breadcrumb category. * * @param string $category The category * * @return static */ public function withCategory(string $category): self { if ($category === $this->category) { return $this; } $new = clone $this; $new->category = $category; return $new; } /** * Gets the breadcrumb message. */ public function getMessage(): ?string { return $this->message; } /** * Sets the breadcrumb message. * * @param string $message The message * * @return static */ public function withMessage(string $message): self { if ($message === $this->message) { return $this; } $new = clone $this; $new->message = $message; return $new; } /** * Gets the breadcrumb meta data. * * @return array */ public function getMetadata(): array { return $this->metadata; } /** * Returns an instance of this class with the provided metadata, replacing * any existing values of any metadata with the same name. * * @param string $name The name of the metadata * @param mixed $value The value * * @return static */ public function withMetadata(string $name, $value): self { if (isset($this->metadata[$name]) && $value === $this->metadata[$name]) { return $this; } $new = clone $this; $new->metadata[$name] = $value; return $new; } /** * Returns an instance of this class without the specified metadata * information. * * @param string $name The name of the metadata * * @return static|Breadcrumb */ public function withoutMetadata(string $name): self { if (!isset($this->metadata[$name])) { return $this; } $new = clone $this; unset($new->metadata[$name]); return $new; } /** * Gets the breadcrumb timestamp. */ public function getTimestamp(): float { return $this->timestamp; } /** * Sets the breadcrumb timestamp. * * @param float $timestamp The timestamp * * @return static */ public function withTimestamp(float $timestamp): self { if ($timestamp === $this->timestamp) { return $this; } $new = clone $this; $new->timestamp = $timestamp; return $new; } /** * Helper method to create an instance of this class from an array of data. * * @param array $data Data used to populate the breadcrumb * * @psalm-param array{ * level: string, * type?: string, * category: string, * message?: string|null, * data?: array, * timestamp?: float|null * } $data */ public static function fromArray(array $data): self { return new self( $data['level'], $data['type'] ?? self::TYPE_DEFAULT, $data['category'], $data['message'] ?? null, $data['data'] ?? [], $data['timestamp'] ?? null ); } } PKî%W]&™s[CSCSsentry/src/Event.phpnu„[µü¤, * } * @phpstan-type SdkPackageEntry array{ * name: string, * version: string, * } */ final class Event { public const DEFAULT_ENVIRONMENT = 'production'; /** * @var EventId The ID */ private $id; /** * @var float|null The date and time of when this event was generated */ private $timestamp; /** * This property is used if it's a Transaction event together with $timestamp it's the duration of the transaction. * * @var float|null The date and time of when this event was generated */ private $startTimestamp; /** * @var Severity|null The severity of this event */ private $level; /** * @var string|null The name of the logger which created the record */ private $logger; /** * @var string|null the name of the transaction (or culprit) which caused this exception */ private $transaction; /** * @var CheckIn|null The check in data */ private $checkIn; /** * @var Log[] */ private $logs = []; /** * @var string|null The name of the server (e.g. the host name) */ private $serverName; /** * @var string|null The release of the program */ private $release; /** * @var string|null The error message */ private $message; /** * @var string|null The formatted error message */ private $messageFormatted; /** * @var string[] The parameters to use to format the message */ private $messageParams = []; /** * @var string|null The environment where this event generated (e.g. production) */ private $environment; /** * @var array A list of relevant modules and their versions */ private $modules = []; /** * @var array The request data */ private $request = []; /** * @var array A list of tags associated to this event */ private $tags = []; /** * @var OsContext|null The server OS context data */ private $osContext; /** * @var RuntimeContext|null The runtime context data */ private $runtimeContext; /** * @var UserDataBag|null The user context data */ private $user; /** * @var array> An arbitrary mapping of additional contexts associated to this event */ private $contexts = []; /** * @var array An arbitrary mapping of additional metadata */ private $extra = []; /** * @var string[] An array of strings used to dictate the deduplication of this event */ private $fingerprint = []; /** * @var Breadcrumb[] The associated breadcrumbs */ private $breadcrumbs = []; /** * @var Span[] The array of spans if it's a transaction */ private $spans = []; /** * @var ExceptionDataBag[] The exceptions */ private $exceptions = []; /** * @var Stacktrace|null The stacktrace that generated this event */ private $stacktrace; /** * A place to stash data which is needed at some point in the SDK's * event processing pipeline but which shouldn't get sent to Sentry. * * @var array */ private $sdkMetadata = []; /** * @var string The Sentry SDK identifier */ private $sdkIdentifier = Client::SDK_IDENTIFIER; /** * @var string The Sentry SDK version */ private $sdkVersion = Client::SDK_VERSION; /** * @var SdkPackageEntry[] The Sentry SDK packages */ private $sdkPackages = [ [ 'name' => 'composer:sentry/sentry', 'version' => Client::SDK_VERSION, ], ]; /** * @var EventType The type of the Event */ private $type; /** * @var Profile|null The profile data */ private $profile; private function __construct(?EventId $eventId, EventType $eventType) { $this->id = $eventId ?? EventId::generate(); $this->timestamp = microtime(true); $this->type = $eventType; } /** * Creates a new event. * * @param EventId|null $eventId The ID of the event */ public static function createEvent(?EventId $eventId = null): self { return new self($eventId, EventType::event()); } /** * Creates a new transaction event. * * @param EventId|null $eventId The ID of the event */ public static function createTransaction(?EventId $eventId = null): self { return new self($eventId, EventType::transaction()); } public static function createCheckIn(?EventId $eventId = null): self { return new self($eventId, EventType::checkIn()); } public static function createLogs(?EventId $eventId = null): self { return new self($eventId, EventType::logs()); } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public static function createMetrics(?EventId $eventId = null): self { return new self($eventId, EventType::metrics()); } /** * Gets the ID of this event. */ public function getId(): EventId { return $this->id; } /** * Gets the identifier of the SDK package that generated this event. * * @internal */ public function getSdkIdentifier(): string { return $this->sdkIdentifier; } /** * Sets the identifier of the SDK package that generated this event. * * @internal */ public function setSdkIdentifier(string $sdkIdentifier): self { $this->sdkIdentifier = $sdkIdentifier; return $this; } /** * Gets the version of the SDK package that generated this Event. * * @internal */ public function getSdkVersion(): string { return $this->sdkVersion; } /** * Sets the version of the SDK package that generated this Event. * * @internal */ public function setSdkVersion(string $sdkVersion): self { $this->sdkVersion = $sdkVersion; return $this; } /** * Append a package to the list of SDK packages. * * @param SdkPackageEntry $package The package to append * * @return $this * * @internal */ public function appendSdkPackage(array $package): self { $this->sdkPackages[] = $package; return $this; } /** * Gets the SDK playload that will be sent to Sentry. * * @see https://develop.sentry.dev/sdk/data-model/event-payloads/sdk/ * * @return array{name: string, version: string, packages: SdkPackageEntry[]} * * @internal */ public function getSdkPayload(): array { return [ 'name' => $this->sdkIdentifier, 'version' => $this->sdkVersion, 'packages' => $this->sdkPackages, ]; } /** * Gets the timestamp of when this event was generated. */ public function getTimestamp(): ?float { return $this->timestamp; } /** * Sets the timestamp of when the Event was created. */ public function setTimestamp(?float $timestamp): self { $this->timestamp = $timestamp; return $this; } /** * Gets the severity of this event. */ public function getLevel(): ?Severity { return $this->level; } /** * Sets the severity of this event. * * @param Severity|null $level The severity */ public function setLevel(?Severity $level): self { $this->level = $level; return $this; } /** * Gets the name of the logger which created the event. */ public function getLogger(): ?string { return $this->logger; } /** * Sets the name of the logger which created the event. * * @param string|null $logger The logger name */ public function setLogger(?string $logger): self { $this->logger = $logger; return $this; } /** * Gets the name of the transaction (or culprit) which caused this * exception. */ public function getTransaction(): ?string { return $this->transaction; } /** * Sets the name of the transaction (or culprit) which caused this * exception. * * @param string|null $transaction The transaction name */ public function setTransaction(?string $transaction): self { $this->transaction = $transaction; return $this; } public function getCheckIn(): ?CheckIn { return $this->checkIn; } public function setCheckIn(?CheckIn $checkIn): self { $this->checkIn = $checkIn; return $this; } /** * @return Log[] */ public function getLogs(): array { return $this->logs; } /** * @param Log[] $logs */ public function setLogs(array $logs): self { $this->logs = $logs; return $this; } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function getMetrics(): array { return []; } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function setMetrics(array $metrics): self { return $this; } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function getMetricsSummary(): array { return []; } /** * @deprecated Metrics are no longer supported. Metrics API is a no-op and will be removed in 5.x. */ public function setMetricsSummary(array $metricsSummary): self { return $this; } /** * Gets the name of the server. */ public function getServerName(): ?string { return $this->serverName; } /** * Sets the name of the server. * * @param string|null $serverName The server name */ public function setServerName(?string $serverName): self { $this->serverName = $serverName; return $this; } /** * Gets the release of the program. */ public function getRelease(): ?string { return $this->release; } /** * Sets the release of the program. * * @param string|null $release The release */ public function setRelease(?string $release): self { $this->release = $release; return $this; } /** * Gets the error message. */ public function getMessage(): ?string { return $this->message; } /** * Gets the formatted message. */ public function getMessageFormatted(): ?string { return $this->messageFormatted; } /** * Gets the parameters to use to format the message. * * @return string[] */ public function getMessageParams(): array { return $this->messageParams; } /** * Sets the error message. * * @param string $message The message * @param string[] $params The parameters to use to format the message * @param string|null $formatted The formatted message */ public function setMessage(string $message, array $params = [], ?string $formatted = null): self { $this->message = $message; $this->messageParams = $params; $this->messageFormatted = $formatted; return $this; } /** * Gets a list of relevant modules and their versions. * * @return array */ public function getModules(): array { return $this->modules; } /** * Sets a list of relevant modules and their versions. * * @param array $modules */ public function setModules(array $modules): self { $this->modules = $modules; return $this; } /** * Gets the request data. * * @return array */ public function getRequest(): array { return $this->request; } /** * Sets the request data. * * @param array $request The request data */ public function setRequest(array $request): self { $this->request = $request; return $this; } /** * Gets an arbitrary mapping of additional contexts. * * @return array> */ public function getContexts(): array { return $this->contexts; } /** * Sets data to the context by a given name. * * @param string $name The name that uniquely identifies the context * @param array $data The data of the context */ public function setContext(string $name, array $data): self { if (!empty($data)) { $this->contexts[$name] = $data; } return $this; } /** * Gets an arbitrary mapping of additional metadata. * * @return array */ public function getExtra(): array { return $this->extra; } /** * Sets an arbitrary mapping of additional metadata. * * @param array $extra The context object */ public function setExtra(array $extra): self { $this->extra = $extra; return $this; } /** * Gets a list of tags associated to this event. * * @return array */ public function getTags(): array { return $this->tags; } /** * Sets a list of tags associated to this event. * * @param array $tags The tags to set */ public function setTags(array $tags): self { $this->tags = $tags; return $this; } /** * Sets or updates a tag in this event. * * @param string $key The key that uniquely identifies the tag * @param string $value The value */ public function setTag(string $key, string $value): self { $this->tags[$key] = $value; return $this; } /** * Removes a given tag from the event. * * @param string $key The key that uniquely identifies the tag */ public function removeTag(string $key): self { unset($this->tags[$key]); return $this; } /** * Gets the user context. */ public function getUser(): ?UserDataBag { return $this->user; } /** * Sets the user context. * * @param UserDataBag|null $user The context object */ public function setUser(?UserDataBag $user): self { $this->user = $user; return $this; } /** * Gets the server OS context. */ public function getOsContext(): ?OsContext { return $this->osContext; } /** * Sets the server OS context. * * @param OsContext|null $osContext The context object */ public function setOsContext(?OsContext $osContext): self { $this->osContext = $osContext; return $this; } /** * Gets the runtime context data. */ public function getRuntimeContext(): ?RuntimeContext { return $this->runtimeContext; } /** * Sets the runtime context data. * * @param RuntimeContext|null $runtimeContext The context object */ public function setRuntimeContext(?RuntimeContext $runtimeContext): self { $this->runtimeContext = $runtimeContext; return $this; } /** * Gets an array of strings used to dictate the deduplication of this * event. * * @return string[] */ public function getFingerprint(): array { return $this->fingerprint; } /** * Sets an array of strings used to dictate the deduplication of this * event. * * @param string[] $fingerprint The strings */ public function setFingerprint(array $fingerprint): self { $this->fingerprint = $fingerprint; return $this; } /** * Gets the environment in which this event was generated. */ public function getEnvironment(): ?string { return $this->environment; } /** * Sets the environment in which this event was generated. * * @param string|null $environment The name of the environment */ public function setEnvironment(?string $environment): self { $this->environment = $environment; return $this; } /** * Gets the breadcrumbs. * * @return Breadcrumb[] */ public function getBreadcrumbs(): array { return $this->breadcrumbs; } /** * Set new breadcrumbs to the event. * * @param Breadcrumb[] $breadcrumbs The breadcrumb array */ public function setBreadcrumb(array $breadcrumbs): self { $this->breadcrumbs = $breadcrumbs; return $this; } /** * Gets the exception. * * @return ExceptionDataBag[] */ public function getExceptions(): array { return $this->exceptions; } /** * Sets the exceptions. * * @param ExceptionDataBag[] $exceptions The exceptions */ public function setExceptions(array $exceptions): self { foreach ($exceptions as $exception) { if (!$exception instanceof ExceptionDataBag) { throw new \UnexpectedValueException(\sprintf('Expected an instance of the "%s" class. Got: "%s".', ExceptionDataBag::class, get_debug_type($exception))); } } $this->exceptions = $exceptions; return $this; } /** * Gets the stacktrace that generated this event. */ public function getStacktrace(): ?Stacktrace { return $this->stacktrace; } /** * Sets the stacktrace that generated this event. * * @param Stacktrace|null $stacktrace The stacktrace instance */ public function setStacktrace(?Stacktrace $stacktrace): self { $this->stacktrace = $stacktrace; return $this; } public function getType(): EventType { return $this->type; } /** * Sets the SDK metadata with the given name. * * @param string $name The name that uniquely identifies the SDK metadata * @param mixed $data The data of the SDK metadata */ public function setSdkMetadata(string $name, $data): self { $this->sdkMetadata[$name] = $data; return $this; } /** * Gets the SDK metadata. * * @psalm-template T of string|null * * @psalm-param T $name * * @return mixed * * @psalm-return (T is string ? mixed : array|null) */ public function getSdkMetadata(?string $name = null) { if ($name !== null) { return $this->sdkMetadata[$name] ?? null; } return $this->sdkMetadata; } /** * Gets a timestamp representing when the measuring of a transaction started. */ public function getStartTimestamp(): ?float { return $this->startTimestamp; } /** * Sets a timestamp representing when the measuring of a transaction started. * * @param float|null $startTimestamp The start time of the measurement */ public function setStartTimestamp(?float $startTimestamp): self { $this->startTimestamp = $startTimestamp; return $this; } /** * A list of timed application events that have a start and end time. * * @return Span[] */ public function getSpans(): array { return $this->spans; } /** * Sets a list of timed application events that have a start and end time. * * @param Span[] $spans The list of spans */ public function setSpans(array $spans): self { $this->spans = $spans; return $this; } public function getProfile(): ?Profile { return $this->profile; } public function setProfile(?Profile $profile): self { $this->profile = $profile; return $this; } public function getTraceId(): ?string { $traceId = $this->getContexts()['trace']['trace_id']; if (\is_string($traceId) && !empty($traceId)) { return $traceId; } return null; } } PKî%W]¢iè³³sentry/src/UserDataBag.phpnu„[µü¤ Additional data */ private $metadata = []; /** * UserDataBag constructor. * * @param string|int|null $id */ public function __construct( $id = null, ?string $email = null, ?string $ipAddress = null, ?string $username = null, ?string $segment = null ) { $this->setId($id); $this->setEmail($email); $this->setIpAddress($ipAddress); $this->setUsername($username); $this->setSegment($segment); } /** * Creates an instance of this object from a user ID. * * @param string|int $id The ID of the user */ public static function createFromUserIdentifier($id): self { return new self($id); } /** * Creates an instance of this object from an IP address. * * @param string $ipAddress The IP address of the user */ public static function createFromUserIpAddress(string $ipAddress): self { return new self(null, null, $ipAddress); } /** * Creates an instance of this object from the given data. * * @param array $data The raw data */ public static function createFromArray(array $data): self { $instance = new self(); foreach ($data as $field => $value) { switch ($field) { case 'id': $instance->setId($value); break; case 'ip_address': $instance->setIpAddress($value); break; case 'email': $instance->setEmail($value); break; case 'username': $instance->setUsername($value); break; case 'segment': $instance->setSegment($value); break; default: $instance->setMetadata($field, $value); break; } } return $instance; } /** * Gets the ID of the user. * * @return string|int|null */ public function getId() { return $this->id; } /** * Sets the ID of the user. * * @param string|int|null $id The ID */ public function setId($id): self { if ($id !== null && !\is_string($id) && !\is_int($id)) { throw new \UnexpectedValueException(\sprintf('Expected an integer or string value for the $id argument. Got: "%s".', get_debug_type($id))); } $this->id = $id; return $this; } /** * Gets the username of the user. */ public function getUsername(): ?string { return $this->username; } /** * Sets the username of the user. * * @param string|null $username The username */ public function setUsername(?string $username): self { $this->username = $username; return $this; } /** * Gets the email of the user. */ public function getEmail(): ?string { return $this->email; } /** * Sets the email of the user. * * @param string|null $email The email */ public function setEmail(?string $email): self { $this->email = $email; return $this; } /** * Gets the segement of the user. * * @deprecated since version 4.4. To be removed in version 5.0 */ public function getSegment(): ?string { return $this->segment; } /** * Sets the segment of the user. * * @param string|null $segment The segment * * @deprecated since version 4.4. To be removed in version 5.0. You may use a custom tag or context instead. */ public function setSegment(?string $segment): self { $this->segment = $segment; return $this; } /** * Gets the ip address of the user. */ public function getIpAddress(): ?string { return $this->ipAddress; } /** * Sets the ip address of the user. * * @param string|null $ipAddress The ip address */ public function setIpAddress(?string $ipAddress): self { if ($ipAddress !== null && filter_var($ipAddress, \FILTER_VALIDATE_IP) === false) { throw new \InvalidArgumentException(\sprintf('The "%s" value is not a valid IP address.', $ipAddress)); } $this->ipAddress = $ipAddress; return $this; } /** * Gets additional metadata. * * @return array */ public function getMetadata(): array { return $this->metadata; } /** * Sets the given field in the additional metadata. * * @param string $name The name of the field * @param mixed $value The value */ public function setMetadata(string $name, $value): self { $this->metadata[$name] = $value; return $this; } /** * Removes the given field from the additional metadata. * * @param string $name The name of the field */ public function removeMetadata(string $name): self { unset($this->metadata[$name]); return $this; } /** * Merges the given context with this one. * * @param UserDataBag $other The context to merge the data with * * @return $this */ public function merge(self $other): self { $this->id = $other->id; $this->email = $other->email; $this->ipAddress = $other->ipAddress; $this->username = $other->username; $this->segment = $other->segment; $this->metadata = array_merge($this->metadata, $other->metadata); return $this; } } PKî%W]û«:ÓÓsentry/src/Util/Str.phpnu„[µü¤ $values */ public static function vsprintfOrNull(string $message, array $values): ?string { if (empty($values)) { return $message; } foreach ($values as $value) { // If the value is not a scalar or null, we cannot safely format it if (!\is_scalar($value) && $value !== null) { return null; } } try { $result = @vsprintf($message, $values); // @phpstan-ignore-next-line on PHP 7 `vsprintf` does not throw an exception but can return `false` return $result === false ? null : $result; } catch (\Error $e) { // This is technically a `ValueError` in PHP 8.0+ but this works in PHP 7 as well return null; } } } PKî%W]uÕM^^^sentry/src/Util/PHPVersion.phpnu„[µü¤\d\.\d\.\d{1,2})(?-(beta|rc)-?(\d+)?(-dev)?)?/i'; /** * Parses the given string representing a PHP version and returns it in a * normalized form. * * @param string $version The string to parse */ public static function parseVersion(string $version = \PHP_VERSION): string { if (!preg_match(self::VERSION_PARSING_REGEX, $version, $matches)) { return $version; } $version = $matches['base']; if (isset($matches['extra'])) { $version .= $matches['extra']; } return $version; } } PKî%W]ÚdÐjrr"sentry/src/Util/PrefixStripper.phpnu„[µü¤getPrefixes() as $prefix) { if (mb_substr($filePath, 0, mb_strlen($prefix)) === $prefix) { return mb_substr($filePath, mb_strlen($prefix)); } } return $filePath; } } PKî%W]yÍíKöösentry/src/Util/Http.phpnu„[µü¤getPublicKey(), ]; return [ 'Content-Type: application/x-sentry-envelope', 'X-Sentry-Auth: Sentry ' . implode(', ', $authHeader), ]; } /** * @param string[][] $headers * * @param-out string[][] $headers */ public static function parseResponseHeaders(string $headerLine, array &$headers): int { if (strpos($headerLine, ':') === false) { return \strlen($headerLine); } [$name, $value] = explode(':', trim($headerLine), 2); $name = trim($name); $value = trim($value); if (isset($headers[$name])) { $headers[$name][] = $value; } else { $headers[$name] = (array) $value; } return \strlen($headerLine); } } PKî%W]š×ô˜˜sentry/src/Util/ClockMock.phpnu„[µü¤ * @author Dominic Tubach */ class ClockMock { private static $now; public static function withClockMock($enable = null): ?bool { if ($enable === null) { return self::$now !== null; } self::$now = is_numeric($enable) ? (float) $enable : ($enable ? microtime(true) : null); return null; } public static function time(): int { if (self::$now === null) { return time(); } return (int) self::$now; } public static function sleep($s): int { if (self::$now === null) { return sleep($s); } self::$now += (int) $s; return 0; } public static function usleep($us): void { if (self::$now === null) { usleep($us); } else { self::$now += $us / 1000000; } } /** * @return string|float */ public static function microtime($asFloat = false) { if (self::$now === null) { return microtime($asFloat); } if ($asFloat) { return self::$now; } return \sprintf('%0.6f00 %d', self::$now - (int) self::$now, (int) self::$now); } public static function date($format, $timestamp = null): string { if ($timestamp === null) { $timestamp = self::time(); } return date($format, $timestamp); } public static function gmdate($format, $timestamp = null): string { if ($timestamp === null) { $timestamp = self::time(); } return gmdate($format, $timestamp); } /** * @return array|int|float */ public static function hrtime($asNumber = false) { $ns = (self::$now - (int) self::$now) * 1000000000; if ($asNumber) { $number = \sprintf('%d%d', (int) self::$now, $ns); return \PHP_INT_SIZE === 8 ? (int) $number : (float) $number; } return [(int) self::$now, (int) $ns]; } /** * @return false|int */ public static function strtotime(string $datetime, ?int $timestamp = null) { if ($timestamp === null) { $timestamp = self::time(); } return strtotime($datetime, $timestamp); } public static function register($class): void { $self = static::class; $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; if (strpos($class, '\\Tests\\') > 0) { $ns = str_replace('\\Tests\\', '\\', $class); $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); } elseif (strpos($class, 'Tests\\') === 0) { $mockedNs[] = substr($class, 6, strrpos($class, '\\') - 6); } foreach ($mockedNs as $ns) { if (\function_exists($ns . '\time')) { continue; } eval(<< $array * * @return array */ public static function simpleDot(array $array): array { $results = []; $flatten = static function ($data, $prefix = '') use (&$results, &$flatten): void { foreach ($data as $key => $value) { $newKey = $prefix . $key; if (\is_array($value) && !empty($value) && !self::isList($value)) { $flatten($value, $newKey . '.'); } else { $results[$newKey] = $value; } } }; $flatten($array); return $results; } /** * Checks whether a given array is a list. * * `array_is_list` is introduced in PHP 8.1, so we have a polyfill for it. * * @see https://www.php.net/manual/en/function.array-is-list.php#126794 * * @param array $array */ public static function isList(array $array): bool { $i = 0; foreach ($array as $k => $v) { if ($k !== $i++) { return false; } } return true; } } PKî%W]æ“¢ ¹¹$sentry/src/Util/PHPConfiguration.phpnu„[µü¤ * * @internal */ final class JSON { /** * Encodes the given data into JSON. * * @param mixed $data The data to encode * @param int $options Bitmask consisting of JSON_* constants * @param int $maxDepth The maximum depth allowed for serializing $data * * @throws JsonException If the encoding failed */ public static function encode($data, int $options = 0, int $maxDepth = 512): string { if ($maxDepth < 1) { throw new \InvalidArgumentException('The $maxDepth argument must be an integer greater than 0.'); } $options |= \JSON_UNESCAPED_UNICODE | \JSON_INVALID_UTF8_SUBSTITUTE | \JSON_PARTIAL_OUTPUT_ON_ERROR; $encodedData = json_encode($data, $options, $maxDepth); $allowedErrors = [\JSON_ERROR_NONE, \JSON_ERROR_RECURSION, \JSON_ERROR_INF_OR_NAN, \JSON_ERROR_UNSUPPORTED_TYPE]; if (\defined('JSON_ERROR_NON_BACKED_ENUM')) { $allowedErrors[] = \JSON_ERROR_NON_BACKED_ENUM; } $encounteredAnyError = json_last_error() !== \JSON_ERROR_NONE; if (($encounteredAnyError && ($encodedData === 'null' || $encodedData === false)) || !\in_array(json_last_error(), $allowedErrors, true)) { throw new JsonException(\sprintf('Could not encode value into JSON format. Error was: "%s".', json_last_error_msg())); } return $encodedData; } /** * Decodes the given data from JSON. * * @param string $data The data to decode * * @return mixed * * @throws JsonException If the decoding failed */ public static function decode(string $data) { $decodedData = json_decode($data, true); if (json_last_error() !== \JSON_ERROR_NONE) { throw new JsonException(\sprintf('Could not decode value from JSON format. Error was: "%s".', json_last_error_msg())); } return $decodedData; } } PKî%W]J„��ÁÁsentry/src/Util/SentryUid.phpnu„[µü¤ */ final class Frame { public const INTERNAL_FRAME_FILENAME = '[internal]'; /** * @deprecated This constant is deprecated and will be removed in 5.x. */ public const ANONYMOUS_CLASS_PREFIX = "class@anonymous\x00"; /** * @var string|null The name of the function being called */ private $functionName; /** * @var string|null The original function name, if the function name is * shortened or demangled */ private $rawFunctionName; /** * @var string The file where the frame originated */ private $file; /** * @var string|null The absolute path to the source file */ private $absoluteFilePath; /** * @var int The line at which the frame originated */ private $line; /** * @var string[] A list of source code lines before the one where the frame * originated */ private $preContext = []; /** * @var string|null The source code written at the line number of the file that * originated this frame */ private $contextLine; /** * @var string[] A list of source code lines after the one where the frame * originated */ private $postContext = []; /** * @var bool Flag telling whether the frame is related to the execution of * the relevant code in this stacktrace */ private $inApp; /** * @var array A mapping of variables which were available within * this frame (usually context-locals) */ private $vars = []; /** * Initializes a new instance of this class using the provided information. * * @param string|null $functionName The name of the function being called * @param string $file The file where the frame originated * @param string|null $rawFunctionName The original function name, if the function * name is shortened or demangled * @param string|null $absoluteFilePath The absolute path to the source file * @param int $line The line at which the frame originated * @param array $vars A mapping of variables which were available * within the frame * @param bool $inApp Whether the frame is related to the * execution of code relevant to the * application */ public function __construct(?string $functionName, string $file, int $line, ?string $rawFunctionName = null, ?string $absoluteFilePath = null, array $vars = [], bool $inApp = true) { $this->functionName = $functionName; $this->file = $file; $this->line = $line; $this->rawFunctionName = $rawFunctionName; $this->absoluteFilePath = $absoluteFilePath; $this->vars = $vars; $this->inApp = $inApp; } /** * Gets the name of the function being called. */ public function getFunctionName(): ?string { return $this->functionName; } /** * Gets the original function name, if the function name is shortened or * demangled. */ public function getRawFunctionName(): ?string { return $this->rawFunctionName; } /** * Gets the file where the frame originated. */ public function getFile(): string { return $this->file; } /** * Gets the absolute path to the source file. */ public function getAbsoluteFilePath(): ?string { return $this->absoluteFilePath; } /** * Gets the line at which the frame originated. */ public function getLine(): int { return $this->line; } /** * Gets a list of source code lines before the one where the frame originated. * * @return string[] */ public function getPreContext(): array { return $this->preContext; } /** * Sets a list of source code lines before the one where the frame originated. * * @param string[] $preContext The source code lines */ public function setPreContext(array $preContext): self { $this->preContext = $preContext; return $this; } /** * Gets the source code written at the line number of the file that originated * this frame. */ public function getContextLine(): ?string { return $this->contextLine; } /** * Sets the source code written at the line number of the file that originated * this frame. * * @param string|null $contextLine The source code line */ public function setContextLine(?string $contextLine): self { $this->contextLine = $contextLine; return $this; } /** * Gets a list of source code lines after the one where the frame originated. * * @return string[] */ public function getPostContext(): array { return $this->postContext; } /** * Sets a list of source code lines after the one where the frame originated. * * @param string[] $postContext The source code lines */ public function setPostContext(array $postContext): self { $this->postContext = $postContext; return $this; } /** * Gets whether the frame is related to the execution of the relevant code * in this stacktrace. */ public function isInApp(): bool { return $this->inApp; } /** * Sets whether the frame is related to the execution of the relevant code * in this stacktrace. * * @param bool $inApp flag indicating whether the frame is application-related */ public function setIsInApp(bool $inApp): self { $this->inApp = $inApp; return $this; } /** * Gets a mapping of variables which were available within this frame * (usually context-locals). * * @return array */ public function getVars(): array { return $this->vars; } /** * Sets a mapping of variables which were available within this frame * (usually context-locals). * * @param array $vars The variables */ public function setVars(array $vars): self { $this->vars = $vars; return $this; } /** * Gets whether the frame is internal. */ public function isInternal(): bool { return $this->file === self::INTERNAL_FRAME_FILENAME; } } PKî%W].GÇÓÓsentry/UPGRADE-3.0.mdnu„[µü¤# Upgrade 2.x to 3.0 - Removed the `HubInterface::getCurrentHub()` and `HubInterface::setCurrentHub()` methods. Use `SentrySdk::getCurrentHub()` and `SentrySdk::setCurrentHub()` instead - Removed the `ErrorHandler::registerOnce()` method, use `ErrorHandler::register*Handler()` instead - Removed the `ErrorHandler::addErrorListener` method, use `ErrorHandler::addErrorHandlerListener()` instead - Removed the `ErrorHandler::addFatalErrorListener` method, use `ErrorHandler::addFatalErrorHandlerListener()` instead - Removed the `ErrorHandler::addExceptionListener` method, use `ErrorHandler::addExceptionHandlerListener()` instead - The signature of the `ErrorListenerIntegration::__construct()` method changed to not accept any parameter - The signature of the `FatalErrorListenerIntegration::__construct()` method changed to not accept any parameter - The `ErrorListenerIntegration` integration does not get called anymore when a fatal error occurs - The default value of the `error_types` option changed to the value get from `error_reporting()` - The signature of the `capture*()` global functions changed to return an instance of the `EventId` class instead of a `string` - The signature of the `ClientInterface::capture*()` methods changed to return an instance of the `EventId` class instead of a `string` - The signature of the `HubInterface::capture*e()` methods changed to return an instance of the `EventId` class instead of a `string` - The signature of the `Event::getId()` method changed to return an instance of the `EventId` class instead of a `string` - The signature of the `Options::getDsn()` method changed to always return an instance of the `Dsn` class instead of a `string` - Removed the `Options::getProjectId`, `Options::getPublicKey` and `Options::getSecretKey` methods, use `Options::getDsn()` instead - Removed the `Breadcrumb::LEVEL_CRITICAL` constant, use `Breadcrumb::LEVEL_FATAL` instead - Removed the `Breadcrumb::levelFromErrorException()` method - Removed the `PluggableHttpClientFactory` class - Removed the following methods from the `ClientBuilderInterface` interface, use `ClientBuilderInterface::setTransportFactory()` instead: - `ClientBuilderInterface::setUriFactory()` - `ClientBuilderInterface::setMessageFactory()` - `ClientBuilderInterface::setTransport()` - `ClientBuilderInterface::setHttpClient()` - `ClientBuilderInterface::addHttpClientPlugin()` - `ClientBuilderInterface::removeHttpClientPlugin()`. - Removed the following methods from the `Options` class, use the `IgnoreErrorsIntegration` integration instead: - `Options::getExcludedExceptions()` - `Options::setExcludedExceptions()` - `Options::isExcludedException()` - `Options::getProjectRoot()` - `Options::setProjectRoot()` - Removed the `Context::CONTEXT_USER`, `Context::CONTEXT_RUNTIME`, `Context::CONTEXT_TAGS`, `Context::CONTEXT_EXTRA`, `Context::CONTEXT_SERVER_OS` constants - The signature of the `Scope::setUser()` method changed to `setUser(array|Sentry\UserDataBag $user)` - The signature of the `TransportInterface::send()` method changed to return a promise instead of the event ID - The signature of the `HttpClientFactory::__construct()` method changed to accept instances of the PSR-17 factories in place of Httplug's ones - The signature of the `DefaultTransportFactory::__construct()` method changed to accept instances of the PSR-17 factories in place of Httplug's ones - The signature of the `GzipEncoderPlugin::__construct()` method changed to accept an instance of the `Psr\Http\Message\StreamFactoryInterface` interface only - The Monolog handler does not set anymore the tags and extras on the event by extracting automatically the data from the record payload. You can decorate the class and set such data on the scope as shown below: ```php use Monolog\Handler\HandlerInterface; use Sentry\State\Scope; use function Sentry\withScope; final class MonologHandler implements HandlerInterface { private $decoratedHandler; public function __construct(HandlerInterface $decoratedHandler) { $this->decoratedHandler = $decoratedHandler; } public function isHandling(array $record): bool { return $this->decoratedHandler->isHandling($record); } public function handle(array $record): bool { $result = false; withScope(function (Scope $scope) use ($record, &$result): void { $scope->setTags(...); $scope->setExtras(...); $result = $this->decoratedHandler->handle($record); }); return $result; } public function handleBatch(array $records): void { $this->decoratedHandler->handleBatch($records); } public function close(): void { $this->decoratedHandler->close(); } } ``` - Removed the `TagsContext`, `ExtraContext` and `Context` classes, data is now stored in a plain array - Renamed the `ServerOsContext` class to `OsContext` - The `OsContext` and `RuntimeContext` classes do not implement anymore the `ArrayAccess`, `IteratorAggregate` and `JsonSerializable` interfaces and became `final` - The following methods have been removed from the `OsContext` and `RuntimeContext` classes: - `*Context::merge()` - `*Context::setData()` - `*Context::replaceData()` - `*Context::clear()` - `*Context::isEmpty()` - `*Context::toArray()` - Removed the `UserContext` class, use `UserDataBag` instead - The signature of the constructor of the `RuntimeContext` class changed to `RuntimeContext::__construct(string $name, ?string $version = null)` - The signature of the constructor of the `OsContext` class changed to `OsContext::__construct(string $name, ?string $version = null, ?string $build = null, ?string $kernelVersion = null)` - Removed the `Event::getExtraContext()` method, use `Event::getExtra()` instead - Removed the `Event::getTagsContext()` method, use `Event::getTags()` instead - Removed the `Event::getUserContext()` method, use `Event::getUser()` instead - Renamed the `Event::getServerOsContext()` method to `Event::getOsContext()` - The signature of the `Scope::setUser()` method changed to accept a plain array - Removed the `FlushableClientInterface` and `ClosableTransportInterface` interfaces. Their methods have been moved to the corresponding `ClientInterface` and `TransportInterface` interfaces - Removed the `Event::toArray()` and `Event::jsonSerialize()` methods, use `PayloadSerializerInterface::serialize()` instead - Removed the `Breadcrumb::toArray()` and `Breadcrumb::jsonSerialize()` methods - Removed the `Frame::toArray()` and `Frame::jsonSerialize()` methods - Removed the `Stacktrace::toArray()` and `Stacktrace::jsonSerialize()` methods - Removed the `SpoolTransport` class and the `SpoolInterface` interface with related implementation - Made the `Event::__construct()` method `private`, use the named constructors instead - The signature of `ClientInterface::captureEvent()` changed to `ClientInterface::captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null)` - The signature of `HubInterface::captureEvent()` changed to `HubInterface::captureEvent(Event $event, ?EventHint $hint = null)` - The signature of `captureEvent()` changed to `captureEvent(Event $event, ?EventHint $hint = null)` - The signature of `Scope::applyToEvent()` changed to `Scope::applyToEvent(Event $event, ?EventHint $hint = null)` - Global and scope event processors will now receive a `EventHint` as the second parameter, callable should now have the signature `callable(Event $event, EventHint $hint)` PKî%W]9!1Š [ [sentry/CHANGELOG.mdnu„[µü¤# CHANGELOG ## 4.17.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.17.1. ### Misc - Call `curl_close` only on PHP version 7.4 and below to prevent deprecation warnings. [(#1947)](https://github.com/getsentry/sentry-php/pull/1947) ## 4.17.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.17.0. ### Bug Fixes - Empty strings will no longer display `` when serialized. [(#1940)](https://github.com/getsentry/sentry-php/pull/1940) ### Misc - Remove `symfony/phpunit-bridge` as a dev dependency. [(#1930)](https://github.com/getsentry/sentry-php/pull/1930) - Update `sentry.origin` to be consistent with other SDKs. [(#1938)](https://github.com/getsentry/sentry-php/pull/1938) ## 4.16.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.16.0. ### Features - Remove `max_breadcrumbs` limit. [(#1890)](https://github.com/getsentry/sentry-php/pull/1890) - Implement `__destruct` in `LogsHandler` to make sure logs are always flushed. [(#1916)](https://github.com/getsentry/sentry-php/pull/1916) ### Bug Fixes - Use PSR log level when logging messages using the PSR-3 logger within the SDK. [(#1907)](https://github.com/getsentry/sentry-php/pull/1907) - Remove `@internal` annotation from `Sentry\Transport\Result`. [(#1904)](https://github.com/getsentry/sentry-php/pull/1904) ### Misc - Add `sentry.origin` attribute to `LogsHandler`. [(#1917)](https://github.com/getsentry/sentry-php/pull/1917) ## 4.15.2 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.2. ### Bug Fixes - Ensure the Monolog handler only processes records permitted by their log level. [(#1888)](https://github.com/getsentry/sentry-php/pull/1888) ## 4.15.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.1. ### Bug Fixes - Do not send `template` attribute with logs when there are no template values [(#1885)](https://github.com/getsentry/sentry-php/pull/1885) ## 4.15.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.15.0. ### Features - Add Monolog Sentry Logs handler [(#1867)](https://github.com/getsentry/sentry-php/pull/1867) This new handler allows you to capture Monolog logs as Sentry logs. To use it, configure your Monolog logger: ```php use Monolog\Logger; use Sentry\Monolog\LogsHandler; use Sentry\Logs\LogLevel; // Initialize Sentry SDK first (make sure 'enable_logs' is set to true) \Sentry\init([ 'dsn' => '__YOUR_DSN__', 'enable_logs' => true, ]); // Create a Monolog logger $logger = new Logger('my-app'); // Add the Sentry logs handler // Optional: specify minimum log level (defaults to LogLevel::debug()) $handler = new LogsHandler(LogLevel::info()); $logger->pushHandler($handler); // Now your logs will be sent to Sentry $logger->info('User logged in', ['user_id' => 123]); $logger->error('Payment failed', ['order_id' => 456]); ``` Note: The handler will not collect logs for exceptions (they should be handled separately via `captureException`). ### Bug Fixes - Fix non string indexed attributes passed as log attributes [(#1882)](https://github.com/getsentry/sentry-php/pull/1882) - Use correct `sample_rate` key when deriving sampleRand [(#1874)](https://github.com/getsentry/sentry-php/pull/1874) - Do not call `Reflection*::setAccessible()` in PHP >= 8.1 [(#1872)](https://github.com/getsentry/sentry-php/pull/1872) ## 4.14.2 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.14.2. ### Bug Fixes - Add missing sample rates in the envelope header [(#1870)](https://github.com/getsentry/sentry-php/pull/1870) ## 4.14.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.14.1. ### Bug Fixes - Fix missing user attributes on logs [(#1864)](https://github.com/getsentry/sentry-php/pull/1864) ## 4.14.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.14.0. ### Features - Serialize enum variants with the variant name [(#1860)](https://github.com/getsentry/sentry-php/pull/1860) ### Bug Fixes - Fix handling of backtrace frames [(#1862)](https://github.com/getsentry/sentry-php/pull/1862) - Set allowed types for `http_ssl_native_ca` [(#1858)](https://github.com/getsentry/sentry-php/pull/1858) ## 4.13.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.13.0. ### Features - Add regex support for `ignore_exceptions` and `ignore_transactions` [(#1850)](https://github.com/getsentry/sentry-php/pull/1850) You can now use regular expressions to ignore exceptions and transactions: ```php Sentry\init([ 'ignore_exceptions' => [ '/.*ArgumentException$/', ], 'ignore_transactions' => [ '/^GET \/api\/users\/\d+$/', ], ]); ``` - Add support for variadic parameters and null values [(#1849)](https://github.com/getsentry/sentry-php/pull/1849) ### Bug Fixes - Fix `Options::setEnableLogs` [(#1852)](https://github.com/getsentry/sentry-php/pull/1852) - Fix `vsprintf` not handling errors [(#1855)](https://github.com/getsentry/sentry-php/pull/1855) ## 4.12.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.12.0. ### Features - Add support for Sentry Structured Logs [(#1813)](https://github.com/getsentry/sentry-php/pull/1813) You can now send logs directly to Sentry using the new logging API: ```php Sentry\init([ // Enable logs to be sent to Sentry 'enable_logs' => true, ]); ``` ```php use function Sentry\logger; // Log messages at different levels logger()->info('User logged in', ['user_id' => 123]); logger()->warn('Deprecated function used', ['function' => 'old_function']); logger()->error('Database connection failed', ['host' => 'db.example.com']); logger()->fatal('Critical system failure: %s', ['Out of memory'], ['component' => 'database']); // Flush logs to Sentry logger()->flush(); // We recommend registering the flushing in a shutdown function register_shutdown_function(static fn () => logger()->flush()); ``` To learn more, head over to our [docs](https://docs.sentry.io/platforms/php/logs/). ### Bug Fixes - Log correct source of sampling decision [(#1836)](https://github.com/getsentry/sentry-php/pull/1836) ## 4.11.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.11.1. ### Bug Fixes - Fix stripping prefixes from closure frames for PHP 8.4 and up [(#1828)](https://github.com/getsentry/sentry-php/pull/1828) ## 4.11.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.11.0. ### Features - Serialize `\DateTimeInterface` objects by default [(#1803)](https://github.com/getsentry/sentry-php/pull/1803) - Add support for [Propagated Ramdom Value](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value) [(#1793)](https://github.com/getsentry/sentry-php/pull/1793) - Use the `SENTRY_SPOTLIGHT` environment variable as the input for the `spotlight` configuration option [(#1789)](https://github.com/getsentry/sentry-php/pull/1789) ### Bug Fixes - Fix cases where anonymous stacktrace frames did not get their prefixes stripped [(#1820)](https://github.com/getsentry/sentry-php/pull/1820) - Fix Guzzle middleware not setting the span it created as the current span, which resulted in nesting issues in the trace view [(#1801)](https://github.com/getsentry/sentry-php/pull/1801) ## 4.10.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.10.0. ### Features - The SDK was updated to support PHP 8.4 [(#1760)](https://github.com/getsentry/sentry-php/pull/1760) - Expose a new `http_ssl_native_ca` option to tell the HTTP client to use the operating system's native CA store for certificate verification [(#1766)](https://github.com/getsentry/sentry-php/pull/1766) ### Bug Fixes - Fix the `http_timeout` & `http_connect_timeout` options, which now also work with sub second values [(#1785)](https://github.com/getsentry/sentry-php/pull/1785) ### Misc - HTTP breadcrumbs created by the `GuzzleTracingMiddleware` are now set to a warning status for `4xx` responses and an error status for `5xx` responses [(#1773)](https://github.com/getsentry/sentry-php/pull/1773) - All public Metrics APIs are now no-op, intneral APIs were removed [(#1786)](https://github.com/getsentry/sentry-php/pull/1786) ## 4.9.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.9.0. ### Features - Allow retrieving a single piece of data from the span by it's key [(#1767)](https://github.com/getsentry/sentry-php/pull/1767) ```php \Sentry\SentrySdk::getCurrentHub()->getSpan()?->setData([ 'failure' => $span->getData('failure', 0) + 1, ]); ``` - Add span trace origin [(#1769)](https://github.com/getsentry/sentry-php/pull/1769) ## 4.8.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.8.1. ### Bug Fixes - Guard against empty `REMOTE_ADDR` [(#1751)](https://github.com/getsentry/sentry-php/pull/1751) ## 4.8.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.8.0. ### Features - Add timing span when emiting a timing metric [(#1717)](https://github.com/getsentry/sentry-php/pull/1717) ```php use function Sentry\metrics; // This will now both emit a distribution metric and a span with the "expensive-operation" key metrics()->timing( key: 'expensive-operation', callback: fn() => doExpensiveOperation(), ); ``` ### Bug Fixes - Fix missing data on HTTP spans [(#1735)](https://github.com/getsentry/sentry-php/pull/1735) - Test span sampled status before creating child spans [(#1740)](https://github.com/getsentry/sentry-php/pull/1740) ### Misc - Implement fast path for ignoring errors [(#1737)](https://github.com/getsentry/sentry-php/pull/1737) - Add array shape for better autocomplete of `Sentry\init` function [(#1738)](https://github.com/getsentry/sentry-php/pull/1738) - Represent callable strings as strings [(#1741)](https://github.com/getsentry/sentry-php/pull/1741) - Use `AWS_LAMBDA_FUNCTION_VERSION` environment variable for release if available [(#1742)](https://github.com/getsentry/sentry-php/pull/1742) ## 4.7.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.7.0. ### Features - Improve debugging experience by emitting more logs from the SDK [(#1705)](https://github.com/getsentry/sentry-php/pull/1705) - Handle `metric_bucket` rate limits [(#1726)](https://github.com/getsentry/sentry-php/pull/1726) & [(#1728)](https://github.com/getsentry/sentry-php/pull/1728) ### Bug Fixes - Fix deprecation notice when trying to serialize a callable [(#1732)](https://github.com/getsentry/sentry-php/pull/1732) ### Misc - Deprecated `SpanStatus::resourceExchausted()`. Use `SpanStatus::resourceExhausted()` instead [(#1725)](https://github.com/getsentry/sentry-php/pull/1725) - Update metric normalization [(#1729)](https://github.com/getsentry/sentry-php/pull/1729) ## 4.6.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.6.1. ### Bug Fixes - Always add the sampled flag to the W3C `traceparent` header [(#1713)](https://github.com/getsentry/sentry-php/pull/1713) - Add `JSON_ERROR_NON_BACKED_ENUM` to allowed `JSON::encode()` errors. [(#1707)](https://github.com/getsentry/sentry-php/pull/1707) ## 4.6.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.6.0. ### Features - Add the PHP SAPI to the runtime context [(#1700)](https://github.com/getsentry/sentry-php/pull/1700) ### Bug Fixes - Correctly apply properties/options in `ClientBuilder::class` [(#1699)](https://github.com/getsentry/sentry-php/pull/1699) - Attach `_metrics_summary` to transactions [(#1702)](https://github.com/getsentry/sentry-php/pull/1702) ### Misc - Remove `final` from `Metrics::class` [(#1697)](https://github.com/getsentry/sentry-php/pull/1697) - Return early when using `ignore_exceptions` [(#1701)](https://github.com/getsentry/sentry-php/pull/1701) - Attach exceptions to the log message from `FrameContextifierIntegration::class` [(#1678)](https://github.com/getsentry/sentry-php/pull/1678) ## 4.5.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.5.0. ### Features - Add `before_send_check_in` and `before_send_metrics` [(#1690)](https://github.com/getsentry/sentry-php/pull/1690) ```php \Sentry\init([ 'before_send_check_in' => function (\Sentry\Event $event) { $checkIn = $event->getCheckIn(), // modify the check-in or return null to not send it }, ]); ``` ```php \Sentry\init([ 'before_send_metrics' => function (\Sentry\Event $event) { $metrics = $event->getMetrics(), // modify the metrics or return null to not send it }, ]); ``` ### Bug Fixes - Fix `_metrics_summary` formatting [(#1682)](https://github.com/getsentry/sentry-php/pull/1682) - Fix `DebugFileLogger` and `DebugStdOutLogger` to be usable with PHP 7.2 and up [(#1691)](https://github.com/getsentry/sentry-php/pull/1691) - Allow whitespace in metric tag values [(#1692)](https://github.com/getsentry/sentry-php/pull/1692) ## 4.4.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.4.0. ### Features - Add `metrics()->timing()` [(#1670)](https://github.com/getsentry/sentry-php/pull/1670) This allows you to emit a distribution metric based on the duration of the provided callback. ```php use function Sentry\metrics; metrics()->timing( key: 'my-metric', callback: fn() => doSomething(), ); ``` - Add `withMonitor()` [(#1679)](https://github.com/getsentry/sentry-php/pull/1679) This wraps a callback into monitor check-ins. ```php use function Sentry\withMonitor; withMonitor( slug: 'my-monitor', callback: fn () => doSomething(), monitorConfig: new MonitorConfig(...), ); ``` - Add new `failure_issue_threshold` and `recovery_threshold` configuration to `MonitorConfig` [(#1685)](https://github.com/getsentry/sentry-php/pull/1685) - Add `TransactionContext::make()` and `SpanContext::make()` [(#1684)](https://github.com/getsentry/sentry-php/pull/1684) ```php use Sentry\Tracing\SpanContext; $spanCpntext = SpanContext::make() ->setOp('http.client') ->setDescription('GET https://example.com') ``` - Add support for fluent use of `Transaction::setName()` [(#1687)](https://github.com/getsentry/sentry-php/pull/1687) - Add support for the W3C `traceparent` header [(#1680)](https://github.com/getsentry/sentry-php/pull/1680) ### Bug Fixes - Do not send an empty event if no metrics are in the bucket [(#1676)](https://github.com/getsentry/sentry-php/pull/1676) - Fix the `http_ssl_verify_peer` option to set the correct value to `CURLOPT_SSL_VERIFYPEER` [(#1686)](https://github.com/getsentry/sentry-php/pull/1686) ### Misc - Depreacted `UserDataBag::getSegment()` and `UserDataBag::setSegment()`. You may use a custom tag or context instead [(#1681)](https://github.com/getsentry/sentry-php/pull/1681) ## 4.3.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.3.1. ### Bug Fixes - Fix tags not being serialized correctly for metrics [(#1672)](https://github.com/getsentry/sentry-php/pull/1672) ### Misc - Remove `@internal` annotation from `MetricsUnit` class [(#1671)](https://github.com/getsentry/sentry-php/pull/1671) ## 4.3.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.3.0. ### Features - Add support for Sentry Developer Metrics [(#1619)](https://github.com/getsentry/sentry-php/pull/1619) ```php use function Sentry\metrics; // Add 4 to a counter named hits metrics()->increment(key: 'hits', value: 4); // Add 25 to a distribution named response_time with unit milliseconds metrics()->distribution(key: 'response_time', value: 25, unit: MetricsUnit::millisecond()); // Add 2 to gauge named parallel_requests, tagged with type: "a" metrics()->gauge(key: 'parallel_requests, value: 2, tags: ['type': 'a']); // Add a user's email to a set named users.sessions, tagged with role: "admin" metrics()->set('users.sessions, 'jane.doe@example.com', null, ['role' => User::admin()]); // Add 2 to gauge named `parallel_requests`, tagged with `type: "a"` Sentry.metrics.gauge('parallel_requests', 2, { tags: { type: 'a' } }); // Flush the metrics to Sentry metrics()->flush(); // We recommend registering the flushing in a shutdown function register_shutdown_function(static fn () => metrics()->flush()); ``` To learn more about Sentry Developer Merics, join the discussion at https://github.com/getsentry/sentry-php/discussions/1666. ### Bug Fixes - Disallow to seralize the `HubAdapter::class` [(#1663)](https://github.com/getsentry/sentry-php/pull/1663) - Do not overwrite trace context on event [(#1668)](https://github.com/getsentry/sentry-php/pull/1668) - Serialize breadcrumb data to display correct in the Sentry UI [(#1669)](https://github.com/getsentry/sentry-php/pull/1669) ### Misc - Remove the `final` keyword from `Hub::class`, `Client::class` and `Scope::class` [(#1665)](https://github.com/getsentry/sentry-php/pull/1665) ## 4.2.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.2.0. ### Features - Add a config option to allow overriding the Spotlight url [(#1659)](https://github.com/getsentry/sentry-php/pull/1659) ```php Sentry\init([ 'spotlight_url' => 'http://localhost:8969', ]); ``` ### Bug Fixes - Restore setting the `logger` value on the event payload [(#1657)](https://github.com/getsentry/sentry-php/pull/1657) - Only apply the `sample_rate` on error/message events [(#1662)](https://github.com/getsentry/sentry-php/pull/1662) This fixes an issue where Cron Check-Ins were wrongly sampled out if a `sample_rate` lower than `1.0` is used. ### Misc - Remove the `@internal` annotation from `ClientBuilder::class` [(#1661)](https://github.com/getsentry/sentry-php/pull/1661) ## 4.1.0 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.1.0. ### Features - Add support for Spotlight [(#1647)](https://github.com/getsentry/sentry-php/pull/1647) Spotlight is Sentry for Development. Inspired by an old project, Django Debug Toolbar. Spotlight brings a rich debug overlay into development environments, and it does it by leveraging the existing power of Sentry's SDKs. To learn more about Spotlight, go to https://spotlightjs.com/. ### Misc - Normalize `response` status [(#1644)](https://github.com/getsentry/sentry-php/pull/1644) ## 4.0.1 The Sentry SDK team is happy to announce the immediate availability of Sentry PHP SDK v4.0.1. ### Bug Fixes - Fix capturing out-of-memory errors when memory-constrained [(#1636)](https://github.com/getsentry/sentry-php/pull/1636) - Check if the cURL extension is installed [(#1632)](https://github.com/getsentry/sentry-php/pull/1632) ## 4.0.0 The Sentry SDK team is thrilled to announce the immediate availability of Sentry PHP SDK v4.0.0. ### Breaking Change Please refer to the [UPGRADE-4.0.md](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 Sentry\init([ '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 - Add new fluent APIs [(#1601)](https://github.com/getsentry/sentry-php/pull/1601) ```php // Before $transactionContext = new TransactionContext(); $transactionContext->setName('GET /example'); $transactionContext->setOp('http.server'); // After $transactionContext = (new TransactionContext()) ->setName('GET /example'); ->setOp('http.server'); ``` - 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 `logger` option [(#1625)](https://github.com/getsentry/sentry-php/pull/1625) To make it easier to debug the internals of the SDK, the `logger` option now accepts a `Psr\Log\LoggerInterface` instance. We do provide two implementations, `Sentry\Logger\DebugFileLogger` and `Sentry\Logger\DebugStdOutLogger`. ```php // This logs messages to the provided file path Sentry\init([ 'logger' => new DebugFileLogger(filePath: ROOT . DS . 'sentry.log'), ]); // This logs messages to stdout Sentry\init([ 'logger' => new DebugStdOutLogger(), ]); ``` - New default cURL HTTP client [(#1589)](https://github.com/getsentry/sentry-php/pull/1589) The SDK now ships with its own HTTP client based on cURL. A few new options were added. ```php Sentry\init([ 'http_proxy_authentication' => 'username:password', // user name and password to use for proxy authentication 'http_ssl_verify_peer' => false, // default true, verify the peer's SSL certificate 'http_compression' => false, // default true, http request body compression ]); ``` To use a different client, you may use the `http_client` option. ```php use Sentry\Client; use Sentry\HttpClient\HttpClientInterface; use Sentry\HttpClient\Request; use Sentry\HttpClient\Response; use Sentry\Options; $httpClient = new class() implements HttpClientInterface { public function sendRequest(Request $request, Options $options): Response { // your custom implementation return new Response($response->getStatusCode(), $response->getHeaders(), ''); } }; Sentry\init([ 'http_client' => $httpClient, ]); ``` To use a different transport, you may use the `transport` option. A custom transport must implement the `TransportInterface`. If you use the `transport` option, the `http_client` option has no effect. ### Misc - The abandoned package `php-http/message-factory` was removed. PKî%W]!Áû�GGsentry/UPGRADE-2.0.mdnu„[µü¤# Upgrade from 1.10 to 2.0 Version `2.x` is a complete rewrite of the existing code base. The public API has been trimmed down to a minimum. The preferred way of using the SDK is through our "Static API" / global functions. Here is a simple example to get started: ```php \Sentry\init(['dsn' => '___PUBLIC_DSN___' ]); \Sentry\configureScope(function (\Sentry\State\Scope $scope): void { $scope->setTag('page_locale', 'de-at'); $scope->setUser(['email' => 'john.doe@example.com']); $scope->setLevel(\Sentry\Severity::warning()); $scope->setExtra('character_name', 'Mighty Fighter'); }); // The following capture call will contain the data from the previous configured Scope try { thisFunctionThrows(); // -> throw new \Exception('foo bar'); } catch (\Exception $exception) { \Sentry\captureException($exception); } \Sentry\addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting', 'Message')); ``` The call to `\Sentry\init()` sets up global exception/error handlers and any uncaught error will be sent to Sentry. Version `>= 2.0` conforms to the [Unified SDK API](https://docs.sentry.io/development/sdk-dev/unified-api/). It has a fundamentally different concept, it's no longer recommended to just use a `Client` unless you really know what you are doing. Please visit [our docs](https://docs.sentry.io/error-reporting/quickstart/?platform=php) to get a full overview. ### Client options - The `exclude` option has been removed. - The `excluded_app_path` option has been renamed to `in_app_exclude` - The `send_callback` option has been renamed to `before_send`. - The `name` option has been renamed to `server_name`. - The `secret_key` option has been removed. - The `public_key` option has been removed. - The `message_limit` option has been removed. - The `project` option has been removed. - The `severity_map` option has been removed. - The `ignore_server_port` option has been removed. - The `trust_x_forwarded_proto` option has been removed. - The `mb_detect_order` option has been removed. - The `trace` option has been removed. - The `tags` option has been removed in favour of setting them in the scope. - The `site` option has been removed. - The `extra_data` option has been removed in favour of setting additional data in the scope. - The `curl_method` option has been removed. - The `curl_path` option has been removed. - The `curl_ipv4` option has been removed. - The `curl_ssl_version` option has been removed. - The `verify_ssl` option has been removed. - The `ca_cert` option has been removed. - The `proxy` option has been removed in favour of leaving to the user the burden of configuring the HTTP client options using a custom client. - The `processors` option has been removed. - The `processors_options` option has been removed. - The `transport` option has been removed in favour of setting it using the client builder. - The `install_default_breadcrumb_handlers` option has been removed. - The `serialize_all_object` option has been removed. - The `context_lines` option has been added to configure the number of lines of code context to capture. - The `max_value_length` option has been added to set an upper bound to the length of serialized items. - The `server` option has been renamed to `dsn`. ### Misc - All the classes have been renamed and moved around to follow the PSR-4 convention and `final` have been added where appropriate. - The `Raven_Autoloader` class has been removed. To install and use the library you are required to use [Composer](https://getcomposer.org/). - The `Raven_Util` class has been removed. - The `Raven_Compat` class has been removed. - The `Raven_Util` class has been removed. - The `Raven_CurlHandler` class has been removed. - The `Raven_TransactionStack` class has been removed. - The `Raven_Exception` class has been removed. ### Client - The constructor of the `Client` (before `Raven_Client`) class has changed its signature and now requires to be passed a configuration object, an instance of a transport and an event factory. Before: ```php public function __construct($options_or_dsn = null, $options = array()) { // ... } ``` After: ```php public function __construct(Options $options, TransportInterface $transport, EventFactoryInterface $eventFactory) { // ... } ``` The suggested way to create your own instance of the client is to use the provided builder (`ClientBuilder`) that will take care of instantiating a few dependencies like the PSR-7 factories and the HTTP client. - The method `Raven_Client::close_all_children_link` has been removed and there - The methods `Raven_Client::getRelease` and `Raven_Client::setRelease` have been removed. You should use `Options::getRelease` and `Options::setRelease` instead. Before: ```php $client->getRelease(); $client->setRelease(...); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getRelease(); $options->setRelease(...); ``` - The methods `Raven_Client::getEnvironment` and `Raven_Client::setEnvironment` have been removed. You should use `Options::getEnvironment` and `Options::setEnvironment` instead. Before: ```php $client->getEnvironment(); $client->setEnvironment(...); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getEnvironment(); $options->setEnvironment(...); ``` - The method `Raven_Client::getInputStream` has been removed. - The methods `Raven_Client::getDefaultPrefixes` and `Raven_Client::setPrefixes` have been removed. You should use `Options::getPrefixes` and `Options::setPrefixes` instead. Before: ```php $client->getPrefixes(); $client->setPrefixes(...); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getPrefixes(); $options->setPrefixes(...); ``` - The methods `Raven_Client::getAppPath` and `Raven_Client::setAppPath` have been removed. You should use `Options::getProjectRoot` and `Options::setProjectRoot` instead. Before: ```php $client->getAppPath(); $client->setAppPath(...); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getProjectRoot(); $options->setProjectRoot(...); ``` - The methods `Raven_Client::getExcludedAppPaths` and `Raven_Client::setExcludedAppPaths` have been removed. You should use `Options::getInAppExcludedPaths` and `Options::setInAppExcludedPaths` instead. Before: ```php $client->getExcludedAppPaths(); $client->setExcludedAppPaths(...); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getExcludedAppPaths(); $options->setExcludedAppPaths(...); ``` - The methods `Raven_Client::getSendCallback` and `Raven_Client::setSendCallback` have been removed. You should use `Options::getBeforeSendCallback` and `Options::setBeforeSendCallback` instead. Before: ```php $client->getSendCallback(); $client->setSendCallback(...); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getBeforeSendCallback(); $options->setBeforeSendCallback(...); - The method `Raven_Client::getServerEndpoint` has been removed. You should use `Options::getDsn` instead. Before: ```php $client->getServerEndpoint(); ``` After: ```php use Sentry\State\Hub; $options = Hub::getCurrent()->getClient()->getOptions(); $options->getDsn(); ``` - The methods `Raven_Client::getTransport` and `Raven_Client::setTransport` have been removed. The transport is now a required dependency of the client and must be passed as required constructor argument. - The method `Raven_Client::getUserAgent` has been removed. - The method `Raven_Client::getErrorTypes` has been removed. You should use `Configuration::getErrorTypes` instead. Before: ```php $client->getErrorTypes(); ``` After: ```php $client->getConfig()->getErrorTypes(); ``` - The `Raven_Client::getDefaultProcessors` method has been removed. - The `Raven_Client::setProcessorsFromOptions` method has been removed. - The `Raven_Client::getLastEventID` method has been removed. The ID of the last event that was captured is now returned by each of the `Client::capture*` methods. You can also use `Hub::getCurrent()->getLastEventId()`. - The `Raven_Client::parseDSN` method has been removed. - The `Raven_Client::getLastError` method has been removed. - The `Raven_Client::getIdent` method has been removed. - The `Raven_Client::registerShutdownFunction` method has been removed. - The `Raven_Client::is_http_request` method has been removed. - The `Raven_Client::get_http_data` method has been removed. - The `Raven_Client::get_user_data` method has been removed. - The `Raven_Client::get_extra_data` method has been removed. - The `Raven_Client::get_default_data` method has been removed. - The `Raven_Client::message` method has been removed. - The `Raven_Client::exception` method has been removed. - The `Raven_Client::captureQuery` method has been removed. - The `Raven_Client::captureMessage` method has changed its signature. Before: ```php public function captureMessage($message, $params = array(), $data = array(), $stack = false, $vars = null) { // ... } ``` After: ```php public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null): ?string { // ... } ``` - The `Raven_Client::captureException` method has changed its signature. Before: ```php public function captureException($exception, $data = null, $logger = null, $vars = null) { // ... } ``` After: ```php public function captureException(\Throwable $exception, ?Scope $scope = null): ?string { // ... } ``` - The `Raven_Client::captureLastError` method has changed its signature. Before: ```php public function captureLastError() { // ... } ``` After: ```php public function captureLastError(?Scope $scope = null): ?string { // ... } ``` - The method `Raven_Client::capture` has been removed. - The method `Raven_Client::sanitize` has been removed. - The method `Raven_Client::process` has been removed. - The method `Raven_Client::sendUnsentErrors` has been removed. - The method `Raven_Client::encode` has been removed. - The method `Raven_Client::send` has been removed. - The method `Raven_Client::send_remote` has been removed. - The method `Raven_Client::get_default_ca_cert` has been removed. - The method `Raven_Client::get_curl_options` has been removed. - The method `Raven_Client::send_http` has been removed. - The method `Raven_Client::buildCurlCommand` has been removed. - The method `Raven_Client::send_http_asynchronous_curl_exec` has been removed. - The method `Raven_Client::send_http_synchronous` has been removed. - The method `Raven_Client::get_auth_header` has been removed. - The method `Raven_Client::getAuthHeader` has been removed. - The method `Raven_Client::uuid4` has been removed. - The method `Raven_Client::get_current_url` has been removed. - The method `Raven_Client::isHttps` has been removed. - The method `Raven_Client::translateSeverity` has been removed. - The method `Raven_Client::registerSeverityMap` has been removed. - The method `Raven_Client::set_user_data` has been removed. - The method `Raven_Client::onShutdown` has been removed. - The method `Raven_Client::createProcessors` has been removed. - The method `Raven_Client::setProcessors` has been removed. - The method `Raven_Client::getLastSentryError` has been removed. - The method `Raven_Client::getShutdownFunctionHasBeenSet` has been removed. - The method `Raven_Client::close_curl_resource` has been removed. - The method `Raven_Client::setSerializer` has been removed. You can set it using the client builder. Before: ```php $client = new Raven_Client(); $client->setSerializer(...); ``` After: ```php use Sentry\ClientBuilder; $clientBuilder = ClientBuilder::create(); $clientBuilder->setSerializer(...); ``` - The method `Raven_Client::setReprSerializer` has been removed. You can set it using the client builder. Before: ```php $client = new Raven_Client(); $client->setSerializer(...); ``` After: ```php use Sentry\ClientBuilder; $clientBuilder = ClientBuilder::create(); $clientBuilder->setRepresentationSerializer(...); ``` - The method `Raven_Client::cleanup_php_version` has been removed. - The method `Raven_Client::registerDefaultBreadcrumbHandlers` has been removed. - The `Raven_Client::user_context` method has been removed. You can set this data in the current active scope. Before: ```php $client->user_context(array('foo', 'bar')); ``` After: ```php use Sentry\State\Hub; use Sentry\State\Scope; Hub::getCurrent()->configureScope(function (Scope $scope): void { $scope->setUser(['email' => 'foo@example.com']); }); ``` - The `Raven_Client::tags_context` method has been removed. You can set this data in the current active scope. Before: ```php $client->tags_context(array('foo', 'bar')); ``` After: ```php use Sentry\State\Hub; use Sentry\State\Scope; Hub::getCurrent()->configureScope(function (Scope $scope): void { $scope->setTag('tag_name', 'tag_value'); }); ``` - The `Raven_Client::extra_context` method has been removed. You can set this data in the current active scope. Before: ```php $client->extra_context(array('foo' => 'bar')); ``` After: ```php use Sentry\State\Hub; use Sentry\State\Scope; Hub::getCurrent()->configureScope(function (Scope $scope): void { $scope->setExtra('extra_key', 'extra_value'); }); ``` - The method `Raven_Client::install` has been removed. The error handler is registered automatically when using the `ExceptionListenerIntegration` and `ErrorListenerIntegration` integrations (which are enabled by default). ### Processors - The `Raven_Processor_RemoveCookiesProcessor` class has been removed. - The `Raven_Processor_SanitizeStacktraceProcessor` class has been removed. - The `Raven_Processor_SanitizeHttpHeadersProcessor` class has been removed. - The `Raven_Processor_RemoveHttpBodyProcessor` class has been removed. - The `Raven_Processor_SanitizeDataProcessor` class has been removed. - The `Raven_Processor` class has been removed. ### Context - The `Raven_Context` class has been renamed to `Context`. - The `tags`, `extra` and `user` properties of the `Raven_Context` class have been removed. Each instance of the new class represents now a single context type at once. ## Error handlers - The `Raven_Breadcrumbs_ErrorHandler` class has been removed. - The `Raven_Breadcrumbs_MonologHandler` class has been removed. - The `Raven_ErrorHandler` class has been renamed to `ErrorHandler` and has been made `final`. - The method `Raven_ErrorHandler::handleError` has changed its signature by removing the `$context` argument and it has been marked as `internal` to make it clear that it should not be called publicly and its method visibility is subject to changes without any notice. - The methods `Raven_ErrorHandler::registerErrorHandler`, `Raven_ErrorHandler::registerExceptionHandler` and `Raven_ErrorHandler::registerShutdownFunction` have been removed. You should use the `ErrorHandler::register` method instead, but note that it registers all error handlers (error, exception and fatal error) at once and there is no way anymore to only use one of them. Before: ```php $errorHandler = new Raven_ErrorHandler($client); $errorHandler->registerErrorHandler(); $errorHandler->registerExceptionHandler(); $errorHandler->registerShutdownFunction(); ``` After: ```php use Sentry\ErrorHandler; ErrorHandler::register(function (\Throwable $exception): void { // ... }); ``` - The method `Raven_ErrorHandler::handleError` has changed its signature by removing the `$context` argument and it has been marked as `internal` to make it clear that it should not be called publicly and its method visibility is subject to changes without any notice. - The method `Raven_ErrorHandler::handleFatalError` has changed its signature by adding an optional argument named `$error` and it has been marked as `internal` to make it clear that it should not be called publicly and its method visibility is subject to changes without any notice. - The method `Raven_ErrorHandler::handleException` has changed its signature by removing the `$isError` and `$vars` arguments and it has been marked as `internal` to make it clear that it should not be called publicly and its method visibility is subject to changes without any notice. - The method `Raven_ErrorHandler::bitwiseOr` has been removed and there is no replacement for it. - The method `Raven_ErrorHandler::shouldCaptureFatalError` has been removed and there is no replacement for it. ### Serializers - The `Raven_Serializer` class has been renamed to `Serializer` and its constructor changed signature. Before: ```php public function __construct($mb_detect_order = null, $message_limit = null) { // ... } ``` After: ```php public function __construct(int $maxDepth = 3, ?string $mbDetectOrder = null, int $messageLimit = Client::MESSAGE_MAX_LENGTH_LIMIT) { // ... } ``` - The `Raven_ReprSerializer` class has been renamed to `RepresentationSerializer` and its constructor changed signature. Before: ```php public function __construct($mb_detect_order = null, $message_limit = null) { // ... } ``` After: ```php public function __construct(int $maxDepth = 3, ?string $mbDetectOrder = null, int $messageLimit = Client::MESSAGE_MAX_LENGTH_LIMIT) { // ... } ``` PKî%W]–�®ŸGGsentry/composer.jsonnu„[µü¤{ "name": "sentry/sentry", "type": "library", "description": "PHP SDK for Sentry (http://sentry.io)", "keywords": [ "sentry", "log", "logging", "error-monitoring", "error-handler", "crash-reporting", "crash-reports", "profiling", "tracing" ], "homepage": "http://sentry.io", "license": "MIT", "authors": [ { "name": "Sentry", "email": "accounts@sentry.io" } ], "require": { "php": "^7.2|^8.0", "ext-json": "*", "ext-mbstring": "*", "ext-curl": "*", "guzzlehttp/psr7": "^1.8.4|^2.1.1", "jean85/pretty-package-versions": "^1.5|^2.0.4", "psr/log": "^1.0|^2.0|^3.0", "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.4", "guzzlehttp/promises": "^2.0.3", "guzzlehttp/psr7": "^1.8.4|^2.1.1", "monolog/monolog": "^1.6|^2.0|^3.0", "phpbench/phpbench": "^1.0", "phpstan/phpstan": "^1.3", "phpunit/phpunit": "^8.5|^9.6", "vimeo/psalm": "^4.17" }, "suggest": { "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." }, "conflict": { "raven/raven": "*" }, "autoload": { "files": [ "src/functions.php" ], "psr-4": { "Sentry\\": "src/" } }, "autoload-dev": { "psr-4": { "Sentry\\Tests\\": "tests/" } }, "scripts": { "check": [ "@cs-check", "@phpstan", "@psalm", "@tests" ], "tests": "vendor/bin/phpunit --verbose", "cs-check": "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run", "cs-fix": "vendor/bin/php-cs-fixer fix --verbose --diff", "phpstan": "vendor/bin/phpstan analyse", "psalm": "vendor/bin/psalm" }, "config": { "sort-packages": true }, "prefer-stable": true } PKî%W]” /7@@sentry/.git-blame-ignore-revsnu„[µü¤# Update PHPCS (#1599) f8b64d330d03cbf809795beb5846b2c48175cd62 PKî%W]aèÓÓsentry/CONTRIBUTING.mdnu„[µü¤

Sentry

# Contributing to the Sentry PHP SDK We welcome contributions to `sentry-php` by the community. Please search the [issue tracker](https://github.com/getsentry/sentry-php/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-php` repository and prepare necessary changes. - Add tests for your changes to `tests/`. - Run tests and make sure all of them pass. - Open a pull request. We will review your pull request as soon as possible. Thank you for contributing! ## PR reviews For feedback in PRs, we use the [LOGAF scale](https://develop.sentry.dev/engineering-practices/code-review/#logaf-scale) to specify how important a comment is. You only need one approval from a maintainer to be able to merge. For some PRs, asking specific or multiple people for review might be adequate. Our different types of reviews: 1. **LGTM without any comments.** You can merge immediately. 2. **LGTM with low and medium comments.** The reviewer trusts you to resolve these comments yourself, and you don't need to wait for another approval. 3. **Only comments.** You must address all the comments and need another review until you merge. 4. **Request changes.** Only use if something critical is in the PR that absolutely must be addressed. We usually use `h` comments for that. When someone requests changes, the same person must approve the changes to allow merging. Use this sparingly. ## Development environment ### Clone the repository ```bash git clone git@github.com:getsentry/sentry-php.git ``` 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. ### Install the dependencies Dependencies are managed through [Composer](https://getcomposer.org/): ```bash composer install ``` ### Running tests Tests can then be run via [PHPUnit](https://phpunit.de/): ```bash vendor/bin/phpunit ``` ## 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-php/commit/877bca3f0f0ac0fc8ec0a218c6070cccea266795. - On GitHub in the `sentry-php` 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î%W]§J£zDDsentry-laravel/LICENSEnu„[µü¤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î%W]7öØsentry-laravel/README.mdnu„[µü¤ _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î%W]�c¥ sentry-laravel/.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î%W] ½AYYsentry-laravel/phpstan.neonnu„[µü¤includes: - phpstan-baseline.neon parameters: level: 1 paths: - src PKî%W]ö¹*Ä))$sentry-laravel/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î%W]Fÿ}4››-sentry-laravel/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î%W]±û--6sentry-laravel/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î%W]xÈï�  5sentry-laravel/src/Sentry/Laravel/Logs/LogChannel.phpnu„[µü¤parseChannel($config), [ $this->prepareHandler($handler, $config), ] ); } } PKî%W]«æ„¤ÚÚ0sentry-laravel/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î%W]6è Àp/p/1sentry-laravel/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î%W]í…×ó'8'82sentry-laravel/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î%W]Œ @­­Esentry-laravel/src/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î%W]oº‘¼¼<sentry-laravel/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î%W]®“F2c#c#9sentry-laravel/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î%W]™wª__Xsentry-laravel/src/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î%W]Ðî æ¨¨Vsentry-laravel/src/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î%W]¤ë]HËËNsentry-laravel/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î%W]Âj9¥¨ ¨ =sentry-laravel/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î%W]Þ½õ^Z Z Psentry-laravel/src/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î%W]ù�;dÝÝAsentry-laravel/src/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î%W]͵ ##:sentry-laravel/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î%W]º¹2uFFAsentry-laravel/src/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î%W]/âU.å#å#8sentry-laravel/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î%W]/äèQÒÒ=sentry-laravel/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î%W]eãrGGAsentry-laravel/src/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î%W]Ý[©Ïãã?sentry-laravel/src/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î%W]í£øÀll@sentry-laravel/src/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î%W]‰Õ½ÂDD9sentry-laravel/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î%W]ΚffKsentry-laravel/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î%W]°’ õóóesentry-laravel/src/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î%W]XÝ™1vvcsentry-laravel/src/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î%W]ãý(ѳ ³ Xsentry-laravel/src/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î%W]*«\sshsentry-laravel/src/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.phpnu„[µü¤ $property, 'kind' => 'missing_attribute', ]; } protected function getViolationException(Model $model, string $property): Exception { return new MissingAttributeException($model, $property); } } PKî%W]ꜗNèèMsentry-laravel/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î%W]˜ù;ÇFÇF5sentry-laravel/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î%W]Äø ²$²$?sentry-laravel/src/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î%W]Ñ­(œ œ Asentry-laravel/src/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î%W]›}ËhÊÊDsentry-laravel/src/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î%W])?#€€Rsentry-laravel/src/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î%W]As/BFsentry-laravel/src/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î%W]ÒYö*¦¦Ksentry-laravel/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î%W]‘.l€OO6sentry-laravel/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î%W]×ó++Isentry-laravel/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î%W]ØŸºµøøOsentry-laravel/src/Sentry/Laravel/Features/Storage/CloudFilesystemDecorator.phpnu„[µü¤withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); } } PKî%W]GÙ™FèèLsentry-laravel/src/Sentry/Laravel/Features/Storage/SentryCloudFilesystem.phpnu„[µü¤filesystem = $filesystem; $this->defaultData = $defaultData; $this->recordSpans = $recordSpans; $this->recordBreadcrumbs = $recordBreadcrumbs; } } PKî%W]¯D\G{{Hsentry-laravel/src/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î%W]¦Îž ppNsentry-laravel/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î%W]Êe¬MMJsentry-laravel/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î%W]òò¾¾Bsentry-laravel/src/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î%W]òµzçííGsentry-laravel/src/Sentry/Laravel/Features/Storage/SentryFilesystem.phpnu„[µü¤filesystem = $filesystem; $this->defaultData = $defaultData; $this->recordSpans = $recordSpans; $this->recordBreadcrumbs = $recordBreadcrumbs; } } PKî%W]2 ž›xxQsentry-laravel/src/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î%W]ìx.I,I,Ksentry-laravel/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î%W]Ü:?°..?sentry-laravel/src/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î%W]'È   Gsentry-laravel/src/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î%W]º”ŒRR=sentry-laravel/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î%W]È *q3sentry-laravel/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î%W]¢#úÄÞÞsentry-laravel/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î%W]ïk¦JJ sentry-laravel/config/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î%W] á22sentry-laravel/CONTRIBUTING.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î%W]¥­”DDsentry-laravel/codecov.ymlnu„[µü¤comment: false coverage: status: project: off patch: off PKî%W],Á† DDsentry/LICENSEnu„[µü¤PKî%W]FoÐ?® ® ‚sentry/UPGRADE-4.0.mdnu„[µü¤PKî%W]Æ™Zö®®usentry/README.mdnu„[µü¤PKî%W]U>-ãžžc&sentry/AUTHORSnu„[µü¤PKî%W]{>…öîî?'sentry/scripts/bump-version.shnu„[µü¤PKî%W]!-Ôrùù{)sentry/src/SentrySdk.phpnu„[µü¤PKî%W]?8¢´ÌÌ4¼.sentry/src/Serializer/PayloadSerializerInterface.phpnu„[µü¤PKî%W]žz’n1n1,ì0sentry/src/Serializer/AbstractSerializer.phpnu„[µü¤PKî%W])øó™™>¶bsentry/src/Serializer/Traits/StacktraceFrameSeralizerTrait.phpnu„[µü¤PKî%W]€j?óó9½isentry/src/Serializer/Traits/BreadcrumbSeralizerTrait.phpnu„[µü¤PKî%W]kŸÆbOO2nsentry/src/Serializer/RepresentationSerializer.phpnu„[µü¤PKî%W]”ñSÅÅ;Êssentry/src/Serializer/RepresentationSerializerInterface.phpnu„[µü¤PKî%W]&ùmm/úvsentry/src/Serializer/SerializableInterface.phpnu„[µü¤PKî%W]”Ñœ¯óó2Æxsentry/src/Serializer/EnvelopItems/ProfileItem.phpnu„[µü¤PKî%W]íi‡ÙEE6|sentry/src/Serializer/EnvelopItems/TransactionItem.phpnu„[µü¤PKî%W]™'ýâÚÚ<Æ’sentry/src/Serializer/EnvelopItems/EnvelopeItemInterface.phpnu„[µü¤PKî%W]ï ÃÆÉÉ0 ”sentry/src/Serializer/EnvelopItems/EventItem.phpnu„[µü¤PKî%W]Y©hu25¬sentry/src/Serializer/EnvelopItems/CheckInItem.phpnu„[µü¤PKî%W][ËBl¦¦/�±sentry/src/Serializer/EnvelopItems/LogsItem.phpnu„[µü¤PKî%W]Ñ’o-¢·sentry/src/Serializer/SerializerInterface.phpnu„[µü¤PKî%W]9ª.tII$~¹sentry/src/Serializer/Serializer.phpnu„[µü¤PKî%W]¯§ò¼Š Š +»sentry/src/Serializer/PayloadSerializer.phpnu„[µü¤PKî%W]½íµoo&Åsentry/src/Exception/JsonException.phpnu„[µü¤PKî%W]fÿÈm88/ÅÅsentry/src/Exception/EventCreationException.phpnu„[µü¤PKî%W]'T?÷÷/\Èsentry/src/Exception/SilencedErrorException.phpnu„[µü¤PKî%W]šßÊïï,²Ésentry/src/Exception/FatalErrorException.phpnu„[µü¤PKî%W]Éë{< < ýÊsentry/src/Metrics/Metrics.phpnu„[µü¤PKî%W]4Œìïw w "‡Ösentry/src/Metrics/MetricsUnit.phpnu„[µü¤PKî%W]¾ f÷’ ’ "Päsentry/src/Monolog/LogsHandler.phpnu„[µü¤PKî%W] JØ& & (4òsentry/src/Monolog/BreadcrumbHandler.phpnu„[µü¤PKî%W]) _%ò ò ²ýsentry/src/Monolog/Handler.phpnu„[µü¤PKî%W]Îå;< < :ò sentry/src/Monolog/CompatibilityProcessingHandlerTrait.phpnu„[µü¤PKî%W]Ç‘œšÊÊ1˜sentry/src/Monolog/CompatibilityLogLevelTrait.phpnu„[µü¤PKî%W]è~—Ž“ “ Ãsentry/src/StacktraceBuilder.phpnu„[µü¤PKî%W]Í$¢ ø ø %¦*sentry/src/Transport/ResultStatus.phpnu„[µü¤PKî%W]£Ô\üó6sentry/src/Transport/Result.phpnu„[µü¤PKî%W]Aš2§žž$J;sentry/src/Transport/RateLimiter.phpnu„[µü¤PKî%W]hd©–ªª&�5+žž.‘|sentry/src/Tracing/GuzzleTracingMiddleware.phpnu„[µü¤PKî%W]®1ÌëJJ(�”sentry/src/Tracing/TransactionSource.phpnu„[µü¤PKî%W];˜4¾’ ’ */œsentry/src/Tracing/TransactionMetadata.phpnu„[µü¤PKî%W]2üæqKK¨sentry/src/Tracing/TraceId.phpnu„[µü¤PKî%W]sè~ƒ==´«sentry/src/Tracing/SpanId.phpnu„[µü¤PKî%W]4'eMš6š6>°sentry/src/Tracing/Span.phpnu„[µü¤PKî%W]ï�‹>°°4#çsentry/src/Tracing/Traits/TraceHeaderParserTrait.phpnu„[µü¤PKî%W]~ê™ww#7÷sentry/src/Tracing/SpanRecorder.phpnu„[µü¤PKî%W]4æø„zz)üsentry/src/Tracing/TransactionContext.phpnu„[µü¤PKî%W]îj’Oõõ&Ôsentry/src/Tracing/SamplingContext.phpnu„[µü¤PKî%W]v†Yºº-sentry/src/Tracing/DynamicSamplingContext.phpnu„[µü¤PKî%W]‹Æ^©œœ!64sentry/src/Tracing/SpanStatus.phpnu„[µü¤PKî%W]óƒÚ)"#Ksentry/src/Tracing/Transaction.phpnu„[µü¤PKî%W]áÁޱ±)z^sentry/src/Tracing/PropagationContext.phpnu„[µü¤PKî%W]ò¨ºˆKK(„ssentry/src/Spotlight/SpotlightClient.phpnu„[µü¤PKî%W]@> Y Y !'{sentry/src/ExceptionMechanism.phpnu„[µü¤PKî%W]%S÷ŽYY!Ñ…sentry/src/Profiling/Profiler.phpnu„[µü¤PKî%W]áî:,:, {�sentry/src/Profiling/Profile.phpnu„[µü¤PKî%W]DfyíÒÒ"ºsentry/src/HttpClient/Response.phpnu„[µü¤PKî%W]ljïòÅÅ-)Ásentry/src/HttpClient/HttpClientInterface.phpnu„[µü¤PKî%W]‹§VVÃÃ!KÂsentry/src/HttpClient/Request.phpnu„[µü¤PKî%W]ç›­ $_Äsentry/src/HttpClient/HttpClient.phpnu„[µü¤PKî%W]EbÒ Ò ÄÕsentry/src/ExceptionDataBag.phpnu„[µü¤PKî%W]S†Jœ‡‡åàsentry/src/ClientBuilder.phpnu„[µü¤PKî%W]P„ú‘I‘I¸ïsentry/src/ErrorHandler.phpnu„[µü¤PKî%W]gN�‹99”9sentry/src/CheckInStatus.phpnu„[µü¤PKî%W]ªj§—œœ7>sentry/src/Integration/FrameContextifierIntegration.phpnu„[µü¤PKî%W]­ äpÕÕ7Rsentry/src/Integration/ExceptionListenerIntegration.phpnu„[µü¤PKî%W] ùÄ::-XVsentry/src/Integration/ModulesIntegration.phpnu„[µü¤PKî%W]hg´rPP)ï^sentry/src/Integration/RequestFetcher.phpnu„[µü¤PKî%W]Öš±Æ‰‰/˜asentry/src/Integration/IntegrationInterface.phpnu„[µü¤PKî%W]¢Oô0ÀÀ3€csentry/src/Integration/ErrorListenerIntegration.phpnu„[µü¤PKî%W]rÉVÜÜ2£lsentry/src/Integration/RequestFetcherInterface.phpnu„[µü¤PKî%W]Ý€x55:ánsentry/src/Integration/OptionAwareIntegrationInterface.phpnu„[µü¤PKî%W]yšÊÊ1€psentry/src/Integration/EnvironmentIntegration.phpnu„[µü¤PKî%W]TË ùù8«ysentry/src/Integration/FatalErrorListenerIntegration.phpnu„[µü¤PKî%W]mÜUñ‡‡; sentry/src/Integration/AbstractErrorListenerIntegration.phpnu„[µü¤PKî%W]%;ƒss1þ„sentry/src/Integration/TransactionIntegration.phpnu„[µü¤PKî%W]Mô�óRR.ÒŠsentry/src/Integration/IntegrationRegistry.phpnu„[µü¤PKî%W]nü–ú'ú'-‚�sentry/src/Integration/RequestIntegration.phpnu„[µü¤PKî%W]aSefÞ´Þ´ÙÅsentry/src/Options.phpnu„[µü¤PKî%W]ÑÖ|>ýzsentry/src/Breadcrumb.phpnu„[µü¤PKî%W]&™s[CSCSbšsentry/src/Event.phpnu„[µü¤PKî%W]¢iè³³éísentry/src/UserDataBag.phpnu„[µü¤PKî%W]û«:ÓÓæsentry/src/Util/Str.phpnu„[µü¤PKî%W]uÕM^^^ sentry/src/Util/PHPVersion.phpnu„[µü¤PKî%W]ÚdÐjrr"¬sentry/src/Util/PrefixStripper.phpnu„[µü¤PKî%W]yÍíKööpsentry/src/Util/Http.phpnu„[µü¤PKî%W]š×ô˜˜®sentry/src/Util/ClockMock.phpnu„[µü¤PKî%W]ós)ƒžž“,sentry/src/Util/Arr.phpnu„[µü¤PKî%W]æ“¢ ¹¹$x3sentry/src/Util/PHPConfiguration.phpnu„[µü¤PKî%W]ÕÊà„„…5sentry/src/Util/JSON.phpnu„[µü¤PKî%W]J„��ÁÁQ>sentry/src/Util/SentryUid.phpnu„[µü¤PKî%W]ú6²___Csentry/src/Frame.phpnu„[µü¤PKî%W].GÇÓÓ_sentry/UPGRADE-3.0.mdnu„[µü¤PKî%W]9!1Š [ [}sentry/CHANGELOG.mdnu„[µü¤PKî%W]!Áû�GGýØsentry/UPGRADE-2.0.mdnu„[µü¤PKî%W]–�®ŸGGU sentry/composer.jsonnu„[µü¤PKî%W]” /7@@à(sentry/.git-blame-ignore-revsnu„[µü¤PKî%W]aèÓÓm)sentry/CONTRIBUTING.mdnu„[µü¤PKî%W]§J£zDD†Asentry-laravel/LICENSEnu„[µü¤PKî%W]7öØFsentry-laravel/README.mdnu„[µü¤PKî%W]�c¥ k]sentry-laravel/.php-cs-fixer.phpnu„[µü¤PKî%W] ½AYYÑ^sentry-laravel/phpstan.neonnu„[µü¤PKî%W]ö¹*Ä))$u_sentry-laravel/phpstan-baseline.neonnu„[µü¤PKî%W]Fÿ}4››-ò}sentry-laravel/src/Sentry/Laravel/Version.phpnu„[µü¤PKî%W]„u^œ((3ê~sentry-laravel/src/Sentry/Laravel/SentryHandler.phpnu„[µü¤PKî%W]±û--6g§sentry-laravel/src/Sentry/Laravel/Logs/LogsHandler.phpnu„[µü¤PKî%W]xÈï�  5úµsentry-laravel/src/Sentry/Laravel/Logs/LogChannel.phpnu„[µü¤PKî%W]«æ„¤ÚÚ0iºsentry-laravel/src/Sentry/Laravel/LogChannel.phpnu„[µü¤PKî%W]6è Àp/p/1£¿sentry-laravel/src/Sentry/Laravel/Integration.phpnu„[µü¤PKî%W]í…×ó'8'82tïsentry-laravel/src/Sentry/Laravel/EventHandler.phpnu„[µü¤PKî%W]Œ @­­Eý' sentry-laravel/src/Sentry/Laravel/Console/AboutCommandIntegration.phpnu„[µü¤PKî%W]oº‘¼¼<0 sentry-laravel/src/Sentry/Laravel/Console/PublishCommand.phpnu„[µü¤PKî%W]®“F2c#c#9GE sentry-laravel/src/Sentry/Laravel/Console/TestCommand.phpnu„[µü¤PKî%W]™wª__Xi sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingControllerDispatcherTracing.phpnu„[µü¤PKî%W]Ðî æ¨¨Vúl sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingCallableDispatcherTracing.phpnu„[µü¤PKî%W]¤ë]HËËN(p sentry-laravel/src/Sentry/Laravel/Tracing/Routing/TracingRoutingDispatcher.phpnu„[µü¤PKî%W]Âj9¥¨ ¨ =qu sentry-laravel/src/Sentry/Laravel/Tracing/BacktraceHelper.phpnu„[µü¤PKî%W]Þ½õ^Z Z P†‚ sentry-laravel/src/Sentry/Laravel/Tracing/Integrations/LighthouseIntegration.phpnu„[µü¤PKî%W]ù�;dÝÝA`£ sentry-laravel/src/Sentry/Laravel/Tracing/ViewEngineDecorator.phpnu„[µü¤PKî%W]͵ ##:®© sentry-laravel/src/Sentry/Laravel/Tracing/EventHandler.phpnu„[µü¤PKî%W]º¹2uFFAÍ sentry-laravel/src/Sentry/Laravel/Tracing/TransactionFinisher.phpnu„[µü¤PKî%W]/âU.å#å#8ÑÑ sentry-laravel/src/Sentry/Laravel/Tracing/Middleware.phpnu„[µü¤PKî%W]/äèQÒÒ=ö sentry-laravel/src/Sentry/Laravel/Tracing/ServiceProvider.phpnu„[µü¤PKî%W]eãrGGA] sentry-laravel/src/Sentry/Laravel/Http/SetRequestIpMiddleware.phpnu„[µü¤PKî%W]Ý[©Ïãã? sentry-laravel/src/Sentry/Laravel/Http/SetRequestMiddleware.phpnu„[µü¤PKî%W]í£øÀll@g sentry-laravel/src/Sentry/Laravel/Http/FlushEventsMiddleware.phpnu„[µü¤PKî%W]-&‹fûû@C sentry-laravel/src/Sentry/Laravel/Http/LaravelRequestFetcher.phpnu„[µü¤PKî%W]‰Õ½ÂDD9® sentry-laravel/src/Sentry/Laravel/BaseServiceProvider.phpnu„[µü¤PKî%W]ΚffK[& sentry-laravel/src/Sentry/Laravel/Integration/LaravelContextIntegration.phpnu„[µü¤PKî%W]°’ õóóe<+ sentry-laravel/src/Sentry/Laravel/Integration/ModelViolations/DiscardedAttributeViolationReporter.phpnu„[µü¤PKî%W]XÝ™1vvcÄ. sentry-laravel/src/Sentry/Laravel/Integration/ModelViolations/LazyLoadingModelViolationReporter.phpnu„[µü¤PKî%W]ãý(ѳ ³ XÍ3 sentry-laravel/src/Sentry/Laravel/Integration/ModelViolations/ModelViolationReporter.phpnu„[µü¤PKî%W]*«\sshB sentry-laravel/src/Sentry/Laravel/Integration/ModelViolations/MissingAttributeModelViolationReporter.phpnu„[µü¤PKî%W]ꜗNèèME sentry-laravel/src/Sentry/Laravel/Integration/ExceptionContextIntegration.phpnu„[µü¤PKî%W]˜ù;ÇFÇF5xI sentry-laravel/src/Sentry/Laravel/ServiceProvider.phpnu„[µü¤PKî%W]Äø ²$²$?¤� sentry-laravel/src/Sentry/Laravel/Features/QueueIntegration.phpnu„[µü¤PKî%W]Ñ­(œ œ Aŵ sentry-laravel/src/Sentry/Laravel/Features/ConsoleIntegration.phpnu„[µü¤PKî%W]›}ËhÊÊDÒÀ sentry-laravel/src/Sentry/Laravel/Features/HttpClientIntegration.phpnu„[µü¤PKî%W])?#€€Rß sentry-laravel/src/Sentry/Laravel/Features/Concerns/TracksPushedScopesAndSpans.phpnu„[µü¤PKî%W]As/BFç sentry-laravel/src/Sentry/Laravel/Features/Concerns/WorksWithSpans.phpnu„[µü¤PKî%W]ÒYö*¦¦KŠê sentry-laravel/src/Sentry/Laravel/Features/Concerns/ResolvesEventOrigin.phpnu„[µü¤PKî%W]‘.l€OO6«ð sentry-laravel/src/Sentry/Laravel/Features/Feature.phpnu„[µü¤PKî%W]×ó++I` sentry-laravel/src/Sentry/Laravel/Features/LivewirePackageIntegration.phpnu„[µü¤PKî%W]ØŸºµøøO! sentry-laravel/src/Sentry/Laravel/Features/Storage/CloudFilesystemDecorator.phpnu„[µü¤PKî%W]GÙ™FèèL{" sentry-laravel/src/Sentry/Laravel/Features/Storage/SentryCloudFilesystem.phpnu„[µü¤PKî%W]¯D\G{{Hß$ sentry-laravel/src/Sentry/Laravel/Features/Storage/SentryS3V3Adapter.phpnu„[µü¤PKî%W]¦Îž ppNÒ' sentry-laravel/src/Sentry/Laravel/Features/Storage/SentryFilesystemAdapter.phpnu„[µü¤PKî%W]Êe¬MMJÀ* sentry-laravel/src/Sentry/Laravel/Features/Storage/FilesystemDecorator.phpnu„[µü¤PKî%W]òò¾¾B‡E sentry-laravel/src/Sentry/Laravel/Features/Storage/Integration.phpnu„[µü¤PKî%W]òµzçííG·^ sentry-laravel/src/Sentry/Laravel/Features/Storage/SentryFilesystem.phpnu„[µü¤PKî%W]2 ž›xxQa sentry-laravel/src/Sentry/Laravel/Features/Storage/FilesystemAdapterDecorator.phpnu„[µü¤PKî%W]ìx.I,I,Kf sentry-laravel/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.phpnu„[µü¤PKî%W]Ü:?°..?Ø’ sentry-laravel/src/Sentry/Laravel/Features/CacheIntegration.phpnu„[µü¤PKî%W]'È   GUÁ sentry-laravel/src/Sentry/Laravel/Features/NotificationsIntegration.phpnu„[µü¤PKî%W]º”ŒRR=ÒÍ sentry-laravel/src/Sentry/Laravel/Features/LogIntegration.phpnu„[µü¤PKî%W]—¨Þ‰õõF‘Ð sentry-laravel/src/Sentry/Laravel/Features/FolioPackageIntegration.phpnu„[µü¤PKî%W]È *q3ü× sentry-laravel/src/Sentry/Laravel/Util/Filesize.phpnu„[µü¤PKî%W]ά^^,pÛ sentry-laravel/src/Sentry/Laravel/Facade.phpnu„[µü¤PKî%W]ÚXe?W?W*â sentry-laravel/CHANGELOG.mdnu„[µü¤PKî%W]¢#úÄÞÞ´9 sentry-laravel/composer.jsonnu„[µü¤PKî%W]ïk¦JJ ÞB sentry-laravel/config/sentry.phpnu„[µü¤PKî%W] á22x\ sentry-laravel/CONTRIBUTING.mdnu„[µü¤PKî%W]¥­”DDør sentry-laravel/codecov.ymlnu„[µü¤PKÃÃoR†s