Skip to content

Commit 8c35756

Browse files
committed
feat(scopes): add new scopes
1 parent 68bda49 commit 8c35756

2 files changed

Lines changed: 290 additions & 0 deletions

File tree

src/State/Scope.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,69 @@ public function __construct(?PropagationContext $propagationContext = null)
112112
$this->propagationContext = $propagationContext ?? PropagationContext::fromDefaults();
113113
}
114114

115+
/**
116+
* Merges the process-global scope underneath the current isolation scope.
117+
*
118+
* The returned scope is transient and should be used for one event capture.
119+
*
120+
* @internal
121+
*/
122+
public static function mergeScopes(self $globalScope, self $isolationScope): self
123+
{
124+
$mergedScope = clone $isolationScope;
125+
126+
$mergedScope->tags = array_merge($globalScope->tags, $isolationScope->tags);
127+
$mergedScope->extra = array_merge($globalScope->extra, $isolationScope->extra);
128+
$mergedScope->contexts = array_merge($globalScope->contexts, $isolationScope->contexts);
129+
130+
if ($globalScope->user !== null && $isolationScope->user !== null) {
131+
$mergedScope->user = (clone $globalScope->user)->merge($isolationScope->user);
132+
} elseif ($globalScope->user !== null) {
133+
$mergedScope->user = clone $globalScope->user;
134+
}
135+
136+
$mergedScope->level = $isolationScope->level ?? $globalScope->level;
137+
$mergedScope->fingerprint = array_merge($globalScope->fingerprint, $isolationScope->fingerprint);
138+
$mergedScope->breadcrumbs = \array_slice(array_merge($globalScope->breadcrumbs, $isolationScope->breadcrumbs), -100);
139+
$mergedScope->flags = self::mergeFlags($globalScope->flags, $isolationScope->flags);
140+
$mergedScope->attachments = array_merge($globalScope->attachments, $isolationScope->attachments);
141+
$mergedScope->eventProcessors = array_merge($globalScope->eventProcessors, $isolationScope->eventProcessors);
142+
143+
return $mergedScope;
144+
}
145+
146+
/**
147+
* @param array<int, array<string, bool>> $globalFlags
148+
* @param array<int, array<string, bool>> $isolationFlags
149+
*
150+
* @return array<int, array<string, bool>>
151+
*/
152+
private static function mergeFlags(array $globalFlags, array $isolationFlags): array
153+
{
154+
$flagsByKey = [];
155+
156+
foreach (array_merge($globalFlags, $isolationFlags) as $flag) {
157+
$flagKey = key($flag);
158+
159+
if ($flagKey === null) {
160+
continue;
161+
}
162+
163+
unset($flagsByKey[$flagKey]);
164+
$flagsByKey[$flagKey] = (bool) current($flag);
165+
}
166+
167+
$flagsByKey = \array_slice($flagsByKey, -self::MAX_FLAGS, self::MAX_FLAGS, true);
168+
169+
$flags = [];
170+
171+
foreach ($flagsByKey as $flagKey => $flagResult) {
172+
$flags[] = [$flagKey => $flagResult];
173+
}
174+
175+
return $flags;
176+
}
177+
115178
/**
116179
* Sets a new tag in the tags context.
117180
*

tests/State/ScopeTest.php

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,233 @@ public function testApplyToEvent(): void
531531
$this->assertSame('566e3688a61d4bc888951642d6f14a19', $dynamicSamplingContext->get('trace_id'));
532532
}
533533

534+
public function testMergeScopesAppliesGlobalScopeUnderIsolationScope(): void
535+
{
536+
$globalBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'global');
537+
$isolationBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation');
538+
$globalAttachment = Attachment::fromBytes('global.txt', 'global');
539+
$isolationAttachment = Attachment::fromBytes('isolation.txt', 'isolation');
540+
541+
$globalUser = UserDataBag::createFromUserIdentifier('global-user');
542+
$globalUser->setMetadata('shared', 'global');
543+
$globalUser->setMetadata('global', true);
544+
545+
$globalScope = new Scope();
546+
$globalScope->setTag('shared', 'global');
547+
$globalScope->setTag('global', 'tag');
548+
$globalScope->setExtra('shared', 'global');
549+
$globalScope->setExtra('global', true);
550+
$globalScope->setContext('shared_context', ['value' => 'global']);
551+
$globalScope->setContext('global_context', ['value' => 'global']);
552+
$globalScope->setUser($globalUser);
553+
$globalScope->setLevel(Severity::error());
554+
$globalScope->setFingerprint(['global-fingerprint']);
555+
$globalScope->addBreadcrumb($globalBreadcrumb);
556+
$globalScope->addFeatureFlag('shared-flag', false);
557+
$globalScope->addFeatureFlag('global-flag', true);
558+
$globalScope->addAttachment($globalAttachment);
559+
560+
$isolationUser = UserDataBag::createFromUserIdentifier('isolation-user');
561+
$isolationUser->setMetadata('shared', 'isolation');
562+
$isolationUser->setMetadata('isolation', true);
563+
564+
$isolationScope = new Scope();
565+
$isolationScope->setTag('shared', 'isolation');
566+
$isolationScope->setTag('isolation', 'tag');
567+
$isolationScope->setExtra('shared', 'isolation');
568+
$isolationScope->setExtra('isolation', true);
569+
$isolationScope->setContext('shared_context', ['value' => 'isolation']);
570+
$isolationScope->setContext('isolation_context', ['value' => 'isolation']);
571+
$isolationScope->setUser($isolationUser);
572+
$isolationScope->setLevel(Severity::warning());
573+
$isolationScope->setFingerprint(['isolation-fingerprint']);
574+
$isolationScope->addBreadcrumb($isolationBreadcrumb);
575+
$isolationScope->addFeatureFlag('shared-flag', true);
576+
$isolationScope->addFeatureFlag('isolation-flag', false);
577+
$isolationScope->addAttachment($isolationAttachment);
578+
579+
$eventUser = UserDataBag::createFromUserIdentifier('event-user');
580+
$eventUser->setMetadata('shared', 'event');
581+
$eventUser->setMetadata('event', true);
582+
583+
$event = Event::createEvent();
584+
$event->setTag('shared', 'event');
585+
$event->setTag('event', 'tag');
586+
$event->setExtra(['shared' => 'event', 'event' => true]);
587+
$event->setContext('shared_context', ['value' => 'event']);
588+
$event->setUser($eventUser);
589+
$event->setFingerprint(['event-fingerprint']);
590+
591+
$event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent($event);
592+
593+
$this->assertNotNull($event);
594+
$this->assertTrue($event->getLevel()->isEqualTo(Severity::warning()));
595+
$this->assertSame(['event-fingerprint', 'global-fingerprint', 'isolation-fingerprint'], $event->getFingerprint());
596+
$this->assertSame([
597+
'shared' => 'event',
598+
'global' => 'tag',
599+
'isolation' => 'tag',
600+
'event' => 'tag',
601+
], $event->getTags());
602+
$this->assertSame([
603+
'shared' => 'event',
604+
'global' => true,
605+
'isolation' => true,
606+
'event' => true,
607+
], $event->getExtra());
608+
$this->assertSame(['value' => 'event'], $event->getContexts()['shared_context']);
609+
$this->assertSame(['value' => 'global'], $event->getContexts()['global_context']);
610+
$this->assertSame(['value' => 'isolation'], $event->getContexts()['isolation_context']);
611+
$this->assertSame([
612+
'values' => [
613+
[
614+
'flag' => 'global-flag',
615+
'result' => true,
616+
],
617+
[
618+
'flag' => 'shared-flag',
619+
'result' => true,
620+
],
621+
[
622+
'flag' => 'isolation-flag',
623+
'result' => false,
624+
],
625+
],
626+
], $event->getContexts()['flags']);
627+
$this->assertSame([$globalBreadcrumb, $isolationBreadcrumb], $event->getBreadcrumbs());
628+
$this->assertSame([$globalAttachment, $isolationAttachment], $event->getAttachments());
629+
630+
$user = $event->getUser();
631+
$this->assertNotNull($user);
632+
$this->assertSame('event-user', $user->getId());
633+
$this->assertSame([
634+
'shared' => 'event',
635+
'global' => true,
636+
'isolation' => true,
637+
'event' => true,
638+
], $user->getMetadata());
639+
}
640+
641+
public function testMergeScopesUsesGlobalLevelWhenIsolationLevelIsUnset(): void
642+
{
643+
$globalScope = new Scope();
644+
$globalScope->setLevel(Severity::error());
645+
646+
$event = Scope::mergeScopes($globalScope, new Scope())->applyToEvent(Event::createEvent());
647+
648+
$this->assertNotNull($event);
649+
$this->assertTrue($event->getLevel()->isEqualTo(Severity::error()));
650+
}
651+
652+
public function testMergeScopesCapsBreadcrumbsAndFlags(): void
653+
{
654+
$globalScope = new Scope();
655+
$globalBreadcrumbs = [];
656+
657+
foreach (range(1, 100) as $i) {
658+
$breadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, "global{$i}");
659+
$globalBreadcrumbs[] = $breadcrumb;
660+
$globalScope->addBreadcrumb($breadcrumb);
661+
$globalScope->addFeatureFlag("feature{$i}", true);
662+
}
663+
664+
$isolationBreadcrumb = new Breadcrumb(Breadcrumb::LEVEL_INFO, Breadcrumb::TYPE_DEFAULT, 'isolation');
665+
$isolationScope = new Scope();
666+
$isolationScope->addBreadcrumb($isolationBreadcrumb);
667+
$isolationScope->addFeatureFlag('feature50', false);
668+
$isolationScope->addFeatureFlag('feature101', true);
669+
670+
$event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent(Event::createEvent());
671+
672+
$this->assertNotNull($event);
673+
$this->assertCount(100, $event->getBreadcrumbs());
674+
$this->assertSame($globalBreadcrumbs[1], $event->getBreadcrumbs()[0]);
675+
$this->assertSame($isolationBreadcrumb, $event->getBreadcrumbs()[99]);
676+
677+
$flags = $event->getContexts()['flags']['values'];
678+
$this->assertCount(Scope::MAX_FLAGS, $flags);
679+
$this->assertSame([
680+
'flag' => 'feature2',
681+
'result' => true,
682+
], $flags[0]);
683+
$this->assertSame([
684+
'flag' => 'feature50',
685+
'result' => false,
686+
], $flags[98]);
687+
$this->assertSame([
688+
'flag' => 'feature101',
689+
'result' => true,
690+
], $flags[99]);
691+
$this->assertFalse(\in_array('feature1', array_column($flags, 'flag'), true));
692+
}
693+
694+
public function testMergeScopesKeepsTraceStateFromIsolationScope(): void
695+
{
696+
$globalPropagationContext = PropagationContext::fromDefaults();
697+
$globalPropagationContext->setTraceId(new TraceId('11111111111111111111111111111111'));
698+
$globalPropagationContext->setSpanId(new SpanId('1111111111111111'));
699+
700+
$globalSpan = new Span();
701+
$globalSpan->setTraceId(new TraceId('22222222222222222222222222222222'));
702+
$globalSpan->setSpanId(new SpanId('2222222222222222'));
703+
704+
$globalScope = new Scope($globalPropagationContext);
705+
$globalScope->setSpan($globalSpan);
706+
707+
$isolationPropagationContext = PropagationContext::fromDefaults();
708+
$isolationPropagationContext->setTraceId(new TraceId('33333333333333333333333333333333'));
709+
$isolationPropagationContext->setSpanId(new SpanId('3333333333333333'));
710+
711+
$isolationScope = new Scope($isolationPropagationContext);
712+
713+
$mergedScope = Scope::mergeScopes($globalScope, $isolationScope);
714+
715+
$this->assertNull($mergedScope->getSpan());
716+
$this->assertNotSame($isolationScope->getPropagationContext(), $mergedScope->getPropagationContext());
717+
$this->assertSame([
718+
'trace_id' => '33333333333333333333333333333333',
719+
'span_id' => '3333333333333333',
720+
], $mergedScope->getTraceContext());
721+
722+
$event = $mergedScope->applyToEvent(Event::createEvent());
723+
724+
$this->assertNotNull($event);
725+
$this->assertSame([
726+
'trace_id' => '33333333333333333333333333333333',
727+
'span_id' => '3333333333333333',
728+
], $event->getContexts()['trace']);
729+
}
730+
731+
public function testMergeScopesKeepsProcessorOrder(): void
732+
{
733+
$calls = [];
734+
735+
Scope::addGlobalEventProcessor(static function (Event $event) use (&$calls): ?Event {
736+
$calls[] = 'static';
737+
738+
return $event;
739+
});
740+
741+
$globalScope = new Scope();
742+
$globalScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event {
743+
$calls[] = 'global';
744+
745+
return $event;
746+
});
747+
748+
$isolationScope = new Scope();
749+
$isolationScope->addEventProcessor(static function (Event $event) use (&$calls): ?Event {
750+
$calls[] = 'isolation';
751+
752+
return $event;
753+
});
754+
755+
$event = Scope::mergeScopes($globalScope, $isolationScope)->applyToEvent(Event::createEvent());
756+
757+
$this->assertNotNull($event);
758+
$this->assertSame(['static', 'global', 'isolation'], $calls);
759+
}
760+
534761
/**
535762
* @dataProvider eventWithLogCountProvider
536763
*/

0 commit comments

Comments
 (0)