From 3022b4e976e11e97338bb5c127226ea52633675f Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 27 Jan 2026 12:46:58 +0100 Subject: [PATCH 1/5] feat(scopes): replace Hub with new scopes --- src/Attributes/AttributeBag.php | 11 + src/Client.php | 24 + src/ClientInterface.php | 7 + .../AbstractErrorListenerIntegration.php | 13 +- src/Integration/EnvironmentIntegration.php | 2 +- src/Integration/ErrorListenerIntegration.php | 8 +- .../ExceptionListenerIntegration.php | 6 +- .../FatalErrorListenerIntegration.php | 8 +- .../FrameContextifierIntegration.php | 2 +- src/Integration/ModulesIntegration.php | 2 +- src/Integration/RequestIntegration.php | 6 +- src/Integration/TransactionIntegration.php | 3 +- src/Logs/LogsAggregator.php | 57 +- src/Metrics/MetricsAggregator.php | 47 +- src/Monolog/BreadcrumbHandler.php | 24 +- src/Monolog/Handler.php | 18 +- src/NoOpClient.php | 5 + src/SentrySdk.php | 266 ++++- src/State/Hub.php | 414 -------- src/State/HubAdapter.php | 230 ----- src/State/HubInterface.php | 161 ---- src/State/Layer.php | 82 -- src/State/Scope.php | 250 ++++- src/State/ScopeManager.php | 164 ++++ src/State/ScopeType.php | 60 ++ src/Tracing/DynamicSamplingContext.php | 6 +- src/Tracing/GuzzleTracingMiddleware.php | 23 +- src/Tracing/PropagationContext.php | 10 +- src/Tracing/Span.php | 2 +- src/Tracing/Transaction.php | 18 +- src/functions.php | 65 +- tests/FunctionsTest.php | 230 +++-- .../EnvironmentIntegrationTest.php | 2 +- .../FrameContextifierIntegrationTest.php | 4 +- tests/Integration/ModulesIntegrationTest.php | 4 +- tests/Integration/RequestIntegrationTest.php | 2 +- .../TransactionIntegrationTest.php | 2 +- tests/Logs/LogsAggregatorTest.php | 14 +- tests/Logs/LogsTest.php | 7 +- tests/Metrics/TraceMetricsTest.php | 8 +- tests/Monolog/BreadcrumbHandlerTest.php | 33 +- tests/Monolog/HandlerTest.php | 5 +- tests/Monolog/LogsHandlerTest.php | 7 +- tests/SentrySdkExtension.php | 2 +- tests/SentrySdkTest.php | 76 +- tests/State/HubAdapterTest.php | 394 -------- tests/State/HubTest.php | 907 ------------------ tests/State/LayerTest.php | 37 - tests/State/ScopeManagerTest.php | 72 ++ tests/State/ScopeTest.php | 151 +++ tests/Tracing/DynamicSamplingContextTest.php | 13 +- tests/Tracing/GuzzleTracingMiddlewareTest.php | 123 +-- tests/Tracing/TransactionTest.php | 51 +- ...errors_not_silencable_on_php_8_and_up.phpt | 2 +- ...spects_capture_silenced_errors_option.phpt | 2 +- ...espects_current_error_reporting_level.phpt | 2 +- ..._option_regardless_of_error_reporting.phpt | 2 +- ...tegration_respects_error_types_option.phpt | 2 +- .../error_handler_captures_fatal_error.phpt | 2 +- ...rror_integration_captures_fatal_error.phpt | 2 +- ...tegration_respects_error_types_option.phpt | 2 +- .../error_handler_captures_fatal_error.phpt | 2 +- ...rror_integration_captures_fatal_error.phpt | 2 +- ...tegration_respects_error_types_option.phpt | 2 +- tests/phpt/test_callable_serialization.phpt | 4 +- 65 files changed, 1454 insertions(+), 2710 deletions(-) delete mode 100644 src/State/Hub.php delete mode 100644 src/State/HubAdapter.php delete mode 100644 src/State/HubInterface.php delete mode 100644 src/State/Layer.php create mode 100644 src/State/ScopeManager.php create mode 100644 src/State/ScopeType.php delete mode 100644 tests/State/HubAdapterTest.php delete mode 100644 tests/State/HubTest.php delete mode 100644 tests/State/LayerTest.php create mode 100644 tests/State/ScopeManagerTest.php diff --git a/src/Attributes/AttributeBag.php b/src/Attributes/AttributeBag.php index 0d5b3018b5..5e9ed7d9f0 100644 --- a/src/Attributes/AttributeBag.php +++ b/src/Attributes/AttributeBag.php @@ -30,6 +30,17 @@ public function set(string $key, $value): self return $this; } + public function __clone() + { + $attributes = []; + + foreach ($this->attributes as $key => $attribute) { + $attributes[$key] = clone $attribute; + } + + $this->attributes = $attributes; + } + public function get(string $key): ?Attribute { return $this->attributes[$key] ?? null; diff --git a/src/Client.php b/src/Client.php index e54cb3d29d..ae692db0ba 100644 --- a/src/Client.php +++ b/src/Client.php @@ -202,6 +202,30 @@ public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scop return null; } + /** + * {@inheritdoc} + * + * @param int|float|null $duration + */ + public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string + { + $options = $this->getOptions(); + $event = Event::createCheckIn(); + $checkIn = new CheckIn( + $slug, + $status, + $checkInId, + $options->getRelease(), + $options->getEnvironment(), + $duration, + $monitorConfig + ); + $event->setCheckIn($checkIn); + $this->captureEvent($event, null, SentrySdk::getMergedScope()); + + return $checkIn->getId(); + } + /** * {@inheritdoc} */ diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 0aab383a4d..c9e75f76d7 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -56,6 +56,13 @@ public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): */ public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId; + /** + * Captures a check-in. + * + * @param int|float|null $duration + */ + public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string; + /** * Returns the integration instance if it is installed on the client. * diff --git a/src/Integration/AbstractErrorListenerIntegration.php b/src/Integration/AbstractErrorListenerIntegration.php index a8894a7e29..db7059be41 100644 --- a/src/Integration/AbstractErrorListenerIntegration.php +++ b/src/Integration/AbstractErrorListenerIntegration.php @@ -6,23 +6,22 @@ use Sentry\Event; use Sentry\ExceptionMechanism; -use Sentry\State\HubInterface; +use Sentry\SentrySdk; use Sentry\State\Scope; abstract class AbstractErrorListenerIntegration implements IntegrationInterface { /** - * Captures the exception using the given hub instance. + * Captures the exception using a forked scope. * - * @param HubInterface $hub The hub instance - * @param \Throwable $exception The exception instance + * @param \Throwable $exception The exception instance */ - protected function captureException(HubInterface $hub, \Throwable $exception): void + protected function captureException(\Throwable $exception): void { - $hub->withScope(function (Scope $scope) use ($hub, $exception): void { + SentrySdk::withScope(function (Scope $scope) use ($exception): void { $scope->addEventProcessor(\Closure::fromCallable([$this, 'addExceptionMechanismToEvent'])); - $hub->captureException($exception); + \Sentry\captureException($exception); }); } diff --git a/src/Integration/EnvironmentIntegration.php b/src/Integration/EnvironmentIntegration.php index c404216ea9..adf4f6a53a 100644 --- a/src/Integration/EnvironmentIntegration.php +++ b/src/Integration/EnvironmentIntegration.php @@ -24,7 +24,7 @@ final class EnvironmentIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $integration = SentrySdk::getClient()->getIntegration(self::class); if ($integration !== null) { $event->setRuntimeContext($integration->updateRuntimeContext($event->getRuntimeContext())); diff --git a/src/Integration/ErrorListenerIntegration.php b/src/Integration/ErrorListenerIntegration.php index a4b95c2ba7..37426a6da6 100644 --- a/src/Integration/ErrorListenerIntegration.php +++ b/src/Integration/ErrorListenerIntegration.php @@ -33,15 +33,13 @@ public function setupOnce(): void ErrorHandler::registerOnceErrorHandler($this->options) ->addErrorHandlerListener( static function (\ErrorException $exception): void { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); if ($integration === null) { return; } - $client = $currentHub->getClient(); - if ($exception instanceof SilencedErrorException && !$client->getOptions()->shouldCaptureSilencedErrors()) { return; } @@ -50,7 +48,7 @@ static function (\ErrorException $exception): void { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); } ); } diff --git a/src/Integration/ExceptionListenerIntegration.php b/src/Integration/ExceptionListenerIntegration.php index 18e7afc757..84b4ec9b3f 100644 --- a/src/Integration/ExceptionListenerIntegration.php +++ b/src/Integration/ExceptionListenerIntegration.php @@ -20,8 +20,8 @@ public function setupOnce(): void { $errorHandler = ErrorHandler::registerOnceExceptionHandler(); $errorHandler->addExceptionHandlerListener(static function (\Throwable $exception): void { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->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 @@ -29,7 +29,7 @@ public function setupOnce(): void return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); }); } } diff --git a/src/Integration/FatalErrorListenerIntegration.php b/src/Integration/FatalErrorListenerIntegration.php index 3cc0566688..d0d478527a 100644 --- a/src/Integration/FatalErrorListenerIntegration.php +++ b/src/Integration/FatalErrorListenerIntegration.php @@ -22,20 +22,18 @@ public function setupOnce(): void { $errorHandler = ErrorHandler::registerOnceFatalErrorHandler(); $errorHandler->addFatalErrorHandlerListener(static function (FatalErrorException $exception): void { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); if ($integration === null) { return; } - $client = $currentHub->getClient(); - if (!($client->getOptions()->getErrorTypes() & $exception->getSeverity())) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); }); } } diff --git a/src/Integration/FrameContextifierIntegration.php b/src/Integration/FrameContextifierIntegration.php index a49693d588..d5bbafc507 100644 --- a/src/Integration/FrameContextifierIntegration.php +++ b/src/Integration/FrameContextifierIntegration.php @@ -41,7 +41,7 @@ public function __construct(?LoggerInterface $logger = null) public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { - $client = SentrySdk::getCurrentHub()->getClient(); + $client = SentrySdk::getClient(); $maxContextLines = $client->getOptions()->getContextLines(); $integration = $client->getIntegration(self::class); diff --git a/src/Integration/ModulesIntegration.php b/src/Integration/ModulesIntegration.php index affe1e062b..f1234f355e 100644 --- a/src/Integration/ModulesIntegration.php +++ b/src/Integration/ModulesIntegration.php @@ -26,7 +26,7 @@ final class ModulesIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $integration = SentrySdk::getClient()->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 diff --git a/src/Integration/RequestIntegration.php b/src/Integration/RequestIntegration.php index 431d7fccf2..223b766bc5 100644 --- a/src/Integration/RequestIntegration.php +++ b/src/Integration/RequestIntegration.php @@ -92,15 +92,13 @@ public function __construct(?RequestFetcherInterface $requestFetcher = null, arr public function setupOnce(): void { Scope::addGlobalEventProcessor(function (Event $event): Event { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); if ($integration === null) { return $event; } - $client = $currentHub->getClient(); - $this->processEvent($event, $client->getOptions()); return $event; diff --git a/src/Integration/TransactionIntegration.php b/src/Integration/TransactionIntegration.php index 1ed22d7538..eaba401318 100644 --- a/src/Integration/TransactionIntegration.php +++ b/src/Integration/TransactionIntegration.php @@ -24,7 +24,8 @@ final class TransactionIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event, EventHint $hint): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->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 diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index a1922d3466..1aa8456098 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -9,7 +9,6 @@ use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; -use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Util\Arr; use Sentry\Util\Str; @@ -37,8 +36,7 @@ public function add( ): void { $timestamp = microtime(true); - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $options = $client->getOptions(); $sdkLogger = $options->getLogger(); @@ -67,31 +65,32 @@ public function add( $formattedMessage = $message; } - $log = (new Log($timestamp, $this->getTraceId($hub), $level, $formattedMessage)) + $log = (new Log($timestamp, $this->getTraceId(), $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); + ->setAttribute('sentry.trace.parent_span_id', SentrySdk::getCurrentScope()->getSpan() ? SentrySdk::getCurrentScope()->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()); - } + $scope = SentrySdk::getMergedScope(); + $this->applyScopeAttributes($log, $scope); + + $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); @@ -155,12 +154,11 @@ public function flush(): ?EventId return null; } - $hub = SentrySdk::getCurrentHub(); $event = Event::createLogs()->setLogs($this->logs); $this->logs = []; - return $hub->captureEvent($event); + return SentrySdk::getClient()->captureEvent($event, null, SentrySdk::getMergedScope()); } /** @@ -171,20 +169,23 @@ public function all(): array return $this->logs; } - private function getTraceId(HubInterface $hub): string + private function getTraceId(): string { - $span = $hub->getSpan(); + $span = SentrySdk::getCurrentScope()->getSpan(); if ($span !== null) { return (string) $span->getTraceId(); } - $traceId = ''; - - $hub->configureScope(function (Scope $scope) use (&$traceId) { - $traceId = (string) $scope->getPropagationContext()->getTraceId(); - }); + return (string) SentrySdk::getIsolationScope()->getPropagationContext()->getTraceId(); + } - return $traceId; + private function applyScopeAttributes(Log $log, Scope $scope): void + { + foreach ($scope->getAttributes()->all() as $key => $attribute) { + if ($log->attributes()->get($key) === null) { + $log->setAttribute($key, $attribute); + } + } } } diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index e18ca7ddfa..f5adac4620 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -12,7 +12,6 @@ use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\SentrySdk; -use Sentry\State\Scope; use Sentry\Unit; use Sentry\Util\RingBuffer; @@ -53,8 +52,7 @@ public function add( array $attributes, ?Unit $unit ): void { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); if (!\is_int($value) && !\is_float($value)) { if ($client !== null) { @@ -64,6 +62,9 @@ public function add( return; } + $scope = SentrySdk::getMergedScope(); + $scopeAttributes = $scope->getAttributes()->all(); + if ($client instanceof Client) { $options = $client->getOptions(); @@ -79,20 +80,18 @@ public function add( ]; if ($options->shouldSendDefaultPii()) { - $hub->configureScope(function (Scope $scope) use (&$defaultAttributes) { - $user = $scope->getUser(); - if ($user !== null) { - if ($user->getId() !== null) { - $defaultAttributes['user.id'] = $user->getId(); - } - if ($user->getEmail() !== null) { - $defaultAttributes['user.email'] = $user->getEmail(); - } - if ($user->getUsername() !== null) { - $defaultAttributes['user.name'] = $user->getUsername(); - } + $user = $scope->getUser(); + if ($user !== null) { + if ($user->getId() !== null && !isset($scopeAttributes['user.id'])) { + $scopeAttributes['user.id'] = $user->getId(); + } + if ($user->getEmail() !== null && !isset($scopeAttributes['user.email'])) { + $scopeAttributes['user.email'] = $user->getEmail(); } - }); + if ($user->getUsername() !== null && !isset($scopeAttributes['user.name'])) { + $scopeAttributes['user.name'] = $user->getUsername(); + } + } } $release = $options->getRelease(); @@ -100,22 +99,23 @@ public function add( $defaultAttributes['sentry.release'] = $release; } + $attributes = array_merge($scopeAttributes, $attributes); $attributes += $defaultAttributes; + } else { + $attributes = array_merge($scopeAttributes, $attributes); } $spanId = null; $traceId = null; - $span = $hub->getSpan(); + $span = SentrySdk::getCurrentScope()->getSpan(); if ($span !== null) { $spanId = $span->getSpanId(); $traceId = $span->getTraceId(); } else { - $hub->configureScope(function (Scope $scope) use (&$traceId, &$spanId) { - $propagationContext = $scope->getPropagationContext(); - $traceId = $propagationContext->getTraceId(); - $spanId = $propagationContext->getSpanId(); - }); + $propagationContext = SentrySdk::getIsolationScope()->getPropagationContext(); + $traceId = $propagationContext->getTraceId(); + $spanId = $propagationContext->getSpanId(); } $metricTypeClass = self::METRIC_TYPES[$type]; @@ -140,9 +140,8 @@ public function flush(): ?EventId return null; } - $hub = SentrySdk::getCurrentHub(); $event = Event::createMetrics()->setMetrics($this->metrics->drain()); - return $hub->captureEvent($event); + return SentrySdk::getClient()->captureEvent($event, null, SentrySdk::getMergedScope()); } } diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index bb2b60ea06..98a00f6cec 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -10,9 +10,7 @@ use Monolog\LogRecord; use Psr\Log\LogLevel; use Sentry\Breadcrumb; -use Sentry\Event; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\SentrySdk; /** * This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see Scope}, @@ -21,23 +19,15 @@ final class BreadcrumbHandler extends AbstractProcessingHandler { /** - * @var HubInterface - */ - private $hub; - - /** - * @param HubInterface $hub The hub to which errors are reported - * @param int|string $level The minimum logging level at which this - * handler will be triggered - * @param bool $bubble Whether the messages that are handled can - * bubble up the stack or not + * @param int|string $level The minimum logging level at which this + * handler will be triggered + * @param bool $bubble Whether the messages that are handled can + * bubble up the stack or not * * @phpstan-param int|string|Level|LogLevel::* $level */ - public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true) + public function __construct($level = Logger::DEBUG, bool $bubble = true) { - $this->hub = $hub; - parent::__construct($level, $bubble); } @@ -63,7 +53,7 @@ protected function write($record): void $record['datetime']->getTimestamp() ); - $this->hub->addBreadcrumb($breadcrumb); + SentrySdk::getIsolationScope()->addBreadcrumb($breadcrumb); } /** diff --git a/src/Monolog/Handler.php b/src/Monolog/Handler.php index 3e8d52bba1..03490d2f0b 100644 --- a/src/Monolog/Handler.php +++ b/src/Monolog/Handler.php @@ -9,12 +9,11 @@ use Monolog\LogRecord; use Sentry\Event; use Sentry\EventHint; -use Sentry\State\HubInterface; +use Sentry\SentrySdk; use Sentry\State\Scope; /** - * This Monolog handler logs every message to a Sentry's server using the given - * hub instance. + * This Monolog handler logs every message to a Sentry's server. * * @author Stefano Arlandini */ @@ -24,11 +23,6 @@ final class Handler extends AbstractProcessingHandler private const CONTEXT_EXCEPTION_KEY = 'exception'; - /** - * @var HubInterface - */ - private $hub; - /** * @var bool */ @@ -37,13 +31,11 @@ final class Handler extends AbstractProcessingHandler /** * {@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) + public function __construct($level = Logger::DEBUG, bool $bubble = true, bool $fillExtraContext = false) { parent::__construct($level, $bubble); - $this->hub = $hub; $this->fillExtraContext = $fillExtraContext; } @@ -63,7 +55,7 @@ protected function doWrite($record): void $hint->exception = $record['context']['exception']; } - $this->hub->withScope(function (Scope $scope) use ($record, $event, $hint): void { + SentrySdk::withScope(function (Scope $scope) use ($record, $event, $hint): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); @@ -79,7 +71,7 @@ protected function doWrite($record): void $scope->setExtra('monolog.extra', $monologExtraData); } - $this->hub->captureEvent($event, $hint); + \Sentry\captureEvent($event, $hint); }); } diff --git a/src/NoOpClient.php b/src/NoOpClient.php index 364b6073e3..902b6eb3de 100644 --- a/src/NoOpClient.php +++ b/src/NoOpClient.php @@ -74,6 +74,11 @@ public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scop return null; } + public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string + { + return null; + } + public function getIntegration(string $className): ?IntegrationInterface { return null; diff --git a/src/SentrySdk.php b/src/SentrySdk.php index 29d43931de..9027543673 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -4,8 +4,11 @@ namespace Sentry; -use Sentry\State\Hub; -use Sentry\State\HubInterface; +use Sentry\State\Scope; +use Sentry\State\ScopeManager; +use Sentry\Tracing\SamplingContext; +use Sentry\Tracing\Transaction; +use Sentry\Tracing\TransactionContext; /** * This class is the main entry point for all the most common SDK features. @@ -15,9 +18,9 @@ final class SentrySdk { /** - * @var HubInterface|null The current hub + * @var ScopeManager|null The scope manager */ - private static $currentHub; + private static $scopeManager; /** * Constructor. @@ -27,41 +30,262 @@ private function __construct() } /** - * Initializes the SDK by creating a new hub instance each time this method - * gets called. + * Initializes the SDK by binding a client to the global scope. */ - public static function init(?ClientInterface $client = null): HubInterface + public static function init(?ClientInterface $client = null): void { if ($client === null) { $client = new NoOpClient(); } - self::$currentHub = new Hub($client); - return self::$currentHub; + $scopeManager = self::getScopeManager(); + $scopeManager->resetScopes(); + $scopeManager->getGlobalScope()->bindClient($client); + } + + public static function getGlobalScope(): Scope + { + return self::getScopeManager()->getGlobalScope(); + } + + public static function getIsolationScope(): Scope + { + return self::getScopeManager()->getIsolationScope(); + } + + public static function getCurrentScope(): Scope + { + return self::getScopeManager()->getCurrentScope(); } /** - * Gets the current hub. If it's not initialized then creates a new instance - * and sets it as current hub. + * Forks the current scope and executes the given callback within it. + * + * @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 */ - public static function getCurrentHub(): HubInterface + public static function withScope(callable $callback) { - if (self::$currentHub === null) { - self::$currentHub = new Hub(new NoOpClient()); - } + return self::getScopeManager()->withScope($callback); + } - return self::$currentHub; + /** + * Forks the isolation scope (and current scope) and executes the callback within it. + * + * @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 + */ + public static function withIsolationScope(callable $callback) + { + return self::getScopeManager()->withIsolationScope($callback); } /** - * Sets the current hub. + * Configures the isolation scope by invoking the callback with it. * - * @param HubInterface $hub The hub to set + * @param callable $callback The callback to be executed + */ + public static function configureScope(callable $callback): void + { + $callback(self::getIsolationScope()); + } + + /** + * Starts a new `Transaction` and returns it. This is the entry point to manual + * tracing instrumentation. + * + * @param TransactionContext $context Properties of the new transaction + * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} + */ + public static function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction + { + $client = self::getClient(); + $transaction = new Transaction($context, $client); + $options = $client->getOptions(); + $logger = $options->getLoggerOrNullLogger(); + + if (!$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 = self::getSampleRate( + $samplingContext->getParentSampled(), + $options->getTracesSampleRate() ?? 0 + ); + $sampleSource = $samplingContext->getParentSampled() !== null ? 'parent:sampling_decision' : 'config:traces_sample_rate'; + } + } + + if (!self::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 (self::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; + } + + public static function getMergedScope(): Scope + { + return Scope::mergeScopes( + self::getGlobalScope(), + self::getIsolationScope(), + self::getCurrentScope() + ); + } + + public static function getClient(): ClientInterface + { + $currentScope = self::getCurrentScope(); + $client = $currentScope->getClient(); + if ($client !== null && !$client instanceof NoOpClient) { + return $client; + } + + $isolationScope = self::getIsolationScope(); + $client = $isolationScope->getClient(); + if ($client !== null && !$client instanceof NoOpClient) { + return $client; + } + + $globalScope = self::getGlobalScope(); + $client = $globalScope->getClient(); + if ($client !== null) { + return $client; + } + + return new NoOpClient(); + } + + private static function getScopeManager(): ScopeManager + { + if (self::$scopeManager === null) { + self::$scopeManager = new ScopeManager(); + } + + return self::$scopeManager; + } + + private static function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float + { + if ($hasParentBeenSampled === true) { + return 1.0; + } + + if ($hasParentBeenSampled === false) { + return 0.0; + } + + return $fallbackSampleRate; + } + + /** + * @param mixed $sampleRate */ - public static function setCurrentHub(HubInterface $hub): HubInterface + private static function sample($sampleRate): bool { - self::$currentHub = $hub; + 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 static function isValidSampleRate($sampleRate): bool + { + if (!\is_float($sampleRate) && !\is_int($sampleRate)) { + return false; + } + + if ($sampleRate < 0 || $sampleRate > 1) { + return false; + } - return $hub; + return true; } } diff --git a/src/State/Hub.php b/src/State/Hub.php deleted file mode 100644 index 330e133454..0000000000 --- a/src/State/Hub.php +++ /dev/null @@ -1,414 +0,0 @@ -stack[] = 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 - { - return $this->lastEventId = $this->getClient()->captureMessage($message, $level, $this->getScope(), $hint); - } - - /** - * {@inheritdoc} - */ - public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureException($exception, $this->getScope(), $hint); - } - - /** - * {@inheritdoc} - */ - public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureEvent($event, $hint, $this->getScope()); - } - - /** - * {@inheritdoc} - */ - public function captureLastError(?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureLastError($this->getScope(), $hint); - } - - /** - * {@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 instanceof NoOpClient) { - 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(); - - // No point in storing breadcrumbs if the client will never send them - if ($client instanceof NoOpClient) { - 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; - } - - public function addAttachment(Attachment $attachment): bool - { - // No point in storing attachments if the client will never send them - if ($this->getClient() instanceof NoOpClient) { - return false; - } - - $this->getScope()->addAttachment($attachment); - - return true; - } - - /** - * {@inheritdoc} - */ - public function getIntegration(string $className): ?IntegrationInterface - { - return $this->getClient()->getIntegration($className); - } - - /** - * {@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); - $options = $this->getClient()->getOptions(); - $logger = $options->getLoggerOrNullLogger(); - - if (!$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; - } -} diff --git a/src/State/HubAdapter.php b/src/State/HubAdapter.php deleted file mode 100644 index 9c52108d7b..0000000000 --- a/src/State/HubAdapter.php +++ /dev/null @@ -1,230 +0,0 @@ -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 addAttachment(Attachment $attachment): bool - { - return SentrySdk::getCurrentHub()->addAttachment($attachment); - } - - /** - * {@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.'); - } -} diff --git a/src/State/HubInterface.php b/src/State/HubInterface.php deleted file mode 100644 index dcce154ab5..0000000000 --- a/src/State/HubInterface.php +++ /dev/null @@ -1,161 +0,0 @@ - $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; - - /** - * Records a new attachment that will be attached to error and transaction events. - */ - public function addAttachment(Attachment $attachment): bool; -} diff --git a/src/State/Layer.php b/src/State/Layer.php deleted file mode 100644 index 3befbd80a8..0000000000 --- a/src/State/Layer.php +++ /dev/null @@ -1,82 +0,0 @@ -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 $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; - } -} diff --git a/src/State/Scope.php b/src/State/Scope.php index b313eb73ba..baa514c06a 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -5,7 +5,9 @@ namespace Sentry\State; use Sentry\Attachment\Attachment; +use Sentry\Attributes\AttributeBag; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; use Sentry\EventType; @@ -36,6 +38,16 @@ class Scope */ private $propagationContext; + /** + * @var ScopeType|null + */ + private $type; + + /** + * @var ClientInterface|null Client bound to this scope + */ + private $client; + /** * @var Breadcrumb[] The list of breadcrumbs recorded in this scope */ @@ -95,6 +107,11 @@ class Scope */ private $attachments = []; + /** + * @var AttributeBag + */ + private $attributes; + /** * @var callable[] List of event processors * @@ -102,9 +119,11 @@ class Scope */ private static $globalEventProcessors = []; - public function __construct(?PropagationContext $propagationContext = null) + public function __construct(?PropagationContext $propagationContext = null, ?ScopeType $type = null) { $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); + $this->type = $type; + $this->attributes = new AttributeBag(); } /** @@ -384,6 +403,7 @@ public function clear(): self $this->extra = []; $this->contexts = []; $this->attachments = []; + $this->attributes = new AttributeBag(); return $this; } @@ -494,6 +514,39 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op return $event; } + /** + * Sets attributes on the scope. + * + * @param array $attributes + */ + public function setAttributes(array $attributes): self + { + foreach ($attributes as $key => $value) { + $this->setAttribute($key, $value); + } + + return $this; + } + + /** + * Sets an attribute on the scope. + * + * @param mixed $value + */ + public function setAttribute(string $key, $value): self + { + $this->attributes->set($key, $value); + + return $this; + } + + public function removeAttribute(string $key): self + { + $this->attributes->forget($key); + + return $this; + } + /** * Returns the span that is on the scope. */ @@ -533,6 +586,38 @@ public function getPropagationContext(): PropagationContext return $this->propagationContext; } + public function getClient(): ?ClientInterface + { + return $this->client; + } + + public function setClient(?ClientInterface $client): self + { + $this->client = $client; + + return $this; + } + + /** + * Binds the given client to this scope. + */ + public function bindClient(ClientInterface $client): self + { + return $this->setClient($client); + } + + public function getType(): ?ScopeType + { + return $this->type; + } + + public function setType(ScopeType $type): self + { + $this->type = $type; + + return $this; + } + public function setPropagationContext(PropagationContext $propagationContext): self { $this->propagationContext = $propagationContext; @@ -540,6 +625,168 @@ public function setPropagationContext(PropagationContext $propagationContext): s return $this; } + /** + * @internal + */ + public function getAttributes(): AttributeBag + { + return $this->attributes; + } + + /** + * @internal + * + * Merges data from the given scope into this one, overwriting existing values + * where applicable. + */ + public function mergeFrom(self $scope): self + { + if ($scope->level !== null) { + $this->level = $scope->level; + } + + if (!empty($scope->fingerprint)) { + $this->fingerprint = array_merge($this->fingerprint, $scope->fingerprint); + } + + if (!empty($scope->breadcrumbs)) { + $this->breadcrumbs = array_merge($this->breadcrumbs, $scope->breadcrumbs); + } + + if (!empty($scope->tags)) { + $this->tags = array_merge($this->tags, $scope->tags); + } + + if (!empty($scope->flags)) { + $this->flags = array_merge($this->flags, $scope->flags); + + if (\count($this->flags) > self::MAX_FLAGS) { + $this->flags = array_slice($this->flags, -self::MAX_FLAGS); + } + } + + if (!empty($scope->extra)) { + $this->extra = array_merge($this->extra, $scope->extra); + } + + if (!empty($scope->contexts)) { + $this->contexts = array_merge($this->contexts, $scope->contexts); + } + + if ($scope->user !== null) { + if ($this->user === null) { + $this->user = clone $scope->user; + } else { + $user = clone $this->user; + $user->merge($scope->user); + $this->user = $user; + } + } + + if ($scope->span !== null) { + $this->span = $scope->span; + } + + if (!empty($scope->attachments)) { + $this->attachments = array_merge($this->attachments, $scope->attachments); + } + + if (!empty($scope->attributes->all())) { + foreach ($scope->attributes->all() as $key => $attribute) { + $this->attributes->set($key, $attribute); + } + } + + if (!empty($scope->eventProcessors)) { + $this->eventProcessors = array_merge($this->eventProcessors, $scope->eventProcessors); + } + + if ($scope->propagationContext !== null && $scope->getType() !== ScopeType::current()) { + $this->propagationContext = $scope->propagationContext; + } + + return $this; + } + + /** + * @internal + */ + public static function mergeScopes(Scope $globalScope, ?Scope $isolationScope, ?Scope $currentScope): self + { + $mergedScope = clone $globalScope; + + if ($isolationScope !== null) { + $mergedScope->mergeFrom($isolationScope); + } + + if ($currentScope !== null) { + $mergedScope->mergeFrom($currentScope); + } + + $mergedScope->setType(ScopeType::merged()); + $mergedScope->sortBreadcrumbsByTimestamp(); + + return $mergedScope; + } + + /** + * @internal + */ + public static function applyToEventFromScopes( + Event $event, + Scope $globalScope, + ?Scope $isolationScope, + ?Scope $currentScope, + ?EventHint $hint = null, + ?Options $options = null + ): ?Event { + $mergedScope = self::mergeScopes($globalScope, $isolationScope, $currentScope); + + return $mergedScope->applyToEvent($event, $hint, $options); + } + + /** + * @internal + * + * Sorts breadcrumbs by their timestamp (ascending), preserving insertion order for ties. + */ + public function sortBreadcrumbsByTimestamp(): self + { + if (\count($this->breadcrumbs) <= 1) { + return $this; + } + + $indexed = []; + + foreach ($this->breadcrumbs as $index => $breadcrumb) { + $indexed[] = [$breadcrumb, $index]; + } + + usort($indexed, static function (array $left, array $right): int { + /** @var Breadcrumb $leftBreadcrumb */ + $leftBreadcrumb = $left[0]; + /** @var Breadcrumb $rightBreadcrumb */ + $rightBreadcrumb = $right[0]; + $leftIndex = $left[1]; + $rightIndex = $right[1]; + + $leftTimestamp = $leftBreadcrumb->getTimestamp(); + $rightTimestamp = $rightBreadcrumb->getTimestamp(); + + if ($leftTimestamp === $rightTimestamp) { + return $leftIndex <=> $rightIndex; + } + + return $leftTimestamp <=> $rightTimestamp; + }); + + $this->breadcrumbs = array_map(static function (array $entry): Breadcrumb { + return $entry[0]; + }, $indexed); + + return $this; + } + public function __clone() { if ($this->user !== null) { @@ -548,6 +795,7 @@ public function __clone() if ($this->propagationContext !== null) { $this->propagationContext = clone $this->propagationContext; } + $this->attributes = clone $this->attributes; } public function addAttachment(Attachment $attachment): self diff --git a/src/State/ScopeManager.php b/src/State/ScopeManager.php new file mode 100644 index 0000000000..e5771a313f --- /dev/null +++ b/src/State/ScopeManager.php @@ -0,0 +1,164 @@ +setClient(new NoOpClient()); + } + + $globalScope->setType(ScopeType::global()); + $this->globalScope = $globalScope; + } + + public function getGlobalScope(): Scope + { + return $this->globalScope; + } + + public function getIsolationScope(): Scope + { + return $this->getOrCreateIsolationScope(); + } + + public function getCurrentScope(): Scope + { + return $this->getOrCreateCurrentScope(); + } + + /** + * Forks the current scope and executes the given callback within it. + * + * @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 + */ + public function withScope(callable $callback) + { + $scope = $this->pushCurrentScope(); + + try { + return $callback($scope); + } finally { + $this->popCurrentScope(); + } + } + + /** + * Forks the isolation scope (and current scope) and executes the callback within it. + * + * @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 + */ + public function withIsolationScope(callable $callback) + { + $this->pushCurrentScope(); + $scope = $this->pushIsolationScope(); + + try { + return $callback($scope); + } finally { + $this->popCurrentScope(); + $this->popIsolationScope(); + } + } + + public function resetScopes(): void + { + $this->isolationScopeStack = []; + $this->currentScopeStack = []; + } + + private function pushIsolationScope(): Scope + { + $scope = clone $this->getOrCreateIsolationScope(); + $scope->setType(ScopeType::isolation()); + $this->isolationScopeStack[] = $scope; + + return $scope; + } + + private function popIsolationScope(): bool + { + if (\count($this->isolationScopeStack) <= 1) { + return false; + } + + return array_pop($this->isolationScopeStack) !== null; + } + + private function pushCurrentScope(): Scope + { + $scope = clone $this->getOrCreateCurrentScope(); + $scope->setType(ScopeType::current()); + $this->currentScopeStack[] = $scope; + + return $scope; + } + + private function popCurrentScope(): bool + { + if (\count($this->currentScopeStack) <= 1) { + return false; + } + + return array_pop($this->currentScopeStack) !== null; + } + + private function getOrCreateIsolationScope(): Scope + { + if (empty($this->isolationScopeStack)) { + $this->isolationScopeStack[] = new Scope(null, ScopeType::isolation()); + } + + return $this->isolationScopeStack[\count($this->isolationScopeStack) - 1]; + } + + private function getOrCreateCurrentScope(): Scope + { + if (empty($this->currentScopeStack)) { + $this->currentScopeStack[] = new Scope(null, ScopeType::current()); + } + + return $this->currentScopeStack[\count($this->currentScopeStack) - 1]; + } +} diff --git a/src/State/ScopeType.php b/src/State/ScopeType.php new file mode 100644 index 0000000000..7f225940e2 --- /dev/null +++ b/src/State/ScopeType.php @@ -0,0 +1,60 @@ + + */ + private static $instances = []; + + private function __construct(string $value) + { + $this->value = $value; + } + + public static function global(): self + { + return self::getInstance('global'); + } + + public static function isolation(): self + { + return self::getInstance('isolation'); + } + + public static function current(): self + { + return self::getInstance('current'); + } + + public static function merged(): self + { + return self::getInstance('merged'); + } + + 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]; + } +} diff --git a/src/Tracing/DynamicSamplingContext.php b/src/Tracing/DynamicSamplingContext.php index 2f6f3f3adb..1d199275a5 100644 --- a/src/Tracing/DynamicSamplingContext.php +++ b/src/Tracing/DynamicSamplingContext.php @@ -4,8 +4,8 @@ namespace Sentry\Tracing; +use Sentry\ClientInterface; use Sentry\Options; -use Sentry\State\HubInterface; use Sentry\State\Scope; /** @@ -149,7 +149,7 @@ public static function fromHeader(string $header): self * * @see https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage-header */ - public static function fromTransaction(Transaction $transaction, HubInterface $hub): self + public static function fromTransaction(Transaction $transaction, ClientInterface $client): self { $samplingContext = new self(); $samplingContext->set('trace_id', (string) $transaction->getTraceId()); @@ -164,8 +164,6 @@ public static function fromTransaction(Transaction $transaction, HubInterface $h $samplingContext->set('transaction', $transaction->getName()); } - $client = $hub->getClient(); - $options = $client->getOptions(); if ($options->getDsn() !== null && $options->getDsn()->getPublicKey() !== null) { diff --git a/src/Tracing/GuzzleTracingMiddleware.php b/src/Tracing/GuzzleTracingMiddleware.php index c1615dff5a..ad07058200 100644 --- a/src/Tracing/GuzzleTracingMiddleware.php +++ b/src/Tracing/GuzzleTracingMiddleware.php @@ -11,7 +11,6 @@ use Sentry\Breadcrumb; use Sentry\ClientInterface; use Sentry\SentrySdk; -use Sentry\State\HubInterface; use function Sentry\getBaggage; use function Sentry\getTraceparent; @@ -21,13 +20,12 @@ */ final class GuzzleTracingMiddleware { - public static function trace(?HubInterface $hub = null): \Closure + public static function trace(): \Closure { - return static function (callable $handler) use ($hub): \Closure { - return static function (RequestInterface $request, array $options) use ($hub, $handler) { - $hub = $hub ?? SentrySdk::getCurrentHub(); - $client = $hub->getClient(); - $parentSpan = $hub->getSpan(); + return static function (callable $handler): \Closure { + return static function (RequestInterface $request, array $options) use ($handler) { + $client = SentrySdk::getClient(); + $parentSpan = SentrySdk::getCurrentScope()->getSpan(); $partialUri = Uri::fromParts([ 'scheme' => $request->getUri()->getScheme(), @@ -59,7 +57,7 @@ public static function trace(?HubInterface $hub = null): \Closure $childSpan = $parentSpan->startChild($spanContext); - $hub->setSpan($childSpan); + SentrySdk::getCurrentScope()->setSpan($childSpan); } if (self::shouldAttachTracingHeaders($client, $request)) { @@ -68,13 +66,13 @@ public static function trace(?HubInterface $hub = null): \Closure ->withHeader('baggage', getBaggage()); } - $handlerPromiseCallback = static function ($responseOrException) use ($hub, $spanAndBreadcrumbData, $childSpan, $parentSpan, $partialUri) { + $handlerPromiseCallback = static function ($responseOrException) use ($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); + SentrySdk::getCurrentScope()->setSpan($parentSpan); } $response = null; @@ -108,7 +106,7 @@ public static function trace(?HubInterface $hub = null): \Closure } } - $hub->addBreadcrumb(new Breadcrumb( + $breadcrumb = new Breadcrumb( $breadcrumbLevel, Breadcrumb::TYPE_HTTP, 'http', @@ -116,7 +114,8 @@ public static function trace(?HubInterface $hub = null): \Closure array_merge([ 'url' => (string) $partialUri, ], $spanAndBreadcrumbData) - )); + ); + SentrySdk::getIsolationScope()->addBreadcrumb($breadcrumb); if ($responseOrException instanceof \Throwable) { throw $responseOrException; diff --git a/src/Tracing/PropagationContext.php b/src/Tracing/PropagationContext.php index b8c7f6a292..af7b1ddba1 100644 --- a/src/Tracing/PropagationContext.php +++ b/src/Tracing/PropagationContext.php @@ -5,7 +5,6 @@ namespace Sentry\Tracing; use Sentry\SentrySdk; -use Sentry\State\Scope; use Sentry\Tracing\Traits\TraceHeaderParserTrait; final class PropagationContext @@ -84,12 +83,9 @@ public function toTraceparent(): string public function toBaggage(): string { if ($this->dynamicSamplingContext === null) { - $hub = SentrySdk::getCurrentHub(); - $options = $hub->getClient()->getOptions(); - - $hub->configureScope(function (Scope $scope) use ($options) { - $this->dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $scope); - }); + $options = SentrySdk::getClient()->getOptions(); + $scope = SentrySdk::getIsolationScope(); + $this->dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $scope); } return (string) $this->dynamicSamplingContext; diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index b2aa1adea5..e79f6d26c0 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -299,7 +299,7 @@ public function setStatus(?SpanStatus $status) */ public function setHttpStatus(int $statusCode) { - SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($statusCode) { + SentrySdk::configureScope(function (Scope $scope) use ($statusCode) { $scope->setContext('response', [ 'status_code' => $statusCode, ]); diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index e2baa09b90..a7e2dada43 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -8,7 +8,7 @@ use Sentry\EventId; use Sentry\Profiling\Profiler; use Sentry\SentrySdk; -use Sentry\State\HubInterface; +use Sentry\ClientInterface; /** * This class stores all the information about a Transaction. @@ -16,9 +16,9 @@ final class Transaction extends Span { /** - * @var HubInterface The hub instance + * @var ClientInterface The client instance */ - private $hub; + private $client; /** * @var string Name of the transaction @@ -44,15 +44,15 @@ final class Transaction extends Span * Span constructor. * * @param TransactionContext $context The context to create the transaction with - * @param HubInterface|null $hub Instance of a hub to flush the transaction + * @param ClientInterface|null $client Instance of a client to flush the transaction * * @internal */ - public function __construct(TransactionContext $context, ?HubInterface $hub = null) + public function __construct(TransactionContext $context, ?ClientInterface $client = null) { parent::__construct($context); - $this->hub = $hub ?? SentrySdk::getCurrentHub(); + $this->client = $client ?? SentrySdk::getClient(); $this->name = $context->getName(); $this->metadata = $context->getMetadata(); $this->transaction = $this; @@ -97,7 +97,7 @@ public function getDynamicSamplingContext(): DynamicSamplingContext return $this->metadata->getDynamicSamplingContext(); } - $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, $this->hub); + $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, $this->client); $this->getMetadata()->setDynamicSamplingContext($samplingContext); return $samplingContext; @@ -122,7 +122,7 @@ public function initSpanRecorder(int $maxSpans = 1000): self public function initProfiler(): Profiler { if ($this->profiler === null) { - $this->profiler = new Profiler($this->hub->getClient()->getOptions()); + $this->profiler = new Profiler($this->client->getOptions()); } return $this->profiler; @@ -187,6 +187,6 @@ public function finish(?float $endTimestamp = null): ?EventId } } - return $this->hub->captureEvent($event); + return $this->client->captureEvent($event, null, SentrySdk::getMergedScope()); } } diff --git a/src/functions.php b/src/functions.php index e0a39c6857..9d5edbd153 100644 --- a/src/functions.php +++ b/src/functions.php @@ -16,7 +16,7 @@ use Sentry\Tracing\TransactionContext; /** - * Creates a new Client and Hub which will be set as current. + * Creates a new Client and binds it to the global scope. * * @param array{ * attach_stacktrace?: bool, @@ -68,7 +68,7 @@ function init(array $options = []): void { $client = ClientBuilder::create($options)->getClient(); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); } /** @@ -80,7 +80,7 @@ function init(array $options = []): void */ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); + return SentrySdk::getClient()->captureMessage($message, $level, SentrySdk::getMergedScope(), $hint); } /** @@ -91,7 +91,7 @@ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hi */ function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureException($exception, $hint); + return SentrySdk::getClient()->captureException($exception, SentrySdk::getMergedScope(), $hint); } /** @@ -102,7 +102,7 @@ function captureException(\Throwable $exception, ?EventHint $hint = null): ?Even */ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureEvent($event, $hint); + return SentrySdk::getClient()->captureEvent($event, $hint, SentrySdk::getMergedScope()); } /** @@ -112,7 +112,7 @@ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId */ function captureLastError(?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureLastError($hint); + return SentrySdk::getClient()->captureLastError(SentrySdk::getMergedScope(), $hint); } /** @@ -126,7 +126,7 @@ function captureLastError(?EventHint $hint = null): ?EventId */ function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { - return SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); + return SentrySdk::getClient()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** @@ -140,7 +140,7 @@ function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ? */ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorConfig = null) { - $checkInId = SentrySdk::getCurrentHub()->captureCheckIn($slug, CheckInStatus::inProgress(), null, $monitorConfig); + $checkInId = captureCheckIn($slug, CheckInStatus::inProgress(), null, $monitorConfig, null); $status = CheckInStatus::ok(); $duration = 0; @@ -156,7 +156,7 @@ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorCo throw $e; } finally { - SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); + captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } } @@ -174,11 +174,11 @@ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorCo */ 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) - ); + $breadcrumb = $category instanceof Breadcrumb + ? $category + : new Breadcrumb($level, $type, $category, $message, $metadata, $timestamp); + + SentrySdk::getIsolationScope()->addBreadcrumb($breadcrumb); } /** @@ -189,7 +189,7 @@ function addBreadcrumb($category, ?string $message = null, array $metadata = [], */ function configureScope(callable $callback): void { - SentrySdk::getCurrentHub()->configureScope($callback); + SentrySdk::configureScope($callback); } /** @@ -208,7 +208,7 @@ function configureScope(callable $callback): void */ function withScope(callable $callback) { - return SentrySdk::getCurrentHub()->withScope($callback); + return SentrySdk::withScope($callback); } /** @@ -231,7 +231,7 @@ function withScope(callable $callback) */ function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); + return SentrySdk::startTransaction($context, $customSamplingContext); } /** @@ -247,7 +247,7 @@ function startTransaction(TransactionContext $context, array $customSamplingCont */ function trace(callable $trace, SpanContext $context) { - return SentrySdk::getCurrentHub()->withScope(function (Scope $scope) use ($context, $trace) { + return SentrySdk::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. @@ -279,19 +279,15 @@ function trace(callable $trace, SpanContext $context) */ function getTraceparent(): string { - $hub = SentrySdk::getCurrentHub(); - $options = $hub->getClient()->getOptions(); + $options = SentrySdk::getClient()->getOptions(); if ($options->isTracingEnabled()) { - $span = SentrySdk::getCurrentHub()->getSpan(); + $span = SentrySdk::getCurrentScope()->getSpan(); if ($span !== null) { return $span->toTraceparent(); } } - $traceParent = ''; - $hub->configureScope(function (Scope $scope) use (&$traceParent) { - $traceParent = $scope->getPropagationContext()->toTraceparent(); - }); + $traceParent = SentrySdk::getIsolationScope()->getPropagationContext()->toTraceparent(); return $traceParent; } @@ -304,19 +300,15 @@ function getTraceparent(): string */ function getBaggage(): string { - $hub = SentrySdk::getCurrentHub(); - $options = $hub->getClient()->getOptions(); + $options = SentrySdk::getClient()->getOptions(); if ($options->isTracingEnabled()) { - $span = SentrySdk::getCurrentHub()->getSpan(); + $span = SentrySdk::getCurrentScope()->getSpan(); if ($span !== null) { return $span->toBaggage(); } } - $baggage = ''; - $hub->configureScope(function (Scope $scope) use (&$baggage) { - $baggage = $scope->getPropagationContext()->toBaggage(); - }); + $baggage = SentrySdk::getIsolationScope()->getPropagationContext()->toBaggage(); return $baggage; } @@ -329,11 +321,8 @@ function getBaggage(): string */ 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); - }); + $propagationContext = PropagationContext::fromHeaders($sentryTrace, $baggage); + SentrySdk::getIsolationScope()->setPropagationContext($propagationContext); return TransactionContext::fromHeaders($sentryTrace, $baggage); } @@ -357,7 +346,7 @@ function trace_metrics(): TraceMetrics */ function addFeatureFlag(string $name, bool $result): void { - SentrySdk::getCurrentHub()->configureScope(function (Scope $scope) use ($name, $result) { + SentrySdk::configureScope(function (Scope $scope) use ($name, $result) { $scope->addFeatureFlag($name, $result); }); } diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 488a40c7a5..287b0b287f 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -18,8 +18,6 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Hub; -use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\Span; @@ -52,23 +50,34 @@ public function testInit(): void { init(['default_integrations' => false]); - $this->assertNotNull(SentrySdk::getCurrentHub()->getClient()); + $this->assertNotNull(SentrySdk::getGlobalScope()->getClient()); } /** * @dataProvider captureMessageDataProvider */ - public function testCaptureMessage(array $functionCallArgs, array $expectedFunctionCallArgs): void + public function testCaptureMessage(array $functionCallArgs): void { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $message = $functionCallArgs[0]; + $level = $functionCallArgs[1] ?? null; + $hint = $functionCallArgs[2] ?? null; + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) + ->with( + $message, + $level, + $this->callback(function (Scope $scope): bool { + return $scope instanceof Scope; + }), + $hint + ) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->assertSame($eventId, captureMessage(...$functionCallArgs)); } @@ -80,11 +89,6 @@ public static function captureMessageDataProvider(): \Generator 'foo', Severity::debug(), ], - [ - 'foo', - Severity::debug(), - null, - ], ]; yield [ @@ -93,28 +97,32 @@ public static function captureMessageDataProvider(): \Generator Severity::debug(), new EventHint(), ], - [ - 'foo', - Severity::debug(), - new EventHint(), - ], ]; } /** * @dataProvider captureExceptionDataProvider */ - public function testCaptureException(array $functionCallArgs, array $expectedFunctionCallArgs): void + public function testCaptureException(array $functionCallArgs): void { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $exception = $functionCallArgs[0]; + $hint = $functionCallArgs[1] ?? null; + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureException') - ->with(...$expectedFunctionCallArgs) + ->with( + $exception, + $this->callback(function (Scope $scope): bool { + return $scope instanceof Scope; + }), + $hint + ) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->assertSame($eventId, captureException(...$functionCallArgs)); } @@ -125,10 +133,6 @@ public static function captureExceptionDataProvider(): \Generator [ new \Exception('foo'), ], - [ - new \Exception('foo'), - null, - ], ]; yield [ @@ -136,10 +140,6 @@ public static function captureExceptionDataProvider(): \Generator new \Exception('foo'), new EventHint(), ], - [ - new \Exception('foo'), - new EventHint(), - ], ]; } @@ -148,13 +148,19 @@ public function testCaptureEvent(): void $event = Event::createEvent(); $hint = new EventHint(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureEvent') - ->with($event, $hint) + ->with( + $event, + $hint, + $this->callback(function (Scope $scope): bool { + return $scope instanceof Scope; + }) + ) ->willReturn($event->getId()); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->assertSame($event->getId(), captureEvent($event, $hint)); } @@ -162,17 +168,24 @@ public function testCaptureEvent(): void /** * @dataProvider captureLastErrorDataProvider */ - public function testCaptureLastError(array $functionCallArgs, array $expectedFunctionCallArgs): void + public function testCaptureLastError(array $functionCallArgs): void { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $hint = $functionCallArgs[0] ?? null; + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) + ->with( + $this->callback(function (Scope $scope): bool { + return $scope instanceof Scope; + }), + $hint + ) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); @trigger_error('foo', \E_USER_NOTICE); @@ -183,12 +196,10 @@ public static function captureLastErrorDataProvider(): \Generator { yield [ [], - [null], ]; yield [ [new EventHint()], - [new EventHint()], ]; } @@ -202,13 +213,19 @@ public function testCaptureCheckIn(): void 'UTC' ); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureCheckIn') - ->with('test-crontab', CheckInStatus::ok(), 10, $monitorConfig, $checkInId) + ->with( + 'test-crontab', + CheckInStatus::ok(), + 10, + $monitorConfig, + $checkInId + ) ->willReturn($checkInId); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->assertSame($checkInId, captureCheckIn( 'test-crontab', @@ -221,8 +238,8 @@ public function testCaptureCheckIn(): void public function testWithMonitor(): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->exactly(2)) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->exactly(2)) ->method('captureCheckIn') ->with( $this->callback(function (string $slug): bool { @@ -242,7 +259,7 @@ public function testWithMonitor(): void }) ); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); withMonitor('test-crontab', function () { // Do something... @@ -258,15 +275,14 @@ public function testWithMonitorCallableThrows(): void { $this->expectException(\Exception::class); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->exactly(2)) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->exactly(2)) ->method('captureCheckIn') ->with( $this->callback(function (string $slug): bool { return $slug === 'test-crontab'; }), $this->callback(function (CheckInStatus $checkInStatus): bool { - // just check for type CheckInStatus return true; }), $this->anything(), @@ -279,7 +295,7 @@ public function testWithMonitorCallableThrows(): void }) ); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); withMonitor('test-crontab', function () { throw new \Exception(); @@ -295,13 +311,7 @@ public function testAddBreadcrumb(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['default_integrations' => false])); - - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init(new NoOpClient()); addBreadcrumb($breadcrumb); configureScope(function (Scope $scope) use ($breadcrumb): void { @@ -338,15 +348,19 @@ public function testStartTransaction(): void $transaction = new Transaction($transactionContext); $customSamplingContext = ['foo' => 'bar']; - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('startTransaction') - ->with($transactionContext, $customSamplingContext) - ->willReturn($transaction); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 1.0, + ])); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $this->assertSame($transaction, startTransaction($transactionContext, $customSamplingContext)); + $transaction = startTransaction($transactionContext, $customSamplingContext); + + $this->assertSame('foo', $transaction->getName()); + $this->assertTrue($transaction->getSampled()); } public function testTraceReturnsClosureResult(): void @@ -362,54 +376,42 @@ public function testTraceReturnsClosureResult(): void public function testTraceCorrectlyReplacesAndRestoresCurrentSpan(): void { - $hub = new Hub(new NoOpClient()); - $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(true); - $hub->setSpan($transaction); + SentrySdk::init(new NoOpClient()); + SentrySdk::getCurrentScope()->setSpan($transaction); - SentrySdk::setCurrentHub($hub); + $this->assertSame($transaction, SentrySdk::getCurrentScope()->getSpan()); - $this->assertSame($transaction, $hub->getSpan()); - - trace(function () use ($transaction, $hub) { - $this->assertNotSame($transaction, $hub->getSpan()); + trace(function () use ($transaction) { + $this->assertNotSame($transaction, SentrySdk::getCurrentScope()->getSpan()); }, new SpanContext()); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, SentrySdk::getCurrentScope()->getSpan()); try { trace(function () { throw new \RuntimeException('Throwing should still restore the previous span'); }, new SpanContext()); } catch (\RuntimeException $e) { - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, SentrySdk::getCurrentScope()->getSpan()); } } public function testTraceDoesntCreateSpanIfTransactionIsNotSampled(): void { - $scope = $this->createMock(Scope::class); - - $hub = new Hub(new NoOpClient(), $scope); - $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(false); - $scope->expects($this->never()) - ->method('setSpan'); - $scope->expects($this->exactly(3)) - ->method('getSpan') - ->willReturn($transaction); - - SentrySdk::setCurrentHub($hub); + SentrySdk::init(new NoOpClient()); + SentrySdk::getCurrentScope()->setSpan($transaction); - trace(function () use ($transaction, $hub) { - $this->assertSame($transaction, $hub->getSpan()); + trace(function () use ($transaction) { + $this->assertSame($transaction, SentrySdk::getCurrentScope()->getSpan()); }, SpanContext::make()); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, SentrySdk::getCurrentScope()->getSpan()); } public function testTraceparentWithTracingDisabled(): void @@ -418,11 +420,8 @@ public function testTraceparentWithTracingDisabled(): void $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $propagationContext->setSpanId(new SpanId('566e3688a61d4bc8')); - $scope = new Scope($propagationContext); - - $hub = new Hub(new NoOpClient(), $scope); - - SentrySdk::setCurrentHub($hub); + SentrySdk::init(new NoOpClient()); + SentrySdk::getIsolationScope()->setPropagationContext($propagationContext); $traceParent = getTraceparent(); @@ -438,9 +437,7 @@ public function testTraceparentWithTracingEnabled(): void 'traces_sample_rate' => 1.0, ])); - $hub = new Hub($client); - - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $spanContext = (new SpanContext()) ->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')) @@ -448,7 +445,7 @@ public function testTraceparentWithTracingEnabled(): void $span = new Span($spanContext); - $hub->setSpan($span); + SentrySdk::getCurrentScope()->setSpan($span); $traceParent = getTraceparent(); @@ -461,8 +458,6 @@ public function testBaggageWithTracingDisabled(): void $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $propagationContext->setSampleRand(0.25); - $scope = new Scope($propagationContext); - $client = $this->createMock(ClientInterface::class); $client->expects($this->atLeastOnce()) ->method('getOptions') @@ -471,9 +466,8 @@ public function testBaggageWithTracingDisabled(): void 'environment' => 'development', ])); - $hub = new Hub($client, $scope); - - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); + SentrySdk::getIsolationScope()->setPropagationContext($propagationContext); $baggage = getBaggage(); @@ -491,22 +485,20 @@ public function testBaggageWithTracingEnabled(): void 'environment' => 'development', ])); - $hub = new Hub($client); - - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $transactionContext = new TransactionContext(); $transactionContext->setName('Test'); $transactionContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $transactionContext->getMetadata()->setSampleRand(0.25); - $transaction = startTransaction($transactionContext); + $transaction = SentrySdk::startTransaction($transactionContext); $spanContext = new SpanContext(); $span = $transaction->startChild($spanContext); - $hub->setSpan($span); + SentrySdk::getCurrentScope()->setSpan($span); $baggage = getBaggage(); @@ -515,9 +507,7 @@ public function testBaggageWithTracingEnabled(): void public function testContinueTrace(): void { - $hub = new Hub(new NoOpClient()); - - SentrySdk::setCurrentHub($hub); + SentrySdk::init(new NoOpClient()); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -528,16 +518,14 @@ public function testContinueTrace(): void $this->assertSame('566e3688a61d4bc8', (string) $transactionContext->getParentSpanId()); $this->assertTrue($transactionContext->getParentSampled()); - configureScope(function (Scope $scope): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = SentrySdk::getIsolationScope()->getPropagationContext(); - $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $propagationContext->getTraceId()); - $this->assertSame('566e3688a61d4bc8', (string) $propagationContext->getParentSpanId()); + $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $propagationContext->getTraceId()); + $this->assertSame('566e3688a61d4bc8', (string) $propagationContext->getParentSpanId()); - $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); + $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); - $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $dynamicSamplingContext->get('trace_id')); - $this->assertTrue($dynamicSamplingContext->isFrozen()); - }); + $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $dynamicSamplingContext->get('trace_id')); + $this->assertTrue($dynamicSamplingContext->isFrozen()); } } diff --git a/tests/Integration/EnvironmentIntegrationTest.php b/tests/Integration/EnvironmentIntegrationTest.php index 788b95fa86..7391acbce4 100644 --- a/tests/Integration/EnvironmentIntegrationTest.php +++ b/tests/Integration/EnvironmentIntegrationTest.php @@ -33,7 +33,7 @@ public function testInvoke(bool $isIntegrationEnabled, ?RuntimeContext $initialR ->method('getIntegration') ->willReturn($isIntegrationEnabled ? $integration : null); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); withScope(function (Scope $scope) use ($expectedRuntimeContext, $expectedOsContext, $initialRuntimeContext, $initialOsContext): void { $event = Event::createEvent(); diff --git a/tests/Integration/FrameContextifierIntegrationTest.php b/tests/Integration/FrameContextifierIntegrationTest.php index 5667826b0d..f13ba8e7d6 100644 --- a/tests/Integration/FrameContextifierIntegrationTest.php +++ b/tests/Integration/FrameContextifierIntegrationTest.php @@ -40,7 +40,7 @@ public function testInvoke(string $fixtureFilePath, int $lineNumber, int $contex ->method('getOptions') ->willReturn($options); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); $stacktrace = new Stacktrace([ new Frame('[unknown]', $fixtureFilePath, $lineNumber, null, $fixtureFilePath), @@ -133,7 +133,7 @@ public function testInvokeLogsWarningMessageIfSourceCodeExcerptCannotBeRetrieved ->method('getOptions') ->willReturn($options); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); $stacktrace = new Stacktrace([ new Frame(null, '[internal]', 0), diff --git a/tests/Integration/ModulesIntegrationTest.php b/tests/Integration/ModulesIntegrationTest.php index 52b96c9d36..659a503989 100644 --- a/tests/Integration/ModulesIntegrationTest.php +++ b/tests/Integration/ModulesIntegrationTest.php @@ -34,7 +34,7 @@ public function testInvoke(bool $isIntegrationEnabled, bool $expectedEmptyModule ->method('getIntegration') ->willReturn($isIntegrationEnabled ? $integration : null); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); withScope(function (Scope $scope) use ($expectedEmptyModules): void { $event = $scope->applyToEvent(Event::createEvent()); @@ -86,7 +86,7 @@ public function testModuleIntegration(): void ->setTransport($transport) ->getClient(); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); $client->captureEvent(Event::createEvent(), null, new Scope()); } diff --git a/tests/Integration/RequestIntegrationTest.php b/tests/Integration/RequestIntegrationTest.php index 66749155a9..c08cc3e1cb 100644 --- a/tests/Integration/RequestIntegrationTest.php +++ b/tests/Integration/RequestIntegrationTest.php @@ -44,7 +44,7 @@ public function testInvoke(array $options, ServerRequestInterface $request, arra ->method('getOptions') ->willReturn(new Options($options)); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); withScope(function (Scope $scope) use ($event, $expectedRequestContextData, $expectedUser): void { $event = $scope->applyToEvent($event); diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index ab46c6a1b3..1faf421764 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -35,7 +35,7 @@ public function testSetupOnce(Event $event, bool $isIntegrationEnabled, ?EventHi ->method('getIntegration') ->willReturn($isIntegrationEnabled ? $integration : null); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); withScope(function (Scope $scope) use ($event, $hint, $expectedTransaction): void { $event = $scope->applyToEvent($event, $hint); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 1478853e80..d79fb41a31 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -10,7 +10,6 @@ use Sentry\Logs\LogLevel; use Sentry\Logs\LogsAggregator; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; @@ -33,8 +32,7 @@ public function testAttributes(array $attributes, array $expected): void 'enable_logs' => true, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -91,8 +89,7 @@ public function testMessageFormatting(string $message, array $values, string $ex 'enable_logs' => true, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -167,10 +164,9 @@ public function testAttributesAreAddedToLogMessage(): void 'server_name' => 'web-server-01', ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $hub->configureScope(function (Scope $scope) { + SentrySdk::configureScope(function (Scope $scope) { $userDataBag = new UserDataBag(); $userDataBag->setId('unique_id'); $userDataBag->setEmail('foo@example.com'); @@ -182,7 +178,7 @@ public function testAttributesAreAddedToLogMessage(): void $spanContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $spanContext->setSpanId(new SpanId('566e3688a61d4bc8')); $span = new Span($spanContext); - $hub->setSpan($span); + SentrySdk::getCurrentScope()->setSpan($span); $aggregator = new LogsAggregator(); diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 3aab97419f..3ac64c85f9 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -13,7 +13,6 @@ use Sentry\Logs\LogLevel; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; @@ -36,8 +35,7 @@ public function testLogNotSentWhenDisabled(): void $client->expects($this->never()) ->method('captureEvent'); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); logger()->info('Some info message'); @@ -186,8 +184,7 @@ private function assertEvent(callable $assert, array $options = []): ClientInter $client = ClientBuilder::create($clientOptions)->setTransport($transport)->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); return $client; } diff --git a/tests/Metrics/TraceMetricsTest.php b/tests/Metrics/TraceMetricsTest.php index 72f497002c..92f4d847fd 100644 --- a/tests/Metrics/TraceMetricsTest.php +++ b/tests/Metrics/TraceMetricsTest.php @@ -12,7 +12,7 @@ use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\Options; -use Sentry\State\HubAdapter; +use Sentry\SentrySdk; use function Sentry\trace_metrics; @@ -20,7 +20,7 @@ final class TraceMetricsTest extends TestCase { protected function setUp(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options(), StubTransport::getInstance())); + SentrySdk::init(new Client(new Options(), StubTransport::getInstance())); StubTransport::$events = []; } @@ -86,7 +86,7 @@ public function testMetricsBufferFull(): void public function testEnableMetrics(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'enable_metrics' => false, ]), StubTransport::getInstance())); @@ -98,7 +98,7 @@ public function testEnableMetrics(): void public function testBeforeSendMetricAltersContent() { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'before_send_metric' => static function (Metric $metric) { $metric->setValue(99999); diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index ffef9e1925..beec8c3305 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -9,7 +9,9 @@ use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; use Sentry\Monolog\BreadcrumbHandler; -use Sentry\State\HubInterface; +use Sentry\Event; +use Sentry\NoOpClient; +use Sentry\SentrySdk; final class BreadcrumbHandlerTest extends TestCase { @@ -18,22 +20,23 @@ final class BreadcrumbHandlerTest extends TestCase */ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('addBreadcrumb') - ->with($this->callback(function (Breadcrumb $breadcrumb) use ($expectedBreadcrumb, $record): bool { - $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); - $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); - $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); - $this->assertEquals($record['datetime']->getTimestamp(), $breadcrumb->getTimestamp()); - $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); - $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); + SentrySdk::init(new NoOpClient()); + $handler = new BreadcrumbHandler(); + $handler->handle($record); - return true; - })); + $event = SentrySdk::getIsolationScope()->applyToEvent(Event::createEvent()); - $handler = new BreadcrumbHandler($hub); - $handler->handle($record); + $this->assertNotNull($event); + $breadcrumbs = $event->getBreadcrumbs(); + $this->assertCount(1, $breadcrumbs); + + $breadcrumb = $breadcrumbs[0]; + $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); + $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); + $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); + $this->assertEquals($record['datetime']->getTimestamp(), $breadcrumb->getTimestamp()); + $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); + $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); } /** diff --git a/tests/Monolog/HandlerTest.php b/tests/Monolog/HandlerTest.php index bc1d4dd67f..48c30314eb 100644 --- a/tests/Monolog/HandlerTest.php +++ b/tests/Monolog/HandlerTest.php @@ -12,8 +12,8 @@ use Sentry\EventHint; use Sentry\Monolog\Handler; use Sentry\Severity; -use Sentry\State\Hub; use Sentry\State\Scope; +use Sentry\SentrySdk; final class HandlerTest extends TestCase { @@ -45,7 +45,8 @@ public function testHandle(bool $fillExtraContext, $record, Event $expectedEvent }) ); - $handler = new Handler(new Hub($client, new Scope()), Logger::DEBUG, true, $fillExtraContext); + SentrySdk::init($client); + $handler = new Handler(Logger::DEBUG, true, $fillExtraContext); $handler->handle($record); } diff --git a/tests/Monolog/LogsHandlerTest.php b/tests/Monolog/LogsHandlerTest.php index a18739b6bd..bad835c90b 100644 --- a/tests/Monolog/LogsHandlerTest.php +++ b/tests/Monolog/LogsHandlerTest.php @@ -13,7 +13,6 @@ use Sentry\Logs\Logs; use Sentry\Monolog\LogsHandler; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; @@ -30,8 +29,7 @@ protected function setUp(): void }, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); } /** @@ -105,8 +103,7 @@ public function getEvents(): array ])->setTransport($transport) ->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->handleLogAndDrop(); diff --git a/tests/SentrySdkExtension.php b/tests/SentrySdkExtension.php index 8637499eec..c4eb0c1dba 100644 --- a/tests/SentrySdkExtension.php +++ b/tests/SentrySdkExtension.php @@ -13,7 +13,7 @@ final class SentrySdkExtension implements BeforeTestHookInterface { public function executeBeforeTest(string $test): void { - $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'currentHub'); + $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'scopeManager'); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(true); } diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index f85fbc9b8a..8a64993ecd 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -7,34 +7,82 @@ use PHPUnit\Framework\TestCase; use Sentry\NoOpClient; use Sentry\SentrySdk; -use Sentry\State\Hub; +use Sentry\State\ScopeType; final class SentrySdkTest extends TestCase { - public function testInit(): void + public function testInitBindsClientToGlobalScope(): void { - $hub1 = SentrySdk::init(); - $hub2 = SentrySdk::getCurrentHub(); + $client = $this->createMock(\Sentry\ClientInterface::class); - $this->assertSame($hub1, $hub2); - $this->assertNotSame(SentrySdk::init(), SentrySdk::init()); + SentrySdk::init($client); + + $this->assertSame($client, SentrySdk::getGlobalScope()->getClient()); + } + + public function testInitResetsIsolationAndCurrentScopes(): void + { + SentrySdk::init(new NoOpClient()); + + $oldIsolationScope = SentrySdk::getIsolationScope(); + $oldCurrentScope = SentrySdk::getCurrentScope(); + + SentrySdk::init(new NoOpClient()); + + $this->assertNotSame($oldIsolationScope, SentrySdk::getIsolationScope()); + $this->assertNotSame($oldCurrentScope, SentrySdk::getCurrentScope()); } - public function testGetCurrentHub(): void + public function testScopeAccessorsReturnTypedScopes(): void { SentrySdk::init(); - $hub2 = SentrySdk::getCurrentHub(); - $hub3 = SentrySdk::getCurrentHub(); + $this->assertSame(ScopeType::global(), SentrySdk::getGlobalScope()->getType()); + $this->assertSame(ScopeType::isolation(), SentrySdk::getIsolationScope()->getType()); + $this->assertSame(ScopeType::current(), SentrySdk::getCurrentScope()->getType()); + } + + public function testGetClientPrefersCurrentThenIsolationThenGlobal(): void + { + $globalClient = $this->createMock(\Sentry\ClientInterface::class); + $isolationClient = $this->createMock(\Sentry\ClientInterface::class); + $currentClient = $this->createMock(\Sentry\ClientInterface::class); + + SentrySdk::init(new NoOpClient()); + SentrySdk::getGlobalScope()->bindClient($globalClient); + SentrySdk::getIsolationScope()->bindClient($isolationClient); + SentrySdk::getCurrentScope()->bindClient($currentClient); + + $this->assertSame($currentClient, SentrySdk::getClient()); - $this->assertSame($hub2, $hub3); + SentrySdk::getCurrentScope()->bindClient(new NoOpClient()); + $this->assertSame($isolationClient, SentrySdk::getClient()); + + SentrySdk::getIsolationScope()->bindClient(new NoOpClient()); + $this->assertSame($globalClient, SentrySdk::getClient()); } - public function testSetCurrentHub(): void + public function testGetMergedScopeCombinesScopeData(): void { - $hub = new Hub(new NoOpClient()); + SentrySdk::init(new NoOpClient()); + + SentrySdk::getGlobalScope()->setTag('global', 'yes'); + SentrySdk::getGlobalScope()->setTag('shared', 'global'); + SentrySdk::getIsolationScope()->setTag('isolation', 'yes'); + SentrySdk::getIsolationScope()->setTag('shared', 'isolation'); + SentrySdk::getCurrentScope()->setTag('current', 'yes'); + SentrySdk::getCurrentScope()->setTag('shared', 'current'); - $this->assertSame($hub, SentrySdk::setCurrentHub($hub)); - $this->assertSame($hub, SentrySdk::getCurrentHub()); + $event = \Sentry\Event::createEvent(); + $event = SentrySdk::getMergedScope()->applyToEvent($event); + + $this->assertNotNull($event); + $this->assertEquals([ + 'global' => 'yes', + 'isolation' => 'yes', + 'current' => 'yes', + 'shared' => 'current', + ], $event->getTags()); } + } diff --git a/tests/State/HubAdapterTest.php b/tests/State/HubAdapterTest.php deleted file mode 100644 index 736ec703da..0000000000 --- a/tests/State/HubAdapterTest.php +++ /dev/null @@ -1,394 +0,0 @@ -assertSame(HubAdapter::getInstance(), HubAdapter::getInstance()); - } - - public function testGetInstanceReturnsUncloneableInstance(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Cloning is forbidden.'); - - clone HubAdapter::getInstance(); - } - - public function testHubAdapterThrowsExceptionOnSerialization(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Serializing instances of this class is forbidden.'); - - serialize(HubAdapter::getInstance()); - } - - public function testHubAdapterThrowsExceptionOnUnserialization(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Unserializing instances of this class is forbidden.'); - - unserialize('O:23:"Sentry\State\HubAdapter":0:{}'); - } - - public function testGetClient(): void - { - $client = $this->createMock(ClientInterface::class); - - SentrySdk::getCurrentHub()->bindClient($client); - - $this->assertSame($client, HubAdapter::getInstance()->getClient()); - } - - public function testGetLastEventId(): void - { - $eventId = EventId::generate(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getLastEventId') - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->getLastEventId()); - } - - public function testPushScope(): void - { - $scope = new Scope(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('pushScope') - ->willReturn($scope); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($scope, HubAdapter::getInstance()->pushScope()); - } - - public function testPopScope(): void - { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('popScope') - ->willReturn(true); - - SentrySdk::setCurrentHub($hub); - - $this->assertTrue(HubAdapter::getInstance()->popScope()); - } - - public function testWithScope(): void - { - $callback = static function (): string { - return 'foobarbaz'; - }; - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('withScope') - ->with($callback) - ->willReturnCallback($callback); - - SentrySdk::setCurrentHub($hub); - - $returnValue = HubAdapter::getInstance()->withScope($callback); - - $this->assertSame('foobarbaz', $returnValue); - } - - public function testConfigureScope(): void - { - $callback = static function () {}; - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('configureScope') - ->with($callback); - - SentrySdk::setCurrentHub($hub); - HubAdapter::getInstance()->configureScope($callback); - } - - public function testBindClient(): void - { - $client = $this->createMock(ClientInterface::class); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('bindClient') - ->with($client); - - SentrySdk::setCurrentHub($hub); - HubAdapter::getInstance()->bindClient($client); - } - - /** - * @dataProvider captureMessageDataProvider - */ - public function testCaptureMessage(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureMessage(...$expectedFunctionCallArgs)); - } - - public static function captureMessageDataProvider(): \Generator - { - yield [ - [ - 'foo', - Severity::debug(), - ], - ]; - - yield [ - [ - 'foo', - Severity::debug(), - new EventHint(), - ], - ]; - } - - /** - * @dataProvider captureExceptionDataProvider - */ - public function testCaptureException(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - $exception = new \Exception(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureException') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureException(...$expectedFunctionCallArgs)); - } - - public static function captureExceptionDataProvider(): \Generator - { - yield [ - [ - new \Exception('foo'), - ], - ]; - - yield [ - [ - new \Exception('foo'), - new EventHint(), - ], - ]; - } - - public function testCaptureEvent(): void - { - $event = Event::createEvent(); - $hint = EventHint::fromArray([]); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureEvent') - ->with($event, $hint) - ->willReturn($event->getId()); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($event->getId(), HubAdapter::getInstance()->captureEvent($event, $hint)); - } - - /** - * @dataProvider captureLastErrorDataProvider - */ - public function testCaptureLastError(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureLastError(...$expectedFunctionCallArgs)); - } - - public static function captureLastErrorDataProvider(): \Generator - { - yield [ - [], - ]; - - yield [ - [ - new EventHint(), - ], - ]; - } - - public function testCaptureCheckIn() - { - $hub = new Hub(new NoOpClient()); - - $options = new Options([ - 'environment' => Event::DEFAULT_ENVIRONMENT, - 'release' => '1.1.8', - ]); - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn($options); - - $hub->bindClient($client); - SentrySdk::setCurrentHub($hub); - - $checkInId = SentryUid::generate(); - - $this->assertSame($checkInId, HubAdapter::getInstance()->captureCheckIn( - 'test-crontab', - CheckInStatus::ok(), - 10, - new MonitorConfig( - MonitorSchedule::crontab('*/5 * * * *'), - 5, - 30, - 'UTC' - ), - $checkInId - )); - } - - public function testAddBreadcrumb(): void - { - $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_DEBUG, Breadcrumb::TYPE_ERROR, 'user'); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('addBreadcrumb') - ->willReturn(true); - - SentrySdk::setCurrentHub($hub); - - $this->assertTrue(HubAdapter::getInstance()->addBreadcrumb($breadcrumb)); - } - - public function testGetIntegration(): void - { - $integration = $this->createMock(IntegrationInterface::class); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getIntegration') - ->with(\get_class($integration)) - ->willReturn($integration); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($integration, HubAdapter::getInstance()->getIntegration(\get_class($integration))); - } - - public function testStartTransaction(): void - { - $transactionContext = new TransactionContext(); - $transaction = new Transaction($transactionContext); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('startTransaction') - ->with($transactionContext) - ->willReturn($transaction); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($transaction, HubAdapter::getInstance()->startTransaction($transactionContext)); - } - - public function testGetTransaction(): void - { - $transaction = new Transaction(new TransactionContext()); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getTransaction') - ->willReturn($transaction); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($transaction, HubAdapter::getInstance()->getTransaction()); - } - - public function testGetSpan(): void - { - $span = new Span(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getSpan') - ->willReturn($span); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($span, HubAdapter::getInstance()->getSpan()); - } - - public function testSetSpan(): void - { - $span = new Span(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('setSpan') - ->with($span) - ->willReturn($hub); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($hub, HubAdapter::getInstance()->setSpan($span)); - } -} diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php deleted file mode 100644 index 0b8c2286e6..0000000000 --- a/tests/State/HubTest.php +++ /dev/null @@ -1,907 +0,0 @@ -createMock(ClientInterface::class); - $hub = new Hub($client); - - $this->assertSame($client, $hub->getClient()); - } - - public function testGetScope(): void - { - $callbackInvoked = false; - $scope = new Scope(); - $hub = new Hub($this->createMock(ClientInterface::class), $scope); - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope) { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testGetLastEventId(): void - { - $eventId = EventId::generate(); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureMessage') - ->willReturn($eventId); - - $hub = new Hub($client); - - $this->assertNull($hub->getLastEventId()); - $this->assertSame($hub->captureMessage('foo'), $hub->getLastEventId()); - $this->assertSame($eventId, $hub->getLastEventId()); - } - - public function testPushScope(): void - { - /** @var ClientInterface&MockObject $client1 */ - $client1 = $this->createMock(ClientInterface::class); - $scope1 = new Scope(); - $hub = new Hub($client1, $scope1); - - $this->assertSame($client1, $hub->getClient()); - - $scope2 = $hub->pushScope(); - - $this->assertSame($client1, $hub->getClient()); - $this->assertNotSame($scope1, $scope2); - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope2): void { - $this->assertSame($scope2, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testPopScope(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $scope1 = new Scope(); - $hub = new Hub($client, $scope1); - - $this->assertFalse($hub->popScope()); - - $scope2 = $hub->pushScope(); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope2, &$callbackInvoked): void { - $this->assertSame($scope2, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - $this->assertSame($client, $hub->getClient()); - - $this->assertTrue($hub->popScope()); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope1, &$callbackInvoked): void { - $this->assertSame($scope1, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - $this->assertSame($client, $hub->getClient()); - - $this->assertFalse($hub->popScope()); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope1, &$callbackInvoked): void { - $this->assertSame($scope1, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - $this->assertSame($client, $hub->getClient()); - } - - public function testWithScope(): void - { - $scope = new Scope(); - $hub = new Hub(new NoOpClient(), $scope); - - $callbackReturn = $hub->withScope(function (Scope $scopeArg) use ($scope): string { - $this->assertNotSame($scope, $scopeArg); - - return 'foobarbaz'; - }); - - $this->assertSame('foobarbaz', $callbackReturn); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope): void { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testWithScopeWhenExceptionIsThrown(): void - { - $scope = new Scope(); - $hub = new Hub($this->createMock(ClientInterface::class), $scope); - $callbackInvoked = false; - - try { - $hub->withScope(function (Scope $scopeArg) use ($scope, &$callbackInvoked): void { - $this->assertNotSame($scope, $scopeArg); - - $callbackInvoked = true; - - // We throw to test that the scope is correctly popped form the - // stack regardless - throw new \RuntimeException(); - }); - } catch (\RuntimeException $exception) { - // Do nothing, we catch this exception to not make the test fail - } - - $this->assertTrue($callbackInvoked); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope): void { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testConfigureScope(): void - { - $scope = new Scope(); - $hub = new Hub(new NoOpClient(), $scope); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope, &$callbackInvoked): void { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testBindClient(): void - { - /** @var ClientInterface&MockObject $client1 */ - $client1 = $this->createMock(ClientInterface::class); - - /** @var ClientInterface&MockObject $client2 */ - $client2 = $this->createMock(ClientInterface::class); - - $hub = new Hub($client1); - - $this->assertSame($client1, $hub->getClient()); - - $hub->bindClient($client2); - - $this->assertSame($client2, $hub->getClient()); - } - - /** - * @dataProvider captureMessageDataProvider - */ - public function testCaptureMessage(array $functionCallArgs, array $expectedFunctionCallArgs, PropagationContext $propagationContext): void - { - $eventId = EventId::generate(); - $hub = new Hub(new NoOpClient()); - - $hub->configureScope(function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - $this->assertNull($hub->captureMessage('foo')); - - $hub->bindClient($client); - - $this->assertSame($eventId, $hub->captureMessage(...$functionCallArgs)); - } - - public static function captureMessageDataProvider(): \Generator - { - $propagationContext = PropagationContext::fromDefaults(); - - yield [ - [ - 'foo', - Severity::debug(), - ], - [ - 'foo', - Severity::debug(), - new Scope($propagationContext), - ], - $propagationContext, - ]; - - yield [ - [ - 'foo', - Severity::debug(), - new EventHint(), - ], - [ - 'foo', - Severity::debug(), - new Scope($propagationContext), - new EventHint(), - ], - $propagationContext, - ]; - } - - /** - * @dataProvider captureExceptionDataProvider - */ - public function testCaptureException(array $functionCallArgs, array $expectedFunctionCallArgs, PropagationContext $propagationContext): void - { - $eventId = EventId::generate(); - $hub = new Hub(new NoOpClient()); - - $hub->configureScope(function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureException') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - $this->assertNull($hub->captureException(new \RuntimeException())); - - $hub->bindClient($client); - - $this->assertSame($eventId, $hub->captureException(...$functionCallArgs)); - } - - public static function captureExceptionDataProvider(): \Generator - { - $propagationContext = PropagationContext::fromDefaults(); - - yield [ - [ - new \Exception('foo'), - ], - [ - new \Exception('foo'), - new Scope($propagationContext), - null, - ], - $propagationContext, - ]; - - yield [ - [ - new \Exception('foo'), - new EventHint(), - ], - [ - new \Exception('foo'), - new Scope($propagationContext), - new EventHint(), - ], - $propagationContext, - ]; - } - - /** - * @dataProvider captureLastErrorDataProvider - */ - public function testCaptureLastError(array $functionCallArgs, array $expectedFunctionCallArgs, PropagationContext $propagationContext): void - { - $eventId = EventId::generate(); - $hub = new Hub(new NoOpClient()); - - $hub->configureScope(function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - $this->assertNull($hub->captureLastError(...$functionCallArgs)); - - $hub->bindClient($client); - - $this->assertSame($eventId, $hub->captureLastError(...$functionCallArgs)); - } - - public static function captureLastErrorDataProvider(): \Generator - { - $propagationContext = PropagationContext::fromDefaults(); - - yield [ - [], - [ - new Scope($propagationContext), - null, - ], - $propagationContext, - ]; - - yield [ - [ - new EventHint(), - ], - [ - new Scope($propagationContext), - new EventHint(), - ], - $propagationContext, - ]; - } - - public function testCaptureCheckIn(): void - { - $expectedCheckIn = new CheckIn( - 'test-crontab', - CheckInStatus::ok(), - SentryUid::generate(), - '0.0.1-dev', - Event::DEFAULT_ENVIRONMENT, - 10, - new MonitorConfig( - MonitorSchedule::crontab('*/5 * * * *'), - 5, - 30, - 'UTC' - ) - ); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'environment' => Event::DEFAULT_ENVIRONMENT, - 'release' => '0.0.1-dev', - ])); - - $client->expects($this->once()) - ->method('captureEvent') - ->with($this->callback(static function (Event $event) use ($expectedCheckIn): bool { - return $event->getCheckIn() == $expectedCheckIn; - })); - - $hub = new Hub($client); - - $this->assertSame($expectedCheckIn->getId(), $hub->captureCheckIn( - $expectedCheckIn->getMonitorSlug(), - $expectedCheckIn->getStatus(), - $expectedCheckIn->getDuration(), - $expectedCheckIn->getMonitorConfig(), - $expectedCheckIn->getId() - )); - } - - public function testCaptureEvent(): void - { - $hub = new Hub(new NoOpClient()); - $event = Event::createEvent(); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureEvent') - ->with($event) - ->willReturn($event->getId()); - - $this->assertNull($hub->captureEvent($event)); - - $hub->bindClient($client); - - $this->assertSame($event->getId(), $hub->captureEvent($event)); - } - - public function testAddBreadcrumb(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - $callbackInvoked = false; - $hub = new Hub(new NoOpClient()); - $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - - $hub->addBreadcrumb($breadcrumb); - $hub->configureScope(function (Scope $scope): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertEmpty($event->getBreadcrumbs()); - }); - - $hub->bindClient($client); - $hub->addBreadcrumb($breadcrumb); - $hub->configureScope(function (Scope $scope) use (&$callbackInvoked, $breadcrumb): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb], $event->getBreadcrumbs()); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testAddBreadcrumbDoesNothingIfMaxBreadcrumbsLimitIsZero(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['max_breadcrumbs' => 0])); - - $hub = new Hub($client); - - $hub->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); - $hub->configureScope(function (Scope $scope): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertEmpty($event->getBreadcrumbs()); - }); - } - - public function testAddBreadcrumbRespectsMaxBreadcrumbsLimit(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->any()) - ->method('getOptions') - ->willReturn(new Options(['max_breadcrumbs' => 2])); - - $hub = new Hub($client); - $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_ERROR, 'error_reporting', 'foo'); - $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_ERROR, 'error_reporting', 'bar'); - $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_ERROR, 'error_reporting', 'baz'); - - $hub->addBreadcrumb($breadcrumb1); - $hub->addBreadcrumb($breadcrumb2); - - $hub->configureScope(function (Scope $scope) use ($breadcrumb1, $breadcrumb2): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb1, $breadcrumb2], $event->getBreadcrumbs()); - }); - - $hub->addBreadcrumb($breadcrumb3); - - $hub->configureScope(function (Scope $scope) use ($breadcrumb2, $breadcrumb3): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb2, $breadcrumb3], $event->getBreadcrumbs()); - }); - } - - public function testAddBreadcrumbDoesNothingWhenBeforeBreadcrumbCallbackReturnsNull(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'before_breadcrumb' => static function () { - return null; - }, - ])); - - $hub = new Hub($client); - - $hub->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); - $hub->configureScope(function (Scope $scope): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertEmpty($event->getBreadcrumbs()); - }); - } - - public function testAddBreadcrumbStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): void - { - $callbackInvoked = false; - $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'before_breadcrumb' => static function () use ($breadcrumb2): Breadcrumb { - return $breadcrumb2; - }, - ])); - - $hub = new Hub($client); - - $hub->addBreadcrumb($breadcrumb1); - $hub->configureScope(function (Scope $scope) use (&$callbackInvoked, $breadcrumb2): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb2], $event->getBreadcrumbs()); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testGetIntegration(): void - { - /** @var IntegrationInterface&MockObject $integration */ - $integration = $this->createMock(IntegrationInterface::class); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getIntegration') - ->with('Foo\\Bar') - ->willReturn($integration); - - $hub = new Hub(new NoOpClient()); - - $this->assertNull($hub->getIntegration('Foo\\Bar')); - - $hub->bindClient($client); - - $this->assertSame($integration, $hub->getIntegration('Foo\\Bar')); - } - - /** - * @dataProvider startTransactionDataProvider - */ - public function testStartTransactionWithTracesSampler(Options $options, TransactionContext $transactionContext, bool $expectedSampled): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn($options); - - $hub = new Hub($client); - $transaction = $hub->startTransaction($transactionContext); - - $this->assertSame($expectedSampled, $transaction->getSampled()); - } - - public static function startTransactionDataProvider(): iterable - { - yield 'Acceptable float value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): float { - return 1.0; - }, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low float value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): float { - return 0.0; - }, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable integer value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): int { - return 1; - }, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low integer value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): int { - return 0; - }, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable float value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 1.0, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low float value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 0.0, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable integer value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 1, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low integer value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 0, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable but too low value returned from traces_sample_rate which is preferred over sample_rate' => [ - new Options([ - 'sample_rate' => 1.0, - 'traces_sample_rate' => 0.0, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable value returned from traces_sample_rate which is preferred over sample_rate' => [ - new Options([ - 'sample_rate' => 0.0, - 'traces_sample_rate' => 1.0, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable value returned from SamplingContext::getParentSampled() which is preferred over traces_sample_rate (x1)' => [ - new Options([ - 'traces_sample_rate' => 0.5, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, true), - true, - ]; - - yield 'Acceptable value returned from SamplingContext::getParentSampled() which is preferred over traces_sample_rate (x2)' => [ - new Options([ - 'traces_sample_rate' => 1.0, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - - yield 'Out of range sample rate returned from traces_sampler (lower than minimum)' => [ - new Options([ - 'traces_sampler' => static function (): float { - return -1.0; - }, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - - yield 'Out of range sample rate returned from traces_sampler (greater than maximum)' => [ - new Options([ - 'traces_sampler' => static function (): float { - return 1.1; - }, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - - yield 'Invalid type returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): string { - return 'foo'; - }, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - } - - public function testStartTransactionDoesNothingIfTracingIsNotEnabled(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertFalse($transaction->getSampled()); - } - - public function testStartTransactionWithCustomSamplingContext(): void - { - $customSamplingContext = ['a' => 'b']; - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { - $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); - - return 1.0; - }, - ])); - - $hub = new Hub($client); - $hub->startTransaction(new TransactionContext(), $customSamplingContext); - } - - public function testStartTransactionUpdatesTheDscSampleRate(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sampler' => function (SamplingContext $samplingContext): float { - return 1.0; - }, - ])); - - $hub = new Hub($client); - - $dsc = DynamicSamplingContext::fromHeader('sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public'); - $transactionMetaData = new TransactionMetadata(null, $dsc); - $transactionContext = new TransactionContext(TransactionContext::DEFAULT_NAME, null, $transactionMetaData); - - $transaction = $hub->startTransaction($transactionContext); - $this->assertSame('1', $transaction->getMetadata()->getDynamicSamplingContext()->get('sample_rate')); - } - - public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsNotSampled(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['traces_sample_rate' => 1])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, false)); - - $hub->configureScope(static function (Scope $scope) use ($transaction): void { - $scope->setSpan($transaction); - }); - - $this->assertSame($transaction, $hub->getTransaction()); - } - - public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsSampled(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['traces_sample_rate' => 1])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, true)); - - $hub->configureScope(static function (Scope $scope) use ($transaction): void { - $scope->setSpan($transaction); - }); - - $this->assertSame($transaction, $hub->getTransaction()); - } - - public function testGetTransactionReturnsNullIfNoTransactionIsSetOnTheScope(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['traces_sample_rate' => 1])); - - $hub = new Hub($client); - $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, true)); - - $this->assertNull($hub->getTransaction()); - } - - public function testEventTraceContextIsAlwaysFilled(): void - { - $hub = new Hub(new NoOpClient()); - - $event = Event::createEvent(); - - $hub->configureScope(function (Scope $scope) use ($event): void { - $event = $scope->applyToEvent($event); - - $this->assertNotEmpty($event->getContexts()['trace']); - }); - } - - public function testEventTraceContextIsNotOverridenWhenPresent(): void - { - $hub = new Hub(new NoOpClient()); - - $traceContext = ['foo' => 'bar']; - - $event = Event::createEvent(); - $event->setContext('trace', $traceContext); - - $hub->configureScope(function (Scope $scope) use ($event, $traceContext): void { - $event = $scope->applyToEvent($event); - - $this->assertEquals($event->getContexts()['trace'], $traceContext); - }); - } -} diff --git a/tests/State/LayerTest.php b/tests/State/LayerTest.php deleted file mode 100644 index 501f0e5793..0000000000 --- a/tests/State/LayerTest.php +++ /dev/null @@ -1,37 +0,0 @@ -createMock(ClientInterface::class); - - /** @var ClientInterface|MockObject $client2 */ - $client2 = $this->createMock(ClientInterface::class); - - $scope1 = new Scope(); - $scope2 = new Scope(); - - $layer = new Layer($client1, $scope1); - - $this->assertSame($client1, $layer->getClient()); - $this->assertSame($scope1, $layer->getScope()); - - $layer->setClient($client2); - $layer->setScope($scope2); - - $this->assertSame($client2, $layer->getClient()); - $this->assertSame($scope2, $layer->getScope()); - } -} diff --git a/tests/State/ScopeManagerTest.php b/tests/State/ScopeManagerTest.php new file mode 100644 index 0000000000..ab3160bebe --- /dev/null +++ b/tests/State/ScopeManagerTest.php @@ -0,0 +1,72 @@ +assertSame(ScopeType::global(), $manager->getGlobalScope()->getType()); + $this->assertSame(ScopeType::isolation(), $manager->getIsolationScope()->getType()); + $this->assertSame(ScopeType::current(), $manager->getCurrentScope()->getType()); + } + + public function testWithScopeForksCurrentOnly(): void + { + $manager = new ScopeManager(); + $originalCurrent = $manager->getCurrentScope(); + + $callbackScope = null; + + $manager->withScope(function (Scope $scope) use ($manager, $originalCurrent, &$callbackScope): void { + $callbackScope = $scope; + + $this->assertNotSame($originalCurrent, $scope); + $this->assertSame($scope, $manager->getCurrentScope()); + $this->assertSame(ScopeType::current(), $scope->getType()); + }); + + $this->assertSame($originalCurrent, $manager->getCurrentScope()); + $this->assertNotNull($callbackScope); + } + + public function testWithIsolationScopeForksIsolationAndCurrent(): void + { + $manager = new ScopeManager(); + $originalIsolation = $manager->getIsolationScope(); + $originalCurrent = $manager->getCurrentScope(); + + $manager->withIsolationScope(function (Scope $scope) use ($manager, $originalIsolation, $originalCurrent): void { + $this->assertNotSame($originalIsolation, $scope); + $this->assertSame($scope, $manager->getIsolationScope()); + $this->assertSame(ScopeType::isolation(), $scope->getType()); + + $this->assertNotSame($originalCurrent, $manager->getCurrentScope()); + }); + + $this->assertSame($originalIsolation, $manager->getIsolationScope()); + $this->assertSame($originalCurrent, $manager->getCurrentScope()); + } + + public function testResetScopesCreatesFreshIsolationAndCurrent(): void + { + $manager = new ScopeManager(); + $originalIsolation = $manager->getIsolationScope(); + $originalCurrent = $manager->getCurrentScope(); + + $manager->resetScopes(); + + $this->assertNotSame($originalIsolation, $manager->getIsolationScope()); + $this->assertNotSame($originalCurrent, $manager->getCurrentScope()); + $this->assertSame(ScopeType::global(), $manager->getGlobalScope()->getType()); + } +} diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 20b90e9280..130c673dbb 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -11,6 +11,7 @@ use Sentry\EventHint; use Sentry\Severity; use Sentry\State\Scope; +use Sentry\State\ScopeType; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\Span; @@ -23,6 +24,156 @@ final class ScopeTest extends TestCase { + public function testScopeTypeSingletons(): void + { + $this->assertSame(ScopeType::global(), ScopeType::global()); + $this->assertSame(ScopeType::isolation(), ScopeType::isolation()); + $this->assertSame(ScopeType::current(), ScopeType::current()); + $this->assertSame(ScopeType::merged(), ScopeType::merged()); + + $this->assertNotSame(ScopeType::global(), ScopeType::isolation()); + $this->assertSame('global', (string) ScopeType::global()); + } + + public function testScopeTypeAssignmentAndClone(): void + { + $scope = new Scope(null, ScopeType::global()); + $this->assertSame(ScopeType::global(), $scope->getType()); + + $scope->setType(ScopeType::current()); + $this->assertSame(ScopeType::current(), $scope->getType()); + + $clone = clone $scope; + $this->assertSame(ScopeType::current(), $clone->getType()); + } + + public function testMergeOrderPrefersCurrentScope(): void + { + $globalScope = new Scope(null, ScopeType::global()); + $globalScope->setTag('global', 'yes'); + $globalScope->setTag('shared', 'global'); + + $isolationScope = new Scope(null, ScopeType::isolation()); + $isolationScope->setTag('isolation', 'yes'); + $isolationScope->setTag('shared', 'isolation'); + + $currentScope = new Scope(null, ScopeType::current()); + $currentScope->setTag('current', 'yes'); + $currentScope->setTag('shared', 'current'); + + $event = Event::createEvent(); + $event = Scope::applyToEventFromScopes($event, $globalScope, $isolationScope, $currentScope); + + $this->assertNotNull($event); + $this->assertEquals([ + 'global' => 'yes', + 'isolation' => 'yes', + 'current' => 'yes', + 'shared' => 'current', + ], $event->getTags()); + } + + public function testBreadcrumbsAreMergedAndSorted(): void + { + $globalScope = new Scope(null, ScopeType::global()); + $globalScope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'global', null, [], 2000.0)); + + $isolationScope = new Scope(null, ScopeType::isolation()); + $isolationScope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation', null, [], 1500.0)); + + $currentScope = new Scope(null, ScopeType::current()); + $currentScope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'current', null, [], 1000.0)); + + $event = Event::createEvent(); + $event = Scope::applyToEventFromScopes($event, $globalScope, $isolationScope, $currentScope); + + $this->assertNotNull($event); + $breadcrumbs = $event->getBreadcrumbs(); + + $this->assertSame('current', $breadcrumbs[0]->getCategory()); + $this->assertSame('isolation', $breadcrumbs[1]->getCategory()); + $this->assertSame('global', $breadcrumbs[2]->getCategory()); + } + + public function testEventProcessorsRunInScopeOrder(): void + { + $globalScope = new Scope(null, ScopeType::global()); + $globalScope->addEventProcessor(function (Event $event) { + $order = $event->getExtra()['order'] ?? []; + $order[] = 'global'; + $event->setExtra(['order' => $order]); + + return $event; + }); + + $isolationScope = new Scope(null, ScopeType::isolation()); + $isolationScope->addEventProcessor(function (Event $event) { + $order = $event->getExtra()['order'] ?? []; + $order[] = 'isolation'; + $event->setExtra(['order' => $order]); + + return $event; + }); + + $currentScope = new Scope(null, ScopeType::current()); + $currentScope->addEventProcessor(function (Event $event) { + $order = $event->getExtra()['order'] ?? []; + $order[] = 'current'; + $event->setExtra(['order' => $order]); + + return $event; + }); + + $event = Event::createEvent(); + $event = Scope::applyToEventFromScopes($event, $globalScope, $isolationScope, $currentScope); + + $this->assertNotNull($event); + $this->assertSame(['global', 'isolation', 'current'], $event->getExtra()['order']); + } + + public function testScopeAttributesApplyToScope(): void + { + $scope = new Scope(); + $scope->setAttribute('app.feature', true); + $scope->setAttributes([ + 'app.session' => 42, + ]); + + $this->assertSame(true, $scope->getAttributes()->get('app.feature')->getValue()); + $this->assertSame(42, $scope->getAttributes()->get('app.session')->getValue()); + } + + public function testAttributesAreMergedWithPrecedence(): void + { + $globalScope = new Scope(null, ScopeType::global()); + $globalScope->setAttribute('global.attribute', 'global'); + $globalScope->setAttribute('overwritten.attribute', 'global'); + + $isolationScope = new Scope(null, ScopeType::isolation()); + $isolationScope->setAttribute('isolation.attribute', 'isolation'); + $isolationScope->setAttribute('overwritten.attribute', 'isolation'); + + $currentScope = new Scope(null, ScopeType::current()); + $currentScope->setAttribute('current.attribute', 'current'); + $currentScope->setAttribute('overwritten.attribute', 'current'); + + $scope = Scope::mergeScopes($globalScope, $isolationScope, $currentScope); + + $this->assertSame('global', $scope->getAttributes()->get('global.attribute')->getValue()); + $this->assertSame('isolation', $scope->getAttributes()->get('isolation.attribute')->getValue()); + $this->assertSame('current', $scope->getAttributes()->get('current.attribute')->getValue()); + $this->assertSame('current', $scope->getAttributes()->get('overwritten.attribute')->getValue()); + } + + public function testRemoveAttribute(): void + { + $scope = new Scope(); + $scope->setAttribute('app.feature', true); + $scope->removeAttribute('app.feature'); + + $this->assertNull($scope->getAttributes()->get('app.feature')); + } + public function testSetTag(): void { $scope = new Scope(); diff --git a/tests/Tracing/DynamicSamplingContextTest.php b/tests/Tracing/DynamicSamplingContextTest.php index 574aaf4da1..f348393e82 100644 --- a/tests/Tracing/DynamicSamplingContextTest.php +++ b/tests/Tracing/DynamicSamplingContextTest.php @@ -8,7 +8,6 @@ use Sentry\ClientInterface; use Sentry\NoOpClient; use Sentry\Options; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; @@ -91,16 +90,14 @@ public function testFromTransaction(): void 'environment' => 'test', ])); - $hub = new Hub($client); - $transactionContext = new TransactionContext(); $transactionContext->setName('foo'); - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext, $client); $transaction->getMetadata()->setSamplingRate(1.0); $transaction->getMetadata()->setSampleRand(0.5); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertSame((string) $transaction->getTraceId(), $samplingContext->get('trace_id')); $this->assertSame((string) $transaction->getMetaData()->getSamplingRate(), $samplingContext->get('sample_rate')); @@ -114,15 +111,15 @@ public function testFromTransaction(): void public function testFromTransactionSourceUrl(): void { - $hub = new Hub(new NoOpClient()); + $client = new NoOpClient(); $transactionContext = new TransactionContext(); $transactionContext->setName('/foo/bar/123'); $transactionContext->setSource(TransactionSource::url()); - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext, $client); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertNull($samplingContext->get('transaction')); } diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index f43b7fcf27..9acf5d016f 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -7,6 +7,8 @@ use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Promise\RejectedPromise; +use GuzzleHttp\Promise\TaskQueue; +use GuzzleHttp\Promise\Utils; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; @@ -15,7 +17,7 @@ use Sentry\Event; use Sentry\EventType; use Sentry\Options; -use Sentry\State\Hub; +use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Tracing\GuzzleTracingMiddleware; use Sentry\Tracing\SpanStatus; @@ -23,6 +25,13 @@ final class GuzzleTracingMiddlewareTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + Utils::queue(new TaskQueue(false)); + } + public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void { $client = $this->createMock(ClientInterface::class); @@ -32,15 +41,14 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void 'traces_sample_rate' => 0, ])); - $hub = new Hub($client); - - $transaction = $hub->startTransaction(TransactionContext::make()); + SentrySdk::init($client); + $transaction = SentrySdk::startTransaction(TransactionContext::make()); $this->assertFalse($transaction->getSampled()); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(static function () use ($expectedPromiseResult): PromiseInterface { return new FulfilledPromise($expectedPromiseResult); }); @@ -58,7 +66,7 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void $this->assertNull($transaction->getSpanRecorder()); - $hub->configureScope(function (Scope $scope): void { + SentrySdk::configureScope(function (Scope $scope): void { $event = Event::createEvent(); $scope->applyToEvent($event); @@ -76,15 +84,14 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void 'traces_sample_rate' => 1, ])); - $hub = new Hub($client); - - $transaction = $hub->startTransaction(TransactionContext::make()); + SentrySdk::init($client); + $transaction = SentrySdk::startTransaction(TransactionContext::make()); $this->assertTrue($transaction->getSampled()); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(static function () use ($expectedPromiseResult): PromiseInterface { return new FulfilledPromise($expectedPromiseResult); }); @@ -103,7 +110,7 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void $this->assertNotNull($transaction->getSpanRecorder()); $this->assertCount(1, $transaction->getSpanRecorder()->getSpans()); - $hub->configureScope(function (Scope $scope): void { + SentrySdk::configureScope(function (Scope $scope): void { $event = Event::createEvent(); $scope->applyToEvent($event); @@ -118,15 +125,14 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void public function testTraceHeaders(Request $request, Options $options, bool $headersShouldBePresent): void { $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) + $client->expects($this->atLeastOnce()) ->method('getOptions') ->willReturn($options); - $hub = new Hub($client); - $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + SentrySdk::init($client); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult, $headersShouldBePresent): PromiseInterface { if ($headersShouldBePresent) { $this->assertNotEmpty($request->getHeader('sentry-trace')); @@ -153,15 +159,13 @@ public function testTraceHeadersWithTransaction(Request $request, Options $optio ->method('getOptions') ->willReturn($options); - $hub = new Hub($client); - - $transaction = $hub->startTransaction(new TransactionContext()); - - $hub->setSpan($transaction); + SentrySdk::init($client); + $transaction = SentrySdk::startTransaction(new TransactionContext()); + SentrySdk::getCurrentScope()->setSpan($transaction); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult, $headersShouldBePresent): PromiseInterface { if ($headersShouldBePresent) { $this->assertNotEmpty($request->getHeader('sentry-trace')); @@ -282,7 +286,7 @@ public static function traceHeadersDataProvider(): iterable public function testTrace(Request $request, $expectedPromiseResult, array $expectedBreadcrumbData, array $expectedSpanData): void { $client = $this->createMock(ClientInterface::class); - $client->expects($this->exactly(4)) + $client->expects($this->atLeastOnce()) ->method('getOptions') ->willReturn(new Options([ 'traces_sample_rate' => 1, @@ -291,53 +295,64 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec ], ])); - $hub = new Hub($client); + SentrySdk::init($client); + + $capturedEvent = null; $client->expects($this->once()) ->method('captureEvent') - ->with($this->callback(function (Event $eventArg) use ($hub, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { - $this->assertSame(EventType::transaction(), $eventArg->getType()); + ->with( + $this->callback(function (Event $eventArg) use (&$capturedEvent): bool { + $capturedEvent = $eventArg; + + return $eventArg->getType() === EventType::transaction(); + }), + null, + $this->callback(function (Scope $scope) use (&$capturedEvent, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { + if ($capturedEvent === null) { + return false; + } - $hub->configureScope(static function (Scope $scope) use ($eventArg): void { - $scope->applyToEvent($eventArg); - }); + $eventArg = $scope->applyToEvent($capturedEvent); - $spans = $eventArg->getSpans(); - $breadcrumbs = $eventArg->getBreadcrumbs(); + $this->assertNotNull($eventArg); - $this->assertCount(1, $spans); - $this->assertCount(1, $breadcrumbs); + $spans = $eventArg->getSpans(); + $breadcrumbs = $eventArg->getBreadcrumbs(); - $guzzleSpan = $spans[0]; - $guzzleBreadcrumb = $breadcrumbs[0]; + $this->assertCount(1, $spans); + $this->assertCount(1, $breadcrumbs); - $partialUri = Uri::fromParts([ - 'scheme' => $request->getUri()->getScheme(), - 'host' => $request->getUri()->getHost(), - 'port' => $request->getUri()->getPort(), - 'path' => $request->getUri()->getPath(), - ]); + $guzzleSpan = $spans[0]; + $guzzleBreadcrumb = $breadcrumbs[0]; - $this->assertSame('http.client', $guzzleSpan->getOp()); - $this->assertSame("{$request->getMethod()} {$partialUri}", $guzzleSpan->getDescription()); + $partialUri = Uri::fromParts([ + 'scheme' => $request->getUri()->getScheme(), + 'host' => $request->getUri()->getHost(), + 'port' => $request->getUri()->getPort(), + 'path' => $request->getUri()->getPath(), + ]); - if ($expectedPromiseResult instanceof Response) { - $this->assertSame(SpanStatus::createFromHttpStatusCode($expectedPromiseResult->getStatusCode()), $guzzleSpan->getStatus()); - } else { - $this->assertSame(SpanStatus::internalError(), $guzzleSpan->getStatus()); - } + $this->assertSame('http.client', $guzzleSpan->getOp()); + $this->assertSame("{$request->getMethod()} {$partialUri}", $guzzleSpan->getDescription()); - $this->assertSame($expectedSpanData, $guzzleSpan->getData()); - $this->assertSame($expectedBreadcrumbData, $guzzleBreadcrumb->getMetadata()); + if ($expectedPromiseResult instanceof Response) { + $this->assertSame(SpanStatus::createFromHttpStatusCode($expectedPromiseResult->getStatusCode()), $guzzleSpan->getStatus()); + } else { + $this->assertSame(SpanStatus::internalError(), $guzzleSpan->getStatus()); + } - return true; - })); + $this->assertSame($expectedSpanData, $guzzleSpan->getData()); + $this->assertSame($expectedBreadcrumbData, $guzzleBreadcrumb->getMetadata()); - $transaction = $hub->startTransaction(new TransactionContext()); + return true; + }) + ); - $hub->setSpan($transaction); + $transaction = SentrySdk::startTransaction(new TransactionContext()); + SentrySdk::getCurrentScope()->setSpan($transaction); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult): PromiseInterface { $this->assertNotEmpty($request->getHeader('sentry-trace')); $this->assertNotEmpty($request->getHeader('baggage')); diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index 76a2c3aab6..5242657ded 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -10,8 +10,8 @@ use Sentry\EventId; use Sentry\EventType; use Sentry\Options; -use Sentry\State\Hub; -use Sentry\State\HubInterface; +use Sentry\SentrySdk; +use Sentry\State\Scope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -37,30 +37,31 @@ public function testFinish(): void ->method('getOptions') ->willReturn(new Options()); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getClient') - ->willReturn($client); - - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext, $client); $transaction->initSpanRecorder(); $span1 = $transaction->startChild(new SpanContext()); $span2 = $transaction->startChild(new SpanContext()); $span3 = $transaction->startChild(new SpanContext()); // This span isn't finished, so it should not be included in the event - $hub->expects($this->once()) + $client->expects($this->once()) ->method('captureEvent') - ->with($this->callback(function (Event $eventArg) use ($transactionContext, $span1, $span2): bool { - $this->assertSame(EventType::transaction(), $eventArg->getType()); - $this->assertSame($transactionContext->getName(), $eventArg->getTransaction()); - $this->assertSame($transactionContext->getStartTimestamp(), $eventArg->getStartTimestamp()); - $this->assertSame(ClockMock::microtime(true), $eventArg->getTimestamp()); - $this->assertSame($transactionContext->getTags(), $eventArg->getTags()); - $this->assertSame([$span1, $span2], $eventArg->getSpans()); - - return true; - })) + ->with( + $this->callback(function (Event $eventArg) use ($transactionContext, $span1, $span2): bool { + $this->assertSame(EventType::transaction(), $eventArg->getType()); + $this->assertSame($transactionContext->getName(), $eventArg->getTransaction()); + $this->assertSame($transactionContext->getStartTimestamp(), $eventArg->getStartTimestamp()); + $this->assertSame(ClockMock::microtime(true), $eventArg->getTimestamp()); + $this->assertSame($transactionContext->getTags(), $eventArg->getTags()); + $this->assertSame([$span1, $span2], $eventArg->getSpans()); + + return true; + }), + null, + $this->callback(function (Scope $scope): bool { + return $scope instanceof Scope; + }) + ) ->willReturnCallback(static function (Event $eventArg) use (&$expectedEventId): EventId { $expectedEventId = $eventArg->getId(); @@ -77,11 +78,11 @@ public function testFinish(): void public function testFinishDoesNothingIfSampledFlagIsNotTrue(): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->never()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->never()) ->method('captureEvent'); - $transaction = new Transaction(new TransactionContext(), $hub); + $transaction = new Transaction(new TransactionContext(), $client); $transaction->finish(); } @@ -112,7 +113,8 @@ public function testTransactionIsSampledCorrectlyWhenTracingIsSetToZeroInOptions ]) ); - $transaction = (new Hub($client))->startTransaction($context); + SentrySdk::init($client); + $transaction = SentrySdk::startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } @@ -155,7 +157,8 @@ public function testTransactionIsNotSampledWhenTracingIsDisabledInOptions(Transa ]) ); - $transaction = (new Hub($client))->startTransaction($context); + SentrySdk::init($client); + $transaction = SentrySdk::startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } diff --git a/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt b/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt index f52c12932e..ce78b482b3 100644 --- a/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt +++ b/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt @@ -57,7 +57,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering "silenced" E_USER_ERROR error' . PHP_EOL; diff --git a/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt b/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt index 0063489b73..bddadce4ce 100644 --- a/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt +++ b/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt @@ -48,7 +48,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering silenced error' . PHP_EOL; diff --git a/tests/phpt/error_handler_respects_current_error_reporting_level.phpt b/tests/phpt/error_handler_respects_current_error_reporting_level.phpt index f6017b8a55..09e0ab2416 100644 --- a/tests/phpt/error_handler_respects_current_error_reporting_level.phpt +++ b/tests/phpt/error_handler_respects_current_error_reporting_level.phpt @@ -56,7 +56,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering E_USER_NOTICE with error reporting on E_ALL' . PHP_EOL; diff --git a/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt b/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt index 35deedee90..2951de759a 100644 --- a/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt +++ b/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt @@ -50,7 +50,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering E_USER_NOTICE error' . PHP_EOL; diff --git a/tests/phpt/error_listener_integration_respects_error_types_option.phpt b/tests/phpt/error_listener_integration_respects_error_types_option.phpt index 4c66a5f9aa..aea637c91d 100644 --- a/tests/phpt/error_listener_integration_respects_error_types_option.phpt +++ b/tests/phpt/error_listener_integration_respects_error_types_option.phpt @@ -54,7 +54,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); trigger_error('Error thrown', E_USER_NOTICE); trigger_error('Error thrown', E_USER_WARNING); diff --git a/tests/phpt/php84/error_handler_captures_fatal_error.phpt b/tests/phpt/php84/error_handler_captures_fatal_error.phpt index 2c4b9143bd..b25edc0ab8 100644 --- a/tests/phpt/php84/error_handler_captures_fatal_error.phpt +++ b/tests/phpt/php84/error_handler_captures_fatal_error.phpt @@ -51,7 +51,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); $errorHandler = ErrorHandler::registerOnceErrorHandler(); $errorHandler->addErrorHandlerListener(static function (): void { diff --git a/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt b/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt index 2c8e40c4d7..70322599f4 100644 --- a/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt +++ b/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt @@ -56,7 +56,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt b/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt index 6dc65c4349..499103f455 100644 --- a/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt +++ b/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt @@ -57,7 +57,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/php85/error_handler_captures_fatal_error.phpt b/tests/phpt/php85/error_handler_captures_fatal_error.phpt index 4a4fe0e98d..92f2b5759a 100644 --- a/tests/phpt/php85/error_handler_captures_fatal_error.phpt +++ b/tests/phpt/php85/error_handler_captures_fatal_error.phpt @@ -51,7 +51,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); $errorHandler = ErrorHandler::registerOnceErrorHandler(); $errorHandler->addErrorHandlerListener(static function (): void { diff --git a/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt b/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt index 88fdd8f5cc..86b5b7f139 100644 --- a/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt +++ b/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt @@ -56,7 +56,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt b/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt index 7d74233ce5..e2e465c8c9 100644 --- a/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt +++ b/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt @@ -57,7 +57,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/test_callable_serialization.phpt b/tests/phpt/test_callable_serialization.phpt index 416628c585..dfede0423c 100644 --- a/tests/phpt/test_callable_serialization.phpt +++ b/tests/phpt/test_callable_serialization.phpt @@ -49,11 +49,11 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); class Foo { function __construct(string $bar) { - SentrySdk::getCurrentHub()->captureException(new Exception('doh!')); + \Sentry\captureException(new Exception('doh!')); } } From 055147ec80f49bf2e9daed43ade3cd551b98d2cf Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 27 Jan 2026 12:49:36 +0100 Subject: [PATCH 2/5] CS --- src/Monolog/Handler.php | 1 - src/State/Scope.php | 6 +++--- src/Tracing/Transaction.php | 6 +++--- tests/FunctionsTest.php | 1 - tests/Monolog/BreadcrumbHandlerTest.php | 2 +- tests/Monolog/HandlerTest.php | 2 +- tests/SentrySdkTest.php | 1 - tests/State/ScopeTest.php | 2 +- tests/Tracing/GuzzleTracingMiddlewareTest.php | 1 - 9 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Monolog/Handler.php b/src/Monolog/Handler.php index 03490d2f0b..8bca40f543 100644 --- a/src/Monolog/Handler.php +++ b/src/Monolog/Handler.php @@ -30,7 +30,6 @@ final class Handler extends AbstractProcessingHandler /** * {@inheritdoc} - * */ public function __construct($level = Logger::DEBUG, bool $bubble = true, bool $fillExtraContext = false) { diff --git a/src/State/Scope.php b/src/State/Scope.php index baa514c06a..29f723239a 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -637,7 +637,7 @@ public function getAttributes(): AttributeBag * @internal * * Merges data from the given scope into this one, overwriting existing values - * where applicable. + * where applicable */ public function mergeFrom(self $scope): self { @@ -661,7 +661,7 @@ public function mergeFrom(self $scope): self $this->flags = array_merge($this->flags, $scope->flags); if (\count($this->flags) > self::MAX_FLAGS) { - $this->flags = array_slice($this->flags, -self::MAX_FLAGS); + $this->flags = \array_slice($this->flags, -self::MAX_FLAGS); } } @@ -748,7 +748,7 @@ public static function applyToEventFromScopes( /** * @internal * - * Sorts breadcrumbs by their timestamp (ascending), preserving insertion order for ties. + * Sorts breadcrumbs by their timestamp (ascending), preserving insertion order for ties */ public function sortBreadcrumbsByTimestamp(): self { diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index a7e2dada43..af4163d8a6 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -4,11 +4,11 @@ namespace Sentry\Tracing; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\Profiling\Profiler; use Sentry\SentrySdk; -use Sentry\ClientInterface; /** * This class stores all the information about a Transaction. @@ -43,8 +43,8 @@ final class Transaction extends Span /** * Span constructor. * - * @param TransactionContext $context The context to create the transaction with - * @param ClientInterface|null $client Instance of a client to flush the transaction + * @param TransactionContext $context The context to create the transaction with + * @param ClientInterface|null $client Instance of a client to flush the transaction * * @internal */ diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 287b0b287f..67b01c7299 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -4,7 +4,6 @@ namespace Sentry\Tests; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; use Sentry\CheckInStatus; diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index beec8c3305..0388bb60e8 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -8,8 +8,8 @@ use Monolog\LogRecord; use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; -use Sentry\Monolog\BreadcrumbHandler; use Sentry\Event; +use Sentry\Monolog\BreadcrumbHandler; use Sentry\NoOpClient; use Sentry\SentrySdk; diff --git a/tests/Monolog/HandlerTest.php b/tests/Monolog/HandlerTest.php index 48c30314eb..71fd382f27 100644 --- a/tests/Monolog/HandlerTest.php +++ b/tests/Monolog/HandlerTest.php @@ -11,9 +11,9 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\Monolog\Handler; +use Sentry\SentrySdk; use Sentry\Severity; use Sentry\State\Scope; -use Sentry\SentrySdk; final class HandlerTest extends TestCase { diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index 8a64993ecd..eb7c644415 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -84,5 +84,4 @@ public function testGetMergedScopeCombinesScopeData(): void 'shared' => 'current', ], $event->getTags()); } - } diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 130c673dbb..14cc8407c2 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -139,7 +139,7 @@ public function testScopeAttributesApplyToScope(): void 'app.session' => 42, ]); - $this->assertSame(true, $scope->getAttributes()->get('app.feature')->getValue()); + $this->assertTrue($scope->getAttributes()->get('app.feature')->getValue()); $this->assertSame(42, $scope->getAttributes()->get('app.session')->getValue()); } diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index 9acf5d016f..a72ada4854 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -297,7 +297,6 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec SentrySdk::init($client); - $capturedEvent = null; $client->expects($this->once()) ->method('captureEvent') From 38405102968b30c109ca7bfd17b235882da98c31 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 27 Jan 2026 15:02:13 +0100 Subject: [PATCH 3/5] add breadcrumb handling logic in scope --- src/State/Scope.php | 33 +++++++++++++++++++++++++++++++++ src/State/ScopeManager.php | 36 ++++++++++-------------------------- tests/FunctionsTest.php | 34 +++++++++++++++++++++++++++++++++- tests/State/ScopeTest.php | 20 ++++++++++++++++++++ 4 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/State/Scope.php b/src/State/Scope.php index 29f723239a..8f9e7eef0e 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -11,7 +11,9 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\EventType; +use Sentry\NoOpClient; use Sentry\Options; +use Sentry\SentrySdk; use Sentry\Severity; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; @@ -342,6 +344,30 @@ public function setLevel(?Severity $level): self */ public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): self { + $client = $this->getClient() ?? SentrySdk::getClient(); + + // No point in storing breadcrumbs if the client will never send them + if ($client instanceof NoOpClient) { + return $this; + } + + $options = $client->getOptions(); + + if (\func_num_args() < 2) { + $maxBreadcrumbs = $options->getMaxBreadcrumbs(); + } + + if ($maxBreadcrumbs <= 0) { + return $this; + } + + $beforeBreadcrumbCallback = $options->getBeforeBreadcrumbCallback(); + $breadcrumb = $beforeBreadcrumbCallback($breadcrumb); + + if ($breadcrumb === null) { + return $this; + } + $this->breadcrumbs[] = $breadcrumb; $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); @@ -800,6 +826,13 @@ public function __clone() public function addAttachment(Attachment $attachment): self { + $client = $this->getClient() ?? SentrySdk::getClient(); + + // No point in storing attachments if the client will never send them + if ($client instanceof NoOpClient) { + return $this; + } + $this->attachments[] = $attachment; return $this; diff --git a/src/State/ScopeManager.php b/src/State/ScopeManager.php index e5771a313f..b17aadee7d 100644 --- a/src/State/ScopeManager.php +++ b/src/State/ScopeManager.php @@ -17,9 +17,9 @@ final class ScopeManager private $globalScope; /** - * @var Scope[] Stack of isolation scopes (request/execution context) + * @var Scope|null The current isolation scope (request/execution context) */ - private $isolationScopeStack = []; + private $isolationScope; /** * @var Scope[] Stack of current scopes (active span context) @@ -92,40 +92,24 @@ public function withScope(callable $callback) public function withIsolationScope(callable $callback) { $this->pushCurrentScope(); - $scope = $this->pushIsolationScope(); + $previousIsolationScope = $this->getOrCreateIsolationScope(); + $scope = clone $previousIsolationScope; + $this->isolationScope = $scope; try { return $callback($scope); } finally { $this->popCurrentScope(); - $this->popIsolationScope(); + $this->isolationScope = $previousIsolationScope; } } public function resetScopes(): void { - $this->isolationScopeStack = []; + $this->isolationScope = null; $this->currentScopeStack = []; } - private function pushIsolationScope(): Scope - { - $scope = clone $this->getOrCreateIsolationScope(); - $scope->setType(ScopeType::isolation()); - $this->isolationScopeStack[] = $scope; - - return $scope; - } - - private function popIsolationScope(): bool - { - if (\count($this->isolationScopeStack) <= 1) { - return false; - } - - return array_pop($this->isolationScopeStack) !== null; - } - private function pushCurrentScope(): Scope { $scope = clone $this->getOrCreateCurrentScope(); @@ -146,11 +130,11 @@ private function popCurrentScope(): bool private function getOrCreateIsolationScope(): Scope { - if (empty($this->isolationScopeStack)) { - $this->isolationScopeStack[] = new Scope(null, ScopeType::isolation()); + if ($this->isolationScope === null) { + $this->isolationScope = new Scope(null, ScopeType::isolation()); } - return $this->isolationScopeStack[\count($this->isolationScopeStack) - 1]; + return $this->isolationScope; } private function getOrCreateCurrentScope(): Scope diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 67b01c7299..3ef4263bc4 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -310,7 +310,11 @@ public function testAddBreadcrumb(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - SentrySdk::init(new NoOpClient()); + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options(['default_integrations' => false])); + + SentrySdk::init($client); addBreadcrumb($breadcrumb); configureScope(function (Scope $scope) use ($breadcrumb): void { @@ -321,6 +325,34 @@ public function testAddBreadcrumb(): void }); } + public function testAddBreadcrumbRespectsBeforeBreadcrumbAndMaxBreadcrumbs(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function (Breadcrumb $breadcrumb): ?Breadcrumb { + return $breadcrumb->withMessage('mutated'); + }, + 'max_breadcrumbs' => 1, + 'default_integrations' => false, + ])); + + SentrySdk::init($client); + + SentrySdk::getIsolationScope()->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'first', 'first message')); + SentrySdk::getIsolationScope()->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'second', 'second message')); + + configureScope(function (Scope $scope): void { + $event = $scope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $breadcrumbs = $event->getBreadcrumbs(); + $this->assertCount(1, $breadcrumbs); + $this->assertSame('second', $breadcrumbs[0]->getCategory()); + $this->assertSame('mutated', $breadcrumbs[0]->getMessage()); + }); + } + public function testWithScope(): void { $returnValue = withScope(static function (): string { diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 14cc8407c2..77f145bad0 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -7,8 +7,10 @@ use PHPUnit\Framework\TestCase; use Sentry\Attachment\Attachment; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; +use Sentry\Options; use Sentry\Severity; use Sentry\State\Scope; use Sentry\State\ScopeType; @@ -24,6 +26,15 @@ final class ScopeTest extends TestCase { + private function createClientWithOptions(): ClientInterface + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options(['default_integrations' => false])); + + return $client; + } + public function testScopeTypeSingletons(): void { $this->assertSame(ScopeType::global(), ScopeType::global()); @@ -75,13 +86,17 @@ public function testMergeOrderPrefersCurrentScope(): void public function testBreadcrumbsAreMergedAndSorted(): void { + $client = $this->createClientWithOptions(); $globalScope = new Scope(null, ScopeType::global()); + $globalScope->bindClient($client); $globalScope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'global', null, [], 2000.0)); $isolationScope = new Scope(null, ScopeType::isolation()); + $isolationScope->bindClient($client); $isolationScope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation', null, [], 1500.0)); $currentScope = new Scope(null, ScopeType::current()); + $currentScope->bindClient($client); $currentScope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'current', null, [], 1000.0)); $event = Event::createEvent(); @@ -482,6 +497,7 @@ public function testSetLevel(): void public function testAddBreadcrumb(): void { $scope = new Scope(); + $scope->bindClient($this->createClientWithOptions()); $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); @@ -510,6 +526,7 @@ public function testAddBreadcrumb(): void public function testClearBreadcrumbs(): void { $scope = new Scope(); + $scope->bindClient($this->createClientWithOptions()); $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); @@ -592,6 +609,7 @@ public function testEventProcessorReceivesTheEventAndEventHint(): void public function testClear(): void { $scope = new Scope(); + $scope->bindClient($this->createClientWithOptions()); $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $scope->setLevel(Severity::info()); @@ -632,6 +650,7 @@ public function testApplyToEvent(): void $span->setSpanId(new SpanId('566e3688a61d4bc8')); $scope = new Scope(); + $scope->bindClient($this->createClientWithOptions()); $scope->setLevel(Severity::warning()); $scope->setFingerprint(['foo']); $scope->addBreadcrumb($breadcrumb); @@ -687,6 +706,7 @@ public function testApplyToEvent(): void public function testAttachmentsAppliedForType(Event $event, int $attachmentCount): void { $scope = new Scope(); + $scope->bindClient($this->createClientWithOptions()); $scope->addAttachment(Attachment::fromBytes('test', 'abcde')); $scope->applyToEvent($event); $this->assertCount($attachmentCount, $event->getAttachments()); From 88181f8af13e48d317ae56582ad24b9555e8c5c2 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Wed, 28 Jan 2026 11:19:40 +0100 Subject: [PATCH 4/5] fix client for breadcrumb tests --- tests/Monolog/BreadcrumbHandlerTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index 0388bb60e8..1273a3585a 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -8,9 +8,10 @@ use Monolog\LogRecord; use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\Monolog\BreadcrumbHandler; -use Sentry\NoOpClient; +use Sentry\Options; use Sentry\SentrySdk; final class BreadcrumbHandlerTest extends TestCase @@ -20,7 +21,11 @@ final class BreadcrumbHandlerTest extends TestCase */ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void { - SentrySdk::init(new NoOpClient()); + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options(['default_integrations' => false])); + + SentrySdk::init($client); $handler = new BreadcrumbHandler(); $handler->handle($record); From dd9d6311fc775a7181b30e09454d5d6e23261c1d Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Fri, 30 Jan 2026 10:18:31 +0100 Subject: [PATCH 5/5] dont call bindClient on null --- .../php84/out_of_memory_fatal_error_increases_memory_limit.phpt | 2 +- .../php85/out_of_memory_fatal_error_increases_memory_limit.phpt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt b/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt index 321ed418a1..ae2180a79c 100644 --- a/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt +++ b/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt @@ -63,7 +63,7 @@ $options->setTransport($transport); $client = (new ClientBuilder($options))->getClient(); -SentrySdk::init()->bindClient($client); +SentrySdk::init($client); echo 'Before OOM memory limit: ' . \ini_get('memory_limit'); diff --git a/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt b/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt index 92e0305881..aefae23465 100644 --- a/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt +++ b/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt @@ -63,7 +63,7 @@ $options->setTransport($transport); $client = (new ClientBuilder($options))->getClient(); -SentrySdk::init()->bindClient($client); +SentrySdk::init($client); echo 'Before OOM memory limit: ' . \ini_get('memory_limit');