diff --git a/analysis-baseline.toml b/analysis-baseline.toml index d87c756bb..44e216a21 100644 --- a/analysis-baseline.toml +++ b/analysis-baseline.toml @@ -1704,6 +1704,12 @@ code = "invalid-return-statement" message = 'Invalid return type for function `Sentry\Tracing\Transaction::getDynamicSamplingContext`: expected `Sentry\Tracing\DynamicSamplingContext`, but found `Sentry\Tracing\DynamicSamplingContext|null`.' count = 1 +[[issues]] +file = "src/Tracing/Transaction.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 1 + [[issues]] file = "src/Tracing/Transaction.php" code = "nullable-return-statement" diff --git a/src/Profiling/ProfileChunk.php b/src/Profiling/ProfileChunk.php index 2779827f6..4257b8676 100644 --- a/src/Profiling/ProfileChunk.php +++ b/src/Profiling/ProfileChunk.php @@ -57,12 +57,12 @@ final class ProfileChunk /** * @var string */ - private const THREAD_ID = '0'; + public const THREAD_ID = '0'; /** * @var string */ - private const THREAD_NAME = 'main'; + public const THREAD_NAME = 'main'; /** * @var int we need at least 2 samples otherwise the event is dropped diff --git a/src/Tracing/Span.php b/src/Tracing/Span.php index 346b53b79..0956e2062 100644 --- a/src/Tracing/Span.php +++ b/src/Tracing/Span.php @@ -5,6 +5,8 @@ namespace Sentry\Tracing; use Sentry\EventId; +use Sentry\Profiling\ProfileChunk; +use Sentry\Profiling\Profiles; use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Unit; @@ -115,6 +117,15 @@ class Span */ public function __construct(?SpanContext $context = null) { + $profilerId = Profiles::getInstance()->continuousProfiler()->getProfilerId(); + if ($profilerId !== null) { + $this->data = [ + 'thread.id' => ProfileChunk::THREAD_ID, + 'thread.name' => ProfileChunk::THREAD_NAME, + 'profiler_id' => $profilerId, + ]; + } + if ($context === null) { $this->traceId = TraceId::generate(); $this->spanId = SpanId::generate(); @@ -132,7 +143,7 @@ public function __construct(?SpanContext $context = null) $this->status = $context->getStatus(); $this->sampled = $context->getSampled(); $this->tags = $context->getTags(); - $this->data = $context->getData(); + $this->data = $context->getData() + $this->data; $this->endTimestamp = $context->getEndTimestamp(); $this->origin = $context->getOrigin(); } diff --git a/src/Tracing/Transaction.php b/src/Tracing/Transaction.php index 5e4d727d2..dd568e045 100644 --- a/src/Tracing/Transaction.php +++ b/src/Tracing/Transaction.php @@ -183,6 +183,12 @@ public function finish(?float $endTimestamp = null): ?EventId $event->setSdkMetadata('dynamic_sampling_context', $this->getDynamicSamplingContext()); $event->setSdkMetadata('transaction_metadata', $this->getMetadata()); + $profilerId = $this->data['profiler_id'] ?? null; + + if (\is_string($profilerId)) { + $event->setContext('profile', ['profiler_id' => $profilerId]); + } + if ($this->profiler !== null) { $profile = $this->profiler->getProfile(); if ($profile !== null) { diff --git a/tests/Tracing/TransactionTest.php b/tests/Tracing/TransactionTest.php index 76a2c3aab..d14f2c8c0 100644 --- a/tests/Tracing/TransactionTest.php +++ b/tests/Tracing/TransactionTest.php @@ -5,13 +5,17 @@ namespace Sentry\Tests\Tracing; use PHPUnit\Framework\TestCase; +use Sentry\Client; use Sentry\ClientInterface; use Sentry\Event; use Sentry\EventId; use Sentry\EventType; use Sentry\Options; +use Sentry\Profiling\Profiles; +use Sentry\SentrySdk; use Sentry\State\Hub; use Sentry\State\HubInterface; +use Sentry\Tests\StubTransport; use Sentry\Tests\TestUtil\ClockMock; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -75,6 +79,55 @@ public function testFinish(): void $this->assertSame($expectedEventId, $eventId); } + /** + * @requires extension excimer + */ + public function testSpansCreatedWhileProfilingAreLinkedToTheProfile(): void + { + StubTransport::$events = []; + + $hub = SentrySdk::init(); + $hub->bindClient(new Client(new Options([ + 'profile_session_sample_rate' => 1.0, + ]), StubTransport::getInstance())); + + $profiler = Profiles::getInstance()->continuousProfiler(); + $profiler->start(); + + $profilerId = $profiler->getProfilerId(); + + $transaction = new Transaction(TransactionContext::make()->setSampled(true), $hub); + $transaction->initSpanRecorder(); + + $profiledSpan = $transaction->startChild(new SpanContext()); + $profiledSpan->finish(); + + $profiler->stop(); + + $unprofiledSpan = $transaction->startChild(new SpanContext()); + $unprofiledSpan->finish(); + + $transaction->finish(); + + $transactionEvents = array_values(array_filter(StubTransport::$events, static function (Event $event): bool { + return $event->getType() === EventType::transaction(); + })); + + $this->assertCount(1, $transactionEvents); + + $event = $transactionEvents[0]; + $profileData = [ + 'thread.id' => '0', + 'thread.name' => 'main', + 'profiler_id' => $profilerId, + ]; + + $this->assertSame(['profiler_id' => $profilerId], $event->getContexts()['profile']); + $this->assertSame($profileData, $event->getContexts()['trace']['data']); + $this->assertSame($profileData, $profiledSpan->getData()); + $this->assertSame([], $unprofiledSpan->getData()); + } + public function testFinishDoesNothingIfSampledFlagIsNotTrue(): void { $hub = $this->createMock(HubInterface::class);