From aefb3104a62d9735a5ab3fe6e40be4d27d8a5d33 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 23 Jun 2026 00:11:55 +0200 Subject: [PATCH 01/20] test(otel): fix OTLP discovery setup --- tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php | 6 +++++- tests/Integration/OTLPIntegrationTest.php | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php b/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php index 6477cf190..2144bd1f7 100644 --- a/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php +++ b/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php @@ -4,6 +4,7 @@ namespace Sentry\Tests\Fixtures\OpenTelemetry; +use GuzzleHttp\Psr7\HttpFactory; use Nyholm\Psr7\Factory\Psr17Factory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; @@ -21,7 +22,10 @@ public static function getCandidates(string $type): array } if (is_a(RequestFactoryInterface::class, $type, true) || is_a(StreamFactoryInterface::class, $type, true)) { - return [['class' => Psr17Factory::class, 'condition' => Psr17Factory::class]]; + return [ + ['class' => HttpFactory::class, 'condition' => HttpFactory::class], + ['class' => Psr17Factory::class, 'condition' => Psr17Factory::class], + ]; } return []; diff --git a/tests/Integration/OTLPIntegrationTest.php b/tests/Integration/OTLPIntegrationTest.php index a0f3e5680..27f4807b8 100644 --- a/tests/Integration/OTLPIntegrationTest.php +++ b/tests/Integration/OTLPIntegrationTest.php @@ -280,10 +280,10 @@ private function useCapturingHttpClient(): void if (method_exists(HttpClientDiscovery::class, 'setDiscoverers')) { HttpClientDiscovery::setDiscoverers([new TestClientDiscoverer()]); - } else { - ClassDiscovery::prependStrategy(TestDiscoveryStrategy::class); } + ClassDiscovery::prependStrategy(TestDiscoveryStrategy::class); + StubOtelHttpClient::reset(); } From 0fdad47acd6676f953114bf6016a200118cedb9b Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 23 Jun 2026 00:20:00 +0200 Subject: [PATCH 02/20] test(otel): pin sem-conv dev dependency --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index c545b0bbc..cce2f1ddb 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "nyholm/psr7": "^1.8", "open-telemetry/api": "^1.0", "open-telemetry/exporter-otlp": "^1.0", + "open-telemetry/sem-conv": "^1.27", "open-telemetry/sdk": "^1.0", "phpstan/phpstan": "^1.3", "phpunit/phpunit": "^8.5.52|^9.6.34", From b3d22a349c7086a052d619e7b58101dc0a828c06 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Tue, 23 Jun 2026 00:51:20 +0200 Subject: [PATCH 03/20] ci: remove sem-conv on unsupported PHP --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49052d375..8bcf0d0ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: - name: Remove OpenTelemetry dependencies on unsupported PHP versions if: ${{ matrix.php.version == '7.2' || matrix.php.version == '7.3' || matrix.php.version == '7.4' || matrix.php.version == '8.0' }} - run: composer remove open-telemetry/api open-telemetry/exporter-otlp open-telemetry/sdk --dev --no-interaction --no-update + run: composer remove open-telemetry/api open-telemetry/exporter-otlp open-telemetry/sem-conv open-telemetry/sdk --dev --no-interaction --no-update - name: Set phpunit/phpunit version constraint run: composer require phpunit/phpunit:'${{ matrix.php.phpunit }}' --dev --no-interaction --no-update From 54e8b3e197f8b4eb0c9596982331824fae69b87c Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:46 +0200 Subject: [PATCH 04/20] feat(scopes): add functions to merge two scopes (#2113) --- src/State/Scope.php | 63 +++++++++++ tests/State/ScopeTest.php | 227 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) diff --git a/src/State/Scope.php b/src/State/Scope.php index 58d95f00f..7ab43086c 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -112,6 +112,69 @@ public function __construct(?PropagationContext $propagationContext = null) $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); } + /** + * Merges the process-global scope underneath the current isolation scope. + * + * The returned scope is transient and should be used for one event capture. + * + * @internal + */ + public static function mergeScopes(self $globalScope, self $isolationScope): self + { + $mergedScope = clone $isolationScope; + + $mergedScope->tags = array_merge($globalScope->tags, $isolationScope->tags); + $mergedScope->extra = array_merge($globalScope->extra, $isolationScope->extra); + $mergedScope->contexts = array_merge($globalScope->contexts, $isolationScope->contexts); + + if ($globalScope->user !== null && $isolationScope->user !== null) { + $mergedScope->user = (clone $globalScope->user)->merge($isolationScope->user); + } elseif ($globalScope->user !== null) { + $mergedScope->user = clone $globalScope->user; + } + + $mergedScope->level = $isolationScope->level ?? $globalScope->level; + $mergedScope->fingerprint = array_merge($globalScope->fingerprint, $isolationScope->fingerprint); + $mergedScope->breadcrumbs = \array_slice(array_merge($globalScope->breadcrumbs, $isolationScope->breadcrumbs), -100); + $mergedScope->flags = self::mergeFlags($globalScope->flags, $isolationScope->flags); + $mergedScope->attachments = array_merge($globalScope->attachments, $isolationScope->attachments); + $mergedScope->eventProcessors = array_merge($globalScope->eventProcessors, $isolationScope->eventProcessors); + + return $mergedScope; + } + + /** + * @param array> $globalFlags + * @param array> $isolationFlags + * + * @return array> + */ + private static function mergeFlags(array $globalFlags, array $isolationFlags): array + { + $flagsByKey = []; + + foreach (array_merge($globalFlags, $isolationFlags) as $flag) { + $flagKey = key($flag); + + if ($flagKey === null) { + continue; + } + + unset($flagsByKey[$flagKey]); + $flagsByKey[$flagKey] = (bool) current($flag); + } + + $flagsByKey = \array_slice($flagsByKey, -self::MAX_FLAGS, self::MAX_FLAGS, true); + + $flags = []; + + foreach ($flagsByKey as $flagKey => $flagResult) { + $flags[] = [$flagKey => $flagResult]; + } + + return $flags; + } + /** * Sets a new tag in the tags context. * diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index a9a9d3e8a..1dd6174a4 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -531,6 +531,233 @@ public function testApplyToEvent(): void $this->assertSame('566e3688a61d4bc888951642d6f14a19', $dynamicSamplingContext->get('trace_id')); } + public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void + { + $globalBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'global'); + $isolationBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation'); + $globalAttachment = Attachment::fromBytes('global.txt', 'global'); + $isolationAttachment = Attachment::fromBytes('isolation.txt', 'isolation'); + + $globalUser = UserDataBag::createFromUserIdentifier('global-user'); + $globalUser->setMetadata('shared', 'global'); + $globalUser->setMetadata('global', true); + + $globalScope = new Scope(); + $globalScope->setTag('shared', 'global'); + $globalScope->setTag('global', 'tag'); + $globalScope->setExtra('shared', 'global'); + $globalScope->setExtra('global', true); + $globalScope->setContext('shared_context', ['value' => 'global']); + $globalScope->setContext('global_context', ['value' => 'global']); + $globalScope->setUser($globalUser); + $globalScope->setLevel(Severity::error()); + $globalScope->setFingerprint(['global-fingerprint']); + $globalScope->addBreadcrumb($globalBreadcrumb); + $globalScope->addFeatureFlag('shared-flag', false); + $globalScope->addFeatureFlag('global-flag', true); + $globalScope->addAttachment($globalAttachment); + + $isolationUser = UserDataBag::createFromUserIdentifier('isolation-user'); + $isolationUser->setMetadata('shared', 'isolation'); + $isolationUser->setMetadata('isolation', true); + + $isolationScope = new Scope(); + $isolationScope->setTag('shared', 'isolation'); + $isolationScope->setTag('isolation', 'tag'); + $isolationScope->setExtra('shared', 'isolation'); + $isolationScope->setExtra('isolation', true); + $isolationScope->setContext('shared_context', ['value' => 'isolation']); + $isolationScope->setContext('isolation_context', ['value' => 'isolation']); + $isolationScope->setUser($isolationUser); + $isolationScope->setLevel(Severity::warning()); + $isolationScope->setFingerprint(['isolation-fingerprint']); + $isolationScope->addBreadcrumb($isolationBreadcrumb); + $isolationScope->addFeatureFlag('shared-flag', true); + $isolationScope->addFeatureFlag('isolation-flag', false); + $isolationScope->addAttachment($isolationAttachment); + + $eventUser = UserDataBag::createFromUserIdentifier('event-user'); + $eventUser->setMetadata('shared', 'event'); + $eventUser->setMetadata('event', true); + + $event = Event::createEvent(); + $event->setTag('shared', 'event'); + $event->setTag('event', 'tag'); + $event->setExtra(['shared' => 'event', 'event' => true]); + $event->setContext('shared_context', ['value' => 'event']); + $event->setUser($eventUser); + $event->setFingerprint(['event-fingerprint']); + + $event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent($event); + + $this->assertNotNull($event); + $this->assertTrue($event->getLevel()->isEqualTo(Severity::warning())); + $this->assertSame(['event-fingerprint', 'global-fingerprint', 'isolation-fingerprint'], $event->getFingerprint()); + $this->assertSame([ + 'shared' => 'event', + 'global' => 'tag', + 'isolation' => 'tag', + 'event' => 'tag', + ], $event->getTags()); + $this->assertSame([ + 'shared' => 'event', + 'global' => true, + 'isolation' => true, + 'event' => true, + ], $event->getExtra()); + $this->assertSame(['value' => 'event'], $event->getContexts()['shared_context']); + $this->assertSame(['value' => 'global'], $event->getContexts()['global_context']); + $this->assertSame(['value' => 'isolation'], $event->getContexts()['isolation_context']); + $this->assertSame([ + 'values' => [ + [ + 'flag' => 'global-flag', + 'result' => true, + ], + [ + 'flag' => 'shared-flag', + 'result' => true, + ], + [ + 'flag' => 'isolation-flag', + 'result' => false, + ], + ], + ], $event->getContexts()['flags']); + $this->assertSame([$globalBreadcrumb, $isolationBreadcrumb], $event->getBreadcrumbs()); + $this->assertSame([$globalAttachment, $isolationAttachment], $event->getAttachments()); + + $user = $event->getUser(); + $this->assertNotNull($user); + $this->assertSame('event-user', $user->getId()); + $this->assertSame([ + 'shared' => 'event', + 'global' => true, + 'isolation' => true, + 'event' => true, + ], $user->getMetadata()); + } + + public function testMergeScopesUsesGlobalLevelWhenIsolationLevelIsUnset(): void + { + $globalScope = new Scope(); + $globalScope->setLevel(Severity::error()); + + $event = Scope::mergeScopes($globalScope, new Scope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertTrue($event->getLevel()->isEqualTo(Severity::error())); + } + + public function testMergeScopesCapsBreadcrumbsAndFlags(): void + { + $globalScope = new Scope(); + $globalBreadcrumbs = []; + + foreach (range(1, 100) as $i) { + $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, "global{$i}"); + $globalBreadcrumbs[] = $breadcrumb; + $globalScope->addBreadcrumb($breadcrumb); + $globalScope->addFeatureFlag("feature{$i}", true); + } + + $isolationBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation'); + $isolationScope = new Scope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb); + $isolationScope->addFeatureFlag('feature50', false); + $isolationScope->addFeatureFlag('feature101', true); + + $event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertCount(100, $event->getBreadcrumbs()); + $this->assertSame($globalBreadcrumbs[1], $event->getBreadcrumbs()[0]); + $this->assertSame($isolationBreadcrumb, $event->getBreadcrumbs()[99]); + + $flags = $event->getContexts()['flags']['values']; + $this->assertCount(Scope::MAX_FLAGS, $flags); + $this->assertSame([ + 'flag' => 'feature2', + 'result' => true, + ], $flags[0]); + $this->assertSame([ + 'flag' => 'feature50', + 'result' => false, + ], $flags[98]); + $this->assertSame([ + 'flag' => 'feature101', + 'result' => true, + ], $flags[99]); + $this->assertFalse(\in_array('feature1', array_column($flags, 'flag'), true)); + } + + public function testMergeScopesKeepsTraceStateFromIsolationScope(): void + { + $globalPropagationContext = PropagationContext::fromDefaults(); + $globalPropagationContext->setTraceId(new TraceId('11111111111111111111111111111111')); + $globalPropagationContext->setSpanId(new SpanId('1111111111111111')); + + $globalSpan = new Span(); + $globalSpan->setTraceId(new TraceId('22222222222222222222222222222222')); + $globalSpan->setSpanId(new SpanId('2222222222222222')); + + $globalScope = new Scope($globalPropagationContext); + $globalScope->setSpan($globalSpan); + + $isolationPropagationContext = PropagationContext::fromDefaults(); + $isolationPropagationContext->setTraceId(new TraceId('33333333333333333333333333333333')); + $isolationPropagationContext->setSpanId(new SpanId('3333333333333333')); + + $isolationScope = new Scope($isolationPropagationContext); + + $mergedScope = Scope::mergeScopes($globalScope, $isolationScope); + + $this->assertNull($mergedScope->getSpan()); + $this->assertNotSame($isolationScope->getPropagationContext(), $mergedScope->getPropagationContext()); + $this->assertSame([ + 'trace_id' => '33333333333333333333333333333333', + 'span_id' => '3333333333333333', + ], $mergedScope->getTraceContext()); + + $event = $mergedScope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([ + 'trace_id' => '33333333333333333333333333333333', + 'span_id' => '3333333333333333', + ], $event->getContexts()['trace']); + } + + public function testMergeScopesKeepsProcessorOrder(): void + { + $calls = []; + + Scope::addGlobalEventProcessor(static function (Event $event) use (&$calls): ?Event { + $calls[] = 'static'; + + return $event; + }); + + $globalScope = new Scope(); + $globalScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { + $calls[] = 'global'; + + return $event; + }); + + $isolationScope = new Scope(); + $isolationScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { + $calls[] = 'isolation'; + + return $event; + }); + + $event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['static', 'global', 'isolation'], $calls); + } + /** * @dataProvider eventWithLogCountProvider */ From 5e5df5515981b174c7fb5de5d8ad1755712e6cfe Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:46 +0200 Subject: [PATCH 05/20] feat(scopes): add clients to scopes (#2114) --- src/SentrySdk.php | 39 +++++++++++++++-- src/State/RuntimeContext.php | 18 +++++++- src/State/Scope.php | 55 +++++++++++++++++++++++- src/functions.php | 2 +- tests/FunctionsTest.php | 17 ++++++++ tests/SentrySdkExtension.php | 9 ++++ tests/SentrySdkTest.php | 81 ++++++++++++++++++++++++++++++++++++ tests/State/ScopeTest.php | 61 +++++++++++++++++++++++++++ 8 files changed, 275 insertions(+), 7 deletions(-) diff --git a/src/SentrySdk.php b/src/SentrySdk.php index 9adce8c28..e28697b15 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -10,6 +10,7 @@ use Sentry\State\HubInterface; use Sentry\State\RuntimeContext; use Sentry\State\RuntimeContextManager; +use Sentry\State\Scope; /** * This class is the main entry point for all the most common SDK features. @@ -23,6 +24,11 @@ final class SentrySdk */ private static $currentHub; + /** + * @var Scope|null The process-global scope + */ + private static $globalScope; + /** * @var RuntimeContextManager|null */ @@ -41,10 +47,12 @@ private function __construct() */ public static function init(?ClientInterface $client = null): HubInterface { - if ($client === null) { - $client = new NoOpClient(); + $hubClient = $client ?? new NoOpClient(); + + if ($client !== null) { + self::getGlobalScope()->setClient($client); } - self::$currentHub = new Hub($client); + self::$currentHub = new Hub($hubClient); self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub); return self::getCurrentHub(); @@ -79,6 +87,31 @@ public static function setCurrentHub(HubInterface $hub): HubInterface return $hub; } + public static function getGlobalScope(): Scope + { + if (self::$globalScope === null) { + self::$globalScope = new Scope(); + } + + return self::$globalScope; + } + + public static function getIsolationScope(): Scope + { + return self::getCurrentRuntimeContext()->getIsolationScope(); + } + + public static function getClient(): ClientInterface + { + $client = self::getIsolationScope()->getClient(); + + if (!$client instanceof NoOpClient) { + return $client; + } + + return self::getGlobalScope()->getClient(); + } + public static function startContext(): void { self::getRuntimeContextManager()->startContext(); diff --git a/src/State/RuntimeContext.php b/src/State/RuntimeContext.php index 6910cae60..6268ddc2b 100644 --- a/src/State/RuntimeContext.php +++ b/src/State/RuntimeContext.php @@ -27,6 +27,11 @@ final class RuntimeContext */ private $hub; + /** + * @var Scope + */ + private $isolationScope; + /** * @var LogsAggregator */ @@ -37,10 +42,11 @@ final class RuntimeContext */ private $metricsAggregator; - public function __construct(string $id, HubInterface $hub) + public function __construct(string $id, HubInterface $hub, ?Scope $isolationScope = null) { $this->id = $id; $this->hub = $hub; + $this->isolationScope = $isolationScope ?? new Scope(); $this->logsAggregator = new LogsAggregator(); $this->metricsAggregator = new MetricsAggregator(); } @@ -60,6 +66,16 @@ public function setHub(HubInterface $hub): void $this->hub = $hub; } + public function getIsolationScope(): Scope + { + return $this->isolationScope; + } + + public function setIsolationScope(Scope $isolationScope): void + { + $this->isolationScope = $isolationScope; + } + public function getLogsAggregator(): LogsAggregator { return $this->logsAggregator; diff --git a/src/State/Scope.php b/src/State/Scope.php index 7ab43086c..0a53b3686 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -6,9 +6,12 @@ use Sentry\Attachment\Attachment; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; +use Sentry\EventId; use Sentry\EventType; +use Sentry\NoOpClient; use Sentry\Options; use Sentry\Severity; use Sentry\Tracing\DynamicSamplingContext; @@ -36,6 +39,16 @@ class Scope */ private $propagationContext; + /** + * @var ClientInterface The client bound to this scope + */ + private $client; + + /** + * @var EventId|null The ID of the last captured event + */ + private $lastEventId; + /** * @var Breadcrumb[] The list of breadcrumbs recorded in this scope */ @@ -110,6 +123,7 @@ class Scope public function __construct(?PropagationContext $propagationContext = null) { $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); + $this->client = new NoOpClient(); } /** @@ -143,6 +157,42 @@ public static function mergeScopes(self $globalScope, self $isolationScope): sel return $mergedScope; } + /** + * Returns the client bound to this scope. + */ + public function getClient(): ClientInterface + { + return $this->client; + } + + /** + * Sets the client bound to this scope. + * + * @return $this + */ + public function setClient(ClientInterface $client): self + { + $this->client = $client; + + return $this; + } + + /** + * Returns the ID of the last captured event. + */ + public function getLastEventId(): ?EventId + { + return $this->lastEventId; + } + + /** + * @internal + */ + public function setLastEventId(?EventId $lastEventId): void + { + $this->lastEventId = $lastEventId; + } + /** * @param array> $globalFlags * @param array> $isolationFlags @@ -161,7 +211,7 @@ private static function mergeFlags(array $globalFlags, array $isolationFlags): a } unset($flagsByKey[$flagKey]); - $flagsByKey[$flagKey] = (bool) current($flag); + $flagsByKey[$flagKey] = current($flag); } $flagsByKey = \array_slice($flagsByKey, -self::MAX_FLAGS, self::MAX_FLAGS, true); @@ -483,7 +533,8 @@ public static function getExternalPropagationContext(): ?array } /** - * Clears the scope and resets any data it contains. + * Clears event payload data from the scope. The client binding and last + * event ID are preserved. * * @return $this */ diff --git a/src/functions.php b/src/functions.php index 0935d739f..1549bf9fe 100644 --- a/src/functions.php +++ b/src/functions.php @@ -74,7 +74,7 @@ function init(array $options = []): void { $client = ClientBuilder::create($options)->getClient(); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); } /** diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 4611e6054..ba349bde9 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -60,6 +60,23 @@ public function testInit(): void init(['default_integrations' => false]); $this->assertNotNull(SentrySdk::getCurrentHub()->getClient()); + $this->assertSame(SentrySdk::getCurrentHub()->getClient(), SentrySdk::getClient()); + } + + public function testInitPreservesGlobalScope(): void + { + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setTag('baseline', 'yes'); + + init(['default_integrations' => false]); + + $this->assertSame($globalScope, SentrySdk::getGlobalScope()); + $this->assertSame(SentrySdk::getCurrentHub()->getClient(), $globalScope->getClient()); + + $event = $globalScope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['baseline' => 'yes'], $event->getTags()); } /** diff --git a/tests/SentrySdkExtension.php b/tests/SentrySdkExtension.php index b50bc2ed8..76857eb51 100644 --- a/tests/SentrySdkExtension.php +++ b/tests/SentrySdkExtension.php @@ -22,6 +22,15 @@ public function executeBeforeTest(string $test): void $reflectionProperty->setAccessible(false); } + $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'globalScope'); + 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, 'runtimeContextManager'); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(true); diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index 3ce3d6f12..7e9573f82 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -47,6 +47,87 @@ public function testSetCurrentHub(): void $this->assertSame($hub, SentrySdk::getCurrentHub()); } + public function testGetGlobalScope(): void + { + $scope = SentrySdk::getGlobalScope(); + + $this->assertSame($scope, SentrySdk::getGlobalScope()); + } + + public function testGetIsolationScope(): void + { + $scope = SentrySdk::getIsolationScope(); + + $this->assertSame($scope, SentrySdk::getIsolationScope()); + } + + public function testGetClientReturnsCachedNoOpFallbackBeforeInit(): void + { + $client = SentrySdk::getClient(); + + $this->assertInstanceOf(NoOpClient::class, $client); + $this->assertSame($client, SentrySdk::getClient()); + } + + public function testGetClientReturnsGlobalScopeClient(): void + { + $client = $this->createMock(ClientInterface::class); + + SentrySdk::getGlobalScope()->setClient($client); + + $this->assertSame($client, SentrySdk::getClient()); + } + + public function testGetClientReturnsIsolationScopeClientBeforeGlobalScopeClient(): void + { + $globalClient = $this->createMock(ClientInterface::class); + $isolationClient = $this->createMock(ClientInterface::class); + + SentrySdk::getGlobalScope()->setClient($globalClient); + SentrySdk::getIsolationScope()->setClient($isolationClient); + + $this->assertSame($isolationClient, SentrySdk::getClient()); + } + + public function testStartContextUsesSeparateIsolationScope(): void + { + $globalIsolationScope = SentrySdk::getIsolationScope(); + + SentrySdk::startContext(); + + $contextIsolationScope = SentrySdk::getIsolationScope(); + + $this->assertNotSame($globalIsolationScope, $contextIsolationScope); + + SentrySdk::endContext(); + + $this->assertSame($globalIsolationScope, SentrySdk::getIsolationScope()); + } + + public function testInitWithClientSetsGlobalScopeClient(): void + { + $client = $this->createMock(ClientInterface::class); + + SentrySdk::init($client); + + $this->assertSame($client, SentrySdk::getClient()); + } + + public function testInitDoesNotResetGlobalScope(): void + { + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setTag('baseline', 'yes'); + + SentrySdk::init(); + + $this->assertSame($globalScope, SentrySdk::getGlobalScope()); + + $event = $globalScope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['baseline' => 'yes'], $event->getTags()); + } + public function testStartAndEndContextIsolateScopeData(): void { SentrySdk::init(); diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 1dd6174a4..be9362868 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -7,8 +7,11 @@ use PHPUnit\Framework\TestCase; use Sentry\Attachment\Attachment; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; +use Sentry\EventId; +use Sentry\NoOpClient; use Sentry\Options; use Sentry\Severity; use Sentry\State\Scope; @@ -24,6 +27,46 @@ final class ScopeTest extends TestCase { + public function testGetAndSetClient(): void + { + $scope = new Scope(); + + $this->assertInstanceOf(NoOpClient::class, $scope->getClient()); + + $client = $this->createMock(ClientInterface::class); + + $this->assertSame($scope, $scope->setClient($client)); + $this->assertSame($client, $scope->getClient()); + } + + public function testClonedScopeKeepsClientShared(): void + { + $client = $this->createMock(ClientInterface::class); + + $scope = new Scope(); + $scope->setClient($client); + + $clonedScope = clone $scope; + + $this->assertSame($client, $clonedScope->getClient()); + } + + public function testGetAndSetLastEventId(): void + { + $scope = new Scope(); + + $this->assertNull($scope->getLastEventId()); + + $eventId = EventId::generate(); + $scope->setLastEventId($eventId); + + $this->assertSame($eventId, $scope->getLastEventId()); + + $scope->setLastEventId(null); + + $this->assertNull($scope->getLastEventId()); + } + public function testSetTag(): void { $scope = new Scope(); @@ -443,7 +486,11 @@ public function testClear(): void { $scope = new Scope(); $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $client = $this->createMock(ClientInterface::class); + $eventId = EventId::generate(); + $scope->setClient($client); + $scope->setLastEventId($eventId); $scope->setLevel(Severity::info()); $scope->addBreadcrumb($breadcrumb); $scope->setFingerprint(['foo']); @@ -463,6 +510,8 @@ public function testClear(): void $this->assertEmpty($event->getTags()); $this->assertEmpty($event->getUser()); $this->assertArrayNotHasKey('flags', $event->getContexts()); + $this->assertSame($client, $scope->getClient()); + $this->assertSame($eventId, $scope->getLastEventId()); } public function testApplyToEvent(): void @@ -649,6 +698,18 @@ public function testMergeScopesUsesGlobalLevelWhenIsolationLevelIsUnset(): void $this->assertTrue($event->getLevel()->isEqualTo(Severity::error())); } + public function testMergeScopesCarriesIsolationClient(): void + { + $globalScope = new Scope(); + $globalScope->setClient($this->createMock(ClientInterface::class)); + + $isolationClient = $this->createMock(ClientInterface::class); + $isolationScope = new Scope(); + $isolationScope->setClient($isolationClient); + + $this->assertSame($isolationClient, Scope::mergeScopes($globalScope, $isolationScope)->getClient()); + } + public function testMergeScopesCapsBreadcrumbsAndFlags(): void { $globalScope = new Scope(); From b4fe051e6de5da983bfe7ee7f19d6c4c4dd4c21a Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:47 +0200 Subject: [PATCH 06/20] feat(scopes): extract transaction sampling logic into TransactionSampler (#2118) --- src/State/Hub.php | 144 +--------- src/Tracing/TransactionSampler.php | 177 ++++++++++++ tests/Tracing/TransactionSamplerTest.php | 330 +++++++++++++++++++++++ 3 files changed, 509 insertions(+), 142 deletions(-) create mode 100644 src/Tracing/TransactionSampler.php create mode 100644 tests/Tracing/TransactionSamplerTest.php diff --git a/src/State/Hub.php b/src/State/Hub.php index e9132531e..2f4b947ed 100644 --- a/src/State/Hub.php +++ b/src/State/Hub.php @@ -16,10 +16,10 @@ use Sentry\MonitorConfig; use Sentry\NoOpClient; use Sentry\Severity; -use Sentry\Tracing\SamplingContext; use Sentry\Tracing\Span; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSampler; /** * This class is a basic implementation of the {@see HubInterface} interface. @@ -237,103 +237,8 @@ public function getIntegration(string $className): ?IntegrationInterface 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(); - - $profilesSampleSource = 'config:profiles_sample_rate'; - $profilesSampler = $options->getProfilesSampler(); - - if ($profilesSampler !== null) { - $profilesSampleRate = $profilesSampler($samplingContext); - $profilesSampleSource = 'config:profiles_sampler'; - } else { - $profilesSampleRate = $options->getProfilesSampleRate(); - } - - if ($profilesSampleRate === null) { - $logger->info(\sprintf('Transaction [%s] is not profiling because neither `profiles_sample_rate` nor `profiles_sampler` option is set.', (string) $transaction->getTraceId())); - } elseif (!$this->isValidSampleRate($profilesSampleRate)) { - $logger->warning(\sprintf('Transaction [%s] is not profiling because profile sample rate (decided by %s) is invalid.', (string) $transaction->getTraceId(), $profilesSampleSource)); - } 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; + return (new TransactionSampler($this->getClient()->getOptions()))->startTransaction($transaction, $context, $customSamplingContext); } /** @@ -377,49 +282,4 @@ 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/Tracing/TransactionSampler.php b/src/Tracing/TransactionSampler.php new file mode 100644 index 000000000..c3bd46b9f --- /dev/null +++ b/src/Tracing/TransactionSampler.php @@ -0,0 +1,177 @@ +options = $options; + } + + /** + * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} + */ + public function startTransaction(Transaction $transaction, TransactionContext $context, array $customSamplingContext = []): Transaction + { + $logger = $this->options->getLoggerOrNullLogger(); + + if (!$this->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() ?? 0.0; + + if ($transaction->getSampled() === null) { + $tracesSampler = $this->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(), + $this->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(); + + $profilesSampleSource = 'config:profiles_sample_rate'; + $profilesSampler = $this->options->getProfilesSampler(); + + if ($profilesSampler !== null) { + $profilesSampleRate = $profilesSampler($samplingContext); + $profilesSampleSource = 'config:profiles_sampler'; + } else { + $profilesSampleRate = $this->options->getProfilesSampleRate(); + } + + if ($profilesSampleRate === null) { + $logger->info(\sprintf('Transaction [%s] is not profiling because neither `profiles_sample_rate` nor `profiles_sampler` option is set.', (string) $transaction->getTraceId())); + } elseif (!$this->isValidSampleRate($profilesSampleRate)) { + $logger->warning(\sprintf('Transaction [%s] is not profiling because profile sample rate (decided by %s) is invalid.', (string) $transaction->getTraceId(), $profilesSampleSource)); + } elseif ($this->sampleRate($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; + } + + 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 sampleRate($sampleRate): bool + { + if (!\is_float($sampleRate) && !\is_int($sampleRate)) { + return false; + } + + if ($sampleRate === 0.0) { + return false; + } + + if ($sampleRate === 1.0) { + return true; + } + + return mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() < (float) $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/tests/Tracing/TransactionSamplerTest.php b/tests/Tracing/TransactionSamplerTest.php new file mode 100644 index 000000000..1fe3aab72 --- /dev/null +++ b/tests/Tracing/TransactionSamplerTest.php @@ -0,0 +1,330 @@ +sampleTransaction($options, $transactionContext); + + $this->assertSame($expectedSampled, $transaction->getSampled()); + } + + public function testIgnoresBaggageSampleRateWithoutSentryTrace(): void + { + $transactionContext = TransactionContext::fromHeaders('', 'sentry-sample_rate=1'); + $transaction = $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 0.0, + ]), $transactionContext); + + $this->assertFalse($transaction->getSampled()); + } + + public static function sampleTransactionDataProvider(): 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 'Invalid incoming sample_rand is ignored' => [ + new Options([ + 'traces_sample_rate' => 1.0, + ]), + TransactionContext::fromHeaders( + '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8', + 'sentry-sample_rand=2.0' + ), + true, + ]; + + 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 testDoesNothingIfTracingIsNotEnabled(): void + { + $transaction = $this->sampleTransaction(new Options(), new TransactionContext()); + + $this->assertFalse($transaction->getSampled()); + } + + public function testPassesCustomSamplingContextToTracesSampler(): void + { + $customSamplingContext = ['a' => 'b']; + $samplerInvoked = false; + + $this->sampleTransaction(new Options([ + 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext, &$samplerInvoked): float { + $this->assertSame($customSamplingContext, $samplingContext->getAdditionalContext()); + $samplerInvoked = true; + + return 1.0; + }, + ]), new TransactionContext(), $customSamplingContext); + + $this->assertTrue($samplerInvoked); + } + + public function testStartsProfilerWithProfilesSampler(): void + { + $transaction = $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => static function (): float { + return 1.0; + }, + ]), new TransactionContext()); + + $this->assertTrue($transaction->getSampled()); + $this->assertNotNull($transaction->getProfiler()); + } + + public function testDoesNotStartProfilerWhenProfilesSamplerReturnsZero(): void + { + $transaction = $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => static function (): float { + return 0.0; + }, + ]), new TransactionContext()); + + $this->assertTrue($transaction->getSampled()); + $this->assertNull($transaction->getProfiler()); + } + + public function testPrefersProfilesSamplerOverProfilesSampleRate(): void + { + $transaction = $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sample_rate' => 1.0, + 'profiles_sampler' => static function (): float { + return 0.0; + }, + ]), new TransactionContext()); + + $this->assertTrue($transaction->getSampled()); + $this->assertNull($transaction->getProfiler()); + } + + public function testPassesCustomSamplingContextToProfilesSampler(): void + { + $customSamplingContext = ['a' => 'b']; + $samplerInvoked = false; + + $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext, &$samplerInvoked): float { + $this->assertSame($customSamplingContext, $samplingContext->getAdditionalContext()); + $samplerInvoked = true; + + return 0.0; + }, + ]), new TransactionContext(), $customSamplingContext); + + $this->assertTrue($samplerInvoked); + } + + public function testDoesNotStartProfilerWhenProfilesSamplerReturnsInvalidValue(): void + { + $transaction = $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => static function (): string { + return 'foo'; + }, + ]), new TransactionContext()); + + $this->assertTrue($transaction->getSampled()); + $this->assertNull($transaction->getProfiler()); + } + + public function testDoesNotCallProfilesSamplerWhenTransactionIsNotSampled(): void + { + $profilesSamplerInvoked = false; + + $transaction = $this->sampleTransaction(new Options([ + 'traces_sample_rate' => 0.0, + 'profiles_sampler' => static function () use (&$profilesSamplerInvoked): float { + $profilesSamplerInvoked = true; + + return 1.0; + }, + ]), new TransactionContext()); + + $this->assertFalse($transaction->getSampled()); + $this->assertFalse($profilesSamplerInvoked); + $this->assertNull($transaction->getProfiler()); + } + + public function testUpdatesTheDscSampleRate(): void + { + $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 = $this->sampleTransaction(new Options([ + 'traces_sampler' => static function (SamplingContext $samplingContext): float { + return 1.0; + }, + ]), $transactionContext); + + $this->assertSame('1', $transaction->getMetadata()->getDynamicSamplingContext()->get('sample_rate')); + } + + /** + * @param array $customSamplingContext + */ + private function sampleTransaction(Options $options, TransactionContext $transactionContext, array $customSamplingContext = []): Transaction + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions')->willReturn($options); + + $transaction = new Transaction($transactionContext, new Hub($client)); + + return (new TransactionSampler($options))->startTransaction($transaction, $transactionContext, $customSamplingContext); + } +} From f477e1134743ce435d52d51895c6d77f592a7ef6 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:47 +0200 Subject: [PATCH 07/20] feat(scopes): use TransactionSampler (#2120) --- src/State/Hub.php | 4 +-- src/Tracing/Transaction.php | 5 +-- src/Tracing/TransactionSampler.php | 39 +++++++++++------------- tests/State/HubTest.php | 2 +- tests/Tracing/TransactionSamplerTest.php | 9 +----- 5 files changed, 23 insertions(+), 36 deletions(-) diff --git a/src/State/Hub.php b/src/State/Hub.php index 2f4b947ed..82597e79c 100644 --- a/src/State/Hub.php +++ b/src/State/Hub.php @@ -236,9 +236,7 @@ public function getIntegration(string $className): ?IntegrationInterface */ public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - $transaction = new Transaction($context, $this); - - return (new TransactionSampler($this->getClient()->getOptions()))->startTransaction($transaction, $context, $customSamplingContext); + return TransactionSampler::startTransaction($this->getClient()->getOptions(), $context, $customSamplingContext); } /** diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index e2baa09b9..34134b855 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -6,6 +6,7 @@ use Sentry\Event; use Sentry\EventId; +use Sentry\Options; use Sentry\Profiling\Profiler; use Sentry\SentrySdk; use Sentry\State\HubInterface; @@ -119,10 +120,10 @@ public function initSpanRecorder(int $maxSpans = 1000): self return $this; } - public function initProfiler(): Profiler + public function initProfiler(?Options $options = null): Profiler { if ($this->profiler === null) { - $this->profiler = new Profiler($this->hub->getClient()->getOptions()); + $this->profiler = new Profiler($options ?? $this->hub->getClient()->getOptions()); } return $this->profiler; diff --git a/src/Tracing/TransactionSampler.php b/src/Tracing/TransactionSampler.php index c3bd46b9f..ef6a284ec 100644 --- a/src/Tracing/TransactionSampler.php +++ b/src/Tracing/TransactionSampler.php @@ -13,24 +13,19 @@ */ final class TransactionSampler { - /** - * @var Options - */ - private $options; - - public function __construct(Options $options) + private function __construct() { - $this->options = $options; } /** * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} */ - public function startTransaction(Transaction $transaction, TransactionContext $context, array $customSamplingContext = []): Transaction + public static function startTransaction(Options $options, TransactionContext $context, array $customSamplingContext = []): Transaction { - $logger = $this->options->getLoggerOrNullLogger(); + $transaction = new Transaction($context); + $logger = $options->getLoggerOrNullLogger(); - if (!$this->options->isTracingEnabled()) { + if (!$options->isTracingEnabled()) { $transaction->setSampled(false); $logger->warning(\sprintf('Transaction [%s] was started but tracing is not enabled.', (string) $transaction->getTraceId()), ['context' => $context]); @@ -45,7 +40,7 @@ public function startTransaction(Transaction $transaction, TransactionContext $c $sampleRand = $context->getMetadata()->getSampleRand() ?? 0.0; if ($transaction->getSampled() === null) { - $tracesSampler = $this->options->getTracesSampler(); + $tracesSampler = $options->getTracesSampler(); if ($tracesSampler !== null) { $sampleRate = $tracesSampler($samplingContext); @@ -56,15 +51,15 @@ public function startTransaction(Transaction $transaction, TransactionContext $c $sampleRate = $parentSampleRate; $sampleSource = 'parent:sample_rate'; } else { - $sampleRate = $this->getSampleRate( + $sampleRate = self::getSampleRate( $samplingContext->getParentSampled(), - $this->options->getTracesSampleRate() ?? 0 + $options->getTracesSampleRate() ?? 0 ); $sampleSource = $samplingContext->getParentSampled() !== null ? 'parent:sampling_decision' : 'config:traces_sample_rate'; } } - if (!$this->isValidSampleRate($sampleRate)) { + 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]); @@ -102,23 +97,23 @@ public function startTransaction(Transaction $transaction, TransactionContext $c $transaction->initSpanRecorder(); $profilesSampleSource = 'config:profiles_sample_rate'; - $profilesSampler = $this->options->getProfilesSampler(); + $profilesSampler = $options->getProfilesSampler(); if ($profilesSampler !== null) { $profilesSampleRate = $profilesSampler($samplingContext); $profilesSampleSource = 'config:profiles_sampler'; } else { - $profilesSampleRate = $this->options->getProfilesSampleRate(); + $profilesSampleRate = $options->getProfilesSampleRate(); } if ($profilesSampleRate === null) { $logger->info(\sprintf('Transaction [%s] is not profiling because neither `profiles_sample_rate` nor `profiles_sampler` option is set.', (string) $transaction->getTraceId())); - } elseif (!$this->isValidSampleRate($profilesSampleRate)) { + } elseif (!self::isValidSampleRate($profilesSampleRate)) { $logger->warning(\sprintf('Transaction [%s] is not profiling because profile sample rate (decided by %s) is invalid.', (string) $transaction->getTraceId(), $profilesSampleSource)); - } elseif ($this->sampleRate($profilesSampleRate)) { + } elseif (self::sampleRate($profilesSampleRate)) { $logger->info(\sprintf('Transaction [%s] started profiling because it was sampled.', (string) $transaction->getTraceId())); - $transaction->initProfiler()->start(); + $transaction->initProfiler($options)->start(); } else { $logger->info(\sprintf('Transaction [%s] is not profiling because it was not sampled.', (string) $transaction->getTraceId())); } @@ -126,7 +121,7 @@ public function startTransaction(Transaction $transaction, TransactionContext $c return $transaction; } - private function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float + private static function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float { if ($hasParentBeenSampled === true) { return 1.0; @@ -142,7 +137,7 @@ private function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampl /** * @param mixed $sampleRate */ - private function sampleRate($sampleRate): bool + private static function sampleRate($sampleRate): bool { if (!\is_float($sampleRate) && !\is_int($sampleRate)) { return false; @@ -162,7 +157,7 @@ private function sampleRate($sampleRate): bool /** * @param mixed $sampleRate */ - private function isValidSampleRate($sampleRate): bool + private static function isValidSampleRate($sampleRate): bool { if (!\is_float($sampleRate) && !\is_int($sampleRate)) { return false; diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php index 972ce617e..5e9bd9837 100644 --- a/tests/State/HubTest.php +++ b/tests/State/HubTest.php @@ -838,7 +838,7 @@ public function testStartTransactionWithCustomSamplingContext(): void public function testStartTransactionStartsProfilerWithProfilesSampler(): void { $client = $this->createMock(ClientInterface::class); - $client->expects($this->exactly(2)) + $client->expects($this->once()) ->method('getOptions') ->willReturn(new Options([ 'traces_sample_rate' => 1.0, diff --git a/tests/Tracing/TransactionSamplerTest.php b/tests/Tracing/TransactionSamplerTest.php index 1fe3aab72..9118e9c12 100644 --- a/tests/Tracing/TransactionSamplerTest.php +++ b/tests/Tracing/TransactionSamplerTest.php @@ -5,9 +5,7 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; -use Sentry\ClientInterface; use Sentry\Options; -use Sentry\State\Hub; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\SamplingContext; use Sentry\Tracing\Transaction; @@ -320,11 +318,6 @@ public function testUpdatesTheDscSampleRate(): void */ private function sampleTransaction(Options $options, TransactionContext $transactionContext, array $customSamplingContext = []): Transaction { - $client = $this->createMock(ClientInterface::class); - $client->method('getOptions')->willReturn($options); - - $transaction = new Transaction($transactionContext, new Hub($client)); - - return (new TransactionSampler($options))->startTransaction($transaction, $transactionContext, $customSamplingContext); + return TransactionSampler::startTransaction($options, $transactionContext, $customSamplingContext); } } From 9d367f96544af9cff760eadb159a14347f4ce6d7 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:47 +0200 Subject: [PATCH 08/20] feat(scope): remove Hub from Transaction (#2122) --- src/Tracing/DynamicSamplingContext.php | 6 ++--- src/Tracing/Transaction.php | 16 ++++---------- tests/FunctionsTest.php | 1 + tests/Tracing/DynamicSamplingContextTest.php | 23 +++++++------------- tests/Tracing/TransactionTest.php | 22 +++++++++---------- 5 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/Tracing/DynamicSamplingContext.php b/src/Tracing/DynamicSamplingContext.php index 410b4fc53..588eead06 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(); - self::setOrgOptions($client->getOptions(), $samplingContext); if ($transaction->getSampled() !== null) { diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index 34134b855..89086bc96 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -9,18 +9,12 @@ use Sentry\Options; use Sentry\Profiling\Profiler; use Sentry\SentrySdk; -use Sentry\State\HubInterface; /** * This class stores all the information about a Transaction. */ final class Transaction extends Span { - /** - * @var HubInterface The hub instance - */ - private $hub; - /** * @var string Name of the transaction */ @@ -45,15 +39,13 @@ 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 * * @internal */ - public function __construct(TransactionContext $context, ?HubInterface $hub = null) + public function __construct(TransactionContext $context) { parent::__construct($context); - $this->hub = $hub ?? SentrySdk::getCurrentHub(); $this->name = $context->getName(); $this->metadata = $context->getMetadata(); $this->transaction = $this; @@ -98,7 +90,7 @@ public function getDynamicSamplingContext(): DynamicSamplingContext return $this->metadata->getDynamicSamplingContext(); } - $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, $this->hub); + $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, SentrySdk::getClient()); $this->getMetadata()->setDynamicSamplingContext($samplingContext); return $samplingContext; @@ -123,7 +115,7 @@ public function initSpanRecorder(int $maxSpans = 1000): self public function initProfiler(?Options $options = null): Profiler { if ($this->profiler === null) { - $this->profiler = new Profiler($options ?? $this->hub->getClient()->getOptions()); + $this->profiler = new Profiler($options ?? SentrySdk::getClient()->getOptions()); } return $this->profiler; @@ -188,6 +180,6 @@ public function finish(?float $endTimestamp = null): ?EventId } } - return $this->hub->captureEvent($event); + return \Sentry\captureEvent($event); } } diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index ba349bde9..4d5c5166a 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -631,6 +631,7 @@ public function testBaggageWithTracingEnabled(): void $hub = new Hub($client); + SentrySdk::getGlobalScope()->setClient($client); SentrySdk::setCurrentHub($hub); $transactionContext = new TransactionContext(); diff --git a/tests/Tracing/DynamicSamplingContextTest.php b/tests/Tracing/DynamicSamplingContextTest.php index 0996aa702..7f1c153c4 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); $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,13 @@ public function testFromTransaction(): void public function testFromTransactionSourceUrl(): void { - $hub = new Hub(new NoOpClient()); - $transactionContext = new TransactionContext(); $transactionContext->setName('/foo/bar/123'); $transactionContext->setSource(TransactionSource::url()); - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, new NoOpClient()); $this->assertNull($samplingContext->get('transaction')); } @@ -189,9 +184,8 @@ public function testFromTransactionUsesConfiguredOrgIdOverDsnOrgId(): void 'org_id' => 2, ])); - $hub = new Hub($client); - $transaction = new Transaction(new TransactionContext(), $hub); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $transaction = new Transaction(new TransactionContext()); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertSame('2', $samplingContext->get('org_id')); } @@ -205,9 +199,8 @@ public function testFromTransactionFallsBackToDsnOrgId(): void 'dsn' => 'http://public@o1.example.com/1', ])); - $hub = new Hub($client); - $transaction = new Transaction(new TransactionContext(), $hub); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $transaction = new Transaction(new TransactionContext()); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertSame('1', $samplingContext->get('org_id')); } diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index 76a2c3aab..82a767bd3 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -10,8 +10,9 @@ use Sentry\EventId; use Sentry\EventType; use Sentry\Options; +use Sentry\SentrySdk; use Sentry\State\Hub; -use Sentry\State\HubInterface; +use Sentry\State\Scope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -37,19 +38,16 @@ public function testFinish(): void ->method('getOptions') ->willReturn(new Options()); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getClient') - ->willReturn($client); + SentrySdk::init($client); - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext); $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()); @@ -60,7 +58,7 @@ public function testFinish(): void $this->assertSame([$span1, $span2], $eventArg->getSpans()); return true; - })) + }), null, $this->isInstanceOf(Scope::class)) ->willReturnCallback(static function (Event $eventArg) use (&$expectedEventId): EventId { $expectedEventId = $eventArg->getId(); @@ -77,11 +75,13 @@ 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); + SentrySdk::init($client); + + $transaction = new Transaction(new TransactionContext()); $transaction->finish(); } From 03f285092e303ac0bcc3518a6fe120be94d9d040 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:48 +0200 Subject: [PATCH 09/20] feat(scope): update aggregator flush to use a client (#2125) --- src/Logs/LogsAggregator.php | 15 +++++++----- src/Metrics/MetricsAggregator.php | 15 +++++++----- src/State/RuntimeContextManager.php | 8 +++---- tests/Logs/LogsAggregatorTest.php | 36 ++++++++++++++++++++++++++++ tests/Metrics/TraceMetricsTest.php | 37 +++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 0fa66b7fc..99c0370c9 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -6,6 +6,7 @@ use Sentry\Attributes\Attribute; use Sentry\Client; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; @@ -159,20 +160,22 @@ public function add( $logs->push($log); if ($logFlushThreshold !== null && \count($logs) >= $logFlushThreshold) { - $this->flush($hub); + $this->flush($client); } } - public function flush(?HubInterface $hub = null): ?EventId + public function flush(?ClientInterface $client = null): ?EventId { - if ($this->logs === null || $this->logs->isEmpty()) { + $logs = $this->logs; + + if ($logs === null || $logs->isEmpty()) { return null; } - $hub = $hub ?? SentrySdk::getCurrentHub(); - $event = Event::createLogs()->setLogs($this->logs->drain()); + $client = $client ?? SentrySdk::getCurrentHub()->getClient(); + $event = Event::createLogs()->setLogs($logs->drain()); - return $hub->captureEvent($event); + return $client->captureEvent($event); } /** diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index a4a3aef3e..7fdae2394 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -5,6 +5,7 @@ namespace Sentry\Metrics; use Sentry\Client; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\Metrics\Types\CounterMetric; @@ -124,20 +125,22 @@ public function add( $metrics->push($metric); if ($metricFlushThreshold !== null && \count($metrics) >= $metricFlushThreshold) { - $this->flush($hub); + $this->flush($client); } } - public function flush(?HubInterface $hub = null): ?EventId + public function flush(?ClientInterface $client = null): ?EventId { - if ($this->metrics === null || $this->metrics->isEmpty()) { + $metrics = $this->metrics; + + if ($metrics === null || $metrics->isEmpty()) { return null; } - $hub = $hub ?? SentrySdk::getCurrentHub(); - $event = Event::createMetrics()->setMetrics($this->metrics->drain()); + $client = $client ?? SentrySdk::getCurrentHub()->getClient(); + $event = Event::createMetrics()->setMetrics($metrics->drain()); - return $hub->captureEvent($event); + return $client->captureEvent($event); } /** diff --git a/src/State/RuntimeContextManager.php b/src/State/RuntimeContextManager.php index cec72ee48..db1a80a2e 100644 --- a/src/State/RuntimeContextManager.php +++ b/src/State/RuntimeContextManager.php @@ -161,12 +161,12 @@ private function removeContextById(string $runtimeContextId, ?int $timeout = nul private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?int $timeout, LoggerInterface $logger): void { - $hub = $runtimeContext->getHub(); + $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($hub); + $runtimeContext->getLogsAggregator()->flush($client); } catch (\Throwable $exception) { $logger->error('Failed to flush logs while ending a runtime context.', [ 'exception' => $exception, @@ -176,7 +176,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($hub); + $runtimeContext->getMetricsAggregator()->flush($client); } catch (\Throwable $exception) { $logger->error('Failed to flush trace metrics while ending a runtime context.', [ 'exception' => $exception, @@ -184,8 +184,6 @@ private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?i ]); } - $client = $hub->getClient(); - // Custom transports may throw from close(); endContext must stay best-effort and non-fatal. try { $client->flush($timeout); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 5278f5039..97320a6d1 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -7,8 +7,11 @@ use PHPUnit\Framework\TestCase; use Sentry\Client; use Sentry\ClientBuilder; +use Sentry\ClientInterface; +use Sentry\Event; use Sentry\Logs\LogLevel; use Sentry\Logs\LogsAggregator; +use Sentry\Options; use Sentry\SentrySdk; use Sentry\State\Hub; use Sentry\State\Scope; @@ -271,6 +274,39 @@ public function testFlushesImmediatelyWhenThresholdIsReached(): void $this->assertSame('Second message', StubTransport::$events[0]->getLogs()[1]->getBody()); } + public function testFlushCapturesLogsWithProvidedClient(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'enable_logs' => true, + ])); + + $fallbackClient = $this->createMock(ClientInterface::class); + $fallbackClient->method('getOptions') + ->willReturn(new Options([ + 'enable_logs' => true, + ])); + $fallbackClient->expects($this->never()) + ->method('captureEvent'); + SentrySdk::setCurrentHub(new Hub($fallbackClient)); + + $aggregator = new LogsAggregator(); + $aggregator->add(LogLevel::info(), 'Test message'); + + $client->expects($this->once()) + ->method('captureEvent') + ->with( + $this->callback(function (Event $event): bool { + $this->assertCount(1, $event->getLogs()); + + return true; + }) + ); + + $aggregator->flush($client); + } + public function testDoesNotFlushImmediatelyWhenThresholdIsNull(): void { StubTransport::$events = []; diff --git a/tests/Metrics/TraceMetricsTest.php b/tests/Metrics/TraceMetricsTest.php index 1191e076e..bb5773699 100644 --- a/tests/Metrics/TraceMetricsTest.php +++ b/tests/Metrics/TraceMetricsTest.php @@ -6,12 +6,16 @@ use PHPUnit\Framework\TestCase; use Sentry\Client; +use Sentry\ClientInterface; +use Sentry\Event; use Sentry\Metrics\MetricsAggregator; use Sentry\Metrics\Types\CounterMetric; use Sentry\Metrics\Types\DistributionMetric; use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\Options; +use Sentry\SentrySdk; +use Sentry\State\Hub; use Sentry\State\HubAdapter; use Sentry\State\Scope; @@ -110,6 +114,39 @@ public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): voi $this->assertCount(2, StubTransport::$events[0]->getMetrics()); } + public function testFlushCapturesMetricsWithProvidedClient(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'enable_metrics' => true, + ])); + + $fallbackClient = $this->createMock(ClientInterface::class); + $fallbackClient->method('getOptions') + ->willReturn(new Options([ + 'enable_metrics' => true, + ])); + $fallbackClient->expects($this->never()) + ->method('captureEvent'); + SentrySdk::setCurrentHub(new Hub($fallbackClient)); + + $aggregator = new MetricsAggregator(); + $aggregator->add(CounterMetric::TYPE, 'test-count', 2, ['foo' => 'bar'], null); + + $client->expects($this->once()) + ->method('captureEvent') + ->with( + $this->callback(function (Event $event): bool { + $this->assertCount(1, $event->getMetrics()); + + return true; + }) + ); + + $aggregator->flush($client); + } + public function testMetricsBufferFullWhenMetricFlushThresholdIsNull(): void { HubAdapter::getInstance()->bindClient(new Client(new Options([ From 6cfa2984dcbdbcc0c88751cd1f42de37001745a2 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:48 +0200 Subject: [PATCH 10/20] feat(scopes): swap many usages of hub to scope (#2129) --- 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 | 90 +------ src/functions.php | 47 +++- tests/Fixtures/runtime/frankenphp/index.php | 19 +- tests/Fixtures/runtime/roadrunner-worker.php | 19 +- tests/FunctionsTest.php | 228 ++++++++--------- tests/Logs/LogsAggregatorTest.php | 16 +- tests/SentrySdkExtension.php | 9 - tests/SentrySdkTest.php | 75 +++--- tests/State/HubAdapterTest.php | 253 ++++++------------- 15 files changed, 391 insertions(+), 565 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..4fd2c7e1a 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,11 +23,6 @@ final class RuntimeContextManager { private const PROCESS_EXECUTION_CONTEXT_KEY = 'process'; - /** - * @var HubInterface - */ - private $baseHub; - /** * @var RuntimeContext|null */ @@ -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/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 = []; 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 cbea51b60c7d0924e0c717d58d74ec718d58e0a5 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:48 +0200 Subject: [PATCH 11/20] feat(scope): dont use hub for capturing events anymore (#2132) --- src/State/BreadcrumbRecorder.php | 44 ++++ src/State/EventCapturer.php | 107 +++++++++ src/State/HubAdapter.php | 63 +---- src/functions.php | 27 ++- tests/FunctionsTest.php | 318 +++++++++++++++++++++---- tests/State/BreadcrumbRecorderTest.php | 132 ++++++++++ tests/State/EventCapturerTest.php | 205 ++++++++++++++++ 7 files changed, 782 insertions(+), 114 deletions(-) create mode 100644 src/State/BreadcrumbRecorder.php create mode 100644 src/State/EventCapturer.php create mode 100644 tests/State/BreadcrumbRecorderTest.php create mode 100644 tests/State/EventCapturerTest.php diff --git a/src/State/BreadcrumbRecorder.php b/src/State/BreadcrumbRecorder.php new file mode 100644 index 000000000..37b315725 --- /dev/null +++ b/src/State/BreadcrumbRecorder.php @@ -0,0 +1,44 @@ +getOptions(); + $maxBreadcrumbs = $options->getMaxBreadcrumbs(); + + if ($maxBreadcrumbs <= 0) { + return false; + } + + $breadcrumb = ($options->getBeforeBreadcrumbCallback())($breadcrumb); + + if ($breadcrumb !== null) { + $scope->addBreadcrumb($breadcrumb, $maxBreadcrumbs); + } + + return $breadcrumb !== null; + } +} diff --git a/src/State/EventCapturer.php b/src/State/EventCapturer.php new file mode 100644 index 000000000..2511a0013 --- /dev/null +++ b/src/State/EventCapturer.php @@ -0,0 +1,107 @@ +captureMessage($message, $level, $captureScope, $hint); + }); + } + + public static function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId + { + return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($exception, $hint): ?EventId { + return $client->captureException($exception, $captureScope, $hint); + }); + } + + public static function captureEvent(Event $event, ?EventHint $hint = null): ?EventId + { + return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($event, $hint): ?EventId { + return $client->captureEvent($event, $hint, $captureScope); + }); + } + + public static function captureLastError(?EventHint $hint = null): ?EventId + { + return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($hint): ?EventId { + return $client->captureLastError($captureScope, $hint); + }); + } + + /** + * @param int|float|null $duration + */ + public static function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string + { + $isolationScope = SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($isolationScope); + + 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); + + self::captureWithScope($client, $isolationScope, static function (ClientInterface $client, Scope $captureScope) use ($event): ?EventId { + return $client->captureEvent($event, null, $captureScope); + }); + + return $checkIn->getId(); + } + + /** + * @param callable(ClientInterface, Scope): ?EventId $capture + */ + private static function capture(callable $capture): ?EventId + { + $isolationScope = SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, $capture); + } + + /** + * @param callable(ClientInterface, Scope): ?EventId $capture + */ + private static function captureWithScope(ClientInterface $client, Scope $isolationScope, callable $capture): ?EventId + { + $eventId = $capture($client, Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope)); + $isolationScope->setLastEventId($eventId); + + return $eventId; + } +} diff --git a/src/State/HubAdapter.php b/src/State/HubAdapter.php index a5aca9f5b..b376b4cd7 100644 --- a/src/State/HubAdapter.php +++ b/src/State/HubAdapter.php @@ -97,10 +97,7 @@ public function bindClient(ClientInterface $client): void */ public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - $eventId = SentrySdk::getClient()->captureMessage($message, $level, SentrySdk::getIsolationScope(), $hint); - SentrySdk::getIsolationScope()->setLastEventId($eventId); - - return $eventId; + return EventCapturer::captureMessage($message, $level, $hint); } /** @@ -108,10 +105,7 @@ public function captureMessage(string $message, ?Severity $level = null, ?EventH */ public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { - $eventId = SentrySdk::getClient()->captureException($exception, SentrySdk::getIsolationScope(), $hint); - SentrySdk::getIsolationScope()->setLastEventId($eventId); - - return $eventId; + return EventCapturer::captureException($exception, $hint); } /** @@ -119,10 +113,7 @@ public function captureException(\Throwable $exception, ?EventHint $hint = null) */ public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - $eventId = SentrySdk::getClient()->captureEvent($event, $hint, SentrySdk::getIsolationScope()); - SentrySdk::getIsolationScope()->setLastEventId($eventId); - - return $eventId; + return EventCapturer::captureEvent($event, $hint); } /** @@ -130,10 +121,7 @@ public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId */ public function captureLastError(?EventHint $hint = null): ?EventId { - $eventId = SentrySdk::getClient()->captureLastError(SentrySdk::getIsolationScope(), $hint); - SentrySdk::getIsolationScope()->setLastEventId($eventId); - - return $eventId; + return EventCapturer::captureLastError($hint); } /** @@ -143,27 +131,7 @@ public function captureLastError(?EventHint $hint = null): ?EventId */ public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { - $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(); + return EventCapturer::captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** @@ -171,26 +139,9 @@ public function captureCheckIn(string $slug, CheckInStatus $status, $duration = */ public function addBreadcrumb(Breadcrumb $breadcrumb): bool { - $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); - } + $scope = SentrySdk::getIsolationScope(); - return $breadcrumb !== null; + return BreadcrumbRecorder::record(SentrySdk::getClient($scope), $scope, $breadcrumb); } /** diff --git a/src/functions.php b/src/functions.php index 845bfd594..9d3cd51e9 100644 --- a/src/functions.php +++ b/src/functions.php @@ -10,6 +10,8 @@ use Sentry\Integration\OTLPIntegration; use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; +use Sentry\State\BreadcrumbRecorder; +use Sentry\State\EventCapturer; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanContext; @@ -101,7 +103,7 @@ function getClient(): ClientInterface */ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); + return EventCapturer::captureMessage($message, $level, $hint); } /** @@ -112,7 +114,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 EventCapturer::captureException($exception, $hint); } /** @@ -123,7 +125,7 @@ function captureException(\Throwable $exception, ?EventHint $hint = null): ?Even */ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureEvent($event, $hint); + return EventCapturer::captureEvent($event, $hint); } /** @@ -133,7 +135,7 @@ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId */ function captureLastError(?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureLastError($hint); + return EventCapturer::captureLastError($hint); } /** @@ -147,7 +149,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 EventCapturer::captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** @@ -161,7 +163,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); $status = CheckInStatus::ok(); $duration = 0; @@ -177,7 +179,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); } } @@ -195,11 +197,12 @@ 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) - ); + $scope = SentrySdk::getIsolationScope(); + $breadcrumb = $category instanceof Breadcrumb + ? $category + : new Breadcrumb($level, $type, $category, $message, $metadata, $timestamp); + + BreadcrumbRecorder::record(SentrySdk::getClient($scope), $scope, $breadcrumb); } /** diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 2d1d47a7c..577951b91 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -86,18 +86,18 @@ public function testCaptureMessage(array $functionCallArgs, array $expectedFunct $eventId = EventId::generate(); $message = $expectedFunctionCallArgs[0]; $level = $expectedFunctionCallArgs[1]; - $scope = $this->isInstanceOf(Scope::class); $hint = $expectedFunctionCallArgs[2]; $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('captureMessage') - ->with($message, $level, $scope, $hint) + ->with($message, $level, $this->captureScopeConstraint($scope), $hint) ->willReturn($eventId); - SentrySdk::getGlobalScope()->setClient($client); - $this->assertSame($eventId, captureMessage(...$functionCallArgs)); + $this->assertSame($eventId, $scope->getLastEventId()); } public static function captureMessageDataProvider(): \Generator @@ -136,14 +136,15 @@ public function testCaptureException(array $functionCallArgs, array $expectedFun $eventId = EventId::generate(); $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('captureException') - ->with($expectedFunctionCallArgs[0], $this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[1]) + ->with($expectedFunctionCallArgs[0], $this->captureScopeConstraint($scope), $expectedFunctionCallArgs[1]) ->willReturn($eventId); - SentrySdk::getGlobalScope()->setClient($client); - $this->assertSame($eventId, captureException(...$functionCallArgs)); + $this->assertSame($eventId, $scope->getLastEventId()); } public static function captureExceptionDataProvider(): \Generator @@ -176,14 +177,15 @@ public function testCaptureEvent(): void $hint = new EventHint(); $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('captureEvent') - ->with($event, $hint, $this->isInstanceOf(Scope::class)) + ->with($event, $hint, $this->captureScopeConstraint($scope)) ->willReturn($event->getId()); - SentrySdk::getGlobalScope()->setClient($client); - $this->assertSame($event->getId(), captureEvent($event, $hint)); + $this->assertSame($event->getId(), $scope->getLastEventId()); } /** @@ -194,16 +196,17 @@ public function testCaptureLastError(array $functionCallArgs, array $expectedFun $eventId = EventId::generate(); $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('captureLastError') - ->with($this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[0]) + ->with($this->captureScopeConstraint($scope), $expectedFunctionCallArgs[0]) ->willReturn($eventId); - SentrySdk::getGlobalScope()->setClient($client); - @trigger_error('foo', \E_USER_NOTICE); $this->assertSame($eventId, captureLastError(...$functionCallArgs)); + $this->assertSame($eventId, $scope->getLastEventId()); } public static function captureLastErrorDataProvider(): \Generator @@ -219,9 +222,72 @@ public static function captureLastErrorDataProvider(): \Generator ]; } + public function testCaptureMessageClearsLastEventIdWhenClientReturnsNull(): void + { + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $scope->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureMessage') + ->with('foo', null, $this->captureScopeConstraint($scope), null) + ->willReturn(null); + + $this->assertNull(captureMessage('foo')); + $this->assertNull($scope->getLastEventId()); + } + + public function testCaptureExceptionClearsLastEventIdWhenClientReturnsNull(): void + { + $exception = new \RuntimeException('foo'); + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $scope->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureException') + ->with($exception, $this->captureScopeConstraint($scope), null) + ->willReturn(null); + + $this->assertNull(captureException($exception)); + $this->assertNull($scope->getLastEventId()); + } + + public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void + { + $event = Event::createEvent(); + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $scope->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($event, null, $this->captureScopeConstraint($scope)) + ->willReturn(null); + + $this->assertNull(captureEvent($event)); + $this->assertNull($scope->getLastEventId()); + } + + public function testCaptureLastErrorClearsLastEventIdWhenClientReturnsNull(): void + { + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $scope->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureLastError') + ->with($this->captureScopeConstraint($scope), null) + ->willReturn(null); + + $this->assertNull(captureLastError()); + $this->assertNull($scope->getLastEventId()); + } + public function testCaptureCheckIn(): void { $checkInId = SentryUid::generate(); + $eventId = EventId::generate(); $monitorConfig = new MonitorConfig( MonitorSchedule::crontab('*/5 * * * *'), 5, @@ -230,19 +296,29 @@ public function testCaptureCheckIn(): void ); $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('getOptions') - ->willReturn(new Options()); + ->willReturn(new Options([ + 'environment' => Event::DEFAULT_ENVIRONMENT, + 'release' => '1.0.0', + ])); $client->expects($this->once()) ->method('captureEvent') - ->with($this->callback(static function (Event $event) use ($checkInId): bool { + ->with($this->callback(static function (Event $event) use ($checkInId, $monitorConfig): bool { $checkIn = $event->getCheckIn(); - return $checkIn !== null && $checkIn->getId() === $checkInId; - }), null, $this->isInstanceOf(Scope::class)) - ->willReturn(EventId::generate()); - - SentrySdk::getGlobalScope()->setClient($client); + return $checkIn !== null + && $checkIn->getId() === $checkInId + && $checkIn->getMonitorSlug() === 'test-crontab' + && $checkIn->getStatus() == CheckInStatus::ok() + && $checkIn->getRelease() === '1.0.0' + && $checkIn->getEnvironment() === Event::DEFAULT_ENVIRONMENT + && $checkIn->getDuration() === 10 + && $checkIn->getMonitorConfig() === $monitorConfig; + }), null, $this->captureScopeConstraint($scope)) + ->willReturn($eventId); $this->assertSame($checkInId, captureCheckIn( 'test-crontab', @@ -251,11 +327,29 @@ public function testCaptureCheckIn(): void $monitorConfig, $checkInId )); + $this->assertSame($eventId, $scope->getLastEventId()); + } + + public function testCaptureCheckInReturnsNullForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + + $this->assertNull(captureCheckIn('test-crontab', CheckInStatus::ok())); } public function testWithMonitor(): void { + $events = []; + $monitorConfig = new MonitorConfig( + new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), + 5, + 30, + 'UTC' + ); + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->exactly(2)) ->method('getOptions') ->willReturn(new Options()); @@ -268,63 +362,149 @@ public function testWithMonitor(): void && $checkIn->getMonitorSlug() === 'test-crontab' && $checkIn->getMonitorConfig() !== null && $checkIn->getMonitorConfig()->getSchedule()->getValue() === '*/5 * * * *'; - }), null, $this->isInstanceOf(Scope::class)); + }), null, $this->captureScopeConstraint($scope)) + ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?Scope $scope = null) use (&$events): EventId { + $events[] = $event; - SentrySdk::getGlobalScope()->setClient($client); + return EventId::generate(); + }); - withMonitor('test-crontab', static function () { + $result = withMonitor('test-crontab', static function (): string { // Do something... - }, new MonitorConfig( - new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), - 5, - 30, - 'UTC' - )); + return 'done'; + }, $monitorConfig); + + $this->assertSame('done', $result); + $this->assertCount(2, $events); + $this->assertSame(CheckInStatus::inProgress(), $events[0]->getCheckIn()->getStatus()); + $this->assertSame(CheckInStatus::ok(), $events[1]->getCheckIn()->getStatus()); + $this->assertSame($events[0]->getCheckIn()->getId(), $events[1]->getCheckIn()->getId()); } public function testWithMonitorCallableThrows(): void { - $this->expectException(\Exception::class); + $events = []; $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $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)); + ->with($this->isInstanceOf(Event::class), null, $this->captureScopeConstraint($scope)) + ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?Scope $scope = null) use (&$events): EventId { + $events[] = $event; - SentrySdk::getGlobalScope()->setClient($client); + return EventId::generate(); + }); - withMonitor('test-crontab', static function () { - throw new \Exception(); - }, new MonitorConfig( - new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), - 5, - 30, - 'UTC' - )); + try { + withMonitor('test-crontab', static function (): void { + throw new \Exception('monitor failed'); + }, new MonitorConfig( + new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), + 5, + 30, + 'UTC' + )); + + $this->fail('The callback exception should be rethrown.'); + } catch (\Exception $exception) { + $this->assertSame('monitor failed', $exception->getMessage()); + } + + $this->assertCount(2, $events); + $this->assertSame(CheckInStatus::inProgress(), $events[0]->getCheckIn()->getStatus()); + $this->assertSame(CheckInStatus::error(), $events[1]->getCheckIn()->getStatus()); + $this->assertSame($events[0]->getCheckIn()->getId(), $events[1]->getCheckIn()->getId()); } public function testAddBreadcrumb(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $otherScope = new Scope(); /** @var ClientInterface&MockObject $client */ $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('getOptions') ->willReturn(new Options(['default_integrations' => false])); - SentrySdk::getCurrentHub()->bindClient($client); + addBreadcrumb($breadcrumb); + + $this->assertScopeBreadcrumbs($scope, [$breadcrumb]); + $this->assertScopeBreadcrumbs($otherScope, []); + } + + public function testAddBreadcrumbDoesNothingIfMaxBreadcrumbsLimitIsZero(): void + { + $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['max_breadcrumbs' => 0])); addBreadcrumb($breadcrumb); - configureScope(function (Scope $scope) use ($breadcrumb): void { - $event = $scope->applyToEvent(Event::createEvent()); - $this->assertNotNull($event); - $this->assertSame([$breadcrumb], $event->getBreadcrumbs()); - }); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testAddBreadcrumbDoesNothingForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + $scope = SentrySdk::getIsolationScope(); + + addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); + + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testAddBreadcrumbDoesNothingWhenBeforeBreadcrumbCallbackReturnsNull(): void + { + $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () { + return null; + }, + ])); + + addBreadcrumb($breadcrumb); + + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testAddBreadcrumbStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): void + { + $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_DEFAULT, 'custom'); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () use ($breadcrumb2): Breadcrumb { + return $breadcrumb2; + }, + ])); + + addBreadcrumb($breadcrumb1); + + $this->assertScopeBreadcrumbs($scope, [$breadcrumb2]); } public function testWithScope(): void @@ -763,4 +943,50 @@ public function testContinueTraceWhenOrgMatch(): void $this->assertSame('1', $dynamicSamplingContext->get('org_id')); }); } + + private function setClientAndIsolationScope(ClientInterface $client): Scope + { + SentrySdk::init(); + + SentrySdk::getGlobalScope()->clear(); + SentrySdk::getGlobalScope()->setTag('scope', 'global'); + SentrySdk::getGlobalScope()->setTag('global', 'yes'); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new Scope(); + $scope->setTag('scope', 'isolation'); + $scope->setTag('isolation', 'yes'); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); + + return $scope; + } + + private function captureScopeConstraint(Scope $isolationScope) + { + return $this->callback(function (Scope $captureScope) use ($isolationScope): bool { + $this->assertNotSame($isolationScope, $captureScope); + + $event = $captureScope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([ + 'scope' => 'isolation', + 'global' => 'yes', + 'isolation' => 'yes', + ], $event->getTags()); + + return true; + }); + } + + /** + * @param Breadcrumb[] $expectedBreadcrumbs + */ + private function assertScopeBreadcrumbs(Scope $scope, array $expectedBreadcrumbs): void + { + $event = $scope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame($expectedBreadcrumbs, $event->getBreadcrumbs()); + } } diff --git a/tests/State/BreadcrumbRecorderTest.php b/tests/State/BreadcrumbRecorderTest.php new file mode 100644 index 000000000..121825aae --- /dev/null +++ b/tests/State/BreadcrumbRecorderTest.php @@ -0,0 +1,132 @@ +createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); + + $this->assertTrue(BreadcrumbRecorder::record($client, $scope, $breadcrumb)); + $this->assertScopeBreadcrumbs($scope, [$breadcrumb]); + $this->assertScopeBreadcrumbs($otherScope, []); + } + + public function testRecordReturnsFalseForNoOpClient(): void + { + $scope = new Scope(); + + $this->assertFalse(BreadcrumbRecorder::record( + new NoOpClient(), + $scope, + new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting') + )); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testRecordReturnsFalseWhenMaxBreadcrumbsLimitIsZero(): void + { + $scope = new Scope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['max_breadcrumbs' => 0])); + + $this->assertFalse(BreadcrumbRecorder::record( + $client, + $scope, + new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting') + )); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testRecordReturnsFalseWhenBeforeBreadcrumbCallbackReturnsNull(): void + { + $scope = new Scope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () { + return null; + }, + ])); + + $this->assertFalse(BreadcrumbRecorder::record( + $client, + $scope, + new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting') + )); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testRecordStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): void + { + $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_DEFAULT, 'custom'); + $scope = new Scope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () use ($breadcrumb2): Breadcrumb { + return $breadcrumb2; + }, + ])); + + $this->assertTrue(BreadcrumbRecorder::record($client, $scope, $breadcrumb1)); + $this->assertScopeBreadcrumbs($scope, [$breadcrumb2]); + } + + public function testRecordRespectsMaxBreadcrumbsLimit(): void + { + $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'one'); + $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'two'); + $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'three'); + $scope = new Scope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->exactly(3)) + ->method('getOptions') + ->willReturn(new Options(['max_breadcrumbs' => 2])); + + BreadcrumbRecorder::record($client, $scope, $breadcrumb1); + BreadcrumbRecorder::record($client, $scope, $breadcrumb2); + BreadcrumbRecorder::record($client, $scope, $breadcrumb3); + + $this->assertScopeBreadcrumbs($scope, [$breadcrumb2, $breadcrumb3]); + } + + /** + * @param Breadcrumb[] $expectedBreadcrumbs + */ + private function assertScopeBreadcrumbs(Scope $scope, array $expectedBreadcrumbs): void + { + $event = $scope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame($expectedBreadcrumbs, $event->getBreadcrumbs()); + } +} diff --git a/tests/State/EventCapturerTest.php b/tests/State/EventCapturerTest.php new file mode 100644 index 000000000..8e9123cda --- /dev/null +++ b/tests/State/EventCapturerTest.php @@ -0,0 +1,205 @@ +createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureMessage') + ->with('foo', Severity::debug(), $this->callback(function (Scope $scope) use ($isolationScope): bool { + return $this->isMergedCaptureScope($scope, $isolationScope); + }), $hint) + ->willReturn($eventId); + + $this->assertSame($eventId, EventCapturer::captureMessage('foo', Severity::debug(), $hint)); + $this->assertSame($eventId, $isolationScope->getLastEventId()); + } + + public function testCaptureExceptionPassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + { + $eventId = EventId::generate(); + $exception = new \RuntimeException('foo'); + $hint = new EventHint(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureException') + ->with($exception, $this->callback(function (Scope $scope) use ($isolationScope): bool { + return $this->isMergedCaptureScope($scope, $isolationScope); + }), $hint) + ->willReturn($eventId); + + $this->assertSame($eventId, EventCapturer::captureException($exception, $hint)); + $this->assertSame($eventId, $isolationScope->getLastEventId()); + } + + public function testCaptureEventPassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + { + $event = Event::createEvent(); + $hint = new EventHint(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($event, $hint, $this->callback(function (Scope $scope) use ($isolationScope): bool { + return $this->isMergedCaptureScope($scope, $isolationScope); + })) + ->willReturn($event->getId()); + + $this->assertSame($event->getId(), EventCapturer::captureEvent($event, $hint)); + $this->assertSame($event->getId(), $isolationScope->getLastEventId()); + } + + public function testCaptureLastErrorPassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + { + $eventId = EventId::generate(); + $hint = new EventHint(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureLastError') + ->with($this->callback(function (Scope $scope) use ($isolationScope): bool { + return $this->isMergedCaptureScope($scope, $isolationScope); + }), $hint) + ->willReturn($eventId); + + $this->assertSame($eventId, EventCapturer::captureLastError($hint)); + $this->assertSame($eventId, $isolationScope->getLastEventId()); + } + + public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void + { + $event = Event::createEvent(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + $isolationScope->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($event, null, $this->callback(function (Scope $scope) use ($isolationScope): bool { + return $this->isMergedCaptureScope($scope, $isolationScope); + })) + ->willReturn(null); + + $this->assertNull(EventCapturer::captureEvent($event)); + $this->assertNull($isolationScope->getLastEventId()); + } + + public function testCaptureCheckInCreatesEventAndStoresLastEventId(): void + { + $checkInId = SentryUid::generate(); + $eventId = EventId::generate(); + $monitorConfig = new MonitorConfig( + MonitorSchedule::crontab('*/5 * * * *'), + 5, + 30, + 'UTC' + ); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'environment' => Event::DEFAULT_ENVIRONMENT, + 'release' => '1.0.0', + ])); + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event) use ($checkInId, $monitorConfig): bool { + $checkIn = $event->getCheckIn(); + + return $checkIn !== null + && $checkIn->getId() === $checkInId + && $checkIn->getMonitorSlug() === 'test-crontab' + && $checkIn->getStatus() == CheckInStatus::ok() + && $checkIn->getRelease() === '1.0.0' + && $checkIn->getEnvironment() === Event::DEFAULT_ENVIRONMENT + && $checkIn->getDuration() === 10 + && $checkIn->getMonitorConfig() === $monitorConfig; + }), null, $this->callback(function (Scope $scope) use ($isolationScope): bool { + return $this->isMergedCaptureScope($scope, $isolationScope); + })) + ->willReturn($eventId); + + $this->assertSame($checkInId, EventCapturer::captureCheckIn( + 'test-crontab', + CheckInStatus::ok(), + 10, + $monitorConfig, + $checkInId + )); + $this->assertSame($eventId, $isolationScope->getLastEventId()); + } + + public function testCaptureCheckInReturnsNullForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + $eventId = EventId::generate(); + SentrySdk::getIsolationScope()->setLastEventId($eventId); + + $this->assertNull(EventCapturer::captureCheckIn('test-crontab', CheckInStatus::ok())); + $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); + } + + private function setClientAndIsolationScope(ClientInterface $client): Scope + { + SentrySdk::init(); + + SentrySdk::getGlobalScope()->clear(); + SentrySdk::getGlobalScope()->setTag('scope', 'global'); + SentrySdk::getGlobalScope()->setTag('global', 'yes'); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new Scope(); + $scope->setTag('scope', 'isolation'); + $scope->setTag('isolation', 'yes'); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); + + return $scope; + } + + private function isMergedCaptureScope(Scope $captureScope, Scope $isolationScope): bool + { + $this->assertNotSame($isolationScope, $captureScope); + + $event = $captureScope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([ + 'scope' => 'isolation', + 'global' => 'yes', + 'isolation' => 'yes', + ], $event->getTags()); + + return true; + } +} From 142948bd8a91f7cc4ba709f9ccfbf66ba8082472 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:49 +0200 Subject: [PATCH 12/20] feat(scopes): reduce usage of hub further (#2133) --- src/Integration/EnvironmentIntegration.php | 2 +- .../FrameContextifierIntegration.php | 2 +- src/Integration/ModulesIntegration.php | 2 +- src/Integration/OTLPIntegration.php | 8 +- src/Integration/RequestIntegration.php | 6 +- src/Integration/TransactionIntegration.php | 2 +- src/Logs/LogsAggregator.php | 66 +++---- src/Metrics/MetricsAggregator.php | 98 +++++----- src/Tracing/PropagationContext.php | 11 +- src/Tracing/Span.php | 9 +- src/Tracing/Traits/TraceHeaderParserTrait.php | 6 +- src/UserDataBag.php | 10 +- src/functions.php | 58 +++--- tests/FunctionsTest.php | 178 ++++++++++++------ tests/Logs/LogsAggregatorTest.php | 25 +++ tests/Metrics/TraceMetricsTest.php | 14 ++ tests/Tracing/PropagationContextTest.php | 25 +++ tests/Tracing/SpanTest.php | 25 +++ 18 files changed, 319 insertions(+), 228 deletions(-) diff --git a/src/Integration/EnvironmentIntegration.php b/src/Integration/EnvironmentIntegration.php index c404216ea..adf4f6a53 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/FrameContextifierIntegration.php b/src/Integration/FrameContextifierIntegration.php index 8e55a9727..b7ea13b40 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 affe1e062..f1234f355 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/OTLPIntegration.php b/src/Integration/OTLPIntegration.php index e5e04ef8e..184d55b50 100644 --- a/src/Integration/OTLPIntegration.php +++ b/src/Integration/OTLPIntegration.php @@ -56,8 +56,7 @@ public function setupOnce(): void } Scope::registerExternalPropagationContext(static function (): ?array { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $integration = SentrySdk::getClient()->getIntegration(self::class); if (!$integration instanceof self) { return null; @@ -193,9 +192,6 @@ private function getLogger(): LoggerInterface return $this->options->getLoggerOrNullLogger(); } - $currentHub = SentrySdk::getCurrentHub(); - $client = $currentHub->getClient(); - - return $client->getOptions()->getLoggerOrNullLogger(); + return SentrySdk::getClient()->getOptions()->getLoggerOrNullLogger(); } } diff --git a/src/Integration/RequestIntegration.php b/src/Integration/RequestIntegration.php index db123b019..98aa4ccb3 100644 --- a/src/Integration/RequestIntegration.php +++ b/src/Integration/RequestIntegration.php @@ -103,15 +103,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 1ed22d753..746584dab 100644 --- a/src/Integration/TransactionIntegration.php +++ b/src/Integration/TransactionIntegration.php @@ -24,7 +24,7 @@ final class TransactionIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event, EventHint $hint): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $integration = SentrySdk::getClient()->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 0915d4655..f92b0b305 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -10,7 +10,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; @@ -41,8 +40,9 @@ public function add( ): void { $timestamp = microtime(true); - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $isolationScope = SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($isolationScope); + $scope = Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope); $options = $client->getOptions(); $sdkLogger = $options->getLogger(); @@ -71,7 +71,7 @@ public function add( $formattedMessage = $message; } - $traceData = $this->getTraceData($hub); + $traceData = $this->getTraceData($scope); $traceId = $traceData['trace_id']; $parentSpanId = $traceData['parent_span_id']; @@ -86,20 +86,18 @@ public function add( $log->setAttribute('sentry.sdk.version', $client->getSdkVersion()); } - $hub->configureScope(static 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()); - } + $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); @@ -190,9 +188,9 @@ public function all(): array /** * @return array{trace_id: string, parent_span_id: string|null} */ - private function getTraceData(HubInterface $hub): array + private function getTraceData(Scope $scope): array { - $span = $hub->getSpan(); + $span = $scope->getSpan(); if ($span !== null) { return [ @@ -201,28 +199,18 @@ private function getTraceData(HubInterface $hub): array ]; } - $traceData = null; - - $hub->configureScope(static function (Scope $scope) use (&$traceData): void { - $externalPropagationContext = Scope::getExternalPropagationContext(); - - if ($externalPropagationContext !== null) { - $traceData = [ - 'trace_id' => $externalPropagationContext['trace_id'], - 'parent_span_id' => $externalPropagationContext['span_id'], - ]; - - return; - } - - $traceData = [ - 'trace_id' => (string) $scope->getPropagationContext()->getTraceId(), - 'parent_span_id' => null, + $externalPropagationContext = Scope::getExternalPropagationContext(); + if ($externalPropagationContext !== null) { + return [ + 'trace_id' => $externalPropagationContext['trace_id'], + 'parent_span_id' => $externalPropagationContext['span_id'], ]; - }); + } - /** @var array{trace_id: string, parent_span_id: string|null} $traceData */ - return $traceData; + return [ + 'trace_id' => (string) $scope->getPropagationContext()->getTraceId(), + 'parent_span_id' => null, + ]; } /** diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index b58fddd93..f4d9b2336 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -13,7 +13,6 @@ use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\SentrySdk; -use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\SpanId; use Sentry\Tracing\TraceId; @@ -52,60 +51,53 @@ public function add( array $attributes, ?Unit $unit ): void { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); - $metricFlushThreshold = null; + $isolationScope = SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($isolationScope); + $scope = Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope); + $options = $client->getOptions(); + $metricFlushThreshold = $options->getMetricFlushThreshold(); if (!\is_int($value) && !\is_float($value)) { - if ($client !== null) { - $client->getOptions()->getLoggerOrNullLogger()->debug('Metrics value is neither int nor float. Metric will be discarded'); - } + $options->getLoggerOrNullLogger()->debug('Metrics value is neither int nor float. Metric will be discarded'); return; } - if ($client !== null) { - $options = $client->getOptions(); - $metricFlushThreshold = $options->getMetricFlushThreshold(); + if ($options->getEnableMetrics() === false) { + return; + } - if ($options->getEnableMetrics() === false) { - return; - } + $defaultAttributes = [ + 'sentry.environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, + 'server.address' => $options->getServerName(), + ]; - $defaultAttributes = [ - 'sentry.environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, - 'server.address' => $options->getServerName(), - ]; + if ($client instanceof Client) { + $defaultAttributes['sentry.sdk.name'] = $client->getSdkIdentifier(); + $defaultAttributes['sentry.sdk.version'] = $client->getSdkVersion(); + } - if ($client instanceof Client) { - $defaultAttributes['sentry.sdk.name'] = $client->getSdkIdentifier(); - $defaultAttributes['sentry.sdk.version'] = $client->getSdkVersion(); + $user = $scope->getUser(); + if ($user !== null) { + if ($user->getId() !== null) { + $defaultAttributes['user.id'] = $user->getId(); } - - $hub->configureScope(static 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(); - } - } - }); - - $release = $options->getRelease(); - if ($release !== null) { - $defaultAttributes['sentry.release'] = $release; + if ($user->getEmail() !== null) { + $defaultAttributes['user.email'] = $user->getEmail(); } + if ($user->getUsername() !== null) { + $defaultAttributes['user.name'] = $user->getUsername(); + } + } - $attributes += $defaultAttributes; + $release = $options->getRelease(); + if ($release !== null) { + $defaultAttributes['sentry.release'] = $release; } - $traceContext = $this->getTraceContext($hub); + $attributes += $defaultAttributes; + + $traceContext = $this->getTraceContext($scope); $traceId = new TraceId($traceContext['trace_id']); $spanId = new SpanId($traceContext['span_id']); @@ -113,12 +105,10 @@ public function add( /** @var Metric $metric */ $metric = new $metricTypeClass($name, $value, $traceId, $spanId, $attributes, microtime(true), $unit); - if ($client !== null) { - $beforeSendMetric = $client->getOptions()->getBeforeSendMetricCallback(); - $metric = $beforeSendMetric($metric); - if ($metric === null) { - return; - } + $beforeSendMetric = $options->getBeforeSendMetricCallback(); + $metric = $beforeSendMetric($metric); + if ($metric === null) { + return; } $metrics = $this->getStorage($metricFlushThreshold); @@ -147,16 +137,14 @@ public function flush(?ClientInterface $client = null, ?Scope $isolationScope = /** * @return array{trace_id: string, span_id: string} */ - private function getTraceContext(HubInterface $hub): array + private function getTraceContext(Scope $scope): array { - $traceContext = null; - - $hub->configureScope(static function (Scope $scope) use (&$traceContext): void { - $traceContext = $scope->getTraceContext(); - }); + $traceContext = $scope->getTraceContext(); - /** @var array{trace_id: string, span_id: string} $traceContext */ - return $traceContext; + return [ + 'trace_id' => $traceContext['trace_id'], + 'span_id' => $traceContext['span_id'], + ]; } /** diff --git a/src/Tracing/PropagationContext.php b/src/Tracing/PropagationContext.php index e2d525d5d..721321d08 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,10 @@ 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); - }); + $this->dynamicSamplingContext = DynamicSamplingContext::fromOptions( + SentrySdk::getClient()->getOptions(), + SentrySdk::getIsolationScope() + ); } return (string) $this->dynamicSamplingContext; diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index e8df25b88..faebe8a47 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -6,7 +6,6 @@ use Sentry\EventId; use Sentry\SentrySdk; -use Sentry\State\Scope; /** * This class stores all the information about a span. @@ -299,11 +298,9 @@ public function setStatus(?SpanStatus $status) */ public function setHttpStatus(int $statusCode) { - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($statusCode) { - $scope->setContext('response', [ - 'status_code' => $statusCode, - ]); - }); + SentrySdk::getIsolationScope()->setContext('response', [ + 'status_code' => $statusCode, + ]); $status = SpanStatus::createFromHttpStatusCode($statusCode); diff --git a/src/Tracing/Traits/TraceHeaderParserTrait.php b/src/Tracing/Traits/TraceHeaderParserTrait.php index 3accc180b..69933f22b 100644 --- a/src/Tracing/Traits/TraceHeaderParserTrait.php +++ b/src/Tracing/Traits/TraceHeaderParserTrait.php @@ -127,8 +127,7 @@ private static function parseSampleRand(DynamicSamplingContext $samplingContext) } } - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $client->getOptions()->getLoggerOrNullLogger()->debug( 'Ignoring invalid sentry-sample_rand baggage value because it must be a numeric value in the range [0, 1).', ['sample_rand' => $sampleRand] @@ -139,8 +138,7 @@ private static function parseSampleRand(DynamicSamplingContext $samplingContext) private static function shouldContinueTrace(DynamicSamplingContext $samplingContext): bool { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $options = $client->getOptions(); $clientOrgId = $options->getOrgId(); diff --git a/src/UserDataBag.php b/src/UserDataBag.php index 5fecb478c..bff811885 100644 --- a/src/UserDataBag.php +++ b/src/UserDataBag.php @@ -195,13 +195,9 @@ public function setIpAddress(?string $ipAddress): self } if (filter_var($ipAddress, \FILTER_VALIDATE_IP) === false) { - $client = SentrySdk::getCurrentHub()->getClient(); - - if ($client !== null) { - $client->getOptions()->getLoggerOrNullLogger()->debug( - \sprintf('The "%s" value is not a valid IP address.', $ipAddress) - ); - } + SentrySdk::getClient()->getOptions()->getLoggerOrNullLogger()->debug( + \sprintf('The "%s" value is not a valid IP address.', $ipAddress) + ); return $this; } diff --git a/src/functions.php b/src/functions.php index 9d3cd51e9..eea47629f 100644 --- a/src/functions.php +++ b/src/functions.php @@ -20,7 +20,7 @@ use Sentry\Transport\TransportInterface; /** - * Creates a new Client and Hub which will be set as current. + * Creates a new Client and initializes the SDK. * * @param array{ * attach_stacktrace?: bool, @@ -213,7 +213,7 @@ function addBreadcrumb($category, ?string $message = null, array $metadata = [], */ function configureScope(callable $callback): void { - SentrySdk::getCurrentHub()->configureScope($callback); + $callback(SentrySdk::getIsolationScope()); } /** @@ -330,7 +330,7 @@ function startTransaction(TransactionContext $context, array $customSamplingCont */ function trace(callable $trace, SpanContext $context) { - return SentrySdk::getCurrentHub()->withScope(static function (Scope $scope) use ($context, $trace) { + return withIsolationScope(static function (Scope $scope) use ($context, $trace) { $parentSpan = $scope->getSpan(); $span = null; @@ -360,10 +360,9 @@ function trace(callable $trace, SpanContext $context) */ function getOtlpTracesEndpointUrl(): ?string { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); - $integration = $hub->getIntegration(OTLPIntegration::class); + $integration = $client->getIntegration(OTLPIntegration::class); if ($integration instanceof OTLPIntegration && $integration->getCollectorUrl() !== null) { return $integration->getCollectorUrl(); } @@ -384,27 +383,22 @@ function getOtlpTracesEndpointUrl(): ?string */ function getTraceparent(): string { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $options = $client->getOptions(); + $scope = SentrySdk::getIsolationScope(); if ($options->isTracingEnabled()) { - $span = SentrySdk::getCurrentHub()->getSpan(); + $span = $scope->getSpan(); if ($span !== null) { return $span->toTraceparent(); } } - $traceParent = ''; - $hub->configureScope(static function (Scope $scope) use (&$traceParent) { - if ($scope->hasExternalPropagationContext()) { - return; - } - - $traceParent = $scope->getPropagationContext()->toTraceparent(); - }); + if ($scope->hasExternalPropagationContext()) { + return ''; + } - return $traceParent; + return $scope->getPropagationContext()->toTraceparent(); } /** @@ -415,27 +409,22 @@ function getTraceparent(): string */ function getBaggage(): string { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $options = $client->getOptions(); + $scope = SentrySdk::getIsolationScope(); if ($options->isTracingEnabled()) { - $span = SentrySdk::getCurrentHub()->getSpan(); + $span = $scope->getSpan(); if ($span !== null) { return $span->toBaggage(); } } - $baggage = ''; - $hub->configureScope(static function (Scope $scope) use (&$baggage) { - if ($scope->hasExternalPropagationContext()) { - return; - } - - $baggage = $scope->getPropagationContext()->toBaggage(); - }); + if ($scope->hasExternalPropagationContext()) { + return ''; + } - return $baggage; + return $scope->getPropagationContext()->toBaggage(); } /** @@ -466,10 +455,7 @@ function continueTrace(string $sentryTrace, string $baggage): TransactionContext $propagationContext->setDynamicSamplingContext($dynamicSamplingContext); } - $hub = SentrySdk::getCurrentHub(); - $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); + SentrySdk::getIsolationScope()->setPropagationContext($propagationContext); return $transactionContext; } @@ -498,9 +484,7 @@ function traceMetrics(): TraceMetrics */ function addFeatureFlag(string $name, bool $result): void { - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($name, $result) { - $scope->addFeatureFlag($name, $result); - }); + SentrySdk::getIsolationScope()->addFeatureFlag($name, $result); } /** diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 577951b91..e3d163282 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -19,7 +19,6 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\Span; @@ -33,6 +32,7 @@ use Sentry\Util\SentryUid; use function Sentry\addBreadcrumb; +use function Sentry\addFeatureFlag; use function Sentry\captureCheckIn; use function Sentry\captureEvent; use function Sentry\captureException; @@ -516,15 +516,63 @@ public function testWithScope(): void $this->assertSame('foobarbaz', $returnValue); } - public function testConfigureScope(): void + public function testConfigureScopeMutatesCurrentIsolationScopeOnly(): void { - $callbackInvoked = false; + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setTag('scope', 'global'); + + $isolationScope = new Scope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); + + $callbackScope = null; - configureScope(static function () use (&$callbackInvoked): void { - $callbackInvoked = true; + configureScope(static function (Scope $scope) use (&$callbackScope): void { + $callbackScope = $scope; + $scope->setTag('scope', 'isolation'); }); - $this->assertTrue($callbackInvoked); + $this->assertSame($isolationScope, $callbackScope); + + $isolationEvent = $isolationScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($isolationEvent); + $this->assertSame(['scope' => 'isolation'], $isolationEvent->getTags()); + + $globalEvent = $globalScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($globalEvent); + $this->assertSame(['scope' => 'global'], $globalEvent->getTags()); + } + + public function testAddFeatureFlagMutatesCurrentIsolationScopeOnly(): void + { + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->addFeatureFlag('global-only', true); + + $isolationScope = new Scope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); + + addFeatureFlag('isolation-only', false); + + $isolationEvent = $isolationScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($isolationEvent); + $this->assertSame([ + 'values' => [ + [ + 'flag' => 'isolation-only', + 'result' => false, + ], + ], + ], $isolationEvent->getContexts()['flags']); + + $globalEvent = $globalScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($globalEvent); + $this->assertSame([ + 'values' => [ + [ + 'flag' => 'global-only', + 'result' => true, + ], + ], + ], $globalEvent->getContexts()['flags']); } public function testStartAndEndContext(): void @@ -653,46 +701,58 @@ public function testTraceCorrectlyReplacesAndRestoresCurrentSpan(): void { $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(true); + $outerScope = SentrySdk::getIsolationScope(); + $outerScope->setSpan($transaction); - SentrySdk::getCurrentHub()->setSpan($transaction); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); + $childSpan = null; - trace(function (Scope $scope) use ($transaction) { - $this->assertNotSame($transaction, $scope->getSpan()); + trace(function (Scope $scope) use ($outerScope, $transaction, &$childSpan): void { + $childSpan = $scope->getSpan(); + + $this->assertNotSame($outerScope, $scope); + $this->assertNotSame($transaction, $childSpan); + $this->assertSame($childSpan, SentrySdk::getIsolationScope()->getSpan()); + $this->assertNull($childSpan->getEndTimestamp()); }, new SpanContext()); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); + $this->assertNotNull($childSpan); + $this->assertNotNull($childSpan->getEndTimestamp()); + $this->assertSame($outerScope, SentrySdk::getIsolationScope()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); try { - trace(static function () { + trace(function (Scope $scope) use ($transaction): void { + $this->assertNotSame($transaction, $scope->getSpan()); + throw new \RuntimeException('Throwing should still restore the previous span'); }, new SpanContext()); } catch (\RuntimeException $e) { - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); + $this->assertSame($outerScope, SentrySdk::getIsolationScope()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); } } public function testTraceDoesntCreateSpanIfTransactionIsNotSampled(): void { - $scope = $this->createMock(Scope::class); - $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(false); - $scope->expects($this->never()) - ->method('setSpan'); - $scope->expects($this->exactly(3)) - ->method('getSpan') - ->willReturn($transaction); + $outerScope = SentrySdk::getIsolationScope(); + $outerScope->setSpan($transaction); + $callbackScope = null; - SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); + trace(function (Scope $scope) use ($transaction, &$callbackScope): void { + $callbackScope = $scope; - trace(function () use ($transaction) { - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); + $this->assertSame($transaction, $scope->getSpan()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); }, SpanContext::make()); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); + $this->assertNotSame($outerScope, $callbackScope); + $this->assertSame($outerScope, SentrySdk::getIsolationScope()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); } public function testTraceparentWithTracingDisabled(): void @@ -717,7 +777,7 @@ public function testTraceparentWithTracingEnabled(): void 'traces_sample_rate' => 1.0, ])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getGlobalScope()->setClient($client); $spanContext = (new SpanContext()) ->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')) @@ -725,7 +785,7 @@ public function testTraceparentWithTracingEnabled(): void $span = new Span($spanContext); - SentrySdk::getCurrentHub()->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); $traceParent = getTraceparent(); @@ -767,12 +827,13 @@ public function testBaggageWithTracingDisabled(): void 'environment' => 'development', ])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getGlobalScope()->setClient($client); SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); $baggage = getBaggage(); $this->assertSame('sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.25,sentry-release=1.0.0,sentry-environment=development', $baggage); + $this->assertNotNull($propagationContext->getDynamicSamplingContext()); } public function testBaggageWithTracingEnabled(): void @@ -799,7 +860,7 @@ public function testBaggageWithTracingEnabled(): void $span = $transaction->startChild($spanContext); - SentrySdk::getCurrentHub()->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); $baggage = getBaggage(); @@ -819,7 +880,7 @@ public function testGetOtlpTracesEndpointUrlFallsBackToDsn(): void 'dsn' => 'https://public@example.com/1', ])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame('https://example.com/api/1/integration/otlp/v1/traces/', getOtlpTracesEndpointUrl()); } @@ -838,16 +899,17 @@ public function testGetOtlpTracesEndpointUrlPrefersCollectorUrl(): void 'dsn' => 'https://public@example.com/1', ])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame('http://collector:4318/v1/traces', getOtlpTracesEndpointUrl()); } public function testContinueTrace(): void { - $hub = new Hub(new NoOpClient()); + SentrySdk::getGlobalScope()->setClient(new NoOpClient()); - SentrySdk::setCurrentHub($hub); + $scope = new Scope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -858,17 +920,15 @@ public function testContinueTrace(): void $this->assertSame('566e3688a61d4bc8', (string) $transactionContext->getParentSpanId()); $this->assertTrue($transactionContext->getParentSampled()); - configureScope(function (Scope $scope): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = $scope->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()); } public function testContinueTraceWhenOrgMismatch(): void @@ -881,8 +941,10 @@ public function testContinueTraceWhenOrgMismatch(): void 'org_id' => 1, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new Scope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -899,14 +961,12 @@ public function testContinueTraceWhenOrgMismatch(): void $this->assertNull($transactionContext->getMetadata()->getDynamicSamplingContext()); $this->assertNotNull($newSampleRand); - configureScope(function (Scope $scope) use ($newTraceId, $newSampleRand): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = $scope->getPropagationContext(); - $this->assertSame($newTraceId, (string) $propagationContext->getTraceId()); - $this->assertNull($propagationContext->getParentSpanId()); - $this->assertNull($propagationContext->getDynamicSamplingContext()); - $this->assertSame($newSampleRand, $propagationContext->getSampleRand()); - }); + $this->assertSame($newTraceId, (string) $propagationContext->getTraceId()); + $this->assertNull($propagationContext->getParentSpanId()); + $this->assertNull($propagationContext->getDynamicSamplingContext()); + $this->assertSame($newSampleRand, $propagationContext->getSampleRand()); } public function testContinueTraceWhenOrgMatch(): void @@ -919,8 +979,10 @@ public function testContinueTraceWhenOrgMatch(): void 'org_id' => 1, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new Scope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -931,17 +993,15 @@ public function testContinueTraceWhenOrgMatch(): void $this->assertSame('566e3688a61d4bc8', (string) $transactionContext->getParentSpanId()); $this->assertTrue($transactionContext->getParentSampled()); - configureScope(function (Scope $scope): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = $scope->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->assertNotNull($dynamicSamplingContext); - $this->assertSame('1', $dynamicSamplingContext->get('org_id')); - }); + $this->assertNotNull($dynamicSamplingContext); + $this->assertSame('1', $dynamicSamplingContext->get('org_id')); } private function setClientAndIsolationScope(ClientInterface $client): Scope diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 4f85f4e62..b604f54db 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -213,6 +213,31 @@ public function testAttributesAreAddedToLogMessage(): void $this->assertSame('my_user', $attributes->get('user.name')->getValue()); } + public function testMergedScopeAttributesAreAddedToLogMessage(): void + { + $client = ClientBuilder::create([ + 'enable_logs' => true, + ])->getClient(); + + SentrySdk::getGlobalScope()->setClient($client); + SentrySdk::getGlobalScope()->setUser(UserDataBag::createFromUserIdentifier('global-user')); + + $spanContext = new SpanContext(); + $spanContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); + $spanContext->setSpanId(new SpanId('566e3688a61d4bc8')); + SentrySdk::getIsolationScope()->setSpan(new Span($spanContext)); + + $aggregator = new LogsAggregator(); + $aggregator->add(LogLevel::info(), 'Test message'); + + $logs = $aggregator->all(); + $this->assertCount(1, $logs); + + $attributes = $logs[0]->attributes(); + $this->assertSame('global-user', $attributes->get('user.id')->getValue()); + $this->assertSame('566e3688a61d4bc8', $attributes->get('sentry.trace.parent_span_id')->getValue()); + } + public function testUserAttributesCanBeSetManuallyWithDefaultPiiOff(): void { $client = ClientBuilder::create([ diff --git a/tests/Metrics/TraceMetricsTest.php b/tests/Metrics/TraceMetricsTest.php index bb5773699..3d77e3706 100644 --- a/tests/Metrics/TraceMetricsTest.php +++ b/tests/Metrics/TraceMetricsTest.php @@ -18,6 +18,7 @@ use Sentry\State\Hub; use Sentry\State\HubAdapter; use Sentry\State\Scope; +use Sentry\UserDataBag; use function Sentry\traceMetrics; @@ -114,6 +115,19 @@ public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): voi $this->assertCount(2, StubTransport::$events[0]->getMetrics()); } + public function testMergedScopeAttributesAreAddedToMetric(): void + { + SentrySdk::getGlobalScope()->setUser(UserDataBag::createFromUserIdentifier('global-user')); + + traceMetrics()->count('test-count', 2); + traceMetrics()->flush(); + + $this->assertCount(1, StubTransport::$events); + + $metric = StubTransport::$events[0]->getMetrics()[0]; + $this->assertSame('global-user', $metric->getAttributes()->get('user.id')->getValue()); + } + public function testFlushCapturesMetricsWithProvidedClient(): void { $client = $this->createMock(ClientInterface::class); diff --git a/tests/Tracing/PropagationContextTest.php b/tests/Tracing/PropagationContextTest.php index beefa0cb1..76484896f 100644 --- a/tests/Tracing/PropagationContextTest.php +++ b/tests/Tracing/PropagationContextTest.php @@ -5,7 +5,9 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; +use Sentry\ClientInterface; use Sentry\Options; +use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; @@ -113,6 +115,29 @@ public function testToBaggage(): void $this->assertSame('sentry-trace_id=566e3688a61d4bc888951642d6f14a19', $propagationContext->toBaggage()); } + public function testToBaggageUsesCurrentClientAndIsolationScope(): void + { + $propagationContext = PropagationContext::fromDefaults(); + $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); + $propagationContext->setSampleRand(0.25); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'release' => '1.0.0', + 'environment' => 'development', + ])); + + SentrySdk::getGlobalScope()->setClient($client); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); + + $this->assertSame( + 'sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.25,sentry-release=1.0.0,sentry-environment=development', + $propagationContext->toBaggage() + ); + } + public function testGetTraceContext(): void { $propagationContext = PropagationContext::fromDefaults(); diff --git a/tests/Tracing/SpanTest.php b/tests/Tracing/SpanTest.php index cf8956172..1de0abf6d 100644 --- a/tests/Tracing/SpanTest.php +++ b/tests/Tracing/SpanTest.php @@ -5,6 +5,9 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; +use Sentry\Event; +use Sentry\SentrySdk; +use Sentry\State\Scope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; @@ -73,6 +76,28 @@ public function testStartChild(): void $this->assertSame($spanContext1->getTraceId(), $span2->getTraceId()); } + public function testSetHttpStatusWritesResponseContextToCurrentIsolationScopeOnly(): void + { + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setContext('response', [ + 'status_code' => 201, + ]); + + $isolationScope = new Scope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); + + $span = new Span(); + $span->setHttpStatus(404); + + $isolationEvent = $isolationScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($isolationEvent); + $this->assertSame(404, $isolationEvent->getContexts()['response']['status_code']); + + $globalEvent = $globalScope->applyToEvent(Event::createEvent()); + $this->assertNotNull($globalEvent); + $this->assertSame(201, $globalEvent->getContexts()['response']['status_code']); + } + /** * @dataProvider toTraceparentDataProvider */ From ee8a6f9b42d2252bba54aafd86b5ceb131f9bdc8 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:49 +0200 Subject: [PATCH 13/20] feat(scopes): remove hub usage in handler and middleware (#2134) --- src/ClientReport/ClientReportAggregator.php | 22 ++-- .../AbstractErrorListenerIntegration.php | 15 ++- src/Integration/ErrorListenerIntegration.php | 10 +- .../ExceptionListenerIntegration.php | 8 +- .../FatalErrorListenerIntegration.php | 10 +- src/Monolog/BreadcrumbHandler.php | 24 ++-- src/Monolog/ExceptionToSentryIssueHandler.php | 17 +-- src/Monolog/LogToSentryIssueHandler.php | 16 +-- src/Tracing/GuzzleTracingMiddleware.php | 30 ++--- src/functions.php | 12 +- .../ClientReportAggregatorTest.php | 18 +-- tests/Monolog/BreadcrumbHandlerTest.php | 42 ++++--- .../ExceptionToSentryIssueHandlerTest.php | 24 ++-- tests/Monolog/LogToSentryIssueHandlerTest.php | 30 +++-- tests/Tracing/GuzzleTracingMiddlewareTest.php | 114 ++++++++++++------ ...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 +- 26 files changed, 229 insertions(+), 185 deletions(-) diff --git a/src/ClientReport/ClientReportAggregator.php b/src/ClientReport/ClientReportAggregator.php index 8045a51b2..454b0ba26 100644 --- a/src/ClientReport/ClientReportAggregator.php +++ b/src/ClientReport/ClientReportAggregator.php @@ -5,7 +5,7 @@ namespace Sentry\ClientReport; use Sentry\Event; -use Sentry\State\HubAdapter; +use Sentry\SentrySdk; use Sentry\Transport\DataCategory; class ClientReportAggregator @@ -35,15 +35,13 @@ public function add(DataCategory $category, Reason $reason, int $quantity): void $category = $category->getValue(); $reason = $reason->getValue(); if ($quantity <= 0) { - $client = HubAdapter::getInstance()->getClient(); - if ($client !== null) { - $logger = $client->getOptions()->getLoggerOrNullLogger(); - $logger->debug('Dropping Client report with category={category} and reason={reason} because quantity is zero or negative ({quantity})', [ - 'category' => $category, - 'reason' => $reason, - 'quantity' => $quantity, - ]); - } + $client = SentrySdk::getClient(); + $logger = $client->getOptions()->getLoggerOrNullLogger(); + $logger->debug('Dropping Client report with category={category} and reason={reason} because quantity is zero or negative ({quantity})', [ + 'category' => $category, + 'reason' => $reason, + 'quantity' => $quantity, + ]); return; } @@ -64,11 +62,11 @@ public function flush(): void $event = Event::createClientReport(); $event->setClientReports($reports); - $client = HubAdapter::getInstance()->getClient(); + $client = SentrySdk::getClient(); // Reset the client reports only if we successfully sent an event. If it fails it // can be sent on the next flush, or it gets discarded anyway. - if ($client !== null && $client->captureEvent($event) !== null) { + if ($client->captureEvent($event) !== null) { $this->reports = []; } } diff --git a/src/Integration/AbstractErrorListenerIntegration.php b/src/Integration/AbstractErrorListenerIntegration.php index a8894a7e2..97123b113 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\State\EventCapturer; use Sentry\State\Scope; +use function Sentry\withIsolationScope; + abstract class AbstractErrorListenerIntegration implements IntegrationInterface { /** - * Captures the exception using the given hub instance. - * - * @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 { + withIsolationScope(function (Scope $scope) use ($exception): void { $scope->addEventProcessor(\Closure::fromCallable([$this, 'addExceptionMechanismToEvent'])); - $hub->captureException($exception); + EventCapturer::captureException($exception); }); } diff --git a/src/Integration/ErrorListenerIntegration.php b/src/Integration/ErrorListenerIntegration.php index a4b95c2ba..fa33cd2a0 100644 --- a/src/Integration/ErrorListenerIntegration.php +++ b/src/Integration/ErrorListenerIntegration.php @@ -33,24 +33,22 @@ 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; } - if (!$exception instanceof SilencedErrorException && !($client->getOptions()->getErrorTypes() & $exception->getSeverity())) { + if (!$exception instanceof SilencedErrorException && ($client->getOptions()->getErrorTypes() & $exception->getSeverity()) === 0) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); } ); } diff --git a/src/Integration/ExceptionListenerIntegration.php b/src/Integration/ExceptionListenerIntegration.php index 18e7afc75..c86ff02bb 100644 --- a/src/Integration/ExceptionListenerIntegration.php +++ b/src/Integration/ExceptionListenerIntegration.php @@ -20,16 +20,14 @@ 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 if ($integration === null) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); }); } } diff --git a/src/Integration/FatalErrorListenerIntegration.php b/src/Integration/FatalErrorListenerIntegration.php index 3cc056668..e4b53c279 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())) { + if (($client->getOptions()->getErrorTypes() & $exception->getSeverity()) === 0) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); }); } } diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index 29e166671..7197db202 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -11,7 +11,8 @@ use Psr\Log\LogLevel; use Sentry\Breadcrumb; use Sentry\Event; -use Sentry\State\HubInterface; +use Sentry\SentrySdk; +use Sentry\State\BreadcrumbRecorder; use Sentry\State\Scope; /** @@ -21,23 +22,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); } @@ -66,7 +59,8 @@ protected function write($record): void $timestamp ); - $this->hub->addBreadcrumb($breadcrumb); + $scope = SentrySdk::getIsolationScope(); + BreadcrumbRecorder::record(SentrySdk::getClient($scope), $scope, $breadcrumb); } /** diff --git a/src/Monolog/ExceptionToSentryIssueHandler.php b/src/Monolog/ExceptionToSentryIssueHandler.php index b2e7abeb6..4ab13e89d 100644 --- a/src/Monolog/ExceptionToSentryIssueHandler.php +++ b/src/Monolog/ExceptionToSentryIssueHandler.php @@ -9,26 +9,21 @@ use Monolog\Logger; use Monolog\LogRecord; use Psr\Log\LogLevel; -use Sentry\State\HubInterface; +use Sentry\State\EventCapturer; use Sentry\State\Scope; +use function Sentry\withIsolationScope; + /** * This Monolog handler will collect monolog events and send them to sentry. */ class ExceptionToSentryIssueHandler extends AbstractHandler { - /** - * @var HubInterface - */ - private $hub; - /** * @phpstan-param value-of|value-of|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); } @@ -44,7 +39,7 @@ public function handle($record): bool return false; } - $this->hub->withScope(function (Scope $scope) use ($record, $exception): void { + withIsolationScope(function (Scope $scope) use ($record, $exception): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); $scope->setExtra('monolog.message', $record['message']); @@ -61,7 +56,7 @@ public function handle($record): bool $scope->setExtra('monolog.extra', $monologExtraData); } - $this->hub->captureException($exception); + EventCapturer::captureException($exception); }); return $this->bubble === false; diff --git a/src/Monolog/LogToSentryIssueHandler.php b/src/Monolog/LogToSentryIssueHandler.php index 18dd6eab6..e7c8d9b44 100644 --- a/src/Monolog/LogToSentryIssueHandler.php +++ b/src/Monolog/LogToSentryIssueHandler.php @@ -11,9 +11,11 @@ use Psr\Log\LogLevel; use Sentry\Event; use Sentry\EventHint; -use Sentry\State\HubInterface; +use Sentry\State\EventCapturer; use Sentry\State\Scope; +use function Sentry\withIsolationScope; + /** * This Monolog handler captures log messages as Sentry issues. */ @@ -23,11 +25,6 @@ class LogToSentryIssueHandler extends AbstractProcessingHandler private const CONTEXT_EXCEPTION_KEY = 'exception'; - /** - * @var HubInterface - */ - private $hub; - /** * @var bool */ @@ -36,9 +33,8 @@ class LogToSentryIssueHandler extends AbstractProcessingHandler /** * @phpstan-param value-of|value-of|Level|LogLevel::* $level */ - 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) { - $this->hub = $hub; $this->fillExtraContext = $fillExtraContext; parent::__construct($level, $bubble); @@ -70,7 +66,7 @@ protected function doWrite($record): void $hint = new EventHint(); - $this->hub->withScope(function (Scope $scope) use ($record, $event, $hint): void { + withIsolationScope(function (Scope $scope) use ($record, $event, $hint): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); @@ -88,7 +84,7 @@ protected function doWrite($record): void } } - $this->hub->captureEvent($event, $hint); + EventCapturer::captureEvent($event, $hint); }); } diff --git a/src/Tracing/GuzzleTracingMiddleware.php b/src/Tracing/GuzzleTracingMiddleware.php index 8c14fe0b0..27cdca32a 100644 --- a/src/Tracing/GuzzleTracingMiddleware.php +++ b/src/Tracing/GuzzleTracingMiddleware.php @@ -11,7 +11,8 @@ use Sentry\Breadcrumb; use Sentry\ClientInterface; use Sentry\SentrySdk; -use Sentry\State\HubInterface; +use Sentry\State\BreadcrumbRecorder; +use Sentry\State\Scope; use function Sentry\getBaggage; use function Sentry\getTraceparent; @@ -21,13 +22,13 @@ */ final class GuzzleTracingMiddleware { - public static function trace(?HubInterface $hub = null): \Closure + public static function trace(?Scope $scope = null): \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) use ($scope): \Closure { + return static function (RequestInterface $request, array $options) use ($handler, $scope) { + $scope = $scope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($scope); + $parentSpan = $scope->getSpan(); $partialUri = Uri::fromParts([ 'scheme' => $request->getUri()->getScheme(), @@ -59,28 +60,27 @@ public static function trace(?HubInterface $hub = null): \Closure $childSpan = $parentSpan->startChild($spanContext); - $hub->setSpan($childSpan); + $scope->setSpan($childSpan); } if (self::shouldAttachTracingHeaders($client, $request)) { - $traceParent = getTraceparent(); + $traceParent = getTraceparent($scope); if ($traceParent !== '') { $request = $request->withHeader('sentry-trace', $traceParent); } - $baggage = getBaggage(); + $baggage = getBaggage($scope); if ($baggage !== '') { $request = $request->withHeader('baggage', $baggage); } } - $handlerPromiseCallback = static function ($responseOrException) use ($hub, $spanAndBreadcrumbData, $childSpan, $parentSpan, $partialUri) { + $handlerPromiseCallback = static function ($responseOrException) use ($client, $scope, $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 + // We finish the span first to keep the measured duration as close as possible to the HTTP request time. $childSpan->finish(); - $hub->setSpan($parentSpan); + $scope->setSpan($parentSpan); } $response = null; @@ -113,7 +113,7 @@ public static function trace(?HubInterface $hub = null): \Closure } } - $hub->addBreadcrumb(new Breadcrumb( + BreadcrumbRecorder::record($client, $scope, new Breadcrumb( $breadcrumbLevel, Breadcrumb::TYPE_HTTP, 'http', diff --git a/src/functions.php b/src/functions.php index eea47629f..9db1674d4 100644 --- a/src/functions.php +++ b/src/functions.php @@ -381,11 +381,11 @@ function getOtlpTracesEndpointUrl(): ?string * This function is context aware, as in it either returns the traceparent based * on the current span, or the scope's propagation context. */ -function getTraceparent(): string +function getTraceparent(?Scope $scope = null): string { - $client = SentrySdk::getClient(); + $scope = $scope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($scope); $options = $client->getOptions(); - $scope = SentrySdk::getIsolationScope(); if ($options->isTracingEnabled()) { $span = $scope->getSpan(); @@ -407,11 +407,11 @@ function getTraceparent(): string * This function is context aware, as in it either returns the baggage based * on the current span or the scope's propagation context. */ -function getBaggage(): string +function getBaggage(?Scope $scope = null): string { - $client = SentrySdk::getClient(); + $scope = $scope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($scope); $options = $client->getOptions(); - $scope = SentrySdk::getIsolationScope(); if ($options->isTracingEnabled()) { $span = $scope->getSpan(); diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php index 7766c7ea5..9c117a3cb 100644 --- a/tests/ClientReport/ClientReportAggregatorTest.php +++ b/tests/ClientReport/ClientReportAggregatorTest.php @@ -11,11 +11,12 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tests\StubLogger; use Sentry\Tests\StubTransport; use Sentry\Transport\DataCategory; +use function Sentry\captureMessage; + class ClientReportAggregatorTest extends TestCase { protected function setUp(): void @@ -23,7 +24,7 @@ protected function setUp(): void ini_set('zend.exception_ignore_args', '0'); StubTransport::$events = []; StubLogger::$logs = []; - SentrySdk::init()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'logger' => StubLogger::getInstance(), ]), StubTransport::getInstance())); } @@ -69,16 +70,15 @@ public function testClientReportAggregation(): void public function testFlushDoesNotOverwriteLastEventId(): void { - $hub = SentrySdk::getCurrentHub(); - $eventId = $hub->captureMessage('foo'); + $eventId = captureMessage('foo'); $this->assertNotNull($eventId); - $this->assertSame($eventId, $hub->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), 10); ClientReportAggregator::getInstance()->flush(); - $this->assertSame($eventId, $hub->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); } public function testNegativeQuantityDiscarded(): void @@ -103,13 +103,13 @@ public function testZeroQuantityDiscarded(): void public function testNegativeQuantityDiscardedWhenNoClientIsBound(): void { - SentrySdk::setCurrentHub(new Hub(new NoOpClient())); + SentrySdk::init(new NoOpClient()); ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), -10); - SentrySdk::setCurrentHub(new Hub(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'logger' => StubLogger::getInstance(), - ]), StubTransport::getInstance()))); + ]), StubTransport::getInstance())); ClientReportAggregator::getInstance()->flush(); diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index 812c5d12f..55c6a085e 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -8,8 +8,11 @@ use Monolog\LogRecord; use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; +use Sentry\ClientInterface; +use Sentry\Event; use Sentry\Monolog\BreadcrumbHandler; -use Sentry\State\HubInterface; +use Sentry\Options; +use Sentry\SentrySdk; final class BreadcrumbHandlerTest extends TestCase { @@ -20,22 +23,29 @@ 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): bool { - $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); - $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); - $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); - $this->assertEquals($expectedBreadcrumb->getTimestamp(), $breadcrumb->getTimestamp()); - $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); - $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); - - return true; - })); - - $handler = new BreadcrumbHandler($hub); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); + + SentrySdk::init($client); + + $handler = new BreadcrumbHandler(); $handler->handle($record); + + $event = Event::createEvent(); + SentrySdk::getIsolationScope()->applyToEvent($event); + + $this->assertCount(1, $event->getBreadcrumbs()); + + $breadcrumb = $event->getBreadcrumbs()[0]; + + $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); + $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); + $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); + $this->assertEquals($expectedBreadcrumb->getTimestamp(), $breadcrumb->getTimestamp()); + $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); + $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); } /** diff --git a/tests/Monolog/ExceptionToSentryIssueHandlerTest.php b/tests/Monolog/ExceptionToSentryIssueHandlerTest.php index 20dd68139..839c2db54 100644 --- a/tests/Monolog/ExceptionToSentryIssueHandlerTest.php +++ b/tests/Monolog/ExceptionToSentryIssueHandlerTest.php @@ -11,7 +11,7 @@ use Sentry\ClientInterface; use Sentry\Event; use Sentry\Monolog\ExceptionToSentryIssueHandler; -use Sentry\State\Hub; +use Sentry\SentrySdk; use Sentry\State\Scope; final class ExceptionToSentryIssueHandlerTest extends TestCase @@ -41,7 +41,9 @@ public function testHandleCapturesExceptionAndAddsMetadata($record, \Throwable $ null ); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope())); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(); $this->assertTrue($handler->isHandling($record)); $handler->handle($record); @@ -57,7 +59,9 @@ public function testHandleReturnsFalseWhenBubblingEnabled(): void ->method('captureException') ->with($this->identicalTo($exception), $this->isInstanceOf(Scope::class), null); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::WARNING); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(Logger::WARNING); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -82,7 +86,9 @@ public function testHandleReturnsTrueWhenBubblingDisabled(): void ->method('captureException') ->with($this->identicalTo($exception), $this->isInstanceOf(Scope::class), null); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::WARNING, false); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(Logger::WARNING, false); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -109,7 +115,9 @@ public function testHandleIgnoresRecordsWithoutThrowable($record): void $client->expects($this->never()) ->method('captureException'); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, false); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(Logger::DEBUG, false); $this->assertTrue($handler->isHandling($record)); $this->assertFalse($handler->handle($record)); @@ -124,7 +132,9 @@ public function testHandleIgnoresRecordsBelowThreshold(): void $client->expects($this->never()) ->method('captureException'); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::ERROR, false); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(Logger::ERROR, false); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -145,7 +155,7 @@ public function testLegacyIsHandlingUsesMinimalLevelRecord(): void $this->markTestSkipped('Test only works for Monolog < 3'); } - $handler = new ExceptionToSentryIssueHandler(new Hub($this->createMock(ClientInterface::class), new Scope()), Logger::WARNING); + $handler = new ExceptionToSentryIssueHandler(Logger::WARNING); $this->assertTrue($handler->isHandling(['level' => Logger::WARNING])); $this->assertFalse($handler->isHandling(['level' => Logger::INFO])); diff --git a/tests/Monolog/LogToSentryIssueHandlerTest.php b/tests/Monolog/LogToSentryIssueHandlerTest.php index 7bbff023b..ec9a53d60 100644 --- a/tests/Monolog/LogToSentryIssueHandlerTest.php +++ b/tests/Monolog/LogToSentryIssueHandlerTest.php @@ -14,8 +14,8 @@ use Sentry\EventHint; use Sentry\Monolog\ExceptionToSentryIssueHandler; use Sentry\Monolog\LogToSentryIssueHandler; +use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tests\StubTransport; @@ -59,7 +59,9 @@ public function testHandleCapturesLogMessageAsIssue(bool $fillExtraContext, $rec }) ); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, true, $fillExtraContext); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::DEBUG, true, $fillExtraContext); $this->assertTrue($handler->isHandling($record)); $this->assertFalse($handler->handle($record)); @@ -73,7 +75,9 @@ public function testHandleReturnsTrueWhenBubblingDisabled(): void ->method('captureEvent') ->with($this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), $this->isInstanceOf(Scope::class)); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::WARNING, false); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::WARNING, false); $record = RecordFactory::create('foo bar', Logger::WARNING, 'channel.foo', [], []); $this->assertTrue($handler->isHandling($record)); @@ -87,7 +91,9 @@ public function testHandleIgnoresRecordsWithThrowableExceptionContext(): void $client->expects($this->never()) ->method('captureEvent'); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, false); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::DEBUG, false); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -127,7 +133,9 @@ public function testHandleCapturesRecordsWithNonThrowableExceptionContext(): voi }) ); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, false, true); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::DEBUG, false, true); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -149,7 +157,9 @@ public function testHandleIgnoresRecordsBelowThreshold(): void $client->expects($this->never()) ->method('captureEvent'); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::ERROR, false); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::ERROR, false); $record = RecordFactory::create('foo bar', Logger::WARNING, 'channel.foo', [], []); $this->assertFalse($handler->isHandling($record)); @@ -162,7 +172,7 @@ public function testLegacyIsHandlingUsesMinimalLevelRecord(): void $this->markTestSkipped('Test only works for Monolog < 3'); } - $handler = new LogToSentryIssueHandler(new Hub($this->createMock(ClientInterface::class), new Scope()), Logger::WARNING); + $handler = new LogToSentryIssueHandler(Logger::WARNING); $this->assertTrue($handler->isHandling(['level' => Logger::WARNING])); $this->assertFalse($handler->isHandling(['level' => Logger::INFO])); @@ -173,11 +183,11 @@ public function testLogAndExceptionIssueHandlersReplaceLegacyHandlerUseCases(): $client = ClientBuilder::create() ->setTransport(StubTransport::getInstance()) ->getClient(); - $hub = new Hub($client, new Scope()); + SentrySdk::init($client); $logger = new Logger('channel.foo', [ - new LogToSentryIssueHandler($hub, Logger::WARNING, true, true), - new ExceptionToSentryIssueHandler($hub, Logger::WARNING), + new LogToSentryIssueHandler(Logger::WARNING, true, true), + new ExceptionToSentryIssueHandler(Logger::WARNING), ]); $logger->warning('plain warning', [ diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index becfa84a8..e0f3df44e 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -16,7 +16,6 @@ use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tracing\GuzzleTracingMiddleware; use Sentry\Tracing\SpanStatus; @@ -33,16 +32,15 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void 'traces_sample_rate' => 0, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $transaction = $hub->startTransaction(TransactionContext::make()); + $transaction = \Sentry\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); }); @@ -60,13 +58,11 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void $this->assertNull($transaction->getSpanRecorder()); - $hub->configureScope(function (Scope $scope): void { - $event = Event::createEvent(); + $event = Event::createEvent(); - $scope->applyToEvent($event); + SentrySdk::getIsolationScope()->applyToEvent($event); - $this->assertCount(1, $event->getBreadcrumbs()); - }); + $this->assertCount(1, $event->getBreadcrumbs()); } public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void @@ -78,16 +74,15 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void 'traces_sample_rate' => 1, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $transaction = $hub->startTransaction(TransactionContext::make()); + $transaction = \Sentry\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); }); @@ -106,13 +101,61 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void $this->assertNotNull($transaction->getSpanRecorder()); $this->assertCount(1, $transaction->getSpanRecorder()->getSpans()); - $hub->configureScope(function (Scope $scope): void { - $event = Event::createEvent(); + $event = Event::createEvent(); + + SentrySdk::getIsolationScope()->applyToEvent($event); - $scope->applyToEvent($event); + $this->assertCount(1, $event->getBreadcrumbs()); + } - $this->assertCount(1, $event->getBreadcrumbs()); + public function testTraceUsesProvidedIsolationScope(): void + { + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn(new Options([ + 'trace_propagation_targets' => [ + 'www.example.com', + ], + ])); + + SentrySdk::init($client); + + $currentScope = SentrySdk::getIsolationScope(); + + $providedScope = new Scope(); + $providedScope->setClient($client); + + $request = new Request('GET', 'https://www.example.com'); + $expectedPromiseResult = new Response(); + + $middleware = GuzzleTracingMiddleware::trace($providedScope); + $function = $middleware(function (Request $request) use ($expectedPromiseResult): PromiseInterface { + $this->assertNotEmpty($request->getHeader('sentry-trace')); + $this->assertNotEmpty($request->getHeader('baggage')); + + return new FulfilledPromise($expectedPromiseResult); }); + + /** @var PromiseInterface $promise */ + $promise = $function($request, []); + + try { + $promiseResult = $promise->wait(); + } catch (\Throwable $exception) { + $promiseResult = $exception; + } + + $this->assertSame($expectedPromiseResult, $promiseResult); + + $currentEvent = Event::createEvent(); + $currentScope->applyToEvent($currentEvent); + + $providedEvent = Event::createEvent(); + $providedScope->applyToEvent($providedEvent); + + $this->assertCount(0, $currentEvent->getBreadcrumbs()); + $this->assertCount(1, $providedEvent->getBreadcrumbs()); } /** @@ -125,12 +168,11 @@ public function testTraceHeaders(Request $request, Options $options, bool $heade ->method('getOptions') ->willReturn($options); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $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')); @@ -157,16 +199,15 @@ public function testTraceHeadersWithTransaction(Request $request, Options $optio ->method('getOptions') ->willReturn($options); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $transaction = $hub->startTransaction(new TransactionContext()); + $transaction = \Sentry\startTransaction(new TransactionContext()); - $hub->setSpan($transaction); + SentrySdk::getIsolationScope()->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')); @@ -201,11 +242,10 @@ public function testTraceHeadersAreNotAddedWhenExternalPropagationContextIsActiv 'trace_propagation_targets' => null, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult): PromiseInterface { $this->assertEmpty($request->getHeader('sentry-trace')); $this->assertEmpty($request->getHeader('baggage')); @@ -329,17 +369,15 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec ], ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); + $scope = SentrySdk::getIsolationScope(); $client->expects($this->once()) ->method('captureEvent') - ->with($this->callback(function (Event $eventArg) use ($hub, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { + ->with($this->callback(function (Event $eventArg) use ($scope, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { $this->assertSame(EventType::transaction(), $eventArg->getType()); - $hub->configureScope(static function (Scope $scope) use ($eventArg): void { - $scope->applyToEvent($eventArg); - }); + $scope->applyToEvent($eventArg); $spans = $eventArg->getSpans(); $breadcrumbs = $eventArg->getBreadcrumbs(); @@ -372,11 +410,11 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec return true; })); - $transaction = $hub->startTransaction(new TransactionContext()); + $transaction = \Sentry\startTransaction(new TransactionContext()); - $hub->setSpan($transaction); + $scope->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/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 fe1cf65e2..5fb864395 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 11ffa7c3c..b0179c188 100644 --- a/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt +++ b/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt @@ -50,7 +50,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 f6017b8a5..09e0ab241 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 592ebaa37..fd610d824 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 20d413ecd..db00f7b26 100644 --- a/tests/phpt/error_listener_integration_respects_error_types_option.phpt +++ b/tests/phpt/error_listener_integration_respects_error_types_option.phpt @@ -56,7 +56,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 9225fbafe..349c6e661 100644 --- a/tests/phpt/php84/error_handler_captures_fatal_error.phpt +++ b/tests/phpt/php84/error_handler_captures_fatal_error.phpt @@ -53,7 +53,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 7b76f6fed..9fafb9188 100644 --- a/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt +++ b/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt @@ -58,7 +58,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 56620b1c0..f215caeef 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 @@ -59,7 +59,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 03f77c589..594ad434a 100644 --- a/tests/phpt/php85/error_handler_captures_fatal_error.phpt +++ b/tests/phpt/php85/error_handler_captures_fatal_error.phpt @@ -53,7 +53,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 b6c62b636..8911b0b46 100644 --- a/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt +++ b/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt @@ -58,7 +58,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 dbf4e8356..6928a5de5 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 @@ -59,7 +59,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { From c0f98a84372c67fa3f8daf7b8516787408e4627b Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:50 +0200 Subject: [PATCH 14/20] feat(scopes): update tests to not use Hub (#2135) --- tests/FunctionsTest.php | 10 ++-- .../EnvironmentIntegrationTest.php | 2 +- .../FrameContextifierIntegrationTest.php | 4 +- tests/Integration/ModulesIntegrationTest.php | 4 +- tests/Integration/OTLPIntegrationTest.php | 5 +- tests/Integration/RequestIntegrationTest.php | 2 +- .../TransactionIntegrationTest.php | 2 +- tests/Logs/LogsAggregatorTest.php | 52 ++++++++----------- tests/Logs/LogsTest.php | 7 +-- tests/Metrics/TraceMetricsTest.php | 16 +++--- tests/Monolog/LogsHandlerTest.php | 7 +-- tests/SentrySdkTest.php | 52 +++++++------------ tests/Tracing/StrictTraceContinuationTest.php | 5 +- tests/Tracing/TransactionContextTest.php | 5 +- tests/Tracing/TransactionTest.php | 11 ++-- ...ry_fatal_error_increases_memory_limit.phpt | 2 +- ...ry_fatal_error_increases_memory_limit.phpt | 2 +- tests/phpt/test_callable_serialization.phpt | 6 ++- 18 files changed, 85 insertions(+), 109 deletions(-) diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index e3d163282..6e3984c91 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -58,8 +58,10 @@ public function testInit(): void { init(['default_integrations' => false]); - $this->assertNotNull(SentrySdk::getCurrentHub()->getClient()); - $this->assertSame(SentrySdk::getCurrentHub()->getClient(), SentrySdk::getClient()); + $client = SentrySdk::getClient(); + + $this->assertNotInstanceOf(NoOpClient::class, $client); + $this->assertSame($client, SentrySdk::getGlobalScope()->getClient()); } public function testInitPreservesGlobalScope(): void @@ -70,7 +72,7 @@ public function testInitPreservesGlobalScope(): void init(['default_integrations' => false]); $this->assertSame($globalScope, SentrySdk::getGlobalScope()); - $this->assertSame(SentrySdk::getCurrentHub()->getClient(), $globalScope->getClient()); + $this->assertSame(SentrySdk::getClient(), $globalScope->getClient()); $event = $globalScope->applyToEvent(Event::createEvent()); @@ -655,7 +657,7 @@ public function testWithContextAlwaysEndsContextWithOptionalTimeout(): void ->with(13) ->willReturn(new Result(ResultStatus::success())); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); try { withContext(static function (): void { diff --git a/tests/Integration/EnvironmentIntegrationTest.php b/tests/Integration/EnvironmentIntegrationTest.php index 788b95fa8..7391acbce 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 5667826b0..f13ba8e7d 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 52b96c9d3..659a50398 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/OTLPIntegrationTest.php b/tests/Integration/OTLPIntegrationTest.php index 27f4807b8..b8ef7ce13 100644 --- a/tests/Integration/OTLPIntegrationTest.php +++ b/tests/Integration/OTLPIntegrationTest.php @@ -19,7 +19,6 @@ use Sentry\Integration\OTLPIntegration; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tests\Fixtures\OpenTelemetry\StubOtelHttpClient; use Sentry\Tests\Fixtures\OpenTelemetry\TestClientDiscoverer; @@ -115,7 +114,7 @@ public function testSetupOnceRegistersExternalPropagationContext(): void ->willReturn($integration); try { - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $this->assertSame([ 'trace_id' => '771a43a4192642f0b136d5159a501700', @@ -146,7 +145,7 @@ public function testExternalPropagationContextIsIgnoredWhenCurrentClientDoesNotH ->willReturn(null); try { - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $this->assertNull(Scope::getExternalPropagationContext()); } finally { diff --git a/tests/Integration/RequestIntegrationTest.php b/tests/Integration/RequestIntegrationTest.php index 66749155a..c08cc3e1c 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 ab46c6a1b..1faf42176 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 b604f54db..a62d0bd47 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -13,7 +13,6 @@ use Sentry\Logs\LogsAggregator; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tests\StubTransport; use Sentry\Tracing\PropagationContext; @@ -38,8 +37,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(); @@ -96,8 +94,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(); @@ -173,21 +170,19 @@ public function testAttributesAreAddedToLogMessage(): void 'server_name' => 'web-server-01', ])->getClient(); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) { - $userDataBag = new UserDataBag(); - $userDataBag->setId('unique_id'); - $userDataBag->setEmail('foo@example.com'); - $userDataBag->setUsername('my_user'); - $scope->setUser($userDataBag); - }); + $userDataBag = new UserDataBag(); + $userDataBag->setId('unique_id'); + $userDataBag->setEmail('foo@example.com'); + $userDataBag->setUsername('my_user'); + SentrySdk::getIsolationScope()->setUser($userDataBag); $spanContext = new SpanContext(); $spanContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $spanContext->setSpanId(new SpanId('566e3688a61d4bc8')); $span = new Span($spanContext); - SentrySdk::getCurrentHub()->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); $aggregator = new LogsAggregator(); @@ -219,7 +214,7 @@ public function testMergedScopeAttributesAreAddedToLogMessage(): void 'enable_logs' => true, ])->getClient(); - SentrySdk::getGlobalScope()->setClient($client); + SentrySdk::init($client); SentrySdk::getGlobalScope()->setUser(UserDataBag::createFromUserIdentifier('global-user')); $spanContext = new SpanContext(); @@ -245,15 +240,13 @@ public function testUserAttributesCanBeSetManuallyWithDefaultPiiOff(): void 'send_default_pii' => false, ])->getClient(); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) { - $userDataBag = new UserDataBag(); - $userDataBag->setId('unique_id'); - $userDataBag->setEmail('foo@example.com'); - $userDataBag->setUsername('my_user'); - $scope->setUser($userDataBag); - }); + $userDataBag = new UserDataBag(); + $userDataBag->setId('unique_id'); + $userDataBag->setEmail('foo@example.com'); + $userDataBag->setUsername('my_user'); + SentrySdk::getIsolationScope()->setUser($userDataBag); $aggregator = new LogsAggregator(); $aggregator->add(LogLevel::info(), 'User performed action'); @@ -278,8 +271,7 @@ public function testFlushesImmediatelyWhenThresholdIsReached(): void 'log_flush_threshold' => 2, ])->setTransport($transport)->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -312,7 +304,7 @@ public function testFlushCapturesLogsWithProvidedClient(): void ])); $fallbackClient->expects($this->never()) ->method('captureEvent'); - SentrySdk::setCurrentHub(new Hub($fallbackClient)); + SentrySdk::init($fallbackClient); $aggregator = new LogsAggregator(); $aggregator->add(LogLevel::info(), 'Test message'); @@ -340,8 +332,7 @@ public function testDoesNotFlushImmediatelyWhenThresholdIsNull(): void 'log_flush_threshold' => null, ])->setTransport($transport)->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -362,7 +353,7 @@ public function testDoesNotUsePropagationContextSpanIdAsParentSpanIdWhenNoLocalS $propagationContext->setTraceId(new TraceId('771a43a4192642f0b136d5159a501700')); $propagationContext->setSpanId(new SpanId('1234567890abcdef')); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); $aggregator = new LogsAggregator(); @@ -384,8 +375,7 @@ public function testUsesExternalPropagationContextWhenNoLocalSpanExists(): void 'enable_logs' => true, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); Scope::registerExternalPropagationContext(static function (): array { return [ diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 4dd361b56..a0a4053b3 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 3d77e3706..6903c37c3 100644 --- a/tests/Metrics/TraceMetricsTest.php +++ b/tests/Metrics/TraceMetricsTest.php @@ -15,8 +15,6 @@ use Sentry\Metrics\Types\Metric; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; -use Sentry\State\HubAdapter; use Sentry\State\Scope; use Sentry\UserDataBag; @@ -26,7 +24,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 = []; } @@ -80,7 +78,7 @@ public function testDistributionMetrics(): void public function testFlushesImmediatelyWhenMetricFlushThresholdIsReached(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'metric_flush_threshold' => 2, ]), StubTransport::getInstance())); @@ -100,7 +98,7 @@ public function testFlushesImmediatelyWhenMetricFlushThresholdIsReached(): void public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'metric_flush_threshold' => null, ]), StubTransport::getInstance())); @@ -143,7 +141,7 @@ public function testFlushCapturesMetricsWithProvidedClient(): void ])); $fallbackClient->expects($this->never()) ->method('captureEvent'); - SentrySdk::setCurrentHub(new Hub($fallbackClient)); + SentrySdk::init($fallbackClient); $aggregator = new MetricsAggregator(); $aggregator->add(CounterMetric::TYPE, 'test-count', 2, ['foo' => 'bar'], null); @@ -163,7 +161,7 @@ public function testFlushCapturesMetricsWithProvidedClient(): void public function testMetricsBufferFullWhenMetricFlushThresholdIsNull(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'metric_flush_threshold' => null, ]), StubTransport::getInstance())); @@ -182,7 +180,7 @@ public function testMetricsBufferFullWhenMetricFlushThresholdIsNull(): void public function testEnableMetrics(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'enable_metrics' => false, ]), StubTransport::getInstance())); @@ -194,7 +192,7 @@ public function testEnableMetrics(): void public function testBeforeSendMetricAltersContent(): void { - 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/LogsHandlerTest.php b/tests/Monolog/LogsHandlerTest.php index f3f2965cc..0d94c6800 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\Tests\StubTransport; final class LogsHandlerTest extends TestCase @@ -28,8 +27,7 @@ protected function setUp(): void }, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); } /** @@ -108,8 +106,7 @@ public function testLogsHandlerDestructor(): void ])->setTransport($transport) ->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->handleLogAndDrop(); diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index c439159ef..dcb705292 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -12,12 +12,14 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\State\Hub; -use Sentry\State\Scope; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; +use Sentry\Tracing\TransactionContext; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; +use function Sentry\startTransaction; + final class SentrySdkTest extends TestCase { public function testInit(): void @@ -133,23 +135,16 @@ public function testStartAndEndContextIsolateScopeData(): void { SentrySdk::init(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { - $scope->setTag('baseline', 'yes'); - }); + SentrySdk::getIsolationScope()->setTag('baseline', 'yes'); SentrySdk::startContext(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { - $scope->setTag('request', 'yes'); - }); + SentrySdk::getIsolationScope()->setTag('request', 'yes'); SentrySdk::endContext(); $event = Event::createEvent(); - - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = SentrySdk::getIsolationScope()->applyToEvent($event); $this->assertArrayHasKey('baseline', $event->getTags()); $this->assertArrayNotHasKey('request', $event->getTags()); @@ -160,16 +155,15 @@ public function testStartContextDoesNotInheritBaselineSpan(): void SentrySdk::init(); $baselineSpan = new Span(new SpanContext()); - SentrySdk::getCurrentHub()->setSpan($baselineSpan); + SentrySdk::getIsolationScope()->setSpan($baselineSpan); SentrySdk::startContext(); - $contextHub = SentrySdk::getCurrentHub(); - $this->assertNull($contextHub->getSpan()); + $this->assertNull(SentrySdk::getIsolationScope()->getSpan()); SentrySdk::endContext(); - $this->assertSame($baselineSpan, SentrySdk::getCurrentHub()->getSpan()); + $this->assertSame($baselineSpan, SentrySdk::getIsolationScope()->getSpan()); } public function testStartContextCreatesFreshPropagationContext(): void @@ -195,16 +189,16 @@ public function testWithContextResetsSpanAndTransactionAcrossInvocations(): void SentrySdk::init(); SentrySdk::withContext(function (): void { - $transaction = SentrySdk::getCurrentHub()->startTransaction(new \Sentry\Tracing\TransactionContext('request-1')); - SentrySdk::getCurrentHub()->setSpan($transaction); + $transaction = startTransaction(new TransactionContext('request-1')); + SentrySdk::getIsolationScope()->setSpan($transaction); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getTransaction()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getTransaction()); }); SentrySdk::withContext(function (): void { - $this->assertNull(SentrySdk::getCurrentHub()->getSpan()); - $this->assertNull(SentrySdk::getCurrentHub()->getTransaction()); + $this->assertNull(SentrySdk::getIsolationScope()->getSpan()); + $this->assertNull(SentrySdk::getIsolationScope()->getTransaction()); }); } @@ -242,7 +236,7 @@ public function testEndContextFlushesClientTransportWithOptionalTimeout(): void ->with(12) ->willReturn(new Result(ResultStatus::success())); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); SentrySdk::startContext(); SentrySdk::endContext(12); @@ -257,7 +251,7 @@ public function testFlushFlushesClientTransport(): void ->with(null) ->willReturn(new Result(ResultStatus::success())); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); SentrySdk::flush(); } @@ -295,9 +289,7 @@ public function testNestedWithContextReusesOuterContext(): void $outerScope = SentrySdk::getIsolationScope(); $outerContextId = SentrySdk::getCurrentRuntimeContext()->getId(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { - $scope->setTag('outer', 'yes'); - }); + SentrySdk::getIsolationScope()->setTag('outer', 'yes'); SentrySdk::withContext(static function () use (&$innerScope, &$innerContextId): void { $innerScope = SentrySdk::getIsolationScope(); @@ -306,9 +298,7 @@ public function testNestedWithContextReusesOuterContext(): void $event = Event::createEvent(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = SentrySdk::getIsolationScope()->applyToEvent($event); $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); $this->assertSame('yes', $event->getTags()['outer'] ?? null); @@ -352,9 +342,7 @@ private function getCurrentScopeTraceparent(): string { $traceparent = ''; - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use (&$traceparent): void { - $traceparent = $scope->getPropagationContext()->toTraceparent(); - }); + $traceparent = SentrySdk::getIsolationScope()->getPropagationContext()->toTraceparent(); return $traceparent; } diff --git a/tests/Tracing/StrictTraceContinuationTest.php b/tests/Tracing/StrictTraceContinuationTest.php index 6de0a3a96..88440b5da 100644 --- a/tests/Tracing/StrictTraceContinuationTest.php +++ b/tests/Tracing/StrictTraceContinuationTest.php @@ -8,7 +8,6 @@ use Sentry\ClientInterface; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\TransactionContext; @@ -26,7 +25,7 @@ public function testPropagationContext(Options $options, string $baggage, bool $ ->method('getOptions') ->willReturn($options); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $contexts = [ PropagationContext::fromHeaders(self::SENTRY_TRACE_HEADER, $baggage), @@ -56,7 +55,7 @@ public function testTransactionContext(Options $options, string $baggage, bool $ ->method('getOptions') ->willReturn($options); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $contexts = [ TransactionContext::fromHeaders(self::SENTRY_TRACE_HEADER, $baggage), diff --git a/tests/Tracing/TransactionContextTest.php b/tests/Tracing/TransactionContextTest.php index 678d8d897..9d2b3dd61 100644 --- a/tests/Tracing/TransactionContextTest.php +++ b/tests/Tracing/TransactionContextTest.php @@ -10,7 +10,6 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\SpanId; use Sentry\Tracing\TraceId; @@ -182,7 +181,7 @@ public function testInvalidSampleRandIsLogged(): void $client->method('getOptions') ->willReturn(new Options(['logger' => $logger])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); try { TransactionContext::fromHeaders( @@ -190,7 +189,7 @@ public function testInvalidSampleRandIsLogged(): void 'sentry-sample_rand=-1.0' ); } finally { - SentrySdk::setCurrentHub(new Hub(new NoOpClient())); + SentrySdk::init(new NoOpClient()); } } diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index 82a767bd3..bf77563f2 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -11,13 +11,14 @@ use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use function Sentry\startTransaction; + /** * @group time-sensitive */ @@ -112,7 +113,9 @@ public function testTransactionIsSampledCorrectlyWhenTracingIsSetToZeroInOptions ]) ); - $transaction = (new Hub($client))->startTransaction($context); + SentrySdk::init($client); + + $transaction = startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } @@ -155,7 +158,9 @@ public function testTransactionIsNotSampledWhenTracingIsDisabledInOptions(Transa ]) ); - $transaction = (new Hub($client))->startTransaction($context); + SentrySdk::init($client); + + $transaction = startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } 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 35a32c838..47364f873 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 @@ -65,7 +65,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 a547854da..544929924 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 @@ -65,7 +65,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/test_callable_serialization.phpt b/tests/phpt/test_callable_serialization.phpt index ff5a2d79e..4806cf053 100644 --- a/tests/phpt/test_callable_serialization.phpt +++ b/tests/phpt/test_callable_serialization.phpt @@ -19,6 +19,8 @@ use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; +use function Sentry\captureException; + $vendor = __DIR__; while (!file_exists($vendor . '/vendor')) { @@ -51,11 +53,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!')); + captureException(new Exception('doh!')); } } From e978f092a51029df5c0a03bda93aa2d3b701c862 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:50 +0200 Subject: [PATCH 15/20] feat(scopes): remove Hub and related classes (#2138) --- UPGRADE-5.0.md | 33 + analysis-baseline.toml | 12 - src/Integration/ModulesIntegration.php | 2 - src/Integration/TransactionIntegration.php | 2 - src/SentrySdk.php | 29 +- src/State/Hub.php | 283 ------ src/State/HubAdapter.php | 226 ----- src/State/HubInterface.php | 145 --- src/State/Layer.php | 82 -- tests/SentrySdkTest.php | 28 +- tests/State/HubAdapterTest.php | 303 ------ tests/State/HubTest.php | 1055 -------------------- tests/State/LayerTest.php | 37 - 13 files changed, 43 insertions(+), 2194 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 delete mode 100644 tests/State/HubAdapterTest.php delete mode 100644 tests/State/HubTest.php delete mode 100644 tests/State/LayerTest.php diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 578ffaaaa..92a1f90bc 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -21,6 +21,39 @@ $logger->pushHandler(new \Sentry\Monolog\LogsHandler()); To continue sending Monolog records to Sentry issues instead, use `Sentry\Monolog\LogToSentryIssueHandler` for log messages or `Sentry\Monolog\ExceptionToSentryIssueHandler` for exceptions. +### Hub APIs Removed + +The Hub API has been removed. This includes: + +- **Removed classes and interfaces:** + - `Sentry\State\Hub` + - `Sentry\State\HubAdapter` + - `Sentry\State\HubInterface` + +- **Removed methods:** + - `SentrySdk::getCurrentHub()` + - `SentrySdk::setCurrentHub()` + +The SDK now exposes runtime state directly through `SentrySdk`, global helpers, and `Scope`: + +```php +// Before (4.x) +\Sentry\SentrySdk::getCurrentHub()->configureScope(static function (\Sentry\State\Scope $scope): void { + $scope->setTag('feature', 'checkout'); +}); + +// After (5.0) +\Sentry\configureScope(static function (\Sentry\State\Scope $scope): void { + $scope->setTag('feature', 'checkout'); +}); +``` + +Use `SentrySdk::getClient()` for the active client, `SentrySdk::getGlobalScope()` for process-global data, and `SentrySdk::getIsolationScope()` for the current runtime isolation scope. Use the capture helpers such as `captureMessage()`, `captureException()`, and `captureEvent()` to send events. + +Use `configureScope()` to mutate the current isolation scope, `withIsolationScope()` to run code with a temporary forked scope, and `startTransaction()` to start manual tracing instrumentation. + +`SentrySdk::init()` no longer returns a Hub instance. It now returns `void`. + ### Metrics API Removed The entire Metrics API has been removed as it is no longer supported. This includes: diff --git a/analysis-baseline.toml b/analysis-baseline.toml index 2c30c8e02..bb9bc9876 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -1368,18 +1368,6 @@ code = "possibly-invalid-argument" message = '''Possible argument type mismatch for argument #1 of `Sentry\StacktraceBuilder::buildFromBacktrace`: expected `list, 'class'?: class-string, 'file'?: string, 'function'?: string, 'line'?: int, 'type'?: string}>`, but possibly received `array`.''' count = 1 -[[issues]] -file = "src/State/Hub.php" -code = "mixed-operand" -message = "Right operand in `<` comparison has `mixed` type." -count = 1 - -[[issues]] -file = "src/State/Hub.php" -code = "possibly-null-operand" -message = "Left operand in `<` comparison might be `null` (type `float|null`)." -count = 1 - [[issues]] file = "src/State/Scope.php" code = "impossible-condition" diff --git a/src/Integration/ModulesIntegration.php b/src/Integration/ModulesIntegration.php index f1234f355..9e5ee8c16 100644 --- a/src/Integration/ModulesIntegration.php +++ b/src/Integration/ModulesIntegration.php @@ -28,8 +28,6 @@ public function setupOnce(): void Scope::addGlobalEventProcessor(static function (Event $event): Event { $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 if ($integration !== null) { $event->setModules(self::getComposerPackages()); } diff --git a/src/Integration/TransactionIntegration.php b/src/Integration/TransactionIntegration.php index 746584dab..13c7bfe51 100644 --- a/src/Integration/TransactionIntegration.php +++ b/src/Integration/TransactionIntegration.php @@ -26,8 +26,6 @@ public function setupOnce(): void Scope::addGlobalEventProcessor(static function (Event $event, EventHint $hint): Event { $integration = SentrySdk::getClient()->getIntegration(self::class); - // The client bound to the current hub, if any, could not have this - // integration enabled. If this is the case, bail out if ($integration === null) { return $event; } diff --git a/src/SentrySdk.php b/src/SentrySdk.php index 3822d4a28..d01434fec 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -6,8 +6,6 @@ use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; -use Sentry\State\HubAdapter; -use Sentry\State\HubInterface; use Sentry\State\RuntimeContext; use Sentry\State\RuntimeContextManager; use Sentry\State\Scope; @@ -37,38 +35,15 @@ private function __construct() } /** - * Initializes the SDK by binding the client to the global scope and reset + * Initializes the SDK by binding the client to the global scope and resetting * the current local runtime state. */ - public static function init(?ClientInterface $client = null): HubInterface + public static function init(?ClientInterface $client = null): void { if ($client !== null) { self::getGlobalScope()->setClient($client); } self::$runtimeContextManager = new RuntimeContextManager(); - - return self::getCurrentHub(); - } - - public static function getCurrentHub(): HubInterface - { - return HubAdapter::getInstance(); - } - - /** - * Sets the current hub. - * - * If called while an explicit runtime context is active, the hub update is - * scoped to that active context only. Otherwise, it updates the baseline - * hub used by the global fallback context and future contexts. - * - * @param HubInterface $hub The hub to set - */ - public static function setCurrentHub(HubInterface $hub): HubInterface - { - self::getGlobalScope()->setClient($hub->getClient()); - - return $hub; } public static function getGlobalScope(): Scope diff --git a/src/State/Hub.php b/src/State/Hub.php deleted file mode 100644 index 82597e79c..000000000 --- a/src/State/Hub.php +++ /dev/null @@ -1,283 +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 - { - return TransactionSampler::startTransaction($this->getClient()->getOptions(), $context, $customSamplingContext); - } - - /** - * {@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]; - } -} diff --git a/src/State/HubAdapter.php b/src/State/HubAdapter.php deleted file mode 100644 index b376b4cd7..000000000 --- a/src/State/HubAdapter.php +++ /dev/null @@ -1,226 +0,0 @@ -getLastEventId(); - } - - /** - * {@inheritdoc} - */ - public function withScope(callable $callback) - { - return \Sentry\withIsolationScope($callback); - } - - /** - * {@inheritdoc} - */ - public function configureScope(callable $callback): void - { - $callback(SentrySdk::getIsolationScope()); - } - - /** - * {@inheritdoc} - */ - public function bindClient(ClientInterface $client): void - { - SentrySdk::getGlobalScope()->setClient($client); - } - - /** - * {@inheritdoc} - */ - public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId - { - return EventCapturer::captureMessage($message, $level, $hint); - } - - /** - * {@inheritdoc} - */ - public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId - { - return EventCapturer::captureException($exception, $hint); - } - - /** - * {@inheritdoc} - */ - public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId - { - return EventCapturer::captureEvent($event, $hint); - } - - /** - * {@inheritdoc} - */ - public function captureLastError(?EventHint $hint = null): ?EventId - { - return EventCapturer::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 EventCapturer::captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); - } - - /** - * {@inheritdoc} - */ - public function addBreadcrumb(Breadcrumb $breadcrumb): bool - { - $scope = SentrySdk::getIsolationScope(); - - return BreadcrumbRecorder::record(SentrySdk::getClient($scope), $scope, $breadcrumb); - } - - /** - * {@inheritDoc} - */ - public function addAttachment(Attachment $attachment): bool - { - if (SentrySdk::getClient() instanceof NoOpClient) { - return false; - } - - SentrySdk::getIsolationScope()->addAttachment($attachment); - - return true; - } - - /** - * {@inheritdoc} - */ - public function getIntegration(string $className): ?IntegrationInterface - { - return SentrySdk::getClient()->getIntegration($className); - } - - /** - * {@inheritdoc} - */ - public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction - { - return TransactionSampler::startTransaction(SentrySdk::getClient()->getOptions(), $context, $customSamplingContext); - } - - /** - * {@inheritdoc} - */ - public function getTransaction(): ?Transaction - { - return SentrySdk::getIsolationScope()->getTransaction(); - } - - /** - * {@inheritdoc} - */ - public function getSpan(): ?Span - { - return SentrySdk::getIsolationScope()->getSpan(); - } - - /** - * {@inheritdoc} - */ - public function setSpan(?Span $span): HubInterface - { - SentrySdk::getIsolationScope()->setSpan($span); - - return $this; - } - - /** - * @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(): void - { - 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 c34ab7e43..000000000 --- a/src/State/HubInterface.php +++ /dev/null @@ -1,145 +0,0 @@ - $className - * - * @phpstan-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 3befbd80a..000000000 --- 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/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index dcb705292..d72be230f 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -11,7 +11,6 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TransactionContext; @@ -22,32 +21,21 @@ final class SentrySdkTest extends TestCase { - public function testInit(): void + public function testInitResetsRuntimeContext(): void { - $hub1 = SentrySdk::init(); - $hub2 = SentrySdk::getCurrentHub(); + $previousScope = SentrySdk::getIsolationScope(); + $previousScope->setTag('runtime', 'old'); - $this->assertSame($hub1, $hub2); - $this->assertSame(SentrySdk::init(), SentrySdk::init()); - } - - public function testGetCurrentHub(): void - { SentrySdk::init(); - $hub2 = SentrySdk::getCurrentHub(); - $hub3 = SentrySdk::getCurrentHub(); + $currentScope = SentrySdk::getIsolationScope(); - $this->assertSame($hub2, $hub3); - } + $this->assertNotSame($previousScope, $currentScope); - public function testSetCurrentHub(): void - { - $client = $this->createMock(ClientInterface::class); - $hub = new Hub($client); + $event = $currentScope->applyToEvent(Event::createEvent()); - $this->assertSame($hub, SentrySdk::setCurrentHub($hub)); - $this->assertSame($client, SentrySdk::getClient()); + $this->assertNotNull($event); + $this->assertSame([], $event->getTags()); } public function testGetGlobalScope(): void diff --git a/tests/State/HubAdapterTest.php b/tests/State/HubAdapterTest.php deleted file mode 100644 index 149ba203f..000000000 --- a/tests/State/HubAdapterTest.php +++ /dev/null @@ -1,303 +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); - - HubAdapter::getInstance()->bindClient($client); - - $this->assertSame($client, HubAdapter::getInstance()->getClient()); - } - - public function testGetLastEventId(): void - { - $event = Event::createEvent(); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureEvent') - ->willReturn($event->getId()); - - SentrySdk::getGlobalScope()->setClient($client); - - HubAdapter::getInstance()->captureEvent($event); - - $this->assertSame($event->getId(), HubAdapter::getInstance()->getLastEventId()); - } - - public function testWithScope(): void - { - $baseScope = SentrySdk::getIsolationScope(); - - $returnValue = HubAdapter::getInstance()->withScope(static function (Scope $scope): string { - $scope->setTag('nested', 'yes'); - - return 'foobarbaz'; - }); - - $this->assertSame('foobarbaz', $returnValue); - $this->assertSame($baseScope, SentrySdk::getIsolationScope()); - - $event = $baseScope->applyToEvent(Event::createEvent()); - $this->assertNotNull($event); - $this->assertArrayNotHasKey('nested', $event->getTags()); - } - - public function testConfigureScope(): void - { - HubAdapter::getInstance()->configureScope(static function (Scope $scope): void { - $scope->setTag('foo', 'bar'); - }); - - $event = SentrySdk::getIsolationScope()->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame(['foo' => 'bar'], $event->getTags()); - } - - /** - * @dataProvider captureMessageDataProvider - */ - public function testCaptureMessage(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureMessage') - ->with($expectedFunctionCallArgs[0], $expectedFunctionCallArgs[1], $this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[2] ?? null) - ->willReturn($eventId); - - 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()]]; - } - - /** - * @dataProvider captureExceptionDataProvider - */ - public function testCaptureException(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureException') - ->with($expectedFunctionCallArgs[0], $this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[1] ?? null) - ->willReturn($eventId); - - 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()]]; - } - - public function testCaptureEvent(): void - { - $event = Event::createEvent(); - $hint = EventHint::fromArray([]); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureEvent') - ->with($event, $hint, $this->isInstanceOf(Scope::class)) - ->willReturn($event->getId()); - - SentrySdk::getGlobalScope()->setClient($client); - - $this->assertSame($event->getId(), HubAdapter::getInstance()->captureEvent($event, $hint)); - } - - /** - * @dataProvider captureLastErrorDataProvider - */ - public function testCaptureLastError(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureLastError') - ->with($this->isInstanceOf(Scope::class), $expectedFunctionCallArgs[0] ?? null) - ->willReturn($eventId); - - SentrySdk::getGlobalScope()->setClient($client); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureLastError(...$expectedFunctionCallArgs)); - } - - public static function captureLastErrorDataProvider(): \Generator - { - yield [[]]; - yield [[new EventHint()]]; - } - - public function testCaptureCheckIn(): void - { - $checkInId = SentryUid::generate(); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->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(); - - return $checkIn !== null && $checkIn->getId() === $checkInId; - }), null, $this->isInstanceOf(Scope::class)); - - SentrySdk::getGlobalScope()->setClient($client); - - $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'); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - SentrySdk::getGlobalScope()->setClient($client); - - $this->assertTrue(HubAdapter::getInstance()->addBreadcrumb($breadcrumb)); - } - - public function testGetIntegration(): void - { - $integration = $this->createMock(IntegrationInterface::class); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getIntegration') - ->with(\get_class($integration)) - ->willReturn($integration); - - SentrySdk::getGlobalScope()->setClient($client); - - $this->assertSame($integration, HubAdapter::getInstance()->getIntegration(\get_class($integration))); - } - - public function testStartTransaction(): void - { - $transactionContext = new TransactionContext('test-transaction'); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - SentrySdk::getGlobalScope()->setClient($client); - - $transaction = HubAdapter::getInstance()->startTransaction($transactionContext); - - $this->assertSame('test-transaction', $transaction->getName()); - } - - public function testGetTransaction(): void - { - $transaction = HubAdapter::getInstance()->startTransaction(new TransactionContext()); - SentrySdk::getIsolationScope()->setSpan($transaction); - - $this->assertSame($transaction, HubAdapter::getInstance()->getTransaction()); - } - - public function testGetSpan(): void - { - $span = new Span(); - SentrySdk::getIsolationScope()->setSpan($span); - - $this->assertSame($span, HubAdapter::getInstance()->getSpan()); - } - - public function testSetSpan(): void - { - $span = new Span(); - - $this->assertSame(HubAdapter::getInstance(), HubAdapter::getInstance()->setSpan($span)); - $this->assertSame($span, SentrySdk::getIsolationScope()->getSpan()); - } -} diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php deleted file mode 100644 index 5e9bd9837..000000000 --- a/tests/State/HubTest.php +++ /dev/null @@ -1,1055 +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(static 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(static 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(static 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 function testStartTransactionIgnoresBaggageSampleRateWithoutSentryTrace(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 0.0, - ])); - - $hub = new Hub($client); - $transactionContext = TransactionContext::fromHeaders('', 'sentry-sample_rate=1'); - $transaction = $hub->startTransaction($transactionContext); - - $this->assertFalse($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 'Invalid incoming sample_rand is ignored' => [ - new Options([ - 'traces_sample_rate' => 1.0, - ]), - TransactionContext::fromHeaders( - '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8', - 'sentry-sample_rand=2.0' - ), - true, - ]; - - 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 testStartTransactionStartsProfilerWithProfilesSampler(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 1.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNotNull($transaction->getProfiler()); - } - - public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerReturnsZero(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 0.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionPrefersProfilesSamplerOverProfilesSampleRate(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 0.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionWithProfilesSamplerReceivesCustomSamplingContext(): void - { - $customSamplingContext = ['a' => 'b']; - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { - $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); - - return 0.0; - }, - ])); - - $hub = new Hub($client); - $hub->startTransaction(new TransactionContext(), $customSamplingContext); - } - - public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerReturnsInvalidValue(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): string { - return 'foo'; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionDoesNotCallProfilesSamplerWhenTransactionIsNotSampled(): void - { - $profilesSamplerInvoked = false; - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 0.0, - 'profiles_sampler' => static function () use (&$profilesSamplerInvoked): float { - $profilesSamplerInvoked = true; - - return 1.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertFalse($transaction->getSampled()); - $this->assertFalse($profilesSamplerInvoked); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionUpdatesTheDscSampleRate(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sampler' => static 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 501f0e579..000000000 --- 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()); - } -} From 606034b5751f4ea660f1fb79270cf113535e7a63 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 14:16:50 +0200 Subject: [PATCH 16/20] feat(scopes): introduce typed scopes (#2139) --- mago.toml | 5 + src/Client.php | 21 +- src/ClientInterface.php | 34 +- .../AbstractErrorListenerIntegration.php | 9 +- src/Logs/LogsAggregator.php | 12 +- src/Metrics/MetricsAggregator.php | 13 +- src/Monolog/BreadcrumbHandler.php | 4 +- src/Monolog/ExceptionToSentryIssueHandler.php | 4 +- src/Monolog/LogToSentryIssueHandler.php | 4 +- src/NoOpClient.php | 10 +- src/SentrySdk.php | 13 +- src/State/BreadcrumbRecorder.php | 2 +- src/State/EventCapturer.php | 18 +- src/State/GlobalScope.php | 20 + src/State/IsolationScope.php | 149 ++++ src/State/MergedScope.php | 146 ++++ src/State/MutableScope.php | 267 +++++++ src/State/RuntimeContext.php | 10 +- src/State/Scope.php | 665 +----------------- src/State/ScopeData.php | 405 +++++++++++ src/Tracing/DynamicSamplingContext.php | 16 +- src/Tracing/GuzzleTracingMiddleware.php | 4 +- src/functions.php | 18 +- tests/ClientTest.php | 5 +- tests/Fixtures/runtime/frankenphp/index.php | 3 +- tests/Fixtures/runtime/roadrunner-worker.php | 3 +- tests/FunctionsTest.php | 74 +- .../EnvironmentIntegrationTest.php | 6 +- .../FrameContextifierIntegrationTest.php | 10 +- tests/Integration/ModulesIntegrationTest.php | 8 +- tests/Integration/RequestIntegrationTest.php | 6 +- .../TransactionIntegrationTest.php | 6 +- tests/Logs/LogsAggregatorTest.php | 5 +- tests/Metrics/TraceMetricsTest.php | 2 +- tests/Monolog/BreadcrumbHandlerTest.php | 2 +- .../ExceptionToSentryIssueHandlerTest.php | 11 +- tests/Monolog/LogToSentryIssueHandlerTest.php | 14 +- tests/SentrySdkTest.php | 9 +- tests/State/BreadcrumbRecorderTest.php | 21 +- tests/State/EventCapturerTest.php | 44 +- tests/State/ScopeTest.php | 224 +++--- tests/Tracing/DynamicSamplingContextTest.php | 8 +- tests/Tracing/GuzzleTracingMiddlewareTest.php | 14 +- tests/Tracing/PropagationContextTest.php | 6 +- tests/Tracing/SpanTest.php | 9 +- tests/Tracing/TransactionTest.php | 4 +- 46 files changed, 1350 insertions(+), 993 deletions(-) create mode 100644 src/State/GlobalScope.php create mode 100644 src/State/IsolationScope.php create mode 100644 src/State/MergedScope.php create mode 100644 src/State/MutableScope.php create mode 100644 src/State/ScopeData.php diff --git a/mago.toml b/mago.toml index f8b021c00..b4534de5d 100644 --- a/mago.toml +++ b/mago.toml @@ -10,3 +10,8 @@ excludes = [ [analyzer] baseline = "analysis-baseline.toml" +ignore = [ + { code = "no-value", pattern = "DebugType::getDebugType" }, + "impossible-condition", + "redundant-logical-operation", +] diff --git a/src/Client.php b/src/Client.php index 2bb80c730..9255639e5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,7 +10,7 @@ use Sentry\Integration\IntegrationRegistry; use Sentry\Serializer\RepresentationSerializer; use Sentry\Serializer\RepresentationSerializerInterface; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; use Sentry\Transport\TransportInterface; @@ -140,7 +140,7 @@ public function getCspReportUrl(): ?string /** * {@inheritdoc} */ - public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureMessage(string $message, ?Severity $level = null, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { $event = Event::createEvent(); $event->setMessage($message); @@ -152,7 +152,7 @@ public function captureMessage(string $message, ?Severity $level = null, ?Scope /** * {@inheritdoc} */ - public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureException(\Throwable $exception, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { $className = \get_class($exception); if ($this->shouldIgnoreException($className)) { @@ -176,7 +176,7 @@ public function captureException(\Throwable $exception, ?Scope $scope = null, ?E /** * {@inheritdoc} */ - public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId + public function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?EventId { // Client reports don't need to be augmented in the prepareEvent pipeline. if ($event->getType() !== EventType::clientReport()) { @@ -208,7 +208,7 @@ public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scop /** * {@inheritdoc} */ - public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureLastError(?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { $error = error_get_last(); @@ -277,13 +277,13 @@ public function getSdkVersion(): string /** * Assembles an event and prepares it to be sent of to Sentry. * - * @param Event $event The payload that will be converted to an Event - * @param EventHint|null $hint May contain additional information about the event - * @param Scope|null $scope Optional scope which enriches the Event + * @param Event $event The payload that will be converted to an Event + * @param EventHint|null $hint May contain additional information about the event + * @param IsolationScope|null $scope Optional scope which enriches the Event * * @return Event|null The prepared event object or null if it must be discarded */ - private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?Event + private function prepareEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?Event { if ($hint !== null) { if ($hint->exception !== null && empty($event->getExceptions())) { @@ -339,7 +339,8 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco if ($scope !== null) { $beforeEventProcessors = $event; - $event = $scope->applyToEvent($event, $hint, $this->options); + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scope)->applyToEvent($event, $hint, $this->options); if ($event === null) { $this->logger->info( diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 5d511519f..8aa245b70 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -5,7 +5,7 @@ namespace Sentry; use Sentry\Integration\IntegrationInterface; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; interface ClientInterface @@ -23,38 +23,38 @@ public function getCspReportUrl(): ?string; /** * Logs a message. * - * @param string $message The message (primary description) for the event - * @param Severity|null $level The level of the message to be sent - * @param Scope|null $scope An optional scope keeping the state - * @param EventHint|null $hint Object that can contain additional information about the event + * @param string $message The message (primary description) for the event + * @param Severity|null $level The level of the message to be sent + * @param IsolationScope|null $scope An optional scope keeping the state + * @param EventHint|null $hint Object that can contain additional information about the event */ - public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId; + public function captureMessage(string $message, ?Severity $level = null, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId; /** * Logs an exception. * - * @param \Throwable $exception The exception object - * @param Scope|null $scope An optional scope keeping the state - * @param EventHint|null $hint Object that can contain additional information about the event + * @param \Throwable $exception The exception object + * @param IsolationScope|null $scope An optional scope keeping the state + * @param EventHint|null $hint Object that can contain additional information about the event */ - public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId; + public function captureException(\Throwable $exception, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId; /** * Logs the most recent error (obtained with {@link error_get_last}). * - * @param Scope|null $scope An optional scope keeping the state - * @param EventHint|null $hint Object that can contain additional information about the event + * @param IsolationScope|null $scope An optional scope keeping the state + * @param EventHint|null $hint Object that can contain additional information about the event */ - public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId; + public function captureLastError(?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId; /** * Captures a new event using the provided data. * - * @param Event $event The event being captured - * @param EventHint|null $hint May contain additional information about the event - * @param Scope|null $scope An optional scope keeping the state + * @param Event $event The event being captured + * @param EventHint|null $hint May contain additional information about the event + * @param IsolationScope|null $scope An optional scope keeping the state */ - public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId; + public function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?EventId; /** * Returns the integration instance if it is installed on the client. diff --git a/src/Integration/AbstractErrorListenerIntegration.php b/src/Integration/AbstractErrorListenerIntegration.php index 97123b113..36482a987 100644 --- a/src/Integration/AbstractErrorListenerIntegration.php +++ b/src/Integration/AbstractErrorListenerIntegration.php @@ -5,9 +5,10 @@ namespace Sentry\Integration; use Sentry\Event; +use Sentry\EventHint; use Sentry\ExceptionMechanism; use Sentry\State\EventCapturer; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withIsolationScope; @@ -18,8 +19,10 @@ abstract class AbstractErrorListenerIntegration implements IntegrationInterface */ protected function captureException(\Throwable $exception): void { - withIsolationScope(function (Scope $scope) use ($exception): void { - $scope->addEventProcessor(\Closure::fromCallable([$this, 'addExceptionMechanismToEvent'])); + withIsolationScope(function (IsolationScope $scope) use ($exception): void { + $scope->addEventProcessor(function (Event $event, EventHint $hint): Event { + return $this->addExceptionMechanismToEvent($event); + }); EventCapturer::captureException($exception); }); diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index f92b0b305..4bd2c5e77 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -10,6 +10,7 @@ use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Util\Arr; use Sentry\Util\Str; @@ -42,7 +43,8 @@ public function add( $isolationScope = SentrySdk::getIsolationScope(); $client = SentrySdk::getClient($isolationScope); - $scope = Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope); + $globalScope = SentrySdk::getGlobalScope(); + $scope = $globalScope->merge($isolationScope); $options = $client->getOptions(); $sdkLogger = $options->getLogger(); @@ -71,7 +73,7 @@ public function add( $formattedMessage = $message; } - $traceData = $this->getTraceData($scope); + $traceData = $this->getTraceData($isolationScope); $traceId = $traceData['trace_id']; $parentSpanId = $traceData['parent_span_id']; @@ -162,7 +164,7 @@ public function add( } } - public function flush(?ClientInterface $client = null, ?Scope $isolationScope = null): ?EventId + public function flush(?ClientInterface $client = null, ?IsolationScope $isolationScope = null): ?EventId { $logs = $this->logs; @@ -174,7 +176,7 @@ public function flush(?ClientInterface $client = null, ?Scope $isolationScope = $client = $client ?? SentrySdk::getClient($isolationScope); $event = Event::createLogs()->setLogs($logs->drain()); - return $client->captureEvent($event, null, Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope)); + return $client->captureEvent($event, null, $isolationScope); } /** @@ -188,7 +190,7 @@ public function all(): array /** * @return array{trace_id: string, parent_span_id: string|null} */ - private function getTraceData(Scope $scope): array + private function getTraceData(IsolationScope $scope): array { $span = $scope->getSpan(); diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index f4d9b2336..f4519da1c 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -13,7 +13,7 @@ use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tracing\SpanId; use Sentry\Tracing\TraceId; use Sentry\Unit; @@ -53,7 +53,8 @@ public function add( ): void { $isolationScope = SentrySdk::getIsolationScope(); $client = SentrySdk::getClient($isolationScope); - $scope = Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope); + $globalScope = SentrySdk::getGlobalScope(); + $scope = $globalScope->merge($isolationScope); $options = $client->getOptions(); $metricFlushThreshold = $options->getMetricFlushThreshold(); @@ -97,7 +98,7 @@ public function add( $attributes += $defaultAttributes; - $traceContext = $this->getTraceContext($scope); + $traceContext = $this->getTraceContext($isolationScope); $traceId = new TraceId($traceContext['trace_id']); $spanId = new SpanId($traceContext['span_id']); @@ -119,7 +120,7 @@ public function add( } } - public function flush(?ClientInterface $client = null, ?Scope $isolationScope = null): ?EventId + public function flush(?ClientInterface $client = null, ?IsolationScope $isolationScope = null): ?EventId { $metrics = $this->metrics; @@ -131,13 +132,13 @@ public function flush(?ClientInterface $client = null, ?Scope $isolationScope = $client = $client ?? SentrySdk::getClient($isolationScope); $event = Event::createMetrics()->setMetrics($metrics->drain()); - return $client->captureEvent($event, null, Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope)); + return $client->captureEvent($event, null, $isolationScope); } /** * @return array{trace_id: string, span_id: string} */ - private function getTraceContext(Scope $scope): array + private function getTraceContext(IsolationScope $scope): array { $traceContext = $scope->getTraceContext(); diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index 7197db202..9fd3538bf 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -13,10 +13,10 @@ use Sentry\Event; use Sentry\SentrySdk; use Sentry\State\BreadcrumbRecorder; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; /** - * This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see Scope}, + * This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see IsolationScope}, * to enrich any event sent to Sentry. */ final class BreadcrumbHandler extends AbstractProcessingHandler diff --git a/src/Monolog/ExceptionToSentryIssueHandler.php b/src/Monolog/ExceptionToSentryIssueHandler.php index 4ab13e89d..7998c9c83 100644 --- a/src/Monolog/ExceptionToSentryIssueHandler.php +++ b/src/Monolog/ExceptionToSentryIssueHandler.php @@ -10,7 +10,7 @@ use Monolog\LogRecord; use Psr\Log\LogLevel; use Sentry\State\EventCapturer; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withIsolationScope; @@ -39,7 +39,7 @@ public function handle($record): bool return false; } - withIsolationScope(function (Scope $scope) use ($record, $exception): void { + withIsolationScope(function (IsolationScope $scope) use ($record, $exception): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); $scope->setExtra('monolog.message', $record['message']); diff --git a/src/Monolog/LogToSentryIssueHandler.php b/src/Monolog/LogToSentryIssueHandler.php index e7c8d9b44..f38f03020 100644 --- a/src/Monolog/LogToSentryIssueHandler.php +++ b/src/Monolog/LogToSentryIssueHandler.php @@ -12,7 +12,7 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\State\EventCapturer; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withIsolationScope; @@ -66,7 +66,7 @@ protected function doWrite($record): void $hint = new EventHint(); - withIsolationScope(function (Scope $scope) use ($record, $event, $hint): void { + withIsolationScope(function (IsolationScope $scope) use ($record, $event, $hint): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); diff --git a/src/NoOpClient.php b/src/NoOpClient.php index 364b6073e..acf238b0d 100644 --- a/src/NoOpClient.php +++ b/src/NoOpClient.php @@ -6,7 +6,7 @@ use Sentry\Integration\IntegrationInterface; use Sentry\Serializer\RepresentationSerializer; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; @@ -54,22 +54,22 @@ public function getCspReportUrl(): ?string return null; } - public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureMessage(string $message, ?Severity $level = null, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { return null; } - public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureException(\Throwable $exception, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { return null; } - public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureLastError(?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { return null; } - public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId + public function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?EventId { return null; } diff --git a/src/SentrySdk.php b/src/SentrySdk.php index d01434fec..b273ccc72 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -6,9 +6,10 @@ use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\RuntimeContext; use Sentry\State\RuntimeContextManager; -use Sentry\State\Scope; /** * This class is the main entry point for all the most common SDK features. @@ -18,7 +19,7 @@ final class SentrySdk { /** - * @var Scope|null The process-global scope + * @var GlobalScope|null The process-global scope */ private static $globalScope; @@ -46,21 +47,21 @@ public static function init(?ClientInterface $client = null): void self::$runtimeContextManager = new RuntimeContextManager(); } - public static function getGlobalScope(): Scope + public static function getGlobalScope(): GlobalScope { if (self::$globalScope === null) { - self::$globalScope = new Scope(); + self::$globalScope = new GlobalScope(); } return self::$globalScope; } - public static function getIsolationScope(): Scope + public static function getIsolationScope(): IsolationScope { return self::getCurrentRuntimeContext()->getIsolationScope(); } - public static function getClient(?Scope $isolationScope = null): ClientInterface + public static function getClient(?IsolationScope $isolationScope = null): ClientInterface { $client = ($isolationScope ?? self::getIsolationScope())->getClient(); diff --git a/src/State/BreadcrumbRecorder.php b/src/State/BreadcrumbRecorder.php index 37b315725..d3bfe25a4 100644 --- a/src/State/BreadcrumbRecorder.php +++ b/src/State/BreadcrumbRecorder.php @@ -20,7 +20,7 @@ private function __construct() /** * Records the breadcrumb on the given scope if the client configuration allows it. */ - public static function record(ClientInterface $client, Scope $scope, Breadcrumb $breadcrumb): bool + public static function record(ClientInterface $client, IsolationScope $scope, Breadcrumb $breadcrumb): bool { if ($client instanceof NoOpClient) { return false; diff --git a/src/State/EventCapturer.php b/src/State/EventCapturer.php index 2511a0013..a21b44e75 100644 --- a/src/State/EventCapturer.php +++ b/src/State/EventCapturer.php @@ -26,28 +26,28 @@ private function __construct() public static function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($message, $level, $hint): ?EventId { + return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($message, $level, $hint): ?EventId { return $client->captureMessage($message, $level, $captureScope, $hint); }); } public static function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { - return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($exception, $hint): ?EventId { + return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($exception, $hint): ?EventId { return $client->captureException($exception, $captureScope, $hint); }); } public static function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($event, $hint): ?EventId { + return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($event, $hint): ?EventId { return $client->captureEvent($event, $hint, $captureScope); }); } public static function captureLastError(?EventHint $hint = null): ?EventId { - return self::capture(static function (ClientInterface $client, Scope $captureScope) use ($hint): ?EventId { + return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($hint): ?EventId { return $client->captureLastError($captureScope, $hint); }); } @@ -77,7 +77,7 @@ public static function captureCheckIn(string $slug, CheckInStatus $status, $dura ); $event->setCheckIn($checkIn); - self::captureWithScope($client, $isolationScope, static function (ClientInterface $client, Scope $captureScope) use ($event): ?EventId { + self::captureWithScope($client, $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($event): ?EventId { return $client->captureEvent($event, null, $captureScope); }); @@ -85,7 +85,7 @@ public static function captureCheckIn(string $slug, CheckInStatus $status, $dura } /** - * @param callable(ClientInterface, Scope): ?EventId $capture + * @param callable(ClientInterface, IsolationScope): ?EventId $capture */ private static function capture(callable $capture): ?EventId { @@ -95,11 +95,11 @@ private static function capture(callable $capture): ?EventId } /** - * @param callable(ClientInterface, Scope): ?EventId $capture + * @param callable(ClientInterface, IsolationScope): ?EventId $capture */ - private static function captureWithScope(ClientInterface $client, Scope $isolationScope, callable $capture): ?EventId + private static function captureWithScope(ClientInterface $client, IsolationScope $isolationScope, callable $capture): ?EventId { - $eventId = $capture($client, Scope::mergeScopes(SentrySdk::getGlobalScope(), $isolationScope)); + $eventId = $capture($client, $isolationScope); $isolationScope->setLastEventId($eventId); return $eventId; diff --git a/src/State/GlobalScope.php b/src/State/GlobalScope.php new file mode 100644 index 000000000..7483b22d6 --- /dev/null +++ b/src/State/GlobalScope.php @@ -0,0 +1,20 @@ +scopeData->merge($scope->scopeData), $scope->getSpan()); + } +} diff --git a/src/State/IsolationScope.php b/src/State/IsolationScope.php new file mode 100644 index 000000000..0aa36183e --- /dev/null +++ b/src/State/IsolationScope.php @@ -0,0 +1,149 @@ +scopeData->setPropagationContext($propagationContext ?? PropagationContext::fromDefaults()); + } + + /** + * Returns the ID of the last captured event. + */ + public function getLastEventId(): ?EventId + { + return $this->lastEventId; + } + + /** + * @internal + */ + public function setLastEventId(?EventId $lastEventId): void + { + $this->lastEventId = $lastEventId; + } + + /** + * Adds a feature flag to the scope. + * + * @return $this + */ + public function addFeatureFlag(string $key, bool $result): self + { + $this->scopeData->addFeatureFlag($key, $result); + + if ($this->span !== null) { + $this->span->setFlag($key, $result); + } + + return $this; + } + + /** + * Clears event payload data from the scope. The client binding and last + * event ID are preserved. + */ + public function clear(): void + { + parent::clear(); + $this->span = null; + } + + /** + * Returns the span that is on the scope. + */ + public function getSpan(): ?Span + { + return $this->span; + } + + /** + * Sets the span on the scope. + * + * @param Span|null $span The span + * + * @return $this + */ + public function setSpan(?Span $span): self + { + $this->span = $span; + + return $this; + } + + /** + * Returns the transaction attached to the scope (if there is one). + */ + public function getTransaction(): ?Transaction + { + if ($this->span !== null) { + return $this->span->getTransaction(); + } + + return null; + } + + public function hasExternalPropagationContext(): bool + { + return $this->span === null && self::getExternalPropagationContext() !== null; + } + + public function getPropagationContext(): PropagationContext + { + return $this->scopeData->getPropagationContext(); + } + + public function setPropagationContext(PropagationContext $propagationContext): self + { + $this->scopeData->setPropagationContext($propagationContext); + + return $this; + } + + /** + * @return array{ + * trace_id: string, + * span_id: string, + * parent_span_id?: string, + * data?: array, + * description?: string, + * op?: string, + * status?: string, + * tags?: array, + * origin?: string + * } + */ + public function getTraceContext(): array + { + if ($this->span !== null) { + return $this->span->getTraceContext(); + } + + return self::getExternalPropagationContext() ?? $this->scopeData->getPropagationContext()->getTraceContext(); + } +} diff --git a/src/State/MergedScope.php b/src/State/MergedScope.php new file mode 100644 index 000000000..d6af43d5a --- /dev/null +++ b/src/State/MergedScope.php @@ -0,0 +1,146 @@ +scopeData = $scopeData; + $this->span = $span; + } + + /** + * Applies the current context and fingerprint to the event. If the event has + * already some breadcrumbs on it, the ones from this scope won't get merged. + * + * @param Event $event The event object that will be enriched with scope data + */ + public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event + { + $event->setFingerprint(array_merge($event->getFingerprint(), $this->scopeData->getFingerprint())); + + if (empty($event->getBreadcrumbs())) { + $event->setBreadcrumb($this->scopeData->getBreadcrumbs()); + } + + if ($this->scopeData->getLevel() !== null) { + $event->setLevel($this->scopeData->getLevel()); + } + + if (!empty($this->scopeData->getTags())) { + $event->setTags(array_merge($this->scopeData->getTags(), $event->getTags())); + } + + if (!empty($this->scopeData->getFlags())) { + $event->setContext('flags', [ + 'values' => array_map(static function (array $flag) { + return [ + 'flag' => key($flag), + 'result' => current($flag), + ]; + }, array_values($this->scopeData->getFlags())), + ]); + } + + if (!empty($this->scopeData->getExtra())) { + $event->setExtra(array_merge($this->scopeData->getExtra(), $event->getExtra())); + } + + $scopeUser = $this->scopeData->getUser(); + if ($scopeUser !== null) { + $user = $event->getUser(); + + if ($user === null) { + $user = $scopeUser; + } else { + $user = (clone $scopeUser)->merge($user); + } + + $event->setUser($user); + } + + /** + * Apply the trace context to errors if there is a Span on the Scope. + * Else fallback to the external propagation context or to the + * propagation context. + * But do not override a trace context already present. + */ + $externalPropagationContext = null; + if ($this->span === null) { + $externalPropagationContext = self::getExternalPropagationContext(); + } + + $traceContext = $this->span !== null + ? $this->span->getTraceContext() + : ($externalPropagationContext ?? $this->scopeData->getPropagationContext()->getTraceContext()); + + if (!\array_key_exists('trace', $event->getContexts())) { + $event->setContext('trace', $traceContext); + } + + if ($this->span !== null) { + // Apply the dynamic sampling context to errors if there is a Transaction on the Scope + $transaction = $this->span->getTransaction(); + if ($transaction !== null) { + $event->setSdkMetadata('dynamic_sampling_context', $transaction->getDynamicSamplingContext()); + } + } elseif ($externalPropagationContext === null) { + $propagationContext = $this->scopeData->getPropagationContext(); + $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); + if ($dynamicSamplingContext === null && $options !== null) { + $dynamicSamplingContext = DynamicSamplingContext::fromOptionsAndPropagationContext($options, $propagationContext); + } + $event->setSdkMetadata('dynamic_sampling_context', $dynamicSamplingContext); + } + + foreach (array_merge($this->scopeData->getContexts(), $event->getContexts()) as $name => $data) { + $event->setContext($name, $data); + } + + // We create a empty `EventHint` instance to allow processors to always receive a `EventHint` instance even if there wasn't one + if ($hint === null) { + $hint = new EventHint(); + } + + if ($event->getType() === EventType::event() || $event->getType() === EventType::transaction()) { + if (empty($event->getAttachments())) { + $event->setAttachments($this->scopeData->getAttachments()); + } + } + + foreach (array_merge(parent::$globalEventProcessors, $this->scopeData->getEventProcessors()) as $processor) { + $event = $processor($event, $hint); + + if ($event === null) { + return null; + } + + if (!$event instanceof Event) { + throw new \InvalidArgumentException(\sprintf('The event processor must return null or an instance of the %s class', Event::class)); + } + } + + return $event; + } +} diff --git a/src/State/MutableScope.php b/src/State/MutableScope.php new file mode 100644 index 000000000..36c295733 --- /dev/null +++ b/src/State/MutableScope.php @@ -0,0 +1,267 @@ +scopeData->getClient(); + } + + /** + * Sets the client bound to this scope. + * + * @return $this + */ + public function setClient(ClientInterface $client): self + { + $this->scopeData->setClient($client); + + return $this; + } + + /** + * Sets a new tag in the tags context. + * + * @param string $key The key that uniquely identifies the tag + * @param string $value The value + * + * @return $this + */ + public function setTag(string $key, string $value): self + { + $this->scopeData->setTag($key, $value); + + return $this; + } + + /** + * Merges the given tags into the current tags context. + * + * @param array $tags The tags to merge into the current context + * + * @return $this + */ + public function setTags(array $tags): self + { + $this->scopeData->setTags(array_merge($this->scopeData->getTags(), $tags)); + + return $this; + } + + /** + * Removes a given tag from the tags context. + * + * @param string $key The key that uniquely identifies the tag + * + * @return $this + */ + public function removeTag(string $key): self + { + $this->scopeData->removeTag($key); + + return $this; + } + + /** + * Sets data to the context by a given name. + * + * @param string $name The name that uniquely identifies the context + * @param array $value The value + * + * @return $this + */ + public function setContext(string $name, array $value): self + { + if (!empty($value)) { + $this->scopeData->setContext($name, $value); + } + + return $this; + } + + /** + * Removes the context from the scope. + * + * @param string $name The name that uniquely identifies the context + * + * @return $this + */ + public function removeContext(string $name): self + { + $this->scopeData->removeContext($name); + + return $this; + } + + /** + * Sets a new information in the extra context. + * + * @param string $key The key that uniquely identifies the information + * @param mixed $value The value + * + * @return $this + */ + public function setExtra(string $key, $value): self + { + $this->scopeData->setExtra($key, $value); + + return $this; + } + + /** + * Merges the given data into the current extras context. + * + * @param array $extras Data to merge into the current context + * + * @return $this + */ + public function setExtras(array $extras): self + { + $this->scopeData->setExtras(array_merge($this->scopeData->getExtra(), $extras)); + + return $this; + } + + /** + * Merges the given data in the user context. + * + * @param array|UserDataBag $user The user data + * + * @return $this + */ + public function setUser($user): self + { + $this->scopeData->setUser($user); + + return $this; + } + + /** + * Removes all data of the user context. + * + * @return $this + */ + public function removeUser(): self + { + $this->scopeData->removeUser(); + + return $this; + } + + /** + * Sets the list of strings used to dictate the deduplication of this event. + * + * @param string[] $fingerprint The fingerprint values + * + * @return $this + */ + public function setFingerprint(array $fingerprint): self + { + $this->scopeData->setFingerprint($fingerprint); + + return $this; + } + + /** + * Sets the severity to apply to all events captured in this scope. + * + * @param Severity|null $level The severity + * + * @return $this + */ + public function setLevel(?Severity $level): self + { + $this->scopeData->setLevel($level); + + return $this; + } + + /** + * Add the given breadcrumb to the scope. + * + * @param Breadcrumb $breadcrumb The breadcrumb to add + * @param int $maxBreadcrumbs The maximum number of breadcrumbs to record + * + * @return $this + */ + public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): self + { + $this->scopeData->addBreadcrumb($breadcrumb, $maxBreadcrumbs); + + return $this; + } + + /** + * Clears all the breadcrumbs. + * + * @return $this + */ + public function clearBreadcrumbs(): self + { + $this->scopeData->clearBreadcrumbs(); + + return $this; + } + + /** + * Adds a new event processor that will be called after {@see MergedScope::applyToEvent} + * finished its work. + * + * @param callable(Event, EventHint): ?Event $eventProcessor The event processor + * + * @return $this + */ + public function addEventProcessor(callable $eventProcessor): self + { + $this->scopeData->addEventProcessor($eventProcessor); + + return $this; + } + + /** + * Clears event payload data from the scope. The client binding and last + * event ID are preserved. + */ + public function clear(): void + { + $this->scopeData->clear(); + } + + public function __clone() + { + $this->scopeData = clone $this->scopeData; + } + + public function addAttachment(Attachment $attachment): self + { + $this->scopeData->addAttachment($attachment); + + return $this; + } + + public function clearAttachments(): self + { + $this->scopeData->setAttachments([]); + + return $this; + } +} diff --git a/src/State/RuntimeContext.php b/src/State/RuntimeContext.php index b0dfe177c..dcc097ef9 100644 --- a/src/State/RuntimeContext.php +++ b/src/State/RuntimeContext.php @@ -23,7 +23,7 @@ final class RuntimeContext private $id; /** - * @var Scope + * @var IsolationScope */ private $isolationScope; @@ -37,10 +37,10 @@ final class RuntimeContext */ private $metricsAggregator; - public function __construct(string $id, ?Scope $isolationScope = null) + public function __construct(string $id, ?IsolationScope $isolationScope = null) { $this->id = $id; - $this->isolationScope = $isolationScope ?? new Scope(); + $this->isolationScope = $isolationScope ?? new IsolationScope(); $this->logsAggregator = new LogsAggregator(); $this->metricsAggregator = new MetricsAggregator(); } @@ -50,12 +50,12 @@ public function getId(): string return $this->id; } - public function getIsolationScope(): Scope + public function getIsolationScope(): IsolationScope { return $this->isolationScope; } - public function setIsolationScope(Scope $isolationScope): void + public function setIsolationScope(IsolationScope $isolationScope): void { $this->isolationScope = $isolationScope; } diff --git a/src/State/Scope.php b/src/State/Scope.php index 0a53b3686..90ce93938 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -4,28 +4,17 @@ namespace Sentry\State; -use Sentry\Attachment\Attachment; -use Sentry\Breadcrumb; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; -use Sentry\EventId; -use Sentry\EventType; use Sentry\NoOpClient; -use Sentry\Options; -use Sentry\Severity; -use Sentry\Tracing\DynamicSamplingContext; -use Sentry\Tracing\PropagationContext; -use Sentry\Tracing\Span; -use Sentry\Tracing\Transaction; use Sentry\UserDataBag; -use Sentry\Util\DebugType; /** * The scope holds data that should implicitly be sent with Sentry events. It * can hold context data, extra parameters, level overrides, fingerprints etc. */ -class Scope +abstract class Scope { /** * Maximum number of flags allowed. We only track the first flags set. @@ -35,126 +24,28 @@ class Scope public const MAX_FLAGS = 100; /** - * @var PropagationContext - */ - private $propagationContext; - - /** - * @var ClientInterface The client bound to this scope - */ - private $client; - - /** - * @var EventId|null The ID of the last captured event - */ - private $lastEventId; - - /** - * @var Breadcrumb[] The list of breadcrumbs recorded in this scope - */ - private $breadcrumbs = []; - - /** - * @var UserDataBag|null The user data associated to this scope - */ - private $user; - - /** - * @var array> The list of contexts associated to this scope - */ - private $contexts = []; - - /** - * @var array The list of tags associated to this scope - */ - private $tags = []; - - /** - * @var array> The list of flags associated to this scope - */ - private $flags = []; - - /** - * @var array A set of extra data associated to this scope - */ - private $extra = []; - - /** - * @var string[] List of fingerprints used to group events together in - * Sentry - */ - private $fingerprint = []; - - /** - * @var Severity|null The severity to associate to the events captured in - * this scope - */ - private $level; - - /** - * @var callable[] List of event processors + * @internal * - * @phpstan-var array + * @var ScopeData */ - private $eventProcessors = []; - - /** - * @var Span|null Set a Span on the Scope - */ - private $span; - - /** - * @var Attachment[] - */ - private $attachments = []; + protected $scopeData; /** * @var callable[] List of event processors * * @phpstan-var array */ - private static $globalEventProcessors = []; + protected static $globalEventProcessors = []; /** * @var callable|null */ - private static $externalPropagationContextCallback; - - public function __construct(?PropagationContext $propagationContext = null) - { - $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); - $this->client = new NoOpClient(); - } + protected static $externalPropagationContextCallback; - /** - * Merges the process-global scope underneath the current isolation scope. - * - * The returned scope is transient and should be used for one event capture. - * - * @internal - */ - public static function mergeScopes(self $globalScope, self $isolationScope): self + public function __construct() { - $mergedScope = clone $isolationScope; - - $mergedScope->tags = array_merge($globalScope->tags, $isolationScope->tags); - $mergedScope->extra = array_merge($globalScope->extra, $isolationScope->extra); - $mergedScope->contexts = array_merge($globalScope->contexts, $isolationScope->contexts); - - if ($globalScope->user !== null && $isolationScope->user !== null) { - $mergedScope->user = (clone $globalScope->user)->merge($isolationScope->user); - } elseif ($globalScope->user !== null) { - $mergedScope->user = clone $globalScope->user; - } - - $mergedScope->level = $isolationScope->level ?? $globalScope->level; - $mergedScope->fingerprint = array_merge($globalScope->fingerprint, $isolationScope->fingerprint); - $mergedScope->breadcrumbs = \array_slice(array_merge($globalScope->breadcrumbs, $isolationScope->breadcrumbs), -100); - $mergedScope->flags = self::mergeFlags($globalScope->flags, $isolationScope->flags); - $mergedScope->attachments = array_merge($globalScope->attachments, $isolationScope->attachments); - $mergedScope->eventProcessors = array_merge($globalScope->eventProcessors, $isolationScope->eventProcessors); - - return $mergedScope; + $this->scopeData = new ScopeData(); + $this->scopeData->setClient(new NoOpClient()); } /** @@ -162,199 +53,7 @@ public static function mergeScopes(self $globalScope, self $isolationScope): sel */ public function getClient(): ClientInterface { - return $this->client; - } - - /** - * Sets the client bound to this scope. - * - * @return $this - */ - public function setClient(ClientInterface $client): self - { - $this->client = $client; - - return $this; - } - - /** - * Returns the ID of the last captured event. - */ - public function getLastEventId(): ?EventId - { - return $this->lastEventId; - } - - /** - * @internal - */ - public function setLastEventId(?EventId $lastEventId): void - { - $this->lastEventId = $lastEventId; - } - - /** - * @param array> $globalFlags - * @param array> $isolationFlags - * - * @return array> - */ - private static function mergeFlags(array $globalFlags, array $isolationFlags): array - { - $flagsByKey = []; - - foreach (array_merge($globalFlags, $isolationFlags) as $flag) { - $flagKey = key($flag); - - if ($flagKey === null) { - continue; - } - - unset($flagsByKey[$flagKey]); - $flagsByKey[$flagKey] = current($flag); - } - - $flagsByKey = \array_slice($flagsByKey, -self::MAX_FLAGS, self::MAX_FLAGS, true); - - $flags = []; - - foreach ($flagsByKey as $flagKey => $flagResult) { - $flags[] = [$flagKey => $flagResult]; - } - - return $flags; - } - - /** - * Sets a new tag in the tags context. - * - * @param string $key The key that uniquely identifies the tag - * @param string $value The value - * - * @return $this - */ - public function setTag(string $key, string $value): self - { - $this->tags[$key] = $value; - - return $this; - } - - /** - * Merges the given tags into the current tags context. - * - * @param array $tags The tags to merge into the current context - * - * @return $this - */ - public function setTags(array $tags): self - { - $this->tags = array_merge($this->tags, $tags); - - return $this; - } - - /** - * Removes a given tag from the tags context. - * - * @param string $key The key that uniquely identifies the tag - * - * @return $this - */ - public function removeTag(string $key): self - { - unset($this->tags[$key]); - - return $this; - } - - /** - * Adds a feature flag to the scope. - * - * @return $this - */ - public function addFeatureFlag(string $key, bool $result): self - { - // If the flag was already set, remove it first - // This basically mimics an LRU cache so that the most recently added flags are kept - foreach ($this->flags as $flagIndex => $flag) { - if (isset($flag[$key])) { - unset($this->flags[$flagIndex]); - } - } - - // Keep only the most recent MAX_FLAGS flags - if (\count($this->flags) >= self::MAX_FLAGS) { - array_shift($this->flags); - } - - $this->flags[] = [$key => $result]; - - if ($this->span !== null) { - $this->span->setFlag($key, $result); - } - - return $this; - } - - /** - * Sets data to the context by a given name. - * - * @param string $name The name that uniquely identifies the context - * @param array $value The value - * - * @return $this - */ - public function setContext(string $name, array $value): self - { - if (!empty($value)) { - $this->contexts[$name] = $value; - } - - return $this; - } - - /** - * Removes the context from the scope. - * - * @param string $name The name that uniquely identifies the context - * - * @return $this - */ - public function removeContext(string $name): self - { - unset($this->contexts[$name]); - - return $this; - } - - /** - * Sets a new information in the extra context. - * - * @param string $key The key that uniquely identifies the information - * @param mixed $value The value - * - * @return $this - */ - public function setExtra(string $key, $value): self - { - $this->extra[$key] = $value; - - return $this; - } - - /** - * Merges the given data into the current extras context. - * - * @param array $extras Data to merge into the current context - * - * @return $this - */ - public function setExtras(array $extras): self - { - $this->extra = array_merge($this->extra, $extras); - - return $this; + return $this->scopeData->getClient(); } /** @@ -362,120 +61,11 @@ public function setExtras(array $extras): self */ public function getUser(): ?UserDataBag { - return $this->user; - } - - /** - * Merges the given data in the user context. - * - * @param array|UserDataBag $user The user data - * - * @return $this - */ - public function setUser($user): self - { - if (!\is_array($user) && !$user instanceof UserDataBag) { - throw new \TypeError(\sprintf('The $user argument must be either an array or an instance of the "%s" class. Got: "%s".', UserDataBag::class, DebugType::getDebugType($user))); - } - - if (\is_array($user)) { - $user = UserDataBag::createFromArray($user); - } - - if ($this->user === null) { - $this->user = $user; - } else { - $this->user = $this->user->merge($user); - } - - return $this; - } - - /** - * Removes all data of the user context. - * - * @return $this - */ - public function removeUser(): self - { - $this->user = null; - - return $this; - } - - /** - * Sets the list of strings used to dictate the deduplication of this event. - * - * @param string[] $fingerprint The fingerprint values - * - * @return $this - */ - public function setFingerprint(array $fingerprint): self - { - $this->fingerprint = $fingerprint; - - return $this; - } - - /** - * Sets the severity to apply to all events captured in this scope. - * - * @param Severity|null $level The severity - * - * @return $this - */ - public function setLevel(?Severity $level): self - { - $this->level = $level; - - return $this; + return $this->scopeData->getUser(); } /** - * Add the given breadcrumb to the scope. - * - * @param Breadcrumb $breadcrumb The breadcrumb to add - * @param int $maxBreadcrumbs The maximum number of breadcrumbs to record - * - * @return $this - */ - public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): self - { - $this->breadcrumbs[] = $breadcrumb; - $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); - - return $this; - } - - /** - * Clears all the breadcrumbs. - * - * @return $this - */ - public function clearBreadcrumbs(): self - { - $this->breadcrumbs = []; - - return $this; - } - - /** - * Adds a new event processor that will be called after {@see Scope::applyToEvent} - * finished its work. - * - * @param callable $eventProcessor The event processor - * - * @return $this - */ - public function addEventProcessor(callable $eventProcessor): self - { - $this->eventProcessors[] = $eventProcessor; - - return $this; - } - - /** - * Adds a new event processor that will be called after {@see Scope::applyToEvent} + * Adds a new event processor that will be called after {@see MergedScope::applyToEvent} * finished its work. * * @param callable $eventProcessor The event processor @@ -531,235 +121,4 @@ public static function getExternalPropagationContext(): ?array 'span_id' => $spanId, ]; } - - /** - * Clears event payload data from the scope. The client binding and last - * event ID are preserved. - * - * @return $this - */ - public function clear(): self - { - $this->user = null; - $this->level = null; - $this->span = null; - $this->fingerprint = []; - $this->breadcrumbs = []; - $this->tags = []; - $this->flags = []; - $this->extra = []; - $this->contexts = []; - $this->attachments = []; - - return $this; - } - - /** - * Applies the current context and fingerprint to the event. If the event has - * already some breadcrumbs on it, the ones from this scope won't get merged. - * - * @param Event $event The event object that will be enriched with scope data - */ - public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event - { - $event->setFingerprint(array_merge($event->getFingerprint(), $this->fingerprint)); - - if (empty($event->getBreadcrumbs())) { - $event->setBreadcrumb($this->breadcrumbs); - } - - if ($this->level !== null) { - $event->setLevel($this->level); - } - - if (!empty($this->tags)) { - $event->setTags(array_merge($this->tags, $event->getTags())); - } - - if (!empty($this->flags)) { - $event->setContext('flags', [ - 'values' => array_map(static function (array $flag) { - return [ - 'flag' => key($flag), - 'result' => current($flag), - ]; - }, array_values($this->flags)), - ]); - } - - if (!empty($this->extra)) { - $event->setExtra(array_merge($this->extra, $event->getExtra())); - } - - if ($this->user !== null) { - $user = $event->getUser(); - - if ($user === null) { - $user = $this->user; - } else { - $user = $this->user->merge($user); - } - - $event->setUser($user); - } - - /** - * Apply the trace context to errors if there is a Span on the Scope. - * Else fallback to the external propagation context or to the - * propagation context. - * But do not override a trace context already present. - */ - $externalPropagationContext = null; - if ($this->span === null) { - $externalPropagationContext = self::getExternalPropagationContext(); - } - - $traceContext = $this->span !== null - ? $this->span->getTraceContext() - : ($externalPropagationContext ?? $this->propagationContext->getTraceContext()); - - if (!\array_key_exists('trace', $event->getContexts())) { - $event->setContext('trace', $traceContext); - } - - if ($this->span !== null) { - // Apply the dynamic sampling context to errors if there is a Transaction on the Scope - $transaction = $this->span->getTransaction(); - if ($transaction !== null) { - $event->setSdkMetadata('dynamic_sampling_context', $transaction->getDynamicSamplingContext()); - } - } elseif ($externalPropagationContext === null) { - $dynamicSamplingContext = $this->propagationContext->getDynamicSamplingContext(); - if ($dynamicSamplingContext === null && $options !== null) { - $dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $this); - } - $event->setSdkMetadata('dynamic_sampling_context', $dynamicSamplingContext); - } - - foreach (array_merge($this->contexts, $event->getContexts()) as $name => $data) { - $event->setContext($name, $data); - } - - // We create a empty `EventHint` instance to allow processors to always receive a `EventHint` instance even if there wasn't one - if ($hint === null) { - $hint = new EventHint(); - } - - if ($event->getType() === EventType::event() || $event->getType() === EventType::transaction()) { - if (empty($event->getAttachments())) { - $event->setAttachments($this->attachments); - } - } - - foreach (array_merge(self::$globalEventProcessors, $this->eventProcessors) as $processor) { - $event = $processor($event, $hint); - - if ($event === null) { - return null; - } - - if (!$event instanceof Event) { - throw new \InvalidArgumentException(\sprintf('The event processor must return null or an instance of the %s class', Event::class)); - } - } - - return $event; - } - - /** - * Returns the span that is on the scope. - */ - public function getSpan(): ?Span - { - return $this->span; - } - - /** - * Sets the span on the scope. - * - * @param Span|null $span The span - * - * @return $this - */ - public function setSpan(?Span $span): self - { - $this->span = $span; - - return $this; - } - - /** - * Returns the transaction attached to the scope (if there is one). - */ - public function getTransaction(): ?Transaction - { - if ($this->span !== null) { - return $this->span->getTransaction(); - } - - return null; - } - - public function hasExternalPropagationContext(): bool - { - return $this->span === null && self::getExternalPropagationContext() !== null; - } - - /** - * @return array{ - * trace_id: string, - * span_id: string, - * parent_span_id?: string, - * data?: array, - * description?: string, - * op?: string, - * status?: string, - * tags?: array, - * origin?: string - * } - */ - public function getTraceContext(): array - { - if ($this->span !== null) { - return $this->span->getTraceContext(); - } - - return self::getExternalPropagationContext() ?? $this->propagationContext->getTraceContext(); - } - - public function getPropagationContext(): PropagationContext - { - return $this->propagationContext; - } - - public function setPropagationContext(PropagationContext $propagationContext): self - { - $this->propagationContext = $propagationContext; - - return $this; - } - - public function __clone() - { - if ($this->user !== null) { - $this->user = clone $this->user; - } - if ($this->propagationContext !== null) { - $this->propagationContext = clone $this->propagationContext; - } - } - - public function addAttachment(Attachment $attachment): self - { - $this->attachments[] = $attachment; - - return $this; - } - - public function clearAttachments(): self - { - $this->attachments = []; - - return $this; - } } diff --git a/src/State/ScopeData.php b/src/State/ScopeData.php new file mode 100644 index 000000000..eab5336a3 --- /dev/null +++ b/src/State/ScopeData.php @@ -0,0 +1,405 @@ +> The list of contexts associated to this scope + */ + private $contexts = []; + + /** + * @var array The list of tags associated to this scope + */ + private $tags = []; + + /** + * @var array> The list of flags associated to this scope + */ + private $flags = []; + + /** + * @var array A set of extra data associated to this scope + */ + private $extra = []; + + /** + * @var string[] List of fingerprints used to group events together in + * Sentry + */ + private $fingerprint = []; + + /** + * @var Severity|null The severity to associate to the events captured in + * this scope + */ + private $level; + + /** + * @var callable[] List of event processors + * + * @phpstan-var array + */ + private $eventProcessors = []; + + /** + * @var Attachment[] + */ + private $attachments = []; + + public function __construct(?PropagationContext $propagationContext = null) + { + $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); + } + + public function getPropagationContext(): PropagationContext + { + return $this->propagationContext; + } + + public function setPropagationContext(PropagationContext $propagationContext): void + { + $this->propagationContext = $propagationContext; + } + + public function getClient(): ClientInterface + { + return $this->client; + } + + public function setClient(ClientInterface $client): void + { + $this->client = $client; + } + + /** + * @return Breadcrumb[] + */ + public function getBreadcrumbs(): array + { + return $this->breadcrumbs; + } + + public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): void + { + $this->breadcrumbs[] = $breadcrumb; + $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); + } + + public function clearBreadcrumbs(): void + { + $this->breadcrumbs = []; + } + + public function getUser(): ?UserDataBag + { + return $this->user; + } + + /** + * @param array|UserDataBag $user + */ + public function setUser($user): void + { + if (!\is_array($user) && !$user instanceof UserDataBag) { + throw new \TypeError(\sprintf('The $user argument must be either an array or an instance of the "%s" class. Got: "%s".', UserDataBag::class, DebugType::getDebugType($user))); + } + + if (\is_array($user)) { + $user = UserDataBag::createFromArray($user); + } + + if ($this->user === null) { + $this->user = $user; + } else { + $this->user = $this->user->merge($user); + } + } + + public function removeUser(): void + { + $this->user = null; + } + + /** + * @return array> + */ + public function getContexts(): array + { + return $this->contexts; + } + + /** + * @param array $value + */ + public function setContext(string $name, array $value): void + { + $this->contexts[$name] = $value; + } + + public function removeContext(string $key): void + { + unset($this->contexts[$key]); + } + + /** + * @return array + */ + public function getTags(): array + { + return $this->tags; + } + + /** + * @param array $tags + */ + public function setTags(array $tags): void + { + $this->tags = $tags; + } + + public function setTag(string $key, string $value): void + { + $this->tags[$key] = $value; + } + + public function removeTag(string $key): void + { + unset($this->tags[$key]); + } + + /** + * @return array> + */ + public function getFlags(): array + { + return $this->flags; + } + + public function addFeatureFlag(string $key, bool $result): self + { + // If the flag was already set, remove it first + // This basically mimics an LRU cache so that the most recently added flags are kept + foreach ($this->flags as $flagIndex => $flag) { + if (isset($flag[$key])) { + unset($this->flags[$flagIndex]); + } + } + + // Keep only the most recent MAX_FLAGS flags + if (\count($this->flags) >= Scope::MAX_FLAGS) { + array_shift($this->flags); + } + + $this->flags[] = [$key => $result]; + + return $this; + } + + /** + * @return array + */ + public function getExtra(): array + { + return $this->extra; + } + + /** + * @param array $extra + */ + public function setExtras(array $extra): void + { + $this->extra = $extra; + } + + /** + * @param mixed $value + */ + public function setExtra(string $key, $value): void + { + $this->extra[$key] = $value; + } + + /** + * @return string[] + */ + public function getFingerprint(): array + { + return $this->fingerprint; + } + + /** + * @param string[] $fingerprint + */ + public function setFingerprint(array $fingerprint): void + { + $this->fingerprint = $fingerprint; + } + + public function getLevel(): ?Severity + { + return $this->level; + } + + public function setLevel(?Severity $level): void + { + $this->level = $level; + } + + /** + * @return array + */ + public function getEventProcessors(): array + { + return $this->eventProcessors; + } + + /** + * @param callable(Event, EventHint): ?Event $eventProcessor + */ + public function addEventProcessor(callable $eventProcessor): void + { + $this->eventProcessors[] = $eventProcessor; + } + + /** + * @return Attachment[] + */ + public function getAttachments(): array + { + return $this->attachments; + } + + /** + * @param Attachment[] $attachments + */ + public function setAttachments(array $attachments): void + { + $this->attachments = $attachments; + } + + public function addAttachment(Attachment $attachment): void + { + $this->attachments[] = $attachment; + } + + public function clear(): void + { + $this->user = null; + $this->level = null; + $this->fingerprint = []; + $this->breadcrumbs = []; + $this->tags = []; + $this->flags = []; + $this->extra = []; + $this->contexts = []; + $this->attachments = []; + } + + public function __clone() + { + if ($this->user !== null) { + $this->user = clone $this->user; + } + $this->propagationContext = clone $this->propagationContext; + } + + /** + * Merges data of one ScopeData object into the other one. Data stored in $other will have precedence over + * $this. It is generally assumed $this refers to the global scope while $other is the isolation scope. + */ + public function merge(self $other): self + { + $merged = clone $other; + $merged->tags = array_merge($this->tags, $other->tags); + $merged->extra = array_merge($this->extra, $other->extra); + $merged->contexts = array_merge($this->contexts, $other->contexts); + + if ($this->user !== null && $other->user !== null) { + $merged->user = (clone $this->user)->merge($other->user); + } elseif ($this->user !== null) { + $merged->user = clone $this->user; + } + + $merged->level = $other->level ?? $this->level; + $merged->fingerprint = array_merge($this->fingerprint, $other->fingerprint); + $merged->breadcrumbs = \array_slice(array_merge($this->breadcrumbs, $other->breadcrumbs), -100); + $merged->flags = self::mergeFlags($this->flags, $other->flags); + $merged->attachments = array_merge($this->attachments, $other->attachments); + $merged->eventProcessors = array_merge($this->eventProcessors, $other->eventProcessors); + + return $merged; + } + + /** + * @param array> $globalFlags + * @param array> $isolationFlags + * + * @return array> + */ + private static function mergeFlags(array $globalFlags, array $isolationFlags): array + { + $flagsByKey = []; + + foreach (array_merge($globalFlags, $isolationFlags) as $flag) { + $flagKey = key($flag); + + if ($flagKey === null) { + continue; + } + + unset($flagsByKey[$flagKey]); + $flagsByKey[$flagKey] = current($flag); + } + + $flagsByKey = \array_slice($flagsByKey, -Scope::MAX_FLAGS, Scope::MAX_FLAGS, true); + + $flags = []; + + foreach ($flagsByKey as $flagKey => $flagResult) { + $flags[] = [$flagKey => $flagResult]; + } + + return $flags; + } +} diff --git a/src/Tracing/DynamicSamplingContext.php b/src/Tracing/DynamicSamplingContext.php index 588eead06..714aab6ff 100644 --- a/src/Tracing/DynamicSamplingContext.php +++ b/src/Tracing/DynamicSamplingContext.php @@ -6,7 +6,7 @@ use Sentry\ClientInterface; use Sentry\Options; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; /** * This class represents the Dynamic Sampling Context (dsc). @@ -179,11 +179,19 @@ public static function fromTransaction(Transaction $transaction, ClientInterface return $samplingContext; } - public static function fromOptions(Options $options, Scope $scope): self + public static function fromOptions(Options $options, IsolationScope $scope): self + { + return self::fromOptionsAndPropagationContext($options, $scope->getPropagationContext()); + } + + /** + * @internal + */ + public static function fromOptionsAndPropagationContext(Options $options, PropagationContext $propagationContext): self { $samplingContext = new self(); - $samplingContext->set('trace_id', (string) $scope->getPropagationContext()->getTraceId()); - $samplingContext->set('sample_rand', (string) $scope->getPropagationContext()->getSampleRand()); + $samplingContext->set('trace_id', (string) $propagationContext->getTraceId()); + $samplingContext->set('sample_rand', (string) $propagationContext->getSampleRand()); if ($options->getTracesSampleRate() !== null) { $samplingContext->set('sample_rate', (string) $options->getTracesSampleRate()); diff --git a/src/Tracing/GuzzleTracingMiddleware.php b/src/Tracing/GuzzleTracingMiddleware.php index 27cdca32a..50d73466d 100644 --- a/src/Tracing/GuzzleTracingMiddleware.php +++ b/src/Tracing/GuzzleTracingMiddleware.php @@ -12,7 +12,7 @@ use Sentry\ClientInterface; use Sentry\SentrySdk; use Sentry\State\BreadcrumbRecorder; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\getBaggage; use function Sentry\getTraceparent; @@ -22,7 +22,7 @@ */ final class GuzzleTracingMiddleware { - public static function trace(?Scope $scope = null): \Closure + public static function trace(?IsolationScope $scope = null): \Closure { return static function (callable $handler) use ($scope): \Closure { return static function (RequestInterface $request, array $options) use ($handler, $scope) { diff --git a/src/functions.php b/src/functions.php index 9db1674d4..bb234f982 100644 --- a/src/functions.php +++ b/src/functions.php @@ -12,6 +12,8 @@ use Sentry\Metrics\TraceMetrics; use Sentry\State\BreadcrumbRecorder; use Sentry\State\EventCapturer; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanContext; @@ -79,12 +81,12 @@ function init(array $options = []): void SentrySdk::init($client); } -function getGlobalScope(): Scope +function getGlobalScope(): GlobalScope { return SentrySdk::getGlobalScope(); } -function getIsolationScope(): Scope +function getIsolationScope(): IsolationScope { return SentrySdk::getIsolationScope(); } @@ -244,7 +246,7 @@ function withScope(callable $callback) * * @phpstan-template T * - * @phpstan-param callable(Scope): T $callback + * @phpstan-param callable(IsolationScope): T $callback * * @return mixed|void The callback's return value, upon successful execution * @@ -323,14 +325,14 @@ function startTransaction(TransactionContext $context, array $customSamplingCont * * @template T * - * @param callable(Scope): T $trace The callable that is going to be traced - * @param SpanContext $context The context of the span to be created + * @param callable(IsolationScope): T $trace The callable that is going to be traced + * @param SpanContext $context The context of the span to be created * * @return T */ function trace(callable $trace, SpanContext $context) { - return withIsolationScope(static function (Scope $scope) use ($context, $trace) { + return withIsolationScope(static function (IsolationScope $scope) use ($context, $trace) { $parentSpan = $scope->getSpan(); $span = null; @@ -381,7 +383,7 @@ function getOtlpTracesEndpointUrl(): ?string * This function is context aware, as in it either returns the traceparent based * on the current span, or the scope's propagation context. */ -function getTraceparent(?Scope $scope = null): string +function getTraceparent(?IsolationScope $scope = null): string { $scope = $scope ?? SentrySdk::getIsolationScope(); $client = SentrySdk::getClient($scope); @@ -407,7 +409,7 @@ function getTraceparent(?Scope $scope = null): string * This function is context aware, as in it either returns the baggage based * on the current span or the scope's propagation context. */ -function getBaggage(?Scope $scope = null): string +function getBaggage(?IsolationScope $scope = null): string { $scope = $scope ?? SentrySdk::getIsolationScope(); $client = SentrySdk::getClient($scope); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6f23fba98..c6fef94c2 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -20,6 +20,7 @@ use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\Severity; use Sentry\Stacktrace; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; @@ -69,7 +70,7 @@ public function setupOnce(): void $logger ); - $client->captureEvent(Event::createEvent(), null, new Scope()); + $client->captureEvent(Event::createEvent(), null, new IsolationScope()); $this->assertTrue($integrationCalled); } @@ -966,7 +967,7 @@ public function testProcessEventDiscardsEventWhenEventProcessorReturnsNull(): vo ->setLogger($logger) ->getClient(); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addEventProcessor(static function () { return null; }); diff --git a/tests/Fixtures/runtime/frankenphp/index.php b/tests/Fixtures/runtime/frankenphp/index.php index f6f615299..e0367175e 100644 --- a/tests/Fixtures/runtime/frankenphp/index.php +++ b/tests/Fixtures/runtime/frankenphp/index.php @@ -4,7 +4,6 @@ use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; use function Sentry\getTraceparent; use function Sentry\init; @@ -50,7 +49,7 @@ } $event = Event::createEvent(); - $event = Scope::mergeScopes(SentrySdk::getGlobalScope(), SentrySdk::getIsolationScope())->applyToEvent($event); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $tags = []; diff --git a/tests/Fixtures/runtime/roadrunner-worker.php b/tests/Fixtures/runtime/roadrunner-worker.php index a5cc7c648..f8d6bf79d 100644 --- a/tests/Fixtures/runtime/roadrunner-worker.php +++ b/tests/Fixtures/runtime/roadrunner-worker.php @@ -6,7 +6,6 @@ use Nyholm\Psr7\Response; use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; use Spiral\RoadRunner\Http\PSR7Worker; use Spiral\RoadRunner\Worker; @@ -106,7 +105,7 @@ function handleRequest($request): Response } $event = Event::createEvent(); - $event = Scope::mergeScopes(SentrySdk::getGlobalScope(), SentrySdk::getIsolationScope())->applyToEvent($event); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $tags = []; diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 6e3984c91..c0e597a4f 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -19,6 +19,8 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Severity; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\Span; @@ -74,7 +76,7 @@ public function testInitPreservesGlobalScope(): void $this->assertSame($globalScope, SentrySdk::getGlobalScope()); $this->assertSame(SentrySdk::getClient(), $globalScope->getClient()); - $event = $globalScope->applyToEvent(Event::createEvent()); + $event = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['baseline' => 'yes'], $event->getTags()); @@ -365,7 +367,7 @@ public function testWithMonitor(): void && $checkIn->getMonitorConfig() !== null && $checkIn->getMonitorConfig()->getSchedule()->getValue() === '*/5 * * * *'; }), null, $this->captureScopeConstraint($scope)) - ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?Scope $scope = null) use (&$events): EventId { + ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null) use (&$events): EventId { $events[] = $event; return EventId::generate(); @@ -396,7 +398,7 @@ public function testWithMonitorCallableThrows(): void $client->expects($this->exactly(2)) ->method('captureEvent') ->with($this->isInstanceOf(Event::class), null, $this->captureScopeConstraint($scope)) - ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?Scope $scope = null) use (&$events): EventId { + ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null) use (&$events): EventId { $events[] = $event; return EventId::generate(); @@ -426,7 +428,7 @@ public function testWithMonitorCallableThrows(): void public function testAddBreadcrumb(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - $otherScope = new Scope(); + $otherScope = new IsolationScope(); /** @var ClientInterface&MockObject $client */ $client = $this->createMock(ClientInterface::class); @@ -523,23 +525,23 @@ public function testConfigureScopeMutatesCurrentIsolationScopeOnly(): void $globalScope = SentrySdk::getGlobalScope(); $globalScope->setTag('scope', 'global'); - $isolationScope = new Scope(); + $isolationScope = new IsolationScope(); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); $callbackScope = null; - configureScope(static function (Scope $scope) use (&$callbackScope): void { + configureScope(static function (IsolationScope $scope) use (&$callbackScope): void { $callbackScope = $scope; $scope->setTag('scope', 'isolation'); }); $this->assertSame($isolationScope, $callbackScope); - $isolationEvent = $isolationScope->applyToEvent(Event::createEvent()); + $isolationEvent = (new GlobalScope())->merge($isolationScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($isolationEvent); $this->assertSame(['scope' => 'isolation'], $isolationEvent->getTags()); - $globalEvent = $globalScope->applyToEvent(Event::createEvent()); + $globalEvent = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); $this->assertNotNull($globalEvent); $this->assertSame(['scope' => 'global'], $globalEvent->getTags()); } @@ -547,14 +549,13 @@ public function testConfigureScopeMutatesCurrentIsolationScopeOnly(): void public function testAddFeatureFlagMutatesCurrentIsolationScopeOnly(): void { $globalScope = SentrySdk::getGlobalScope(); - $globalScope->addFeatureFlag('global-only', true); - $isolationScope = new Scope(); + $isolationScope = new IsolationScope(); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); addFeatureFlag('isolation-only', false); - $isolationEvent = $isolationScope->applyToEvent(Event::createEvent()); + $isolationEvent = (new GlobalScope())->merge($isolationScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($isolationEvent); $this->assertSame([ 'values' => [ @@ -565,16 +566,9 @@ public function testAddFeatureFlagMutatesCurrentIsolationScopeOnly(): void ], ], $isolationEvent->getContexts()['flags']); - $globalEvent = $globalScope->applyToEvent(Event::createEvent()); + $globalEvent = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); $this->assertNotNull($globalEvent); - $this->assertSame([ - 'values' => [ - [ - 'flag' => 'global-only', - 'result' => true, - ], - ], - ], $globalEvent->getContexts()['flags']); + $this->assertArrayNotHasKey('flags', $globalEvent->getContexts()); } public function testStartAndEndContext(): void @@ -621,7 +615,7 @@ public function testNestedWithContextReusesOuterContext(): void withContext(function () use (&$outerScope, &$innerScope, $globalScope): void { $outerScope = SentrySdk::getIsolationScope(); - configureScope(static function (Scope $scope): void { + configureScope(static function (IsolationScope $scope): void { $scope->setTag('outer', 'yes'); }); @@ -631,8 +625,8 @@ public function testNestedWithContextReusesOuterContext(): void $event = Event::createEvent(); - configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); + configureScope(static function (IsolationScope $scope) use (&$event): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); }); $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); @@ -710,7 +704,7 @@ public function testTraceCorrectlyReplacesAndRestoresCurrentSpan(): void $childSpan = null; - trace(function (Scope $scope) use ($outerScope, $transaction, &$childSpan): void { + trace(function (IsolationScope $scope) use ($outerScope, $transaction, &$childSpan): void { $childSpan = $scope->getSpan(); $this->assertNotSame($outerScope, $scope); @@ -725,7 +719,7 @@ public function testTraceCorrectlyReplacesAndRestoresCurrentSpan(): void $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); try { - trace(function (Scope $scope) use ($transaction): void { + trace(function (IsolationScope $scope) use ($transaction): void { $this->assertNotSame($transaction, $scope->getSpan()); throw new \RuntimeException('Throwing should still restore the previous span'); @@ -745,7 +739,7 @@ public function testTraceDoesntCreateSpanIfTransactionIsNotSampled(): void $outerScope->setSpan($transaction); $callbackScope = null; - trace(function (Scope $scope) use ($transaction, &$callbackScope): void { + trace(function (IsolationScope $scope) use ($transaction, &$callbackScope): void { $callbackScope = $scope; $this->assertSame($transaction, $scope->getSpan()); @@ -763,7 +757,7 @@ public function testTraceparentWithTracingDisabled(): void $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $propagationContext->setSpanId(new SpanId('566e3688a61d4bc8')); - SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $traceParent = getTraceparent(); @@ -807,7 +801,7 @@ public function testTraceHeadersAreEmptyWhenExternalPropagationContextIsActive() ]; }); - SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $this->assertSame('', getTraceparent()); $this->assertSame('', getBaggage()); @@ -830,7 +824,7 @@ public function testBaggageWithTracingDisabled(): void ])); SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $baggage = getBaggage(); @@ -910,7 +904,7 @@ public function testContinueTrace(): void { SentrySdk::getGlobalScope()->setClient(new NoOpClient()); - $scope = new Scope(); + $scope = new IsolationScope(); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( @@ -945,7 +939,7 @@ public function testContinueTraceWhenOrgMismatch(): void SentrySdk::getGlobalScope()->setClient($client); - $scope = new Scope(); + $scope = new IsolationScope(); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( @@ -983,7 +977,7 @@ public function testContinueTraceWhenOrgMatch(): void SentrySdk::getGlobalScope()->setClient($client); - $scope = new Scope(); + $scope = new IsolationScope(); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( @@ -1006,7 +1000,7 @@ public function testContinueTraceWhenOrgMatch(): void $this->assertSame('1', $dynamicSamplingContext->get('org_id')); } - private function setClientAndIsolationScope(ClientInterface $client): Scope + private function setClientAndIsolationScope(ClientInterface $client): IsolationScope { SentrySdk::init(); @@ -1015,7 +1009,7 @@ private function setClientAndIsolationScope(ClientInterface $client): Scope SentrySdk::getGlobalScope()->setTag('global', 'yes'); SentrySdk::getGlobalScope()->setClient($client); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setTag('scope', 'isolation'); $scope->setTag('isolation', 'yes'); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); @@ -1023,12 +1017,12 @@ private function setClientAndIsolationScope(ClientInterface $client): Scope return $scope; } - private function captureScopeConstraint(Scope $isolationScope) + private function captureScopeConstraint(IsolationScope $isolationScope) { - return $this->callback(function (Scope $captureScope) use ($isolationScope): bool { - $this->assertNotSame($isolationScope, $captureScope); + return $this->callback(function (IsolationScope $captureScope) use ($isolationScope): bool { + $this->assertSame($isolationScope, $captureScope); - $event = $captureScope->applyToEvent(Event::createEvent()); + $event = SentrySdk::getGlobalScope()->merge($captureScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -1044,9 +1038,9 @@ private function captureScopeConstraint(Scope $isolationScope) /** * @param Breadcrumb[] $expectedBreadcrumbs */ - private function assertScopeBreadcrumbs(Scope $scope, array $expectedBreadcrumbs): void + private function assertScopeBreadcrumbs(IsolationScope $scope, array $expectedBreadcrumbs): void { - $event = $scope->applyToEvent(Event::createEvent()); + $event = (new GlobalScope())->merge($scope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame($expectedBreadcrumbs, $event->getBreadcrumbs()); diff --git a/tests/Integration/EnvironmentIntegrationTest.php b/tests/Integration/EnvironmentIntegrationTest.php index 7391acbce..3c7a66fe7 100644 --- a/tests/Integration/EnvironmentIntegrationTest.php +++ b/tests/Integration/EnvironmentIntegrationTest.php @@ -12,7 +12,7 @@ use Sentry\Event; use Sentry\Integration\EnvironmentIntegration; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Util\PHPVersion; use function Sentry\withScope; @@ -35,12 +35,12 @@ public function testInvoke(bool $isIntegrationEnabled, ?RuntimeContext $initialR SentrySdk::init($client); - withScope(function (Scope $scope) use ($expectedRuntimeContext, $expectedOsContext, $initialRuntimeContext, $initialOsContext): void { + withScope(function (IsolationScope $scope) use ($expectedRuntimeContext, $expectedOsContext, $initialRuntimeContext, $initialOsContext): void { $event = Event::createEvent(); $event->setRuntimeContext($initialRuntimeContext); $event->setOsContext($initialOsContext); - $event = $scope->applyToEvent($event); + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); $this->assertNotNull($event); diff --git a/tests/Integration/FrameContextifierIntegrationTest.php b/tests/Integration/FrameContextifierIntegrationTest.php index f13ba8e7d..ac49a4388 100644 --- a/tests/Integration/FrameContextifierIntegrationTest.php +++ b/tests/Integration/FrameContextifierIntegrationTest.php @@ -14,7 +14,7 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Stacktrace; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withScope; @@ -49,8 +49,8 @@ public function testInvoke(string $fixtureFilePath, int $lineNumber, int $contex $event = Event::createEvent(); $event->setStacktrace($stacktrace); - withScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); + withScope(static function (IsolationScope $scope) use (&$event): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); }); $this->assertNotNull($event); @@ -143,8 +143,8 @@ public function testInvokeLogsWarningMessageIfSourceCodeExcerptCannotBeRetrieved $event = Event::createEvent(); $event->setStacktrace($stacktrace); - withScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); + withScope(static function (IsolationScope $scope) use (&$event): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); }); $this->assertNotNull($event); diff --git a/tests/Integration/ModulesIntegrationTest.php b/tests/Integration/ModulesIntegrationTest.php index 659a50398..33abcc502 100644 --- a/tests/Integration/ModulesIntegrationTest.php +++ b/tests/Integration/ModulesIntegrationTest.php @@ -11,7 +11,7 @@ use Sentry\Event; use Sentry\Integration\ModulesIntegration; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; @@ -36,8 +36,8 @@ public function testInvoke(bool $isIntegrationEnabled, bool $expectedEmptyModule SentrySdk::init($client); - withScope(function (Scope $scope) use ($expectedEmptyModules): void { - $event = $scope->applyToEvent(Event::createEvent()); + withScope(function (IsolationScope $scope) use ($expectedEmptyModules): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); @@ -88,7 +88,7 @@ public function testModuleIntegration(): void SentrySdk::init($client); - $client->captureEvent(Event::createEvent(), null, new Scope()); + $client->captureEvent(Event::createEvent(), null, new IsolationScope()); } /** diff --git a/tests/Integration/RequestIntegrationTest.php b/tests/Integration/RequestIntegrationTest.php index c08cc3e1c..7de6c166c 100644 --- a/tests/Integration/RequestIntegrationTest.php +++ b/tests/Integration/RequestIntegrationTest.php @@ -16,7 +16,7 @@ use Sentry\Integration\RequestIntegration; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\UserDataBag; use function Sentry\withScope; @@ -46,8 +46,8 @@ public function testInvoke(array $options, ServerRequestInterface $request, arra SentrySdk::init($client); - withScope(function (Scope $scope) use ($event, $expectedRequestContextData, $expectedUser): void { - $event = $scope->applyToEvent($event); + withScope(function (IsolationScope $scope) use ($event, $expectedRequestContextData, $expectedUser): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); $this->assertNotNull($event); $this->assertSame($expectedRequestContextData, $event->getRequest()); diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index 1faf42176..91bced84a 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -11,7 +11,7 @@ use Sentry\EventHint; use Sentry\Integration\TransactionIntegration; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withScope; @@ -37,8 +37,8 @@ public function testSetupOnce(Event $event, bool $isIntegrationEnabled, ?EventHi SentrySdk::init($client); - withScope(function (Scope $scope) use ($event, $hint, $expectedTransaction): void { - $event = $scope->applyToEvent($event, $hint); + withScope(function (IsolationScope $scope) use ($event, $hint, $expectedTransaction): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event, $hint); $this->assertNotNull($event); $this->assertSame($event->getTransaction(), $expectedTransaction); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index a62d0bd47..02621a0bc 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -13,6 +13,7 @@ use Sentry\Logs\LogsAggregator; use Sentry\Options; use Sentry\SentrySdk; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tests\StubTransport; use Sentry\Tracing\PropagationContext; @@ -208,7 +209,7 @@ public function testAttributesAreAddedToLogMessage(): void $this->assertSame('my_user', $attributes->get('user.name')->getValue()); } - public function testMergedScopeAttributesAreAddedToLogMessage(): void + public function testGlobalScopeAttributesAreAddedToLogMessage(): void { $client = ClientBuilder::create([ 'enable_logs' => true, @@ -354,7 +355,7 @@ public function testDoesNotUsePropagationContextSpanIdAsParentSpanIdWhenNoLocalS $propagationContext->setSpanId(new SpanId('1234567890abcdef')); SentrySdk::init($client); - SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $aggregator = new LogsAggregator(); $aggregator->add(LogLevel::info(), 'Test message'); diff --git a/tests/Metrics/TraceMetricsTest.php b/tests/Metrics/TraceMetricsTest.php index 6903c37c3..97ab7eebc 100644 --- a/tests/Metrics/TraceMetricsTest.php +++ b/tests/Metrics/TraceMetricsTest.php @@ -113,7 +113,7 @@ public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): voi $this->assertCount(2, StubTransport::$events[0]->getMetrics()); } - public function testMergedScopeAttributesAreAddedToMetric(): void + public function testGlobalScopeAttributesAreAddedToMetric(): void { SentrySdk::getGlobalScope()->setUser(UserDataBag::createFromUserIdentifier('global-user')); diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index 55c6a085e..e9f5765ed 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -34,7 +34,7 @@ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void $handler->handle($record); $event = Event::createEvent(); - SentrySdk::getIsolationScope()->applyToEvent($event); + SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $this->assertCount(1, $event->getBreadcrumbs()); diff --git a/tests/Monolog/ExceptionToSentryIssueHandlerTest.php b/tests/Monolog/ExceptionToSentryIssueHandlerTest.php index 839c2db54..8d8beae65 100644 --- a/tests/Monolog/ExceptionToSentryIssueHandlerTest.php +++ b/tests/Monolog/ExceptionToSentryIssueHandlerTest.php @@ -12,7 +12,7 @@ use Sentry\Event; use Sentry\Monolog\ExceptionToSentryIssueHandler; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; final class ExceptionToSentryIssueHandlerTest extends TestCase { @@ -30,8 +30,9 @@ public function testHandleCapturesExceptionAndAddsMetadata($record, \Throwable $ ->method('captureException') ->with( $this->identicalTo($exception), - $this->callback(function (Scope $scopeArg) use ($expectedExtra): bool { - $event = $scopeArg->applyToEvent(Event::createEvent()); + $this->callback(function (IsolationScope $scopeArg) use ($expectedExtra): bool { + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scopeArg)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame($expectedExtra, $event->getExtra()); @@ -57,7 +58,7 @@ public function testHandleReturnsFalseWhenBubblingEnabled(): void $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) ->method('captureException') - ->with($this->identicalTo($exception), $this->isInstanceOf(Scope::class), null); + ->with($this->identicalTo($exception), $this->isInstanceOf(IsolationScope::class), null); SentrySdk::init($client); @@ -84,7 +85,7 @@ public function testHandleReturnsTrueWhenBubblingDisabled(): void $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) ->method('captureException') - ->with($this->identicalTo($exception), $this->isInstanceOf(Scope::class), null); + ->with($this->identicalTo($exception), $this->isInstanceOf(IsolationScope::class), null); SentrySdk::init($client); diff --git a/tests/Monolog/LogToSentryIssueHandlerTest.php b/tests/Monolog/LogToSentryIssueHandlerTest.php index ec9a53d60..087057039 100644 --- a/tests/Monolog/LogToSentryIssueHandlerTest.php +++ b/tests/Monolog/LogToSentryIssueHandlerTest.php @@ -16,7 +16,7 @@ use Sentry\Monolog\LogToSentryIssueHandler; use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tests\StubTransport; final class LogToSentryIssueHandlerTest extends TestCase @@ -49,8 +49,9 @@ public function testHandleCapturesLogMessageAsIssue(bool $fillExtraContext, $rec return true; }), - $this->callback(function (Scope $scopeArg) use ($expectedExtra): bool { - $event = $scopeArg->applyToEvent(Event::createEvent()); + $this->callback(function (IsolationScope $scopeArg) use ($expectedExtra): bool { + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scopeArg)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame($expectedExtra, $event->getExtra()); @@ -73,7 +74,7 @@ public function testHandleReturnsTrueWhenBubblingDisabled(): void $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) ->method('captureEvent') - ->with($this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), $this->isInstanceOf(Scope::class)); + ->with($this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), $this->isInstanceOf(IsolationScope::class)); SentrySdk::init($client); @@ -117,8 +118,9 @@ public function testHandleCapturesRecordsWithNonThrowableExceptionContext(): voi ->with( $this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), - $this->callback(function (Scope $scopeArg): bool { - $event = $scopeArg->applyToEvent(Event::createEvent()); + $this->callback(function (IsolationScope $scopeArg): bool { + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scopeArg)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index d72be230f..911b8ee87 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -11,6 +11,7 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; +use Sentry\State\IsolationScope; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TransactionContext; @@ -32,7 +33,7 @@ public function testInitResetsRuntimeContext(): void $this->assertNotSame($previousScope, $currentScope); - $event = $currentScope->applyToEvent(Event::createEvent()); + $event = SentrySdk::getGlobalScope()->merge($currentScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame([], $event->getTags()); @@ -113,7 +114,7 @@ public function testInitDoesNotResetGlobalScope(): void $this->assertSame($globalScope, SentrySdk::getGlobalScope()); - $event = $globalScope->applyToEvent(Event::createEvent()); + $event = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['baseline' => 'yes'], $event->getTags()); @@ -132,7 +133,7 @@ public function testStartAndEndContextIsolateScopeData(): void SentrySdk::endContext(); $event = Event::createEvent(); - $event = SentrySdk::getIsolationScope()->applyToEvent($event); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $this->assertArrayHasKey('baseline', $event->getTags()); $this->assertArrayNotHasKey('request', $event->getTags()); @@ -286,7 +287,7 @@ public function testNestedWithContextReusesOuterContext(): void $event = Event::createEvent(); - $event = SentrySdk::getIsolationScope()->applyToEvent($event); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); $this->assertSame('yes', $event->getTags()['outer'] ?? null); diff --git a/tests/State/BreadcrumbRecorderTest.php b/tests/State/BreadcrumbRecorderTest.php index 121825aae..a61ef479f 100644 --- a/tests/State/BreadcrumbRecorderTest.php +++ b/tests/State/BreadcrumbRecorderTest.php @@ -11,15 +11,16 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\State\BreadcrumbRecorder; -use Sentry\State\Scope; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; final class BreadcrumbRecorderTest extends TestCase { public function testRecordAddsBreadcrumbToGivenScope(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - $scope = new Scope(); - $otherScope = new Scope(); + $scope = new IsolationScope(); + $otherScope = new IsolationScope(); $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) @@ -33,7 +34,7 @@ public function testRecordAddsBreadcrumbToGivenScope(): void public function testRecordReturnsFalseForNoOpClient(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $this->assertFalse(BreadcrumbRecorder::record( new NoOpClient(), @@ -45,7 +46,7 @@ public function testRecordReturnsFalseForNoOpClient(): void public function testRecordReturnsFalseWhenMaxBreadcrumbsLimitIsZero(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) @@ -62,7 +63,7 @@ public function testRecordReturnsFalseWhenMaxBreadcrumbsLimitIsZero(): void public function testRecordReturnsFalseWhenBeforeBreadcrumbCallbackReturnsNull(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) @@ -85,7 +86,7 @@ public function testRecordStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): { $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_DEFAULT, 'custom'); - $scope = new Scope(); + $scope = new IsolationScope(); $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) @@ -105,7 +106,7 @@ public function testRecordRespectsMaxBreadcrumbsLimit(): void $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'one'); $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'two'); $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'three'); - $scope = new Scope(); + $scope = new IsolationScope(); $client = $this->createMock(ClientInterface::class); $client->expects($this->exactly(3)) @@ -122,9 +123,9 @@ public function testRecordRespectsMaxBreadcrumbsLimit(): void /** * @param Breadcrumb[] $expectedBreadcrumbs */ - private function assertScopeBreadcrumbs(Scope $scope, array $expectedBreadcrumbs): void + private function assertScopeBreadcrumbs(IsolationScope $scope, array $expectedBreadcrumbs): void { - $event = $scope->applyToEvent(Event::createEvent()); + $event = (new GlobalScope())->merge($scope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame($expectedBreadcrumbs, $event->getBreadcrumbs()); diff --git a/tests/State/EventCapturerTest.php b/tests/State/EventCapturerTest.php index 8e9123cda..3d8c985f4 100644 --- a/tests/State/EventCapturerTest.php +++ b/tests/State/EventCapturerTest.php @@ -17,12 +17,12 @@ use Sentry\SentrySdk; use Sentry\Severity; use Sentry\State\EventCapturer; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Util\SentryUid; final class EventCapturerTest extends TestCase { - public function testCaptureMessagePassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + public function testCaptureMessagePassesIsolationScopeAndStoresLastEventId(): void { $eventId = EventId::generate(); $hint = new EventHint(); @@ -31,8 +31,8 @@ public function testCaptureMessagePassesMergedScopeAndStoresLastEventIdOnIsolati $client->expects($this->once()) ->method('captureMessage') - ->with('foo', Severity::debug(), $this->callback(function (Scope $scope) use ($isolationScope): bool { - return $this->isMergedCaptureScope($scope, $isolationScope); + ->with('foo', Severity::debug(), $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); }), $hint) ->willReturn($eventId); @@ -40,7 +40,7 @@ public function testCaptureMessagePassesMergedScopeAndStoresLastEventIdOnIsolati $this->assertSame($eventId, $isolationScope->getLastEventId()); } - public function testCaptureExceptionPassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + public function testCaptureExceptionPassesIsolationScopeAndStoresLastEventId(): void { $eventId = EventId::generate(); $exception = new \RuntimeException('foo'); @@ -50,8 +50,8 @@ public function testCaptureExceptionPassesMergedScopeAndStoresLastEventIdOnIsola $client->expects($this->once()) ->method('captureException') - ->with($exception, $this->callback(function (Scope $scope) use ($isolationScope): bool { - return $this->isMergedCaptureScope($scope, $isolationScope); + ->with($exception, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); }), $hint) ->willReturn($eventId); @@ -59,7 +59,7 @@ public function testCaptureExceptionPassesMergedScopeAndStoresLastEventIdOnIsola $this->assertSame($eventId, $isolationScope->getLastEventId()); } - public function testCaptureEventPassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + public function testCaptureEventPassesIsolationScopeAndStoresLastEventId(): void { $event = Event::createEvent(); $hint = new EventHint(); @@ -68,8 +68,8 @@ public function testCaptureEventPassesMergedScopeAndStoresLastEventIdOnIsolation $client->expects($this->once()) ->method('captureEvent') - ->with($event, $hint, $this->callback(function (Scope $scope) use ($isolationScope): bool { - return $this->isMergedCaptureScope($scope, $isolationScope); + ->with($event, $hint, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); })) ->willReturn($event->getId()); @@ -77,7 +77,7 @@ public function testCaptureEventPassesMergedScopeAndStoresLastEventIdOnIsolation $this->assertSame($event->getId(), $isolationScope->getLastEventId()); } - public function testCaptureLastErrorPassesMergedScopeAndStoresLastEventIdOnIsolationScope(): void + public function testCaptureLastErrorPassesIsolationScopeAndStoresLastEventId(): void { $eventId = EventId::generate(); $hint = new EventHint(); @@ -86,8 +86,8 @@ public function testCaptureLastErrorPassesMergedScopeAndStoresLastEventIdOnIsola $client->expects($this->once()) ->method('captureLastError') - ->with($this->callback(function (Scope $scope) use ($isolationScope): bool { - return $this->isMergedCaptureScope($scope, $isolationScope); + ->with($this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); }), $hint) ->willReturn($eventId); @@ -104,8 +104,8 @@ public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void $client->expects($this->once()) ->method('captureEvent') - ->with($event, null, $this->callback(function (Scope $scope) use ($isolationScope): bool { - return $this->isMergedCaptureScope($scope, $isolationScope); + ->with($event, null, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); })) ->willReturn(null); @@ -145,8 +145,8 @@ public function testCaptureCheckInCreatesEventAndStoresLastEventId(): void && $checkIn->getEnvironment() === Event::DEFAULT_ENVIRONMENT && $checkIn->getDuration() === 10 && $checkIn->getMonitorConfig() === $monitorConfig; - }), null, $this->callback(function (Scope $scope) use ($isolationScope): bool { - return $this->isMergedCaptureScope($scope, $isolationScope); + }), null, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); })) ->willReturn($eventId); @@ -170,7 +170,7 @@ public function testCaptureCheckInReturnsNullForNoOpClient(): void $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); } - private function setClientAndIsolationScope(ClientInterface $client): Scope + private function setClientAndIsolationScope(ClientInterface $client): IsolationScope { SentrySdk::init(); @@ -179,7 +179,7 @@ private function setClientAndIsolationScope(ClientInterface $client): Scope SentrySdk::getGlobalScope()->setTag('global', 'yes'); SentrySdk::getGlobalScope()->setClient($client); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setTag('scope', 'isolation'); $scope->setTag('isolation', 'yes'); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); @@ -187,11 +187,11 @@ private function setClientAndIsolationScope(ClientInterface $client): Scope return $scope; } - private function isMergedCaptureScope(Scope $captureScope, Scope $isolationScope): bool + private function isCaptureScope(IsolationScope $captureScope, IsolationScope $isolationScope): bool { - $this->assertNotSame($isolationScope, $captureScope); + $this->assertSame($isolationScope, $captureScope); - $event = $captureScope->applyToEvent(Event::createEvent()); + $event = SentrySdk::getGlobalScope()->merge($captureScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index be9362868..9d23d8d4c 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -14,6 +14,8 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\Severity; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; @@ -27,9 +29,16 @@ final class ScopeTest extends TestCase { + private function applyScope(IsolationScope $scope, Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event + { + $globalScope = new GlobalScope(); + + return $globalScope->merge($scope)->applyToEvent($event, $hint, $options); + } + public function testGetAndSetClient(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $this->assertInstanceOf(NoOpClient::class, $scope->getClient()); @@ -43,7 +52,7 @@ public function testClonedScopeKeepsClientShared(): void { $client = $this->createMock(ClientInterface::class); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setClient($client); $clonedScope = clone $scope; @@ -53,7 +62,7 @@ public function testClonedScopeKeepsClientShared(): void public function testGetAndSetLastEventId(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $this->assertNull($scope->getLastEventId()); @@ -69,8 +78,8 @@ public function testGetAndSetLastEventId(): void public function testSetTag(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getTags()); @@ -78,7 +87,7 @@ public function testSetTag(): void $scope->setTag('foo', 'bar'); $scope->setTag('bar', 'baz'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getTags()); @@ -86,17 +95,17 @@ public function testSetTag(): void public function testSetTags(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setTags(['foo' => 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar'], $event->getTags()); $scope->setTags(['bar' => 'baz']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getTags()); @@ -104,20 +113,20 @@ public function testSetTags(): void public function testRemoveTag(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $scope->setTag('foo', 'bar'); $scope->setTag('bar', 'baz'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getTags()); $scope->removeTag('foo'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['bar' => 'baz'], $event->getTags()); @@ -125,8 +134,8 @@ public function testRemoveTag(): void public function testSetFlag(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayNotHasKey('flags', $event->getContexts()); @@ -134,7 +143,7 @@ public function testSetFlag(): void $scope->addFeatureFlag('foo', true); $scope->addFeatureFlag('bar', false); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayHasKey('flags', $event->getContexts()); @@ -154,8 +163,8 @@ public function testSetFlag(): void public function testSetFlagLimit(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayNotHasKey('flags', $event->getContexts()); @@ -171,7 +180,7 @@ public function testSetFlagLimit(): void ]; } - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayHasKey('flags', $event->getContexts()); @@ -186,7 +195,7 @@ public function testSetFlagLimit(): void 'result' => true, ]; - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayHasKey('flags', $event->getContexts()); @@ -197,7 +206,7 @@ public function testSetFlagPropagatesToSpan(): void { $span = new Span(); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setSpan($span); $scope->addFeatureFlag('feature', true); @@ -209,10 +218,10 @@ public function testSetAndRemoveContext(): void { $propgationContext = PropagationContext::fromDefaults(); - $scope = new Scope($propgationContext); + $scope = new IsolationScope($propgationContext); $scope->setContext('foo', ['foo' => 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -225,7 +234,7 @@ public function testSetAndRemoveContext(): void $scope->removeContext('foo'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -237,7 +246,7 @@ public function testSetAndRemoveContext(): void $scope->setContext('foo', []); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -250,8 +259,8 @@ public function testSetAndRemoveContext(): void public function testSetExtra(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getExtra()); @@ -259,7 +268,7 @@ public function testSetExtra(): void $scope->setExtra('foo', 'bar'); $scope->setExtra('bar', 'baz'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getExtra()); @@ -267,17 +276,17 @@ public function testSetExtra(): void public function testSetExtras(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setExtras(['foo' => 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar'], $event->getExtra()); $scope->setExtras(['bar' => 'baz']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getExtra()); @@ -285,8 +294,8 @@ public function testSetExtras(): void public function testSetUser(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getUser()); @@ -296,21 +305,21 @@ public function testSetUser(): void $scope->setUser($user); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); - $this->assertSame($user, $event->getUser()); + $this->assertEquals($user, $event->getUser()); - $user = UserDataBag::createFromUserIpAddress('127.0.0.1'); - $user->setMetadata('subscription', 'basic'); - $user->setMetadata('subscription_expires_at', '2020-08-26'); + $expectedUser = UserDataBag::createFromUserIdentifier('unique_id'); + $expectedUser->setMetadata('subscription', 'basic'); + $expectedUser->setMetadata('subscription_expires_at', '2020-08-26'); $scope->setUser(['ip_address' => '127.0.0.1', 'subscription_expires_at' => '2020-08-26']); - $event = $scope->applyToEvent($event); + $event = $this->applyScope($scope, $event); $this->assertNotNull($event); - $this->assertEquals($user, $event->getUser()); + $this->assertEquals($expectedUser, $event->getUser()); } public function testSetUserThrowsOnInvalidArgument(): void @@ -318,23 +327,23 @@ public function testSetUserThrowsOnInvalidArgument(): void $this->expectException(\TypeError::class); $this->expectExceptionMessage('The $user argument must be either an array or an instance of the "Sentry\UserDataBag" class. Got: "string".'); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setUser('foo'); } public function testRemoveUser(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setUser(UserDataBag::createFromUserIdentifier('unique_id')); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNotNull($event->getUser()); $scope->removeUser(); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getUser()); @@ -342,15 +351,15 @@ public function testRemoveUser(): void public function testSetFingerprint(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getFingerprint()); $scope->setFingerprint(['foo', 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo', 'bar'], $event->getFingerprint()); @@ -358,15 +367,15 @@ public function testSetFingerprint(): void public function testSetLevel(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getLevel()); $scope->setLevel(Severity::debug()); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEquals(Severity::debug(), $event->getLevel()); @@ -374,12 +383,12 @@ public function testSetLevel(): void public function testAddBreadcrumb(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $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'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getBreadcrumbs()); @@ -387,14 +396,14 @@ public function testAddBreadcrumb(): void $scope->addBreadcrumb($breadcrumb1); $scope->addBreadcrumb($breadcrumb2); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([$breadcrumb1, $breadcrumb2], $event->getBreadcrumbs()); $scope->addBreadcrumb($breadcrumb3, 2); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([$breadcrumb2, $breadcrumb3], $event->getBreadcrumbs()); @@ -402,19 +411,19 @@ public function testAddBreadcrumb(): void public function testClearBreadcrumbs(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNotEmpty($event->getBreadcrumbs()); $scope->clearBreadcrumbs(); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getBreadcrumbs()); @@ -427,7 +436,7 @@ public function testAddEventProcessor(): void $callback3Called = false; $event = Event::createEvent(); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addEventProcessor(function (Event $eventArg) use (&$callback1Called, $callback2Called, $callback3Called): ?Event { $this->assertFalse($callback2Called); @@ -438,7 +447,7 @@ public function testAddEventProcessor(): void return $eventArg; }); - $this->assertSame($event, $scope->applyToEvent($event)); + $this->assertSame($event, $this->applyScope($scope, $event)); $this->assertTrue($callback1Called); $scope->addEventProcessor(function () use ($callback1Called, &$callback2Called, $callback3Called) { @@ -456,7 +465,7 @@ public function testAddEventProcessor(): void return null; }); - $this->assertNull($scope->applyToEvent($event)); + $this->assertNull($this->applyScope($scope, $event)); $this->assertTrue($callback2Called); $this->assertFalse($callback3Called); } @@ -464,7 +473,7 @@ public function testAddEventProcessor(): void public function testEventProcessorReceivesTheEventAndEventHint(): void { $event = Event::createEvent(); - $scope = new Scope(); + $scope = new IsolationScope(); $hint = new EventHint(); $processorCalled = false; @@ -477,14 +486,14 @@ public function testEventProcessorReceivesTheEventAndEventHint(): void return $eventArg; }); - $this->assertSame($event, $scope->applyToEvent($event, $hint)); + $this->assertSame($event, $this->applyScope($scope, $event, $hint)); $this->assertSame($hint, $processorReceivedHint); $this->assertTrue($processorCalled); } public function testClear(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $client = $this->createMock(ClientInterface::class); $eventId = EventId::generate(); @@ -500,7 +509,7 @@ public function testClear(): void $scope->setUser(UserDataBag::createFromUserIdentifier('unique_id')); $scope->clear(); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getLevel()); @@ -530,7 +539,7 @@ public function testApplyToEvent(): void $span = $transaction->startChild(new SpanContext()); $span->setSpanId(new SpanId('566e3688a61d4bc8')); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setLevel(Severity::warning()); $scope->setFingerprint(['foo']); $scope->addBreadcrumb($breadcrumb); @@ -542,13 +551,13 @@ public function testApplyToEvent(): void $scope->addFeatureFlag('feature', true); $scope->setSpan($span); - $this->assertSame($event, $scope->applyToEvent($event)); + $this->assertSame($event, $this->applyScope($scope, $event)); $this->assertTrue($event->getLevel()->isEqualTo(Severity::warning())); $this->assertSame(['foo'], $event->getFingerprint()); $this->assertSame([$breadcrumb], $event->getBreadcrumbs()); $this->assertSame(['foo' => 'bar'], $event->getTags()); $this->assertSame(['bar' => 'foo'], $event->getExtra()); - $this->assertSame($user, $event->getUser()); + $this->assertEquals($user, $event->getUser()); $this->assertSame([ 'foocontext' => [ 'foo' => 'foo', @@ -591,7 +600,7 @@ public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void $globalUser->setMetadata('shared', 'global'); $globalUser->setMetadata('global', true); - $globalScope = new Scope(); + $globalScope = new GlobalScope(); $globalScope->setTag('shared', 'global'); $globalScope->setTag('global', 'tag'); $globalScope->setExtra('shared', 'global'); @@ -602,15 +611,13 @@ public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void $globalScope->setLevel(Severity::error()); $globalScope->setFingerprint(['global-fingerprint']); $globalScope->addBreadcrumb($globalBreadcrumb); - $globalScope->addFeatureFlag('shared-flag', false); - $globalScope->addFeatureFlag('global-flag', true); $globalScope->addAttachment($globalAttachment); $isolationUser = UserDataBag::createFromUserIdentifier('isolation-user'); $isolationUser->setMetadata('shared', 'isolation'); $isolationUser->setMetadata('isolation', true); - $isolationScope = new Scope(); + $isolationScope = new IsolationScope(); $isolationScope->setTag('shared', 'isolation'); $isolationScope->setTag('isolation', 'tag'); $isolationScope->setExtra('shared', 'isolation'); @@ -637,7 +644,7 @@ public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void $event->setUser($eventUser); $event->setFingerprint(['event-fingerprint']); - $event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent($event); + $event = $globalScope->merge($isolationScope)->applyToEvent($event); $this->assertNotNull($event); $this->assertTrue($event->getLevel()->isEqualTo(Severity::warning())); @@ -659,10 +666,6 @@ public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void $this->assertSame(['value' => 'isolation'], $event->getContexts()['isolation_context']); $this->assertSame([ 'values' => [ - [ - 'flag' => 'global-flag', - 'result' => true, - ], [ 'flag' => 'shared-flag', 'result' => true, @@ -689,46 +692,38 @@ public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void public function testMergeScopesUsesGlobalLevelWhenIsolationLevelIsUnset(): void { - $globalScope = new Scope(); + $globalScope = new GlobalScope(); $globalScope->setLevel(Severity::error()); - $event = Scope::mergeScopes($globalScope, new Scope())->applyToEvent(Event::createEvent()); + $event = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertTrue($event->getLevel()->isEqualTo(Severity::error())); } - public function testMergeScopesCarriesIsolationClient(): void - { - $globalScope = new Scope(); - $globalScope->setClient($this->createMock(ClientInterface::class)); - - $isolationClient = $this->createMock(ClientInterface::class); - $isolationScope = new Scope(); - $isolationScope->setClient($isolationClient); - - $this->assertSame($isolationClient, Scope::mergeScopes($globalScope, $isolationScope)->getClient()); - } - public function testMergeScopesCapsBreadcrumbsAndFlags(): void { - $globalScope = new Scope(); + $globalScope = new GlobalScope(); $globalBreadcrumbs = []; foreach (range(1, 100) as $i) { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, "global{$i}"); $globalBreadcrumbs[] = $breadcrumb; $globalScope->addBreadcrumb($breadcrumb); - $globalScope->addFeatureFlag("feature{$i}", true); } $isolationBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation'); - $isolationScope = new Scope(); + $isolationScope = new IsolationScope(); $isolationScope->addBreadcrumb($isolationBreadcrumb); + + foreach (range(1, Scope::MAX_FLAGS) as $i) { + $isolationScope->addFeatureFlag("feature{$i}", true); + } + $isolationScope->addFeatureFlag('feature50', false); $isolationScope->addFeatureFlag('feature101', true); - $event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent(Event::createEvent()); + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertCount(100, $event->getBreadcrumbs()); @@ -754,31 +749,22 @@ public function testMergeScopesCapsBreadcrumbsAndFlags(): void public function testMergeScopesKeepsTraceStateFromIsolationScope(): void { - $globalPropagationContext = PropagationContext::fromDefaults(); - $globalPropagationContext->setTraceId(new TraceId('11111111111111111111111111111111')); - $globalPropagationContext->setSpanId(new SpanId('1111111111111111')); - - $globalSpan = new Span(); - $globalSpan->setTraceId(new TraceId('22222222222222222222222222222222')); - $globalSpan->setSpanId(new SpanId('2222222222222222')); - - $globalScope = new Scope($globalPropagationContext); - $globalScope->setSpan($globalSpan); + $globalScope = new GlobalScope(); $isolationPropagationContext = PropagationContext::fromDefaults(); $isolationPropagationContext->setTraceId(new TraceId('33333333333333333333333333333333')); $isolationPropagationContext->setSpanId(new SpanId('3333333333333333')); - $isolationScope = new Scope($isolationPropagationContext); - - $mergedScope = Scope::mergeScopes($globalScope, $isolationScope); + $isolationScope = new IsolationScope($isolationPropagationContext); - $this->assertNull($mergedScope->getSpan()); - $this->assertNotSame($isolationScope->getPropagationContext(), $mergedScope->getPropagationContext()); + $this->assertNull($isolationScope->getSpan()); $this->assertSame([ 'trace_id' => '33333333333333333333333333333333', 'span_id' => '3333333333333333', - ], $mergedScope->getTraceContext()); + ], $isolationScope->getTraceContext()); + + $mergedScope = $globalScope->merge($isolationScope); + $isolationScope->setPropagationContext(PropagationContext::fromDefaults()); $event = $mergedScope->applyToEvent(Event::createEvent()); @@ -799,21 +785,21 @@ public function testMergeScopesKeepsProcessorOrder(): void return $event; }); - $globalScope = new Scope(); + $globalScope = new GlobalScope(); $globalScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { $calls[] = 'global'; return $event; }); - $isolationScope = new Scope(); + $isolationScope = new IsolationScope(); $isolationScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { $calls[] = 'isolation'; return $event; }); - $event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent(Event::createEvent()); + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['static', 'global', 'isolation'], $calls); @@ -824,9 +810,9 @@ public function testMergeScopesKeepsProcessorOrder(): void */ public function testAttachmentsAppliedForType(Event $event, int $attachmentCount): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addAttachment(Attachment::fromBytes('test', 'abcde')); - $scope->applyToEvent($event); + $this->applyScope($scope, $event); $this->assertCount($attachmentCount, $event->getAttachments()); } @@ -851,7 +837,7 @@ public function testGetTraceContextPrefersExternalPropagationContextOverPropagat ]; }); - $scope = new Scope($propagationContext); + $scope = new IsolationScope($propagationContext); $this->assertSame([ 'trace_id' => '771a43a4192642f0b136d5159a501700', @@ -876,7 +862,7 @@ public function testGetTraceContextPrefersLocalSpanOverExternalPropagationContex $span = $transaction->startChild(new SpanContext()); $span->setSpanId(new SpanId('566e3688a61d4bc8')); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setSpan($span); $this->assertSame([ @@ -898,8 +884,8 @@ public function testApplyToEventSkipsDynamicSamplingContextWhenUsingExternalProp ]; }); - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent(), null, new Options([ + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent(), null, new Options([ 'dsn' => 'http://public@example.com/1', 'release' => '1.0.0', 'environment' => 'test', diff --git a/tests/Tracing/DynamicSamplingContextTest.php b/tests/Tracing/DynamicSamplingContextTest.php index 7f1c153c4..70424e0d0 100644 --- a/tests/Tracing/DynamicSamplingContextTest.php +++ b/tests/Tracing/DynamicSamplingContextTest.php @@ -8,7 +8,7 @@ use Sentry\ClientInterface; use Sentry\NoOpClient; use Sentry\Options; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\TraceId; @@ -135,7 +135,7 @@ public function testFromOptions(): void $propagationContext->setTraceId(new TraceId('21160e9b836d479f81611368b2aa3d2c')); $propagationContext->setSampleRand(0.5); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setPropagationContext($propagationContext); $samplingContext = DynamicSamplingContext::fromOptions($options, $scope); @@ -156,7 +156,7 @@ public function testFromOptionsUsesConfiguredOrgIdOverDsnOrgId(): void 'org_id' => 2, ]); - $scope = new Scope(); + $scope = new IsolationScope(); $samplingContext = DynamicSamplingContext::fromOptions($options, $scope); $this->assertSame('2', $samplingContext->get('org_id')); @@ -168,7 +168,7 @@ public function testFromOptionsFallsBackToDsnOrgId(): void 'dsn' => 'http://public@o1.example.com/1', ]); - $scope = new Scope(); + $scope = new IsolationScope(); $samplingContext = DynamicSamplingContext::fromOptions($options, $scope); $this->assertSame('1', $samplingContext->get('org_id')); diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index e0f3df44e..61610f5ca 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -16,6 +16,8 @@ use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\GuzzleTracingMiddleware; use Sentry\Tracing\SpanStatus; @@ -60,7 +62,7 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void $event = Event::createEvent(); - SentrySdk::getIsolationScope()->applyToEvent($event); + (new GlobalScope())->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $this->assertCount(1, $event->getBreadcrumbs()); } @@ -103,7 +105,7 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void $event = Event::createEvent(); - SentrySdk::getIsolationScope()->applyToEvent($event); + (new GlobalScope())->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $this->assertCount(1, $event->getBreadcrumbs()); } @@ -123,7 +125,7 @@ public function testTraceUsesProvidedIsolationScope(): void $currentScope = SentrySdk::getIsolationScope(); - $providedScope = new Scope(); + $providedScope = new IsolationScope(); $providedScope->setClient($client); $request = new Request('GET', 'https://www.example.com'); @@ -149,10 +151,10 @@ public function testTraceUsesProvidedIsolationScope(): void $this->assertSame($expectedPromiseResult, $promiseResult); $currentEvent = Event::createEvent(); - $currentScope->applyToEvent($currentEvent); + (new GlobalScope())->merge($currentScope)->applyToEvent($currentEvent); $providedEvent = Event::createEvent(); - $providedScope->applyToEvent($providedEvent); + (new GlobalScope())->merge($providedScope)->applyToEvent($providedEvent); $this->assertCount(0, $currentEvent->getBreadcrumbs()); $this->assertCount(1, $providedEvent->getBreadcrumbs()); @@ -377,7 +379,7 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec ->with($this->callback(function (Event $eventArg) use ($scope, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { $this->assertSame(EventType::transaction(), $eventArg->getType()); - $scope->applyToEvent($eventArg); + (new GlobalScope())->merge($scope)->applyToEvent($eventArg); $spans = $eventArg->getSpans(); $breadcrumbs = $eventArg->getBreadcrumbs(); diff --git a/tests/Tracing/PropagationContextTest.php b/tests/Tracing/PropagationContextTest.php index 76484896f..8d2421913 100644 --- a/tests/Tracing/PropagationContextTest.php +++ b/tests/Tracing/PropagationContextTest.php @@ -8,7 +8,7 @@ use Sentry\ClientInterface; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanId; @@ -130,7 +130,7 @@ public function testToBaggageUsesCurrentClientAndIsolationScope(): void ])); SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new Scope($propagationContext)); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $this->assertSame( 'sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.25,sentry-release=1.0.0,sentry-environment=development', @@ -224,7 +224,7 @@ public function testGettersAndSetters(string $getterMethod, string $setterMethod public static function gettersAndSettersDataProvider(): array { - $scope = new Scope(); + $scope = new IsolationScope(); $options = new Options([ 'dsn' => 'http://public@example.com/sentry/1', 'release' => '1.0.0', diff --git a/tests/Tracing/SpanTest.php b/tests/Tracing/SpanTest.php index 1de0abf6d..251a9eaf8 100644 --- a/tests/Tracing/SpanTest.php +++ b/tests/Tracing/SpanTest.php @@ -7,7 +7,8 @@ use PHPUnit\Framework\TestCase; use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; @@ -83,17 +84,17 @@ public function testSetHttpStatusWritesResponseContextToCurrentIsolationScopeOnl 'status_code' => 201, ]); - $isolationScope = new Scope(); + $isolationScope = new IsolationScope(); SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); $span = new Span(); $span->setHttpStatus(404); - $isolationEvent = $isolationScope->applyToEvent(Event::createEvent()); + $isolationEvent = (new GlobalScope())->merge($isolationScope)->applyToEvent(Event::createEvent()); $this->assertNotNull($isolationEvent); $this->assertSame(404, $isolationEvent->getContexts()['response']['status_code']); - $globalEvent = $globalScope->applyToEvent(Event::createEvent()); + $globalEvent = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); $this->assertNotNull($globalEvent); $this->assertSame(201, $globalEvent->getContexts()['response']['status_code']); } diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index bf77563f2..cfb2eb959 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -11,7 +11,7 @@ use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -59,7 +59,7 @@ public function testFinish(): void $this->assertSame([$span1, $span2], $eventArg->getSpans()); return true; - }), null, $this->isInstanceOf(Scope::class)) + }), null, $this->isInstanceOf(IsolationScope::class)) ->willReturnCallback(static function (Event $eventArg) use (&$expectedEventId): EventId { $expectedEventId = $eventArg->getId(); From 79b8b52b094557b849fb2929bbe07b027f8ff0c2 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 16 Jul 2026 17:12:16 +0200 Subject: [PATCH 17/20] ref(scopes): feedback from stacked PRs (#2158) --- .../AbstractErrorListenerIntegration.php | 4 +- src/Monolog/ExceptionToSentryIssueHandler.php | 4 +- src/Monolog/LogToSentryIssueHandler.php | 4 +- .../{EventCapturer.php => EventRecorder.php} | 40 +++++++++---------- src/Tracing/Transaction.php | 16 ++++++-- src/functions.php | 12 +++--- ...CapturerTest.php => EventRecorderTest.php} | 21 +++++----- tests/Tracing/TransactionTest.php | 8 +++- 8 files changed, 60 insertions(+), 49 deletions(-) rename src/State/{EventCapturer.php => EventRecorder.php} (61%) rename tests/State/{EventCapturerTest.php => EventRecorderTest.php} (90%) diff --git a/src/Integration/AbstractErrorListenerIntegration.php b/src/Integration/AbstractErrorListenerIntegration.php index 36482a987..9b1cc8a0d 100644 --- a/src/Integration/AbstractErrorListenerIntegration.php +++ b/src/Integration/AbstractErrorListenerIntegration.php @@ -7,7 +7,7 @@ use Sentry\Event; use Sentry\EventHint; use Sentry\ExceptionMechanism; -use Sentry\State\EventCapturer; +use Sentry\State\EventRecorder; use Sentry\State\IsolationScope; use function Sentry\withIsolationScope; @@ -24,7 +24,7 @@ protected function captureException(\Throwable $exception): void return $this->addExceptionMechanismToEvent($event); }); - EventCapturer::captureException($exception); + EventRecorder::captureException($exception, null, $scope); }); } diff --git a/src/Monolog/ExceptionToSentryIssueHandler.php b/src/Monolog/ExceptionToSentryIssueHandler.php index 7998c9c83..5173631d5 100644 --- a/src/Monolog/ExceptionToSentryIssueHandler.php +++ b/src/Monolog/ExceptionToSentryIssueHandler.php @@ -9,7 +9,7 @@ use Monolog\Logger; use Monolog\LogRecord; use Psr\Log\LogLevel; -use Sentry\State\EventCapturer; +use Sentry\State\EventRecorder; use Sentry\State\IsolationScope; use function Sentry\withIsolationScope; @@ -56,7 +56,7 @@ public function handle($record): bool $scope->setExtra('monolog.extra', $monologExtraData); } - EventCapturer::captureException($exception); + EventRecorder::captureException($exception, null, $scope); }); return $this->bubble === false; diff --git a/src/Monolog/LogToSentryIssueHandler.php b/src/Monolog/LogToSentryIssueHandler.php index f38f03020..2866d32af 100644 --- a/src/Monolog/LogToSentryIssueHandler.php +++ b/src/Monolog/LogToSentryIssueHandler.php @@ -11,7 +11,7 @@ use Psr\Log\LogLevel; use Sentry\Event; use Sentry\EventHint; -use Sentry\State\EventCapturer; +use Sentry\State\EventRecorder; use Sentry\State\IsolationScope; use function Sentry\withIsolationScope; @@ -84,7 +84,7 @@ protected function doWrite($record): void } } - EventCapturer::captureEvent($event, $hint); + EventRecorder::captureEvent($event, $hint, $scope); }); } diff --git a/src/State/EventCapturer.php b/src/State/EventRecorder.php similarity index 61% rename from src/State/EventCapturer.php rename to src/State/EventRecorder.php index a21b44e75..dbc6c4f41 100644 --- a/src/State/EventCapturer.php +++ b/src/State/EventRecorder.php @@ -18,36 +18,44 @@ /** * @internal */ -final class EventCapturer +final class EventRecorder { private function __construct() { } - public static function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId + public static function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId { - return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($message, $level, $hint): ?EventId { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($message, $level, $hint): ?EventId { return $client->captureMessage($message, $level, $captureScope, $hint); }); } - public static function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId + public static function captureException(\Throwable $exception, ?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId { - return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($exception, $hint): ?EventId { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($exception, $hint): ?EventId { return $client->captureException($exception, $captureScope, $hint); }); } - public static function captureEvent(Event $event, ?EventHint $hint = null): ?EventId + public static function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId { - return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($event, $hint): ?EventId { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($event, $hint): ?EventId { return $client->captureEvent($event, $hint, $captureScope); }); } - public static function captureLastError(?EventHint $hint = null): ?EventId + public static function captureLastError(?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId { - return self::capture(static function (ClientInterface $client, IsolationScope $captureScope) use ($hint): ?EventId { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($hint): ?EventId { return $client->captureLastError($captureScope, $hint); }); } @@ -55,9 +63,9 @@ public static function captureLastError(?EventHint $hint = null): ?EventId /** * @param int|float|null $duration */ - public static function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string + public static function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null, ?IsolationScope $isolationScope = null): ?string { - $isolationScope = SentrySdk::getIsolationScope(); + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); $client = SentrySdk::getClient($isolationScope); if ($client instanceof NoOpClient) { @@ -84,16 +92,6 @@ public static function captureCheckIn(string $slug, CheckInStatus $status, $dura return $checkIn->getId(); } - /** - * @param callable(ClientInterface, IsolationScope): ?EventId $capture - */ - private static function capture(callable $capture): ?EventId - { - $isolationScope = SentrySdk::getIsolationScope(); - - return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, $capture); - } - /** * @param callable(ClientInterface, IsolationScope): ?EventId $capture */ diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index 89086bc96..2b3cba6b1 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -9,6 +9,8 @@ use Sentry\Options; use Sentry\Profiling\Profiler; use Sentry\SentrySdk; +use Sentry\State\EventRecorder; +use Sentry\State\IsolationScope; /** * This class stores all the information about a Transaction. @@ -20,6 +22,11 @@ final class Transaction extends Span */ private $name; + /** + * @var IsolationScope + */ + private $scope; + /** * @var Transaction The transaction */ @@ -42,11 +49,12 @@ final class Transaction extends Span * * @internal */ - public function __construct(TransactionContext $context) + public function __construct(TransactionContext $context, ?IsolationScope $scope = null) { parent::__construct($context); $this->name = $context->getName(); + $this->scope = $scope ?? SentrySdk::getIsolationScope(); $this->metadata = $context->getMetadata(); $this->transaction = $this; } @@ -90,7 +98,7 @@ public function getDynamicSamplingContext(): DynamicSamplingContext return $this->metadata->getDynamicSamplingContext(); } - $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, SentrySdk::getClient()); + $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, SentrySdk::getClient($this->scope)); $this->getMetadata()->setDynamicSamplingContext($samplingContext); return $samplingContext; @@ -115,7 +123,7 @@ public function initSpanRecorder(int $maxSpans = 1000): self public function initProfiler(?Options $options = null): Profiler { if ($this->profiler === null) { - $this->profiler = new Profiler($options ?? SentrySdk::getClient()->getOptions()); + $this->profiler = new Profiler($options ?? SentrySdk::getClient($this->scope)->getOptions()); } return $this->profiler; @@ -180,6 +188,6 @@ public function finish(?float $endTimestamp = null): ?EventId } } - return \Sentry\captureEvent($event); + return EventRecorder::captureEvent($event, null, $this->scope); } } diff --git a/src/functions.php b/src/functions.php index bb234f982..84aadd28e 100644 --- a/src/functions.php +++ b/src/functions.php @@ -11,7 +11,7 @@ use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; use Sentry\State\BreadcrumbRecorder; -use Sentry\State\EventCapturer; +use Sentry\State\EventRecorder; use Sentry\State\GlobalScope; use Sentry\State\IsolationScope; use Sentry\State\Scope; @@ -105,7 +105,7 @@ function getClient(): ClientInterface */ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - return EventCapturer::captureMessage($message, $level, $hint); + return EventRecorder::captureMessage($message, $level, $hint); } /** @@ -116,7 +116,7 @@ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hi */ function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { - return EventCapturer::captureException($exception, $hint); + return EventRecorder::captureException($exception, $hint); } /** @@ -127,7 +127,7 @@ function captureException(\Throwable $exception, ?EventHint $hint = null): ?Even */ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - return EventCapturer::captureEvent($event, $hint); + return EventRecorder::captureEvent($event, $hint); } /** @@ -137,7 +137,7 @@ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId */ function captureLastError(?EventHint $hint = null): ?EventId { - return EventCapturer::captureLastError($hint); + return EventRecorder::captureLastError($hint); } /** @@ -151,7 +151,7 @@ function captureLastError(?EventHint $hint = null): ?EventId */ function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { - return EventCapturer::captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); + return EventRecorder::captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** diff --git a/tests/State/EventCapturerTest.php b/tests/State/EventRecorderTest.php similarity index 90% rename from tests/State/EventCapturerTest.php rename to tests/State/EventRecorderTest.php index 3d8c985f4..8eb644697 100644 --- a/tests/State/EventCapturerTest.php +++ b/tests/State/EventRecorderTest.php @@ -16,11 +16,11 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\EventCapturer; +use Sentry\State\EventRecorder; use Sentry\State\IsolationScope; use Sentry\Util\SentryUid; -final class EventCapturerTest extends TestCase +final class EventRecorderTest extends TestCase { public function testCaptureMessagePassesIsolationScopeAndStoresLastEventId(): void { @@ -36,7 +36,7 @@ public function testCaptureMessagePassesIsolationScopeAndStoresLastEventId(): vo }), $hint) ->willReturn($eventId); - $this->assertSame($eventId, EventCapturer::captureMessage('foo', Severity::debug(), $hint)); + $this->assertSame($eventId, EventRecorder::captureMessage('foo', Severity::debug(), $hint, $isolationScope)); $this->assertSame($eventId, $isolationScope->getLastEventId()); } @@ -55,7 +55,7 @@ public function testCaptureExceptionPassesIsolationScopeAndStoresLastEventId(): }), $hint) ->willReturn($eventId); - $this->assertSame($eventId, EventCapturer::captureException($exception, $hint)); + $this->assertSame($eventId, EventRecorder::captureException($exception, $hint, $isolationScope)); $this->assertSame($eventId, $isolationScope->getLastEventId()); } @@ -73,7 +73,7 @@ public function testCaptureEventPassesIsolationScopeAndStoresLastEventId(): void })) ->willReturn($event->getId()); - $this->assertSame($event->getId(), EventCapturer::captureEvent($event, $hint)); + $this->assertSame($event->getId(), EventRecorder::captureEvent($event, $hint, $isolationScope)); $this->assertSame($event->getId(), $isolationScope->getLastEventId()); } @@ -91,7 +91,7 @@ public function testCaptureLastErrorPassesIsolationScopeAndStoresLastEventId(): }), $hint) ->willReturn($eventId); - $this->assertSame($eventId, EventCapturer::captureLastError($hint)); + $this->assertSame($eventId, EventRecorder::captureLastError($hint, $isolationScope)); $this->assertSame($eventId, $isolationScope->getLastEventId()); } @@ -109,7 +109,7 @@ public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void })) ->willReturn(null); - $this->assertNull(EventCapturer::captureEvent($event)); + $this->assertNull(EventRecorder::captureEvent($event)); $this->assertNull($isolationScope->getLastEventId()); } @@ -150,12 +150,13 @@ public function testCaptureCheckInCreatesEventAndStoresLastEventId(): void })) ->willReturn($eventId); - $this->assertSame($checkInId, EventCapturer::captureCheckIn( + $this->assertSame($checkInId, EventRecorder::captureCheckIn( 'test-crontab', CheckInStatus::ok(), 10, $monitorConfig, - $checkInId + $checkInId, + $isolationScope )); $this->assertSame($eventId, $isolationScope->getLastEventId()); } @@ -166,7 +167,7 @@ public function testCaptureCheckInReturnsNullForNoOpClient(): void $eventId = EventId::generate(); SentrySdk::getIsolationScope()->setLastEventId($eventId); - $this->assertNull(EventCapturer::captureCheckIn('test-crontab', CheckInStatus::ok())); + $this->assertNull(EventRecorder::captureCheckIn('test-crontab', CheckInStatus::ok())); $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); } diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index cfb2eb959..aff82026f 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -41,7 +41,9 @@ public function testFinish(): void SentrySdk::init($client); - $transaction = new Transaction($transactionContext); + $scope = SentrySdk::getIsolationScope(); + $transaction = new Transaction($transactionContext, $scope); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope()); $transaction->initSpanRecorder(); $span1 = $transaction->startChild(new SpanContext()); @@ -59,7 +61,7 @@ public function testFinish(): void $this->assertSame([$span1, $span2], $eventArg->getSpans()); return true; - }), null, $this->isInstanceOf(IsolationScope::class)) + }), null, $scope) ->willReturnCallback(static function (Event $eventArg) use (&$expectedEventId): EventId { $expectedEventId = $eventArg->getId(); @@ -72,6 +74,8 @@ public function testFinish(): void $eventId = $transaction->finish(); $this->assertSame($expectedEventId, $eventId); + $this->assertSame($expectedEventId, $scope->getLastEventId()); + $this->assertNull(SentrySdk::getIsolationScope()->getLastEventId()); } public function testFinishDoesNothingIfSampledFlagIsNotTrue(): void From c719f6417ce98ba1498154b0b196e91f5f9e793d Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Fri, 17 Jul 2026 13:41:27 +0200 Subject: [PATCH 18/20] fix max breadcrumbs --- src/State/MergedScope.php | 9 ++- src/State/ScopeData.php | 51 ++++++++++++- tests/State/ScopeTest.php | 147 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 3 deletions(-) diff --git a/src/State/MergedScope.php b/src/State/MergedScope.php index d6af43d5a..0245e2217 100644 --- a/src/State/MergedScope.php +++ b/src/State/MergedScope.php @@ -41,7 +41,14 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op $event->setFingerprint(array_merge($event->getFingerprint(), $this->scopeData->getFingerprint())); if (empty($event->getBreadcrumbs())) { - $event->setBreadcrumb($this->scopeData->getBreadcrumbs()); + $breadcrumbs = $this->scopeData->getBreadcrumbs(); + $maxBreadcrumbs = $options !== null ? $options->getMaxBreadcrumbs() : Options::DEFAULT_MAX_BREADCRUMBS; + + if (\count($breadcrumbs) > $maxBreadcrumbs) { + $breadcrumbs = $maxBreadcrumbs > 0 ? \array_slice($breadcrumbs, -$maxBreadcrumbs) : []; + } + + $event->setBreadcrumb($breadcrumbs); } if ($this->scopeData->getLevel() !== null) { diff --git a/src/State/ScopeData.php b/src/State/ScopeData.php index eab5336a3..202e5d77e 100644 --- a/src/State/ScopeData.php +++ b/src/State/ScopeData.php @@ -123,7 +123,9 @@ public function getBreadcrumbs(): array public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): void { $this->breadcrumbs[] = $breadcrumb; - $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); + if (\count($this->breadcrumbs) > $maxBreadcrumbs) { + $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); + } } public function clearBreadcrumbs(): void @@ -363,7 +365,7 @@ public function merge(self $other): self $merged->level = $other->level ?? $this->level; $merged->fingerprint = array_merge($this->fingerprint, $other->fingerprint); - $merged->breadcrumbs = \array_slice(array_merge($this->breadcrumbs, $other->breadcrumbs), -100); + $merged->breadcrumbs = self::mergeBreadcrumbs($this->breadcrumbs, $other->breadcrumbs); $merged->flags = self::mergeFlags($this->flags, $other->flags); $merged->attachments = array_merge($this->attachments, $other->attachments); $merged->eventProcessors = array_merge($this->eventProcessors, $other->eventProcessors); @@ -371,6 +373,51 @@ public function merge(self $other): self return $merged; } + /** + * @param Breadcrumb[] $globalBreadcrumbs + * @param Breadcrumb[] $isolationBreadcrumbs + * + * @return Breadcrumb[] + */ + private static function mergeBreadcrumbs(array $globalBreadcrumbs, array $isolationBreadcrumbs): array + { + if (empty($globalBreadcrumbs)) { + return $isolationBreadcrumbs; + } + + if (empty($isolationBreadcrumbs)) { + return $globalBreadcrumbs; + } + + // if the last global is before the first isolation, it means that no global breadcrumb was created after + // the first isolation was written, so we can just merge them normally + if ($globalBreadcrumbs[\count($globalBreadcrumbs) - 1]->getTimestamp() <= $isolationBreadcrumbs[0]->getTimestamp()) { + return array_merge($globalBreadcrumbs, $isolationBreadcrumbs); + } + + $globalCount = \count($globalBreadcrumbs); + $isolationCount = \count($isolationBreadcrumbs); + $globalIndex = 0; + $isolationIndex = 0; + $merged = []; + + // we iterate over both lists as long as both have elements. Once we reached the end of one list, + // we can stop iterating and merge the remaining list fully + while ($globalIndex < $globalCount && $isolationIndex < $isolationCount) { + if ($globalBreadcrumbs[$globalIndex]->getTimestamp() <= $isolationBreadcrumbs[$isolationIndex]->getTimestamp()) { + $merged[] = $globalBreadcrumbs[$globalIndex++]; + } else { + $merged[] = $isolationBreadcrumbs[$isolationIndex++]; + } + } + + return array_merge( + $merged, + \array_slice($globalBreadcrumbs, $globalIndex), + \array_slice($isolationBreadcrumbs, $isolationIndex) + ); + } + /** * @param array> $globalFlags * @param array> $isolationFlags diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index 9d23d8d4c..c9810dc61 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -36,6 +36,11 @@ private function applyScope(IsolationScope $scope, Event $event, ?EventHint $hin return $globalScope->merge($scope)->applyToEvent($event, $hint, $options); } + private function breadcrumbWithTimestamp(float $timestamp, string $category = 'test'): Breadcrumb + { + return new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, $category, null, [], $timestamp); + } + public function testGetAndSetClient(): void { $scope = new IsolationScope(); @@ -747,6 +752,148 @@ public function testMergeScopesCapsBreadcrumbsAndFlags(): void $this->assertFalse(\in_array('feature1', array_column($flags, 'flag'), true)); } + public function testMergeScopesInterleavesBreadcrumbsChronologically(): void + { + $globalBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'global1'); + $globalBreadcrumb2 = $this->breadcrumbWithTimestamp(3.0, 'global2'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(2.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(4.0, 'isolation2'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb1); + $globalScope->addBreadcrumb($globalBreadcrumb2); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([$globalBreadcrumb1, $isolationBreadcrumb1, $globalBreadcrumb2, $isolationBreadcrumb2], $event->getBreadcrumbs()); + } + + public function testMergeScopesRespectsMaxBreadcrumbsLargerThanDefault(): void + { + $options = new Options(['max_breadcrumbs' => 150]); + $globalScope = new GlobalScope(); + $isolationScope = new IsolationScope(); + $breadcrumbs = []; + + foreach (range(1, 100) as $i) { + $breadcrumb = $this->breadcrumbWithTimestamp((float) $i, "global{$i}"); + $breadcrumbs[] = $breadcrumb; + $globalScope->addBreadcrumb($breadcrumb, 150); + } + + foreach (range(101, 200) as $i) { + $breadcrumb = $this->breadcrumbWithTimestamp((float) $i, "isolation{$i}"); + $breadcrumbs[] = $breadcrumb; + $isolationScope->addBreadcrumb($breadcrumb, 150); + } + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent(), null, $options); + + $this->assertNotNull($event); + $this->assertCount(150, $event->getBreadcrumbs()); + $this->assertSame(\array_slice($breadcrumbs, -150), $event->getBreadcrumbs()); + } + + public function testMergeScopesAppliesMaxBreadcrumbsSmallerThanMergedCount(): void + { + $globalBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'global1'); + $globalBreadcrumb2 = $this->breadcrumbWithTimestamp(4.0, 'global2'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(2.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(3.0, 'isolation2'); + $isolationBreadcrumb3 = $this->breadcrumbWithTimestamp(5.0, 'isolation3'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb1); + $globalScope->addBreadcrumb($globalBreadcrumb2); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + $isolationScope->addBreadcrumb($isolationBreadcrumb3); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent(), null, new Options(['max_breadcrumbs' => 3])); + + $this->assertNotNull($event); + $this->assertSame([$isolationBreadcrumb2, $globalBreadcrumb2, $isolationBreadcrumb3], $event->getBreadcrumbs()); + } + + public function testMergeScopesBreadcrumbsWithEqualTimestampsKeepGlobalScopeFirst(): void + { + $globalBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'global1'); + $globalBreadcrumb2 = $this->breadcrumbWithTimestamp(1.0, 'global2'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(1.0, 'isolation2'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb1); + $globalScope->addBreadcrumb($globalBreadcrumb2); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([$globalBreadcrumb1, $globalBreadcrumb2, $isolationBreadcrumb1, $isolationBreadcrumb2], $event->getBreadcrumbs()); + } + + public function testMergeScopesPreservesInsertionOrderWithinScopeForOutOfOrderTimestamps(): void + { + // Breadcrumbs recorded on the same scope are never reordered, even when + // their user-supplied timestamps are not monotonic; only breadcrumbs of + // different scopes are interleaved by timestamp. + $globalBreadcrumb = $this->breadcrumbWithTimestamp(4.0, 'global1'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(2.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(6.0, 'isolation2'); + $isolationBreadcrumb3 = $this->breadcrumbWithTimestamp(1.0, 'isolation3'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + $isolationScope->addBreadcrumb($isolationBreadcrumb3); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([$isolationBreadcrumb1, $globalBreadcrumb, $isolationBreadcrumb2, $isolationBreadcrumb3], $event->getBreadcrumbs()); + } + + public function testApplyToEventDropsScopeBreadcrumbsWhenMaxBreadcrumbsIsZero(): void + { + $scope = new IsolationScope(); + $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'test')); + + $event = $this->applyScope($scope, Event::createEvent(), null, new Options(['max_breadcrumbs' => 0])); + + $this->assertNotNull($event); + $this->assertSame([], $event->getBreadcrumbs()); + } + + public function testApplyToEventDoesNotOverrideEventBreadcrumbs(): void + { + $eventBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'event'); + + $scope = new IsolationScope(); + $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'scope')); + + $event = Event::createEvent(); + $event->setBreadcrumb([$eventBreadcrumb]); + + $event = $this->applyScope($scope, $event); + + $this->assertNotNull($event); + $this->assertSame([$eventBreadcrumb], $event->getBreadcrumbs()); + } + public function testMergeScopesKeepsTraceStateFromIsolationScope(): void { $globalScope = new GlobalScope(); From 8c8de73559ebd69264be6e2e77f1ef93cd8eac68 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Fri, 17 Jul 2026 13:57:27 +0200 Subject: [PATCH 19/20] fix dynamic sampling context --- src/Tracing/PropagationContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tracing/PropagationContext.php b/src/Tracing/PropagationContext.php index 721321d08..497c513c0 100644 --- a/src/Tracing/PropagationContext.php +++ b/src/Tracing/PropagationContext.php @@ -83,9 +83,9 @@ public function toTraceparent(): string public function toBaggage(): string { if ($this->dynamicSamplingContext === null) { - $this->dynamicSamplingContext = DynamicSamplingContext::fromOptions( + $this->dynamicSamplingContext = DynamicSamplingContext::fromOptionsAndPropagationContext( SentrySdk::getClient()->getOptions(), - SentrySdk::getIsolationScope() + $this ); } From 744dc134521981ec2fff74f59c0846d41b93c662 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Fri, 17 Jul 2026 14:43:13 +0200 Subject: [PATCH 20/20] store last event id on context --- src/SentrySdk.php | 5 ++ src/State/EventRecorder.php | 6 ++- src/State/IsolationScope.php | 23 --------- src/State/RuntimeContext.php | 16 +++++++ .../ClientReportAggregatorTest.php | 4 +- tests/FunctionsTest.php | 47 ++++++++++++++----- tests/State/EventRecorderTest.php | 31 ++++++++---- tests/State/ScopeTest.php | 20 -------- tests/Tracing/TransactionTest.php | 3 +- 9 files changed, 85 insertions(+), 70 deletions(-) diff --git a/src/SentrySdk.php b/src/SentrySdk.php index b273ccc72..8145c8269 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -61,6 +61,11 @@ public static function getIsolationScope(): IsolationScope return self::getCurrentRuntimeContext()->getIsolationScope(); } + public static function getLastEventId(): ?EventId + { + return self::getCurrentRuntimeContext()->getLastEventId(); + } + public static function getClient(?IsolationScope $isolationScope = null): ClientInterface { $client = ($isolationScope ?? self::getIsolationScope())->getClient(); diff --git a/src/State/EventRecorder.php b/src/State/EventRecorder.php index dbc6c4f41..cd417ed6a 100644 --- a/src/State/EventRecorder.php +++ b/src/State/EventRecorder.php @@ -97,8 +97,12 @@ public static function captureCheckIn(string $slug, CheckInStatus $status, $dura */ private static function captureWithScope(ClientInterface $client, IsolationScope $isolationScope, callable $capture): ?EventId { + if ($client instanceof NoOpClient) { + return null; + } + $eventId = $capture($client, $isolationScope); - $isolationScope->setLastEventId($eventId); + SentrySdk::getCurrentRuntimeContext()->setLastEventId($eventId); return $eventId; } diff --git a/src/State/IsolationScope.php b/src/State/IsolationScope.php index 0aa36183e..c9b66d8e2 100644 --- a/src/State/IsolationScope.php +++ b/src/State/IsolationScope.php @@ -4,8 +4,6 @@ namespace Sentry\State; -use Sentry\Event; -use Sentry\EventId; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\Span; use Sentry\Tracing\Transaction; @@ -21,33 +19,12 @@ class IsolationScope extends MutableScope */ private $span; - /** - * @var EventId|null The ID of the last captured event - */ - private $lastEventId; - public function __construct(?PropagationContext $propagationContext = null) { parent::__construct(); $this->scopeData->setPropagationContext($propagationContext ?? PropagationContext::fromDefaults()); } - /** - * Returns the ID of the last captured event. - */ - public function getLastEventId(): ?EventId - { - return $this->lastEventId; - } - - /** - * @internal - */ - public function setLastEventId(?EventId $lastEventId): void - { - $this->lastEventId = $lastEventId; - } - /** * Adds a feature flag to the scope. * diff --git a/src/State/RuntimeContext.php b/src/State/RuntimeContext.php index dcc097ef9..a2b3a45d6 100644 --- a/src/State/RuntimeContext.php +++ b/src/State/RuntimeContext.php @@ -4,6 +4,7 @@ namespace Sentry\State; +use Sentry\EventId; use Sentry\Logs\LogsAggregator; use Sentry\Metrics\MetricsAggregator; @@ -37,6 +38,11 @@ final class RuntimeContext */ private $metricsAggregator; + /** + * @var EventId|null The ID of the last event captured within this context + */ + private $lastEventId; + public function __construct(string $id, ?IsolationScope $isolationScope = null) { $this->id = $id; @@ -69,4 +75,14 @@ public function getMetricsAggregator(): MetricsAggregator { return $this->metricsAggregator; } + + public function getLastEventId(): ?EventId + { + return $this->lastEventId; + } + + public function setLastEventId(?EventId $lastEventId): void + { + $this->lastEventId = $lastEventId; + } } diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php index 9c117a3cb..d59dbda4a 100644 --- a/tests/ClientReport/ClientReportAggregatorTest.php +++ b/tests/ClientReport/ClientReportAggregatorTest.php @@ -73,12 +73,12 @@ public function testFlushDoesNotOverwriteLastEventId(): void $eventId = captureMessage('foo'); $this->assertNotNull($eventId); - $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), 10); ClientReportAggregator::getInstance()->flush(); - $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testNegativeQuantityDiscarded(): void diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index c0e597a4f..34e77e592 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -51,6 +51,7 @@ use function Sentry\startTransaction; use function Sentry\trace; use function Sentry\withContext; +use function Sentry\withIsolationScope; use function Sentry\withMonitor; use function Sentry\withScope; @@ -101,7 +102,7 @@ public function testCaptureMessage(array $functionCallArgs, array $expectedFunct ->willReturn($eventId); $this->assertSame($eventId, captureMessage(...$functionCallArgs)); - $this->assertSame($eventId, $scope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public static function captureMessageDataProvider(): \Generator @@ -148,7 +149,7 @@ public function testCaptureException(array $functionCallArgs, array $expectedFun ->willReturn($eventId); $this->assertSame($eventId, captureException(...$functionCallArgs)); - $this->assertSame($eventId, $scope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public static function captureExceptionDataProvider(): \Generator @@ -189,7 +190,7 @@ public function testCaptureEvent(): void ->willReturn($event->getId()); $this->assertSame($event->getId(), captureEvent($event, $hint)); - $this->assertSame($event->getId(), $scope->getLastEventId()); + $this->assertSame($event->getId(), SentrySdk::getLastEventId()); } /** @@ -210,7 +211,7 @@ public function testCaptureLastError(array $functionCallArgs, array $expectedFun @trigger_error('foo', \E_USER_NOTICE); $this->assertSame($eventId, captureLastError(...$functionCallArgs)); - $this->assertSame($eventId, $scope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public static function captureLastErrorDataProvider(): \Generator @@ -230,7 +231,7 @@ public function testCaptureMessageClearsLastEventIdWhenClientReturnsNull(): void { $client = $this->createMock(ClientInterface::class); $scope = $this->setClientAndIsolationScope($client); - $scope->setLastEventId(EventId::generate()); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); $client->expects($this->once()) ->method('captureMessage') @@ -238,7 +239,7 @@ public function testCaptureMessageClearsLastEventIdWhenClientReturnsNull(): void ->willReturn(null); $this->assertNull(captureMessage('foo')); - $this->assertNull($scope->getLastEventId()); + $this->assertNull(SentrySdk::getLastEventId()); } public function testCaptureExceptionClearsLastEventIdWhenClientReturnsNull(): void @@ -246,7 +247,7 @@ public function testCaptureExceptionClearsLastEventIdWhenClientReturnsNull(): vo $exception = new \RuntimeException('foo'); $client = $this->createMock(ClientInterface::class); $scope = $this->setClientAndIsolationScope($client); - $scope->setLastEventId(EventId::generate()); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); $client->expects($this->once()) ->method('captureException') @@ -254,7 +255,7 @@ public function testCaptureExceptionClearsLastEventIdWhenClientReturnsNull(): vo ->willReturn(null); $this->assertNull(captureException($exception)); - $this->assertNull($scope->getLastEventId()); + $this->assertNull(SentrySdk::getLastEventId()); } public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void @@ -262,7 +263,7 @@ public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void $event = Event::createEvent(); $client = $this->createMock(ClientInterface::class); $scope = $this->setClientAndIsolationScope($client); - $scope->setLastEventId(EventId::generate()); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); $client->expects($this->once()) ->method('captureEvent') @@ -270,14 +271,14 @@ public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void ->willReturn(null); $this->assertNull(captureEvent($event)); - $this->assertNull($scope->getLastEventId()); + $this->assertNull(SentrySdk::getLastEventId()); } public function testCaptureLastErrorClearsLastEventIdWhenClientReturnsNull(): void { $client = $this->createMock(ClientInterface::class); $scope = $this->setClientAndIsolationScope($client); - $scope->setLastEventId(EventId::generate()); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); $client->expects($this->once()) ->method('captureLastError') @@ -285,7 +286,27 @@ public function testCaptureLastErrorClearsLastEventIdWhenClientReturnsNull(): vo ->willReturn(null); $this->assertNull(captureLastError()); - $this->assertNull($scope->getLastEventId()); + $this->assertNull(SentrySdk::getLastEventId()); + } + + public function testCaptureExceptionInsideWithIsolationScopeUpdatesLastEventId(): void + { + $eventId = EventId::generate(); + $exception = new \RuntimeException('foo'); + $client = $this->createMock(ClientInterface::class); + $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureException') + ->willReturn($eventId); + + withIsolationScope(static function () use ($exception): void { + captureException($exception); + }); + + // The forked isolation scope is discarded after the callback, but the + // captured event ID must remain readable from the runtime context. + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testCaptureCheckIn(): void @@ -331,7 +352,7 @@ public function testCaptureCheckIn(): void $monitorConfig, $checkInId )); - $this->assertSame($eventId, $scope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testCaptureCheckInReturnsNullForNoOpClient(): void diff --git a/tests/State/EventRecorderTest.php b/tests/State/EventRecorderTest.php index 8eb644697..b26d5b14b 100644 --- a/tests/State/EventRecorderTest.php +++ b/tests/State/EventRecorderTest.php @@ -37,7 +37,7 @@ public function testCaptureMessagePassesIsolationScopeAndStoresLastEventId(): vo ->willReturn($eventId); $this->assertSame($eventId, EventRecorder::captureMessage('foo', Severity::debug(), $hint, $isolationScope)); - $this->assertSame($eventId, $isolationScope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testCaptureExceptionPassesIsolationScopeAndStoresLastEventId(): void @@ -56,7 +56,7 @@ public function testCaptureExceptionPassesIsolationScopeAndStoresLastEventId(): ->willReturn($eventId); $this->assertSame($eventId, EventRecorder::captureException($exception, $hint, $isolationScope)); - $this->assertSame($eventId, $isolationScope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testCaptureEventPassesIsolationScopeAndStoresLastEventId(): void @@ -74,7 +74,7 @@ public function testCaptureEventPassesIsolationScopeAndStoresLastEventId(): void ->willReturn($event->getId()); $this->assertSame($event->getId(), EventRecorder::captureEvent($event, $hint, $isolationScope)); - $this->assertSame($event->getId(), $isolationScope->getLastEventId()); + $this->assertSame($event->getId(), SentrySdk::getLastEventId()); } public function testCaptureLastErrorPassesIsolationScopeAndStoresLastEventId(): void @@ -92,7 +92,7 @@ public function testCaptureLastErrorPassesIsolationScopeAndStoresLastEventId(): ->willReturn($eventId); $this->assertSame($eventId, EventRecorder::captureLastError($hint, $isolationScope)); - $this->assertSame($eventId, $isolationScope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void @@ -100,7 +100,7 @@ public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void $event = Event::createEvent(); $client = $this->createMock(ClientInterface::class); $isolationScope = $this->setClientAndIsolationScope($client); - $isolationScope->setLastEventId(EventId::generate()); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); $client->expects($this->once()) ->method('captureEvent') @@ -110,7 +110,7 @@ public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void ->willReturn(null); $this->assertNull(EventRecorder::captureEvent($event)); - $this->assertNull($isolationScope->getLastEventId()); + $this->assertNull(SentrySdk::getLastEventId()); } public function testCaptureCheckInCreatesEventAndStoresLastEventId(): void @@ -158,17 +158,30 @@ public function testCaptureCheckInCreatesEventAndStoresLastEventId(): void $checkInId, $isolationScope )); - $this->assertSame($eventId, $isolationScope->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testCaptureCheckInReturnsNullForNoOpClient(): void { SentrySdk::init(new NoOpClient()); $eventId = EventId::generate(); - SentrySdk::getIsolationScope()->setLastEventId($eventId); + SentrySdk::getCurrentRuntimeContext()->setLastEventId($eventId); $this->assertNull(EventRecorder::captureCheckIn('test-crontab', CheckInStatus::ok())); - $this->assertSame($eventId, SentrySdk::getIsolationScope()->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureDoesNotClearLastEventIdForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + $eventId = EventId::generate(); + SentrySdk::getCurrentRuntimeContext()->setLastEventId($eventId); + + $this->assertNull(EventRecorder::captureMessage('foo')); + $this->assertNull(EventRecorder::captureException(new \RuntimeException('foo'))); + $this->assertNull(EventRecorder::captureEvent(Event::createEvent())); + $this->assertNull(EventRecorder::captureLastError()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } private function setClientAndIsolationScope(ClientInterface $client): IsolationScope diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index c9810dc61..b02a18af0 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -10,7 +10,6 @@ use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; -use Sentry\EventId; use Sentry\NoOpClient; use Sentry\Options; use Sentry\Severity; @@ -65,22 +64,6 @@ public function testClonedScopeKeepsClientShared(): void $this->assertSame($client, $clonedScope->getClient()); } - public function testGetAndSetLastEventId(): void - { - $scope = new IsolationScope(); - - $this->assertNull($scope->getLastEventId()); - - $eventId = EventId::generate(); - $scope->setLastEventId($eventId); - - $this->assertSame($eventId, $scope->getLastEventId()); - - $scope->setLastEventId(null); - - $this->assertNull($scope->getLastEventId()); - } - public function testSetTag(): void { $scope = new IsolationScope(); @@ -501,10 +484,8 @@ public function testClear(): void $scope = new IsolationScope(); $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $client = $this->createMock(ClientInterface::class); - $eventId = EventId::generate(); $scope->setClient($client); - $scope->setLastEventId($eventId); $scope->setLevel(Severity::info()); $scope->addBreadcrumb($breadcrumb); $scope->setFingerprint(['foo']); @@ -525,7 +506,6 @@ public function testClear(): void $this->assertEmpty($event->getUser()); $this->assertArrayNotHasKey('flags', $event->getContexts()); $this->assertSame($client, $scope->getClient()); - $this->assertSame($eventId, $scope->getLastEventId()); } public function testApplyToEvent(): void diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index aff82026f..1727fb373 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -74,8 +74,7 @@ public function testFinish(): void $eventId = $transaction->finish(); $this->assertSame($expectedEventId, $eventId); - $this->assertSame($expectedEventId, $scope->getLastEventId()); - $this->assertNull(SentrySdk::getIsolationScope()->getLastEventId()); + $this->assertSame($expectedEventId, SentrySdk::getLastEventId()); } public function testFinishDoesNothingIfSampledFlagIsNotTrue(): void