From b906025c56269d2949cf6760921f9d134169a232 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:24:04 +0000 Subject: [PATCH] feat: allow providing a hub when starting a runtime context Allow async runtime integrations to supply their own context-aware Hub when starting a runtime context. Keep the default Hub cloning and nested start behavior unchanged while avoiding throwaway Hub and Scope allocations for custom runtimes. --- src/SentrySdk.php | 23 ++++++++++++++-- src/State/RuntimeContextManager.php | 10 +++++-- src/functions.php | 24 +++++++++++++++-- tests/FunctionsTest.php | 16 +++++++++++ tests/SentrySdkTest.php | 42 +++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 6 deletions(-) diff --git a/src/SentrySdk.php b/src/SentrySdk.php index 1a4c4b2d4..86f2b79ee 100644 --- a/src/SentrySdk.php +++ b/src/SentrySdk.php @@ -104,11 +104,30 @@ public static function setCurrentHub(HubInterface $hub): HubInterface return $hub; } - public static function startContext(): void + /** + * Starts an isolated context for the current logical execution. + * + * A provided hub is used as-is, allowing runtimes with their own HubInterface + * implementation to manage hub isolation. When no hub is provided, the SDK + * creates an isolated hub from the baseline. + * + * If a context is already active, this method is a no-op and the provided hub + * is ignored. Use setCurrentHub() to replace the active context's hub. + * + * @param HubInterface|null $hub The hub to use for the new context + */ + public static function startContext(?HubInterface $hub = null): void { - self::getRuntimeContextManager()->startContext(); + self::getRuntimeContextManager()->startContext($hub); } + /** + * Ends and flushes the active context for the current logical execution. + * + * When no context is active this is a no-op. + * + * @param int|null $timeout The maximum number of seconds to wait while flushing the client transport + */ public static function endContext(?int $timeout = null): void { self::getRuntimeContextManager()->endContext($timeout); diff --git a/src/State/RuntimeContextManager.php b/src/State/RuntimeContextManager.php index 9ec0e7f47..db39364fd 100644 --- a/src/State/RuntimeContextManager.php +++ b/src/State/RuntimeContextManager.php @@ -87,9 +87,13 @@ public function getCurrentContext(): RuntimeContext /** * Starts an isolated context for the current logical execution. * + * A provided hub is used as-is. It is ignored when a context is already active. + * + * @param HubInterface|null $hub The hub to use for the new context + * * @return bool Whether a new context was started */ - public function startContext(): bool + public function startContext(?HubInterface $hub = null): bool { if ($this->getActiveContext() !== null) { // Nested start calls for the same logical execution should be a no-op. @@ -98,7 +102,7 @@ public function startContext(): bool ErrorHandler::resetFatalErrorHandlerState(); - $this->setActiveContext(new RuntimeContext($this->generateRuntimeContextId(), $this->createHubFromBaseHub())); + $this->setActiveContext(new RuntimeContext($this->generateRuntimeContextId(), $hub ?? $this->createHubFromBaseHub())); return true; } @@ -107,6 +111,8 @@ public function startContext(): bool * Ends and flushes the active context for the current logical execution. * * When no context is active this is a no-op. + * + * @param int|null $timeout The maximum number of seconds to wait while flushing the client transport */ public function endContext(?int $timeout = null): void { diff --git a/src/functions.php b/src/functions.php index e6e41e4d4..9be4ff484 100644 --- a/src/functions.php +++ b/src/functions.php @@ -11,6 +11,7 @@ use Sentry\Logs\Logs; use Sentry\Metrics\Metrics; use Sentry\Metrics\TraceMetrics; +use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\Tracing\PropagationContext; use Sentry\Tracing\SpanContext; @@ -222,11 +223,30 @@ function withScope(callable $callback) return SentrySdk::getCurrentHub()->withScope($callback); } -function startContext(): void +/** + * Starts an isolated context for the current logical execution. + * + * A provided hub is used as-is, allowing runtimes with their own HubInterface + * implementation to manage hub isolation. When no hub is provided, the SDK + * creates an isolated hub from the baseline. + * + * If a context is already active, this function is a no-op and the provided hub + * is ignored. Use SentrySdk::setCurrentHub() to replace the active context's hub. + * + * @param HubInterface|null $hub The hub to use for the new context + */ +function startContext(?HubInterface $hub = null): void { - SentrySdk::startContext(); + SentrySdk::startContext($hub); } +/** + * Ends and flushes the active context for the current logical execution. + * + * When no context is active this is a no-op. + * + * @param int|null $timeout The maximum number of seconds to wait while flushing the client transport + */ function endContext(?int $timeout = null): void { SentrySdk::endContext($timeout); diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index 3c091bda0..99f1e9f02 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -372,6 +372,22 @@ public function testStartAndEndContext(): void $this->assertSame($globalHub, SentrySdk::getCurrentHub()); } + public function testStartContextForwardsProvidedHub(): void + { + SentrySdk::init(); + + $globalHub = SentrySdk::getCurrentHub(); + $hub = new Hub(); + + startContext($hub); + + $this->assertSame($hub, SentrySdk::getCurrentHub()); + + endContext(); + + $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + } + public function testWithContext(): void { SentrySdk::init(); diff --git a/tests/SentrySdkTest.php b/tests/SentrySdkTest.php index 5b049168b..f9d5cdb75 100644 --- a/tests/SentrySdkTest.php +++ b/tests/SentrySdkTest.php @@ -91,6 +91,30 @@ public function testStartContextDoesNotInheritBaselineSpan(): void $this->assertSame($baselineSpan, SentrySdk::getCurrentHub()->getSpan()); } + public function testStartContextUsesProvidedHubAsIs(): void + { + SentrySdk::init(); + + $globalHub = SentrySdk::getCurrentHub(); + $span = new Span(new SpanContext()); + $hub = new Hub(); + $hub->setSpan($span); + $traceparent = ''; + $hub->configureScope(static function (Scope $scope) use (&$traceparent): void { + $traceparent = $scope->getPropagationContext()->toTraceparent(); + }); + + SentrySdk::startContext($hub); + + $this->assertSame($hub, SentrySdk::getCurrentHub()); + $this->assertSame($span, SentrySdk::getCurrentHub()->getSpan()); + $this->assertSame($traceparent, $this->getCurrentScopeTraceparent()); + + SentrySdk::endContext(); + + $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + } + public function testStartContextCreatesFreshPropagationContext(): void { SentrySdk::init(); @@ -149,6 +173,24 @@ public function testNestedStartContextIsNoOp(): void $this->assertSame($globalHub, SentrySdk::getCurrentHub()); } + public function testNestedStartContextIgnoresProvidedHub(): void + { + SentrySdk::init(); + + $globalHub = SentrySdk::getCurrentHub(); + + SentrySdk::startContext(); + $contextHub = SentrySdk::getCurrentHub(); + + SentrySdk::startContext(new Hub()); + + $this->assertSame($contextHub, SentrySdk::getCurrentHub()); + + SentrySdk::endContext(); + + $this->assertSame($globalHub, SentrySdk::getCurrentHub()); + } + public function testRuntimeContextStorageIsolatesConcurrentExecutions(): void { $storage = new StubRuntimeContextStorage();