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
6 changes: 6 additions & 0 deletions src/sentry/publish/sentry.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@
'enable_queue_metrics' => env('SENTRY_ENABLE_QUEUE_METRICS', true),
'metrics_interval' => (int) env('SENTRY_METRICS_INTERVAL', 10),

// The maximum number of spans allowed within a single transaction. Once the
// budget is exhausted new spans are skipped (the callable still runs) to
// prevent the span tree of a long-lived coroutine from growing unboundedly.
// A value of 0 (or negative) disables the limit.
'max_spans' => (int) env('SENTRY_MAX_SPANS', 1000),
Comment on lines +78 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor the SDK semantics when disabling the custom budget

max_spans is also a Sentry SDK option: ClientBuilderFactory retains every key defined by Sentry\Options and forwards it to ClientBuilder. Consequently, setting SENTRY_MAX_SPANS=0 makes this custom budget unlimited but gives the SDK recorder a literal zero-span cap, so the resulting transactions record no child spans instead of being unlimited as advertised. Use a component-specific option or avoid forwarding the disabling value to the SDK.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the span budget in every Sentry documentation source

This adds a user-facing tracing configuration key and environment variable, but neither component README nor any of the four locale Sentry pages includes max_spans or SENTRY_MAX_SPANS. The repository specifically requires tracing changes to remain synchronized across all four pages and both READMEs, so users following the documented configuration cannot discover this new default or its opt-out behavior.

AGENTS.md reference: AGENTS.md:L129-L130

Useful? React with 👍 / 👎.


// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send_default_pii
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', true),

Expand Down
5 changes: 5 additions & 0 deletions src/sentry/src/Feature.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public function getMetricsInterval(int $default = 10): int
return $interval;
}

public function getMaxSpans(int $default = 1000): int
{
return (int) $this->config->get('sentry.max_spans', $default);
}

public function isTracingEnabled(string $key, bool $default = true): bool
{
return (bool) $this->config->get('sentry.tracing.' . $key, $default);
Expand Down
70 changes: 70 additions & 0 deletions src/sentry/src/Tracing/SpanBudget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);
/**
* This file is part of friendsofhyperf/components.
*
* @link https://github.com/friendsofhyperf/components
* @document https://github.com/friendsofhyperf/components/blob/main/README.md
* @contact huangdijia@gmail.com
*/

namespace FriendsOfHyperf\Sentry\Tracing;

use Hyperf\Context\Context;

/**
* Bounds the number of spans that can be created within a single transaction.
*
* When a long-lived coroutine keeps spawning child coroutines, the parent
* transaction span tree would otherwise grow unboundedly and leak memory for
* the whole lifetime of the coroutine. A limit of 0 (or negative) disables
* the budget entirely.
*/
final class SpanBudget
{
public const CONTEXT_KEY = 'sentry.tracing.span_budget.count';

public function __construct(private int $limit)
{
}

/**
* Reset the counter for the current coroutine context.
*/
public function reset(): void
{
Context::set(self::CONTEXT_KEY, 0);
}

/**
* Try to acquire one slot of the budget.
*
* When the limit is not positive the budget is disabled and this method
* always returns true without counting.
*/
public function tryAcquire(): bool
{
if ($this->limit <= 0) {
return true;
}

$count = Context::getOrSet(self::CONTEXT_KEY, fn () => 0);

if ($count >= $this->limit) {
return false;
}

Context::set(self::CONTEXT_KEY, $count + 1);

return true;
}

/**
* Return the number of spans already acquired in the current context.
*/
public function count(): int
{
return (int) Context::get(self::CONTEXT_KEY, 0);
}
}
16 changes: 16 additions & 0 deletions src/sentry/src/Tracing/Tracer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace FriendsOfHyperf\Sentry\Tracing;

use FriendsOfHyperf\Sentry\Feature;
use Hyperf\Engine\Coroutine as Co;
use Sentry\SentrySdk;
use Sentry\State\Scope;
Expand All @@ -26,11 +27,17 @@

class Tracer
{
public function __construct(private Feature $feature)
{
}

/**
* Starts a new Transaction and returns it. This is the entry point to manual tracing instrumentation.
*/
public function startTransaction(TransactionContext $transactionContext, array $customSamplingContext = []): Transaction
{
(new SpanBudget($this->feature->getMaxSpans()))->reset();

$hub = SentrySdk::getCurrentHub();
$hub->pushScope();
$hub->configureScope(static fn (Scope $scope) => $scope->clearBreadcrumbs());
Expand Down Expand Up @@ -79,6 +86,15 @@

$context->setData(['coroutine.id' => Co::id()] + $context->getData());

$hub = SentrySdk::getCurrentHub();

// The budget only constrains spans created inside a transaction: when the
// budget is exhausted we skip creating the span and execute the callable
// directly to bound the memory used by the transaction span tree.
if ($hub->getSpan() !== null && ! (new SpanBudget($this->feature->getMaxSpans()))->tryAcquire()) {
return $hub->configureScope(static fn (Scope $scope) => $trace($scope));

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.4, ^7.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.2, ^3.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.2, ^6.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.3, ^6.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.3, ^9.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.3, ^2.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.3, ^9.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.4, ^8.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.2, ^9.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.4, ^2.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.4, ^3.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.2, ^6.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.2, ^7.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.2, ^8.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.2, ^2.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.3, ^3.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.2, ^2.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.3, ^8.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.4, ^8.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.2, ^9.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.4, ^9.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.3, ^3.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.2, ^7.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.2, ^8.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.4, ^9.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.3, ^6.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.4, ^7.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.4, ^6.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.3, ^2.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.3, ^7.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.4, ^6.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.2, ^3.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.4, ^2.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Elasticsearch (8.3, ^8.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Nesbot Carbon (8.4, ^3.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test for Symfony Components (8.3, ^7.0)

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.2 with Swoole 6.1.3

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.2 with Swoole 6.1.3

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.4 with Swoole 6.0.2

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.3 with Swoole 6.0.2

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.2 with Swoole 6.0.2

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.3 with Swoole 6.0.2

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.4 with Swoole 6.1.3

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.4 with Swoole 6.1.3

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.2 with Swoole 6.0.2

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.3 with Swoole 6.1.3

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.4 with Swoole 6.0.2

Result of method Sentry\State\HubInterface::configureScope() (void) is used.

Check failure on line 95 in src/sentry/src/Tracing/Tracer.php

View workflow job for this annotation

GitHub Actions / Test on PHP 8.3 with Swoole 6.1.3

Result of method Sentry\State\HubInterface::configureScope() (void) is used.
}

return trace(
function (Scope $scope) use ($trace) {
try {
Expand Down
88 changes: 88 additions & 0 deletions tests/Sentry/Tracing/SpanBudgetTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);
/**
* This file is part of friendsofhyperf/components.
*
* @link https://github.com/friendsofhyperf/components
* @document https://github.com/friendsofhyperf/components/blob/main/README.md
* @contact huangdijia@gmail.com
*/
use FriendsOfHyperf\Sentry\Tracing\SpanBudget;
use Hyperf\Context\Context;
use Hyperf\Engine\Channel;
use Swoole\Coroutine;

uses()->group('sentry');

beforeEach(function () {
Context::destroy(SpanBudget::CONTEXT_KEY);
});

test('limit of 3 allows only the first three acquisitions', function () {
$budget = new SpanBudget(3);

expect($budget->tryAcquire())->toBeTrue()
->and($budget->tryAcquire())->toBeTrue()
->and($budget->tryAcquire())->toBeTrue()
->and($budget->count())->toBe(3)
->and($budget->tryAcquire())->toBeFalse();
});

test('reset allows acquiring again', function () {
$budget = new SpanBudget(3);

$budget->tryAcquire();
$budget->tryAcquire();
$budget->tryAcquire();

expect($budget->tryAcquire())->toBeFalse();

$budget->reset();

expect($budget->tryAcquire())->toBeTrue()
->and($budget->count())->toBe(1);
});

test('limit of 0 means unlimited', function () {
$budget = new SpanBudget(0);

for ($i = 0; $i < 100; ++$i) {
expect($budget->tryAcquire())->toBeTrue();
}

expect($budget->count())->toBe(0);
});

test('counter is isolated between coroutines', function () {
Swoole\Coroutine\run(function () {
$budget = new SpanBudget(3);
$channel = new Channel(2);
$results = [];

Coroutine::create(function () use ($budget, $channel, &$results) {
$results['co1'] = [
$budget->tryAcquire(),
$budget->tryAcquire(),
$budget->tryAcquire(),
$budget->tryAcquire(),
$budget->count(),
];
$channel->push(true);
});

Coroutine::create(function () use ($budget, $channel, &$results) {
$results['co2'] = [
$budget->tryAcquire(),
$budget->count(),
];
$channel->push(true);
});

$channel->pop();
$channel->pop();

expect($results['co1'])->toBe([true, true, true, false, 3])
->and($results['co2'])->toBe([true, 1]);
});
});
Loading