Skip to content
Merged
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
53 changes: 53 additions & 0 deletions src/State/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
*/
class Scope
{
/**
* Maximum number of flags allowed. We only track the first flags set.
*
* @internal
*/
public const MAX_FLAGS = 100;

/**
* @var PropagationContext
*/
Expand All @@ -46,6 +53,11 @@ class Scope
*/
private $tags = [];

/**
* @var array<int, array<string, bool>> The list of flags associated to this scope
*/
private $flags = [];

/**
* @var array<string, mixed> A set of extra data associated to this scope
*/
Expand Down Expand Up @@ -130,6 +142,35 @@ public function removeTag(string $key): self
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.
*
Expand Down Expand Up @@ -331,6 +372,7 @@ public function clear(): self
$this->fingerprint = [];
$this->breadcrumbs = [];
$this->tags = [];
$this->flags = [];
$this->extra = [];
$this->contexts = [];

Expand Down Expand Up @@ -359,6 +401,17 @@ public function applyToEvent(Event $event, ?EventHint $hint = null, ?Options $op
$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),
];
}, $this->flags),
]);
}

if (!empty($this->extra)) {
$event->setExtra(array_merge($this->extra, $event->getExtra()));
}
Expand Down
34 changes: 33 additions & 1 deletion src/Tracing/Span.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
*/
class Span
{
/**
* Maximum number of flags allowed. We only track the first flags set.
*
* @internal
*/
public const MAX_FLAGS = 10;

/**
* @var SpanId Span ID
*/
Expand Down Expand Up @@ -62,6 +69,11 @@ class Span
*/
protected $tags = [];

/**
* @var array<string, bool> A List of flags associated to this span
*/
protected $flags = [];

/**
* @var array<string, mixed> An arbitrary mapping of additional metadata
*/
Expand Down Expand Up @@ -328,6 +340,20 @@ public function setTags(array $tags)
return $this;
}

/**
* Sets a feature flag associated to this span.
*
* @return $this
*/
public function setFlag(string $key, bool $result)
{
if (\count($this->flags) < self::MAX_FLAGS) {
$this->flags[$key] = $result;
}

return $this;
}

/**
* Gets the ID of the span.
*/
Expand Down Expand Up @@ -369,7 +395,13 @@ public function setSampled(?bool $sampled)
public function getData(?string $key = null, $default = null)
{
if ($key === null) {
return $this->data;
$data = $this->data;

foreach ($this->flags as $flagKey => $flagValue) {
$data["flag.evaluation.{$flagKey}"] = $flagValue;
}

return $data;
}

return $this->data[$key] ?? $default;
Expand Down
94 changes: 94 additions & 0 deletions tests/State/ScopeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Sentry\State\Scope;
use Sentry\Tracing\DynamicSamplingContext;
use Sentry\Tracing\PropagationContext;
use Sentry\Tracing\Span;
use Sentry\Tracing\SpanContext;
use Sentry\Tracing\SpanId;
use Sentry\Tracing\TraceId;
Expand Down Expand Up @@ -77,6 +78,88 @@ public function testRemoveTag(): void
$this->assertSame(['bar' => 'baz'], $event->getTags());
}

public function testSetFlag(): void
{
$scope = new Scope();
$event = $scope->applyToEvent(Event::createEvent());

$this->assertNotNull($event);
$this->assertArrayNotHasKey('flags', $event->getContexts());

$scope->addFeatureFlag('foo', true);
$scope->addFeatureFlag('bar', false);

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

$this->assertNotNull($event);
$this->assertArrayHasKey('flags', $event->getContexts());
$this->assertEquals([
'values' => [
[
'flag' => 'foo',
'result' => true,
],
[
'flag' => 'bar',
'result' => false,
],
],
], $event->getContexts()['flags']);
}

public function testSetFlagLimit(): void
{
$scope = new Scope();
$event = $scope->applyToEvent(Event::createEvent());

$this->assertNotNull($event);
$this->assertArrayNotHasKey('flags', $event->getContexts());

$expectedFlags = [];

foreach (range(1, Scope::MAX_FLAGS) as $i) {
$scope->addFeatureFlag("feature{$i}", true);

$expectedFlags[] = [
'flag' => "feature{$i}",
'result' => true,
];
}

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

$this->assertNotNull($event);
$this->assertArrayHasKey('flags', $event->getContexts());
$this->assertEquals(['values' => $expectedFlags], $event->getContexts()['flags']);

array_shift($expectedFlags);

$scope->addFeatureFlag('should-not-be-discarded', true);

$expectedFlags[] = [
'flag' => 'should-not-be-discarded',
'result' => true,
];

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

$this->assertNotNull($event);
$this->assertArrayHasKey('flags', $event->getContexts());
$this->assertEquals(['values' => $expectedFlags], $event->getContexts()['flags']);
}

public function testSetFlagPropagatesToSpan(): void
{
$span = new Span();

$scope = new Scope();
$scope->setSpan($span);

$scope->addFeatureFlag('feature', true);

$this->assertSame(['flag.evaluation.feature' => true], $span->getData());
}

public function testSetAndRemoveContext(): void
{
$propgationContext = PropagationContext::fromDefaults();
Expand Down Expand Up @@ -364,6 +447,7 @@ public function testClear(): void
$scope->setFingerprint(['foo']);
$scope->setExtras(['foo' => 'bar']);
$scope->setTags(['bar' => 'foo']);
$scope->addFeatureFlag('feature', true);
$scope->setUser(UserDataBag::createFromUserIdentifier('unique_id'));
$scope->clear();

Expand All @@ -376,6 +460,7 @@ public function testClear(): void
$this->assertEmpty($event->getExtra());
$this->assertEmpty($event->getTags());
$this->assertEmpty($event->getUser());
$this->assertArrayNotHasKey('flags', $event->getContexts());
}

public function testApplyToEvent(): void
Expand Down Expand Up @@ -403,6 +488,7 @@ public function testApplyToEvent(): void
$scope->setUser($user);
$scope->setContext('foocontext', ['foo' => 'bar']);
$scope->setContext('barcontext', ['bar' => 'foo']);
$scope->addFeatureFlag('feature', true);
$scope->setSpan($span);

$this->assertSame($event, $scope->applyToEvent($event));
Expand All @@ -417,6 +503,14 @@ public function testApplyToEvent(): void
'foo' => 'foo',
'bar' => 'bar',
],
'flags' => [
'values' => [
[
'flag' => 'feature',
'result' => true,
],
],
],
'trace' => [
'span_id' => '566e3688a61d4bc8',
'trace_id' => '566e3688a61d4bc888951642d6f14a19',
Expand Down
30 changes: 30 additions & 0 deletions tests/Tracing/SpanTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,34 @@ public function testOriginIsCopiedFromContext(): void
$this->assertSame($context->getOrigin(), $span->getOrigin());
$this->assertSame($context->getOrigin(), $span->getTraceContext()['origin']);
}

public function testFlagIsRecorded(): void
{
$span = new Span();

$span->setFlag('feature', true);

$this->assertSame(['flag.evaluation.feature' => true], $span->getData());
}

public function testFlagLimitRecorded(): void
{
$span = new Span();

$expectedFlags = [
'flag.evaluation.should-not-be-discarded' => true,
];

$span->setFlag('should-not-be-discarded', true);

foreach (range(1, Span::MAX_FLAGS - 1) as $i) {
$span->setFlag("feature{$i}", true);

$expectedFlags["flag.evaluation.feature{$i}"] = true;
}

$span->setFlag('should-be-discarded', true);

$this->assertSame($expectedFlags, $span->getData());
}
}