From 8c357561c9f1711977df595e92f7e70b103125e0 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Wed, 10 Jun 2026 22:05:32 +0200 Subject: [PATCH 1/6] feat(scopes): add new scopes --- 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 ecfa38bf97a45238ed4392ec19d1080b53e1ecb3 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 11 Jun 2026 00:11:19 +0200 Subject: [PATCH 2/6] feat(scopes): add clients to scopes --- src/SentrySdk.php | 39 +++++++++++++++-- src/State/RuntimeContext.php | 18 +++++++- src/State/Scope.php | 55 +++++++++++++++++++++++- src/functions.php | 2 +- tests/FunctionsTest.php | 18 ++++++++ tests/SentrySdkExtension.php | 9 ++++ tests/SentrySdkTest.php | 81 ++++++++++++++++++++++++++++++++++++ tests/State/ScopeTest.php | 61 +++++++++++++++++++++++++++ 8 files changed, 276 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..bad7db925 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -43,6 +43,7 @@ use function Sentry\continueTrace; use function Sentry\endContext; use function Sentry\getBaggage; +use function Sentry\getClient; use function Sentry\getOtlpTracesEndpointUrl; use function Sentry\getTraceparent; use function Sentry\init; @@ -60,6 +61,23 @@ public function testInit(): void init(['default_integrations' => false]); $this->assertNotNull(SentrySdk::getCurrentHub()->getClient()); + $this->assertSame(SentrySdk::getCurrentHub()->getClient(), 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 8b22eacc2cab19efab9e09b1a10abb6069228f57 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 11 Jun 2026 00:13:20 +0200 Subject: [PATCH 3/6] fix --- tests/FunctionsTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index bad7db925..ba349bde9 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -43,7 +43,6 @@ use function Sentry\continueTrace; use function Sentry\endContext; use function Sentry\getBaggage; -use function Sentry\getClient; use function Sentry\getOtlpTracesEndpointUrl; use function Sentry\getTraceparent; use function Sentry\init; @@ -61,7 +60,7 @@ public function testInit(): void init(['default_integrations' => false]); $this->assertNotNull(SentrySdk::getCurrentHub()->getClient()); - $this->assertSame(SentrySdk::getCurrentHub()->getClient(), getClient()); + $this->assertSame(SentrySdk::getCurrentHub()->getClient(), SentrySdk::getClient()); } public function testInitPreservesGlobalScope(): void From 56159170b3b48262d7c229eb4444107da9236690 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 11 Jun 2026 11:21:34 +0200 Subject: [PATCH 4/6] feat(scopes): extract transaction sampling logic into TransactionSampler --- 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 f28ee44b7fb05f7b33e28b2cc4d3b65d7d4d17be Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 11 Jun 2026 13:36:06 +0200 Subject: [PATCH 5/6] make TransactionSampler static --- src/State/Hub.php | 5 +- src/Tracing/TransactionSampler.php | 39 ++-- src/functions.php | 3 +- tests/FunctionsTest.php | 42 ++-- tests/State/HubTest.php | 186 ++++++------------ tests/Tracing/GuzzleTracingMiddlewareTest.php | 82 +++----- tests/Tracing/TransactionSamplerTest.php | 9 +- tests/Tracing/TransactionTest.php | 36 ++-- 8 files changed, 150 insertions(+), 252 deletions(-) diff --git a/src/State/Hub.php b/src/State/Hub.php index 2f4b947ed..5cebfbda4 100644 --- a/src/State/Hub.php +++ b/src/State/Hub.php @@ -19,7 +19,6 @@ 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. @@ -236,9 +235,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 \Sentry\startTransaction($context, $customSamplingContext); } /** diff --git a/src/Tracing/TransactionSampler.php b/src/Tracing/TransactionSampler.php index c3bd46b9f..6d946d680 100644 --- a/src/Tracing/TransactionSampler.php +++ b/src/Tracing/TransactionSampler.php @@ -7,30 +7,23 @@ use Sentry\Options; /** - * Applies tracing and profiling sampling decisions to transactions. - * * @internal */ 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 +38,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 +49,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,20 +95,20 @@ 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(); @@ -126,7 +119,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 +135,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 +155,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/src/functions.php b/src/functions.php index 1549bf9fe..b48dc728b 100644 --- a/src/functions.php +++ b/src/functions.php @@ -15,6 +15,7 @@ use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSampler; use Sentry\Transport\TransportInterface; /** @@ -268,7 +269,7 @@ function withContext(callable $callback, ?int $timeout = null) */ function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); + return TransactionSampler::startTransaction(SentrySdk::getClient()->getOptions(), $context, $customSamplingContext); } /** diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index ba349bde9..57a2e8f79 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; use Sentry\CheckInStatus; +use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; @@ -23,6 +24,7 @@ use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; +use Sentry\Tracing\SamplingContext; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\SpanId; @@ -452,18 +454,28 @@ public function testWithContextAlwaysEndsContextWithOptionalTimeout(): void public function testStartTransaction(): void { $transactionContext = new TransactionContext('foo'); - $transaction = new Transaction($transactionContext); $customSamplingContext = ['foo' => 'bar']; + $samplerInvoked = false; - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('startTransaction') - ->with($transactionContext, $customSamplingContext) - ->willReturn($transaction); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext, &$samplerInvoked): float { + $this->assertSame($customSamplingContext, $samplingContext->getAdditionalContext()); + $samplerInvoked = true; - SentrySdk::setCurrentHub($hub); + return 1.0; + }, + ])); + + SentrySdk::init($client); - $this->assertSame($transaction, startTransaction($transactionContext, $customSamplingContext)); + $transaction = startTransaction($transactionContext, $customSamplingContext); + + $this->assertSame('foo', $transaction->getName()); + $this->assertTrue($transaction->getSampled()); + $this->assertTrue($samplerInvoked); } public function testTraceReturnsClosureResult(): void @@ -620,17 +632,15 @@ public function testBaggageWithTracingDisabled(): void public function testBaggageWithTracingEnabled(): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeastOnce()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'release' => '1.0.0', - 'environment' => 'development', - ])); + $client = new Client(new Options([ + 'traces_sample_rate' => 1.0, + 'release' => '1.0.0', + 'environment' => 'development', + ]), StubTransport::getInstance()); $hub = new Hub($client); + SentrySdk::getGlobalScope()->setClient($client); SentrySdk::setCurrentHub($hub); $transactionContext = new TransactionContext(); diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php index 972ce617e..74eac77f3 100644 --- a/tests/State/HubTest.php +++ b/tests/State/HubTest.php @@ -9,6 +9,7 @@ use Sentry\Breadcrumb; use Sentry\CheckIn; use Sentry\CheckInStatus; +use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; @@ -18,9 +19,11 @@ use Sentry\MonitorSchedule; use Sentry\NoOpClient; use Sentry\Options; +use Sentry\SentrySdk; use Sentry\Severity; use Sentry\State\Hub; use Sentry\State\Scope; +use Sentry\Tests\StubTransport; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SamplingContext; @@ -626,12 +629,7 @@ public function testGetIntegration(): void */ 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); + $hub = SentrySdk::init(new Client($options, StubTransport::getInstance())); $transaction = $hub->startTransaction($transactionContext); $this->assertSame($expectedSampled, $transaction->getSampled()); @@ -639,14 +637,9 @@ public function testStartTransactionWithTracesSampler(Options $options, Transact 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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 0.0, + ]), StubTransport::getInstance())); $transactionContext = TransactionContext::fromHeaders('', 'sentry-sample_rate=1'); $transaction = $hub->startTransaction($transactionContext); @@ -805,12 +798,7 @@ public static function startTransactionDataProvider(): iterable public function testStartTransactionDoesNothingIfTracingIsNotEnabled(): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - $hub = new Hub($client); + $hub = SentrySdk::init(new Client(new Options(), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertFalse($transaction->getSampled()); @@ -820,34 +808,25 @@ 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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { + $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); - return 1.0; - }, - ])); + return 1.0; + }, + ]), StubTransport::getInstance())); - $hub = new Hub($client); $hub->startTransaction(new TransactionContext(), $customSamplingContext); } public function testStartTransactionStartsProfilerWithProfilesSampler(): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->exactly(2)) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 1.0; - }, - ])); - - $hub = new Hub($client); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => static function (): float { + return 1.0; + }, + ]), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertTrue($transaction->getSampled()); @@ -856,17 +835,12 @@ public function testStartTransactionStartsProfilerWithProfilesSampler(): void 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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => static function (): float { + return 0.0; + }, + ]), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertTrue($transaction->getSampled()); @@ -875,18 +849,13 @@ public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerRetur 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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sample_rate' => 1.0, + 'profiles_sampler' => static function (): float { + return 0.0; + }, + ]), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertTrue($transaction->getSampled()); @@ -897,35 +866,26 @@ public function testStartTransactionWithProfilesSamplerReceivesCustomSamplingCon { $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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { + $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); - return 0.0; - }, - ])); + return 0.0; + }, + ]), StubTransport::getInstance())); - $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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 1.0, + 'profiles_sampler' => static function (): string { + return 'foo'; + }, + ]), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertTrue($transaction->getSampled()); @@ -936,19 +896,15 @@ public function testStartTransactionDoesNotCallProfilesSamplerWhenTransactionIsN { $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; + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sample_rate' => 0.0, + 'profiles_sampler' => static function () use (&$profilesSamplerInvoked): float { + $profilesSamplerInvoked = true; - return 1.0; - }, - ])); + return 1.0; + }, + ]), StubTransport::getInstance())); - $hub = new Hub($client); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertFalse($transaction->getSampled()); @@ -958,16 +914,11 @@ public function testStartTransactionDoesNotCallProfilesSamplerWhenTransactionIsN 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); + $hub = SentrySdk::init(new Client(new Options([ + 'traces_sampler' => static function (SamplingContext $samplingContext): float { + return 1.0; + }, + ]), StubTransport::getInstance())); $dsc = DynamicSamplingContext::fromHeader('sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public'); $transactionMetaData = new TransactionMetadata(null, $dsc); @@ -979,12 +930,7 @@ public function testStartTransactionUpdatesTheDscSampleRate(): void 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); + $hub = SentrySdk::init(new Client(new Options(['traces_sample_rate' => 1]), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, false)); $hub->configureScope(static function (Scope $scope) use ($transaction): void { @@ -996,12 +942,7 @@ public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsNot 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); + $hub = SentrySdk::init(new Client(new Options(['traces_sample_rate' => 1]), StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, true)); $hub->configureScope(static function (Scope $scope) use ($transaction): void { @@ -1013,12 +954,7 @@ public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsSam 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 = SentrySdk::init(new Client(new Options(['traces_sample_rate' => 1]), StubTransport::getInstance())); $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, true)); $this->assertNull($hub->getTransaction()); diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index becfa84a8..fffeb3482 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -11,13 +11,13 @@ use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; use PHPUnit\Framework\TestCase; -use Sentry\ClientInterface; +use Sentry\Client; use Sentry\Event; use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; +use Sentry\Tests\StubTransport; use Sentry\Tracing\GuzzleTracingMiddleware; use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TransactionContext; @@ -26,15 +26,11 @@ final class GuzzleTracingMiddlewareTest extends TestCase { public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeast(2)) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 0, - ])); + $client = new Client(new Options([ + 'traces_sample_rate' => 0, + ]), StubTransport::getInstance()); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + $hub = SentrySdk::init($client); $transaction = $hub->startTransaction(TransactionContext::make()); @@ -71,15 +67,11 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeast(2)) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1, - ])); + $client = new Client(new Options([ + 'traces_sample_rate' => 1, + ]), StubTransport::getInstance()); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + $hub = SentrySdk::init($client); $transaction = $hub->startTransaction(TransactionContext::make()); @@ -120,13 +112,7 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void */ public function testTraceHeaders(Request $request, Options $options, bool $headersShouldBePresent): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeastOnce()) - ->method('getOptions') - ->willReturn($options); - - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + $hub = SentrySdk::init(new Client($options, StubTransport::getInstance())); $expectedPromiseResult = new Response(); @@ -152,13 +138,7 @@ public function testTraceHeaders(Request $request, Options $options, bool $heade */ public function testTraceHeadersWithTransaction(Request $request, Options $options, bool $headersShouldBePresent): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeast(2)) - ->method('getOptions') - ->willReturn($options); - - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + $hub = SentrySdk::init(new Client($options, StubTransport::getInstance())); $transaction = $hub->startTransaction(new TransactionContext()); @@ -194,15 +174,9 @@ public function testTraceHeadersAreNotAddedWhenExternalPropagationContextIsActiv ]; }); - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeastOnce()) - ->method('getOptions') - ->willReturn(new Options([ - 'trace_propagation_targets' => null, - ])); - - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + $hub = SentrySdk::init(new Client(new Options([ + 'trace_propagation_targets' => null, + ]), StubTransport::getInstance())); $expectedPromiseResult = new Response(); $middleware = GuzzleTracingMiddleware::trace($hub); @@ -319,18 +293,20 @@ public static function traceHeadersDataProvider(): iterable */ public function testTrace(Request $request, $expectedPromiseResult, array $expectedBreadcrumbData, array $expectedSpanData): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->atLeast(4)) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1, - 'trace_propagation_targets' => [ - 'www.example.com', - ], - ])); - - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + $client = $this->getMockBuilder(Client::class) + ->setConstructorArgs([ + new Options([ + 'traces_sample_rate' => 1, + 'trace_propagation_targets' => [ + 'www.example.com', + ], + ]), + StubTransport::getInstance(), + ]) + ->onlyMethods(['captureEvent']) + ->getMock(); + + $hub = SentrySdk::init($client); $client->expects($this->once()) ->method('captureEvent') 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); } } diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index 76a2c3aab..f27b12825 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -5,13 +5,15 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; +use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\EventType; use Sentry\Options; -use Sentry\State\Hub; +use Sentry\SentrySdk; use Sentry\State\HubInterface; +use Sentry\Tests\StubTransport; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -102,17 +104,12 @@ public function testFluentApi(): void */ public function testTransactionIsSampledCorrectlyWhenTracingIsSetToZeroInOptions(TransactionContext $context, bool $expectedSampled): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn( - new Options([ - 'traces_sampler' => null, - 'traces_sample_rate' => 0, - ]) - ); + $client = new Client(new Options([ + 'traces_sampler' => null, + 'traces_sample_rate' => 0, + ]), StubTransport::getInstance()); - $transaction = (new Hub($client))->startTransaction($context); + $transaction = SentrySdk::init($client)->startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } @@ -145,17 +142,12 @@ public static function parentTransactionContextDataProvider(): \Generator */ public function testTransactionIsNotSampledWhenTracingIsDisabledInOptions(TransactionContext $context, bool $expectedSampled): void { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn( - new Options([ - 'traces_sampler' => null, - 'traces_sample_rate' => null, - ]) - ); - - $transaction = (new Hub($client))->startTransaction($context); + $client = new Client(new Options([ + 'traces_sampler' => null, + 'traces_sample_rate' => null, + ]), StubTransport::getInstance()); + + $transaction = SentrySdk::init($client)->startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } From 0ab8198c6a7ee7780cb5a509e478c4eb1f7ac803 Mon Sep 17 00:00:00 2001 From: Martin Linzmayer Date: Thu, 11 Jun 2026 15:54:32 +0200 Subject: [PATCH 6/6] Revert "make TransactionSampler static" This reverts commit f28ee44b7fb05f7b33e28b2cc4d3b65d7d4d17be. --- src/State/Hub.php | 5 +- src/Tracing/TransactionSampler.php | 39 ++-- src/functions.php | 3 +- tests/FunctionsTest.php | 42 ++-- tests/State/HubTest.php | 186 ++++++++++++------ tests/Tracing/GuzzleTracingMiddlewareTest.php | 82 +++++--- tests/Tracing/TransactionSamplerTest.php | 9 +- tests/Tracing/TransactionTest.php | 36 ++-- 8 files changed, 252 insertions(+), 150 deletions(-) diff --git a/src/State/Hub.php b/src/State/Hub.php index 5cebfbda4..2f4b947ed 100644 --- a/src/State/Hub.php +++ b/src/State/Hub.php @@ -19,6 +19,7 @@ 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. @@ -235,7 +236,9 @@ public function getIntegration(string $className): ?IntegrationInterface */ public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return \Sentry\startTransaction($context, $customSamplingContext); + $transaction = new Transaction($context, $this); + + return (new TransactionSampler($this->getClient()->getOptions()))->startTransaction($transaction, $context, $customSamplingContext); } /** diff --git a/src/Tracing/TransactionSampler.php b/src/Tracing/TransactionSampler.php index 6d946d680..c3bd46b9f 100644 --- a/src/Tracing/TransactionSampler.php +++ b/src/Tracing/TransactionSampler.php @@ -7,23 +7,30 @@ use Sentry\Options; /** + * Applies tracing and profiling sampling decisions to transactions. + * * @internal */ final class TransactionSampler { - private function __construct() + /** + * @var Options + */ + private $options; + + public function __construct(Options $options) { + $this->options = $options; } /** * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} */ - public static function startTransaction(Options $options, TransactionContext $context, array $customSamplingContext = []): Transaction + public function startTransaction(Transaction $transaction, TransactionContext $context, array $customSamplingContext = []): Transaction { - $transaction = new Transaction($context); - $logger = $options->getLoggerOrNullLogger(); + $logger = $this->options->getLoggerOrNullLogger(); - if (!$options->isTracingEnabled()) { + if (!$this->options->isTracingEnabled()) { $transaction->setSampled(false); $logger->warning(\sprintf('Transaction [%s] was started but tracing is not enabled.', (string) $transaction->getTraceId()), ['context' => $context]); @@ -38,7 +45,7 @@ public static function startTransaction(Options $options, TransactionContext $co $sampleRand = $context->getMetadata()->getSampleRand() ?? 0.0; if ($transaction->getSampled() === null) { - $tracesSampler = $options->getTracesSampler(); + $tracesSampler = $this->options->getTracesSampler(); if ($tracesSampler !== null) { $sampleRate = $tracesSampler($samplingContext); @@ -49,15 +56,15 @@ public static function startTransaction(Options $options, TransactionContext $co $sampleRate = $parentSampleRate; $sampleSource = 'parent:sample_rate'; } else { - $sampleRate = self::getSampleRate( + $sampleRate = $this->getSampleRate( $samplingContext->getParentSampled(), - $options->getTracesSampleRate() ?? 0 + $this->options->getTracesSampleRate() ?? 0 ); $sampleSource = $samplingContext->getParentSampled() !== null ? 'parent:sampling_decision' : 'config:traces_sample_rate'; } } - if (!self::isValidSampleRate($sampleRate)) { + 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]); @@ -95,20 +102,20 @@ public static function startTransaction(Options $options, TransactionContext $co $transaction->initSpanRecorder(); $profilesSampleSource = 'config:profiles_sample_rate'; - $profilesSampler = $options->getProfilesSampler(); + $profilesSampler = $this->options->getProfilesSampler(); if ($profilesSampler !== null) { $profilesSampleRate = $profilesSampler($samplingContext); $profilesSampleSource = 'config:profiles_sampler'; } else { - $profilesSampleRate = $options->getProfilesSampleRate(); + $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 (!self::isValidSampleRate($profilesSampleRate)) { + } 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 (self::sampleRate($profilesSampleRate)) { + } elseif ($this->sampleRate($profilesSampleRate)) { $logger->info(\sprintf('Transaction [%s] started profiling because it was sampled.', (string) $transaction->getTraceId())); $transaction->initProfiler()->start(); @@ -119,7 +126,7 @@ public static function startTransaction(Options $options, TransactionContext $co return $transaction; } - private static function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float + private function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float { if ($hasParentBeenSampled === true) { return 1.0; @@ -135,7 +142,7 @@ private static function getSampleRate(?bool $hasParentBeenSampled, float $fallba /** * @param mixed $sampleRate */ - private static function sampleRate($sampleRate): bool + private function sampleRate($sampleRate): bool { if (!\is_float($sampleRate) && !\is_int($sampleRate)) { return false; @@ -155,7 +162,7 @@ private static function sampleRate($sampleRate): bool /** * @param mixed $sampleRate */ - private static function isValidSampleRate($sampleRate): bool + private function isValidSampleRate($sampleRate): bool { if (!\is_float($sampleRate) && !\is_int($sampleRate)) { return false; diff --git a/src/functions.php b/src/functions.php index b48dc728b..1549bf9fe 100644 --- a/src/functions.php +++ b/src/functions.php @@ -15,7 +15,6 @@ use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; -use Sentry\Tracing\TransactionSampler; use Sentry\Transport\TransportInterface; /** @@ -269,7 +268,7 @@ function withContext(callable $callback, ?int $timeout = null) */ function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return TransactionSampler::startTransaction(SentrySdk::getClient()->getOptions(), $context, $customSamplingContext); + return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); } /** diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 57a2e8f79..ba349bde9 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; use Sentry\CheckInStatus; -use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; @@ -24,7 +23,6 @@ use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; -use Sentry\Tracing\SamplingContext; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; use Sentry\Tracing\SpanId; @@ -454,28 +452,18 @@ public function testWithContextAlwaysEndsContextWithOptionalTimeout(): void public function testStartTransaction(): void { $transactionContext = new TransactionContext('foo'); + $transaction = new Transaction($transactionContext); $customSamplingContext = ['foo' => 'bar']; - $samplerInvoked = false; - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext, &$samplerInvoked): float { - $this->assertSame($customSamplingContext, $samplingContext->getAdditionalContext()); - $samplerInvoked = true; - - return 1.0; - }, - ])); - - SentrySdk::init($client); + $hub = $this->createMock(HubInterface::class); + $hub->expects($this->once()) + ->method('startTransaction') + ->with($transactionContext, $customSamplingContext) + ->willReturn($transaction); - $transaction = startTransaction($transactionContext, $customSamplingContext); + SentrySdk::setCurrentHub($hub); - $this->assertSame('foo', $transaction->getName()); - $this->assertTrue($transaction->getSampled()); - $this->assertTrue($samplerInvoked); + $this->assertSame($transaction, startTransaction($transactionContext, $customSamplingContext)); } public function testTraceReturnsClosureResult(): void @@ -632,15 +620,17 @@ public function testBaggageWithTracingDisabled(): void public function testBaggageWithTracingEnabled(): void { - $client = new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'release' => '1.0.0', - 'environment' => 'development', - ]), StubTransport::getInstance()); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 1.0, + 'release' => '1.0.0', + 'environment' => 'development', + ])); $hub = new Hub($client); - SentrySdk::getGlobalScope()->setClient($client); SentrySdk::setCurrentHub($hub); $transactionContext = new TransactionContext(); diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php index 74eac77f3..972ce617e 100644 --- a/tests/State/HubTest.php +++ b/tests/State/HubTest.php @@ -9,7 +9,6 @@ use Sentry\Breadcrumb; use Sentry\CheckIn; use Sentry\CheckInStatus; -use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; @@ -19,11 +18,9 @@ use Sentry\MonitorSchedule; use Sentry\NoOpClient; use Sentry\Options; -use Sentry\SentrySdk; use Sentry\Severity; use Sentry\State\Hub; use Sentry\State\Scope; -use Sentry\Tests\StubTransport; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SamplingContext; @@ -629,7 +626,12 @@ public function testGetIntegration(): void */ public function testStartTransactionWithTracesSampler(Options $options, TransactionContext $transactionContext, bool $expectedSampled): void { - $hub = SentrySdk::init(new Client($options, StubTransport::getInstance())); + $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()); @@ -637,9 +639,14 @@ public function testStartTransactionWithTracesSampler(Options $options, Transact public function testStartTransactionIgnoresBaggageSampleRateWithoutSentryTrace(): void { - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 0.0, - ]), StubTransport::getInstance())); + $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); @@ -798,7 +805,12 @@ public static function startTransactionDataProvider(): iterable public function testStartTransactionDoesNothingIfTracingIsNotEnabled(): void { - $hub = SentrySdk::init(new Client(new Options(), StubTransport::getInstance())); + $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()); @@ -808,25 +820,34 @@ public function testStartTransactionWithCustomSamplingContext(): void { $customSamplingContext = ['a' => 'b']; - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { - $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); + $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; - }, - ]), StubTransport::getInstance())); + return 1.0; + }, + ])); + $hub = new Hub($client); $hub->startTransaction(new TransactionContext(), $customSamplingContext); } public function testStartTransactionStartsProfilerWithProfilesSampler(): void { - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 1.0; - }, - ]), StubTransport::getInstance())); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->exactly(2)) + ->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()); @@ -835,12 +856,17 @@ public function testStartTransactionStartsProfilerWithProfilesSampler(): void public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerReturnsZero(): void { - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 0.0; - }, - ]), StubTransport::getInstance())); + $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()); @@ -849,13 +875,18 @@ public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerRetur public function testStartTransactionPrefersProfilesSamplerOverProfilesSampleRate(): void { - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 0.0; - }, - ]), StubTransport::getInstance())); + $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()); @@ -866,26 +897,35 @@ public function testStartTransactionWithProfilesSamplerReceivesCustomSamplingCon { $customSamplingContext = ['a' => 'b']; - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { - $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); + $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; - }, - ]), StubTransport::getInstance())); + return 0.0; + }, + ])); + $hub = new Hub($client); $hub->startTransaction(new TransactionContext(), $customSamplingContext); } public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerReturnsInvalidValue(): void { - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): string { - return 'foo'; - }, - ]), StubTransport::getInstance())); + $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()); @@ -896,15 +936,19 @@ public function testStartTransactionDoesNotCallProfilesSamplerWhenTransactionIsN { $profilesSamplerInvoked = false; - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sample_rate' => 0.0, - 'profiles_sampler' => static function () use (&$profilesSamplerInvoked): float { - $profilesSamplerInvoked = true; + $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; - }, - ]), StubTransport::getInstance())); + return 1.0; + }, + ])); + $hub = new Hub($client); $transaction = $hub->startTransaction(new TransactionContext()); $this->assertFalse($transaction->getSampled()); @@ -914,11 +958,16 @@ public function testStartTransactionDoesNotCallProfilesSamplerWhenTransactionIsN public function testStartTransactionUpdatesTheDscSampleRate(): void { - $hub = SentrySdk::init(new Client(new Options([ - 'traces_sampler' => static function (SamplingContext $samplingContext): float { - return 1.0; - }, - ]), StubTransport::getInstance())); + $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); @@ -930,7 +979,12 @@ public function testStartTransactionUpdatesTheDscSampleRate(): void public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsNotSampled(): void { - $hub = SentrySdk::init(new Client(new Options(['traces_sample_rate' => 1]), StubTransport::getInstance())); + $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 { @@ -942,7 +996,12 @@ public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsNot public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsSampled(): void { - $hub = SentrySdk::init(new Client(new Options(['traces_sample_rate' => 1]), StubTransport::getInstance())); + $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 { @@ -954,7 +1013,12 @@ public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsSam public function testGetTransactionReturnsNullIfNoTransactionIsSetOnTheScope(): void { - $hub = SentrySdk::init(new Client(new Options(['traces_sample_rate' => 1]), StubTransport::getInstance())); + $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()); diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index fffeb3482..becfa84a8 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -11,13 +11,13 @@ use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Uri; use PHPUnit\Framework\TestCase; -use Sentry\Client; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; +use Sentry\State\Hub; use Sentry\State\Scope; -use Sentry\Tests\StubTransport; use Sentry\Tracing\GuzzleTracingMiddleware; use Sentry\Tracing\SpanStatus; use Sentry\Tracing\TransactionContext; @@ -26,11 +26,15 @@ final class GuzzleTracingMiddlewareTest extends TestCase { public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void { - $client = new Client(new Options([ - 'traces_sample_rate' => 0, - ]), StubTransport::getInstance()); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeast(2)) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 0, + ])); - $hub = SentrySdk::init($client); + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); $transaction = $hub->startTransaction(TransactionContext::make()); @@ -67,11 +71,15 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void { - $client = new Client(new Options([ - 'traces_sample_rate' => 1, - ]), StubTransport::getInstance()); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeast(2)) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 1, + ])); - $hub = SentrySdk::init($client); + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); $transaction = $hub->startTransaction(TransactionContext::make()); @@ -112,7 +120,13 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void */ public function testTraceHeaders(Request $request, Options $options, bool $headersShouldBePresent): void { - $hub = SentrySdk::init(new Client($options, StubTransport::getInstance())); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn($options); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); $expectedPromiseResult = new Response(); @@ -138,7 +152,13 @@ public function testTraceHeaders(Request $request, Options $options, bool $heade */ public function testTraceHeadersWithTransaction(Request $request, Options $options, bool $headersShouldBePresent): void { - $hub = SentrySdk::init(new Client($options, StubTransport::getInstance())); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeast(2)) + ->method('getOptions') + ->willReturn($options); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); $transaction = $hub->startTransaction(new TransactionContext()); @@ -174,9 +194,15 @@ public function testTraceHeadersAreNotAddedWhenExternalPropagationContextIsActiv ]; }); - $hub = SentrySdk::init(new Client(new Options([ - 'trace_propagation_targets' => null, - ]), StubTransport::getInstance())); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn(new Options([ + 'trace_propagation_targets' => null, + ])); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); $expectedPromiseResult = new Response(); $middleware = GuzzleTracingMiddleware::trace($hub); @@ -293,20 +319,18 @@ public static function traceHeadersDataProvider(): iterable */ public function testTrace(Request $request, $expectedPromiseResult, array $expectedBreadcrumbData, array $expectedSpanData): void { - $client = $this->getMockBuilder(Client::class) - ->setConstructorArgs([ - new Options([ - 'traces_sample_rate' => 1, - 'trace_propagation_targets' => [ - 'www.example.com', - ], - ]), - StubTransport::getInstance(), - ]) - ->onlyMethods(['captureEvent']) - ->getMock(); - - $hub = SentrySdk::init($client); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeast(4)) + ->method('getOptions') + ->willReturn(new Options([ + 'traces_sample_rate' => 1, + 'trace_propagation_targets' => [ + 'www.example.com', + ], + ])); + + $hub = new Hub($client); + SentrySdk::setCurrentHub($hub); $client->expects($this->once()) ->method('captureEvent') diff --git a/tests/Tracing/TransactionSamplerTest.php b/tests/Tracing/TransactionSamplerTest.php index 9118e9c12..1fe3aab72 100644 --- a/tests/Tracing/TransactionSamplerTest.php +++ b/tests/Tracing/TransactionSamplerTest.php @@ -5,7 +5,9 @@ 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; @@ -318,6 +320,11 @@ public function testUpdatesTheDscSampleRate(): void */ private function sampleTransaction(Options $options, TransactionContext $transactionContext, array $customSamplingContext = []): Transaction { - return TransactionSampler::startTransaction($options, $transactionContext, $customSamplingContext); + $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); } } diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index f27b12825..76a2c3aab 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -5,15 +5,13 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; -use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\EventType; use Sentry\Options; -use Sentry\SentrySdk; +use Sentry\State\Hub; use Sentry\State\HubInterface; -use Sentry\Tests\StubTransport; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -104,12 +102,17 @@ public function testFluentApi(): void */ public function testTransactionIsSampledCorrectlyWhenTracingIsSetToZeroInOptions(TransactionContext $context, bool $expectedSampled): void { - $client = new Client(new Options([ - 'traces_sampler' => null, - 'traces_sample_rate' => 0, - ]), StubTransport::getInstance()); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn( + new Options([ + 'traces_sampler' => null, + 'traces_sample_rate' => 0, + ]) + ); - $transaction = SentrySdk::init($client)->startTransaction($context); + $transaction = (new Hub($client))->startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } @@ -142,12 +145,17 @@ public static function parentTransactionContextDataProvider(): \Generator */ public function testTransactionIsNotSampledWhenTracingIsDisabledInOptions(TransactionContext $context, bool $expectedSampled): void { - $client = new Client(new Options([ - 'traces_sampler' => null, - 'traces_sample_rate' => null, - ]), StubTransport::getInstance()); - - $transaction = SentrySdk::init($client)->startTransaction($context); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn( + new Options([ + 'traces_sampler' => null, + 'traces_sample_rate' => null, + ]) + ); + + $transaction = (new Hub($client))->startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); }