diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49052d375d..8bcf0d0ca3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: - name: Remove OpenTelemetry dependencies on unsupported PHP versions if: ${{ matrix.php.version == '7.2' || matrix.php.version == '7.3' || matrix.php.version == '7.4' || matrix.php.version == '8.0' }} - run: composer remove open-telemetry/api open-telemetry/exporter-otlp open-telemetry/sdk --dev --no-interaction --no-update + run: composer remove open-telemetry/api open-telemetry/exporter-otlp open-telemetry/sem-conv open-telemetry/sdk --dev --no-interaction --no-update - name: Set phpunit/phpunit version constraint run: composer require phpunit/phpunit:'${{ matrix.php.phpunit }}' --dev --no-interaction --no-update diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 578ffaaaaa..92a1f90bc5 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -21,6 +21,39 @@ $logger->pushHandler(new \Sentry\Monolog\LogsHandler()); To continue sending Monolog records to Sentry issues instead, use `Sentry\Monolog\LogToSentryIssueHandler` for log messages or `Sentry\Monolog\ExceptionToSentryIssueHandler` for exceptions. +### Hub APIs Removed + +The Hub API has been removed. This includes: + +- **Removed classes and interfaces:** + - `Sentry\State\Hub` + - `Sentry\State\HubAdapter` + - `Sentry\State\HubInterface` + +- **Removed methods:** + - `SentrySdk::getCurrentHub()` + - `SentrySdk::setCurrentHub()` + +The SDK now exposes runtime state directly through `SentrySdk`, global helpers, and `Scope`: + +```php +// Before (4.x) +\Sentry\SentrySdk::getCurrentHub()->configureScope(static function (\Sentry\State\Scope $scope): void { + $scope->setTag('feature', 'checkout'); +}); + +// After (5.0) +\Sentry\configureScope(static function (\Sentry\State\Scope $scope): void { + $scope->setTag('feature', 'checkout'); +}); +``` + +Use `SentrySdk::getClient()` for the active client, `SentrySdk::getGlobalScope()` for process-global data, and `SentrySdk::getIsolationScope()` for the current runtime isolation scope. Use the capture helpers such as `captureMessage()`, `captureException()`, and `captureEvent()` to send events. + +Use `configureScope()` to mutate the current isolation scope, `withIsolationScope()` to run code with a temporary forked scope, and `startTransaction()` to start manual tracing instrumentation. + +`SentrySdk::init()` no longer returns a Hub instance. It now returns `void`. + ### Metrics API Removed The entire Metrics API has been removed as it is no longer supported. This includes: diff --git a/analysis-baseline.toml b/analysis-baseline.toml index 2c30c8e024..bb9bc9876e 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -1368,18 +1368,6 @@ code = "possibly-invalid-argument" message = '''Possible argument type mismatch for argument #1 of `Sentry\StacktraceBuilder::buildFromBacktrace`: expected `list, 'class'?: class-string, 'file'?: string, 'function'?: string, 'line'?: int, 'type'?: string}>`, but possibly received `array`.''' count = 1 -[[issues]] -file = "src/State/Hub.php" -code = "mixed-operand" -message = "Right operand in `<` comparison has `mixed` type." -count = 1 - -[[issues]] -file = "src/State/Hub.php" -code = "possibly-null-operand" -message = "Left operand in `<` comparison might be `null` (type `float|null`)." -count = 1 - [[issues]] file = "src/State/Scope.php" code = "impossible-condition" diff --git a/composer.json b/composer.json index c545b0bbcf..cce2f1ddb8 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "nyholm/psr7": "^1.8", "open-telemetry/api": "^1.0", "open-telemetry/exporter-otlp": "^1.0", + "open-telemetry/sem-conv": "^1.27", "open-telemetry/sdk": "^1.0", "phpstan/phpstan": "^1.3", "phpunit/phpunit": "^8.5.52|^9.6.34", diff --git a/mago.toml b/mago.toml index f8b021c004..b4534de5de 100644 --- a/mago.toml +++ b/mago.toml @@ -10,3 +10,8 @@ excludes = [ [analyzer] baseline = "analysis-baseline.toml" +ignore = [ + { code = "no-value", pattern = "DebugType::getDebugType" }, + "impossible-condition", + "redundant-logical-operation", +] diff --git a/src/Client.php b/src/Client.php index 2bb80c7307..9255639e54 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,7 +10,7 @@ use Sentry\Integration\IntegrationRegistry; use Sentry\Serializer\RepresentationSerializer; use Sentry\Serializer\RepresentationSerializerInterface; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; use Sentry\Transport\TransportInterface; @@ -140,7 +140,7 @@ public function getCspReportUrl(): ?string /** * {@inheritdoc} */ - public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureMessage(string $message, ?Severity $level = null, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { $event = Event::createEvent(); $event->setMessage($message); @@ -152,7 +152,7 @@ public function captureMessage(string $message, ?Severity $level = null, ?Scope /** * {@inheritdoc} */ - public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureException(\Throwable $exception, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { $className = \get_class($exception); if ($this->shouldIgnoreException($className)) { @@ -176,7 +176,7 @@ public function captureException(\Throwable $exception, ?Scope $scope = null, ?E /** * {@inheritdoc} */ - public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId + public function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?EventId { // Client reports don't need to be augmented in the prepareEvent pipeline. if ($event->getType() !== EventType::clientReport()) { @@ -208,7 +208,7 @@ public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scop /** * {@inheritdoc} */ - public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureLastError(?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { $error = error_get_last(); @@ -277,13 +277,13 @@ public function getSdkVersion(): string /** * Assembles an event and prepares it to be sent of to Sentry. * - * @param Event $event The payload that will be converted to an Event - * @param EventHint|null $hint May contain additional information about the event - * @param Scope|null $scope Optional scope which enriches the Event + * @param Event $event The payload that will be converted to an Event + * @param EventHint|null $hint May contain additional information about the event + * @param IsolationScope|null $scope Optional scope which enriches the Event * * @return Event|null The prepared event object or null if it must be discarded */ - private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?Event + private function prepareEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?Event { if ($hint !== null) { if ($hint->exception !== null && empty($event->getExceptions())) { @@ -339,7 +339,8 @@ private function prepareEvent(Event $event, ?EventHint $hint = null, ?Scope $sco if ($scope !== null) { $beforeEventProcessors = $event; - $event = $scope->applyToEvent($event, $hint, $this->options); + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scope)->applyToEvent($event, $hint, $this->options); if ($event === null) { $this->logger->info( diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 5d511519f7..8aa245b709 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -5,7 +5,7 @@ namespace Sentry; use Sentry\Integration\IntegrationInterface; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; interface ClientInterface @@ -23,38 +23,38 @@ public function getCspReportUrl(): ?string; /** * Logs a message. * - * @param string $message The message (primary description) for the event - * @param Severity|null $level The level of the message to be sent - * @param Scope|null $scope An optional scope keeping the state - * @param EventHint|null $hint Object that can contain additional information about the event + * @param string $message The message (primary description) for the event + * @param Severity|null $level The level of the message to be sent + * @param IsolationScope|null $scope An optional scope keeping the state + * @param EventHint|null $hint Object that can contain additional information about the event */ - public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId; + public function captureMessage(string $message, ?Severity $level = null, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId; /** * Logs an exception. * - * @param \Throwable $exception The exception object - * @param Scope|null $scope An optional scope keeping the state - * @param EventHint|null $hint Object that can contain additional information about the event + * @param \Throwable $exception The exception object + * @param IsolationScope|null $scope An optional scope keeping the state + * @param EventHint|null $hint Object that can contain additional information about the event */ - public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId; + public function captureException(\Throwable $exception, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId; /** * Logs the most recent error (obtained with {@link error_get_last}). * - * @param Scope|null $scope An optional scope keeping the state - * @param EventHint|null $hint Object that can contain additional information about the event + * @param IsolationScope|null $scope An optional scope keeping the state + * @param EventHint|null $hint Object that can contain additional information about the event */ - public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId; + public function captureLastError(?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId; /** * Captures a new event using the provided data. * - * @param Event $event The event being captured - * @param EventHint|null $hint May contain additional information about the event - * @param Scope|null $scope An optional scope keeping the state + * @param Event $event The event being captured + * @param EventHint|null $hint May contain additional information about the event + * @param IsolationScope|null $scope An optional scope keeping the state */ - public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId; + public function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?EventId; /** * Returns the integration instance if it is installed on the client. diff --git a/src/ClientReport/ClientReportAggregator.php b/src/ClientReport/ClientReportAggregator.php index 8045a51b2b..454b0ba267 100644 --- a/src/ClientReport/ClientReportAggregator.php +++ b/src/ClientReport/ClientReportAggregator.php @@ -5,7 +5,7 @@ namespace Sentry\ClientReport; use Sentry\Event; -use Sentry\State\HubAdapter; +use Sentry\SentrySdk; use Sentry\Transport\DataCategory; class ClientReportAggregator @@ -35,15 +35,13 @@ public function add(DataCategory $category, Reason $reason, int $quantity): void $category = $category->getValue(); $reason = $reason->getValue(); if ($quantity <= 0) { - $client = HubAdapter::getInstance()->getClient(); - if ($client !== null) { - $logger = $client->getOptions()->getLoggerOrNullLogger(); - $logger->debug('Dropping Client report with category={category} and reason={reason} because quantity is zero or negative ({quantity})', [ - 'category' => $category, - 'reason' => $reason, - 'quantity' => $quantity, - ]); - } + $client = SentrySdk::getClient(); + $logger = $client->getOptions()->getLoggerOrNullLogger(); + $logger->debug('Dropping Client report with category={category} and reason={reason} because quantity is zero or negative ({quantity})', [ + 'category' => $category, + 'reason' => $reason, + 'quantity' => $quantity, + ]); return; } @@ -64,11 +62,11 @@ public function flush(): void $event = Event::createClientReport(); $event->setClientReports($reports); - $client = HubAdapter::getInstance()->getClient(); + $client = SentrySdk::getClient(); // Reset the client reports only if we successfully sent an event. If it fails it // can be sent on the next flush, or it gets discarded anyway. - if ($client !== null && $client->captureEvent($event) !== null) { + if ($client->captureEvent($event) !== null) { $this->reports = []; } } diff --git a/src/Integration/AbstractErrorListenerIntegration.php b/src/Integration/AbstractErrorListenerIntegration.php index a8894a7e29..9b1cc8a0dd 100644 --- a/src/Integration/AbstractErrorListenerIntegration.php +++ b/src/Integration/AbstractErrorListenerIntegration.php @@ -5,24 +5,26 @@ namespace Sentry\Integration; use Sentry\Event; +use Sentry\EventHint; use Sentry\ExceptionMechanism; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\State\EventRecorder; +use Sentry\State\IsolationScope; + +use function Sentry\withIsolationScope; abstract class AbstractErrorListenerIntegration implements IntegrationInterface { /** - * Captures the exception using the given hub instance. - * - * @param HubInterface $hub The hub instance - * @param \Throwable $exception The exception instance + * @param \Throwable $exception The exception instance */ - protected function captureException(HubInterface $hub, \Throwable $exception): void + protected function captureException(\Throwable $exception): void { - $hub->withScope(function (Scope $scope) use ($hub, $exception): void { - $scope->addEventProcessor(\Closure::fromCallable([$this, 'addExceptionMechanismToEvent'])); + withIsolationScope(function (IsolationScope $scope) use ($exception): void { + $scope->addEventProcessor(function (Event $event, EventHint $hint): Event { + return $this->addExceptionMechanismToEvent($event); + }); - $hub->captureException($exception); + EventRecorder::captureException($exception, null, $scope); }); } diff --git a/src/Integration/EnvironmentIntegration.php b/src/Integration/EnvironmentIntegration.php index c404216ea9..adf4f6a53a 100644 --- a/src/Integration/EnvironmentIntegration.php +++ b/src/Integration/EnvironmentIntegration.php @@ -24,7 +24,7 @@ final class EnvironmentIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $integration = SentrySdk::getClient()->getIntegration(self::class); if ($integration !== null) { $event->setRuntimeContext($integration->updateRuntimeContext($event->getRuntimeContext())); diff --git a/src/Integration/ErrorListenerIntegration.php b/src/Integration/ErrorListenerIntegration.php index a4b95c2ba7..fa33cd2a0c 100644 --- a/src/Integration/ErrorListenerIntegration.php +++ b/src/Integration/ErrorListenerIntegration.php @@ -33,24 +33,22 @@ public function setupOnce(): void ErrorHandler::registerOnceErrorHandler($this->options) ->addErrorHandlerListener( static function (\ErrorException $exception): void { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); if ($integration === null) { return; } - $client = $currentHub->getClient(); - if ($exception instanceof SilencedErrorException && !$client->getOptions()->shouldCaptureSilencedErrors()) { return; } - if (!$exception instanceof SilencedErrorException && !($client->getOptions()->getErrorTypes() & $exception->getSeverity())) { + if (!$exception instanceof SilencedErrorException && ($client->getOptions()->getErrorTypes() & $exception->getSeverity()) === 0) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); } ); } diff --git a/src/Integration/ExceptionListenerIntegration.php b/src/Integration/ExceptionListenerIntegration.php index 18e7afc757..c86ff02bb2 100644 --- a/src/Integration/ExceptionListenerIntegration.php +++ b/src/Integration/ExceptionListenerIntegration.php @@ -20,16 +20,14 @@ public function setupOnce(): void { $errorHandler = ErrorHandler::registerOnceExceptionHandler(); $errorHandler->addExceptionHandlerListener(static function (\Throwable $exception): void { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); - // The client bound to the current hub, if any, could not have this - // integration enabled. If this is the case, bail out if ($integration === null) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); }); } } diff --git a/src/Integration/FatalErrorListenerIntegration.php b/src/Integration/FatalErrorListenerIntegration.php index 3cc0566688..e4b53c279c 100644 --- a/src/Integration/FatalErrorListenerIntegration.php +++ b/src/Integration/FatalErrorListenerIntegration.php @@ -22,20 +22,18 @@ public function setupOnce(): void { $errorHandler = ErrorHandler::registerOnceFatalErrorHandler(); $errorHandler->addFatalErrorHandlerListener(static function (FatalErrorException $exception): void { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); if ($integration === null) { return; } - $client = $currentHub->getClient(); - - if (!($client->getOptions()->getErrorTypes() & $exception->getSeverity())) { + if (($client->getOptions()->getErrorTypes() & $exception->getSeverity()) === 0) { return; } - $integration->captureException($currentHub, $exception); + $integration->captureException($exception); }); } } diff --git a/src/Integration/FrameContextifierIntegration.php b/src/Integration/FrameContextifierIntegration.php index 8e55a97278..b7ea13b401 100644 --- a/src/Integration/FrameContextifierIntegration.php +++ b/src/Integration/FrameContextifierIntegration.php @@ -41,7 +41,7 @@ public function __construct(?LoggerInterface $logger = null) public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { - $client = SentrySdk::getCurrentHub()->getClient(); + $client = SentrySdk::getClient(); $maxContextLines = $client->getOptions()->getContextLines(); $integration = $client->getIntegration(self::class); diff --git a/src/Integration/ModulesIntegration.php b/src/Integration/ModulesIntegration.php index affe1e062b..9e5ee8c164 100644 --- a/src/Integration/ModulesIntegration.php +++ b/src/Integration/ModulesIntegration.php @@ -26,10 +26,8 @@ final class ModulesIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $integration = SentrySdk::getClient()->getIntegration(self::class); - // The integration could be bound to a client that is not the one - // attached to the current hub. If this is the case, bail out if ($integration !== null) { $event->setModules(self::getComposerPackages()); } diff --git a/src/Integration/OTLPIntegration.php b/src/Integration/OTLPIntegration.php index e5e04ef8e2..184d55b50f 100644 --- a/src/Integration/OTLPIntegration.php +++ b/src/Integration/OTLPIntegration.php @@ -56,8 +56,7 @@ public function setupOnce(): void } Scope::registerExternalPropagationContext(static function (): ?array { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $integration = SentrySdk::getClient()->getIntegration(self::class); if (!$integration instanceof self) { return null; @@ -193,9 +192,6 @@ private function getLogger(): LoggerInterface return $this->options->getLoggerOrNullLogger(); } - $currentHub = SentrySdk::getCurrentHub(); - $client = $currentHub->getClient(); - - return $client->getOptions()->getLoggerOrNullLogger(); + return SentrySdk::getClient()->getOptions()->getLoggerOrNullLogger(); } } diff --git a/src/Integration/RequestIntegration.php b/src/Integration/RequestIntegration.php index db123b0198..98aa4ccb3e 100644 --- a/src/Integration/RequestIntegration.php +++ b/src/Integration/RequestIntegration.php @@ -103,15 +103,13 @@ public function __construct(?RequestFetcherInterface $requestFetcher = null, arr public function setupOnce(): void { Scope::addGlobalEventProcessor(function (Event $event): Event { - $currentHub = SentrySdk::getCurrentHub(); - $integration = $currentHub->getIntegration(self::class); + $client = SentrySdk::getClient(); + $integration = $client->getIntegration(self::class); if ($integration === null) { return $event; } - $client = $currentHub->getClient(); - $this->processEvent($event, $client->getOptions()); return $event; diff --git a/src/Integration/TransactionIntegration.php b/src/Integration/TransactionIntegration.php index 1ed22d7538..13c7bfe51c 100644 --- a/src/Integration/TransactionIntegration.php +++ b/src/Integration/TransactionIntegration.php @@ -24,10 +24,8 @@ final class TransactionIntegration implements IntegrationInterface public function setupOnce(): void { Scope::addGlobalEventProcessor(static function (Event $event, EventHint $hint): Event { - $integration = SentrySdk::getCurrentHub()->getIntegration(self::class); + $integration = SentrySdk::getClient()->getIntegration(self::class); - // The client bound to the current hub, if any, could not have this - // integration enabled. If this is the case, bail out if ($integration === null) { return $event; } diff --git a/src/Logs/LogsAggregator.php b/src/Logs/LogsAggregator.php index 0fa66b7fcb..4bd2c5e77a 100644 --- a/src/Logs/LogsAggregator.php +++ b/src/Logs/LogsAggregator.php @@ -6,10 +6,11 @@ use Sentry\Attributes\Attribute; use Sentry\Client; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\SentrySdk; -use Sentry\State\HubInterface; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Util\Arr; use Sentry\Util\Str; @@ -40,8 +41,10 @@ public function add( ): void { $timestamp = microtime(true); - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $isolationScope = SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($isolationScope); + $globalScope = SentrySdk::getGlobalScope(); + $scope = $globalScope->merge($isolationScope); $options = $client->getOptions(); $sdkLogger = $options->getLogger(); @@ -70,7 +73,7 @@ public function add( $formattedMessage = $message; } - $traceData = $this->getTraceData($hub); + $traceData = $this->getTraceData($isolationScope); $traceId = $traceData['trace_id']; $parentSpanId = $traceData['parent_span_id']; @@ -85,20 +88,18 @@ public function add( $log->setAttribute('sentry.sdk.version', $client->getSdkVersion()); } - $hub->configureScope(static function (Scope $scope) use ($log) { - $user = $scope->getUser(); - if ($user !== null) { - if ($user->getId() !== null) { - $log->setAttribute('user.id', $user->getId()); - } - if ($user->getEmail() !== null) { - $log->setAttribute('user.email', $user->getEmail()); - } - if ($user->getUsername() !== null) { - $log->setAttribute('user.name', $user->getUsername()); - } + $user = $scope->getUser(); + if ($user !== null) { + if ($user->getId() !== null) { + $log->setAttribute('user.id', $user->getId()); + } + if ($user->getEmail() !== null) { + $log->setAttribute('user.email', $user->getEmail()); } - }); + if ($user->getUsername() !== null) { + $log->setAttribute('user.name', $user->getUsername()); + } + } if (\count($values)) { $log->setAttribute('sentry.message.template', $message); @@ -159,20 +160,23 @@ public function add( $logs->push($log); if ($logFlushThreshold !== null && \count($logs) >= $logFlushThreshold) { - $this->flush($hub); + $this->flush($client); } } - public function flush(?HubInterface $hub = null): ?EventId + public function flush(?ClientInterface $client = null, ?IsolationScope $isolationScope = null): ?EventId { - if ($this->logs === null || $this->logs->isEmpty()) { + $logs = $this->logs; + + if ($logs === null || $logs->isEmpty()) { return null; } - $hub = $hub ?? SentrySdk::getCurrentHub(); - $event = Event::createLogs()->setLogs($this->logs->drain()); + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + $client = $client ?? SentrySdk::getClient($isolationScope); + $event = Event::createLogs()->setLogs($logs->drain()); - return $hub->captureEvent($event); + return $client->captureEvent($event, null, $isolationScope); } /** @@ -186,9 +190,9 @@ public function all(): array /** * @return array{trace_id: string, parent_span_id: string|null} */ - private function getTraceData(HubInterface $hub): array + private function getTraceData(IsolationScope $scope): array { - $span = $hub->getSpan(); + $span = $scope->getSpan(); if ($span !== null) { return [ @@ -197,28 +201,18 @@ private function getTraceData(HubInterface $hub): array ]; } - $traceData = null; - - $hub->configureScope(static function (Scope $scope) use (&$traceData): void { - $externalPropagationContext = Scope::getExternalPropagationContext(); - - if ($externalPropagationContext !== null) { - $traceData = [ - 'trace_id' => $externalPropagationContext['trace_id'], - 'parent_span_id' => $externalPropagationContext['span_id'], - ]; - - return; - } - - $traceData = [ - 'trace_id' => (string) $scope->getPropagationContext()->getTraceId(), - 'parent_span_id' => null, + $externalPropagationContext = Scope::getExternalPropagationContext(); + if ($externalPropagationContext !== null) { + return [ + 'trace_id' => $externalPropagationContext['trace_id'], + 'parent_span_id' => $externalPropagationContext['span_id'], ]; - }); + } - /** @var array{trace_id: string, parent_span_id: string|null} $traceData */ - return $traceData; + return [ + 'trace_id' => (string) $scope->getPropagationContext()->getTraceId(), + 'parent_span_id' => null, + ]; } /** diff --git a/src/Metrics/MetricsAggregator.php b/src/Metrics/MetricsAggregator.php index a4a3aef3e4..f4519da1c4 100644 --- a/src/Metrics/MetricsAggregator.php +++ b/src/Metrics/MetricsAggregator.php @@ -5,6 +5,7 @@ namespace Sentry\Metrics; use Sentry\Client; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\Metrics\Types\CounterMetric; @@ -12,8 +13,7 @@ use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\SentrySdk; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tracing\SpanId; use Sentry\Tracing\TraceId; use Sentry\Unit; @@ -51,60 +51,54 @@ public function add( array $attributes, ?Unit $unit ): void { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); - $metricFlushThreshold = null; + $isolationScope = SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($isolationScope); + $globalScope = SentrySdk::getGlobalScope(); + $scope = $globalScope->merge($isolationScope); + $options = $client->getOptions(); + $metricFlushThreshold = $options->getMetricFlushThreshold(); if (!\is_int($value) && !\is_float($value)) { - if ($client !== null) { - $client->getOptions()->getLoggerOrNullLogger()->debug('Metrics value is neither int nor float. Metric will be discarded'); - } + $options->getLoggerOrNullLogger()->debug('Metrics value is neither int nor float. Metric will be discarded'); return; } - if ($client !== null) { - $options = $client->getOptions(); - $metricFlushThreshold = $options->getMetricFlushThreshold(); + if ($options->getEnableMetrics() === false) { + return; + } - if ($options->getEnableMetrics() === false) { - return; - } + $defaultAttributes = [ + 'sentry.environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, + 'server.address' => $options->getServerName(), + ]; - $defaultAttributes = [ - 'sentry.environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT, - 'server.address' => $options->getServerName(), - ]; + if ($client instanceof Client) { + $defaultAttributes['sentry.sdk.name'] = $client->getSdkIdentifier(); + $defaultAttributes['sentry.sdk.version'] = $client->getSdkVersion(); + } - if ($client instanceof Client) { - $defaultAttributes['sentry.sdk.name'] = $client->getSdkIdentifier(); - $defaultAttributes['sentry.sdk.version'] = $client->getSdkVersion(); + $user = $scope->getUser(); + if ($user !== null) { + if ($user->getId() !== null) { + $defaultAttributes['user.id'] = $user->getId(); } - - $hub->configureScope(static function (Scope $scope) use (&$defaultAttributes) { - $user = $scope->getUser(); - if ($user !== null) { - if ($user->getId() !== null) { - $defaultAttributes['user.id'] = $user->getId(); - } - if ($user->getEmail() !== null) { - $defaultAttributes['user.email'] = $user->getEmail(); - } - if ($user->getUsername() !== null) { - $defaultAttributes['user.name'] = $user->getUsername(); - } - } - }); - - $release = $options->getRelease(); - if ($release !== null) { - $defaultAttributes['sentry.release'] = $release; + if ($user->getEmail() !== null) { + $defaultAttributes['user.email'] = $user->getEmail(); + } + if ($user->getUsername() !== null) { + $defaultAttributes['user.name'] = $user->getUsername(); } + } - $attributes += $defaultAttributes; + $release = $options->getRelease(); + if ($release !== null) { + $defaultAttributes['sentry.release'] = $release; } - $traceContext = $this->getTraceContext($hub); + $attributes += $defaultAttributes; + + $traceContext = $this->getTraceContext($isolationScope); $traceId = new TraceId($traceContext['trace_id']); $spanId = new SpanId($traceContext['span_id']); @@ -112,47 +106,46 @@ public function add( /** @var Metric $metric */ $metric = new $metricTypeClass($name, $value, $traceId, $spanId, $attributes, microtime(true), $unit); - if ($client !== null) { - $beforeSendMetric = $client->getOptions()->getBeforeSendMetricCallback(); - $metric = $beforeSendMetric($metric); - if ($metric === null) { - return; - } + $beforeSendMetric = $options->getBeforeSendMetricCallback(); + $metric = $beforeSendMetric($metric); + if ($metric === null) { + return; } $metrics = $this->getStorage($metricFlushThreshold); $metrics->push($metric); if ($metricFlushThreshold !== null && \count($metrics) >= $metricFlushThreshold) { - $this->flush($hub); + $this->flush($client); } } - public function flush(?HubInterface $hub = null): ?EventId + public function flush(?ClientInterface $client = null, ?IsolationScope $isolationScope = null): ?EventId { - if ($this->metrics === null || $this->metrics->isEmpty()) { + $metrics = $this->metrics; + + if ($metrics === null || $metrics->isEmpty()) { return null; } - $hub = $hub ?? SentrySdk::getCurrentHub(); - $event = Event::createMetrics()->setMetrics($this->metrics->drain()); + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + $client = $client ?? SentrySdk::getClient($isolationScope); + $event = Event::createMetrics()->setMetrics($metrics->drain()); - return $hub->captureEvent($event); + return $client->captureEvent($event, null, $isolationScope); } /** * @return array{trace_id: string, span_id: string} */ - private function getTraceContext(HubInterface $hub): array + private function getTraceContext(IsolationScope $scope): array { - $traceContext = null; - - $hub->configureScope(static function (Scope $scope) use (&$traceContext): void { - $traceContext = $scope->getTraceContext(); - }); + $traceContext = $scope->getTraceContext(); - /** @var array{trace_id: string, span_id: string} $traceContext */ - return $traceContext; + return [ + 'trace_id' => $traceContext['trace_id'], + 'span_id' => $traceContext['span_id'], + ]; } /** diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php index 29e166671b..9fd3538bf6 100644 --- a/src/Monolog/BreadcrumbHandler.php +++ b/src/Monolog/BreadcrumbHandler.php @@ -11,33 +11,26 @@ use Psr\Log\LogLevel; use Sentry\Breadcrumb; use Sentry\Event; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\SentrySdk; +use Sentry\State\BreadcrumbRecorder; +use Sentry\State\IsolationScope; /** - * This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see Scope}, + * This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see IsolationScope}, * to enrich any event sent to Sentry. */ final class BreadcrumbHandler extends AbstractProcessingHandler { /** - * @var HubInterface - */ - private $hub; - - /** - * @param HubInterface $hub The hub to which errors are reported - * @param int|string $level The minimum logging level at which this - * handler will be triggered - * @param bool $bubble Whether the messages that are handled can - * bubble up the stack or not + * @param int|string $level The minimum logging level at which this + * handler will be triggered + * @param bool $bubble Whether the messages that are handled can + * bubble up the stack or not * * @phpstan-param int|string|Level|LogLevel::* $level */ - public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true) + public function __construct($level = Logger::DEBUG, bool $bubble = true) { - $this->hub = $hub; - parent::__construct($level, $bubble); } @@ -66,7 +59,8 @@ protected function write($record): void $timestamp ); - $this->hub->addBreadcrumb($breadcrumb); + $scope = SentrySdk::getIsolationScope(); + BreadcrumbRecorder::record(SentrySdk::getClient($scope), $scope, $breadcrumb); } /** diff --git a/src/Monolog/ExceptionToSentryIssueHandler.php b/src/Monolog/ExceptionToSentryIssueHandler.php index b2e7abeb6b..5173631d56 100644 --- a/src/Monolog/ExceptionToSentryIssueHandler.php +++ b/src/Monolog/ExceptionToSentryIssueHandler.php @@ -9,26 +9,21 @@ use Monolog\Logger; use Monolog\LogRecord; use Psr\Log\LogLevel; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\State\EventRecorder; +use Sentry\State\IsolationScope; + +use function Sentry\withIsolationScope; /** * This Monolog handler will collect monolog events and send them to sentry. */ class ExceptionToSentryIssueHandler extends AbstractHandler { - /** - * @var HubInterface - */ - private $hub; - /** * @phpstan-param value-of|value-of|Level|LogLevel::* $level */ - public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true) + public function __construct($level = Logger::DEBUG, bool $bubble = true) { - $this->hub = $hub; - parent::__construct($level, $bubble); } @@ -44,7 +39,7 @@ public function handle($record): bool return false; } - $this->hub->withScope(function (Scope $scope) use ($record, $exception): void { + withIsolationScope(function (IsolationScope $scope) use ($record, $exception): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); $scope->setExtra('monolog.message', $record['message']); @@ -61,7 +56,7 @@ public function handle($record): bool $scope->setExtra('monolog.extra', $monologExtraData); } - $this->hub->captureException($exception); + EventRecorder::captureException($exception, null, $scope); }); return $this->bubble === false; diff --git a/src/Monolog/LogToSentryIssueHandler.php b/src/Monolog/LogToSentryIssueHandler.php index 18dd6eab61..2866d32afb 100644 --- a/src/Monolog/LogToSentryIssueHandler.php +++ b/src/Monolog/LogToSentryIssueHandler.php @@ -11,8 +11,10 @@ use Psr\Log\LogLevel; use Sentry\Event; use Sentry\EventHint; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\State\EventRecorder; +use Sentry\State\IsolationScope; + +use function Sentry\withIsolationScope; /** * This Monolog handler captures log messages as Sentry issues. @@ -23,11 +25,6 @@ class LogToSentryIssueHandler extends AbstractProcessingHandler private const CONTEXT_EXCEPTION_KEY = 'exception'; - /** - * @var HubInterface - */ - private $hub; - /** * @var bool */ @@ -36,9 +33,8 @@ class LogToSentryIssueHandler extends AbstractProcessingHandler /** * @phpstan-param value-of|value-of|Level|LogLevel::* $level */ - public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true, bool $fillExtraContext = false) + public function __construct($level = Logger::DEBUG, bool $bubble = true, bool $fillExtraContext = false) { - $this->hub = $hub; $this->fillExtraContext = $fillExtraContext; parent::__construct($level, $bubble); @@ -70,7 +66,7 @@ protected function doWrite($record): void $hint = new EventHint(); - $this->hub->withScope(function (Scope $scope) use ($record, $event, $hint): void { + withIsolationScope(function (IsolationScope $scope) use ($record, $event, $hint): void { $scope->setExtra('monolog.channel', $record['channel']); $scope->setExtra('monolog.level', $record['level_name']); @@ -88,7 +84,7 @@ protected function doWrite($record): void } } - $this->hub->captureEvent($event, $hint); + EventRecorder::captureEvent($event, $hint, $scope); }); } diff --git a/src/NoOpClient.php b/src/NoOpClient.php index 364b6073e3..acf238b0de 100644 --- a/src/NoOpClient.php +++ b/src/NoOpClient.php @@ -6,7 +6,7 @@ use Sentry\Integration\IntegrationInterface; use Sentry\Serializer\RepresentationSerializer; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; @@ -54,22 +54,22 @@ public function getCspReportUrl(): ?string return null; } - public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureMessage(string $message, ?Severity $level = null, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { return null; } - public function captureException(\Throwable $exception, ?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureException(\Throwable $exception, ?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { return null; } - public function captureLastError(?Scope $scope = null, ?EventHint $hint = null): ?EventId + public function captureLastError(?IsolationScope $scope = null, ?EventHint $hint = null): ?EventId { return null; } - public function captureEvent(Event $event, ?EventHint $hint = null, ?Scope $scope = null): ?EventId + public function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null): ?EventId { return null; } diff --git a/src/SentrySdk.php b/src/SentrySdk.php index 9adce8c282..8145c82695 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -6,8 +6,8 @@ use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; -use Sentry\State\Hub; -use Sentry\State\HubInterface; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\RuntimeContext; use Sentry\State\RuntimeContextManager; @@ -19,9 +19,9 @@ final class SentrySdk { /** - * @var HubInterface|null The baseline hub + * @var GlobalScope|null The process-global scope */ - private static $currentHub; + private static $globalScope; /** * @var RuntimeContextManager|null @@ -36,47 +36,45 @@ private function __construct() } /** - * Initializes the SDK by creating a new hub instance each time this method - * gets called. + * Initializes the SDK by binding the client to the global scope and resetting + * the current local runtime state. */ - public static function init(?ClientInterface $client = null): HubInterface + public static function init(?ClientInterface $client = null): void { - if ($client === null) { - $client = new NoOpClient(); + if ($client !== null) { + self::getGlobalScope()->setClient($client); + } + self::$runtimeContextManager = new RuntimeContextManager(); + } + + public static function getGlobalScope(): GlobalScope + { + if (self::$globalScope === null) { + self::$globalScope = new GlobalScope(); } - self::$currentHub = new Hub($client); - self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub); - return self::getCurrentHub(); + return self::$globalScope; } - /** - * Gets the current hub. If it's not initialized then creates a new instance - * and sets it as current hub. - */ - public static function getCurrentHub(): HubInterface + public static function getIsolationScope(): IsolationScope { - return self::getRuntimeContextManager()->getCurrentHub(); + return self::getCurrentRuntimeContext()->getIsolationScope(); } - /** - * Sets the current hub. - * - * If called while an explicit runtime context is active, the hub update is - * scoped to that active context only. Otherwise, it updates the baseline - * hub used by the global fallback context and future contexts. - * - * @param HubInterface $hub The hub to set - */ - public static function setCurrentHub(HubInterface $hub): HubInterface + public static function getLastEventId(): ?EventId { - $wasSetOnActiveRuntimeContext = self::getRuntimeContextManager()->setCurrentHub($hub); + return self::getCurrentRuntimeContext()->getLastEventId(); + } - if (!$wasSetOnActiveRuntimeContext) { - self::$currentHub = $hub; + public static function getClient(?IsolationScope $isolationScope = null): ClientInterface + { + $client = ($isolationScope ?? self::getIsolationScope())->getClient(); + + if (!$client instanceof NoOpClient) { + return $client; } - return $hub; + return self::getGlobalScope()->getClient(); } public static function startContext(): void @@ -148,18 +146,13 @@ public static function flush(): void Logs::getInstance()->flush(); TraceMetrics::getInstance()->flush(); - $client = self::getCurrentHub()->getClient(); - $client->flush(); + self::getClient()->flush(); } private static function getRuntimeContextManager(): RuntimeContextManager { - if (self::$currentHub === null) { - self::$currentHub = new Hub(new NoOpClient()); - } - if (self::$runtimeContextManager === null) { - self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub); + self::$runtimeContextManager = new RuntimeContextManager(); } return self::$runtimeContextManager; diff --git a/src/State/BreadcrumbRecorder.php b/src/State/BreadcrumbRecorder.php new file mode 100644 index 0000000000..d3bfe25a4b --- /dev/null +++ b/src/State/BreadcrumbRecorder.php @@ -0,0 +1,44 @@ +getOptions(); + $maxBreadcrumbs = $options->getMaxBreadcrumbs(); + + if ($maxBreadcrumbs <= 0) { + return false; + } + + $breadcrumb = ($options->getBeforeBreadcrumbCallback())($breadcrumb); + + if ($breadcrumb !== null) { + $scope->addBreadcrumb($breadcrumb, $maxBreadcrumbs); + } + + return $breadcrumb !== null; + } +} diff --git a/src/State/EventRecorder.php b/src/State/EventRecorder.php new file mode 100644 index 0000000000..cd417ed6aa --- /dev/null +++ b/src/State/EventRecorder.php @@ -0,0 +1,109 @@ +captureMessage($message, $level, $captureScope, $hint); + }); + } + + public static function captureException(\Throwable $exception, ?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId + { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($exception, $hint): ?EventId { + return $client->captureException($exception, $captureScope, $hint); + }); + } + + public static function captureEvent(Event $event, ?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId + { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($event, $hint): ?EventId { + return $client->captureEvent($event, $hint, $captureScope); + }); + } + + public static function captureLastError(?EventHint $hint = null, ?IsolationScope $isolationScope = null): ?EventId + { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + + return self::captureWithScope(SentrySdk::getClient($isolationScope), $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($hint): ?EventId { + return $client->captureLastError($captureScope, $hint); + }); + } + + /** + * @param int|float|null $duration + */ + public static function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null, ?IsolationScope $isolationScope = null): ?string + { + $isolationScope = $isolationScope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($isolationScope); + + if ($client instanceof NoOpClient) { + return null; + } + + $options = $client->getOptions(); + $event = Event::createCheckIn(); + $checkIn = new CheckIn( + $slug, + $status, + $checkInId, + $options->getRelease(), + $options->getEnvironment(), + $duration, + $monitorConfig + ); + $event->setCheckIn($checkIn); + + self::captureWithScope($client, $isolationScope, static function (ClientInterface $client, IsolationScope $captureScope) use ($event): ?EventId { + return $client->captureEvent($event, null, $captureScope); + }); + + return $checkIn->getId(); + } + + /** + * @param callable(ClientInterface, IsolationScope): ?EventId $capture + */ + private static function captureWithScope(ClientInterface $client, IsolationScope $isolationScope, callable $capture): ?EventId + { + if ($client instanceof NoOpClient) { + return null; + } + + $eventId = $capture($client, $isolationScope); + SentrySdk::getCurrentRuntimeContext()->setLastEventId($eventId); + + return $eventId; + } +} diff --git a/src/State/GlobalScope.php b/src/State/GlobalScope.php new file mode 100644 index 0000000000..7483b22d6d --- /dev/null +++ b/src/State/GlobalScope.php @@ -0,0 +1,20 @@ +scopeData->merge($scope->scopeData), $scope->getSpan()); + } +} diff --git a/src/State/Hub.php b/src/State/Hub.php deleted file mode 100644 index e9132531e0..0000000000 --- a/src/State/Hub.php +++ /dev/null @@ -1,425 +0,0 @@ -stack[] = new Layer($client, $scope ?? new Scope()); - } - - /** - * {@inheritdoc} - */ - public function getClient(): ClientInterface - { - return $this->getStackTop()->getClient(); - } - - /** - * {@inheritdoc} - */ - public function getLastEventId(): ?EventId - { - return $this->lastEventId; - } - - /** - * {@inheritdoc} - */ - public function pushScope(): Scope - { - $clonedScope = clone $this->getScope(); - - $this->stack[] = new Layer($this->getClient(), $clonedScope); - - return $clonedScope; - } - - /** - * {@inheritdoc} - */ - public function popScope(): bool - { - if (\count($this->stack) === 1) { - return false; - } - - return array_pop($this->stack) !== null; - } - - /** - * {@inheritdoc} - */ - public function withScope(callable $callback) - { - $scope = $this->pushScope(); - - try { - return $callback($scope); - } finally { - $this->popScope(); - } - } - - /** - * {@inheritdoc} - */ - public function configureScope(callable $callback): void - { - $callback($this->getScope()); - } - - /** - * {@inheritdoc} - */ - public function bindClient(ClientInterface $client): void - { - $layer = $this->getStackTop(); - $layer->setClient($client); - } - - /** - * {@inheritdoc} - */ - public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureMessage($message, $level, $this->getScope(), $hint); - } - - /** - * {@inheritdoc} - */ - public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureException($exception, $this->getScope(), $hint); - } - - /** - * {@inheritdoc} - */ - public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureEvent($event, $hint, $this->getScope()); - } - - /** - * {@inheritdoc} - */ - public function captureLastError(?EventHint $hint = null): ?EventId - { - return $this->lastEventId = $this->getClient()->captureLastError($this->getScope(), $hint); - } - - /** - * {@inheritdoc} - * - * @param int|float|null $duration - */ - public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string - { - $client = $this->getClient(); - - if ($client instanceof NoOpClient) { - return null; - } - - $options = $client->getOptions(); - $event = Event::createCheckIn(); - $checkIn = new CheckIn( - $slug, - $status, - $checkInId, - $options->getRelease(), - $options->getEnvironment(), - $duration, - $monitorConfig - ); - $event->setCheckIn($checkIn); - $this->captureEvent($event); - - return $checkIn->getId(); - } - - /** - * {@inheritdoc} - */ - public function addBreadcrumb(Breadcrumb $breadcrumb): bool - { - $client = $this->getClient(); - - // No point in storing breadcrumbs if the client will never send them - if ($client instanceof NoOpClient) { - return false; - } - - $options = $client->getOptions(); - $beforeBreadcrumbCallback = $options->getBeforeBreadcrumbCallback(); - $maxBreadcrumbs = $options->getMaxBreadcrumbs(); - - if ($maxBreadcrumbs <= 0) { - return false; - } - - $breadcrumb = $beforeBreadcrumbCallback($breadcrumb); - - if ($breadcrumb !== null) { - $this->getScope()->addBreadcrumb($breadcrumb, $maxBreadcrumbs); - } - - return $breadcrumb !== null; - } - - public function addAttachment(Attachment $attachment): bool - { - // No point in storing attachments if the client will never send them - if ($this->getClient() instanceof NoOpClient) { - return false; - } - - $this->getScope()->addAttachment($attachment); - - return true; - } - - /** - * {@inheritdoc} - */ - public function getIntegration(string $className): ?IntegrationInterface - { - return $this->getClient()->getIntegration($className); - } - - /** - * {@inheritdoc} - * - * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} - */ - public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction - { - $transaction = new Transaction($context, $this); - $options = $this->getClient()->getOptions(); - $logger = $options->getLoggerOrNullLogger(); - - if (!$options->isTracingEnabled()) { - $transaction->setSampled(false); - - $logger->warning(\sprintf('Transaction [%s] was started but tracing is not enabled.', (string) $transaction->getTraceId()), ['context' => $context]); - - return $transaction; - } - - $samplingContext = SamplingContext::getDefault($context); - $samplingContext->setAdditionalContext($customSamplingContext); - - $sampleSource = 'context'; - $sampleRand = $context->getMetadata()->getSampleRand(); - - if ($transaction->getSampled() === null) { - $tracesSampler = $options->getTracesSampler(); - - if ($tracesSampler !== null) { - $sampleRate = $tracesSampler($samplingContext); - $sampleSource = 'config:traces_sampler'; - } else { - $parentSampleRate = $context->getMetadata()->getParentSamplingRate(); - if ($parentSampleRate !== null) { - $sampleRate = $parentSampleRate; - $sampleSource = 'parent:sample_rate'; - } else { - $sampleRate = $this->getSampleRate( - $samplingContext->getParentSampled(), - $options->getTracesSampleRate() ?? 0 - ); - $sampleSource = $samplingContext->getParentSampled() !== null ? 'parent:sampling_decision' : 'config:traces_sample_rate'; - } - } - - if (!$this->isValidSampleRate($sampleRate)) { - $transaction->setSampled(false); - - $logger->warning(\sprintf('Transaction [%s] was started but not sampled because sample rate (decided by %s) is invalid.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); - - return $transaction; - } - - $transaction->getMetadata()->setSamplingRate($sampleRate); - - // Always overwrite the sample_rate in the DSC - $dynamicSamplingContext = $context->getMetadata()->getDynamicSamplingContext(); - if ($dynamicSamplingContext !== null) { - $dynamicSamplingContext->set('sample_rate', (string) $sampleRate, true); - } - - if ($sampleRate === 0.0) { - $transaction->setSampled(false); - - $logger->info(\sprintf('Transaction [%s] was started but not sampled because sample rate (decided by %s) is %s.', (string) $transaction->getTraceId(), $sampleSource, $sampleRate), ['context' => $context]); - - return $transaction; - } - - $transaction->setSampled($sampleRand < $sampleRate); - } - - if (!$transaction->getSampled()) { - $logger->info(\sprintf('Transaction [%s] was started but not sampled, decided by %s.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); - - return $transaction; - } - - $logger->info(\sprintf('Transaction [%s] was started and sampled, decided by %s.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); - - $transaction->initSpanRecorder(); - - $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; - } - - /** - * {@inheritdoc} - */ - public function getTransaction(): ?Transaction - { - return $this->getScope()->getTransaction(); - } - - /** - * {@inheritdoc} - */ - public function setSpan(?Span $span): HubInterface - { - $this->getScope()->setSpan($span); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getSpan(): ?Span - { - return $this->getScope()->getSpan(); - } - - /** - * Gets the scope bound to the top of the stack. - */ - private function getScope(): Scope - { - return $this->getStackTop()->getScope(); - } - - /** - * Gets the topmost client/layer pair in the stack. - */ - private function getStackTop(): Layer - { - return $this->stack[\count($this->stack) - 1]; - } - - private function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float - { - if ($hasParentBeenSampled === true) { - return 1.0; - } - - if ($hasParentBeenSampled === false) { - return 0.0; - } - - return $fallbackSampleRate; - } - - /** - * @param mixed $sampleRate - */ - private function sample($sampleRate): bool - { - if ($sampleRate === 0.0 || $sampleRate === null) { - return false; - } - - if ($sampleRate === 1.0) { - return true; - } - - return mt_rand(0, mt_getrandmax() - 1) / mt_getrandmax() < $sampleRate; - } - - /** - * @param mixed $sampleRate - */ - private function isValidSampleRate($sampleRate): bool - { - if (!\is_float($sampleRate) && !\is_int($sampleRate)) { - return false; - } - - if ($sampleRate < 0 || $sampleRate > 1) { - return false; - } - - return true; - } -} diff --git a/src/State/HubAdapter.php b/src/State/HubAdapter.php deleted file mode 100644 index 83272be9b6..0000000000 --- a/src/State/HubAdapter.php +++ /dev/null @@ -1,230 +0,0 @@ -getClient(); - } - - /** - * {@inheritdoc} - */ - public function getLastEventId(): ?EventId - { - return SentrySdk::getCurrentHub()->getLastEventId(); - } - - /** - * {@inheritdoc} - */ - public function pushScope(): Scope - { - return SentrySdk::getCurrentHub()->pushScope(); - } - - /** - * {@inheritdoc} - */ - public function popScope(): bool - { - return SentrySdk::getCurrentHub()->popScope(); - } - - /** - * {@inheritdoc} - */ - public function withScope(callable $callback) - { - return SentrySdk::getCurrentHub()->withScope($callback); - } - - /** - * {@inheritdoc} - */ - public function configureScope(callable $callback): void - { - SentrySdk::getCurrentHub()->configureScope($callback); - } - - /** - * {@inheritdoc} - */ - public function bindClient(ClientInterface $client): void - { - SentrySdk::getCurrentHub()->bindClient($client); - } - - /** - * {@inheritdoc} - */ - public function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId - { - return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); - } - - /** - * {@inheritdoc} - */ - public function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId - { - return SentrySdk::getCurrentHub()->captureException($exception, $hint); - } - - /** - * {@inheritdoc} - */ - public function captureEvent(Event $event, ?EventHint $hint = null): ?EventId - { - return SentrySdk::getCurrentHub()->captureEvent($event, $hint); - } - - /** - * {@inheritdoc} - */ - public function captureLastError(?EventHint $hint = null): ?EventId - { - return SentrySdk::getCurrentHub()->captureLastError($hint); - } - - /** - * {@inheritdoc} - * - * @param int|float|null $duration - */ - public function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string - { - return SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); - } - - /** - * {@inheritdoc} - */ - public function addBreadcrumb(Breadcrumb $breadcrumb): bool - { - return SentrySdk::getCurrentHub()->addBreadcrumb($breadcrumb); - } - - /** - * {@inheritDoc} - */ - public function addAttachment(Attachment $attachment): bool - { - return SentrySdk::getCurrentHub()->addAttachment($attachment); - } - - /** - * {@inheritdoc} - */ - public function getIntegration(string $className): ?IntegrationInterface - { - return SentrySdk::getCurrentHub()->getIntegration($className); - } - - /** - * {@inheritdoc} - */ - public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction - { - return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); - } - - /** - * {@inheritdoc} - */ - public function getTransaction(): ?Transaction - { - return SentrySdk::getCurrentHub()->getTransaction(); - } - - /** - * {@inheritdoc} - */ - public function getSpan(): ?Span - { - return SentrySdk::getCurrentHub()->getSpan(); - } - - /** - * {@inheritdoc} - */ - public function setSpan(?Span $span): HubInterface - { - return SentrySdk::getCurrentHub()->setSpan($span); - } - - /** - * @see https://www.php.net/manual/en/language.oop5.cloning.php#object.clone - */ - public function __clone() - { - throw new \BadMethodCallException('Cloning is forbidden.'); - } - - /** - * @see https://www.php.net/manual/en/language.oop5.magic.php#object.wakeup - */ - public function __wakeup(): void - { - throw new \BadMethodCallException('Unserializing instances of this class is forbidden.'); - } - - /** - * @see https://www.php.net/manual/en/language.oop5.magic.php#object.sleep - */ - public function __sleep() - { - throw new \BadMethodCallException('Serializing instances of this class is forbidden.'); - } -} diff --git a/src/State/HubInterface.php b/src/State/HubInterface.php deleted file mode 100644 index c3c8c67a11..0000000000 --- a/src/State/HubInterface.php +++ /dev/null @@ -1,161 +0,0 @@ - $className - * - * @phpstan-return T|null - */ - public function getIntegration(string $className): ?IntegrationInterface; - - /** - * Starts a new `Transaction` and returns it. This is the entry point to manual - * tracing instrumentation. - * - * A tree structure can be built by adding child spans to the transaction, and - * child spans to other spans. To start a new child span within the transaction - * or any span, call the respective `startChild()` method. - * - * Every child span must be finished before the transaction is finished, - * otherwise the unfinished spans are discarded. - * - * The transaction must be finished with a call to its `finish()` method, at - * which point the transaction with all its finished child spans will be sent to - * Sentry. - * - * @param array $customSamplingContext Additional context that will be passed to the {@see SamplingContext} - */ - public function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction; - - /** - * Returns the transaction that is on the Hub. - */ - public function getTransaction(): ?Transaction; - - /** - * Returns the span that is on the Hub. - */ - public function getSpan(): ?Span; - - /** - * Sets the span on the Hub. - */ - public function setSpan(?Span $span): HubInterface; - - /** - * Records a new attachment that will be attached to error and transaction events. - */ - public function addAttachment(Attachment $attachment): bool; -} diff --git a/src/State/IsolationScope.php b/src/State/IsolationScope.php new file mode 100644 index 0000000000..c9b66d8e2d --- /dev/null +++ b/src/State/IsolationScope.php @@ -0,0 +1,126 @@ +scopeData->setPropagationContext($propagationContext ?? PropagationContext::fromDefaults()); + } + + /** + * Adds a feature flag to the scope. + * + * @return $this + */ + public function addFeatureFlag(string $key, bool $result): self + { + $this->scopeData->addFeatureFlag($key, $result); + + if ($this->span !== null) { + $this->span->setFlag($key, $result); + } + + return $this; + } + + /** + * Clears event payload data from the scope. The client binding and last + * event ID are preserved. + */ + public function clear(): void + { + parent::clear(); + $this->span = null; + } + + /** + * Returns the span that is on the scope. + */ + public function getSpan(): ?Span + { + return $this->span; + } + + /** + * Sets the span on the scope. + * + * @param Span|null $span The span + * + * @return $this + */ + public function setSpan(?Span $span): self + { + $this->span = $span; + + return $this; + } + + /** + * Returns the transaction attached to the scope (if there is one). + */ + public function getTransaction(): ?Transaction + { + if ($this->span !== null) { + return $this->span->getTransaction(); + } + + return null; + } + + public function hasExternalPropagationContext(): bool + { + return $this->span === null && self::getExternalPropagationContext() !== null; + } + + public function getPropagationContext(): PropagationContext + { + return $this->scopeData->getPropagationContext(); + } + + public function setPropagationContext(PropagationContext $propagationContext): self + { + $this->scopeData->setPropagationContext($propagationContext); + + return $this; + } + + /** + * @return array{ + * trace_id: string, + * span_id: string, + * parent_span_id?: string, + * data?: array, + * description?: string, + * op?: string, + * status?: string, + * tags?: array, + * origin?: string + * } + */ + public function getTraceContext(): array + { + if ($this->span !== null) { + return $this->span->getTraceContext(); + } + + return self::getExternalPropagationContext() ?? $this->scopeData->getPropagationContext()->getTraceContext(); + } +} diff --git a/src/State/Layer.php b/src/State/Layer.php deleted file mode 100644 index 3befbd80a8..0000000000 --- a/src/State/Layer.php +++ /dev/null @@ -1,82 +0,0 @@ -client = $client; - $this->scope = $scope; - } - - /** - * Gets the client held by this layer. - */ - public function getClient(): ClientInterface - { - return $this->client; - } - - /** - * Sets the client held by this layer. - * - * @param ClientInterface $client The client instance - * - * @return $this - */ - public function setClient(ClientInterface $client): self - { - $this->client = $client; - - return $this; - } - - /** - * Gets the scope held by this layer. - */ - public function getScope(): Scope - { - return $this->scope; - } - - /** - * Sets the scope held by this layer. - * - * @param Scope $scope The scope instance - * - * @return $this - */ - public function setScope(Scope $scope): self - { - $this->scope = $scope; - - return $this; - } -} diff --git a/src/State/MergedScope.php b/src/State/MergedScope.php new file mode 100644 index 0000000000..0245e22175 --- /dev/null +++ b/src/State/MergedScope.php @@ -0,0 +1,153 @@ +scopeData = $scopeData; + $this->span = $span; + } + + /** + * Applies the current context and fingerprint to the event. If the event has + * already some breadcrumbs on it, the ones from this scope won't get merged. + * + * @param Event $event The event object that will be enriched with scope data + */ + public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event + { + $event->setFingerprint(array_merge($event->getFingerprint(), $this->scopeData->getFingerprint())); + + if (empty($event->getBreadcrumbs())) { + $breadcrumbs = $this->scopeData->getBreadcrumbs(); + $maxBreadcrumbs = $options !== null ? $options->getMaxBreadcrumbs() : Options::DEFAULT_MAX_BREADCRUMBS; + + if (\count($breadcrumbs) > $maxBreadcrumbs) { + $breadcrumbs = $maxBreadcrumbs > 0 ? \array_slice($breadcrumbs, -$maxBreadcrumbs) : []; + } + + $event->setBreadcrumb($breadcrumbs); + } + + if ($this->scopeData->getLevel() !== null) { + $event->setLevel($this->scopeData->getLevel()); + } + + if (!empty($this->scopeData->getTags())) { + $event->setTags(array_merge($this->scopeData->getTags(), $event->getTags())); + } + + if (!empty($this->scopeData->getFlags())) { + $event->setContext('flags', [ + 'values' => array_map(static function (array $flag) { + return [ + 'flag' => key($flag), + 'result' => current($flag), + ]; + }, array_values($this->scopeData->getFlags())), + ]); + } + + if (!empty($this->scopeData->getExtra())) { + $event->setExtra(array_merge($this->scopeData->getExtra(), $event->getExtra())); + } + + $scopeUser = $this->scopeData->getUser(); + if ($scopeUser !== null) { + $user = $event->getUser(); + + if ($user === null) { + $user = $scopeUser; + } else { + $user = (clone $scopeUser)->merge($user); + } + + $event->setUser($user); + } + + /** + * Apply the trace context to errors if there is a Span on the Scope. + * Else fallback to the external propagation context or to the + * propagation context. + * But do not override a trace context already present. + */ + $externalPropagationContext = null; + if ($this->span === null) { + $externalPropagationContext = self::getExternalPropagationContext(); + } + + $traceContext = $this->span !== null + ? $this->span->getTraceContext() + : ($externalPropagationContext ?? $this->scopeData->getPropagationContext()->getTraceContext()); + + if (!\array_key_exists('trace', $event->getContexts())) { + $event->setContext('trace', $traceContext); + } + + if ($this->span !== null) { + // Apply the dynamic sampling context to errors if there is a Transaction on the Scope + $transaction = $this->span->getTransaction(); + if ($transaction !== null) { + $event->setSdkMetadata('dynamic_sampling_context', $transaction->getDynamicSamplingContext()); + } + } elseif ($externalPropagationContext === null) { + $propagationContext = $this->scopeData->getPropagationContext(); + $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); + if ($dynamicSamplingContext === null && $options !== null) { + $dynamicSamplingContext = DynamicSamplingContext::fromOptionsAndPropagationContext($options, $propagationContext); + } + $event->setSdkMetadata('dynamic_sampling_context', $dynamicSamplingContext); + } + + foreach (array_merge($this->scopeData->getContexts(), $event->getContexts()) as $name => $data) { + $event->setContext($name, $data); + } + + // We create a empty `EventHint` instance to allow processors to always receive a `EventHint` instance even if there wasn't one + if ($hint === null) { + $hint = new EventHint(); + } + + if ($event->getType() === EventType::event() || $event->getType() === EventType::transaction()) { + if (empty($event->getAttachments())) { + $event->setAttachments($this->scopeData->getAttachments()); + } + } + + foreach (array_merge(parent::$globalEventProcessors, $this->scopeData->getEventProcessors()) as $processor) { + $event = $processor($event, $hint); + + if ($event === null) { + return null; + } + + if (!$event instanceof Event) { + throw new \InvalidArgumentException(\sprintf('The event processor must return null or an instance of the %s class', Event::class)); + } + } + + return $event; + } +} diff --git a/src/State/MutableScope.php b/src/State/MutableScope.php new file mode 100644 index 0000000000..36c2957338 --- /dev/null +++ b/src/State/MutableScope.php @@ -0,0 +1,267 @@ +scopeData->getClient(); + } + + /** + * Sets the client bound to this scope. + * + * @return $this + */ + public function setClient(ClientInterface $client): self + { + $this->scopeData->setClient($client); + + return $this; + } + + /** + * Sets a new tag in the tags context. + * + * @param string $key The key that uniquely identifies the tag + * @param string $value The value + * + * @return $this + */ + public function setTag(string $key, string $value): self + { + $this->scopeData->setTag($key, $value); + + return $this; + } + + /** + * Merges the given tags into the current tags context. + * + * @param array $tags The tags to merge into the current context + * + * @return $this + */ + public function setTags(array $tags): self + { + $this->scopeData->setTags(array_merge($this->scopeData->getTags(), $tags)); + + return $this; + } + + /** + * Removes a given tag from the tags context. + * + * @param string $key The key that uniquely identifies the tag + * + * @return $this + */ + public function removeTag(string $key): self + { + $this->scopeData->removeTag($key); + + return $this; + } + + /** + * Sets data to the context by a given name. + * + * @param string $name The name that uniquely identifies the context + * @param array $value The value + * + * @return $this + */ + public function setContext(string $name, array $value): self + { + if (!empty($value)) { + $this->scopeData->setContext($name, $value); + } + + return $this; + } + + /** + * Removes the context from the scope. + * + * @param string $name The name that uniquely identifies the context + * + * @return $this + */ + public function removeContext(string $name): self + { + $this->scopeData->removeContext($name); + + return $this; + } + + /** + * Sets a new information in the extra context. + * + * @param string $key The key that uniquely identifies the information + * @param mixed $value The value + * + * @return $this + */ + public function setExtra(string $key, $value): self + { + $this->scopeData->setExtra($key, $value); + + return $this; + } + + /** + * Merges the given data into the current extras context. + * + * @param array $extras Data to merge into the current context + * + * @return $this + */ + public function setExtras(array $extras): self + { + $this->scopeData->setExtras(array_merge($this->scopeData->getExtra(), $extras)); + + return $this; + } + + /** + * Merges the given data in the user context. + * + * @param array|UserDataBag $user The user data + * + * @return $this + */ + public function setUser($user): self + { + $this->scopeData->setUser($user); + + return $this; + } + + /** + * Removes all data of the user context. + * + * @return $this + */ + public function removeUser(): self + { + $this->scopeData->removeUser(); + + return $this; + } + + /** + * Sets the list of strings used to dictate the deduplication of this event. + * + * @param string[] $fingerprint The fingerprint values + * + * @return $this + */ + public function setFingerprint(array $fingerprint): self + { + $this->scopeData->setFingerprint($fingerprint); + + return $this; + } + + /** + * Sets the severity to apply to all events captured in this scope. + * + * @param Severity|null $level The severity + * + * @return $this + */ + public function setLevel(?Severity $level): self + { + $this->scopeData->setLevel($level); + + return $this; + } + + /** + * Add the given breadcrumb to the scope. + * + * @param Breadcrumb $breadcrumb The breadcrumb to add + * @param int $maxBreadcrumbs The maximum number of breadcrumbs to record + * + * @return $this + */ + public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): self + { + $this->scopeData->addBreadcrumb($breadcrumb, $maxBreadcrumbs); + + return $this; + } + + /** + * Clears all the breadcrumbs. + * + * @return $this + */ + public function clearBreadcrumbs(): self + { + $this->scopeData->clearBreadcrumbs(); + + return $this; + } + + /** + * Adds a new event processor that will be called after {@see MergedScope::applyToEvent} + * finished its work. + * + * @param callable(Event, EventHint): ?Event $eventProcessor The event processor + * + * @return $this + */ + public function addEventProcessor(callable $eventProcessor): self + { + $this->scopeData->addEventProcessor($eventProcessor); + + return $this; + } + + /** + * Clears event payload data from the scope. The client binding and last + * event ID are preserved. + */ + public function clear(): void + { + $this->scopeData->clear(); + } + + public function __clone() + { + $this->scopeData = clone $this->scopeData; + } + + public function addAttachment(Attachment $attachment): self + { + $this->scopeData->addAttachment($attachment); + + return $this; + } + + public function clearAttachments(): self + { + $this->scopeData->setAttachments([]); + + return $this; + } +} diff --git a/src/State/RuntimeContext.php b/src/State/RuntimeContext.php index 6910cae608..a2b3a45d61 100644 --- a/src/State/RuntimeContext.php +++ b/src/State/RuntimeContext.php @@ -4,6 +4,7 @@ namespace Sentry\State; +use Sentry\EventId; use Sentry\Logs\LogsAggregator; use Sentry\Metrics\MetricsAggregator; @@ -23,9 +24,9 @@ final class RuntimeContext private $id; /** - * @var HubInterface + * @var IsolationScope */ - private $hub; + private $isolationScope; /** * @var LogsAggregator @@ -37,10 +38,15 @@ final class RuntimeContext */ private $metricsAggregator; - public function __construct(string $id, HubInterface $hub) + /** + * @var EventId|null The ID of the last event captured within this context + */ + private $lastEventId; + + public function __construct(string $id, ?IsolationScope $isolationScope = null) { $this->id = $id; - $this->hub = $hub; + $this->isolationScope = $isolationScope ?? new IsolationScope(); $this->logsAggregator = new LogsAggregator(); $this->metricsAggregator = new MetricsAggregator(); } @@ -50,14 +56,14 @@ public function getId(): string return $this->id; } - public function getHub(): HubInterface + public function getIsolationScope(): IsolationScope { - return $this->hub; + return $this->isolationScope; } - public function setHub(HubInterface $hub): void + public function setIsolationScope(IsolationScope $isolationScope): void { - $this->hub = $hub; + $this->isolationScope = $isolationScope; } public function getLogsAggregator(): LogsAggregator @@ -69,4 +75,14 @@ public function getMetricsAggregator(): MetricsAggregator { return $this->metricsAggregator; } + + public function getLastEventId(): ?EventId + { + return $this->lastEventId; + } + + public function setLastEventId(?EventId $lastEventId): void + { + $this->lastEventId = $lastEventId; + } } diff --git a/src/State/RuntimeContextManager.php b/src/State/RuntimeContextManager.php index cec72ee48e..4fd2c7e1a5 100644 --- a/src/State/RuntimeContextManager.php +++ b/src/State/RuntimeContextManager.php @@ -5,7 +5,8 @@ namespace Sentry\State; use Psr\Log\LoggerInterface; -use Sentry\Tracing\PropagationContext; +use Sentry\ClientInterface; +use Sentry\SentrySdk; /** * Manages runtime-local SDK state across different execution models. @@ -22,11 +23,6 @@ final class RuntimeContextManager { private const PROCESS_EXECUTION_CONTEXT_KEY = 'process'; - /** - * @var HubInterface - */ - private $baseHub; - /** * @var RuntimeContext|null */ @@ -42,46 +38,6 @@ final class RuntimeContextManager */ private $executionContextToRuntimeContext = []; - public function __construct(HubInterface $baseHub) - { - $this->baseHub = $baseHub; - $this->globalContext = null; - } - - /** - * Sets the current hub with context-aware behavior. - * - * If a runtime context is active for the current execution key, the hub is - * updated only for that active context. Otherwise, the baseline/global hub - * template is updated. - * - * @return bool Whether the hub was set on an active runtime context - */ - public function setCurrentHub(HubInterface $hub): bool - { - $executionContextKey = $this->getExecutionContextKey(); - - if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) { - $runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey]; - $this->activeContexts[$runtimeContextId]->setHub($hub); - - return true; - } - - $this->baseHub = $hub; - - if ($this->globalContext !== null) { - $this->globalContext->setHub($hub); - } - - return false; - } - - public function getCurrentHub(): HubInterface - { - return $this->getCurrentContext()->getHub(); - } - public function getCurrentContext(): RuntimeContext { $executionContextKey = $this->getExecutionContextKey(); @@ -137,7 +93,7 @@ public function endContext(?int $timeout = null): void private function createContextForExecutionContextKey(string $executionContextKey): void { $runtimeContextId = $this->generateRuntimeContextId(); - $runtimeContext = new RuntimeContext($runtimeContextId, $this->createHubFromBaseHub()); + $runtimeContext = new RuntimeContext($runtimeContextId); $this->activeContexts[$runtimeContextId] = $runtimeContext; $this->executionContextToRuntimeContext[$executionContextKey] = $runtimeContextId; @@ -154,19 +110,18 @@ private function removeContextById(string $runtimeContextId, ?int $timeout = nul // Remove any key mappings that may still reference this context. $this->removeExecutionContextMappingsForRuntimeContext($runtimeContextId); - $logger = $this->getLoggerFromHub($runtimeContext->getHub()); + $client = SentrySdk::getClient($runtimeContext->getIsolationScope()); + $logger = $client->getOptions()->getLoggerOrNullLogger(); - $this->flushRuntimeContextResources($runtimeContext, $timeout, $logger); + $this->flushRuntimeContextResources($runtimeContext, $client, $timeout, $logger); } - private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?int $timeout, LoggerInterface $logger): void + private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ClientInterface $client, ?int $timeout, LoggerInterface $logger): void { - $hub = $runtimeContext->getHub(); - // captureEvent can throw before transport send (for example from scope event processors // or before_send callbacks), so we isolate failures and continue flushing other resources. try { - $runtimeContext->getLogsAggregator()->flush($hub); + $runtimeContext->getLogsAggregator()->flush($client, $runtimeContext->getIsolationScope()); } catch (\Throwable $exception) { $logger->error('Failed to flush logs while ending a runtime context.', [ 'exception' => $exception, @@ -176,7 +131,7 @@ private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?i // Keep metrics flush independent from logs flush so one bad callback does not block the rest. try { - $runtimeContext->getMetricsAggregator()->flush($hub); + $runtimeContext->getMetricsAggregator()->flush($client, $runtimeContext->getIsolationScope()); } catch (\Throwable $exception) { $logger->error('Failed to flush trace metrics while ending a runtime context.', [ 'exception' => $exception, @@ -184,8 +139,6 @@ private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?i ]); } - $client = $hub->getClient(); - // Custom transports may throw from close(); endContext must stay best-effort and non-fatal. try { $client->flush($timeout); @@ -224,31 +177,6 @@ private function hasActiveContextForExecutionContextKey(string $executionContext return true; } - private function createHubFromBaseHub(): HubInterface - { - if (!$this->baseHub instanceof Hub) { - return new Hub($this->baseHub->getClient()); - } - - $clonedScope = null; - - $this->baseHub->configureScope(static function (Scope $scope) use (&$clonedScope): void { - $clonedScope = clone $scope; - // Do not inherit active traces into a new runtime context. - $clonedScope->setSpan(null); - $clonedScope->setPropagationContext(PropagationContext::fromDefaults()); - }); - - return new Hub($this->baseHub->getClient(), $clonedScope ?? new Scope()); - } - - private function getLoggerFromHub(HubInterface $hub): LoggerInterface - { - $client = $hub->getClient(); - - return $client->getOptions()->getLoggerOrNullLogger(); - } - private function generateRuntimeContextId(): string { return \sprintf('%s-%d', str_replace('.', '', uniqid('', true)), mt_rand()); @@ -264,7 +192,7 @@ private function getGlobalContext(): RuntimeContext { if ($this->globalContext === null) { // Lazy fallback keeps baseline behavior when users do not opt into explicit context lifecycle. - $this->globalContext = new RuntimeContext('global', $this->baseHub); + $this->globalContext = new RuntimeContext('global'); } return $this->globalContext; diff --git a/src/State/Scope.php b/src/State/Scope.php index 58d95f00ff..90ce93938f 100644 --- a/src/State/Scope.php +++ b/src/State/Scope.php @@ -4,25 +4,17 @@ namespace Sentry\State; -use Sentry\Attachment\Attachment; -use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; -use Sentry\EventType; -use Sentry\Options; -use Sentry\Severity; -use Sentry\Tracing\DynamicSamplingContext; -use Sentry\Tracing\PropagationContext; -use Sentry\Tracing\Span; -use Sentry\Tracing\Transaction; +use Sentry\NoOpClient; use Sentry\UserDataBag; -use Sentry\Util\DebugType; /** * The scope holds data that should implicitly be sent with Sentry events. It * can hold context data, extra parameters, level overrides, fingerprints etc. */ -class Scope +abstract class Scope { /** * Maximum number of flags allowed. We only track the first flags set. @@ -32,216 +24,36 @@ class Scope public const MAX_FLAGS = 100; /** - * @var PropagationContext - */ - private $propagationContext; - - /** - * @var Breadcrumb[] The list of breadcrumbs recorded in this scope - */ - private $breadcrumbs = []; - - /** - * @var UserDataBag|null The user data associated to this scope - */ - private $user; - - /** - * @var array> The list of contexts associated to this scope - */ - private $contexts = []; - - /** - * @var array The list of tags associated to this scope - */ - private $tags = []; - - /** - * @var array> The list of flags associated to this scope - */ - private $flags = []; - - /** - * @var array A set of extra data associated to this scope - */ - private $extra = []; - - /** - * @var string[] List of fingerprints used to group events together in - * Sentry - */ - private $fingerprint = []; - - /** - * @var Severity|null The severity to associate to the events captured in - * this scope - */ - private $level; - - /** - * @var callable[] List of event processors + * @internal * - * @phpstan-var array - */ - private $eventProcessors = []; - - /** - * @var Span|null Set a Span on the Scope + * @var ScopeData */ - private $span; - - /** - * @var Attachment[] - */ - private $attachments = []; + protected $scopeData; /** * @var callable[] List of event processors * * @phpstan-var array */ - private static $globalEventProcessors = []; + protected static $globalEventProcessors = []; /** * @var callable|null */ - private static $externalPropagationContextCallback; - - public function __construct(?PropagationContext $propagationContext = null) - { - $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); - } - - /** - * Sets a new tag in the tags context. - * - * @param string $key The key that uniquely identifies the tag - * @param string $value The value - * - * @return $this - */ - public function setTag(string $key, string $value): self - { - $this->tags[$key] = $value; + protected static $externalPropagationContextCallback; - return $this; - } - - /** - * Merges the given tags into the current tags context. - * - * @param array $tags The tags to merge into the current context - * - * @return $this - */ - public function setTags(array $tags): self + public function __construct() { - $this->tags = array_merge($this->tags, $tags); - - return $this; + $this->scopeData = new ScopeData(); + $this->scopeData->setClient(new NoOpClient()); } /** - * Removes a given tag from the tags context. - * - * @param string $key The key that uniquely identifies the tag - * - * @return $this + * Returns the client bound to this scope. */ - public function removeTag(string $key): self + public function getClient(): ClientInterface { - unset($this->tags[$key]); - - return $this; - } - - /** - * Adds a feature flag to the scope. - * - * @return $this - */ - public function addFeatureFlag(string $key, bool $result): self - { - // If the flag was already set, remove it first - // This basically mimics an LRU cache so that the most recently added flags are kept - foreach ($this->flags as $flagIndex => $flag) { - if (isset($flag[$key])) { - unset($this->flags[$flagIndex]); - } - } - - // Keep only the most recent MAX_FLAGS flags - if (\count($this->flags) >= self::MAX_FLAGS) { - array_shift($this->flags); - } - - $this->flags[] = [$key => $result]; - - if ($this->span !== null) { - $this->span->setFlag($key, $result); - } - - return $this; - } - - /** - * Sets data to the context by a given name. - * - * @param string $name The name that uniquely identifies the context - * @param array $value The value - * - * @return $this - */ - public function setContext(string $name, array $value): self - { - if (!empty($value)) { - $this->contexts[$name] = $value; - } - - return $this; - } - - /** - * Removes the context from the scope. - * - * @param string $name The name that uniquely identifies the context - * - * @return $this - */ - public function removeContext(string $name): self - { - unset($this->contexts[$name]); - - return $this; - } - - /** - * Sets a new information in the extra context. - * - * @param string $key The key that uniquely identifies the information - * @param mixed $value The value - * - * @return $this - */ - public function setExtra(string $key, $value): self - { - $this->extra[$key] = $value; - - return $this; - } - - /** - * Merges the given data into the current extras context. - * - * @param array $extras Data to merge into the current context - * - * @return $this - */ - public function setExtras(array $extras): self - { - $this->extra = array_merge($this->extra, $extras); - - return $this; + return $this->scopeData->getClient(); } /** @@ -249,120 +61,11 @@ public function setExtras(array $extras): self */ public function getUser(): ?UserDataBag { - return $this->user; - } - - /** - * Merges the given data in the user context. - * - * @param array|UserDataBag $user The user data - * - * @return $this - */ - public function setUser($user): self - { - if (!\is_array($user) && !$user instanceof UserDataBag) { - throw new \TypeError(\sprintf('The $user argument must be either an array or an instance of the "%s" class. Got: "%s".', UserDataBag::class, DebugType::getDebugType($user))); - } - - if (\is_array($user)) { - $user = UserDataBag::createFromArray($user); - } - - if ($this->user === null) { - $this->user = $user; - } else { - $this->user = $this->user->merge($user); - } - - return $this; - } - - /** - * Removes all data of the user context. - * - * @return $this - */ - public function removeUser(): self - { - $this->user = null; - - return $this; - } - - /** - * Sets the list of strings used to dictate the deduplication of this event. - * - * @param string[] $fingerprint The fingerprint values - * - * @return $this - */ - public function setFingerprint(array $fingerprint): self - { - $this->fingerprint = $fingerprint; - - return $this; + return $this->scopeData->getUser(); } /** - * Sets the severity to apply to all events captured in this scope. - * - * @param Severity|null $level The severity - * - * @return $this - */ - public function setLevel(?Severity $level): self - { - $this->level = $level; - - return $this; - } - - /** - * Add the given breadcrumb to the scope. - * - * @param Breadcrumb $breadcrumb The breadcrumb to add - * @param int $maxBreadcrumbs The maximum number of breadcrumbs to record - * - * @return $this - */ - public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): self - { - $this->breadcrumbs[] = $breadcrumb; - $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); - - return $this; - } - - /** - * Clears all the breadcrumbs. - * - * @return $this - */ - public function clearBreadcrumbs(): self - { - $this->breadcrumbs = []; - - return $this; - } - - /** - * Adds a new event processor that will be called after {@see Scope::applyToEvent} - * finished its work. - * - * @param callable $eventProcessor The event processor - * - * @return $this - */ - public function addEventProcessor(callable $eventProcessor): self - { - $this->eventProcessors[] = $eventProcessor; - - return $this; - } - - /** - * Adds a new event processor that will be called after {@see Scope::applyToEvent} + * Adds a new event processor that will be called after {@see MergedScope::applyToEvent} * finished its work. * * @param callable $eventProcessor The event processor @@ -418,234 +121,4 @@ public static function getExternalPropagationContext(): ?array 'span_id' => $spanId, ]; } - - /** - * Clears the scope and resets any data it contains. - * - * @return $this - */ - public function clear(): self - { - $this->user = null; - $this->level = null; - $this->span = null; - $this->fingerprint = []; - $this->breadcrumbs = []; - $this->tags = []; - $this->flags = []; - $this->extra = []; - $this->contexts = []; - $this->attachments = []; - - return $this; - } - - /** - * Applies the current context and fingerprint to the event. If the event has - * already some breadcrumbs on it, the ones from this scope won't get merged. - * - * @param Event $event The event object that will be enriched with scope data - */ - public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event - { - $event->setFingerprint(array_merge($event->getFingerprint(), $this->fingerprint)); - - if (empty($event->getBreadcrumbs())) { - $event->setBreadcrumb($this->breadcrumbs); - } - - if ($this->level !== null) { - $event->setLevel($this->level); - } - - if (!empty($this->tags)) { - $event->setTags(array_merge($this->tags, $event->getTags())); - } - - if (!empty($this->flags)) { - $event->setContext('flags', [ - 'values' => array_map(static function (array $flag) { - return [ - 'flag' => key($flag), - 'result' => current($flag), - ]; - }, array_values($this->flags)), - ]); - } - - if (!empty($this->extra)) { - $event->setExtra(array_merge($this->extra, $event->getExtra())); - } - - if ($this->user !== null) { - $user = $event->getUser(); - - if ($user === null) { - $user = $this->user; - } else { - $user = $this->user->merge($user); - } - - $event->setUser($user); - } - - /** - * Apply the trace context to errors if there is a Span on the Scope. - * Else fallback to the external propagation context or to the - * propagation context. - * But do not override a trace context already present. - */ - $externalPropagationContext = null; - if ($this->span === null) { - $externalPropagationContext = self::getExternalPropagationContext(); - } - - $traceContext = $this->span !== null - ? $this->span->getTraceContext() - : ($externalPropagationContext ?? $this->propagationContext->getTraceContext()); - - if (!\array_key_exists('trace', $event->getContexts())) { - $event->setContext('trace', $traceContext); - } - - if ($this->span !== null) { - // Apply the dynamic sampling context to errors if there is a Transaction on the Scope - $transaction = $this->span->getTransaction(); - if ($transaction !== null) { - $event->setSdkMetadata('dynamic_sampling_context', $transaction->getDynamicSamplingContext()); - } - } elseif ($externalPropagationContext === null) { - $dynamicSamplingContext = $this->propagationContext->getDynamicSamplingContext(); - if ($dynamicSamplingContext === null && $options !== null) { - $dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $this); - } - $event->setSdkMetadata('dynamic_sampling_context', $dynamicSamplingContext); - } - - foreach (array_merge($this->contexts, $event->getContexts()) as $name => $data) { - $event->setContext($name, $data); - } - - // We create a empty `EventHint` instance to allow processors to always receive a `EventHint` instance even if there wasn't one - if ($hint === null) { - $hint = new EventHint(); - } - - if ($event->getType() === EventType::event() || $event->getType() === EventType::transaction()) { - if (empty($event->getAttachments())) { - $event->setAttachments($this->attachments); - } - } - - foreach (array_merge(self::$globalEventProcessors, $this->eventProcessors) as $processor) { - $event = $processor($event, $hint); - - if ($event === null) { - return null; - } - - if (!$event instanceof Event) { - throw new \InvalidArgumentException(\sprintf('The event processor must return null or an instance of the %s class', Event::class)); - } - } - - return $event; - } - - /** - * Returns the span that is on the scope. - */ - public function getSpan(): ?Span - { - return $this->span; - } - - /** - * Sets the span on the scope. - * - * @param Span|null $span The span - * - * @return $this - */ - public function setSpan(?Span $span): self - { - $this->span = $span; - - return $this; - } - - /** - * Returns the transaction attached to the scope (if there is one). - */ - public function getTransaction(): ?Transaction - { - if ($this->span !== null) { - return $this->span->getTransaction(); - } - - return null; - } - - public function hasExternalPropagationContext(): bool - { - return $this->span === null && self::getExternalPropagationContext() !== null; - } - - /** - * @return array{ - * trace_id: string, - * span_id: string, - * parent_span_id?: string, - * data?: array, - * description?: string, - * op?: string, - * status?: string, - * tags?: array, - * origin?: string - * } - */ - public function getTraceContext(): array - { - if ($this->span !== null) { - return $this->span->getTraceContext(); - } - - return self::getExternalPropagationContext() ?? $this->propagationContext->getTraceContext(); - } - - public function getPropagationContext(): PropagationContext - { - return $this->propagationContext; - } - - public function setPropagationContext(PropagationContext $propagationContext): self - { - $this->propagationContext = $propagationContext; - - return $this; - } - - public function __clone() - { - if ($this->user !== null) { - $this->user = clone $this->user; - } - if ($this->propagationContext !== null) { - $this->propagationContext = clone $this->propagationContext; - } - } - - public function addAttachment(Attachment $attachment): self - { - $this->attachments[] = $attachment; - - return $this; - } - - public function clearAttachments(): self - { - $this->attachments = []; - - return $this; - } } diff --git a/src/State/ScopeData.php b/src/State/ScopeData.php new file mode 100644 index 0000000000..202e5d77ea --- /dev/null +++ b/src/State/ScopeData.php @@ -0,0 +1,452 @@ +> The list of contexts associated to this scope + */ + private $contexts = []; + + /** + * @var array The list of tags associated to this scope + */ + private $tags = []; + + /** + * @var array> The list of flags associated to this scope + */ + private $flags = []; + + /** + * @var array A set of extra data associated to this scope + */ + private $extra = []; + + /** + * @var string[] List of fingerprints used to group events together in + * Sentry + */ + private $fingerprint = []; + + /** + * @var Severity|null The severity to associate to the events captured in + * this scope + */ + private $level; + + /** + * @var callable[] List of event processors + * + * @phpstan-var array + */ + private $eventProcessors = []; + + /** + * @var Attachment[] + */ + private $attachments = []; + + public function __construct(?PropagationContext $propagationContext = null) + { + $this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults(); + } + + public function getPropagationContext(): PropagationContext + { + return $this->propagationContext; + } + + public function setPropagationContext(PropagationContext $propagationContext): void + { + $this->propagationContext = $propagationContext; + } + + public function getClient(): ClientInterface + { + return $this->client; + } + + public function setClient(ClientInterface $client): void + { + $this->client = $client; + } + + /** + * @return Breadcrumb[] + */ + public function getBreadcrumbs(): array + { + return $this->breadcrumbs; + } + + public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100): void + { + $this->breadcrumbs[] = $breadcrumb; + if (\count($this->breadcrumbs) > $maxBreadcrumbs) { + $this->breadcrumbs = \array_slice($this->breadcrumbs, -$maxBreadcrumbs); + } + } + + public function clearBreadcrumbs(): void + { + $this->breadcrumbs = []; + } + + public function getUser(): ?UserDataBag + { + return $this->user; + } + + /** + * @param array|UserDataBag $user + */ + public function setUser($user): void + { + if (!\is_array($user) && !$user instanceof UserDataBag) { + throw new \TypeError(\sprintf('The $user argument must be either an array or an instance of the "%s" class. Got: "%s".', UserDataBag::class, DebugType::getDebugType($user))); + } + + if (\is_array($user)) { + $user = UserDataBag::createFromArray($user); + } + + if ($this->user === null) { + $this->user = $user; + } else { + $this->user = $this->user->merge($user); + } + } + + public function removeUser(): void + { + $this->user = null; + } + + /** + * @return array> + */ + public function getContexts(): array + { + return $this->contexts; + } + + /** + * @param array $value + */ + public function setContext(string $name, array $value): void + { + $this->contexts[$name] = $value; + } + + public function removeContext(string $key): void + { + unset($this->contexts[$key]); + } + + /** + * @return array + */ + public function getTags(): array + { + return $this->tags; + } + + /** + * @param array $tags + */ + public function setTags(array $tags): void + { + $this->tags = $tags; + } + + public function setTag(string $key, string $value): void + { + $this->tags[$key] = $value; + } + + public function removeTag(string $key): void + { + unset($this->tags[$key]); + } + + /** + * @return array> + */ + public function getFlags(): array + { + return $this->flags; + } + + public function addFeatureFlag(string $key, bool $result): self + { + // If the flag was already set, remove it first + // This basically mimics an LRU cache so that the most recently added flags are kept + foreach ($this->flags as $flagIndex => $flag) { + if (isset($flag[$key])) { + unset($this->flags[$flagIndex]); + } + } + + // Keep only the most recent MAX_FLAGS flags + if (\count($this->flags) >= Scope::MAX_FLAGS) { + array_shift($this->flags); + } + + $this->flags[] = [$key => $result]; + + return $this; + } + + /** + * @return array + */ + public function getExtra(): array + { + return $this->extra; + } + + /** + * @param array $extra + */ + public function setExtras(array $extra): void + { + $this->extra = $extra; + } + + /** + * @param mixed $value + */ + public function setExtra(string $key, $value): void + { + $this->extra[$key] = $value; + } + + /** + * @return string[] + */ + public function getFingerprint(): array + { + return $this->fingerprint; + } + + /** + * @param string[] $fingerprint + */ + public function setFingerprint(array $fingerprint): void + { + $this->fingerprint = $fingerprint; + } + + public function getLevel(): ?Severity + { + return $this->level; + } + + public function setLevel(?Severity $level): void + { + $this->level = $level; + } + + /** + * @return array + */ + public function getEventProcessors(): array + { + return $this->eventProcessors; + } + + /** + * @param callable(Event, EventHint): ?Event $eventProcessor + */ + public function addEventProcessor(callable $eventProcessor): void + { + $this->eventProcessors[] = $eventProcessor; + } + + /** + * @return Attachment[] + */ + public function getAttachments(): array + { + return $this->attachments; + } + + /** + * @param Attachment[] $attachments + */ + public function setAttachments(array $attachments): void + { + $this->attachments = $attachments; + } + + public function addAttachment(Attachment $attachment): void + { + $this->attachments[] = $attachment; + } + + public function clear(): void + { + $this->user = null; + $this->level = null; + $this->fingerprint = []; + $this->breadcrumbs = []; + $this->tags = []; + $this->flags = []; + $this->extra = []; + $this->contexts = []; + $this->attachments = []; + } + + public function __clone() + { + if ($this->user !== null) { + $this->user = clone $this->user; + } + $this->propagationContext = clone $this->propagationContext; + } + + /** + * Merges data of one ScopeData object into the other one. Data stored in $other will have precedence over + * $this. It is generally assumed $this refers to the global scope while $other is the isolation scope. + */ + public function merge(self $other): self + { + $merged = clone $other; + $merged->tags = array_merge($this->tags, $other->tags); + $merged->extra = array_merge($this->extra, $other->extra); + $merged->contexts = array_merge($this->contexts, $other->contexts); + + if ($this->user !== null && $other->user !== null) { + $merged->user = (clone $this->user)->merge($other->user); + } elseif ($this->user !== null) { + $merged->user = clone $this->user; + } + + $merged->level = $other->level ?? $this->level; + $merged->fingerprint = array_merge($this->fingerprint, $other->fingerprint); + $merged->breadcrumbs = self::mergeBreadcrumbs($this->breadcrumbs, $other->breadcrumbs); + $merged->flags = self::mergeFlags($this->flags, $other->flags); + $merged->attachments = array_merge($this->attachments, $other->attachments); + $merged->eventProcessors = array_merge($this->eventProcessors, $other->eventProcessors); + + return $merged; + } + + /** + * @param Breadcrumb[] $globalBreadcrumbs + * @param Breadcrumb[] $isolationBreadcrumbs + * + * @return Breadcrumb[] + */ + private static function mergeBreadcrumbs(array $globalBreadcrumbs, array $isolationBreadcrumbs): array + { + if (empty($globalBreadcrumbs)) { + return $isolationBreadcrumbs; + } + + if (empty($isolationBreadcrumbs)) { + return $globalBreadcrumbs; + } + + // if the last global is before the first isolation, it means that no global breadcrumb was created after + // the first isolation was written, so we can just merge them normally + if ($globalBreadcrumbs[\count($globalBreadcrumbs) - 1]->getTimestamp() <= $isolationBreadcrumbs[0]->getTimestamp()) { + return array_merge($globalBreadcrumbs, $isolationBreadcrumbs); + } + + $globalCount = \count($globalBreadcrumbs); + $isolationCount = \count($isolationBreadcrumbs); + $globalIndex = 0; + $isolationIndex = 0; + $merged = []; + + // we iterate over both lists as long as both have elements. Once we reached the end of one list, + // we can stop iterating and merge the remaining list fully + while ($globalIndex < $globalCount && $isolationIndex < $isolationCount) { + if ($globalBreadcrumbs[$globalIndex]->getTimestamp() <= $isolationBreadcrumbs[$isolationIndex]->getTimestamp()) { + $merged[] = $globalBreadcrumbs[$globalIndex++]; + } else { + $merged[] = $isolationBreadcrumbs[$isolationIndex++]; + } + } + + return array_merge( + $merged, + \array_slice($globalBreadcrumbs, $globalIndex), + \array_slice($isolationBreadcrumbs, $isolationIndex) + ); + } + + /** + * @param array> $globalFlags + * @param array> $isolationFlags + * + * @return array> + */ + private static function mergeFlags(array $globalFlags, array $isolationFlags): array + { + $flagsByKey = []; + + foreach (array_merge($globalFlags, $isolationFlags) as $flag) { + $flagKey = key($flag); + + if ($flagKey === null) { + continue; + } + + unset($flagsByKey[$flagKey]); + $flagsByKey[$flagKey] = current($flag); + } + + $flagsByKey = \array_slice($flagsByKey, -Scope::MAX_FLAGS, Scope::MAX_FLAGS, true); + + $flags = []; + + foreach ($flagsByKey as $flagKey => $flagResult) { + $flags[] = [$flagKey => $flagResult]; + } + + return $flags; + } +} diff --git a/src/Tracing/DynamicSamplingContext.php b/src/Tracing/DynamicSamplingContext.php index 410b4fc533..714aab6ff3 100644 --- a/src/Tracing/DynamicSamplingContext.php +++ b/src/Tracing/DynamicSamplingContext.php @@ -4,9 +4,9 @@ namespace Sentry\Tracing; +use Sentry\ClientInterface; use Sentry\Options; -use Sentry\State\HubInterface; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; /** * This class represents the Dynamic Sampling Context (dsc). @@ -149,7 +149,7 @@ public static function fromHeader(string $header): self * * @see https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#baggage-header */ - public static function fromTransaction(Transaction $transaction, HubInterface $hub): self + public static function fromTransaction(Transaction $transaction, ClientInterface $client): self { $samplingContext = new self(); $samplingContext->set('trace_id', (string) $transaction->getTraceId()); @@ -164,8 +164,6 @@ public static function fromTransaction(Transaction $transaction, HubInterface $h $samplingContext->set('transaction', $transaction->getName()); } - $client = $hub->getClient(); - self::setOrgOptions($client->getOptions(), $samplingContext); if ($transaction->getSampled() !== null) { @@ -181,11 +179,19 @@ public static function fromTransaction(Transaction $transaction, HubInterface $h return $samplingContext; } - public static function fromOptions(Options $options, Scope $scope): self + public static function fromOptions(Options $options, IsolationScope $scope): self + { + return self::fromOptionsAndPropagationContext($options, $scope->getPropagationContext()); + } + + /** + * @internal + */ + public static function fromOptionsAndPropagationContext(Options $options, PropagationContext $propagationContext): self { $samplingContext = new self(); - $samplingContext->set('trace_id', (string) $scope->getPropagationContext()->getTraceId()); - $samplingContext->set('sample_rand', (string) $scope->getPropagationContext()->getSampleRand()); + $samplingContext->set('trace_id', (string) $propagationContext->getTraceId()); + $samplingContext->set('sample_rand', (string) $propagationContext->getSampleRand()); if ($options->getTracesSampleRate() !== null) { $samplingContext->set('sample_rate', (string) $options->getTracesSampleRate()); diff --git a/src/Tracing/GuzzleTracingMiddleware.php b/src/Tracing/GuzzleTracingMiddleware.php index 8c14fe0b07..50d73466d9 100644 --- a/src/Tracing/GuzzleTracingMiddleware.php +++ b/src/Tracing/GuzzleTracingMiddleware.php @@ -11,7 +11,8 @@ use Sentry\Breadcrumb; use Sentry\ClientInterface; use Sentry\SentrySdk; -use Sentry\State\HubInterface; +use Sentry\State\BreadcrumbRecorder; +use Sentry\State\IsolationScope; use function Sentry\getBaggage; use function Sentry\getTraceparent; @@ -21,13 +22,13 @@ */ final class GuzzleTracingMiddleware { - public static function trace(?HubInterface $hub = null): \Closure + public static function trace(?IsolationScope $scope = null): \Closure { - return static function (callable $handler) use ($hub): \Closure { - return static function (RequestInterface $request, array $options) use ($hub, $handler) { - $hub = $hub ?? SentrySdk::getCurrentHub(); - $client = $hub->getClient(); - $parentSpan = $hub->getSpan(); + return static function (callable $handler) use ($scope): \Closure { + return static function (RequestInterface $request, array $options) use ($handler, $scope) { + $scope = $scope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($scope); + $parentSpan = $scope->getSpan(); $partialUri = Uri::fromParts([ 'scheme' => $request->getUri()->getScheme(), @@ -59,28 +60,27 @@ public static function trace(?HubInterface $hub = null): \Closure $childSpan = $parentSpan->startChild($spanContext); - $hub->setSpan($childSpan); + $scope->setSpan($childSpan); } if (self::shouldAttachTracingHeaders($client, $request)) { - $traceParent = getTraceparent(); + $traceParent = getTraceparent($scope); if ($traceParent !== '') { $request = $request->withHeader('sentry-trace', $traceParent); } - $baggage = getBaggage(); + $baggage = getBaggage($scope); if ($baggage !== '') { $request = $request->withHeader('baggage', $baggage); } } - $handlerPromiseCallback = static function ($responseOrException) use ($hub, $spanAndBreadcrumbData, $childSpan, $parentSpan, $partialUri) { + $handlerPromiseCallback = static function ($responseOrException) use ($client, $scope, $spanAndBreadcrumbData, $childSpan, $parentSpan, $partialUri) { if ($childSpan !== null) { - // We finish the span (which means setting the span end timestamp) first to ensure the measured time - // the span spans is as close to only the HTTP request time and do the data collection afterwards + // We finish the span first to keep the measured duration as close as possible to the HTTP request time. $childSpan->finish(); - $hub->setSpan($parentSpan); + $scope->setSpan($parentSpan); } $response = null; @@ -113,7 +113,7 @@ public static function trace(?HubInterface $hub = null): \Closure } } - $hub->addBreadcrumb(new Breadcrumb( + BreadcrumbRecorder::record($client, $scope, new Breadcrumb( $breadcrumbLevel, Breadcrumb::TYPE_HTTP, 'http', diff --git a/src/Tracing/PropagationContext.php b/src/Tracing/PropagationContext.php index e2d525d5d6..497c513c0c 100644 --- a/src/Tracing/PropagationContext.php +++ b/src/Tracing/PropagationContext.php @@ -5,7 +5,6 @@ namespace Sentry\Tracing; use Sentry\SentrySdk; -use Sentry\State\Scope; use Sentry\Tracing\Traits\TraceHeaderParserTrait; final class PropagationContext @@ -84,12 +83,10 @@ public function toTraceparent(): string public function toBaggage(): string { if ($this->dynamicSamplingContext === null) { - $hub = SentrySdk::getCurrentHub(); - $options = $hub->getClient()->getOptions(); - - $hub->configureScope(function (Scope $scope) use ($options) { - $this->dynamicSamplingContext = DynamicSamplingContext::fromOptions($options, $scope); - }); + $this->dynamicSamplingContext = DynamicSamplingContext::fromOptionsAndPropagationContext( + SentrySdk::getClient()->getOptions(), + $this + ); } return (string) $this->dynamicSamplingContext; diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index e8df25b88d..faebe8a476 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -6,7 +6,6 @@ use Sentry\EventId; use Sentry\SentrySdk; -use Sentry\State\Scope; /** * This class stores all the information about a span. @@ -299,11 +298,9 @@ public function setStatus(?SpanStatus $status) */ public function setHttpStatus(int $statusCode) { - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($statusCode) { - $scope->setContext('response', [ - 'status_code' => $statusCode, - ]); - }); + SentrySdk::getIsolationScope()->setContext('response', [ + 'status_code' => $statusCode, + ]); $status = SpanStatus::createFromHttpStatusCode($statusCode); diff --git a/src/Tracing/Traits/TraceHeaderParserTrait.php b/src/Tracing/Traits/TraceHeaderParserTrait.php index 3accc180b1..69933f22bb 100644 --- a/src/Tracing/Traits/TraceHeaderParserTrait.php +++ b/src/Tracing/Traits/TraceHeaderParserTrait.php @@ -127,8 +127,7 @@ private static function parseSampleRand(DynamicSamplingContext $samplingContext) } } - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $client->getOptions()->getLoggerOrNullLogger()->debug( 'Ignoring invalid sentry-sample_rand baggage value because it must be a numeric value in the range [0, 1).', ['sample_rand' => $sampleRand] @@ -139,8 +138,7 @@ private static function parseSampleRand(DynamicSamplingContext $samplingContext) private static function shouldContinueTrace(DynamicSamplingContext $samplingContext): bool { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); $options = $client->getOptions(); $clientOrgId = $options->getOrgId(); diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index e2baa09b90..2b3cba6b11 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -6,9 +6,11 @@ use Sentry\Event; use Sentry\EventId; +use Sentry\Options; use Sentry\Profiling\Profiler; use Sentry\SentrySdk; -use Sentry\State\HubInterface; +use Sentry\State\EventRecorder; +use Sentry\State\IsolationScope; /** * This class stores all the information about a Transaction. @@ -16,14 +18,14 @@ final class Transaction extends Span { /** - * @var HubInterface The hub instance + * @var string Name of the transaction */ - private $hub; + private $name; /** - * @var string Name of the transaction + * @var IsolationScope */ - private $name; + private $scope; /** * @var Transaction The transaction @@ -44,16 +46,15 @@ final class Transaction extends Span * Span constructor. * * @param TransactionContext $context The context to create the transaction with - * @param HubInterface|null $hub Instance of a hub to flush the transaction * * @internal */ - public function __construct(TransactionContext $context, ?HubInterface $hub = null) + public function __construct(TransactionContext $context, ?IsolationScope $scope = null) { parent::__construct($context); - $this->hub = $hub ?? SentrySdk::getCurrentHub(); $this->name = $context->getName(); + $this->scope = $scope ?? SentrySdk::getIsolationScope(); $this->metadata = $context->getMetadata(); $this->transaction = $this; } @@ -97,7 +98,7 @@ public function getDynamicSamplingContext(): DynamicSamplingContext return $this->metadata->getDynamicSamplingContext(); } - $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, $this->hub); + $samplingContext = DynamicSamplingContext::fromTransaction($this->transaction, SentrySdk::getClient($this->scope)); $this->getMetadata()->setDynamicSamplingContext($samplingContext); return $samplingContext; @@ -119,10 +120,10 @@ public function initSpanRecorder(int $maxSpans = 1000): self return $this; } - public function initProfiler(): Profiler + public function initProfiler(?Options $options = null): Profiler { if ($this->profiler === null) { - $this->profiler = new Profiler($this->hub->getClient()->getOptions()); + $this->profiler = new Profiler($options ?? SentrySdk::getClient($this->scope)->getOptions()); } return $this->profiler; @@ -187,6 +188,6 @@ public function finish(?float $endTimestamp = null): ?EventId } } - return $this->hub->captureEvent($event); + return EventRecorder::captureEvent($event, null, $this->scope); } } diff --git a/src/Tracing/TransactionSampler.php b/src/Tracing/TransactionSampler.php new file mode 100644 index 0000000000..ef6a284eca --- /dev/null +++ b/src/Tracing/TransactionSampler.php @@ -0,0 +1,172 @@ + $customSamplingContext Additional context that will be passed to the {@see SamplingContext} + */ + public static function startTransaction(Options $options, TransactionContext $context, array $customSamplingContext = []): Transaction + { + $transaction = new Transaction($context); + $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() ?? 0.0; + + if ($transaction->getSampled() === null) { + $tracesSampler = $options->getTracesSampler(); + + if ($tracesSampler !== null) { + $sampleRate = $tracesSampler($samplingContext); + $sampleSource = 'config:traces_sampler'; + } else { + $parentSampleRate = $context->getMetadata()->getParentSamplingRate(); + if ($parentSampleRate !== null) { + $sampleRate = $parentSampleRate; + $sampleSource = 'parent:sample_rate'; + } else { + $sampleRate = self::getSampleRate( + $samplingContext->getParentSampled(), + $options->getTracesSampleRate() ?? 0 + ); + $sampleSource = $samplingContext->getParentSampled() !== null ? 'parent:sampling_decision' : 'config:traces_sample_rate'; + } + } + + if (!self::isValidSampleRate($sampleRate)) { + $transaction->setSampled(false); + + $logger->warning(\sprintf('Transaction [%s] was started but not sampled because sample rate (decided by %s) is invalid.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); + + return $transaction; + } + + $transaction->getMetadata()->setSamplingRate($sampleRate); + + // Always overwrite the sample_rate in the DSC + $dynamicSamplingContext = $context->getMetadata()->getDynamicSamplingContext(); + if ($dynamicSamplingContext !== null) { + $dynamicSamplingContext->set('sample_rate', (string) $sampleRate, true); + } + + if ($sampleRate === 0.0) { + $transaction->setSampled(false); + + $logger->info(\sprintf('Transaction [%s] was started but not sampled because sample rate (decided by %s) is %s.', (string) $transaction->getTraceId(), $sampleSource, $sampleRate), ['context' => $context]); + + return $transaction; + } + + $transaction->setSampled($sampleRand < $sampleRate); + } + + if (!$transaction->getSampled()) { + $logger->info(\sprintf('Transaction [%s] was started but not sampled, decided by %s.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); + + return $transaction; + } + + $logger->info(\sprintf('Transaction [%s] was started and sampled, decided by %s.', (string) $transaction->getTraceId(), $sampleSource), ['context' => $context]); + + $transaction->initSpanRecorder(); + + $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 (!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 (self::sampleRate($profilesSampleRate)) { + $logger->info(\sprintf('Transaction [%s] started profiling because it was sampled.', (string) $transaction->getTraceId())); + + $transaction->initProfiler($options)->start(); + } else { + $logger->info(\sprintf('Transaction [%s] is not profiling because it was not sampled.', (string) $transaction->getTraceId())); + } + + return $transaction; + } + + private static function getSampleRate(?bool $hasParentBeenSampled, float $fallbackSampleRate): float + { + if ($hasParentBeenSampled === true) { + return 1.0; + } + + if ($hasParentBeenSampled === false) { + return 0.0; + } + + return $fallbackSampleRate; + } + + /** + * @param mixed $sampleRate + */ + private static 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 static 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/UserDataBag.php b/src/UserDataBag.php index 5fecb478c9..bff8118852 100644 --- a/src/UserDataBag.php +++ b/src/UserDataBag.php @@ -195,13 +195,9 @@ public function setIpAddress(?string $ipAddress): self } if (filter_var($ipAddress, \FILTER_VALIDATE_IP) === false) { - $client = SentrySdk::getCurrentHub()->getClient(); - - if ($client !== null) { - $client->getOptions()->getLoggerOrNullLogger()->debug( - \sprintf('The "%s" value is not a valid IP address.', $ipAddress) - ); - } + SentrySdk::getClient()->getOptions()->getLoggerOrNullLogger()->debug( + \sprintf('The "%s" value is not a valid IP address.', $ipAddress) + ); return $this; } diff --git a/src/functions.php b/src/functions.php index 0935d739fc..84aadd28e1 100644 --- a/src/functions.php +++ b/src/functions.php @@ -10,6 +10,10 @@ use Sentry\Integration\OTLPIntegration; use Sentry\Logs\Logs; use Sentry\Metrics\TraceMetrics; +use Sentry\State\BreadcrumbRecorder; +use Sentry\State\EventRecorder; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanContext; @@ -18,7 +22,7 @@ use Sentry\Transport\TransportInterface; /** - * Creates a new Client and Hub which will be set as current. + * Creates a new Client and initializes the SDK. * * @param array{ * attach_stacktrace?: bool, @@ -74,7 +78,22 @@ function init(array $options = []): void { $client = ClientBuilder::create($options)->getClient(); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); +} + +function getGlobalScope(): GlobalScope +{ + return SentrySdk::getGlobalScope(); +} + +function getIsolationScope(): IsolationScope +{ + return SentrySdk::getIsolationScope(); +} + +function getClient(): ClientInterface +{ + return SentrySdk::getClient(); } /** @@ -86,7 +105,7 @@ function init(array $options = []): void */ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureMessage($message, $level, $hint); + return EventRecorder::captureMessage($message, $level, $hint); } /** @@ -97,7 +116,7 @@ function captureMessage(string $message, ?Severity $level = null, ?EventHint $hi */ function captureException(\Throwable $exception, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureException($exception, $hint); + return EventRecorder::captureException($exception, $hint); } /** @@ -108,7 +127,7 @@ function captureException(\Throwable $exception, ?EventHint $hint = null): ?Even */ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureEvent($event, $hint); + return EventRecorder::captureEvent($event, $hint); } /** @@ -118,7 +137,7 @@ function captureEvent(Event $event, ?EventHint $hint = null): ?EventId */ function captureLastError(?EventHint $hint = null): ?EventId { - return SentrySdk::getCurrentHub()->captureLastError($hint); + return EventRecorder::captureLastError($hint); } /** @@ -132,7 +151,7 @@ function captureLastError(?EventHint $hint = null): ?EventId */ function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ?MonitorConfig $monitorConfig = null, ?string $checkInId = null): ?string { - return SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); + return EventRecorder::captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } /** @@ -146,7 +165,7 @@ function captureCheckIn(string $slug, CheckInStatus $status, $duration = null, ? */ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorConfig = null) { - $checkInId = SentrySdk::getCurrentHub()->captureCheckIn($slug, CheckInStatus::inProgress(), null, $monitorConfig); + $checkInId = captureCheckIn($slug, CheckInStatus::inProgress(), null, $monitorConfig); $status = CheckInStatus::ok(); $duration = 0; @@ -162,7 +181,7 @@ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorCo throw $e; } finally { - SentrySdk::getCurrentHub()->captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); + captureCheckIn($slug, $status, $duration, $monitorConfig, $checkInId); } } @@ -180,11 +199,12 @@ function withMonitor(string $slug, callable $callback, ?MonitorConfig $monitorCo */ function addBreadcrumb($category, ?string $message = null, array $metadata = [], string $level = Breadcrumb::LEVEL_INFO, string $type = Breadcrumb::TYPE_DEFAULT, ?float $timestamp = null): void { - SentrySdk::getCurrentHub()->addBreadcrumb( - $category instanceof Breadcrumb - ? $category - : new Breadcrumb($level, $type, $category, $message, $metadata, $timestamp) - ); + $scope = SentrySdk::getIsolationScope(); + $breadcrumb = $category instanceof Breadcrumb + ? $category + : new Breadcrumb($level, $type, $category, $message, $metadata, $timestamp); + + BreadcrumbRecorder::record(SentrySdk::getClient($scope), $scope, $breadcrumb); } /** @@ -195,7 +215,7 @@ function addBreadcrumb($category, ?string $message = null, array $metadata = [], */ function configureScope(callable $callback): void { - SentrySdk::getCurrentHub()->configureScope($callback); + $callback(SentrySdk::getIsolationScope()); } /** @@ -211,10 +231,38 @@ function configureScope(callable $callback): void * @return mixed|void The callback's return value, upon successful execution * * @phpstan-return T + * + * @deprecated This function will be removed in a follow-up PR. Use {@see withIsolationScope()} instead. */ function withScope(callable $callback) { - return SentrySdk::getCurrentHub()->withScope($callback); + return withIsolationScope($callback); +} + +/** + * Forks the current isolation scope for the duration of the callback. + * + * @param callable $callback The callback to be executed + * + * @phpstan-template T + * + * @phpstan-param callable(IsolationScope): T $callback + * + * @return mixed|void The callback's return value, upon successful execution + * + * @phpstan-return T + */ +function withIsolationScope(callable $callback) +{ + $context = SentrySdk::getCurrentRuntimeContext(); + $previousScope = $context->getIsolationScope(); + $context->setIsolationScope(clone $previousScope); + + try { + return $callback($context->getIsolationScope()); + } finally { + $context->setIsolationScope($previousScope); + } } function startContext(): void @@ -268,7 +316,7 @@ function withContext(callable $callback, ?int $timeout = null) */ function startTransaction(TransactionContext $context, array $customSamplingContext = []): Transaction { - return SentrySdk::getCurrentHub()->startTransaction($context, $customSamplingContext); + return Tracing\TransactionSampler::startTransaction(SentrySdk::getClient()->getOptions(), $context, $customSamplingContext); } /** @@ -277,14 +325,14 @@ function startTransaction(TransactionContext $context, array $customSamplingCont * * @template T * - * @param callable(Scope): T $trace The callable that is going to be traced - * @param SpanContext $context The context of the span to be created + * @param callable(IsolationScope): T $trace The callable that is going to be traced + * @param SpanContext $context The context of the span to be created * * @return T */ function trace(callable $trace, SpanContext $context) { - return SentrySdk::getCurrentHub()->withScope(static function (Scope $scope) use ($context, $trace) { + return withIsolationScope(static function (IsolationScope $scope) use ($context, $trace) { $parentSpan = $scope->getSpan(); $span = null; @@ -314,10 +362,9 @@ function trace(callable $trace, SpanContext $context) */ function getOtlpTracesEndpointUrl(): ?string { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $client = SentrySdk::getClient(); - $integration = $hub->getIntegration(OTLPIntegration::class); + $integration = $client->getIntegration(OTLPIntegration::class); if ($integration instanceof OTLPIntegration && $integration->getCollectorUrl() !== null) { return $integration->getCollectorUrl(); } @@ -336,29 +383,24 @@ function getOtlpTracesEndpointUrl(): ?string * This function is context aware, as in it either returns the traceparent based * on the current span, or the scope's propagation context. */ -function getTraceparent(): string +function getTraceparent(?IsolationScope $scope = null): string { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $scope = $scope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($scope); $options = $client->getOptions(); if ($options->isTracingEnabled()) { - $span = SentrySdk::getCurrentHub()->getSpan(); + $span = $scope->getSpan(); if ($span !== null) { return $span->toTraceparent(); } } - $traceParent = ''; - $hub->configureScope(static function (Scope $scope) use (&$traceParent) { - if ($scope->hasExternalPropagationContext()) { - return; - } - - $traceParent = $scope->getPropagationContext()->toTraceparent(); - }); + if ($scope->hasExternalPropagationContext()) { + return ''; + } - return $traceParent; + return $scope->getPropagationContext()->toTraceparent(); } /** @@ -367,29 +409,24 @@ function getTraceparent(): string * This function is context aware, as in it either returns the baggage based * on the current span or the scope's propagation context. */ -function getBaggage(): string +function getBaggage(?IsolationScope $scope = null): string { - $hub = SentrySdk::getCurrentHub(); - $client = $hub->getClient(); + $scope = $scope ?? SentrySdk::getIsolationScope(); + $client = SentrySdk::getClient($scope); $options = $client->getOptions(); if ($options->isTracingEnabled()) { - $span = SentrySdk::getCurrentHub()->getSpan(); + $span = $scope->getSpan(); if ($span !== null) { return $span->toBaggage(); } } - $baggage = ''; - $hub->configureScope(static function (Scope $scope) use (&$baggage) { - if ($scope->hasExternalPropagationContext()) { - return; - } - - $baggage = $scope->getPropagationContext()->toBaggage(); - }); + if ($scope->hasExternalPropagationContext()) { + return ''; + } - return $baggage; + return $scope->getPropagationContext()->toBaggage(); } /** @@ -420,10 +457,7 @@ function continueTrace(string $sentryTrace, string $baggage): TransactionContext $propagationContext->setDynamicSamplingContext($dynamicSamplingContext); } - $hub = SentrySdk::getCurrentHub(); - $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); + SentrySdk::getIsolationScope()->setPropagationContext($propagationContext); return $transactionContext; } @@ -452,9 +486,7 @@ function traceMetrics(): TraceMetrics */ function addFeatureFlag(string $name, bool $result): void { - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($name, $result) { - $scope->addFeatureFlag($name, $result); - }); + SentrySdk::getIsolationScope()->addFeatureFlag($name, $result); } /** diff --git a/tests/ClientReport/ClientReportAggregatorTest.php b/tests/ClientReport/ClientReportAggregatorTest.php index 7766c7ea50..d59dbda4a3 100644 --- a/tests/ClientReport/ClientReportAggregatorTest.php +++ b/tests/ClientReport/ClientReportAggregatorTest.php @@ -11,11 +11,12 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tests\StubLogger; use Sentry\Tests\StubTransport; use Sentry\Transport\DataCategory; +use function Sentry\captureMessage; + class ClientReportAggregatorTest extends TestCase { protected function setUp(): void @@ -23,7 +24,7 @@ protected function setUp(): void ini_set('zend.exception_ignore_args', '0'); StubTransport::$events = []; StubLogger::$logs = []; - SentrySdk::init()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'logger' => StubLogger::getInstance(), ]), StubTransport::getInstance())); } @@ -69,16 +70,15 @@ public function testClientReportAggregation(): void public function testFlushDoesNotOverwriteLastEventId(): void { - $hub = SentrySdk::getCurrentHub(); - $eventId = $hub->captureMessage('foo'); + $eventId = captureMessage('foo'); $this->assertNotNull($eventId); - $this->assertSame($eventId, $hub->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), 10); ClientReportAggregator::getInstance()->flush(); - $this->assertSame($eventId, $hub->getLastEventId()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public function testNegativeQuantityDiscarded(): void @@ -103,13 +103,13 @@ public function testZeroQuantityDiscarded(): void public function testNegativeQuantityDiscardedWhenNoClientIsBound(): void { - SentrySdk::setCurrentHub(new Hub(new NoOpClient())); + SentrySdk::init(new NoOpClient()); ClientReportAggregator::getInstance()->add(DataCategory::profile(), Reason::eventProcessor(), -10); - SentrySdk::setCurrentHub(new Hub(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'logger' => StubLogger::getInstance(), - ]), StubTransport::getInstance()))); + ]), StubTransport::getInstance())); ClientReportAggregator::getInstance()->flush(); diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 6f23fba983..c6fef94c2f 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -20,6 +20,7 @@ use Sentry\Serializer\RepresentationSerializerInterface; use Sentry\Severity; use Sentry\Stacktrace; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; @@ -69,7 +70,7 @@ public function setupOnce(): void $logger ); - $client->captureEvent(Event::createEvent(), null, new Scope()); + $client->captureEvent(Event::createEvent(), null, new IsolationScope()); $this->assertTrue($integrationCalled); } @@ -966,7 +967,7 @@ public function testProcessEventDiscardsEventWhenEventProcessorReturnsNull(): vo ->setLogger($logger) ->getClient(); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addEventProcessor(static function () { return null; }); diff --git a/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php b/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php index 6477cf1900..2144bd1f7a 100644 --- a/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php +++ b/tests/Fixtures/OpenTelemetry/TestDiscoveryStrategy.php @@ -4,6 +4,7 @@ namespace Sentry\Tests\Fixtures\OpenTelemetry; +use GuzzleHttp\Psr7\HttpFactory; use Nyholm\Psr7\Factory\Psr17Factory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; @@ -21,7 +22,10 @@ public static function getCandidates(string $type): array } if (is_a(RequestFactoryInterface::class, $type, true) || is_a(StreamFactoryInterface::class, $type, true)) { - return [['class' => Psr17Factory::class, 'condition' => Psr17Factory::class]]; + return [ + ['class' => HttpFactory::class, 'condition' => HttpFactory::class], + ['class' => Psr17Factory::class, 'condition' => Psr17Factory::class], + ]; } return []; diff --git a/tests/Fixtures/runtime/frankenphp/index.php b/tests/Fixtures/runtime/frankenphp/index.php index bbcceb5bb3..e0367175ee 100644 --- a/tests/Fixtures/runtime/frankenphp/index.php +++ b/tests/Fixtures/runtime/frankenphp/index.php @@ -4,9 +4,7 @@ use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; -use function Sentry\configureScope; use function Sentry\getTraceparent; use function Sentry\init; use function Sentry\withContext; @@ -20,9 +18,7 @@ 'default_integrations' => false, ]); -configureScope(static function (Scope $scope): void { - $scope->setTag('baseline', 'yes'); -}); +SentrySdk::getGlobalScope()->setTag('baseline', 'yes'); $handler = static function (): void { $path = parse_url($_SERVER['REQUEST_URI'] ?? '/', \PHP_URL_PATH); @@ -46,18 +42,14 @@ $leakTag = isset($_GET['leak']) ? (string) $_GET['leak'] : null; withContext(static function () use ($requestTag, $leakTag): void { - configureScope(static function (Scope $scope) use ($requestTag, $leakTag): void { - $scope->setTag('request', $requestTag); + SentrySdk::getIsolationScope()->setTag('request', $requestTag); - if ($leakTag !== null) { - $scope->setTag('leak', $leakTag); - } - }); + if ($leakTag !== null) { + SentrySdk::getIsolationScope()->setTag('leak', $leakTag); + } $event = Event::createEvent(); - configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $tags = []; diff --git a/tests/Fixtures/runtime/roadrunner-worker.php b/tests/Fixtures/runtime/roadrunner-worker.php index 6a4680da83..f8d6bf79d6 100644 --- a/tests/Fixtures/runtime/roadrunner-worker.php +++ b/tests/Fixtures/runtime/roadrunner-worker.php @@ -6,11 +6,9 @@ use Nyholm\Psr7\Response; use Sentry\Event; use Sentry\SentrySdk; -use Sentry\State\Scope; use Spiral\RoadRunner\Http\PSR7Worker; use Spiral\RoadRunner\Worker; -use function Sentry\configureScope; use function Sentry\getTraceparent; use function Sentry\init; use function Sentry\withContext; @@ -37,9 +35,7 @@ 'default_integrations' => false, ]); -configureScope(static function (Scope $scope): void { - $scope->setTag('baseline', 'yes'); -}); +SentrySdk::getGlobalScope()->setTag('baseline', 'yes'); $factory = new Psr17Factory(); $worker = Worker::create(); @@ -102,18 +98,14 @@ function handleRequest($request): Response $leakTag = isset($query['leak']) ? (string) $query['leak'] : null; $payload = withContext(static function () use ($requestTag, $leakTag): string { - configureScope(static function (Scope $scope) use ($requestTag, $leakTag): void { - $scope->setTag('request', $requestTag); + SentrySdk::getIsolationScope()->setTag('request', $requestTag); - if ($leakTag !== null) { - $scope->setTag('leak', $leakTag); - } - }); + if ($leakTag !== null) { + SentrySdk::getIsolationScope()->setTag('leak', $leakTag); + } $event = Event::createEvent(); - configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $tags = []; diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 4611e60547..34e77e592f 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -19,8 +19,8 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Hub; -use Sentry\State\HubInterface; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\Span; @@ -34,6 +34,7 @@ use Sentry\Util\SentryUid; use function Sentry\addBreadcrumb; +use function Sentry\addFeatureFlag; use function Sentry\captureCheckIn; use function Sentry\captureEvent; use function Sentry\captureException; @@ -50,6 +51,7 @@ use function Sentry\startTransaction; use function Sentry\trace; use function Sentry\withContext; +use function Sentry\withIsolationScope; use function Sentry\withMonitor; use function Sentry\withScope; @@ -59,7 +61,26 @@ public function testInit(): void { init(['default_integrations' => false]); - $this->assertNotNull(SentrySdk::getCurrentHub()->getClient()); + $client = SentrySdk::getClient(); + + $this->assertNotInstanceOf(NoOpClient::class, $client); + $this->assertSame($client, SentrySdk::getGlobalScope()->getClient()); + } + + public function testInitPreservesGlobalScope(): void + { + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setTag('baseline', 'yes'); + + init(['default_integrations' => false]); + + $this->assertSame($globalScope, SentrySdk::getGlobalScope()); + $this->assertSame(SentrySdk::getClient(), $globalScope->getClient()); + + $event = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['baseline' => 'yes'], $event->getTags()); } /** @@ -68,16 +89,20 @@ public function testInit(): void public function testCaptureMessage(array $functionCallArgs, array $expectedFunctionCallArgs): void { $eventId = EventId::generate(); + $message = $expectedFunctionCallArgs[0]; + $level = $expectedFunctionCallArgs[1]; + $hint = $expectedFunctionCallArgs[2]; + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client->expects($this->once()) ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) + ->with($message, $level, $this->captureScopeConstraint($scope), $hint) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); - $this->assertSame($eventId, captureMessage(...$functionCallArgs)); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public static function captureMessageDataProvider(): \Generator @@ -115,15 +140,16 @@ public function testCaptureException(array $functionCallArgs, array $expectedFun { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) ->method('captureException') - ->with(...$expectedFunctionCallArgs) + ->with($expectedFunctionCallArgs[0], $this->captureScopeConstraint($scope), $expectedFunctionCallArgs[1]) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); - $this->assertSame($eventId, captureException(...$functionCallArgs)); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public static function captureExceptionDataProvider(): \Generator @@ -155,15 +181,16 @@ public function testCaptureEvent(): void $event = Event::createEvent(); $hint = new EventHint(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) ->method('captureEvent') - ->with($event, $hint) + ->with($event, $hint, $this->captureScopeConstraint($scope)) ->willReturn($event->getId()); - SentrySdk::setCurrentHub($hub); - $this->assertSame($event->getId(), captureEvent($event, $hint)); + $this->assertSame($event->getId(), SentrySdk::getLastEventId()); } /** @@ -173,17 +200,18 @@ public function testCaptureLastError(array $functionCallArgs, array $expectedFun { $eventId = EventId::generate(); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) + ->with($this->captureScopeConstraint($scope), $expectedFunctionCallArgs[0]) ->willReturn($eventId); - SentrySdk::setCurrentHub($hub); - @trigger_error('foo', \E_USER_NOTICE); $this->assertSame($eventId, captureLastError(...$functionCallArgs)); + $this->assertSame($eventId, SentrySdk::getLastEventId()); } public static function captureLastErrorDataProvider(): \Generator @@ -199,9 +227,92 @@ public static function captureLastErrorDataProvider(): \Generator ]; } + public function testCaptureMessageClearsLastEventIdWhenClientReturnsNull(): void + { + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureMessage') + ->with('foo', null, $this->captureScopeConstraint($scope), null) + ->willReturn(null); + + $this->assertNull(captureMessage('foo')); + $this->assertNull(SentrySdk::getLastEventId()); + } + + public function testCaptureExceptionClearsLastEventIdWhenClientReturnsNull(): void + { + $exception = new \RuntimeException('foo'); + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureException') + ->with($exception, $this->captureScopeConstraint($scope), null) + ->willReturn(null); + + $this->assertNull(captureException($exception)); + $this->assertNull(SentrySdk::getLastEventId()); + } + + public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void + { + $event = Event::createEvent(); + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($event, null, $this->captureScopeConstraint($scope)) + ->willReturn(null); + + $this->assertNull(captureEvent($event)); + $this->assertNull(SentrySdk::getLastEventId()); + } + + public function testCaptureLastErrorClearsLastEventIdWhenClientReturnsNull(): void + { + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureLastError') + ->with($this->captureScopeConstraint($scope), null) + ->willReturn(null); + + $this->assertNull(captureLastError()); + $this->assertNull(SentrySdk::getLastEventId()); + } + + public function testCaptureExceptionInsideWithIsolationScopeUpdatesLastEventId(): void + { + $eventId = EventId::generate(); + $exception = new \RuntimeException('foo'); + $client = $this->createMock(ClientInterface::class); + $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureException') + ->willReturn($eventId); + + withIsolationScope(static function () use ($exception): void { + captureException($exception); + }); + + // The forked isolation scope is discarded after the callback, but the + // captured event ID must remain readable from the runtime context. + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + public function testCaptureCheckIn(): void { $checkInId = SentryUid::generate(); + $eventId = EventId::generate(); $monitorConfig = new MonitorConfig( MonitorSchedule::crontab('*/5 * * * *'), 5, @@ -209,13 +320,30 @@ public function testCaptureCheckIn(): void 'UTC' ); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureCheckIn') - ->with('test-crontab', CheckInStatus::ok(), 10, $monitorConfig, $checkInId) - ->willReturn($checkInId); + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); - SentrySdk::setCurrentHub($hub); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'environment' => Event::DEFAULT_ENVIRONMENT, + 'release' => '1.0.0', + ])); + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event) use ($checkInId, $monitorConfig): bool { + $checkIn = $event->getCheckIn(); + + return $checkIn !== null + && $checkIn->getId() === $checkInId + && $checkIn->getMonitorSlug() === 'test-crontab' + && $checkIn->getStatus() == CheckInStatus::ok() + && $checkIn->getRelease() === '1.0.0' + && $checkIn->getEnvironment() === Event::DEFAULT_ENVIRONMENT + && $checkIn->getDuration() === 10 + && $checkIn->getMonitorConfig() === $monitorConfig; + }), null, $this->captureScopeConstraint($scope)) + ->willReturn($eventId); $this->assertSame($checkInId, captureCheckIn( 'test-crontab', @@ -224,99 +352,184 @@ public function testCaptureCheckIn(): void $monitorConfig, $checkInId )); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureCheckInReturnsNullForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + + $this->assertNull(captureCheckIn('test-crontab', CheckInStatus::ok())); } public function testWithMonitor(): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->exactly(2)) - ->method('captureCheckIn') - ->with( - $this->callback(static function (string $slug): bool { - return $slug === 'test-crontab'; - }), - $this->callback(static function (CheckInStatus $checkInStatus): bool { - // just check for type CheckInStatus - return true; - }), - $this->anything(), - $this->callback(static function (MonitorConfig $monitorConfig): bool { - return $monitorConfig->getSchedule()->getValue() === '*/5 * * * *' - && $monitorConfig->getSchedule()->getType() === MonitorSchedule::TYPE_CRONTAB - && $monitorConfig->getCheckinMargin() === 5 - && $monitorConfig->getMaxRuntime() === 30 - && $monitorConfig->getTimezone() === 'UTC'; - }) - ); - - SentrySdk::setCurrentHub($hub); - - withMonitor('test-crontab', static function () { - // Do something... - }, new MonitorConfig( + $events = []; + $monitorConfig = new MonitorConfig( new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), 5, 30, 'UTC' - )); + ); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->exactly(2)) + ->method('getOptions') + ->willReturn(new Options()); + $client->expects($this->exactly(2)) + ->method('captureEvent') + ->with($this->callback(static function (Event $event): bool { + $checkIn = $event->getCheckIn(); + + return $checkIn !== null + && $checkIn->getMonitorSlug() === 'test-crontab' + && $checkIn->getMonitorConfig() !== null + && $checkIn->getMonitorConfig()->getSchedule()->getValue() === '*/5 * * * *'; + }), null, $this->captureScopeConstraint($scope)) + ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null) use (&$events): EventId { + $events[] = $event; + + return EventId::generate(); + }); + + $result = withMonitor('test-crontab', static function (): string { + // Do something... + return 'done'; + }, $monitorConfig); + + $this->assertSame('done', $result); + $this->assertCount(2, $events); + $this->assertSame(CheckInStatus::inProgress(), $events[0]->getCheckIn()->getStatus()); + $this->assertSame(CheckInStatus::ok(), $events[1]->getCheckIn()->getStatus()); + $this->assertSame($events[0]->getCheckIn()->getId(), $events[1]->getCheckIn()->getId()); } public function testWithMonitorCallableThrows(): void { - $this->expectException(\Exception::class); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->exactly(2)) - ->method('captureCheckIn') - ->with( - $this->callback(static function (string $slug): bool { - return $slug === 'test-crontab'; - }), - $this->callback(static function (CheckInStatus $checkInStatus): bool { - // just check for type CheckInStatus - return true; - }), - $this->anything(), - $this->callback(static function (MonitorConfig $monitorConfig): bool { - return $monitorConfig->getSchedule()->getValue() === '*/5 * * * *' - && $monitorConfig->getSchedule()->getType() === MonitorSchedule::TYPE_CRONTAB - && $monitorConfig->getCheckinMargin() === 5 - && $monitorConfig->getMaxRuntime() === 30 - && $monitorConfig->getTimezone() === 'UTC'; - }) - ); - - SentrySdk::setCurrentHub($hub); - - withMonitor('test-crontab', static function () { - throw new \Exception(); - }, new MonitorConfig( - new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), - 5, - 30, - 'UTC' - )); + $events = []; + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->exactly(2)) + ->method('getOptions') + ->willReturn(new Options()); + $client->expects($this->exactly(2)) + ->method('captureEvent') + ->with($this->isInstanceOf(Event::class), null, $this->captureScopeConstraint($scope)) + ->willReturnCallback(static function (Event $event, ?EventHint $hint = null, ?IsolationScope $scope = null) use (&$events): EventId { + $events[] = $event; + + return EventId::generate(); + }); + + try { + withMonitor('test-crontab', static function (): void { + throw new \Exception('monitor failed'); + }, new MonitorConfig( + new MonitorSchedule(MonitorSchedule::TYPE_CRONTAB, '*/5 * * * *'), + 5, + 30, + 'UTC' + )); + + $this->fail('The callback exception should be rethrown.'); + } catch (\Exception $exception) { + $this->assertSame('monitor failed', $exception->getMessage()); + } + + $this->assertCount(2, $events); + $this->assertSame(CheckInStatus::inProgress(), $events[0]->getCheckIn()->getStatus()); + $this->assertSame(CheckInStatus::error(), $events[1]->getCheckIn()->getStatus()); + $this->assertSame($events[0]->getCheckIn()->getId(), $events[1]->getCheckIn()->getId()); } public function testAddBreadcrumb(): void { $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $otherScope = new IsolationScope(); /** @var ClientInterface&MockObject $client */ $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + $client->expects($this->once()) ->method('getOptions') ->willReturn(new Options(['default_integrations' => false])); - SentrySdk::getCurrentHub()->bindClient($client); + addBreadcrumb($breadcrumb); + + $this->assertScopeBreadcrumbs($scope, [$breadcrumb]); + $this->assertScopeBreadcrumbs($otherScope, []); + } + + public function testAddBreadcrumbDoesNothingIfMaxBreadcrumbsLimitIsZero(): void + { + $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['max_breadcrumbs' => 0])); addBreadcrumb($breadcrumb); - configureScope(function (Scope $scope) use ($breadcrumb): void { - $event = $scope->applyToEvent(Event::createEvent()); - $this->assertNotNull($event); - $this->assertSame([$breadcrumb], $event->getBreadcrumbs()); - }); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testAddBreadcrumbDoesNothingForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + $scope = SentrySdk::getIsolationScope(); + + addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); + + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testAddBreadcrumbDoesNothingWhenBeforeBreadcrumbCallbackReturnsNull(): void + { + $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () { + return null; + }, + ])); + + addBreadcrumb($breadcrumb); + + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testAddBreadcrumbStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): void + { + $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_DEFAULT, 'custom'); + + $client = $this->createMock(ClientInterface::class); + $scope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () use ($breadcrumb2): Breadcrumb { + return $breadcrumb2; + }, + ])); + + addBreadcrumb($breadcrumb1); + + $this->assertScopeBreadcrumbs($scope, [$breadcrumb2]); } public function testWithScope(): void @@ -328,83 +541,123 @@ public function testWithScope(): void $this->assertSame('foobarbaz', $returnValue); } - public function testConfigureScope(): void + public function testConfigureScopeMutatesCurrentIsolationScopeOnly(): void { - $callbackInvoked = false; + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setTag('scope', 'global'); + + $isolationScope = new IsolationScope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); - configureScope(static function () use (&$callbackInvoked): void { - $callbackInvoked = true; + $callbackScope = null; + + configureScope(static function (IsolationScope $scope) use (&$callbackScope): void { + $callbackScope = $scope; + $scope->setTag('scope', 'isolation'); }); - $this->assertTrue($callbackInvoked); + $this->assertSame($isolationScope, $callbackScope); + + $isolationEvent = (new GlobalScope())->merge($isolationScope)->applyToEvent(Event::createEvent()); + $this->assertNotNull($isolationEvent); + $this->assertSame(['scope' => 'isolation'], $isolationEvent->getTags()); + + $globalEvent = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + $this->assertNotNull($globalEvent); + $this->assertSame(['scope' => 'global'], $globalEvent->getTags()); + } + + public function testAddFeatureFlagMutatesCurrentIsolationScopeOnly(): void + { + $globalScope = SentrySdk::getGlobalScope(); + + $isolationScope = new IsolationScope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); + + addFeatureFlag('isolation-only', false); + + $isolationEvent = (new GlobalScope())->merge($isolationScope)->applyToEvent(Event::createEvent()); + $this->assertNotNull($isolationEvent); + $this->assertSame([ + 'values' => [ + [ + 'flag' => 'isolation-only', + 'result' => false, + ], + ], + ], $isolationEvent->getContexts()['flags']); + + $globalEvent = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + $this->assertNotNull($globalEvent); + $this->assertArrayNotHasKey('flags', $globalEvent->getContexts()); } public function testStartAndEndContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); + $globalScope = SentrySdk::getIsolationScope(); startContext(); - $requestHub = SentrySdk::getCurrentHub(); + $requestScope = SentrySdk::getIsolationScope(); - $this->assertNotSame($globalHub, $requestHub); + $this->assertNotSame($globalScope, $requestScope); endContext(); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testWithContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); + $globalScope = SentrySdk::getIsolationScope(); - $result = withContext(function () use ($globalHub): string { - $this->assertNotSame($globalHub, SentrySdk::getCurrentHub()); + $result = withContext(function () use ($globalScope): string { + $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); return 'ok'; }); $this->assertSame('ok', $result); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testNestedWithContextReusesOuterContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $outerHub = null; - $innerHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $outerScope = null; + $innerScope = null; - withContext(function () use (&$outerHub, &$innerHub, $globalHub): void { - $outerHub = SentrySdk::getCurrentHub(); + withContext(function () use (&$outerScope, &$innerScope, $globalScope): void { + $outerScope = SentrySdk::getIsolationScope(); - configureScope(static function (Scope $scope): void { + configureScope(static function (IsolationScope $scope): void { $scope->setTag('outer', 'yes'); }); - withContext(static function () use (&$innerHub): void { - $innerHub = SentrySdk::getCurrentHub(); + withContext(static function () use (&$innerScope): void { + $innerScope = SentrySdk::getIsolationScope(); }); $event = Event::createEvent(); - configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); + configureScope(static function (IsolationScope $scope) use (&$event): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); }); - $this->assertNotSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); $this->assertSame('yes', $event->getTags()['outer'] ?? null); }); - $this->assertNotNull($outerHub); - $this->assertNotNull($innerHub); - $this->assertSame($outerHub, $innerHub); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotNull($outerScope); + $this->assertNotNull($innerScope); + $this->assertSame($outerScope, $innerScope); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testWithContextAlwaysEndsContextWithOptionalTimeout(): void @@ -419,7 +672,7 @@ public function testWithContextAlwaysEndsContextWithOptionalTimeout(): void ->with(13) ->willReturn(new Result(ResultStatus::success())); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); try { withContext(static function (): void { @@ -438,15 +691,16 @@ public function testStartTransaction(): void $transaction = new Transaction($transactionContext); $customSamplingContext = ['foo' => 'bar']; - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('startTransaction') - ->with($transactionContext, $customSamplingContext) - ->willReturn($transaction); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); + + SentrySdk::getGlobalScope()->setClient($client); - SentrySdk::setCurrentHub($hub); + $transaction = startTransaction($transactionContext, $customSamplingContext); - $this->assertSame($transaction, startTransaction($transactionContext, $customSamplingContext)); + $this->assertSame('foo', $transaction->getName()); } public function testTraceReturnsClosureResult(): void @@ -462,54 +716,60 @@ public function testTraceReturnsClosureResult(): void public function testTraceCorrectlyReplacesAndRestoresCurrentSpan(): void { - $hub = new Hub(new NoOpClient()); - $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(true); + $outerScope = SentrySdk::getIsolationScope(); + $outerScope->setSpan($transaction); - $hub->setSpan($transaction); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); - SentrySdk::setCurrentHub($hub); + $childSpan = null; - $this->assertSame($transaction, $hub->getSpan()); + trace(function (IsolationScope $scope) use ($outerScope, $transaction, &$childSpan): void { + $childSpan = $scope->getSpan(); - trace(function () use ($transaction, $hub) { - $this->assertNotSame($transaction, $hub->getSpan()); + $this->assertNotSame($outerScope, $scope); + $this->assertNotSame($transaction, $childSpan); + $this->assertSame($childSpan, SentrySdk::getIsolationScope()->getSpan()); + $this->assertNull($childSpan->getEndTimestamp()); }, new SpanContext()); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertNotNull($childSpan); + $this->assertNotNull($childSpan->getEndTimestamp()); + $this->assertSame($outerScope, SentrySdk::getIsolationScope()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); try { - trace(static function () { + trace(function (IsolationScope $scope) use ($transaction): void { + $this->assertNotSame($transaction, $scope->getSpan()); + throw new \RuntimeException('Throwing should still restore the previous span'); }, new SpanContext()); } catch (\RuntimeException $e) { - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($outerScope, SentrySdk::getIsolationScope()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); } } public function testTraceDoesntCreateSpanIfTransactionIsNotSampled(): void { - $scope = $this->createMock(Scope::class); - - $hub = new Hub(new NoOpClient(), $scope); - $transaction = new Transaction(TransactionContext::make()); $transaction->setSampled(false); - $scope->expects($this->never()) - ->method('setSpan'); - $scope->expects($this->exactly(3)) - ->method('getSpan') - ->willReturn($transaction); + $outerScope = SentrySdk::getIsolationScope(); + $outerScope->setSpan($transaction); + $callbackScope = null; - SentrySdk::setCurrentHub($hub); + trace(function (IsolationScope $scope) use ($transaction, &$callbackScope): void { + $callbackScope = $scope; - trace(function () use ($transaction, $hub) { - $this->assertSame($transaction, $hub->getSpan()); + $this->assertSame($transaction, $scope->getSpan()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); }, SpanContext::make()); - $this->assertSame($transaction, $hub->getSpan()); + $this->assertNotSame($outerScope, $callbackScope); + $this->assertSame($outerScope, SentrySdk::getIsolationScope()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); } public function testTraceparentWithTracingDisabled(): void @@ -518,11 +778,7 @@ public function testTraceparentWithTracingDisabled(): void $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $propagationContext->setSpanId(new SpanId('566e3688a61d4bc8')); - $scope = new Scope($propagationContext); - - $hub = new Hub(new NoOpClient(), $scope); - - SentrySdk::setCurrentHub($hub); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $traceParent = getTraceparent(); @@ -538,9 +794,7 @@ public function testTraceparentWithTracingEnabled(): void 'traces_sample_rate' => 1.0, ])); - $hub = new Hub($client); - - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $spanContext = (new SpanContext()) ->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')) @@ -548,7 +802,7 @@ public function testTraceparentWithTracingEnabled(): void $span = new Span($spanContext); - $hub->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); $traceParent = getTraceparent(); @@ -568,7 +822,7 @@ public function testTraceHeadersAreEmptyWhenExternalPropagationContextIsActive() ]; }); - SentrySdk::setCurrentHub(new Hub(new NoOpClient(), new Scope($propagationContext))); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $this->assertSame('', getTraceparent()); $this->assertSame('', getBaggage()); @@ -582,8 +836,6 @@ public function testBaggageWithTracingDisabled(): void $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $propagationContext->setSampleRand(0.25); - $scope = new Scope($propagationContext); - $client = $this->createMock(ClientInterface::class); $client->expects($this->atLeastOnce()) ->method('getOptions') @@ -592,13 +844,13 @@ public function testBaggageWithTracingDisabled(): void 'environment' => 'development', ])); - $hub = new Hub($client, $scope); - - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $baggage = getBaggage(); $this->assertSame('sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.25,sentry-release=1.0.0,sentry-environment=development', $baggage); + $this->assertNotNull($propagationContext->getDynamicSamplingContext()); } public function testBaggageWithTracingEnabled(): void @@ -612,9 +864,7 @@ public function testBaggageWithTracingEnabled(): void 'environment' => 'development', ])); - $hub = new Hub($client); - - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); $transactionContext = new TransactionContext(); $transactionContext->setName('Test'); @@ -627,7 +877,7 @@ public function testBaggageWithTracingEnabled(): void $span = $transaction->startChild($spanContext); - $hub->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); $baggage = getBaggage(); @@ -647,7 +897,7 @@ public function testGetOtlpTracesEndpointUrlFallsBackToDsn(): void 'dsn' => 'https://public@example.com/1', ])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame('https://example.com/api/1/integration/otlp/v1/traces/', getOtlpTracesEndpointUrl()); } @@ -666,16 +916,17 @@ public function testGetOtlpTracesEndpointUrlPrefersCollectorUrl(): void 'dsn' => 'https://public@example.com/1', ])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::getGlobalScope()->setClient($client); $this->assertSame('http://collector:4318/v1/traces', getOtlpTracesEndpointUrl()); } public function testContinueTrace(): void { - $hub = new Hub(new NoOpClient()); + SentrySdk::getGlobalScope()->setClient(new NoOpClient()); - SentrySdk::setCurrentHub($hub); + $scope = new IsolationScope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -686,17 +937,15 @@ public function testContinueTrace(): void $this->assertSame('566e3688a61d4bc8', (string) $transactionContext->getParentSpanId()); $this->assertTrue($transactionContext->getParentSampled()); - configureScope(function (Scope $scope): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = $scope->getPropagationContext(); - $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $propagationContext->getTraceId()); - $this->assertSame('566e3688a61d4bc8', (string) $propagationContext->getParentSpanId()); + $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $propagationContext->getTraceId()); + $this->assertSame('566e3688a61d4bc8', (string) $propagationContext->getParentSpanId()); - $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); + $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); - $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $dynamicSamplingContext->get('trace_id')); - $this->assertTrue($dynamicSamplingContext->isFrozen()); - }); + $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $dynamicSamplingContext->get('trace_id')); + $this->assertTrue($dynamicSamplingContext->isFrozen()); } public function testContinueTraceWhenOrgMismatch(): void @@ -709,8 +958,10 @@ public function testContinueTraceWhenOrgMismatch(): void 'org_id' => 1, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new IsolationScope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -727,14 +978,12 @@ public function testContinueTraceWhenOrgMismatch(): void $this->assertNull($transactionContext->getMetadata()->getDynamicSamplingContext()); $this->assertNotNull($newSampleRand); - configureScope(function (Scope $scope) use ($newTraceId, $newSampleRand): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = $scope->getPropagationContext(); - $this->assertSame($newTraceId, (string) $propagationContext->getTraceId()); - $this->assertNull($propagationContext->getParentSpanId()); - $this->assertNull($propagationContext->getDynamicSamplingContext()); - $this->assertSame($newSampleRand, $propagationContext->getSampleRand()); - }); + $this->assertSame($newTraceId, (string) $propagationContext->getTraceId()); + $this->assertNull($propagationContext->getParentSpanId()); + $this->assertNull($propagationContext->getDynamicSamplingContext()); + $this->assertSame($newSampleRand, $propagationContext->getSampleRand()); } public function testContinueTraceWhenOrgMatch(): void @@ -747,8 +996,10 @@ public function testContinueTraceWhenOrgMatch(): void 'org_id' => 1, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new IsolationScope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); $transactionContext = continueTrace( '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8-1', @@ -759,16 +1010,60 @@ public function testContinueTraceWhenOrgMatch(): void $this->assertSame('566e3688a61d4bc8', (string) $transactionContext->getParentSpanId()); $this->assertTrue($transactionContext->getParentSampled()); - configureScope(function (Scope $scope): void { - $propagationContext = $scope->getPropagationContext(); + $propagationContext = $scope->getPropagationContext(); + + $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $propagationContext->getTraceId()); + $this->assertSame('566e3688a61d4bc8', (string) $propagationContext->getParentSpanId()); + + $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); + + $this->assertNotNull($dynamicSamplingContext); + $this->assertSame('1', $dynamicSamplingContext->get('org_id')); + } + + private function setClientAndIsolationScope(ClientInterface $client): IsolationScope + { + SentrySdk::init(); - $this->assertSame('566e3688a61d4bc888951642d6f14a19', (string) $propagationContext->getTraceId()); - $this->assertSame('566e3688a61d4bc8', (string) $propagationContext->getParentSpanId()); + SentrySdk::getGlobalScope()->clear(); + SentrySdk::getGlobalScope()->setTag('scope', 'global'); + SentrySdk::getGlobalScope()->setTag('global', 'yes'); + SentrySdk::getGlobalScope()->setClient($client); - $dynamicSamplingContext = $propagationContext->getDynamicSamplingContext(); + $scope = new IsolationScope(); + $scope->setTag('scope', 'isolation'); + $scope->setTag('isolation', 'yes'); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); - $this->assertNotNull($dynamicSamplingContext); - $this->assertSame('1', $dynamicSamplingContext->get('org_id')); + return $scope; + } + + private function captureScopeConstraint(IsolationScope $isolationScope) + { + return $this->callback(function (IsolationScope $captureScope) use ($isolationScope): bool { + $this->assertSame($isolationScope, $captureScope); + + $event = SentrySdk::getGlobalScope()->merge($captureScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([ + 'scope' => 'isolation', + 'global' => 'yes', + 'isolation' => 'yes', + ], $event->getTags()); + + return true; }); } + + /** + * @param Breadcrumb[] $expectedBreadcrumbs + */ + private function assertScopeBreadcrumbs(IsolationScope $scope, array $expectedBreadcrumbs): void + { + $event = (new GlobalScope())->merge($scope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame($expectedBreadcrumbs, $event->getBreadcrumbs()); + } } diff --git a/tests/Integration/EnvironmentIntegrationTest.php b/tests/Integration/EnvironmentIntegrationTest.php index 788b95fa86..3c7a66fe73 100644 --- a/tests/Integration/EnvironmentIntegrationTest.php +++ b/tests/Integration/EnvironmentIntegrationTest.php @@ -12,7 +12,7 @@ use Sentry\Event; use Sentry\Integration\EnvironmentIntegration; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Util\PHPVersion; use function Sentry\withScope; @@ -33,14 +33,14 @@ public function testInvoke(bool $isIntegrationEnabled, ?RuntimeContext $initialR ->method('getIntegration') ->willReturn($isIntegrationEnabled ? $integration : null); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); - withScope(function (Scope $scope) use ($expectedRuntimeContext, $expectedOsContext, $initialRuntimeContext, $initialOsContext): void { + withScope(function (IsolationScope $scope) use ($expectedRuntimeContext, $expectedOsContext, $initialRuntimeContext, $initialOsContext): void { $event = Event::createEvent(); $event->setRuntimeContext($initialRuntimeContext); $event->setOsContext($initialOsContext); - $event = $scope->applyToEvent($event); + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); $this->assertNotNull($event); diff --git a/tests/Integration/FrameContextifierIntegrationTest.php b/tests/Integration/FrameContextifierIntegrationTest.php index 5667826b0d..ac49a43883 100644 --- a/tests/Integration/FrameContextifierIntegrationTest.php +++ b/tests/Integration/FrameContextifierIntegrationTest.php @@ -14,7 +14,7 @@ use Sentry\Options; use Sentry\SentrySdk; use Sentry\Stacktrace; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withScope; @@ -40,7 +40,7 @@ public function testInvoke(string $fixtureFilePath, int $lineNumber, int $contex ->method('getOptions') ->willReturn($options); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); $stacktrace = new Stacktrace([ new Frame('[unknown]', $fixtureFilePath, $lineNumber, null, $fixtureFilePath), @@ -49,8 +49,8 @@ public function testInvoke(string $fixtureFilePath, int $lineNumber, int $contex $event = Event::createEvent(); $event->setStacktrace($stacktrace); - withScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); + withScope(static function (IsolationScope $scope) use (&$event): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); }); $this->assertNotNull($event); @@ -133,7 +133,7 @@ public function testInvokeLogsWarningMessageIfSourceCodeExcerptCannotBeRetrieved ->method('getOptions') ->willReturn($options); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); $stacktrace = new Stacktrace([ new Frame(null, '[internal]', 0), @@ -143,8 +143,8 @@ public function testInvokeLogsWarningMessageIfSourceCodeExcerptCannotBeRetrieved $event = Event::createEvent(); $event->setStacktrace($stacktrace); - withScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); + withScope(static function (IsolationScope $scope) use (&$event): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); }); $this->assertNotNull($event); diff --git a/tests/Integration/ModulesIntegrationTest.php b/tests/Integration/ModulesIntegrationTest.php index 52b96c9d36..33abcc5022 100644 --- a/tests/Integration/ModulesIntegrationTest.php +++ b/tests/Integration/ModulesIntegrationTest.php @@ -11,7 +11,7 @@ use Sentry\Event; use Sentry\Integration\ModulesIntegration; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; @@ -34,10 +34,10 @@ public function testInvoke(bool $isIntegrationEnabled, bool $expectedEmptyModule ->method('getIntegration') ->willReturn($isIntegrationEnabled ? $integration : null); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); - withScope(function (Scope $scope) use ($expectedEmptyModules): void { - $event = $scope->applyToEvent(Event::createEvent()); + withScope(function (IsolationScope $scope) use ($expectedEmptyModules): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); @@ -86,9 +86,9 @@ public function testModuleIntegration(): void ->setTransport($transport) ->getClient(); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); - $client->captureEvent(Event::createEvent(), null, new Scope()); + $client->captureEvent(Event::createEvent(), null, new IsolationScope()); } /** diff --git a/tests/Integration/OTLPIntegrationTest.php b/tests/Integration/OTLPIntegrationTest.php index a0f3e5680f..b8ef7ce13d 100644 --- a/tests/Integration/OTLPIntegrationTest.php +++ b/tests/Integration/OTLPIntegrationTest.php @@ -19,7 +19,6 @@ use Sentry\Integration\OTLPIntegration; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\State\Scope; use Sentry\Tests\Fixtures\OpenTelemetry\StubOtelHttpClient; use Sentry\Tests\Fixtures\OpenTelemetry\TestClientDiscoverer; @@ -115,7 +114,7 @@ public function testSetupOnceRegistersExternalPropagationContext(): void ->willReturn($integration); try { - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $this->assertSame([ 'trace_id' => '771a43a4192642f0b136d5159a501700', @@ -146,7 +145,7 @@ public function testExternalPropagationContextIsIgnoredWhenCurrentClientDoesNotH ->willReturn(null); try { - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $this->assertNull(Scope::getExternalPropagationContext()); } finally { @@ -280,10 +279,10 @@ private function useCapturingHttpClient(): void if (method_exists(HttpClientDiscovery::class, 'setDiscoverers')) { HttpClientDiscovery::setDiscoverers([new TestClientDiscoverer()]); - } else { - ClassDiscovery::prependStrategy(TestDiscoveryStrategy::class); } + ClassDiscovery::prependStrategy(TestDiscoveryStrategy::class); + StubOtelHttpClient::reset(); } diff --git a/tests/Integration/RequestIntegrationTest.php b/tests/Integration/RequestIntegrationTest.php index 66749155a9..7de6c166c6 100644 --- a/tests/Integration/RequestIntegrationTest.php +++ b/tests/Integration/RequestIntegrationTest.php @@ -16,7 +16,7 @@ use Sentry\Integration\RequestIntegration; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\UserDataBag; use function Sentry\withScope; @@ -44,10 +44,10 @@ public function testInvoke(array $options, ServerRequestInterface $request, arra ->method('getOptions') ->willReturn(new Options($options)); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); - withScope(function (Scope $scope) use ($event, $expectedRequestContextData, $expectedUser): void { - $event = $scope->applyToEvent($event); + withScope(function (IsolationScope $scope) use ($event, $expectedRequestContextData, $expectedUser): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event); $this->assertNotNull($event); $this->assertSame($expectedRequestContextData, $event->getRequest()); diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index ab46c6a1b3..91bced84a2 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -11,7 +11,7 @@ use Sentry\EventHint; use Sentry\Integration\TransactionIntegration; use Sentry\SentrySdk; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use function Sentry\withScope; @@ -35,10 +35,10 @@ public function testSetupOnce(Event $event, bool $isIntegrationEnabled, ?EventHi ->method('getIntegration') ->willReturn($isIntegrationEnabled ? $integration : null); - SentrySdk::getCurrentHub()->bindClient($client); + SentrySdk::init($client); - withScope(function (Scope $scope) use ($event, $hint, $expectedTransaction): void { - $event = $scope->applyToEvent($event, $hint); + withScope(function (IsolationScope $scope) use ($event, $hint, $expectedTransaction): void { + $event = SentrySdk::getGlobalScope()->merge($scope)->applyToEvent($event, $hint); $this->assertNotNull($event); $this->assertSame($event->getTransaction(), $expectedTransaction); diff --git a/tests/Logs/LogsAggregatorTest.php b/tests/Logs/LogsAggregatorTest.php index 5278f5039d..02621a0bcd 100644 --- a/tests/Logs/LogsAggregatorTest.php +++ b/tests/Logs/LogsAggregatorTest.php @@ -7,10 +7,13 @@ use PHPUnit\Framework\TestCase; use Sentry\Client; use Sentry\ClientBuilder; +use Sentry\ClientInterface; +use Sentry\Event; use Sentry\Logs\LogLevel; use Sentry\Logs\LogsAggregator; +use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tests\StubTransport; use Sentry\Tracing\PropagationContext; @@ -35,8 +38,7 @@ public function testAttributes(array $attributes, array $expected): void 'enable_logs' => true, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -93,8 +95,7 @@ public function testMessageFormatting(string $message, array $values, string $ex 'enable_logs' => true, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -170,22 +171,19 @@ public function testAttributesAreAddedToLogMessage(): void 'server_name' => 'web-server-01', ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $hub->configureScope(static function (Scope $scope) { - $userDataBag = new UserDataBag(); - $userDataBag->setId('unique_id'); - $userDataBag->setEmail('foo@example.com'); - $userDataBag->setUsername('my_user'); - $scope->setUser($userDataBag); - }); + $userDataBag = new UserDataBag(); + $userDataBag->setId('unique_id'); + $userDataBag->setEmail('foo@example.com'); + $userDataBag->setUsername('my_user'); + SentrySdk::getIsolationScope()->setUser($userDataBag); $spanContext = new SpanContext(); $spanContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); $spanContext->setSpanId(new SpanId('566e3688a61d4bc8')); $span = new Span($spanContext); - $hub->setSpan($span); + SentrySdk::getIsolationScope()->setSpan($span); $aggregator = new LogsAggregator(); @@ -211,6 +209,31 @@ public function testAttributesAreAddedToLogMessage(): void $this->assertSame('my_user', $attributes->get('user.name')->getValue()); } + public function testGlobalScopeAttributesAreAddedToLogMessage(): void + { + $client = ClientBuilder::create([ + 'enable_logs' => true, + ])->getClient(); + + SentrySdk::init($client); + SentrySdk::getGlobalScope()->setUser(UserDataBag::createFromUserIdentifier('global-user')); + + $spanContext = new SpanContext(); + $spanContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); + $spanContext->setSpanId(new SpanId('566e3688a61d4bc8')); + SentrySdk::getIsolationScope()->setSpan(new Span($spanContext)); + + $aggregator = new LogsAggregator(); + $aggregator->add(LogLevel::info(), 'Test message'); + + $logs = $aggregator->all(); + $this->assertCount(1, $logs); + + $attributes = $logs[0]->attributes(); + $this->assertSame('global-user', $attributes->get('user.id')->getValue()); + $this->assertSame('566e3688a61d4bc8', $attributes->get('sentry.trace.parent_span_id')->getValue()); + } + public function testUserAttributesCanBeSetManuallyWithDefaultPiiOff(): void { $client = ClientBuilder::create([ @@ -218,16 +241,13 @@ public function testUserAttributesCanBeSetManuallyWithDefaultPiiOff(): void 'send_default_pii' => false, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $hub->configureScope(static function (Scope $scope) { - $userDataBag = new UserDataBag(); - $userDataBag->setId('unique_id'); - $userDataBag->setEmail('foo@example.com'); - $userDataBag->setUsername('my_user'); - $scope->setUser($userDataBag); - }); + $userDataBag = new UserDataBag(); + $userDataBag->setId('unique_id'); + $userDataBag->setEmail('foo@example.com'); + $userDataBag->setUsername('my_user'); + SentrySdk::getIsolationScope()->setUser($userDataBag); $aggregator = new LogsAggregator(); $aggregator->add(LogLevel::info(), 'User performed action'); @@ -252,8 +272,7 @@ public function testFlushesImmediatelyWhenThresholdIsReached(): void 'log_flush_threshold' => 2, ])->setTransport($transport)->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -271,6 +290,39 @@ public function testFlushesImmediatelyWhenThresholdIsReached(): void $this->assertSame('Second message', StubTransport::$events[0]->getLogs()[1]->getBody()); } + public function testFlushCapturesLogsWithProvidedClient(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'enable_logs' => true, + ])); + + $fallbackClient = $this->createMock(ClientInterface::class); + $fallbackClient->method('getOptions') + ->willReturn(new Options([ + 'enable_logs' => true, + ])); + $fallbackClient->expects($this->never()) + ->method('captureEvent'); + SentrySdk::init($fallbackClient); + + $aggregator = new LogsAggregator(); + $aggregator->add(LogLevel::info(), 'Test message'); + + $client->expects($this->once()) + ->method('captureEvent') + ->with( + $this->callback(function (Event $event): bool { + $this->assertCount(1, $event->getLogs()); + + return true; + }) + ); + + $aggregator->flush($client); + } + public function testDoesNotFlushImmediatelyWhenThresholdIsNull(): void { StubTransport::$events = []; @@ -281,8 +333,7 @@ public function testDoesNotFlushImmediatelyWhenThresholdIsNull(): void 'log_flush_threshold' => null, ])->setTransport($transport)->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $aggregator = new LogsAggregator(); @@ -303,8 +354,8 @@ public function testDoesNotUsePropagationContextSpanIdAsParentSpanIdWhenNoLocalS $propagationContext->setTraceId(new TraceId('771a43a4192642f0b136d5159a501700')); $propagationContext->setSpanId(new SpanId('1234567890abcdef')); - $hub = new Hub($client, new Scope($propagationContext)); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); $aggregator = new LogsAggregator(); $aggregator->add(LogLevel::info(), 'Test message'); @@ -325,8 +376,7 @@ public function testUsesExternalPropagationContextWhenNoLocalSpanExists(): void 'enable_logs' => true, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); Scope::registerExternalPropagationContext(static function (): array { return [ diff --git a/tests/Logs/LogsTest.php b/tests/Logs/LogsTest.php index 4dd361b564..a0a4053b39 100644 --- a/tests/Logs/LogsTest.php +++ b/tests/Logs/LogsTest.php @@ -13,7 +13,6 @@ use Sentry\Logs\LogLevel; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; @@ -36,8 +35,7 @@ public function testLogNotSentWhenDisabled(): void $client->expects($this->never()) ->method('captureEvent'); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); logger()->info('Some info message'); @@ -186,8 +184,7 @@ private function assertEvent(callable $assert, array $options = []): ClientInter $client = ClientBuilder::create($clientOptions)->setTransport($transport)->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); return $client; } diff --git a/tests/Metrics/TraceMetricsTest.php b/tests/Metrics/TraceMetricsTest.php index 1191e076ec..97ab7eebcb 100644 --- a/tests/Metrics/TraceMetricsTest.php +++ b/tests/Metrics/TraceMetricsTest.php @@ -6,14 +6,17 @@ use PHPUnit\Framework\TestCase; use Sentry\Client; +use Sentry\ClientInterface; +use Sentry\Event; use Sentry\Metrics\MetricsAggregator; use Sentry\Metrics\Types\CounterMetric; use Sentry\Metrics\Types\DistributionMetric; use Sentry\Metrics\Types\GaugeMetric; use Sentry\Metrics\Types\Metric; use Sentry\Options; -use Sentry\State\HubAdapter; +use Sentry\SentrySdk; use Sentry\State\Scope; +use Sentry\UserDataBag; use function Sentry\traceMetrics; @@ -21,7 +24,7 @@ final class TraceMetricsTest extends TestCase { protected function setUp(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options(), StubTransport::getInstance())); + SentrySdk::init(new Client(new Options(), StubTransport::getInstance())); StubTransport::$events = []; } @@ -75,7 +78,7 @@ public function testDistributionMetrics(): void public function testFlushesImmediatelyWhenMetricFlushThresholdIsReached(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'metric_flush_threshold' => 2, ]), StubTransport::getInstance())); @@ -95,7 +98,7 @@ public function testFlushesImmediatelyWhenMetricFlushThresholdIsReached(): void public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'metric_flush_threshold' => null, ]), StubTransport::getInstance())); @@ -110,9 +113,55 @@ public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): voi $this->assertCount(2, StubTransport::$events[0]->getMetrics()); } + public function testGlobalScopeAttributesAreAddedToMetric(): void + { + SentrySdk::getGlobalScope()->setUser(UserDataBag::createFromUserIdentifier('global-user')); + + traceMetrics()->count('test-count', 2); + traceMetrics()->flush(); + + $this->assertCount(1, StubTransport::$events); + + $metric = StubTransport::$events[0]->getMetrics()[0]; + $this->assertSame('global-user', $metric->getAttributes()->get('user.id')->getValue()); + } + + public function testFlushCapturesMetricsWithProvidedClient(): void + { + $client = $this->createMock(ClientInterface::class); + $client->method('getOptions') + ->willReturn(new Options([ + 'enable_metrics' => true, + ])); + + $fallbackClient = $this->createMock(ClientInterface::class); + $fallbackClient->method('getOptions') + ->willReturn(new Options([ + 'enable_metrics' => true, + ])); + $fallbackClient->expects($this->never()) + ->method('captureEvent'); + SentrySdk::init($fallbackClient); + + $aggregator = new MetricsAggregator(); + $aggregator->add(CounterMetric::TYPE, 'test-count', 2, ['foo' => 'bar'], null); + + $client->expects($this->once()) + ->method('captureEvent') + ->with( + $this->callback(function (Event $event): bool { + $this->assertCount(1, $event->getMetrics()); + + return true; + }) + ); + + $aggregator->flush($client); + } + public function testMetricsBufferFullWhenMetricFlushThresholdIsNull(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'metric_flush_threshold' => null, ]), StubTransport::getInstance())); @@ -131,7 +180,7 @@ public function testMetricsBufferFullWhenMetricFlushThresholdIsNull(): void public function testEnableMetrics(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'enable_metrics' => false, ]), StubTransport::getInstance())); @@ -143,7 +192,7 @@ public function testEnableMetrics(): void public function testBeforeSendMetricAltersContent(): void { - HubAdapter::getInstance()->bindClient(new Client(new Options([ + SentrySdk::init(new Client(new Options([ 'before_send_metric' => static function (Metric $metric) { $metric->setValue(99999); diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php index 812c5d12fd..e9f5765edd 100644 --- a/tests/Monolog/BreadcrumbHandlerTest.php +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -8,8 +8,11 @@ use Monolog\LogRecord; use PHPUnit\Framework\TestCase; use Sentry\Breadcrumb; +use Sentry\ClientInterface; +use Sentry\Event; use Sentry\Monolog\BreadcrumbHandler; -use Sentry\State\HubInterface; +use Sentry\Options; +use Sentry\SentrySdk; final class BreadcrumbHandlerTest extends TestCase { @@ -20,22 +23,29 @@ final class BreadcrumbHandlerTest extends TestCase */ public function testHandle($record, Breadcrumb $expectedBreadcrumb): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('addBreadcrumb') - ->with($this->callback(function (Breadcrumb $breadcrumb) use ($expectedBreadcrumb): bool { - $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); - $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); - $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); - $this->assertEquals($expectedBreadcrumb->getTimestamp(), $breadcrumb->getTimestamp()); - $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); - $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); - - return true; - })); - - $handler = new BreadcrumbHandler($hub); + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); + + SentrySdk::init($client); + + $handler = new BreadcrumbHandler(); $handler->handle($record); + + $event = Event::createEvent(); + SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); + + $this->assertCount(1, $event->getBreadcrumbs()); + + $breadcrumb = $event->getBreadcrumbs()[0]; + + $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); + $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); + $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); + $this->assertEquals($expectedBreadcrumb->getTimestamp(), $breadcrumb->getTimestamp()); + $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); + $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); } /** diff --git a/tests/Monolog/ExceptionToSentryIssueHandlerTest.php b/tests/Monolog/ExceptionToSentryIssueHandlerTest.php index 20dd681397..8d8beae655 100644 --- a/tests/Monolog/ExceptionToSentryIssueHandlerTest.php +++ b/tests/Monolog/ExceptionToSentryIssueHandlerTest.php @@ -11,8 +11,8 @@ use Sentry\ClientInterface; use Sentry\Event; use Sentry\Monolog\ExceptionToSentryIssueHandler; -use Sentry\State\Hub; -use Sentry\State\Scope; +use Sentry\SentrySdk; +use Sentry\State\IsolationScope; final class ExceptionToSentryIssueHandlerTest extends TestCase { @@ -30,8 +30,9 @@ public function testHandleCapturesExceptionAndAddsMetadata($record, \Throwable $ ->method('captureException') ->with( $this->identicalTo($exception), - $this->callback(function (Scope $scopeArg) use ($expectedExtra): bool { - $event = $scopeArg->applyToEvent(Event::createEvent()); + $this->callback(function (IsolationScope $scopeArg) use ($expectedExtra): bool { + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scopeArg)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame($expectedExtra, $event->getExtra()); @@ -41,7 +42,9 @@ public function testHandleCapturesExceptionAndAddsMetadata($record, \Throwable $ null ); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope())); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(); $this->assertTrue($handler->isHandling($record)); $handler->handle($record); @@ -55,9 +58,11 @@ public function testHandleReturnsFalseWhenBubblingEnabled(): void $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) ->method('captureException') - ->with($this->identicalTo($exception), $this->isInstanceOf(Scope::class), null); + ->with($this->identicalTo($exception), $this->isInstanceOf(IsolationScope::class), null); + + SentrySdk::init($client); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::WARNING); + $handler = new ExceptionToSentryIssueHandler(Logger::WARNING); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -80,9 +85,11 @@ public function testHandleReturnsTrueWhenBubblingDisabled(): void $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) ->method('captureException') - ->with($this->identicalTo($exception), $this->isInstanceOf(Scope::class), null); + ->with($this->identicalTo($exception), $this->isInstanceOf(IsolationScope::class), null); + + SentrySdk::init($client); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::WARNING, false); + $handler = new ExceptionToSentryIssueHandler(Logger::WARNING, false); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -109,7 +116,9 @@ public function testHandleIgnoresRecordsWithoutThrowable($record): void $client->expects($this->never()) ->method('captureException'); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, false); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(Logger::DEBUG, false); $this->assertTrue($handler->isHandling($record)); $this->assertFalse($handler->handle($record)); @@ -124,7 +133,9 @@ public function testHandleIgnoresRecordsBelowThreshold(): void $client->expects($this->never()) ->method('captureException'); - $handler = new ExceptionToSentryIssueHandler(new Hub($client, new Scope()), Logger::ERROR, false); + SentrySdk::init($client); + + $handler = new ExceptionToSentryIssueHandler(Logger::ERROR, false); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -145,7 +156,7 @@ public function testLegacyIsHandlingUsesMinimalLevelRecord(): void $this->markTestSkipped('Test only works for Monolog < 3'); } - $handler = new ExceptionToSentryIssueHandler(new Hub($this->createMock(ClientInterface::class), new Scope()), Logger::WARNING); + $handler = new ExceptionToSentryIssueHandler(Logger::WARNING); $this->assertTrue($handler->isHandling(['level' => Logger::WARNING])); $this->assertFalse($handler->isHandling(['level' => Logger::INFO])); diff --git a/tests/Monolog/LogToSentryIssueHandlerTest.php b/tests/Monolog/LogToSentryIssueHandlerTest.php index 7bbff023b4..087057039e 100644 --- a/tests/Monolog/LogToSentryIssueHandlerTest.php +++ b/tests/Monolog/LogToSentryIssueHandlerTest.php @@ -14,9 +14,9 @@ use Sentry\EventHint; use Sentry\Monolog\ExceptionToSentryIssueHandler; use Sentry\Monolog\LogToSentryIssueHandler; +use Sentry\SentrySdk; use Sentry\Severity; -use Sentry\State\Hub; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tests\StubTransport; final class LogToSentryIssueHandlerTest extends TestCase @@ -49,8 +49,9 @@ public function testHandleCapturesLogMessageAsIssue(bool $fillExtraContext, $rec return true; }), - $this->callback(function (Scope $scopeArg) use ($expectedExtra): bool { - $event = $scopeArg->applyToEvent(Event::createEvent()); + $this->callback(function (IsolationScope $scopeArg) use ($expectedExtra): bool { + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scopeArg)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame($expectedExtra, $event->getExtra()); @@ -59,7 +60,9 @@ public function testHandleCapturesLogMessageAsIssue(bool $fillExtraContext, $rec }) ); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, true, $fillExtraContext); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::DEBUG, true, $fillExtraContext); $this->assertTrue($handler->isHandling($record)); $this->assertFalse($handler->handle($record)); @@ -71,9 +74,11 @@ public function testHandleReturnsTrueWhenBubblingDisabled(): void $client = $this->createMock(ClientInterface::class); $client->expects($this->once()) ->method('captureEvent') - ->with($this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), $this->isInstanceOf(Scope::class)); + ->with($this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), $this->isInstanceOf(IsolationScope::class)); + + SentrySdk::init($client); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::WARNING, false); + $handler = new LogToSentryIssueHandler(Logger::WARNING, false); $record = RecordFactory::create('foo bar', Logger::WARNING, 'channel.foo', [], []); $this->assertTrue($handler->isHandling($record)); @@ -87,7 +92,9 @@ public function testHandleIgnoresRecordsWithThrowableExceptionContext(): void $client->expects($this->never()) ->method('captureEvent'); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, false); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::DEBUG, false); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -111,8 +118,9 @@ public function testHandleCapturesRecordsWithNonThrowableExceptionContext(): voi ->with( $this->isInstanceOf(Event::class), $this->isInstanceOf(EventHint::class), - $this->callback(function (Scope $scopeArg): bool { - $event = $scopeArg->applyToEvent(Event::createEvent()); + $this->callback(function (IsolationScope $scopeArg): bool { + $globalScope = SentrySdk::getGlobalScope(); + $event = $globalScope->merge($scopeArg)->applyToEvent(Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -127,7 +135,9 @@ public function testHandleCapturesRecordsWithNonThrowableExceptionContext(): voi }) ); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::DEBUG, false, true); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::DEBUG, false, true); $record = RecordFactory::create( 'foo bar', Logger::WARNING, @@ -149,7 +159,9 @@ public function testHandleIgnoresRecordsBelowThreshold(): void $client->expects($this->never()) ->method('captureEvent'); - $handler = new LogToSentryIssueHandler(new Hub($client, new Scope()), Logger::ERROR, false); + SentrySdk::init($client); + + $handler = new LogToSentryIssueHandler(Logger::ERROR, false); $record = RecordFactory::create('foo bar', Logger::WARNING, 'channel.foo', [], []); $this->assertFalse($handler->isHandling($record)); @@ -162,7 +174,7 @@ public function testLegacyIsHandlingUsesMinimalLevelRecord(): void $this->markTestSkipped('Test only works for Monolog < 3'); } - $handler = new LogToSentryIssueHandler(new Hub($this->createMock(ClientInterface::class), new Scope()), Logger::WARNING); + $handler = new LogToSentryIssueHandler(Logger::WARNING); $this->assertTrue($handler->isHandling(['level' => Logger::WARNING])); $this->assertFalse($handler->isHandling(['level' => Logger::INFO])); @@ -173,11 +185,11 @@ public function testLogAndExceptionIssueHandlersReplaceLegacyHandlerUseCases(): $client = ClientBuilder::create() ->setTransport(StubTransport::getInstance()) ->getClient(); - $hub = new Hub($client, new Scope()); + SentrySdk::init($client); $logger = new Logger('channel.foo', [ - new LogToSentryIssueHandler($hub, Logger::WARNING, true, true), - new ExceptionToSentryIssueHandler($hub, Logger::WARNING), + new LogToSentryIssueHandler(Logger::WARNING, true, true), + new ExceptionToSentryIssueHandler(Logger::WARNING), ]); $logger->warning('plain warning', [ diff --git a/tests/Monolog/LogsHandlerTest.php b/tests/Monolog/LogsHandlerTest.php index f3f2965cc3..0d94c68006 100644 --- a/tests/Monolog/LogsHandlerTest.php +++ b/tests/Monolog/LogsHandlerTest.php @@ -13,7 +13,6 @@ use Sentry\Logs\Logs; use Sentry\Monolog\LogsHandler; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tests\StubTransport; final class LogsHandlerTest extends TestCase @@ -28,8 +27,7 @@ protected function setUp(): void }, ])->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); } /** @@ -108,8 +106,7 @@ public function testLogsHandlerDestructor(): void ])->setTransport($transport) ->getClient(); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $this->handleLogAndDrop(); diff --git a/tests/SentrySdkExtension.php b/tests/SentrySdkExtension.php index b50bc2ed85..afe5ccb1a8 100644 --- a/tests/SentrySdkExtension.php +++ b/tests/SentrySdkExtension.php @@ -13,7 +13,7 @@ final class SentrySdkExtension implements BeforeTestHookInterface { public function executeBeforeTest(string $test): void { - $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'currentHub'); + $reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'globalScope'); if (\PHP_VERSION_ID < 80100) { $reflectionProperty->setAccessible(true); } diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index 3ce3d6f12d..911b8ee879 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -11,63 +11,129 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; +use Sentry\Tracing\TransactionContext; use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; +use function Sentry\startTransaction; + final class SentrySdkTest extends TestCase { - public function testInit(): void + public function testInitResetsRuntimeContext(): void { - $hub1 = SentrySdk::init(); - $hub2 = SentrySdk::getCurrentHub(); + $previousScope = SentrySdk::getIsolationScope(); + $previousScope->setTag('runtime', 'old'); + + SentrySdk::init(); + + $currentScope = SentrySdk::getIsolationScope(); - $this->assertSame($hub1, $hub2); - $this->assertNotSame(SentrySdk::init(), SentrySdk::init()); + $this->assertNotSame($previousScope, $currentScope); + + $event = SentrySdk::getGlobalScope()->merge($currentScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([], $event->getTags()); } - public function testGetCurrentHub(): void + public function testGetGlobalScope(): void { - SentrySdk::init(); + $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); - $hub2 = SentrySdk::getCurrentHub(); - $hub3 = SentrySdk::getCurrentHub(); + $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($hub2, $hub3); + $this->assertSame($client, SentrySdk::getClient()); } - public function testSetCurrentHub(): void + public function testInitDoesNotResetGlobalScope(): void { - $hub = new Hub(new NoOpClient()); + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setTag('baseline', 'yes'); + + SentrySdk::init(); - $this->assertSame($hub, SentrySdk::setCurrentHub($hub)); - $this->assertSame($hub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getGlobalScope()); + + $event = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['baseline' => 'yes'], $event->getTags()); } public function testStartAndEndContextIsolateScopeData(): void { SentrySdk::init(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { - $scope->setTag('baseline', 'yes'); - }); + SentrySdk::getIsolationScope()->setTag('baseline', 'yes'); SentrySdk::startContext(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { - $scope->setTag('request', 'yes'); - }); + SentrySdk::getIsolationScope()->setTag('request', 'yes'); SentrySdk::endContext(); $event = Event::createEvent(); - - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); $this->assertArrayHasKey('baseline', $event->getTags()); $this->assertArrayNotHasKey('request', $event->getTags()); @@ -78,16 +144,15 @@ public function testStartContextDoesNotInheritBaselineSpan(): void SentrySdk::init(); $baselineSpan = new Span(new SpanContext()); - SentrySdk::getCurrentHub()->setSpan($baselineSpan); + SentrySdk::getIsolationScope()->setSpan($baselineSpan); SentrySdk::startContext(); - $contextHub = SentrySdk::getCurrentHub(); - $this->assertNull($contextHub->getSpan()); + $this->assertNull(SentrySdk::getIsolationScope()->getSpan()); SentrySdk::endContext(); - $this->assertSame($baselineSpan, SentrySdk::getCurrentHub()->getSpan()); + $this->assertSame($baselineSpan, SentrySdk::getIsolationScope()->getSpan()); } public function testStartContextCreatesFreshPropagationContext(): void @@ -113,16 +178,16 @@ public function testWithContextResetsSpanAndTransactionAcrossInvocations(): void SentrySdk::init(); SentrySdk::withContext(function (): void { - $transaction = SentrySdk::getCurrentHub()->startTransaction(new \Sentry\Tracing\TransactionContext('request-1')); - SentrySdk::getCurrentHub()->setSpan($transaction); + $transaction = startTransaction(new TransactionContext('request-1')); + SentrySdk::getIsolationScope()->setSpan($transaction); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getSpan()); - $this->assertSame($transaction, SentrySdk::getCurrentHub()->getTransaction()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getSpan()); + $this->assertSame($transaction, SentrySdk::getIsolationScope()->getTransaction()); }); SentrySdk::withContext(function (): void { - $this->assertNull(SentrySdk::getCurrentHub()->getSpan()); - $this->assertNull(SentrySdk::getCurrentHub()->getTransaction()); + $this->assertNull(SentrySdk::getIsolationScope()->getSpan()); + $this->assertNull(SentrySdk::getIsolationScope()->getTransaction()); }); } @@ -130,22 +195,22 @@ public function testNestedStartContextIsNoOp(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); + $globalScope = SentrySdk::getIsolationScope(); SentrySdk::startContext(); - $firstContextHub = SentrySdk::getCurrentHub(); + $firstContextScope = SentrySdk::getIsolationScope(); SentrySdk::startContext(); - $secondContextHub = SentrySdk::getCurrentHub(); + $secondContextScope = SentrySdk::getIsolationScope(); - $this->assertNotSame($globalHub, $firstContextHub); - $this->assertSame($firstContextHub, $secondContextHub); + $this->assertNotSame($globalScope, $firstContextScope); + $this->assertSame($firstContextScope, $secondContextScope); SentrySdk::endContext(); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); SentrySdk::endContext(); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testEndContextFlushesClientTransportWithOptionalTimeout(): void @@ -160,7 +225,7 @@ public function testEndContextFlushesClientTransportWithOptionalTimeout(): void ->with(12) ->willReturn(new Result(ResultStatus::success())); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); SentrySdk::startContext(); SentrySdk::endContext(12); @@ -175,83 +240,79 @@ public function testFlushFlushesClientTransport(): void ->with(null) ->willReturn(new Result(ResultStatus::success())); - SentrySdk::init()->bindClient($client); + SentrySdk::init($client); SentrySdk::flush(); } - public function testWithContextReturnsCallbackResultAndRestoresGlobalHub(): void + public function testWithContextReturnsCallbackResultAndRestoresGlobalIsolationScope(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $callbackHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $callbackScope = null; - $result = SentrySdk::withContext(static function () use (&$callbackHub): string { - $callbackHub = SentrySdk::getCurrentHub(); + $result = SentrySdk::withContext(static function () use (&$callbackScope): string { + $callbackScope = SentrySdk::getIsolationScope(); return 'ok'; }); $this->assertSame('ok', $result); - $this->assertNotNull($callbackHub); - $this->assertNotSame($globalHub, $callbackHub); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotNull($callbackScope); + $this->assertNotSame($globalScope, $callbackScope); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testNestedWithContextReusesOuterContext(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $outerHub = null; - $innerHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $outerScope = null; + $innerScope = null; $outerContextId = null; $innerContextId = null; - SentrySdk::withContext(function () use (&$outerHub, &$innerHub, &$outerContextId, &$innerContextId, $globalHub): void { - $outerHub = SentrySdk::getCurrentHub(); + SentrySdk::withContext(function () use (&$outerScope, &$innerScope, &$outerContextId, &$innerContextId, $globalScope): void { + $outerScope = SentrySdk::getIsolationScope(); $outerContextId = SentrySdk::getCurrentRuntimeContext()->getId(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void { - $scope->setTag('outer', 'yes'); - }); + SentrySdk::getIsolationScope()->setTag('outer', 'yes'); - SentrySdk::withContext(static function () use (&$innerHub, &$innerContextId): void { - $innerHub = SentrySdk::getCurrentHub(); + SentrySdk::withContext(static function () use (&$innerScope, &$innerContextId): void { + $innerScope = SentrySdk::getIsolationScope(); $innerContextId = SentrySdk::getCurrentRuntimeContext()->getId(); }); $event = Event::createEvent(); - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use (&$event): void { - $event = $scope->applyToEvent($event); - }); + $event = SentrySdk::getGlobalScope()->merge(SentrySdk::getIsolationScope())->applyToEvent($event); - $this->assertNotSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotSame($globalScope, SentrySdk::getIsolationScope()); $this->assertSame('yes', $event->getTags()['outer'] ?? null); $this->assertSame($outerContextId, SentrySdk::getCurrentRuntimeContext()->getId()); }); - $this->assertNotNull($outerHub); - $this->assertNotNull($innerHub); + $this->assertNotNull($outerScope); + $this->assertNotNull($innerScope); $this->assertNotNull($outerContextId); $this->assertNotNull($innerContextId); - $this->assertSame($outerHub, $innerHub); + $this->assertSame($outerScope, $innerScope); $this->assertSame($outerContextId, $innerContextId); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } public function testWithContextEndsContextWhenCallbackThrows(): void { SentrySdk::init(); - $globalHub = SentrySdk::getCurrentHub(); - $callbackHub = null; + $globalScope = SentrySdk::getIsolationScope(); + $callbackScope = null; try { - SentrySdk::withContext(static function () use (&$callbackHub): void { - $callbackHub = SentrySdk::getCurrentHub(); + SentrySdk::withContext(static function () use (&$callbackScope): void { + $callbackScope = SentrySdk::getIsolationScope(); throw new \RuntimeException('boom'); }); @@ -261,18 +322,16 @@ public function testWithContextEndsContextWhenCallbackThrows(): void $this->assertSame('boom', $exception->getMessage()); } - $this->assertNotNull($callbackHub); - $this->assertNotSame($globalHub, $callbackHub); - $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + $this->assertNotNull($callbackScope); + $this->assertNotSame($globalScope, $callbackScope); + $this->assertSame($globalScope, SentrySdk::getIsolationScope()); } private function getCurrentScopeTraceparent(): string { $traceparent = ''; - SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use (&$traceparent): void { - $traceparent = $scope->getPropagationContext()->toTraceparent(); - }); + $traceparent = SentrySdk::getIsolationScope()->getPropagationContext()->toTraceparent(); return $traceparent; } diff --git a/tests/State/BreadcrumbRecorderTest.php b/tests/State/BreadcrumbRecorderTest.php new file mode 100644 index 0000000000..a61ef479f4 --- /dev/null +++ b/tests/State/BreadcrumbRecorderTest.php @@ -0,0 +1,133 @@ +createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options()); + + $this->assertTrue(BreadcrumbRecorder::record($client, $scope, $breadcrumb)); + $this->assertScopeBreadcrumbs($scope, [$breadcrumb]); + $this->assertScopeBreadcrumbs($otherScope, []); + } + + public function testRecordReturnsFalseForNoOpClient(): void + { + $scope = new IsolationScope(); + + $this->assertFalse(BreadcrumbRecorder::record( + new NoOpClient(), + $scope, + new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting') + )); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testRecordReturnsFalseWhenMaxBreadcrumbsLimitIsZero(): void + { + $scope = new IsolationScope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['max_breadcrumbs' => 0])); + + $this->assertFalse(BreadcrumbRecorder::record( + $client, + $scope, + new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting') + )); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testRecordReturnsFalseWhenBeforeBreadcrumbCallbackReturnsNull(): void + { + $scope = new IsolationScope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () { + return null; + }, + ])); + + $this->assertFalse(BreadcrumbRecorder::record( + $client, + $scope, + new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting') + )); + $this->assertScopeBreadcrumbs($scope, []); + } + + public function testRecordStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): void + { + $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_DEFAULT, 'custom'); + $scope = new IsolationScope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'before_breadcrumb' => static function () use ($breadcrumb2): Breadcrumb { + return $breadcrumb2; + }, + ])); + + $this->assertTrue(BreadcrumbRecorder::record($client, $scope, $breadcrumb1)); + $this->assertScopeBreadcrumbs($scope, [$breadcrumb2]); + } + + public function testRecordRespectsMaxBreadcrumbsLimit(): void + { + $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'one'); + $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'two'); + $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'three'); + $scope = new IsolationScope(); + $client = $this->createMock(ClientInterface::class); + + $client->expects($this->exactly(3)) + ->method('getOptions') + ->willReturn(new Options(['max_breadcrumbs' => 2])); + + BreadcrumbRecorder::record($client, $scope, $breadcrumb1); + BreadcrumbRecorder::record($client, $scope, $breadcrumb2); + BreadcrumbRecorder::record($client, $scope, $breadcrumb3); + + $this->assertScopeBreadcrumbs($scope, [$breadcrumb2, $breadcrumb3]); + } + + /** + * @param Breadcrumb[] $expectedBreadcrumbs + */ + private function assertScopeBreadcrumbs(IsolationScope $scope, array $expectedBreadcrumbs): void + { + $event = (new GlobalScope())->merge($scope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame($expectedBreadcrumbs, $event->getBreadcrumbs()); + } +} diff --git a/tests/State/EventRecorderTest.php b/tests/State/EventRecorderTest.php new file mode 100644 index 0000000000..b26d5b14bb --- /dev/null +++ b/tests/State/EventRecorderTest.php @@ -0,0 +1,219 @@ +createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureMessage') + ->with('foo', Severity::debug(), $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); + }), $hint) + ->willReturn($eventId); + + $this->assertSame($eventId, EventRecorder::captureMessage('foo', Severity::debug(), $hint, $isolationScope)); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureExceptionPassesIsolationScopeAndStoresLastEventId(): void + { + $eventId = EventId::generate(); + $exception = new \RuntimeException('foo'); + $hint = new EventHint(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureException') + ->with($exception, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); + }), $hint) + ->willReturn($eventId); + + $this->assertSame($eventId, EventRecorder::captureException($exception, $hint, $isolationScope)); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureEventPassesIsolationScopeAndStoresLastEventId(): void + { + $event = Event::createEvent(); + $hint = new EventHint(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($event, $hint, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); + })) + ->willReturn($event->getId()); + + $this->assertSame($event->getId(), EventRecorder::captureEvent($event, $hint, $isolationScope)); + $this->assertSame($event->getId(), SentrySdk::getLastEventId()); + } + + public function testCaptureLastErrorPassesIsolationScopeAndStoresLastEventId(): void + { + $eventId = EventId::generate(); + $hint = new EventHint(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('captureLastError') + ->with($this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); + }), $hint) + ->willReturn($eventId); + + $this->assertSame($eventId, EventRecorder::captureLastError($hint, $isolationScope)); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureEventClearsLastEventIdWhenClientReturnsNull(): void + { + $event = Event::createEvent(); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + SentrySdk::getCurrentRuntimeContext()->setLastEventId(EventId::generate()); + + $client->expects($this->once()) + ->method('captureEvent') + ->with($event, null, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); + })) + ->willReturn(null); + + $this->assertNull(EventRecorder::captureEvent($event)); + $this->assertNull(SentrySdk::getLastEventId()); + } + + public function testCaptureCheckInCreatesEventAndStoresLastEventId(): void + { + $checkInId = SentryUid::generate(); + $eventId = EventId::generate(); + $monitorConfig = new MonitorConfig( + MonitorSchedule::crontab('*/5 * * * *'), + 5, + 30, + 'UTC' + ); + $client = $this->createMock(ClientInterface::class); + $isolationScope = $this->setClientAndIsolationScope($client); + + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'environment' => Event::DEFAULT_ENVIRONMENT, + 'release' => '1.0.0', + ])); + $client->expects($this->once()) + ->method('captureEvent') + ->with($this->callback(static function (Event $event) use ($checkInId, $monitorConfig): bool { + $checkIn = $event->getCheckIn(); + + return $checkIn !== null + && $checkIn->getId() === $checkInId + && $checkIn->getMonitorSlug() === 'test-crontab' + && $checkIn->getStatus() == CheckInStatus::ok() + && $checkIn->getRelease() === '1.0.0' + && $checkIn->getEnvironment() === Event::DEFAULT_ENVIRONMENT + && $checkIn->getDuration() === 10 + && $checkIn->getMonitorConfig() === $monitorConfig; + }), null, $this->callback(function (IsolationScope $scope) use ($isolationScope): bool { + return $this->isCaptureScope($scope, $isolationScope); + })) + ->willReturn($eventId); + + $this->assertSame($checkInId, EventRecorder::captureCheckIn( + 'test-crontab', + CheckInStatus::ok(), + 10, + $monitorConfig, + $checkInId, + $isolationScope + )); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureCheckInReturnsNullForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + $eventId = EventId::generate(); + SentrySdk::getCurrentRuntimeContext()->setLastEventId($eventId); + + $this->assertNull(EventRecorder::captureCheckIn('test-crontab', CheckInStatus::ok())); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + public function testCaptureDoesNotClearLastEventIdForNoOpClient(): void + { + SentrySdk::init(new NoOpClient()); + $eventId = EventId::generate(); + SentrySdk::getCurrentRuntimeContext()->setLastEventId($eventId); + + $this->assertNull(EventRecorder::captureMessage('foo')); + $this->assertNull(EventRecorder::captureException(new \RuntimeException('foo'))); + $this->assertNull(EventRecorder::captureEvent(Event::createEvent())); + $this->assertNull(EventRecorder::captureLastError()); + $this->assertSame($eventId, SentrySdk::getLastEventId()); + } + + private function setClientAndIsolationScope(ClientInterface $client): IsolationScope + { + SentrySdk::init(); + + SentrySdk::getGlobalScope()->clear(); + SentrySdk::getGlobalScope()->setTag('scope', 'global'); + SentrySdk::getGlobalScope()->setTag('global', 'yes'); + SentrySdk::getGlobalScope()->setClient($client); + + $scope = new IsolationScope(); + $scope->setTag('scope', 'isolation'); + $scope->setTag('isolation', 'yes'); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($scope); + + return $scope; + } + + private function isCaptureScope(IsolationScope $captureScope, IsolationScope $isolationScope): bool + { + $this->assertSame($isolationScope, $captureScope); + + $event = SentrySdk::getGlobalScope()->merge($captureScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([ + 'scope' => 'isolation', + 'global' => 'yes', + 'isolation' => 'yes', + ], $event->getTags()); + + return true; + } +} diff --git a/tests/State/HubAdapterTest.php b/tests/State/HubAdapterTest.php deleted file mode 100644 index b07c0de26d..0000000000 --- a/tests/State/HubAdapterTest.php +++ /dev/null @@ -1,394 +0,0 @@ -assertSame(HubAdapter::getInstance(), HubAdapter::getInstance()); - } - - public function testGetInstanceReturnsUncloneableInstance(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Cloning is forbidden.'); - - clone HubAdapter::getInstance(); - } - - public function testHubAdapterThrowsExceptionOnSerialization(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Serializing instances of this class is forbidden.'); - - serialize(HubAdapter::getInstance()); - } - - public function testHubAdapterThrowsExceptionOnUnserialization(): void - { - $this->expectException(\BadMethodCallException::class); - $this->expectExceptionMessage('Unserializing instances of this class is forbidden.'); - - unserialize('O:23:"Sentry\State\HubAdapter":0:{}'); - } - - public function testGetClient(): void - { - $client = $this->createMock(ClientInterface::class); - - SentrySdk::getCurrentHub()->bindClient($client); - - $this->assertSame($client, HubAdapter::getInstance()->getClient()); - } - - public function testGetLastEventId(): void - { - $eventId = EventId::generate(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getLastEventId') - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->getLastEventId()); - } - - public function testPushScope(): void - { - $scope = new Scope(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('pushScope') - ->willReturn($scope); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($scope, HubAdapter::getInstance()->pushScope()); - } - - public function testPopScope(): void - { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('popScope') - ->willReturn(true); - - SentrySdk::setCurrentHub($hub); - - $this->assertTrue(HubAdapter::getInstance()->popScope()); - } - - public function testWithScope(): void - { - $callback = static function (): string { - return 'foobarbaz'; - }; - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('withScope') - ->with($callback) - ->willReturnCallback($callback); - - SentrySdk::setCurrentHub($hub); - - $returnValue = HubAdapter::getInstance()->withScope($callback); - - $this->assertSame('foobarbaz', $returnValue); - } - - public function testConfigureScope(): void - { - $callback = static function () {}; - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('configureScope') - ->with($callback); - - SentrySdk::setCurrentHub($hub); - HubAdapter::getInstance()->configureScope($callback); - } - - public function testBindClient(): void - { - $client = $this->createMock(ClientInterface::class); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('bindClient') - ->with($client); - - SentrySdk::setCurrentHub($hub); - HubAdapter::getInstance()->bindClient($client); - } - - /** - * @dataProvider captureMessageDataProvider - */ - public function testCaptureMessage(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureMessage(...$expectedFunctionCallArgs)); - } - - public static function captureMessageDataProvider(): \Generator - { - yield [ - [ - 'foo', - Severity::debug(), - ], - ]; - - yield [ - [ - 'foo', - Severity::debug(), - new EventHint(), - ], - ]; - } - - /** - * @dataProvider captureExceptionDataProvider - */ - public function testCaptureException(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - $exception = new \Exception(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureException') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureException(...$expectedFunctionCallArgs)); - } - - public static function captureExceptionDataProvider(): \Generator - { - yield [ - [ - new \Exception('foo'), - ], - ]; - - yield [ - [ - new \Exception('foo'), - new EventHint(), - ], - ]; - } - - public function testCaptureEvent(): void - { - $event = Event::createEvent(); - $hint = EventHint::fromArray([]); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureEvent') - ->with($event, $hint) - ->willReturn($event->getId()); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($event->getId(), HubAdapter::getInstance()->captureEvent($event, $hint)); - } - - /** - * @dataProvider captureLastErrorDataProvider - */ - public function testCaptureLastError(array $expectedFunctionCallArgs): void - { - $eventId = EventId::generate(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($eventId, HubAdapter::getInstance()->captureLastError(...$expectedFunctionCallArgs)); - } - - public static function captureLastErrorDataProvider(): \Generator - { - yield [ - [], - ]; - - yield [ - [ - new EventHint(), - ], - ]; - } - - public function testCaptureCheckIn(): void - { - $hub = new Hub(new NoOpClient()); - - $options = new Options([ - 'environment' => Event::DEFAULT_ENVIRONMENT, - 'release' => '1.1.8', - ]); - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn($options); - - $hub->bindClient($client); - SentrySdk::setCurrentHub($hub); - - $checkInId = SentryUid::generate(); - - $this->assertSame($checkInId, HubAdapter::getInstance()->captureCheckIn( - 'test-crontab', - CheckInStatus::ok(), - 10, - new MonitorConfig( - MonitorSchedule::crontab('*/5 * * * *'), - 5, - 30, - 'UTC' - ), - $checkInId - )); - } - - public function testAddBreadcrumb(): void - { - $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_DEBUG, Breadcrumb::TYPE_ERROR, 'user'); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('addBreadcrumb') - ->willReturn(true); - - SentrySdk::setCurrentHub($hub); - - $this->assertTrue(HubAdapter::getInstance()->addBreadcrumb($breadcrumb)); - } - - public function testGetIntegration(): void - { - $integration = $this->createMock(IntegrationInterface::class); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getIntegration') - ->with(\get_class($integration)) - ->willReturn($integration); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($integration, HubAdapter::getInstance()->getIntegration(\get_class($integration))); - } - - public function testStartTransaction(): void - { - $transactionContext = new TransactionContext(); - $transaction = new Transaction($transactionContext); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('startTransaction') - ->with($transactionContext) - ->willReturn($transaction); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($transaction, HubAdapter::getInstance()->startTransaction($transactionContext)); - } - - public function testGetTransaction(): void - { - $transaction = new Transaction(new TransactionContext()); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getTransaction') - ->willReturn($transaction); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($transaction, HubAdapter::getInstance()->getTransaction()); - } - - public function testGetSpan(): void - { - $span = new Span(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getSpan') - ->willReturn($span); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($span, HubAdapter::getInstance()->getSpan()); - } - - public function testSetSpan(): void - { - $span = new Span(); - - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('setSpan') - ->with($span) - ->willReturn($hub); - - SentrySdk::setCurrentHub($hub); - - $this->assertSame($hub, HubAdapter::getInstance()->setSpan($span)); - } -} diff --git a/tests/State/HubTest.php b/tests/State/HubTest.php deleted file mode 100644 index 972ce617e7..0000000000 --- a/tests/State/HubTest.php +++ /dev/null @@ -1,1055 +0,0 @@ -createMock(ClientInterface::class); - $hub = new Hub($client); - - $this->assertSame($client, $hub->getClient()); - } - - public function testGetScope(): void - { - $callbackInvoked = false; - $scope = new Scope(); - $hub = new Hub($this->createMock(ClientInterface::class), $scope); - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope) { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testGetLastEventId(): void - { - $eventId = EventId::generate(); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureMessage') - ->willReturn($eventId); - - $hub = new Hub($client); - - $this->assertNull($hub->getLastEventId()); - $this->assertSame($hub->captureMessage('foo'), $hub->getLastEventId()); - $this->assertSame($eventId, $hub->getLastEventId()); - } - - public function testPushScope(): void - { - /** @var ClientInterface&MockObject $client1 */ - $client1 = $this->createMock(ClientInterface::class); - $scope1 = new Scope(); - $hub = new Hub($client1, $scope1); - - $this->assertSame($client1, $hub->getClient()); - - $scope2 = $hub->pushScope(); - - $this->assertSame($client1, $hub->getClient()); - $this->assertNotSame($scope1, $scope2); - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope2): void { - $this->assertSame($scope2, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testPopScope(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $scope1 = new Scope(); - $hub = new Hub($client, $scope1); - - $this->assertFalse($hub->popScope()); - - $scope2 = $hub->pushScope(); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope2, &$callbackInvoked): void { - $this->assertSame($scope2, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - $this->assertSame($client, $hub->getClient()); - - $this->assertTrue($hub->popScope()); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope1, &$callbackInvoked): void { - $this->assertSame($scope1, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - $this->assertSame($client, $hub->getClient()); - - $this->assertFalse($hub->popScope()); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope1, &$callbackInvoked): void { - $this->assertSame($scope1, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - $this->assertSame($client, $hub->getClient()); - } - - public function testWithScope(): void - { - $scope = new Scope(); - $hub = new Hub(new NoOpClient(), $scope); - - $callbackReturn = $hub->withScope(function (Scope $scopeArg) use ($scope): string { - $this->assertNotSame($scope, $scopeArg); - - return 'foobarbaz'; - }); - - $this->assertSame('foobarbaz', $callbackReturn); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope): void { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testWithScopeWhenExceptionIsThrown(): void - { - $scope = new Scope(); - $hub = new Hub($this->createMock(ClientInterface::class), $scope); - $callbackInvoked = false; - - try { - $hub->withScope(function (Scope $scopeArg) use ($scope, &$callbackInvoked): void { - $this->assertNotSame($scope, $scopeArg); - - $callbackInvoked = true; - - // We throw to test that the scope is correctly popped form the - // stack regardless - throw new \RuntimeException(); - }); - } catch (\RuntimeException $exception) { - // Do nothing, we catch this exception to not make the test fail - } - - $this->assertTrue($callbackInvoked); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use (&$callbackInvoked, $scope): void { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testConfigureScope(): void - { - $scope = new Scope(); - $hub = new Hub(new NoOpClient(), $scope); - - $callbackInvoked = false; - - $hub->configureScope(function (Scope $scopeArg) use ($scope, &$callbackInvoked): void { - $this->assertSame($scope, $scopeArg); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testBindClient(): void - { - /** @var ClientInterface&MockObject $client1 */ - $client1 = $this->createMock(ClientInterface::class); - - /** @var ClientInterface&MockObject $client2 */ - $client2 = $this->createMock(ClientInterface::class); - - $hub = new Hub($client1); - - $this->assertSame($client1, $hub->getClient()); - - $hub->bindClient($client2); - - $this->assertSame($client2, $hub->getClient()); - } - - /** - * @dataProvider captureMessageDataProvider - */ - public function testCaptureMessage(array $functionCallArgs, array $expectedFunctionCallArgs, PropagationContext $propagationContext): void - { - $eventId = EventId::generate(); - $hub = new Hub(new NoOpClient()); - - $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureMessage') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - $this->assertNull($hub->captureMessage('foo')); - - $hub->bindClient($client); - - $this->assertSame($eventId, $hub->captureMessage(...$functionCallArgs)); - } - - public static function captureMessageDataProvider(): \Generator - { - $propagationContext = PropagationContext::fromDefaults(); - - yield [ - [ - 'foo', - Severity::debug(), - ], - [ - 'foo', - Severity::debug(), - new Scope($propagationContext), - ], - $propagationContext, - ]; - - yield [ - [ - 'foo', - Severity::debug(), - new EventHint(), - ], - [ - 'foo', - Severity::debug(), - new Scope($propagationContext), - new EventHint(), - ], - $propagationContext, - ]; - } - - /** - * @dataProvider captureExceptionDataProvider - */ - public function testCaptureException(array $functionCallArgs, array $expectedFunctionCallArgs, PropagationContext $propagationContext): void - { - $eventId = EventId::generate(); - $hub = new Hub(new NoOpClient()); - - $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureException') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - $this->assertNull($hub->captureException(new \RuntimeException())); - - $hub->bindClient($client); - - $this->assertSame($eventId, $hub->captureException(...$functionCallArgs)); - } - - public static function captureExceptionDataProvider(): \Generator - { - $propagationContext = PropagationContext::fromDefaults(); - - yield [ - [ - new \Exception('foo'), - ], - [ - new \Exception('foo'), - new Scope($propagationContext), - null, - ], - $propagationContext, - ]; - - yield [ - [ - new \Exception('foo'), - new EventHint(), - ], - [ - new \Exception('foo'), - new Scope($propagationContext), - new EventHint(), - ], - $propagationContext, - ]; - } - - /** - * @dataProvider captureLastErrorDataProvider - */ - public function testCaptureLastError(array $functionCallArgs, array $expectedFunctionCallArgs, PropagationContext $propagationContext): void - { - $eventId = EventId::generate(); - $hub = new Hub(new NoOpClient()); - - $hub->configureScope(static function (Scope $scope) use ($propagationContext): void { - $scope->setPropagationContext($propagationContext); - }); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureLastError') - ->with(...$expectedFunctionCallArgs) - ->willReturn($eventId); - - $this->assertNull($hub->captureLastError(...$functionCallArgs)); - - $hub->bindClient($client); - - $this->assertSame($eventId, $hub->captureLastError(...$functionCallArgs)); - } - - public static function captureLastErrorDataProvider(): \Generator - { - $propagationContext = PropagationContext::fromDefaults(); - - yield [ - [], - [ - new Scope($propagationContext), - null, - ], - $propagationContext, - ]; - - yield [ - [ - new EventHint(), - ], - [ - new Scope($propagationContext), - new EventHint(), - ], - $propagationContext, - ]; - } - - public function testCaptureCheckIn(): void - { - $expectedCheckIn = new CheckIn( - 'test-crontab', - CheckInStatus::ok(), - SentryUid::generate(), - '0.0.1-dev', - Event::DEFAULT_ENVIRONMENT, - 10, - new MonitorConfig( - MonitorSchedule::crontab('*/5 * * * *'), - 5, - 30, - 'UTC' - ) - ); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'environment' => Event::DEFAULT_ENVIRONMENT, - 'release' => '0.0.1-dev', - ])); - - $client->expects($this->once()) - ->method('captureEvent') - ->with($this->callback(static function (Event $event) use ($expectedCheckIn): bool { - return $event->getCheckIn() == $expectedCheckIn; - })); - - $hub = new Hub($client); - - $this->assertSame($expectedCheckIn->getId(), $hub->captureCheckIn( - $expectedCheckIn->getMonitorSlug(), - $expectedCheckIn->getStatus(), - $expectedCheckIn->getDuration(), - $expectedCheckIn->getMonitorConfig(), - $expectedCheckIn->getId() - )); - } - - public function testCaptureEvent(): void - { - $hub = new Hub(new NoOpClient()); - $event = Event::createEvent(); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('captureEvent') - ->with($event) - ->willReturn($event->getId()); - - $this->assertNull($hub->captureEvent($event)); - - $hub->bindClient($client); - - $this->assertSame($event->getId(), $hub->captureEvent($event)); - } - - public function testAddBreadcrumb(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - $callbackInvoked = false; - $hub = new Hub(new NoOpClient()); - $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - - $hub->addBreadcrumb($breadcrumb); - $hub->configureScope(function (Scope $scope): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertEmpty($event->getBreadcrumbs()); - }); - - $hub->bindClient($client); - $hub->addBreadcrumb($breadcrumb); - $hub->configureScope(function (Scope $scope) use (&$callbackInvoked, $breadcrumb): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb], $event->getBreadcrumbs()); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testAddBreadcrumbDoesNothingIfMaxBreadcrumbsLimitIsZero(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['max_breadcrumbs' => 0])); - - $hub = new Hub($client); - - $hub->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); - $hub->configureScope(function (Scope $scope): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertEmpty($event->getBreadcrumbs()); - }); - } - - public function testAddBreadcrumbRespectsMaxBreadcrumbsLimit(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->any()) - ->method('getOptions') - ->willReturn(new Options(['max_breadcrumbs' => 2])); - - $hub = new Hub($client); - $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_ERROR, 'error_reporting', 'foo'); - $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_ERROR, 'error_reporting', 'bar'); - $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_WARNING, Breadcrumb::TYPE_ERROR, 'error_reporting', 'baz'); - - $hub->addBreadcrumb($breadcrumb1); - $hub->addBreadcrumb($breadcrumb2); - - $hub->configureScope(function (Scope $scope) use ($breadcrumb1, $breadcrumb2): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb1, $breadcrumb2], $event->getBreadcrumbs()); - }); - - $hub->addBreadcrumb($breadcrumb3); - - $hub->configureScope(function (Scope $scope) use ($breadcrumb2, $breadcrumb3): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb2, $breadcrumb3], $event->getBreadcrumbs()); - }); - } - - public function testAddBreadcrumbDoesNothingWhenBeforeBreadcrumbCallbackReturnsNull(): void - { - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'before_breadcrumb' => static function () { - return null; - }, - ])); - - $hub = new Hub($client); - - $hub->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); - $hub->configureScope(function (Scope $scope): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertEmpty($event->getBreadcrumbs()); - }); - } - - public function testAddBreadcrumbStoresBreadcrumbReturnedByBeforeBreadcrumbCallback(): void - { - $callbackInvoked = false; - $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'before_breadcrumb' => static function () use ($breadcrumb2): Breadcrumb { - return $breadcrumb2; - }, - ])); - - $hub = new Hub($client); - - $hub->addBreadcrumb($breadcrumb1); - $hub->configureScope(function (Scope $scope) use (&$callbackInvoked, $breadcrumb2): void { - $event = $scope->applyToEvent(Event::createEvent()); - - $this->assertNotNull($event); - $this->assertSame([$breadcrumb2], $event->getBreadcrumbs()); - - $callbackInvoked = true; - }); - - $this->assertTrue($callbackInvoked); - } - - public function testGetIntegration(): void - { - /** @var IntegrationInterface&MockObject $integration */ - $integration = $this->createMock(IntegrationInterface::class); - - /** @var ClientInterface&MockObject $client */ - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getIntegration') - ->with('Foo\\Bar') - ->willReturn($integration); - - $hub = new Hub(new NoOpClient()); - - $this->assertNull($hub->getIntegration('Foo\\Bar')); - - $hub->bindClient($client); - - $this->assertSame($integration, $hub->getIntegration('Foo\\Bar')); - } - - /** - * @dataProvider startTransactionDataProvider - */ - public function testStartTransactionWithTracesSampler(Options $options, TransactionContext $transactionContext, bool $expectedSampled): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn($options); - - $hub = new Hub($client); - $transaction = $hub->startTransaction($transactionContext); - - $this->assertSame($expectedSampled, $transaction->getSampled()); - } - - public function testStartTransactionIgnoresBaggageSampleRateWithoutSentryTrace(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 0.0, - ])); - - $hub = new Hub($client); - $transactionContext = TransactionContext::fromHeaders('', 'sentry-sample_rate=1'); - $transaction = $hub->startTransaction($transactionContext); - - $this->assertFalse($transaction->getSampled()); - } - - public static function startTransactionDataProvider(): iterable - { - yield 'Acceptable float value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): float { - return 1.0; - }, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low float value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): float { - return 0.0; - }, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable integer value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): int { - return 1; - }, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low integer value returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): int { - return 0; - }, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable float value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 1.0, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low float value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 0.0, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable integer value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 1, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable but too low integer value returned from traces_sample_rate' => [ - new Options([ - 'traces_sample_rate' => 0, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable but too low value returned from traces_sample_rate which is preferred over sample_rate' => [ - new Options([ - 'sample_rate' => 1.0, - 'traces_sample_rate' => 0.0, - ]), - new TransactionContext(), - false, - ]; - - yield 'Acceptable value returned from traces_sample_rate which is preferred over sample_rate' => [ - new Options([ - 'sample_rate' => 0.0, - 'traces_sample_rate' => 1.0, - ]), - new TransactionContext(), - true, - ]; - - yield 'Acceptable value returned from SamplingContext::getParentSampled() which is preferred over traces_sample_rate (x1)' => [ - new Options([ - 'traces_sample_rate' => 0.5, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, true), - true, - ]; - - yield 'Acceptable value returned from SamplingContext::getParentSampled() which is preferred over traces_sample_rate (x2)' => [ - new Options([ - 'traces_sample_rate' => 1.0, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - - yield 'Invalid incoming sample_rand is ignored' => [ - new Options([ - 'traces_sample_rate' => 1.0, - ]), - TransactionContext::fromHeaders( - '566e3688a61d4bc888951642d6f14a19-566e3688a61d4bc8', - 'sentry-sample_rand=2.0' - ), - true, - ]; - - yield 'Out of range sample rate returned from traces_sampler (lower than minimum)' => [ - new Options([ - 'traces_sampler' => static function (): float { - return -1.0; - }, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - - yield 'Out of range sample rate returned from traces_sampler (greater than maximum)' => [ - new Options([ - 'traces_sampler' => static function (): float { - return 1.1; - }, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - - yield 'Invalid type returned from traces_sampler' => [ - new Options([ - 'traces_sampler' => static function (): string { - return 'foo'; - }, - ]), - new TransactionContext(TransactionContext::DEFAULT_NAME, false), - false, - ]; - } - - public function testStartTransactionDoesNothingIfTracingIsNotEnabled(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options()); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertFalse($transaction->getSampled()); - } - - public function testStartTransactionWithCustomSamplingContext(): void - { - $customSamplingContext = ['a' => 'b']; - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { - $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); - - return 1.0; - }, - ])); - - $hub = new Hub($client); - $hub->startTransaction(new TransactionContext(), $customSamplingContext); - } - - public function testStartTransactionStartsProfilerWithProfilesSampler(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->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()); - $this->assertNotNull($transaction->getProfiler()); - } - - public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerReturnsZero(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 0.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionPrefersProfilesSamplerOverProfilesSampleRate(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sample_rate' => 1.0, - 'profiles_sampler' => static function (): float { - return 0.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionWithProfilesSamplerReceivesCustomSamplingContext(): void - { - $customSamplingContext = ['a' => 'b']; - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => function (SamplingContext $samplingContext) use ($customSamplingContext): float { - $this->assertSame($samplingContext->getAdditionalContext(), $customSamplingContext); - - return 0.0; - }, - ])); - - $hub = new Hub($client); - $hub->startTransaction(new TransactionContext(), $customSamplingContext); - } - - public function testStartTransactionDoesNotStartProfilerWhenProfilesSamplerReturnsInvalidValue(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 1.0, - 'profiles_sampler' => static function (): string { - return 'foo'; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertTrue($transaction->getSampled()); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionDoesNotCallProfilesSamplerWhenTransactionIsNotSampled(): void - { - $profilesSamplerInvoked = false; - - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sample_rate' => 0.0, - 'profiles_sampler' => static function () use (&$profilesSamplerInvoked): float { - $profilesSamplerInvoked = true; - - return 1.0; - }, - ])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext()); - - $this->assertFalse($transaction->getSampled()); - $this->assertFalse($profilesSamplerInvoked); - $this->assertNull($transaction->getProfiler()); - } - - public function testStartTransactionUpdatesTheDscSampleRate(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options([ - 'traces_sampler' => static function (SamplingContext $samplingContext): float { - return 1.0; - }, - ])); - - $hub = new Hub($client); - - $dsc = DynamicSamplingContext::fromHeader('sentry-trace_id=d49d9bf66f13450b81f65bc51cf49c03,sentry-public_key=public'); - $transactionMetaData = new TransactionMetadata(null, $dsc); - $transactionContext = new TransactionContext(TransactionContext::DEFAULT_NAME, null, $transactionMetaData); - - $transaction = $hub->startTransaction($transactionContext); - $this->assertSame('1', $transaction->getMetadata()->getDynamicSamplingContext()->get('sample_rate')); - } - - public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsNotSampled(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['traces_sample_rate' => 1])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, false)); - - $hub->configureScope(static function (Scope $scope) use ($transaction): void { - $scope->setSpan($transaction); - }); - - $this->assertSame($transaction, $hub->getTransaction()); - } - - public function testGetTransactionReturnsInstanceSetOnTheScopeIfTransactionIsSampled(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['traces_sample_rate' => 1])); - - $hub = new Hub($client); - $transaction = $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, true)); - - $hub->configureScope(static function (Scope $scope) use ($transaction): void { - $scope->setSpan($transaction); - }); - - $this->assertSame($transaction, $hub->getTransaction()); - } - - public function testGetTransactionReturnsNullIfNoTransactionIsSetOnTheScope(): void - { - $client = $this->createMock(ClientInterface::class); - $client->expects($this->once()) - ->method('getOptions') - ->willReturn(new Options(['traces_sample_rate' => 1])); - - $hub = new Hub($client); - $hub->startTransaction(new TransactionContext(TransactionContext::DEFAULT_NAME, true)); - - $this->assertNull($hub->getTransaction()); - } - - public function testEventTraceContextIsAlwaysFilled(): void - { - $hub = new Hub(new NoOpClient()); - - $event = Event::createEvent(); - - $hub->configureScope(function (Scope $scope) use ($event): void { - $event = $scope->applyToEvent($event); - - $this->assertNotEmpty($event->getContexts()['trace']); - }); - } - - public function testEventTraceContextIsNotOverridenWhenPresent(): void - { - $hub = new Hub(new NoOpClient()); - - $traceContext = ['foo' => 'bar']; - - $event = Event::createEvent(); - $event->setContext('trace', $traceContext); - - $hub->configureScope(function (Scope $scope) use ($event, $traceContext): void { - $event = $scope->applyToEvent($event); - - $this->assertEquals($event->getContexts()['trace'], $traceContext); - }); - } -} diff --git a/tests/State/LayerTest.php b/tests/State/LayerTest.php deleted file mode 100644 index 501f0e5793..0000000000 --- a/tests/State/LayerTest.php +++ /dev/null @@ -1,37 +0,0 @@ -createMock(ClientInterface::class); - - /** @var ClientInterface|MockObject $client2 */ - $client2 = $this->createMock(ClientInterface::class); - - $scope1 = new Scope(); - $scope2 = new Scope(); - - $layer = new Layer($client1, $scope1); - - $this->assertSame($client1, $layer->getClient()); - $this->assertSame($scope1, $layer->getScope()); - - $layer->setClient($client2); - $layer->setScope($scope2); - - $this->assertSame($client2, $layer->getClient()); - $this->assertSame($scope2, $layer->getScope()); - } -} diff --git a/tests/State/ScopeTest.php b/tests/State/ScopeTest.php index a9a9d3e8aa..b02a18af02 100644 --- a/tests/State/ScopeTest.php +++ b/tests/State/ScopeTest.php @@ -7,10 +7,14 @@ use PHPUnit\Framework\TestCase; use Sentry\Attachment\Attachment; use Sentry\Breadcrumb; +use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventHint; +use Sentry\NoOpClient; use Sentry\Options; use Sentry\Severity; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; @@ -24,10 +28,46 @@ final class ScopeTest extends TestCase { + private function applyScope(IsolationScope $scope, Event $event, ?EventHint $hint = null, ?Options $options = null): ?Event + { + $globalScope = new GlobalScope(); + + return $globalScope->merge($scope)->applyToEvent($event, $hint, $options); + } + + private function breadcrumbWithTimestamp(float $timestamp, string $category = 'test'): Breadcrumb + { + return new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, $category, null, [], $timestamp); + } + + public function testGetAndSetClient(): void + { + $scope = new IsolationScope(); + + $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 IsolationScope(); + $scope->setClient($client); + + $clonedScope = clone $scope; + + $this->assertSame($client, $clonedScope->getClient()); + } + public function testSetTag(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getTags()); @@ -35,7 +75,7 @@ public function testSetTag(): void $scope->setTag('foo', 'bar'); $scope->setTag('bar', 'baz'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getTags()); @@ -43,17 +83,17 @@ public function testSetTag(): void public function testSetTags(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setTags(['foo' => 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar'], $event->getTags()); $scope->setTags(['bar' => 'baz']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getTags()); @@ -61,20 +101,20 @@ public function testSetTags(): void public function testRemoveTag(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $scope->setTag('foo', 'bar'); $scope->setTag('bar', 'baz'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getTags()); $scope->removeTag('foo'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['bar' => 'baz'], $event->getTags()); @@ -82,8 +122,8 @@ public function testRemoveTag(): void public function testSetFlag(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayNotHasKey('flags', $event->getContexts()); @@ -91,7 +131,7 @@ public function testSetFlag(): void $scope->addFeatureFlag('foo', true); $scope->addFeatureFlag('bar', false); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayHasKey('flags', $event->getContexts()); @@ -111,8 +151,8 @@ public function testSetFlag(): void public function testSetFlagLimit(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayNotHasKey('flags', $event->getContexts()); @@ -128,7 +168,7 @@ public function testSetFlagLimit(): void ]; } - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayHasKey('flags', $event->getContexts()); @@ -143,7 +183,7 @@ public function testSetFlagLimit(): void 'result' => true, ]; - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertArrayHasKey('flags', $event->getContexts()); @@ -154,7 +194,7 @@ public function testSetFlagPropagatesToSpan(): void { $span = new Span(); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setSpan($span); $scope->addFeatureFlag('feature', true); @@ -166,10 +206,10 @@ public function testSetAndRemoveContext(): void { $propgationContext = PropagationContext::fromDefaults(); - $scope = new Scope($propgationContext); + $scope = new IsolationScope($propgationContext); $scope->setContext('foo', ['foo' => 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -182,7 +222,7 @@ public function testSetAndRemoveContext(): void $scope->removeContext('foo'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -194,7 +234,7 @@ public function testSetAndRemoveContext(): void $scope->setContext('foo', []); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([ @@ -207,8 +247,8 @@ public function testSetAndRemoveContext(): void public function testSetExtra(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getExtra()); @@ -216,7 +256,7 @@ public function testSetExtra(): void $scope->setExtra('foo', 'bar'); $scope->setExtra('bar', 'baz'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getExtra()); @@ -224,17 +264,17 @@ public function testSetExtra(): void public function testSetExtras(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setExtras(['foo' => 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar'], $event->getExtra()); $scope->setExtras(['bar' => 'baz']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo' => 'bar', 'bar' => 'baz'], $event->getExtra()); @@ -242,8 +282,8 @@ public function testSetExtras(): void public function testSetUser(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getUser()); @@ -253,21 +293,21 @@ public function testSetUser(): void $scope->setUser($user); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); - $this->assertSame($user, $event->getUser()); + $this->assertEquals($user, $event->getUser()); - $user = UserDataBag::createFromUserIpAddress('127.0.0.1'); - $user->setMetadata('subscription', 'basic'); - $user->setMetadata('subscription_expires_at', '2020-08-26'); + $expectedUser = UserDataBag::createFromUserIdentifier('unique_id'); + $expectedUser->setMetadata('subscription', 'basic'); + $expectedUser->setMetadata('subscription_expires_at', '2020-08-26'); $scope->setUser(['ip_address' => '127.0.0.1', 'subscription_expires_at' => '2020-08-26']); - $event = $scope->applyToEvent($event); + $event = $this->applyScope($scope, $event); $this->assertNotNull($event); - $this->assertEquals($user, $event->getUser()); + $this->assertEquals($expectedUser, $event->getUser()); } public function testSetUserThrowsOnInvalidArgument(): void @@ -275,23 +315,23 @@ public function testSetUserThrowsOnInvalidArgument(): void $this->expectException(\TypeError::class); $this->expectExceptionMessage('The $user argument must be either an array or an instance of the "Sentry\UserDataBag" class. Got: "string".'); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setUser('foo'); } public function testRemoveUser(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setUser(UserDataBag::createFromUserIdentifier('unique_id')); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNotNull($event->getUser()); $scope->removeUser(); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getUser()); @@ -299,15 +339,15 @@ public function testRemoveUser(): void public function testSetFingerprint(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getFingerprint()); $scope->setFingerprint(['foo', 'bar']); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame(['foo', 'bar'], $event->getFingerprint()); @@ -315,15 +355,15 @@ public function testSetFingerprint(): void public function testSetLevel(): void { - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent()); + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getLevel()); $scope->setLevel(Severity::debug()); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEquals(Severity::debug(), $event->getLevel()); @@ -331,12 +371,12 @@ public function testSetLevel(): void public function testAddBreadcrumb(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $breadcrumb1 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $breadcrumb2 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); $breadcrumb3 = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getBreadcrumbs()); @@ -344,14 +384,14 @@ public function testAddBreadcrumb(): void $scope->addBreadcrumb($breadcrumb1); $scope->addBreadcrumb($breadcrumb2); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([$breadcrumb1, $breadcrumb2], $event->getBreadcrumbs()); $scope->addBreadcrumb($breadcrumb3, 2); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertSame([$breadcrumb2, $breadcrumb3], $event->getBreadcrumbs()); @@ -359,19 +399,19 @@ public function testAddBreadcrumb(): void public function testClearBreadcrumbs(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting')); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNotEmpty($event->getBreadcrumbs()); $scope->clearBreadcrumbs(); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertEmpty($event->getBreadcrumbs()); @@ -384,7 +424,7 @@ public function testAddEventProcessor(): void $callback3Called = false; $event = Event::createEvent(); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addEventProcessor(function (Event $eventArg) use (&$callback1Called, $callback2Called, $callback3Called): ?Event { $this->assertFalse($callback2Called); @@ -395,7 +435,7 @@ public function testAddEventProcessor(): void return $eventArg; }); - $this->assertSame($event, $scope->applyToEvent($event)); + $this->assertSame($event, $this->applyScope($scope, $event)); $this->assertTrue($callback1Called); $scope->addEventProcessor(function () use ($callback1Called, &$callback2Called, $callback3Called) { @@ -413,7 +453,7 @@ public function testAddEventProcessor(): void return null; }); - $this->assertNull($scope->applyToEvent($event)); + $this->assertNull($this->applyScope($scope, $event)); $this->assertTrue($callback2Called); $this->assertFalse($callback3Called); } @@ -421,7 +461,7 @@ public function testAddEventProcessor(): void public function testEventProcessorReceivesTheEventAndEventHint(): void { $event = Event::createEvent(); - $scope = new Scope(); + $scope = new IsolationScope(); $hint = new EventHint(); $processorCalled = false; @@ -434,16 +474,18 @@ public function testEventProcessorReceivesTheEventAndEventHint(): void return $eventArg; }); - $this->assertSame($event, $scope->applyToEvent($event, $hint)); + $this->assertSame($event, $this->applyScope($scope, $event, $hint)); $this->assertSame($hint, $processorReceivedHint); $this->assertTrue($processorCalled); } public function testClear(): void { - $scope = new Scope(); + $scope = new IsolationScope(); $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_ERROR, Breadcrumb::TYPE_ERROR, 'error_reporting'); + $client = $this->createMock(ClientInterface::class); + $scope->setClient($client); $scope->setLevel(Severity::info()); $scope->addBreadcrumb($breadcrumb); $scope->setFingerprint(['foo']); @@ -453,7 +495,7 @@ public function testClear(): void $scope->setUser(UserDataBag::createFromUserIdentifier('unique_id')); $scope->clear(); - $event = $scope->applyToEvent(Event::createEvent()); + $event = $this->applyScope($scope, Event::createEvent()); $this->assertNotNull($event); $this->assertNull($event->getLevel()); @@ -463,6 +505,7 @@ public function testClear(): void $this->assertEmpty($event->getTags()); $this->assertEmpty($event->getUser()); $this->assertArrayNotHasKey('flags', $event->getContexts()); + $this->assertSame($client, $scope->getClient()); } public function testApplyToEvent(): void @@ -481,7 +524,7 @@ public function testApplyToEvent(): void $span = $transaction->startChild(new SpanContext()); $span->setSpanId(new SpanId('566e3688a61d4bc8')); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setLevel(Severity::warning()); $scope->setFingerprint(['foo']); $scope->addBreadcrumb($breadcrumb); @@ -493,13 +536,13 @@ public function testApplyToEvent(): void $scope->addFeatureFlag('feature', true); $scope->setSpan($span); - $this->assertSame($event, $scope->applyToEvent($event)); + $this->assertSame($event, $this->applyScope($scope, $event)); $this->assertTrue($event->getLevel()->isEqualTo(Severity::warning())); $this->assertSame(['foo'], $event->getFingerprint()); $this->assertSame([$breadcrumb], $event->getBreadcrumbs()); $this->assertSame(['foo' => 'bar'], $event->getTags()); $this->assertSame(['bar' => 'foo'], $event->getExtra()); - $this->assertSame($user, $event->getUser()); + $this->assertEquals($user, $event->getUser()); $this->assertSame([ 'foocontext' => [ 'foo' => 'foo', @@ -531,14 +574,372 @@ 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 GlobalScope(); + $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->addAttachment($globalAttachment); + + $isolationUser = UserDataBag::createFromUserIdentifier('isolation-user'); + $isolationUser->setMetadata('shared', 'isolation'); + $isolationUser->setMetadata('isolation', true); + + $isolationScope = new IsolationScope(); + $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 = $globalScope->merge($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' => '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 GlobalScope(); + $globalScope->setLevel(Severity::error()); + + $event = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertTrue($event->getLevel()->isEqualTo(Severity::error())); + } + + public function testMergeScopesCapsBreadcrumbsAndFlags(): void + { + $globalScope = new GlobalScope(); + $globalBreadcrumbs = []; + + foreach (range(1, 100) as $i) { + $breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, "global{$i}"); + $globalBreadcrumbs[] = $breadcrumb; + $globalScope->addBreadcrumb($breadcrumb); + } + + $isolationBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation'); + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb); + + foreach (range(1, Scope::MAX_FLAGS) as $i) { + $isolationScope->addFeatureFlag("feature{$i}", true); + } + + $isolationScope->addFeatureFlag('feature50', false); + $isolationScope->addFeatureFlag('feature101', true); + + $event = $globalScope->merge($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 testMergeScopesInterleavesBreadcrumbsChronologically(): void + { + $globalBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'global1'); + $globalBreadcrumb2 = $this->breadcrumbWithTimestamp(3.0, 'global2'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(2.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(4.0, 'isolation2'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb1); + $globalScope->addBreadcrumb($globalBreadcrumb2); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([$globalBreadcrumb1, $isolationBreadcrumb1, $globalBreadcrumb2, $isolationBreadcrumb2], $event->getBreadcrumbs()); + } + + public function testMergeScopesRespectsMaxBreadcrumbsLargerThanDefault(): void + { + $options = new Options(['max_breadcrumbs' => 150]); + $globalScope = new GlobalScope(); + $isolationScope = new IsolationScope(); + $breadcrumbs = []; + + foreach (range(1, 100) as $i) { + $breadcrumb = $this->breadcrumbWithTimestamp((float) $i, "global{$i}"); + $breadcrumbs[] = $breadcrumb; + $globalScope->addBreadcrumb($breadcrumb, 150); + } + + foreach (range(101, 200) as $i) { + $breadcrumb = $this->breadcrumbWithTimestamp((float) $i, "isolation{$i}"); + $breadcrumbs[] = $breadcrumb; + $isolationScope->addBreadcrumb($breadcrumb, 150); + } + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent(), null, $options); + + $this->assertNotNull($event); + $this->assertCount(150, $event->getBreadcrumbs()); + $this->assertSame(\array_slice($breadcrumbs, -150), $event->getBreadcrumbs()); + } + + public function testMergeScopesAppliesMaxBreadcrumbsSmallerThanMergedCount(): void + { + $globalBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'global1'); + $globalBreadcrumb2 = $this->breadcrumbWithTimestamp(4.0, 'global2'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(2.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(3.0, 'isolation2'); + $isolationBreadcrumb3 = $this->breadcrumbWithTimestamp(5.0, 'isolation3'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb1); + $globalScope->addBreadcrumb($globalBreadcrumb2); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + $isolationScope->addBreadcrumb($isolationBreadcrumb3); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent(), null, new Options(['max_breadcrumbs' => 3])); + + $this->assertNotNull($event); + $this->assertSame([$isolationBreadcrumb2, $globalBreadcrumb2, $isolationBreadcrumb3], $event->getBreadcrumbs()); + } + + public function testMergeScopesBreadcrumbsWithEqualTimestampsKeepGlobalScopeFirst(): void + { + $globalBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'global1'); + $globalBreadcrumb2 = $this->breadcrumbWithTimestamp(1.0, 'global2'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(1.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(1.0, 'isolation2'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb1); + $globalScope->addBreadcrumb($globalBreadcrumb2); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([$globalBreadcrumb1, $globalBreadcrumb2, $isolationBreadcrumb1, $isolationBreadcrumb2], $event->getBreadcrumbs()); + } + + public function testMergeScopesPreservesInsertionOrderWithinScopeForOutOfOrderTimestamps(): void + { + // Breadcrumbs recorded on the same scope are never reordered, even when + // their user-supplied timestamps are not monotonic; only breadcrumbs of + // different scopes are interleaved by timestamp. + $globalBreadcrumb = $this->breadcrumbWithTimestamp(4.0, 'global1'); + $isolationBreadcrumb1 = $this->breadcrumbWithTimestamp(2.0, 'isolation1'); + $isolationBreadcrumb2 = $this->breadcrumbWithTimestamp(6.0, 'isolation2'); + $isolationBreadcrumb3 = $this->breadcrumbWithTimestamp(1.0, 'isolation3'); + + $globalScope = new GlobalScope(); + $globalScope->addBreadcrumb($globalBreadcrumb); + + $isolationScope = new IsolationScope(); + $isolationScope->addBreadcrumb($isolationBreadcrumb1); + $isolationScope->addBreadcrumb($isolationBreadcrumb2); + $isolationScope->addBreadcrumb($isolationBreadcrumb3); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame([$isolationBreadcrumb1, $globalBreadcrumb, $isolationBreadcrumb2, $isolationBreadcrumb3], $event->getBreadcrumbs()); + } + + public function testApplyToEventDropsScopeBreadcrumbsWhenMaxBreadcrumbsIsZero(): void + { + $scope = new IsolationScope(); + $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'test')); + + $event = $this->applyScope($scope, Event::createEvent(), null, new Options(['max_breadcrumbs' => 0])); + + $this->assertNotNull($event); + $this->assertSame([], $event->getBreadcrumbs()); + } + + public function testApplyToEventDoesNotOverrideEventBreadcrumbs(): void + { + $eventBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'event'); + + $scope = new IsolationScope(); + $scope->addBreadcrumb(new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'scope')); + + $event = Event::createEvent(); + $event->setBreadcrumb([$eventBreadcrumb]); + + $event = $this->applyScope($scope, $event); + + $this->assertNotNull($event); + $this->assertSame([$eventBreadcrumb], $event->getBreadcrumbs()); + } + + public function testMergeScopesKeepsTraceStateFromIsolationScope(): void + { + $globalScope = new GlobalScope(); + + $isolationPropagationContext = PropagationContext::fromDefaults(); + $isolationPropagationContext->setTraceId(new TraceId('33333333333333333333333333333333')); + $isolationPropagationContext->setSpanId(new SpanId('3333333333333333')); + + $isolationScope = new IsolationScope($isolationPropagationContext); + + $this->assertNull($isolationScope->getSpan()); + $this->assertSame([ + 'trace_id' => '33333333333333333333333333333333', + 'span_id' => '3333333333333333', + ], $isolationScope->getTraceContext()); + + $mergedScope = $globalScope->merge($isolationScope); + $isolationScope->setPropagationContext(PropagationContext::fromDefaults()); + + $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 GlobalScope(); + $globalScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { + $calls[] = 'global'; + + return $event; + }); + + $isolationScope = new IsolationScope(); + $isolationScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event { + $calls[] = 'isolation'; + + return $event; + }); + + $event = $globalScope->merge($isolationScope)->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertSame(['static', 'global', 'isolation'], $calls); + } + /** * @dataProvider eventWithLogCountProvider */ public function testAttachmentsAppliedForType(Event $event, int $attachmentCount): void { - $scope = new Scope(); + $scope = new IsolationScope(); $scope->addAttachment(Attachment::fromBytes('test', 'abcde')); - $scope->applyToEvent($event); + $this->applyScope($scope, $event); $this->assertCount($attachmentCount, $event->getAttachments()); } @@ -563,7 +964,7 @@ public function testGetTraceContextPrefersExternalPropagationContextOverPropagat ]; }); - $scope = new Scope($propagationContext); + $scope = new IsolationScope($propagationContext); $this->assertSame([ 'trace_id' => '771a43a4192642f0b136d5159a501700', @@ -588,7 +989,7 @@ public function testGetTraceContextPrefersLocalSpanOverExternalPropagationContex $span = $transaction->startChild(new SpanContext()); $span->setSpanId(new SpanId('566e3688a61d4bc8')); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setSpan($span); $this->assertSame([ @@ -610,8 +1011,8 @@ public function testApplyToEventSkipsDynamicSamplingContextWhenUsingExternalProp ]; }); - $scope = new Scope(); - $event = $scope->applyToEvent(Event::createEvent(), null, new Options([ + $scope = new IsolationScope(); + $event = $this->applyScope($scope, Event::createEvent(), null, new Options([ 'dsn' => 'http://public@example.com/1', 'release' => '1.0.0', 'environment' => 'test', diff --git a/tests/Tracing/DynamicSamplingContextTest.php b/tests/Tracing/DynamicSamplingContextTest.php index 0996aa7021..70424e0d04 100644 --- a/tests/Tracing/DynamicSamplingContextTest.php +++ b/tests/Tracing/DynamicSamplingContextTest.php @@ -8,8 +8,7 @@ use Sentry\ClientInterface; use Sentry\NoOpClient; use Sentry\Options; -use Sentry\State\Hub; -use Sentry\State\Scope; +use Sentry\State\IsolationScope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\TraceId; @@ -91,16 +90,14 @@ public function testFromTransaction(): void 'environment' => 'test', ])); - $hub = new Hub($client); - $transactionContext = new TransactionContext(); $transactionContext->setName('foo'); - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext); $transaction->getMetadata()->setSamplingRate(1.0); $transaction->getMetadata()->setSampleRand(0.5); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertSame((string) $transaction->getTraceId(), $samplingContext->get('trace_id')); $this->assertSame((string) $transaction->getMetaData()->getSamplingRate(), $samplingContext->get('sample_rate')); @@ -114,15 +111,13 @@ public function testFromTransaction(): void public function testFromTransactionSourceUrl(): void { - $hub = new Hub(new NoOpClient()); - $transactionContext = new TransactionContext(); $transactionContext->setName('/foo/bar/123'); $transactionContext->setSource(TransactionSource::url()); - $transaction = new Transaction($transactionContext, $hub); + $transaction = new Transaction($transactionContext); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, new NoOpClient()); $this->assertNull($samplingContext->get('transaction')); } @@ -140,7 +135,7 @@ public function testFromOptions(): void $propagationContext->setTraceId(new TraceId('21160e9b836d479f81611368b2aa3d2c')); $propagationContext->setSampleRand(0.5); - $scope = new Scope(); + $scope = new IsolationScope(); $scope->setPropagationContext($propagationContext); $samplingContext = DynamicSamplingContext::fromOptions($options, $scope); @@ -161,7 +156,7 @@ public function testFromOptionsUsesConfiguredOrgIdOverDsnOrgId(): void 'org_id' => 2, ]); - $scope = new Scope(); + $scope = new IsolationScope(); $samplingContext = DynamicSamplingContext::fromOptions($options, $scope); $this->assertSame('2', $samplingContext->get('org_id')); @@ -173,7 +168,7 @@ public function testFromOptionsFallsBackToDsnOrgId(): void 'dsn' => 'http://public@o1.example.com/1', ]); - $scope = new Scope(); + $scope = new IsolationScope(); $samplingContext = DynamicSamplingContext::fromOptions($options, $scope); $this->assertSame('1', $samplingContext->get('org_id')); @@ -189,9 +184,8 @@ public function testFromTransactionUsesConfiguredOrgIdOverDsnOrgId(): void 'org_id' => 2, ])); - $hub = new Hub($client); - $transaction = new Transaction(new TransactionContext(), $hub); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $transaction = new Transaction(new TransactionContext()); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertSame('2', $samplingContext->get('org_id')); } @@ -205,9 +199,8 @@ public function testFromTransactionFallsBackToDsnOrgId(): void 'dsn' => 'http://public@o1.example.com/1', ])); - $hub = new Hub($client); - $transaction = new Transaction(new TransactionContext(), $hub); - $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $hub); + $transaction = new Transaction(new TransactionContext()); + $samplingContext = DynamicSamplingContext::fromTransaction($transaction, $client); $this->assertSame('1', $samplingContext->get('org_id')); } diff --git a/tests/Tracing/GuzzleTracingMiddlewareTest.php b/tests/Tracing/GuzzleTracingMiddlewareTest.php index becfa84a8e..61610f5cab 100644 --- a/tests/Tracing/GuzzleTracingMiddlewareTest.php +++ b/tests/Tracing/GuzzleTracingMiddlewareTest.php @@ -16,7 +16,8 @@ use Sentry\EventType; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\State\Scope; use Sentry\Tracing\GuzzleTracingMiddleware; use Sentry\Tracing\SpanStatus; @@ -33,16 +34,15 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void 'traces_sample_rate' => 0, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $transaction = $hub->startTransaction(TransactionContext::make()); + $transaction = \Sentry\startTransaction(TransactionContext::make()); $this->assertFalse($transaction->getSampled()); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(static function () use ($expectedPromiseResult): PromiseInterface { return new FulfilledPromise($expectedPromiseResult); }); @@ -60,13 +60,11 @@ public function testTraceCreatesBreadcrumbIfSpanIsNotSet(): void $this->assertNull($transaction->getSpanRecorder()); - $hub->configureScope(function (Scope $scope): void { - $event = Event::createEvent(); + $event = Event::createEvent(); - $scope->applyToEvent($event); + (new GlobalScope())->merge(SentrySdk::getIsolationScope())->applyToEvent($event); - $this->assertCount(1, $event->getBreadcrumbs()); - }); + $this->assertCount(1, $event->getBreadcrumbs()); } public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void @@ -78,16 +76,15 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void 'traces_sample_rate' => 1, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $transaction = $hub->startTransaction(TransactionContext::make()); + $transaction = \Sentry\startTransaction(TransactionContext::make()); $this->assertTrue($transaction->getSampled()); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(static function () use ($expectedPromiseResult): PromiseInterface { return new FulfilledPromise($expectedPromiseResult); }); @@ -106,13 +103,61 @@ public function testTraceCreatesBreadcrumbIfSpanIsRecorded(): void $this->assertNotNull($transaction->getSpanRecorder()); $this->assertCount(1, $transaction->getSpanRecorder()->getSpans()); - $hub->configureScope(function (Scope $scope): void { - $event = Event::createEvent(); + $event = Event::createEvent(); + + (new GlobalScope())->merge(SentrySdk::getIsolationScope())->applyToEvent($event); - $scope->applyToEvent($event); + $this->assertCount(1, $event->getBreadcrumbs()); + } - $this->assertCount(1, $event->getBreadcrumbs()); + public function testTraceUsesProvidedIsolationScope(): void + { + $client = $this->createMock(ClientInterface::class); + $client->expects($this->atLeastOnce()) + ->method('getOptions') + ->willReturn(new Options([ + 'trace_propagation_targets' => [ + 'www.example.com', + ], + ])); + + SentrySdk::init($client); + + $currentScope = SentrySdk::getIsolationScope(); + + $providedScope = new IsolationScope(); + $providedScope->setClient($client); + + $request = new Request('GET', 'https://www.example.com'); + $expectedPromiseResult = new Response(); + + $middleware = GuzzleTracingMiddleware::trace($providedScope); + $function = $middleware(function (Request $request) use ($expectedPromiseResult): PromiseInterface { + $this->assertNotEmpty($request->getHeader('sentry-trace')); + $this->assertNotEmpty($request->getHeader('baggage')); + + return new FulfilledPromise($expectedPromiseResult); }); + + /** @var PromiseInterface $promise */ + $promise = $function($request, []); + + try { + $promiseResult = $promise->wait(); + } catch (\Throwable $exception) { + $promiseResult = $exception; + } + + $this->assertSame($expectedPromiseResult, $promiseResult); + + $currentEvent = Event::createEvent(); + (new GlobalScope())->merge($currentScope)->applyToEvent($currentEvent); + + $providedEvent = Event::createEvent(); + (new GlobalScope())->merge($providedScope)->applyToEvent($providedEvent); + + $this->assertCount(0, $currentEvent->getBreadcrumbs()); + $this->assertCount(1, $providedEvent->getBreadcrumbs()); } /** @@ -125,12 +170,11 @@ public function testTraceHeaders(Request $request, Options $options, bool $heade ->method('getOptions') ->willReturn($options); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult, $headersShouldBePresent): PromiseInterface { if ($headersShouldBePresent) { $this->assertNotEmpty($request->getHeader('sentry-trace')); @@ -157,16 +201,15 @@ public function testTraceHeadersWithTransaction(Request $request, Options $optio ->method('getOptions') ->willReturn($options); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); - $transaction = $hub->startTransaction(new TransactionContext()); + $transaction = \Sentry\startTransaction(new TransactionContext()); - $hub->setSpan($transaction); + SentrySdk::getIsolationScope()->setSpan($transaction); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult, $headersShouldBePresent): PromiseInterface { if ($headersShouldBePresent) { $this->assertNotEmpty($request->getHeader('sentry-trace')); @@ -201,11 +244,10 @@ public function testTraceHeadersAreNotAddedWhenExternalPropagationContextIsActiv 'trace_propagation_targets' => null, ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); $expectedPromiseResult = new Response(); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult): PromiseInterface { $this->assertEmpty($request->getHeader('sentry-trace')); $this->assertEmpty($request->getHeader('baggage')); @@ -329,17 +371,15 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec ], ])); - $hub = new Hub($client); - SentrySdk::setCurrentHub($hub); + SentrySdk::init($client); + $scope = SentrySdk::getIsolationScope(); $client->expects($this->once()) ->method('captureEvent') - ->with($this->callback(function (Event $eventArg) use ($hub, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { + ->with($this->callback(function (Event $eventArg) use ($scope, $request, $expectedPromiseResult, $expectedBreadcrumbData, $expectedSpanData): bool { $this->assertSame(EventType::transaction(), $eventArg->getType()); - $hub->configureScope(static function (Scope $scope) use ($eventArg): void { - $scope->applyToEvent($eventArg); - }); + (new GlobalScope())->merge($scope)->applyToEvent($eventArg); $spans = $eventArg->getSpans(); $breadcrumbs = $eventArg->getBreadcrumbs(); @@ -372,11 +412,11 @@ public function testTrace(Request $request, $expectedPromiseResult, array $expec return true; })); - $transaction = $hub->startTransaction(new TransactionContext()); + $transaction = \Sentry\startTransaction(new TransactionContext()); - $hub->setSpan($transaction); + $scope->setSpan($transaction); - $middleware = GuzzleTracingMiddleware::trace($hub); + $middleware = GuzzleTracingMiddleware::trace(); $function = $middleware(function (Request $request) use ($expectedPromiseResult): PromiseInterface { $this->assertNotEmpty($request->getHeader('sentry-trace')); $this->assertNotEmpty($request->getHeader('baggage')); diff --git a/tests/Tracing/PropagationContextTest.php b/tests/Tracing/PropagationContextTest.php index beefa0cb1d..8d24219138 100644 --- a/tests/Tracing/PropagationContextTest.php +++ b/tests/Tracing/PropagationContextTest.php @@ -5,8 +5,10 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; +use Sentry\ClientInterface; use Sentry\Options; -use Sentry\State\Scope; +use Sentry\SentrySdk; +use Sentry\State\IsolationScope; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanId; @@ -113,6 +115,29 @@ public function testToBaggage(): void $this->assertSame('sentry-trace_id=566e3688a61d4bc888951642d6f14a19', $propagationContext->toBaggage()); } + public function testToBaggageUsesCurrentClientAndIsolationScope(): void + { + $propagationContext = PropagationContext::fromDefaults(); + $propagationContext->setTraceId(new TraceId('566e3688a61d4bc888951642d6f14a19')); + $propagationContext->setSampleRand(0.25); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options([ + 'release' => '1.0.0', + 'environment' => 'development', + ])); + + SentrySdk::getGlobalScope()->setClient($client); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope($propagationContext)); + + $this->assertSame( + 'sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.25,sentry-release=1.0.0,sentry-environment=development', + $propagationContext->toBaggage() + ); + } + public function testGetTraceContext(): void { $propagationContext = PropagationContext::fromDefaults(); @@ -199,7 +224,7 @@ public function testGettersAndSetters(string $getterMethod, string $setterMethod public static function gettersAndSettersDataProvider(): array { - $scope = new Scope(); + $scope = new IsolationScope(); $options = new Options([ 'dsn' => 'http://public@example.com/sentry/1', 'release' => '1.0.0', diff --git a/tests/Tracing/SpanTest.php b/tests/Tracing/SpanTest.php index cf8956172d..251a9eaf86 100644 --- a/tests/Tracing/SpanTest.php +++ b/tests/Tracing/SpanTest.php @@ -5,6 +5,10 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; +use Sentry\Event; +use Sentry\SentrySdk; +use Sentry\State\GlobalScope; +use Sentry\State\IsolationScope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\Span; use Sentry\Tracing\SpanContext; @@ -73,6 +77,28 @@ public function testStartChild(): void $this->assertSame($spanContext1->getTraceId(), $span2->getTraceId()); } + public function testSetHttpStatusWritesResponseContextToCurrentIsolationScopeOnly(): void + { + $globalScope = SentrySdk::getGlobalScope(); + $globalScope->setContext('response', [ + 'status_code' => 201, + ]); + + $isolationScope = new IsolationScope(); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope($isolationScope); + + $span = new Span(); + $span->setHttpStatus(404); + + $isolationEvent = (new GlobalScope())->merge($isolationScope)->applyToEvent(Event::createEvent()); + $this->assertNotNull($isolationEvent); + $this->assertSame(404, $isolationEvent->getContexts()['response']['status_code']); + + $globalEvent = $globalScope->merge(new IsolationScope())->applyToEvent(Event::createEvent()); + $this->assertNotNull($globalEvent); + $this->assertSame(201, $globalEvent->getContexts()['response']['status_code']); + } + /** * @dataProvider toTraceparentDataProvider */ diff --git a/tests/Tracing/StrictTraceContinuationTest.php b/tests/Tracing/StrictTraceContinuationTest.php index 6de0a3a962..88440b5da4 100644 --- a/tests/Tracing/StrictTraceContinuationTest.php +++ b/tests/Tracing/StrictTraceContinuationTest.php @@ -8,7 +8,6 @@ use Sentry\ClientInterface; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\TransactionContext; @@ -26,7 +25,7 @@ public function testPropagationContext(Options $options, string $baggage, bool $ ->method('getOptions') ->willReturn($options); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $contexts = [ PropagationContext::fromHeaders(self::SENTRY_TRACE_HEADER, $baggage), @@ -56,7 +55,7 @@ public function testTransactionContext(Options $options, string $baggage, bool $ ->method('getOptions') ->willReturn($options); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); $contexts = [ TransactionContext::fromHeaders(self::SENTRY_TRACE_HEADER, $baggage), diff --git a/tests/Tracing/TransactionContextTest.php b/tests/Tracing/TransactionContextTest.php index 678d8d8979..9d2b3dd61d 100644 --- a/tests/Tracing/TransactionContextTest.php +++ b/tests/Tracing/TransactionContextTest.php @@ -10,7 +10,6 @@ use Sentry\NoOpClient; use Sentry\Options; use Sentry\SentrySdk; -use Sentry\State\Hub; use Sentry\Tracing\DynamicSamplingContext; use Sentry\Tracing\SpanId; use Sentry\Tracing\TraceId; @@ -182,7 +181,7 @@ public function testInvalidSampleRandIsLogged(): void $client->method('getOptions') ->willReturn(new Options(['logger' => $logger])); - SentrySdk::setCurrentHub(new Hub($client)); + SentrySdk::init($client); try { TransactionContext::fromHeaders( @@ -190,7 +189,7 @@ public function testInvalidSampleRandIsLogged(): void 'sentry-sample_rand=-1.0' ); } finally { - SentrySdk::setCurrentHub(new Hub(new NoOpClient())); + SentrySdk::init(new NoOpClient()); } } diff --git a/tests/Tracing/TransactionSamplerTest.php b/tests/Tracing/TransactionSamplerTest.php new file mode 100644 index 0000000000..9118e9c121 --- /dev/null +++ b/tests/Tracing/TransactionSamplerTest.php @@ -0,0 +1,323 @@ +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 + { + return TransactionSampler::startTransaction($options, $transactionContext, $customSamplingContext); + } +} diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index 76a2c3aab6..1727fb373c 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -10,13 +10,15 @@ use Sentry\EventId; use Sentry\EventType; use Sentry\Options; -use Sentry\State\Hub; -use Sentry\State\HubInterface; +use Sentry\SentrySdk; +use Sentry\State\IsolationScope; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; use Sentry\Tracing\TransactionContext; +use function Sentry\startTransaction; + /** * @group time-sensitive */ @@ -37,19 +39,18 @@ public function testFinish(): void ->method('getOptions') ->willReturn(new Options()); - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->once()) - ->method('getClient') - ->willReturn($client); + SentrySdk::init($client); - $transaction = new Transaction($transactionContext, $hub); + $scope = SentrySdk::getIsolationScope(); + $transaction = new Transaction($transactionContext, $scope); + SentrySdk::getCurrentRuntimeContext()->setIsolationScope(new IsolationScope()); $transaction->initSpanRecorder(); $span1 = $transaction->startChild(new SpanContext()); $span2 = $transaction->startChild(new SpanContext()); $span3 = $transaction->startChild(new SpanContext()); // This span isn't finished, so it should not be included in the event - $hub->expects($this->once()) + $client->expects($this->once()) ->method('captureEvent') ->with($this->callback(function (Event $eventArg) use ($transactionContext, $span1, $span2): bool { $this->assertSame(EventType::transaction(), $eventArg->getType()); @@ -60,7 +61,7 @@ public function testFinish(): void $this->assertSame([$span1, $span2], $eventArg->getSpans()); return true; - })) + }), null, $scope) ->willReturnCallback(static function (Event $eventArg) use (&$expectedEventId): EventId { $expectedEventId = $eventArg->getId(); @@ -73,15 +74,18 @@ public function testFinish(): void $eventId = $transaction->finish(); $this->assertSame($expectedEventId, $eventId); + $this->assertSame($expectedEventId, SentrySdk::getLastEventId()); } public function testFinishDoesNothingIfSampledFlagIsNotTrue(): void { - $hub = $this->createMock(HubInterface::class); - $hub->expects($this->never()) + $client = $this->createMock(ClientInterface::class); + $client->expects($this->never()) ->method('captureEvent'); - $transaction = new Transaction(new TransactionContext(), $hub); + SentrySdk::init($client); + + $transaction = new Transaction(new TransactionContext()); $transaction->finish(); } @@ -112,7 +116,9 @@ public function testTransactionIsSampledCorrectlyWhenTracingIsSetToZeroInOptions ]) ); - $transaction = (new Hub($client))->startTransaction($context); + SentrySdk::init($client); + + $transaction = startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } @@ -155,7 +161,9 @@ public function testTransactionIsNotSampledWhenTracingIsDisabledInOptions(Transa ]) ); - $transaction = (new Hub($client))->startTransaction($context); + SentrySdk::init($client); + + $transaction = startTransaction($context); $this->assertSame($expectedSampled, $transaction->getSampled()); } diff --git a/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt b/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt index 35a32c8381..47364f8737 100644 --- a/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt +++ b/tests/phpt-oom/php84/out_of_memory_fatal_error_increases_memory_limit.phpt @@ -65,7 +65,7 @@ $options->setTransport($transport); $client = (new ClientBuilder($options))->getClient(); -SentrySdk::init()->bindClient($client); +SentrySdk::init($client); echo 'Before OOM memory limit: ' . \ini_get('memory_limit'); diff --git a/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt b/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt index a547854dab..544929924c 100644 --- a/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt +++ b/tests/phpt-oom/php85/out_of_memory_fatal_error_increases_memory_limit.phpt @@ -65,7 +65,7 @@ $options->setTransport($transport); $client = (new ClientBuilder($options))->getClient(); -SentrySdk::init()->bindClient($client); +SentrySdk::init($client); echo 'Before OOM memory limit: ' . \ini_get('memory_limit'); diff --git a/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt b/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt index fe1cf65e24..5fb8643950 100644 --- a/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt +++ b/tests/phpt/error_handler_captures_errors_not_silencable_on_php_8_and_up.phpt @@ -57,7 +57,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering "silenced" E_USER_ERROR error' . PHP_EOL; diff --git a/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt b/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt index 11ffa7c3c8..b0179c1889 100644 --- a/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt +++ b/tests/phpt/error_handler_respects_capture_silenced_errors_option.phpt @@ -50,7 +50,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering silenced error' . PHP_EOL; diff --git a/tests/phpt/error_handler_respects_current_error_reporting_level.phpt b/tests/phpt/error_handler_respects_current_error_reporting_level.phpt index f6017b8a55..09e0ab2416 100644 --- a/tests/phpt/error_handler_respects_current_error_reporting_level.phpt +++ b/tests/phpt/error_handler_respects_current_error_reporting_level.phpt @@ -56,7 +56,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering E_USER_NOTICE with error reporting on E_ALL' . PHP_EOL; diff --git a/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt b/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt index 592ebaa37d..fd610d824c 100644 --- a/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt +++ b/tests/phpt/error_handler_respects_error_types_option_regardless_of_error_reporting.phpt @@ -50,7 +50,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); echo 'Triggering E_USER_NOTICE error' . PHP_EOL; diff --git a/tests/phpt/error_listener_integration_respects_error_types_option.phpt b/tests/phpt/error_listener_integration_respects_error_types_option.phpt index 20d413ecde..db00f7b26e 100644 --- a/tests/phpt/error_listener_integration_respects_error_types_option.phpt +++ b/tests/phpt/error_listener_integration_respects_error_types_option.phpt @@ -56,7 +56,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); trigger_error('Error thrown', E_USER_NOTICE); trigger_error('Error thrown', E_USER_WARNING); diff --git a/tests/phpt/php84/error_handler_captures_fatal_error.phpt b/tests/phpt/php84/error_handler_captures_fatal_error.phpt index 9225fbafe1..349c6e6613 100644 --- a/tests/phpt/php84/error_handler_captures_fatal_error.phpt +++ b/tests/phpt/php84/error_handler_captures_fatal_error.phpt @@ -53,7 +53,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); $errorHandler = ErrorHandler::registerOnceErrorHandler(); $errorHandler->addErrorHandlerListener(static function (): void { diff --git a/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt b/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt index 7b76f6fedb..9fafb91883 100644 --- a/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt +++ b/tests/phpt/php84/fatal_error_integration_captures_fatal_error.phpt @@ -58,7 +58,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt b/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt index 56620b1c03..f215caeef8 100644 --- a/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt +++ b/tests/phpt/php84/fatal_error_integration_respects_error_types_option.phpt @@ -59,7 +59,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/php85/error_handler_captures_fatal_error.phpt b/tests/phpt/php85/error_handler_captures_fatal_error.phpt index 03f77c5891..594ad434ab 100644 --- a/tests/phpt/php85/error_handler_captures_fatal_error.phpt +++ b/tests/phpt/php85/error_handler_captures_fatal_error.phpt @@ -53,7 +53,7 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); $errorHandler = ErrorHandler::registerOnceErrorHandler(); $errorHandler->addErrorHandlerListener(static function (): void { diff --git a/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt b/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt index b6c62b636c..8911b0b46d 100644 --- a/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt +++ b/tests/phpt/php85/fatal_error_integration_captures_fatal_error.phpt @@ -58,7 +58,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt b/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt index dbf4e8356f..6928a5de52 100644 --- a/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt +++ b/tests/phpt/php85/fatal_error_integration_respects_error_types_option.phpt @@ -59,7 +59,7 @@ $client = (new ClientBuilder($options)) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); final class TestClass implements \JsonSerializable { diff --git a/tests/phpt/test_callable_serialization.phpt b/tests/phpt/test_callable_serialization.phpt index ff5a2d79e4..4806cf053a 100644 --- a/tests/phpt/test_callable_serialization.phpt +++ b/tests/phpt/test_callable_serialization.phpt @@ -19,6 +19,8 @@ use Sentry\Transport\Result; use Sentry\Transport\ResultStatus; use Sentry\Transport\TransportInterface; +use function Sentry\captureException; + $vendor = __DIR__; while (!file_exists($vendor . '/vendor')) { @@ -51,11 +53,11 @@ $client = ClientBuilder::create($options) ->setTransport($transport) ->getClient(); -SentrySdk::getCurrentHub()->bindClient($client); +SentrySdk::init($client); class Foo { function __construct(string $bar) { - SentrySdk::getCurrentHub()->captureException(new Exception('doh!')); + captureException(new Exception('doh!')); } }