From 19d38f7446af6325700e2cebe6f30eb2a9f97735 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 22 Jun 2026 15:32:07 +0200 Subject: [PATCH 1/3] feat(scopes): swap many usages of hub to scope --- src/Logs/LogsAggregator.php | 7 +- src/Metrics/MetricsAggregator.php | 7 +- src/SentrySdk.php | 41 ++--- src/State/HubAdapter.php | 111 ++++++++---- src/State/HubInterface.php | 16 -- src/State/RuntimeContext.php | 18 +- src/State/RuntimeContextManager.php | 92 ++-------- src/functions.php | 47 +++++- tests/FunctionsTest.php | 228 +++++++++++-------------- tests/Logs/LogsAggregatorTest.php | 16 +- tests/SentrySdkExtension.php | 9 - tests/SentrySdkTest.php | 75 +++++---- tests/State/HubAdapterTest.php | 253 +++++++++------------------- 13 files changed, 380 insertions(+), 540 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 99c0370c9..0915d4655 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -164,7 +164,7 @@ public function add( } } - public function flush(?ClientInterface $client = null): ?EventId + public function flush(?ClientInterface $client = null, ?Scope $isolationScope = null): ?EventId { $logs = $this->logs; @@ -172,10 +172,11 @@ public function flush(?ClientInterface $client = null): ?EventId return null; } - $client = $client ?? SentrySdk::getCurrentHub()->getClient(); + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + $client = $client ?? SentrySdk::getClient($isolationScope); $event = Event::createLogs()->setLogs($logs->drain()); - return $client->captureEvent($event); + return $client->captureEvent($event, null, Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope)); } /** diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index 7fdae2394..b58fddd93 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -129,7 +129,7 @@ public function add( } } - public function flush(?ClientInterface $client = null): ?EventId + public function flush(?ClientInterface $client = null, ?Scope $isolationScope = null): ?EventId { $metrics = $this->metrics; @@ -137,10 +137,11 @@ public function flush(?ClientInterface $client = null): ?EventId return null; } - $client = $client ?? SentrySdk::getCurrentHub()->getClient(); + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + $client = $client ?? SentrySdk::getClient($isolationScope); $event = Event::createMetrics()->setMetrics($metrics->drain()); - return $client->captureEvent($event); + return $client->captureEvent($event, null, Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope)); } /** diff --git a/src/SentrySdk.php b/src/SentrySdk.php index e28697b15..3822d4a28 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -6,7 +6,7 @@ use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; -use Sentry\State\Hub; +use Sentry\State\HubAdapter; use Sentry\State\HubInterface; use Sentry\State\RuntimeContext; use Sentry\State\RuntimeContextManager; @@ -19,11 +19,6 @@ */ final class SentrySdk { - /** - * @var HubInterface|null The baseline hub - */ - private static $currentHub; - /** * @var Scope|null The process-global scope */ @@ -42,29 +37,22 @@ private function __construct() } /** - * Initializes the SDK by creating a new hub instance each time this method - * gets called. + * Initializes the SDK by binding the client to the global scope and reset + * the current local runtime state. */ public static function init(?ClientInterface $client = null): HubInterface { - $hubClient = $client ?? new NoOpClient(); - if ($client !== null) { self::getGlobalScope()->setClient($client); } - self::$currentHub = new Hub($hubClient); - self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub); + self::$runtimeContextManager = new RuntimeContextManager(); return self::getCurrentHub(); } - /** - * Gets the current hub. If it's not initialized then creates a new instance - * and sets it as current hub. - */ public static function getCurrentHub(): HubInterface { - return self::getRuntimeContextManager()->getCurrentHub(); + return HubAdapter::getInstance(); } /** @@ -78,11 +66,7 @@ public static function getCurrentHub(): HubInterface */ public static function setCurrentHub(HubInterface $hub): HubInterface { - $wasSetOnActiveRuntimeContext = self::getRuntimeContextManager()->setCurrentHub($hub); - - if (!$wasSetOnActiveRuntimeContext) { - self::$currentHub = $hub; - } + self::getGlobalScope()->setClient($hub->getClient()); return $hub; } @@ -101,9 +85,9 @@ public static function getIsolationScope(): Scope return self::getCurrentRuntimeContext()->getIsolationScope(); } - public static function getClient(): ClientInterface + public static function getClient(?Scope $isolationScope = null): ClientInterface { - $client = self::getIsolationScope()->getClient(); + $client = ($isolationScope ?? self::getIsolationScope())->getClient(); if (!$client instanceof NoOpClient) { return $client; @@ -181,18 +165,13 @@ public static function flush(): void Logs::getInstance()->flush(); TraceMetrics::getInstance()->flush(); - $client = self::getCurrentHub()->getClient(); - $client->flush(); + self::getClient()->flush(); } private static function getRuntimeContextManager(): RuntimeContextManager { - if (self::$currentHub === null) { - self::$currentHub = new Hub(new NoOpClient()); - } - if (self::$runtimeContextManager === null) { - self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub); + self::$runtimeContextManager = new RuntimeContextManager(); } return self::$runtimeContextManager; diff --git a/src/State/HubAdapter.php b/src/State/HubAdapter.php index 83272be9b..a5aca9f5b 100644 --- a/src/State/HubAdapter.php +++ b/src/State/HubAdapter.php @@ -13,11 +13,13 @@ use Sentry\EventId; use Sentry\Integration\IntegrationInterface; use Sentry\MonitorConfig; +use Sentry\NoOpClient; use Sentry\SentrySdk; use Sentry\Severity; use Sentry\Tracing\Span; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSampler; /** * An implementation of {@see HubInterface} that uses {@see SentrySdk} internally @@ -55,7 +57,7 @@ public static function getInstance(): self */ public function getClient(): ClientInterface { - return SentrySdk::getCurrentHub()->getClient(); + return SentrySdk::getClient(); } /** @@ -63,23 +65,7 @@ public function getClient(): ClientInterface */ 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(); + return SentrySdk::getIsolationScope()->getLastEventId(); } /** @@ -87,7 +73,7 @@ public function popScope(): bool */ public function withScope(callable $callback) { - return SentrySdk::getCurrentHub()->withScope($callback); + return \Sentry\withIsolationScope($callback); } /** @@ -95,7 +81,7 @@ public function withScope(callable $callback) */ public function configureScope(callable $callback): void { - SentrySdk::getCurrentHub()->configureScope($callback); + $callback(SentrySdk::getIsolationScope()); } /** @@ -103,7 +89,7 @@ public function configureScope(callable $callback): void */ public function bindClient(ClientInterface $client): void { - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::getGlobalScope()->setClient($client); } /** @@ -111,7 +97,10 @@ public function bindClient(ClientInterface $client): void */ public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); + $eventId = SentrySdk::getClient()->captureMessage($message, $level, SentrySdk::getIsolationScope(), $hint); + SentrySdk::getIsolationScope()->setLastEventId($eventId); + + return $eventId; } /** @@ -119,7 +108,10 @@ public function captureMessage(string $message, ?Severity $level = null, ?EventH */ public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureException($exception, $hint); + $eventId = SentrySdk::getClient()->captureException($exception, SentrySdk::getIsolationScope(), $hint); + SentrySdk::getIsolationScope()->setLastEventId($eventId); + + return $eventId; } /** @@ -127,7 +119,10 @@ public function captureException(\Throwable $exception, ?EventHint $hint = null) */ public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureEvent($event, $hint); + $eventId = SentrySdk::getClient()->captureEvent($event, $hint, SentrySdk::getIsolationScope()); + SentrySdk::getIsolationScope()->setLastEventId($eventId); + + return $eventId; } /** @@ -135,7 +130,10 @@ public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId */ public function captureLastError(?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureLastError($hint); + $eventId = SentrySdk::getClient()->captureLastError(SentrySdk::getIsolationScope(), $hint); + SentrySdk::getIsolationScope()->setLastEventId($eventId); + + return $eventId; } /** @@ -145,7 +143,27 @@ public function captureLastError(?EventHint $hint = null): ?EventId */ 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); + $client = SentrySdk::getClient(); + + if ($client instanceof NoOpClient) { + return null; + } + + $options = $client->getOptions(); + $event = Event::createCheckIn(); + $checkIn = new \Sentry\CheckIn( + $slug, + $status, + $checkInId, + $options->getRelease(), + $options->getEnvironment(), + $duration, + $monitorConfig + ); + $event->setCheckIn($checkIn); + $this->captureEvent($event); + + return $checkIn->getId(); } /** @@ -153,7 +171,26 @@ public function captureCheckIn(string $slug, CheckInStatus $status, $duration = */ public function addBreadcrumb(Breadcrumb $breadcrumb): bool { - return SentrySdk::getCurrentHub()->addBreadcrumb($breadcrumb); + $client = SentrySdk::getClient(); + + if ($client instanceof NoOpClient) { + return false; + } + + $options = $client->getOptions(); + $maxBreadcrumbs = $options->getMaxBreadcrumbs(); + + if ($maxBreadcrumbs <= 0) { + return false; + } + + $breadcrumb = ($options->getBeforeBreadcrumbCallback())($breadcrumb); + + if ($breadcrumb !== null) { + SentrySdk::getIsolationScope()->addBreadcrumb($breadcrumb, $maxBreadcrumbs); + } + + return $breadcrumb !== null; } /** @@ -161,7 +198,13 @@ public function addBreadcrumb(Breadcrumb $breadcrumb): bool */ public function addAttachment(Attachment $attachment): bool { - return SentrySdk::getCurrentHub()->addAttachment($attachment); + if (SentrySdk::getClient() instanceof NoOpClient) { + return false; + } + + SentrySdk::getIsolationScope()->addAttachment($attachment); + + return true; } /** @@ -169,7 +212,7 @@ public function addAttachment(Attachment $attachment): bool */ public function getIntegration(string $className): ?IntegrationInterface { - return SentrySdk::getCurrentHub()->getIntegration($className); + return SentrySdk::getClient()->getIntegration($className); } /** @@ -177,7 +220,7 @@ public function getIntegration(string $className): ?IntegrationInterface */ public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); + return TransactionSampler::startTransaction(SentrySdk::getClient()->getOptions(), $context, $customSamplingContext); } /** @@ -185,7 +228,7 @@ public function startTransaction(TransactionContext $context, array $customSampl */ public function getTransaction(): ?Transaction { - return SentrySdk::getCurrentHub()->getTransaction(); + return SentrySdk::getIsolationScope()->getTransaction(); } /** @@ -193,7 +236,7 @@ public function getTransaction(): ?Transaction */ public function getSpan(): ?Span { - return SentrySdk::getCurrentHub()->getSpan(); + return SentrySdk::getIsolationScope()->getSpan(); } /** @@ -201,7 +244,9 @@ public function getSpan(): ?Span */ public function setSpan(?Span $span): HubInterface { - return SentrySdk::getCurrentHub()->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); + + return $this; } /** diff --git a/src/State/HubInterface.php b/src/State/HubInterface.php index c3c8c67a1..c34ab7e43 100644 --- a/src/State/HubInterface.php +++ b/src/State/HubInterface.php @@ -30,22 +30,6 @@ public function getClient(): ClientInterface; */ public function getLastEventId(): ?EventId; - /** - * Creates a new scope to store context information that will be layered on - * top of the current one. It is isolated, i.e. all breadcrumbs and context - * information added to this scope will be removed once the scope ends. Be - * sure to always remove this scope with {@see Hub::popScope} when the - * operation finishes or throws. - */ - public function pushScope(): Scope; - - /** - * Removes a previously pushed scope from the stack. This restores the state - * before the scope was pushed. All breadcrumbs and context information added - * since the last call to {@see Hub::pushScope} are discarded. - */ - public function popScope(): bool; - /** * Creates a new scope with and executes the given operation within. The scope * is automatically removed once the operation finishes or throws. diff --git a/src/State/RuntimeContext.php b/src/State/RuntimeContext.php index 6268ddc2b..b0dfe177c 100644 --- a/src/State/RuntimeContext.php +++ b/src/State/RuntimeContext.php @@ -22,11 +22,6 @@ final class RuntimeContext */ private $id; - /** - * @var HubInterface - */ - private $hub; - /** * @var Scope */ @@ -42,10 +37,9 @@ final class RuntimeContext */ private $metricsAggregator; - public function __construct(string $id, HubInterface $hub, ?Scope $isolationScope = null) + public function __construct(string $id, ?Scope $isolationScope = null) { $this->id = $id; - $this->hub = $hub; $this->isolationScope = $isolationScope ?? new Scope(); $this->logsAggregator = new LogsAggregator(); $this->metricsAggregator = new MetricsAggregator(); @@ -56,16 +50,6 @@ public function getId(): string return $this->id; } - public function getHub(): HubInterface - { - return $this->hub; - } - - public function setHub(HubInterface $hub): void - { - $this->hub = $hub; - } - public function getIsolationScope(): Scope { return $this->isolationScope; diff --git a/src/State/RuntimeContextManager.php b/src/State/RuntimeContextManager.php index db1a80a2e..5cd3a723c 100644 --- a/src/State/RuntimeContextManager.php +++ b/src/State/RuntimeContextManager.php @@ -5,7 +5,8 @@ namespace Sentry\State; use Psr\Log\LoggerInterface; -use Sentry\Tracing\PropagationContext; +use Sentry\ClientInterface; +use Sentry\SentrySdk; /** * Manages runtime-local SDK state across different execution models. @@ -22,15 +23,10 @@ final class RuntimeContextManager { private const PROCESS_EXECUTION_CONTEXT_KEY = 'process'; - /** - * @var HubInterface - */ - private $baseHub; - /** * @var RuntimeContext|null */ - private $globalContext; + private $globalContext = null; /** * @var array @@ -42,46 +38,6 @@ final class RuntimeContextManager */ private $executionContextToRuntimeContext = []; - public function __construct(HubInterface $baseHub) - { - $this->baseHub = $baseHub; - $this->globalContext = null; - } - - /** - * Sets the current hub with context-aware behavior. - * - * If a runtime context is active for the current execution key, the hub is - * updated only for that active context. Otherwise, the baseline/global hub - * template is updated. - * - * @return bool Whether the hub was set on an active runtime context - */ - public function setCurrentHub(HubInterface $hub): bool - { - $executionContextKey = $this->getExecutionContextKey(); - - if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) { - $runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey]; - $this->activeContexts[$runtimeContextId]->setHub($hub); - - return true; - } - - $this->baseHub = $hub; - - if ($this->globalContext !== null) { - $this->globalContext->setHub($hub); - } - - return false; - } - - public function getCurrentHub(): HubInterface - { - return $this->getCurrentContext()->getHub(); - } - public function getCurrentContext(): RuntimeContext { $executionContextKey = $this->getExecutionContextKey(); @@ -137,7 +93,7 @@ public function endContext(?int $timeout = null): void private function createContextForExecutionContextKey(string $executionContextKey): void { $runtimeContextId = $this->generateRuntimeContextId(); - $runtimeContext = new RuntimeContext($runtimeContextId, $this->createHubFromBaseHub()); + $runtimeContext = new RuntimeContext($runtimeContextId); $this->activeContexts[$runtimeContextId] = $runtimeContext; $this->executionContextToRuntimeContext[$executionContextKey] = $runtimeContextId; @@ -154,19 +110,18 @@ private function removeContextById(string $runtimeContextId, ?int $timeout = nul // Remove any key mappings that may still reference this context. $this->removeExecutionContextMappingsForRuntimeContext($runtimeContextId); - $logger = $this->getLoggerFromHub($runtimeContext->getHub()); + $client = SentrySdk::getClient($runtimeContext->getIsolationScope()); + $logger = $client->getOptions()->getLoggerOrNullLogger(); - $this->flushRuntimeContextResources($runtimeContext, $timeout, $logger); + $this->flushRuntimeContextResources($runtimeContext, $client, $timeout, $logger); } - private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?int $timeout, LoggerInterface $logger): void + private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ClientInterface $client, ?int $timeout, LoggerInterface $logger): void { - $client = $runtimeContext->getHub()->getClient(); - // captureEvent can throw before transport send (for example from scope event processors // or before_send callbacks), so we isolate failures and continue flushing other resources. try { - $runtimeContext->getLogsAggregator()->flush($client); + $runtimeContext->getLogsAggregator()->flush($client, $runtimeContext->getIsolationScope()); } catch (\Throwable $exception) { $logger->error('Failed to flush logs while ending a runtime context.', [ 'exception' => $exception, @@ -176,7 +131,7 @@ private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?i // Keep metrics flush independent from logs flush so one bad callback does not block the rest. try { - $runtimeContext->getMetricsAggregator()->flush($client); + $runtimeContext->getMetricsAggregator()->flush($client, $runtimeContext->getIsolationScope()); } catch (\Throwable $exception) { $logger->error('Failed to flush trace metrics while ending a runtime context.', [ 'exception' => $exception, @@ -222,31 +177,6 @@ private function hasActiveContextForExecutionContextKey(string $executionContext return true; } - private function createHubFromBaseHub(): HubInterface - { - if (!$this->baseHub instanceof Hub) { - return new Hub($this->baseHub->getClient()); - } - - $clonedScope = null; - - $this->baseHub->configureScope(static function (Scope $scope) use (&$clonedScope): void { - $clonedScope = clone $scope; - // Do not inherit active traces into a new runtime context. - $clonedScope->setSpan(null); - $clonedScope->setPropagationContext(PropagationContext::fromDefaults()); - }); - - return new Hub($this->baseHub->getClient(), $clonedScope ?? new Scope()); - } - - private function getLoggerFromHub(HubInterface $hub): LoggerInterface - { - $client = $hub->getClient(); - - return $client->getOptions()->getLoggerOrNullLogger(); - } - private function generateRuntimeContextId(): string { return \sprintf('%s-%d', str_replace('.', '', uniqid('', true)), mt_rand()); @@ -262,7 +192,7 @@ private function getGlobalContext(): RuntimeContext { if ($this->globalContext === null) { // Lazy fallback keeps baseline behavior when users do not opt into explicit context lifecycle. - $this->globalContext = new RuntimeContext('global', $this->baseHub); + $this->globalContext = new RuntimeContext('global'); } return $this->globalContext; diff --git a/src/functions.php b/src/functions.php index 1549bf9fe..845bfd594 100644 --- a/src/functions.php +++ b/src/functions.php @@ -77,6 +77,21 @@ function init(array $options = []): void SentrySdk::init($client); } +function getGlobalScope(): Scope +{ + return SentrySdk::getGlobalScope(); +} + +function getIsolationScope(): Scope +{ + return SentrySdk::getIsolationScope(); +} + +function getClient(): ClientInterface +{ + return SentrySdk::getClient(); +} + /** * Captures a message event and sends it to Sentry. * @@ -211,10 +226,38 @@ function configureScope(callable $callback): void * @return mixed|void The callback's return value, upon successful execution * * @phpstan-return T + * + * @deprecated This function will be removed in a follow-up PR. Use {@see withIsolationScope()} instead. */ function withScope(callable $callback) { - return SentrySdk::getCurrentHub()->withScope($callback); + return withIsolationScope($callback); +} + +/** + * Forks the current isolation scope for the duration of the callback. + * + * @param callable $callback The callback to be executed + * + * @phpstan-template T + * + * @phpstan-param callable(Scope): T $callback + * + * @return mixed|void The callback's return value, upon successful execution + * + * @phpstan-return T + */ +function withIsolationScope(callable $callback) +{ + $context = SentrySdk::getCurrentRuntimeContext(); + $previousScope = $context->getIsolationScope(); + $context->setIsolationScope(clone $previousScope); + + try { + return $callback($context->getIsolationScope()); + } finally { + $context->setIsolationScope($previousScope); + } } function startContext(): void @@ -268,7 +311,7 @@ function withContext(callable $callback, ?int $timeout = null) */ function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); + return Tracing\TransactionSampler::startTransaction(SentrySdk::getClient()->getOptions(), $context, $customSamplingContext); } /** diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 4d5c5166a..2d1d47a7c 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -20,7 +20,6 @@ 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; @@ -85,14 +84,18 @@ public function testInitPreservesGlobalScope(): void public function testCaptureMessage(array $functionCallArgs, array $expectedFunctionCallArgs): void { $eventId = EventId::generate(); + $message = $expectedFunctionCallArgs[0]; + $level = $expectedFunctionCallArgs[1]; + $scope = $this->isInstanceOf(Scope::class); + $hint = $expectedFunctionCallArgs[2]; - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) + ->with($message, $level, $scope, $hint) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($eventId, captureMessage(...$functionCallArgs)); } @@ -132,13 +135,13 @@ public function testCaptureException(array $functionCallArgs, array $expectedFun { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureException') - ->with(...$expectedFunctionCallArgs) + ->with($expectedFunctionCallArgs[0], $this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[1]) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($eventId, captureException(...$functionCallArgs)); } @@ -172,13 +175,13 @@ 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->isInstanceOf(Scope::class)) ->willReturn($event->getId()); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($event->getId(), captureEvent($event, $hint)); } @@ -190,13 +193,13 @@ public function testCaptureLastError(array $functionCallArgs, array $expectedFun { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) + ->with($this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[0]) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); @trigger_error('foo', \E_USER_NOTICE); @@ -226,13 +229,20 @@ public function testCaptureCheckIn(): void 'UTC' ); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureCheckIn') - ->with('test-crontab', CheckInStatus::ok(), 10, $monitorConfig, $checkInId) - ->willReturn($checkInId); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event) use ($checkInId): bool { + $checkIn = $event->getCheckIn(); - SentrySdk::setCurrentHub($hub); + return $checkIn !== null && $checkIn->getId() === $checkInId; + }), null, $this->isInstanceOf(Scope::class)) + ->willReturn(EventId::generate()); + + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($checkInId, captureCheckIn( 'test-crontab', @@ -245,28 +255,22 @@ public function testCaptureCheckIn(): void public function testWithMonitor(): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->exactly(2)) - ->method('captureCheckIn') - ->with( - $this->callback(static function (string $slug): bool { - return $slug === 'test-crontab'; - }), - $this->callback(static function (CheckInStatus $checkInStatus): bool { - // just check for type CheckInStatus - return true; - }), - $this->anything(), - $this->callback(static function (MonitorConfig $monitorConfig): bool { - return $monitorConfig->getSchedule()->getValue() === '*/5 * * * *' - && $monitorConfig->getSchedule()->getType() === MonitorSchedule::TYPE_CRONTAB - && $monitorConfig->getCheckinMargin() === 5 - && $monitorConfig->getMaxRuntime() === 30 - && $monitorConfig->getTimezone() === 'UTC'; - }) - ); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->exactly(2)) + ->method('getOptions') + ->willReturn(new Options()); + $client->expects($this->exactly(2)) + ->method('captureEvent') + ->with($this->callback(static function (Event $event): bool { + $checkIn = $event->getCheckIn(); - SentrySdk::setCurrentHub($hub); + return $checkIn !== null + && $checkIn->getMonitorSlug() === 'test-crontab' + && $checkIn->getMonitorConfig() !== null + && $checkIn->getMonitorConfig()->getSchedule()->getValue() === '*/5 * * * *'; + }), null, $this->isInstanceOf(Scope::class)); + + SentrySdk::getGlobalScope()->setClient($client); withMonitor('test-crontab', static function () { // Do something... @@ -282,28 +286,15 @@ public function testWithMonitorCallableThrows(): void { $this->expectException(\Exception::class); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->exactly(2)) - ->method('captureCheckIn') - ->with( - $this->callback(static function (string $slug): bool { - return $slug === 'test-crontab'; - }), - $this->callback(static function (CheckInStatus $checkInStatus): bool { - // just check for type CheckInStatus - return true; - }), - $this->anything(), - $this->callback(static function (MonitorConfig $monitorConfig): bool { - return $monitorConfig->getSchedule()->getValue() === '*/5 * * * *' - && $monitorConfig->getSchedule()->getType() === MonitorSchedule::TYPE_CRONTAB - && $monitorConfig->getCheckinMargin() === 5 - && $monitorConfig->getMaxRuntime() === 30 - && $monitorConfig->getTimezone() === 'UTC'; - }) - ); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->exactly(2)) + ->method('getOptions') + ->willReturn(new Options()); + $client->expects($this->exactly(2)) + ->method('captureEvent') + ->with($this->isInstanceOf(Event::class), null, $this->isInstanceOf(Scope::class)); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); withMonitor('test-crontab', static function () { throw new \Exception(); @@ -360,52 +351,52 @@ public function testStartAndEndContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); + $globalScope = SentrySdk::getIsolationScope(); startContext(); - $requestHub = SentrySdk::getCurrentHub(); + $requestScope = SentrySdk::getIsolationScope(); - $this->assertNotSame($globalHub, $requestHub); + $this->assertNotSame($globalScope, $requestScope); endContext(); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testWithContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); + $globalScope = SentrySdk::getIsolationScope(); - $result = withContext(function () use ($globalHub): string { - $this->assertNotSame($globalHub, SentrySdk::getCurrentHub()); + $result = withContext(function () use ($globalScope): string { + $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); return 'ok'; }); $this->assertSame('ok', $result); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testNestedWithContextReusesOuterContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $outerHub = null; - $innerHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $outerScope = null; + $innerScope = null; - withContext(function () use (&$outerHub, &$innerHub, $globalHub): void { - $outerHub = SentrySdk::getCurrentHub(); + withContext(function () use (&$outerScope, &$innerScope, $globalScope): void { + $outerScope = SentrySdk::getIsolationScope(); configureScope(static function (Scope $scope): void { $scope->setTag('outer', 'yes'); }); - withContext(static function () use (&$innerHub): void { - $innerHub = SentrySdk::getCurrentHub(); + withContext(static function () use (&$innerScope): void { + $innerScope = SentrySdk::getIsolationScope(); }); $event = Event::createEvent(); @@ -414,14 +405,14 @@ public function testNestedWithContextReusesOuterContext(): void $event = $scope->applyToEvent($event); }); - $this->assertNotSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); $this->assertSame('yes', $event->getTags()['outer'] ?? null); }); - $this->assertNotNull($outerHub); - $this->assertNotNull($innerHub); - $this->assertSame($outerHub, $innerHub); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotNull($outerScope); + $this->assertNotNull($innerScope); + $this->assertSame($outerScope, $innerScope); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testWithContextAlwaysEndsContextWithOptionalTimeout(): void @@ -455,15 +446,16 @@ 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()); + + SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::setCurrentHub($hub); + $transaction = startTransaction($transactionContext, $customSamplingContext); - $this->assertSame($transaction, startTransaction($transactionContext, $customSamplingContext)); + $this->assertSame('foo', $transaction->getName()); } public function testTraceReturnsClosureResult(): void @@ -479,29 +471,25 @@ 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::setCurrentHub($hub); + SentrySdk::getCurrentHub()->setSpan($transaction); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); - trace(function () use ($transaction, $hub) { - $this->assertNotSame($transaction, $hub->getSpan()); + trace(function (Scope $scope) use ($transaction) { + $this->assertNotSame($transaction, $scope->getSpan()); }, new SpanContext()); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); try { trace(static 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::getCurrentHub()->getSpan()); } } @@ -509,8 +497,6 @@ public function testTraceDoesntCreateSpanIfTransactionIsNotSampled(): void { $scope = $this->createMock(Scope::class); - $hub = new Hub(new NoOpClient(), $scope); - $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(false); @@ -520,13 +506,13 @@ public function testTraceDoesntCreateSpanIfTransactionIsNotSampled(): void ->method('getSpan') ->willReturn($transaction); - SentrySdk::setCurrentHub($hub); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); - trace(function () use ($transaction, $hub) { - $this->assertSame($transaction, $hub->getSpan()); + trace(function () use ($transaction) { + $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); }, SpanContext::make()); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); } public function testTraceparentWithTracingDisabled(): void @@ -535,11 +521,7 @@ 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::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); $traceParent = getTraceparent(); @@ -555,9 +537,7 @@ public function testTraceparentWithTracingEnabled(): void 'traces_sample_rate' => 1.0, ])); - $hub = new Hub($client); - - SentrySdk::setCurrentHub($hub); + SentrySdk::setCurrentHub(new Hub($client)); $spanContext = (new SpanContext()) ->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')) @@ -565,7 +545,7 @@ public function testTraceparentWithTracingEnabled(): void $span = new Span($spanContext); - $hub->setSpan($span); + SentrySdk::getCurrentHub()->setSpan($span); $traceParent = getTraceparent(); @@ -585,7 +565,7 @@ public function testTraceHeadersAreEmptyWhenExternalPropagationContextIsActive() ]; }); - SentrySdk::setCurrentHub(new Hub(new NoOpClient(), new Scope($propagationContext))); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); $this->assertSame('', getTraceparent()); $this->assertSame('', getBaggage()); @@ -599,8 +579,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') @@ -609,9 +587,8 @@ public function testBaggageWithTracingDisabled(): void 'environment' => 'development', ])); - $hub = new Hub($client, $scope); - - SentrySdk::setCurrentHub($hub); + SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); $baggage = getBaggage(); @@ -629,10 +606,7 @@ public function testBaggageWithTracingEnabled(): void 'environment' => 'development', ])); - $hub = new Hub($client); - SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::setCurrentHub($hub); $transactionContext = new TransactionContext(); $transactionContext->setName('Test'); @@ -645,7 +619,7 @@ public function testBaggageWithTracingEnabled(): void $span = $transaction->startChild($spanContext); - $hub->setSpan($span); + SentrySdk::getCurrentHub()->setSpan($span); $baggage = getBaggage(); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 97320a6d1..4f85f4e62 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -173,10 +173,9 @@ public function testAttributesAreAddedToLogMessage(): void 'server_name' => 'web-server-01', ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::setCurrentHub(new Hub($client)); - $hub->configureScope(static function (Scope $scope) { + SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) { $userDataBag = new UserDataBag(); $userDataBag->setId('unique_id'); $userDataBag->setEmail('foo@example.com'); @@ -188,7 +187,7 @@ public function testAttributesAreAddedToLogMessage(): void $spanContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $spanContext->setSpanId(new SpanId('566e3688a61d4bc8')); $span = new Span($spanContext); - $hub->setSpan($span); + SentrySdk::getCurrentHub()->setSpan($span); $aggregator = new LogsAggregator(); @@ -221,10 +220,9 @@ public function testUserAttributesCanBeSetManuallyWithDefaultPiiOff(): void 'send_default_pii' => false, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::setCurrentHub(new Hub($client)); - $hub->configureScope(static function (Scope $scope) { + SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) { $userDataBag = new UserDataBag(); $userDataBag->setId('unique_id'); $userDataBag->setEmail('foo@example.com'); @@ -339,8 +337,8 @@ public function testDoesNotUsePropagationContextSpanIdAsParentSpanIdWhenNoLocalS $propagationContext->setTraceId(new TraceId('771a43a4192642f0b136d5159a501700')); $propagationContext->setSpanId(new SpanId('1234567890abcdef')); - $hub = new Hub($client, new Scope($propagationContext)); - SentrySdk::setCurrentHub($hub); + SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); $aggregator = new LogsAggregator(); $aggregator->add(LogLevel::info(), 'Test message'); diff --git a/tests/SentrySdkExtension.php b/tests/SentrySdkExtension.php index 76857eb51..afe5ccb1a 100644 --- a/tests/SentrySdkExtension.php +++ b/tests/SentrySdkExtension.php @@ -13,15 +13,6 @@ final class SentrySdkExtension implements BeforeTestHookInterface { public function executeBeforeTest(string $test): void { - $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'currentHub'); - if (\PHP_VERSION_ID < 80100) { - $reflectionProperty->setAccessible(true); - } - $reflectionProperty->setValue(null, null); - if (\PHP_VERSION_ID < 80100) { - $reflectionProperty->setAccessible(false); - } - $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'globalScope'); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(true); diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index 7e9573f82..c439159ef 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -26,7 +26,7 @@ public function testInit(): void $hub2 = SentrySdk::getCurrentHub(); $this->assertSame($hub1, $hub2); - $this->assertNotSame(SentrySdk::init(), SentrySdk::init()); + $this->assertSame(SentrySdk::init(), SentrySdk::init()); } public function testGetCurrentHub(): void @@ -41,10 +41,11 @@ public function testGetCurrentHub(): void public function testSetCurrentHub(): void { - $hub = new Hub(new NoOpClient()); + $client = $this->createMock(ClientInterface::class); + $hub = new Hub($client); $this->assertSame($hub, SentrySdk::setCurrentHub($hub)); - $this->assertSame($hub, SentrySdk::getCurrentHub()); + $this->assertSame($client, SentrySdk::getClient()); } public function testGetGlobalScope(): void @@ -211,22 +212,22 @@ public function testNestedStartContextIsNoOp(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); + $globalScope = SentrySdk::getIsolationScope(); SentrySdk::startContext(); - $firstContextHub = SentrySdk::getCurrentHub(); + $firstContextScope = SentrySdk::getIsolationScope(); SentrySdk::startContext(); - $secondContextHub = SentrySdk::getCurrentHub(); + $secondContextScope = SentrySdk::getIsolationScope(); - $this->assertNotSame($globalHub, $firstContextHub); - $this->assertSame($firstContextHub, $secondContextHub); + $this->assertNotSame($globalScope, $firstContextScope); + $this->assertSame($firstContextScope, $secondContextScope); SentrySdk::endContext(); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); SentrySdk::endContext(); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testEndContextFlushesClientTransportWithOptionalTimeout(): void @@ -261,45 +262,45 @@ public function testFlushFlushesClientTransport(): void SentrySdk::flush(); } - public function testWithContextReturnsCallbackResultAndRestoresGlobalHub(): void + public function testWithContextReturnsCallbackResultAndRestoresGlobalIsolationScope(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $callbackHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $callbackScope = null; - $result = SentrySdk::withContext(static function () use (&$callbackHub): string { - $callbackHub = SentrySdk::getCurrentHub(); + $result = SentrySdk::withContext(static function () use (&$callbackScope): string { + $callbackScope = SentrySdk::getIsolationScope(); return 'ok'; }); $this->assertSame('ok', $result); - $this->assertNotNull($callbackHub); - $this->assertNotSame($globalHub, $callbackHub); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotNull($callbackScope); + $this->assertNotSame($globalScope, $callbackScope); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testNestedWithContextReusesOuterContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $outerHub = null; - $innerHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $outerScope = null; + $innerScope = null; $outerContextId = null; $innerContextId = null; - SentrySdk::withContext(function () use (&$outerHub, &$innerHub, &$outerContextId, &$innerContextId, $globalHub): void { - $outerHub = SentrySdk::getCurrentHub(); + SentrySdk::withContext(function () use (&$outerScope, &$innerScope, &$outerContextId, &$innerContextId, $globalScope): void { + $outerScope = SentrySdk::getIsolationScope(); $outerContextId = SentrySdk::getCurrentRuntimeContext()->getId(); SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { $scope->setTag('outer', 'yes'); }); - SentrySdk::withContext(static function () use (&$innerHub, &$innerContextId): void { - $innerHub = SentrySdk::getCurrentHub(); + SentrySdk::withContext(static function () use (&$innerScope, &$innerContextId): void { + $innerScope = SentrySdk::getIsolationScope(); $innerContextId = SentrySdk::getCurrentRuntimeContext()->getId(); }); @@ -309,30 +310,30 @@ public function testNestedWithContextReusesOuterContext(): void $event = $scope->applyToEvent($event); }); - $this->assertNotSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); $this->assertSame('yes', $event->getTags()['outer'] ?? null); $this->assertSame($outerContextId, SentrySdk::getCurrentRuntimeContext()->getId()); }); - $this->assertNotNull($outerHub); - $this->assertNotNull($innerHub); + $this->assertNotNull($outerScope); + $this->assertNotNull($innerScope); $this->assertNotNull($outerContextId); $this->assertNotNull($innerContextId); - $this->assertSame($outerHub, $innerHub); + $this->assertSame($outerScope, $innerScope); $this->assertSame($outerContextId, $innerContextId); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testWithContextEndsContextWhenCallbackThrows(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $callbackHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $callbackScope = null; try { - SentrySdk::withContext(static function () use (&$callbackHub): void { - $callbackHub = SentrySdk::getCurrentHub(); + SentrySdk::withContext(static function () use (&$callbackScope): void { + $callbackScope = SentrySdk::getIsolationScope(); throw new \RuntimeException('boom'); }); @@ -342,9 +343,9 @@ public function testWithContextEndsContextWhenCallbackThrows(): void $this->assertSame('boom', $exception->getMessage()); } - $this->assertNotNull($callbackHub); - $this->assertNotSame($globalHub, $callbackHub); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotNull($callbackScope); + $this->assertNotSame($globalScope, $callbackScope); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } private function getCurrentScopeTraceparent(): string diff --git a/tests/State/HubAdapterTest.php b/tests/State/HubAdapterTest.php index b07c0de26..149ba203f 100644 --- a/tests/State/HubAdapterTest.php +++ b/tests/State/HubAdapterTest.php @@ -4,7 +4,6 @@ namespace Sentry\Tests\State; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; use Sentry\CheckInStatus; @@ -15,16 +14,12 @@ use Sentry\Integration\IntegrationInterface; use Sentry\MonitorConfig; use Sentry\MonitorSchedule; -use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Hub; use Sentry\State\HubAdapter; -use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\Span; -use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; use Sentry\Util\SentryUid; @@ -63,94 +58,55 @@ public function testGetClient(): void { $client = $this->createMock(ClientInterface::class); - SentrySdk::getCurrentHub()->bindClient($client); + HubAdapter::getInstance()->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); + $event = Event::createEvent(); - $this->assertSame($scope, HubAdapter::getInstance()->pushScope()); - } + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('captureEvent') + ->willReturn($event->getId()); - public function testPopScope(): void - { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('popScope') - ->willReturn(true); + SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::setCurrentHub($hub); + HubAdapter::getInstance()->captureEvent($event); - $this->assertTrue(HubAdapter::getInstance()->popScope()); + $this->assertSame($event->getId(), HubAdapter::getInstance()->getLastEventId()); } 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); + $baseScope = SentrySdk::getIsolationScope(); - SentrySdk::setCurrentHub($hub); + $returnValue = HubAdapter::getInstance()->withScope(static function (Scope $scope): string { + $scope->setTag('nested', 'yes'); - $returnValue = HubAdapter::getInstance()->withScope($callback); + return 'foobarbaz'; + }); $this->assertSame('foobarbaz', $returnValue); - } - - public function testConfigureScope(): void - { - $callback = static function () {}; + $this->assertSame($baseScope, SentrySdk::getIsolationScope()); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('configureScope') - ->with($callback); - - SentrySdk::setCurrentHub($hub); - HubAdapter::getInstance()->configureScope($callback); + $event = $baseScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($event); + $this->assertArrayNotHasKey('nested', $event->getTags()); } - public function testBindClient(): void + public function testConfigureScope(): void { - $client = $this->createMock(ClientInterface::class); + HubAdapter::getInstance()->configureScope(static function (Scope $scope): void { + $scope->setTag('foo', 'bar'); + }); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('bindClient') - ->with($client); + $event = SentrySdk::getIsolationScope()->applyToEvent(Event::createEvent()); - SentrySdk::setCurrentHub($hub); - HubAdapter::getInstance()->bindClient($client); + $this->assertNotNull($event); + $this->assertSame(['foo' => 'bar'], $event->getTags()); } /** @@ -160,33 +116,21 @@ public function testCaptureMessage(array $expectedFunctionCallArgs): void { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) + ->with($expectedFunctionCallArgs[0], $expectedFunctionCallArgs[1], $this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[2] ?? null) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($eventId, HubAdapter::getInstance()->captureMessage(...$expectedFunctionCallArgs)); } public static function captureMessageDataProvider(): \Generator { - yield [ - [ - 'foo', - Severity::debug(), - ], - ]; - - yield [ - [ - 'foo', - Severity::debug(), - new EventHint(), - ], - ]; + yield [['foo', Severity::debug()]]; + yield [['foo', Severity::debug(), new EventHint()]]; } /** @@ -195,33 +139,22 @@ public static function captureMessageDataProvider(): \Generator public function testCaptureException(array $expectedFunctionCallArgs): void { $eventId = EventId::generate(); - $exception = new \Exception(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureException') - ->with(...$expectedFunctionCallArgs) + ->with($expectedFunctionCallArgs[0], $this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[1] ?? null) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($eventId, HubAdapter::getInstance()->captureException(...$expectedFunctionCallArgs)); } public static function captureExceptionDataProvider(): \Generator { - yield [ - [ - new \Exception('foo'), - ], - ]; - - yield [ - [ - new \Exception('foo'), - new EventHint(), - ], - ]; + yield [[new \Exception('foo')]]; + yield [[new \Exception('foo'), new EventHint()]]; } public function testCaptureEvent(): void @@ -229,13 +162,13 @@ public function testCaptureEvent(): void $event = Event::createEvent(); $hint = EventHint::fromArray([]); - $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->isInstanceOf(Scope::class)) ->willReturn($event->getId()); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($event->getId(), HubAdapter::getInstance()->captureEvent($event, $hint)); } @@ -247,48 +180,43 @@ public function testCaptureLastError(array $expectedFunctionCallArgs): void { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) + ->with($this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[0] ?? null) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($eventId, HubAdapter::getInstance()->captureLastError(...$expectedFunctionCallArgs)); } public static function captureLastErrorDataProvider(): \Generator { - yield [ - [], - ]; - - yield [ - [ - new EventHint(), - ], - ]; + yield [[]]; + yield [[new EventHint()]]; } public function testCaptureCheckIn(): void { - $hub = new Hub(new NoOpClient()); + $checkInId = SentryUid::generate(); - $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); + ->willReturn(new Options([ + 'environment' => Event::DEFAULT_ENVIRONMENT, + 'release' => '1.1.8', + ])); + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event) use ($checkInId): bool { + $checkIn = $event->getCheckIn(); - $hub->bindClient($client); - SentrySdk::setCurrentHub($hub); + return $checkIn !== null && $checkIn->getId() === $checkInId; + }), null, $this->isInstanceOf(Scope::class)); - $checkInId = SentryUid::generate(); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($checkInId, HubAdapter::getInstance()->captureCheckIn( 'test-crontab', @@ -308,12 +236,12 @@ 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); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertTrue(HubAdapter::getInstance()->addBreadcrumb($breadcrumb)); } @@ -322,43 +250,37 @@ public function testGetIntegration(): void { $integration = $this->createMock(IntegrationInterface::class); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) ->method('getIntegration') ->with(\get_class($integration)) ->willReturn($integration); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame($integration, HubAdapter::getInstance()->getIntegration(\get_class($integration))); } public function testStartTransaction(): void { - $transactionContext = new TransactionContext(); - $transaction = new Transaction($transactionContext); + $transactionContext = new TransactionContext('test-transaction'); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('startTransaction') - ->with($transactionContext) - ->willReturn($transaction); + SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::setCurrentHub($hub); + $transaction = HubAdapter::getInstance()->startTransaction($transactionContext); - $this->assertSame($transaction, HubAdapter::getInstance()->startTransaction($transactionContext)); + $this->assertSame('test-transaction', $transaction->getName()); } 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); + $transaction = HubAdapter::getInstance()->startTransaction(new TransactionContext()); + SentrySdk::getIsolationScope()->setSpan($transaction); $this->assertSame($transaction, HubAdapter::getInstance()->getTransaction()); } @@ -366,13 +288,7 @@ public function testGetTransaction(): void public function testGetSpan(): void { $span = new Span(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getSpan') - ->willReturn($span); - - SentrySdk::setCurrentHub($hub); + SentrySdk::getIsolationScope()->setSpan($span); $this->assertSame($span, HubAdapter::getInstance()->getSpan()); } @@ -381,14 +297,7 @@ 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)); + $this->assertSame(HubAdapter::getInstance(), HubAdapter::getInstance()->setSpan($span)); + $this->assertSame($span, SentrySdk::getIsolationScope()->getSpan()); } } From ee3f48052a4a71bd1a77fbe240d6b5f7aba1e9e4 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 22 Jun 2026 15:45:43 +0200 Subject: [PATCH 2/3] fix --- tests/Fixtures/runtime/frankenphp/index.php | 19 ++++++------------- tests/Fixtures/runtime/roadrunner-worker.php | 19 ++++++------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/tests/Fixtures/runtime/frankenphp/index.php b/tests/Fixtures/runtime/frankenphp/index.php index bbcceb5bb..f6f615299 100644 --- a/tests/Fixtures/runtime/frankenphp/index.php +++ b/tests/Fixtures/runtime/frankenphp/index.php @@ -6,7 +6,6 @@ use Sentry\SentrySdk; use Sentry\State\Scope; -use function Sentry\configureScope; use function Sentry\getTraceparent; use function Sentry\init; use function Sentry\withContext; @@ -20,9 +19,7 @@ 'default_integrations' => false, ]); -configureScope(static function (Scope $scope): void { - $scope->setTag('baseline', 'yes'); -}); +SentrySdk::getGlobalScope()->setTag('baseline', 'yes'); $handler = static function (): void { $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', \PHP_URL_PATH); @@ -46,18 +43,14 @@ $leakTag = isset($_GET['leak']) ? (string) $_GET['leak'] : null; withContext(static function () use ($requestTag, $leakTag): void { - configureScope(static function (Scope $scope) use ($requestTag, $leakTag): void { - $scope->setTag('request', $requestTag); + SentrySdk::getIsolationScope()->setTag('request', $requestTag); - if ($leakTag !== null) { - $scope->setTag('leak', $leakTag); - } - }); + if ($leakTag !== null) { + SentrySdk::getIsolationScope()->setTag('leak', $leakTag); + } $event = Event::createEvent(); - configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = Scope::mergeScopes(SentrySdk::getGlobalScope(), SentrySdk::getIsolationScope())->applyToEvent($event); $tags = []; diff --git a/tests/Fixtures/runtime/roadrunner-worker.php b/tests/Fixtures/runtime/roadrunner-worker.php index 6a4680da8..a5cc7c648 100644 --- a/tests/Fixtures/runtime/roadrunner-worker.php +++ b/tests/Fixtures/runtime/roadrunner-worker.php @@ -10,7 +10,6 @@ use Spiral\RoadRunner\Http\PSR7Worker; use Spiral\RoadRunner\Worker; -use function Sentry\configureScope; use function Sentry\getTraceparent; use function Sentry\init; use function Sentry\withContext; @@ -37,9 +36,7 @@ 'default_integrations' => false, ]); -configureScope(static function (Scope $scope): void { - $scope->setTag('baseline', 'yes'); -}); +SentrySdk::getGlobalScope()->setTag('baseline', 'yes'); $factory = new Psr17Factory(); $worker = Worker::create(); @@ -102,18 +99,14 @@ function handleRequest($request): Response $leakTag = isset($query['leak']) ? (string) $query['leak'] : null; $payload = withContext(static function () use ($requestTag, $leakTag): string { - configureScope(static function (Scope $scope) use ($requestTag, $leakTag): void { - $scope->setTag('request', $requestTag); + SentrySdk::getIsolationScope()->setTag('request', $requestTag); - if ($leakTag !== null) { - $scope->setTag('leak', $leakTag); - } - }); + if ($leakTag !== null) { + SentrySdk::getIsolationScope()->setTag('leak', $leakTag); + } $event = Event::createEvent(); - configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = Scope::mergeScopes(SentrySdk::getGlobalScope(), SentrySdk::getIsolationScope())->applyToEvent($event); $tags = []; From b9bdf639ab836f5629d85317ebf7467aa98bd738 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Mon, 22 Jun 2026 15:51:19 +0200 Subject: [PATCH 3/3] cs --- src/State/RuntimeContextManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/State/RuntimeContextManager.php b/src/State/RuntimeContextManager.php index 5cd3a723c..4fd2c7e1a 100644 --- a/src/State/RuntimeContextManager.php +++ b/src/State/RuntimeContextManager.php @@ -26,7 +26,7 @@ final class RuntimeContextManager /** * @var RuntimeContext|null */ - private $globalContext = null; + private $globalContext; /** * @var array