Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/SentrySdk.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Sentry\State\HubInterface;
use Sentry\State\RuntimeContext;
use Sentry\State\RuntimeContextManager;
use Sentry\State\Scope;

/**
* This class is the main entry point for all the most common SDK features.
Expand All @@ -23,6 +24,11 @@ final class SentrySdk
*/
private static $currentHub;

/**
* @var Scope|null The process-global scope
*/
private static $globalScope;

/**
* @var RuntimeContextManager|null
*/
Expand All @@ -41,10 +47,12 @@ private function __construct()
*/
public static function init(?ClientInterface $client = null): HubInterface
{
if ($client === null) {
$client = new NoOpClient();
$hubClient = $client ?? new NoOpClient();

if ($client !== null) {
self::getGlobalScope()->setClient($client);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-init leaves stale global client

Medium Severity

Calling SentrySdk::init() without a client creates a hub with NoOpClient but does not update the process-global scope’s client when one was set by an earlier init($client). SentrySdk::getClient() then keeps returning the previous real client while the current hub uses a no-op client.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c86807c. Configure here.

Comment thread
cursor[bot] marked this conversation as resolved.
}
self::$currentHub = new Hub($client);
self::$currentHub = new Hub($hubClient);
self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub);

return self::getCurrentHub();
Expand Down Expand Up @@ -79,6 +87,31 @@ public static function setCurrentHub(HubInterface $hub): HubInterface
return $hub;
}

public static function getGlobalScope(): Scope
{
if (self::$globalScope === null) {
self::$globalScope = new Scope();
}

return self::$globalScope;
}

public static function getIsolationScope(): Scope
{
return self::getCurrentRuntimeContext()->getIsolationScope();
}

public static function getClient(): ClientInterface
{
$client = self::getIsolationScope()->getClient();

if (!$client instanceof NoOpClient) {
return $client;
}

return self::getGlobalScope()->getClient();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bindClient ignores global scope

Medium Severity

Hub::bindClient updates only the hub stack layer. It does not update the process-global scope client, while SentrySdk::getClient() resolves the client from isolation and global scopes. After SentrySdk::init()->bindClient($client), the hub sends with the bound client but SentrySdk::getClient() still returns the global scope’s default NoOpClient.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c86807c. Configure here.

Comment thread
cursor[bot] marked this conversation as resolved.
}

public static function startContext(): void
{
self::getRuntimeContextManager()->startContext();
Expand Down
18 changes: 17 additions & 1 deletion src/State/RuntimeContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ final class RuntimeContext
*/
private $hub;

/**
* @var Scope
*/
private $isolationScope;

/**
* @var LogsAggregator
*/
Expand All @@ -37,10 +42,11 @@ final class RuntimeContext
*/
private $metricsAggregator;

public function __construct(string $id, HubInterface $hub)
public function __construct(string $id, HubInterface $hub, ?Scope $isolationScope = null)
{
$this->id = $id;
$this->hub = $hub;
$this->isolationScope = $isolationScope ?? new Scope();
$this->logsAggregator = new LogsAggregator();
$this->metricsAggregator = new MetricsAggregator();
}
Expand All @@ -60,6 +66,16 @@ public function setHub(HubInterface $hub): void
$this->hub = $hub;
}

public function getIsolationScope(): Scope
{
return $this->isolationScope;
}

public function setIsolationScope(Scope $isolationScope): void
{
$this->isolationScope = $isolationScope;
}

public function getLogsAggregator(): LogsAggregator
{
return $this->logsAggregator;
Expand Down
55 changes: 53 additions & 2 deletions src/State/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

use Sentry\Attachment\Attachment;
use Sentry\Breadcrumb;
use Sentry\ClientInterface;
use Sentry\Event;
use Sentry\EventHint;
use Sentry\EventId;
use Sentry\EventType;
use Sentry\NoOpClient;
use Sentry\Options;
use Sentry\Severity;
use Sentry\Tracing\DynamicSamplingContext;
Expand Down Expand Up @@ -36,6 +39,16 @@ class Scope
*/
private $propagationContext;

/**
* @var ClientInterface The client bound to this scope
*/
private $client;

/**
* @var EventId|null The ID of the last captured event
*/
private $lastEventId;

/**
* @var Breadcrumb[] The list of breadcrumbs recorded in this scope
*/
Expand Down Expand Up @@ -110,6 +123,7 @@ class Scope
public function __construct(?PropagationContext $propagationContext = null)
{
$this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults();
$this->client = new NoOpClient();
}

/**
Expand Down Expand Up @@ -143,6 +157,42 @@ public static function mergeScopes(self $globalScope, self $isolationScope): sel
return $mergedScope;
}

/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mergeScopes drops global client

High Severity

Scope::mergeScopes clones the isolation scope and merges tags and other fields, but keeps the isolation scope’s bound client unchanged. After a normal init($client), the global scope holds the real client while the isolation scope still has a default NoOpClient, so a merged capture scope reports a no-op client even though SentrySdk::getClient() would return the global client.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c86807c. Configure here.

Comment thread
cursor[bot] marked this conversation as resolved.
* Returns the client bound to this scope.
*/
public function getClient(): ClientInterface
{
return $this->client;
}

/**
* Sets the client bound to this scope.
*
* @return $this
*/
public function setClient(ClientInterface $client): self
{
$this->client = $client;

return $this;
}

/**
* Returns the ID of the last captured event.
*/
public function getLastEventId(): ?EventId
{
return $this->lastEventId;
}

/**
* @internal
*/
public function setLastEventId(?EventId $lastEventId): void
{
$this->lastEventId = $lastEventId;
}

/**
* @param array<int, array<string, bool>> $globalFlags
* @param array<int, array<string, bool>> $isolationFlags
Expand All @@ -161,7 +211,7 @@ private static function mergeFlags(array $globalFlags, array $isolationFlags): a
}

unset($flagsByKey[$flagKey]);
$flagsByKey[$flagKey] = (bool) current($flag);
$flagsByKey[$flagKey] = current($flag);
}

$flagsByKey = \array_slice($flagsByKey, -self::MAX_FLAGS, self::MAX_FLAGS, true);
Expand Down Expand Up @@ -483,7 +533,8 @@ public static function getExternalPropagationContext(): ?array
}

/**
* Clears the scope and resets any data it contains.
* Clears event payload data from the scope. The client binding and last
* event ID are preserved.
*
* @return $this
*/
Expand Down
2 changes: 1 addition & 1 deletion src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function init(array $options = []): void
{
$client = ClientBuilder::create($options)->getClient();

SentrySdk::init()->bindClient($client);
SentrySdk::init($client);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions tests/FunctionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ public function testInit(): void
init(['default_integrations' => false]);

$this->assertNotNull(SentrySdk::getCurrentHub()->getClient());
$this->assertSame(SentrySdk::getCurrentHub()->getClient(), SentrySdk::getClient());
}

public function testInitPreservesGlobalScope(): void
{
$globalScope = SentrySdk::getGlobalScope();
$globalScope->setTag('baseline', 'yes');

init(['default_integrations' => false]);

$this->assertSame($globalScope, SentrySdk::getGlobalScope());
$this->assertSame(SentrySdk::getCurrentHub()->getClient(), $globalScope->getClient());

$event = $globalScope->applyToEvent(Event::createEvent());

$this->assertNotNull($event);
$this->assertSame(['baseline' => 'yes'], $event->getTags());
}

/**
Expand Down
9 changes: 9 additions & 0 deletions tests/SentrySdkExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ public function executeBeforeTest(string $test): void
$reflectionProperty->setAccessible(false);
}

$reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'globalScope');
if (\PHP_VERSION_ID < 80100) {
$reflectionProperty->setAccessible(true);
}
$reflectionProperty->setValue(null, null);
if (\PHP_VERSION_ID < 80100) {
$reflectionProperty->setAccessible(false);
}

$reflectionProperty = new \ReflectionProperty(SentrySdk::class, 'runtimeContextManager');
if (\PHP_VERSION_ID < 80100) {
$reflectionProperty->setAccessible(true);
Expand Down
81 changes: 81 additions & 0 deletions tests/SentrySdkTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,87 @@ public function testSetCurrentHub(): void
$this->assertSame($hub, SentrySdk::getCurrentHub());
}

public function testGetGlobalScope(): void
{
$scope = SentrySdk::getGlobalScope();

$this->assertSame($scope, SentrySdk::getGlobalScope());
}

public function testGetIsolationScope(): void
{
$scope = SentrySdk::getIsolationScope();

$this->assertSame($scope, SentrySdk::getIsolationScope());
}

public function testGetClientReturnsCachedNoOpFallbackBeforeInit(): void
{
$client = SentrySdk::getClient();

$this->assertInstanceOf(NoOpClient::class, $client);
$this->assertSame($client, SentrySdk::getClient());
}

public function testGetClientReturnsGlobalScopeClient(): void
{
$client = $this->createMock(ClientInterface::class);

SentrySdk::getGlobalScope()->setClient($client);

$this->assertSame($client, SentrySdk::getClient());
}

public function testGetClientReturnsIsolationScopeClientBeforeGlobalScopeClient(): void
{
$globalClient = $this->createMock(ClientInterface::class);
$isolationClient = $this->createMock(ClientInterface::class);

SentrySdk::getGlobalScope()->setClient($globalClient);
SentrySdk::getIsolationScope()->setClient($isolationClient);

$this->assertSame($isolationClient, SentrySdk::getClient());
}

public function testStartContextUsesSeparateIsolationScope(): void
{
$globalIsolationScope = SentrySdk::getIsolationScope();

SentrySdk::startContext();

$contextIsolationScope = SentrySdk::getIsolationScope();

$this->assertNotSame($globalIsolationScope, $contextIsolationScope);

SentrySdk::endContext();

$this->assertSame($globalIsolationScope, SentrySdk::getIsolationScope());
}

public function testInitWithClientSetsGlobalScopeClient(): void
{
$client = $this->createMock(ClientInterface::class);

SentrySdk::init($client);

$this->assertSame($client, SentrySdk::getClient());
}

public function testInitDoesNotResetGlobalScope(): void
{
$globalScope = SentrySdk::getGlobalScope();
$globalScope->setTag('baseline', 'yes');

SentrySdk::init();

$this->assertSame($globalScope, SentrySdk::getGlobalScope());

$event = $globalScope->applyToEvent(Event::createEvent());

$this->assertNotNull($event);
$this->assertSame(['baseline' => 'yes'], $event->getTags());
}

public function testStartAndEndContextIsolateScopeData(): void
{
SentrySdk::init();
Expand Down
Loading
Loading